项目创建
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import { App, Directive } from 'vue'
|
||||
import hljs from 'highlight.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/**
|
||||
* 高亮代码
|
||||
* 插入行号、添加复制按钮、分片处理代码块,解决大数据量一次写入卡顿问题
|
||||
* 支持动态内容监听,确保所有代码块都能被正确处理
|
||||
*/
|
||||
|
||||
// 高亮代码
|
||||
function highlightCode(block: HTMLElement) {
|
||||
hljs.highlightElement(block)
|
||||
}
|
||||
|
||||
// 插入行号
|
||||
function insertLineNumbers(block: HTMLElement) {
|
||||
const lines = block.innerHTML.split('\n')
|
||||
const numberedLines = lines
|
||||
.map((line, index) => {
|
||||
return `<span class="line-number">${index + 1}</span> ${line}`
|
||||
})
|
||||
.join('\n')
|
||||
block.innerHTML = numberedLines
|
||||
}
|
||||
|
||||
// 添加复制按钮:调整 DOM 结构,将代码部分包裹在 .code-wrapper 内
|
||||
function addCopyButton(block: HTMLElement) {
|
||||
const copyButton = document.createElement('i')
|
||||
copyButton.className = 'copy-button iconfont-sys'
|
||||
copyButton.innerHTML = ''
|
||||
copyButton.onclick = () => {
|
||||
// 过滤掉行号,只复制代码内容
|
||||
const codeContent = block.innerText.replace(/^\d+\s+/gm, '')
|
||||
navigator.clipboard.writeText(codeContent).then(() => {
|
||||
ElMessage.success('复制成功')
|
||||
})
|
||||
}
|
||||
|
||||
const preElement = block.parentElement
|
||||
if (preElement) {
|
||||
let codeWrapper: HTMLElement
|
||||
// 如果代码块还没有被包裹,则创建包裹容器
|
||||
if (!block.parentElement.classList.contains('code-wrapper')) {
|
||||
codeWrapper = document.createElement('div')
|
||||
codeWrapper.className = 'code-wrapper'
|
||||
preElement.replaceChild(codeWrapper, block)
|
||||
codeWrapper.appendChild(block)
|
||||
} else {
|
||||
codeWrapper = block.parentElement
|
||||
}
|
||||
// 将复制按钮添加到 pre 元素(而非 codeWrapper 内),这样它不会随滚动条滚动
|
||||
preElement.appendChild(copyButton)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查代码块是否已经被处理过
|
||||
function isBlockProcessed(block: HTMLElement): boolean {
|
||||
return (
|
||||
block.hasAttribute('data-highlighted') ||
|
||||
!!block.querySelector('.line-number') ||
|
||||
!!block.parentElement?.querySelector('.copy-button')
|
||||
)
|
||||
}
|
||||
|
||||
// 标记代码块为已处理
|
||||
function markBlockAsProcessed(block: HTMLElement) {
|
||||
block.setAttribute('data-highlighted', 'true')
|
||||
}
|
||||
|
||||
// 处理单个代码块
|
||||
function processBlock(block: HTMLElement) {
|
||||
if (isBlockProcessed(block)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
highlightCode(block)
|
||||
insertLineNumbers(block)
|
||||
addCopyButton(block)
|
||||
markBlockAsProcessed(block)
|
||||
} catch (error) {
|
||||
console.warn('处理代码块时出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查找并处理所有代码块
|
||||
function processAllCodeBlocks(el: HTMLElement) {
|
||||
const blocks = Array.from(el.querySelectorAll<HTMLElement>('pre code'))
|
||||
const unprocessedBlocks = blocks.filter((block) => !isBlockProcessed(block))
|
||||
|
||||
if (unprocessedBlocks.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (unprocessedBlocks.length <= 10) {
|
||||
// 如果代码块数量少于等于10,直接处理所有代码块
|
||||
unprocessedBlocks.forEach((block) => processBlock(block))
|
||||
} else {
|
||||
// 定义每次处理的代码块数
|
||||
const batchSize = 10
|
||||
let currentIndex = 0
|
||||
|
||||
const processBatch = () => {
|
||||
const batch = unprocessedBlocks.slice(currentIndex, currentIndex + batchSize)
|
||||
|
||||
batch.forEach((block) => {
|
||||
processBlock(block)
|
||||
})
|
||||
|
||||
// 更新索引并继续处理下一批
|
||||
currentIndex += batchSize
|
||||
if (currentIndex < unprocessedBlocks.length) {
|
||||
// 使用 requestAnimationFrame 确保下一帧再处理
|
||||
requestAnimationFrame(processBatch)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始处理第一批代码块
|
||||
processBatch()
|
||||
}
|
||||
}
|
||||
|
||||
// 重试处理函数
|
||||
function retryProcessing(el: HTMLElement, maxRetries: number = 3, delay: number = 200) {
|
||||
let retryCount = 0
|
||||
|
||||
const tryProcess = () => {
|
||||
processAllCodeBlocks(el)
|
||||
|
||||
// 检查是否还有未处理的代码块
|
||||
const remainingBlocks = Array.from(el.querySelectorAll<HTMLElement>('pre code')).filter(
|
||||
(block) => !isBlockProcessed(block)
|
||||
)
|
||||
|
||||
if (remainingBlocks.length > 0 && retryCount < maxRetries) {
|
||||
retryCount++
|
||||
setTimeout(tryProcess, delay * retryCount) // 递增延迟
|
||||
}
|
||||
}
|
||||
|
||||
tryProcess()
|
||||
}
|
||||
|
||||
// 代码高亮、插入行号、复制按钮
|
||||
const highlightDirective: Directive<HTMLElement> = {
|
||||
mounted(el: HTMLElement) {
|
||||
// 立即尝试处理一次
|
||||
processAllCodeBlocks(el)
|
||||
|
||||
// 延迟处理,确保 v-html 内容已经渲染
|
||||
setTimeout(() => {
|
||||
retryProcessing(el)
|
||||
}, 100)
|
||||
|
||||
// 使用 MutationObserver 监听 DOM 变化
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let hasNewCodeBlocks = false
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as HTMLElement
|
||||
// 检查新添加的节点是否包含代码块
|
||||
if (element.tagName === 'PRE' || element.querySelector('pre code')) {
|
||||
hasNewCodeBlocks = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (hasNewCodeBlocks) {
|
||||
// 延迟处理新添加的代码块
|
||||
setTimeout(() => {
|
||||
processAllCodeBlocks(el)
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
|
||||
// 开始观察
|
||||
observer.observe(el, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
|
||||
// 将 observer 存储到元素上,以便在 unmounted 时清理
|
||||
;(el as any)._highlightObserver = observer
|
||||
},
|
||||
|
||||
updated(el: HTMLElement) {
|
||||
// 当组件更新时,重新处理代码块
|
||||
setTimeout(() => {
|
||||
processAllCodeBlocks(el)
|
||||
}, 50)
|
||||
},
|
||||
|
||||
unmounted(el: HTMLElement) {
|
||||
// 清理 MutationObserver
|
||||
const observer = (el as any)._highlightObserver
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
delete (el as any)._highlightObserver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setupHighlightDirective(app: App) {
|
||||
app.directive('highlight', highlightDirective)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { App } from 'vue'
|
||||
import { setupPermissionDirective } from './permission'
|
||||
import { setupHighlightDirective } from './highlight'
|
||||
import { setupRippleDirective } from './ripple'
|
||||
|
||||
export function setupGlobDirectives(app: App) {
|
||||
setupPermissionDirective(app) // 权限指令
|
||||
setupHighlightDirective(app) // 高亮指令
|
||||
setupRippleDirective(app) // 水波纹指令
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { router } from '@/router'
|
||||
import { App, Directive } from 'vue'
|
||||
|
||||
/**
|
||||
* 权限指令(后端控制模式可用)
|
||||
* 用法:
|
||||
* <el-button v-auth="'add'">按钮</el-button>
|
||||
*/
|
||||
const authDirective: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const authList = (router.currentRoute.value.meta.authList as Array<{ auth_mark: string }>) || []
|
||||
|
||||
const hasPermission = authList.some((item) => item.auth_mark === binding.value)
|
||||
|
||||
if (!hasPermission) {
|
||||
el.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setupPermissionDirective(app: App) {
|
||||
app.directive('auth', authDirective)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { App, Directive, DirectiveBinding } from 'vue'
|
||||
|
||||
/**
|
||||
* 水波纹指令
|
||||
* 用法:
|
||||
* <!-- 基础用法 -->
|
||||
* <el-button v-ripple>点击查看水波纹效果</el-button>
|
||||
*
|
||||
* <!-- 自定义颜色 -->
|
||||
* <el-button v-ripple="{ color: 'rgba(0, 0, 0, 0.2)' }">
|
||||
* 自定义水波纹颜色
|
||||
* </el-button>
|
||||
*/
|
||||
export interface RippleOptions {
|
||||
color?: string
|
||||
}
|
||||
|
||||
export const vRipple: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
// 获取指令的配置参数
|
||||
const options: RippleOptions = binding.value || {}
|
||||
|
||||
// 设置元素为相对定位,并隐藏溢出部分
|
||||
el.style.position = 'relative'
|
||||
el.style.overflow = 'hidden'
|
||||
|
||||
// 点击事件处理
|
||||
el.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const left = e.clientX - rect.left
|
||||
const top = e.clientY - rect.top
|
||||
|
||||
// 创建水波纹元素
|
||||
const ripple = document.createElement('div')
|
||||
const diameter = Math.max(el.clientWidth, el.clientHeight)
|
||||
const radius = diameter / 2
|
||||
|
||||
// 根据直径计算动画时间(直径越大,动画时间越长)
|
||||
const baseTime = 600 // 基础动画时间(毫秒)
|
||||
const scaleFactor = 0.5 // 缩放因子
|
||||
const animationDuration = baseTime + diameter * scaleFactor
|
||||
|
||||
// 设置水波纹的尺寸和位置
|
||||
ripple.style.width = ripple.style.height = `${diameter}px`
|
||||
ripple.style.left = `${left - radius}px`
|
||||
ripple.style.top = `${top - radius}px`
|
||||
ripple.style.position = 'absolute'
|
||||
ripple.style.borderRadius = '50%'
|
||||
ripple.style.pointerEvents = 'none'
|
||||
|
||||
// 判断是否为有色按钮(Element Plus 按钮类型)
|
||||
const buttonTypes = ['primary', 'info', 'warning', 'danger', 'success'].map(
|
||||
(type) => `el-button--${type}`
|
||||
)
|
||||
const isColoredButton = buttonTypes.some((type) => el.classList.contains(type))
|
||||
const defaultColor = isColoredButton
|
||||
? 'rgba(255, 255, 255, 0.35)' // 有色按钮使用白色水波纹
|
||||
: 'var(--el-color-primary-light-7)' // 默认按钮使用主题色水波纹
|
||||
|
||||
// 设置水波纹颜色、初始状态和过渡效果
|
||||
ripple.style.backgroundColor = options.color || defaultColor
|
||||
ripple.style.transform = 'scale(0)'
|
||||
ripple.style.transition = `transform ${animationDuration}ms cubic-bezier(0.3, 0, 0.2, 1), opacity ${animationDuration}ms cubic-bezier(0.3, 0, 0.5, 1)`
|
||||
ripple.style.zIndex = '1'
|
||||
|
||||
// 添加水波纹元素到DOM中
|
||||
el.appendChild(ripple)
|
||||
|
||||
// 触发动画
|
||||
requestAnimationFrame(() => {
|
||||
ripple.style.transform = 'scale(2)'
|
||||
ripple.style.opacity = '0'
|
||||
})
|
||||
|
||||
// 动画结束后移除水波纹元素
|
||||
setTimeout(() => {
|
||||
ripple.remove()
|
||||
}, animationDuration + 500) // 增加500ms缓冲时间
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function setupRippleDirective(app: App) {
|
||||
app.directive('ripple', vRipple)
|
||||
}
|
||||
Reference in New Issue
Block a user