This commit is contained in:
2025-10-17 10:31:13 +08:00
commit e6e86f2ce0
1043 changed files with 1031839 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
import { downloadByUrl } from '@/utils/filt';
export default function (jsEnhanceObj: Ref<any>) {
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
//文件大小格式化
const fileSizeFormatter = (fileSize) => {
const unitArr = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
fileSize = parseFloat(fileSize)
const index = Math.floor(Math.log(fileSize) / Math.log(1024))
fileSize = fileSize / Math.pow(1024, index)
//保留的小数位数
if (`${fileSize}`.indexOf('.') != -1) fileSize = fileSize.toFixed(2)
return fileSize + ' ' + unitArr[index]
}
//校验文件类型
const verifyFileType = (fileName) => {
const imgExp = /\.(gif|jpg|jpeg|png|webp|svg|GIF|JPG|JPEG|PNG|WEBP|SVG)/
const videoExp = /\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|ogg|mp4)/
const audioExp = /\.(mp3|wav|MP3|WAV)/
if (imgExp.test(fileName)) return 'image/*'
if (videoExp.test(fileName)) return 'video/*'
if (audioExp.test(fileName)) return 'audio/*'
return false
}
const uploadBefore = async (file, done, loading, column) => {
let bool = false
if (column.controlType == 'image') {
if (column.accept == 'image/*' && verifyFileType(file.name) == column.accept) bool = true
else if (column.accept) {
const accept = column.accept instanceof Array ? column.accept : column.accept.split(',')
if (accept.includes(file.type)) bool = true
} else bool = true
}
if (column.controlType == 'file') {
if (column.accept) {
const nameList = file.name.split('.')
const suffix = `.${nameList[nameList.length - 1]}`
const accept = column.accept instanceof Array ? column.accept : column.accept.split(',')
accept.forEach(type => {
if (['image/*', 'video/*', 'audio/*'].includes(type) && verifyFileType(file.name) == type) bool = true
})
if (accept.includes(suffix) || accept.includes(file.type)) bool = true
} else bool = true
}
try {
if (column.verify) {
bool = await column.verify(file).then(() => true).catch(() => false)
}
} catch (error) { }
if (!bool) {
message.info(`请上传正确的${column.label}格式`)
loading()
return
}
try {
if (jsEnhanceObj.value.beforeUpload) {
const isUpload = await jsEnhanceObj.value.beforeUpload(file)
if (!isUpload) {
loading()
return
}
}
} catch (error) {
console.warn(`'js增强【beforeUpload】方法执行异常请检查'
${error}`)
}
done()
}
const uploadExceed = (limit, files, fileList, column) => {
message.info(`${column.label} 最大可上传 ${limit}${column.controlType == 'image' ? '张' : '件'}`)
}
const uploadSized = (fileSize, files, fileList, column) => {
fileSize = fileSizeFormatter(fileSize)
message.info(`${column.label} 上传大小不可超过 ${fileSize}`)
}
const uploadPreview = (file, column, done) => {
if (column.controlType == 'image') return done()
const bool = verifyFileType(file.url)
if (bool) done()
else downloadByUrl({ url: file.url })
}
return { uploadBefore, uploadExceed, uploadSized, uploadPreview }
}

View File

@@ -0,0 +1,28 @@
import { useClipboard } from '@vueuse/core'
export default function () {
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const copy = async (text: string) => {
if (navigator.clipboard) {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
message.error(t('common.copyError'))
return
}
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
message.success(t('common.copySuccess'));
}
}
return { copyText: copy }
}

View File

@@ -0,0 +1,26 @@
import { useWindowSize } from '@vueuse/core'
export const useCrudHeight = (crudRef) => {
const windowSize = useWindowSize()
const crudHeightTimer = ref<any>(null)
const initTableLayout = () => {
if (crudHeightTimer.value) clearTimeout(crudHeightTimer.value)
crudHeightTimer.value = setTimeout(() => {
if (crudRef instanceof Array) {
crudRef.forEach(itemRef => {
if (itemRef.value) itemRef.value.getTableHeight()
})
} else if (crudRef.value) crudRef.value.getTableHeight()
}, 100)
}
watch(
() => windowSize.height.value,
() => {
initTableLayout()
}
)
return { initTableLayout, windowSize }
}

View File

@@ -0,0 +1,24 @@
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
export const useCrudPermi = () => {
const { wsCache } = useCache()
const all_permission = '*:*'
const permissions = wsCache.get(CACHE_KEY.USER).permissions
const crudBtnObj = {
query: 'viewBtn',
create: 'addBtn',
update: 'editBtn',
delete: 'delBtn',
}
const getCurrPermi = (permiArr: string[]) => {
const crudPermission = {}
permiArr.forEach(permiKey => {
for (const key in crudBtnObj) {
crudPermission[crudBtnObj[key]] = permiKey === all_permission || permissions[`${permiKey}:${key}`]
}
})
return crudPermission
}
return { getCurrPermi }
}

View File

@@ -0,0 +1,78 @@
export default function () {
const onMove = (e) => {
const type = e.draggedContext.element.type
const toClassName = e.to.className.split(' ')
// console.log(type, toClassName)
if (
type == 'layoutGroup' &&
['layout-group__body', 'layout-table__body'].includes(toClassName[0])
) {
//禁止 group 拖拽进 group\table
return false
}
if (type == 'layoutTable' && ['layout-table__body'].includes(toClassName[0])) {
//禁止 table 拖拽进 table
return false
}
if (
type == 'layoutTabs' &&
['layout-tabs__body', 'layout-table__body'].includes(toClassName[0])
) {
//禁止 tabs 拖拽进 tabs/table
return false
}
if (type == 'layoutTabs' && ['tabs-layout-group__body'].includes(toClassName[3])) {
//禁止 tabs 拖拽进 tabs内的group
return false
}
if (type == 'comboBox' && ['layout-table__body'].includes(toClassName[0])) {
//禁止 comboBox 拖拽进 table
return false
}
if (
['ueditor', 'buttonList', 'title'].includes(type) &&
['layout-table__body'].includes(toClassName[0])
) {
//禁止 富文本、按钮组、文本 拖拽进 table
return false
}
// 限制组合框可拖拽控件
if (['combo-box__body'].includes(toClassName[0])) {
if (['input', 'select', 'date', 'time'].includes(e.draggedContext.element.controlType)) {
if (['textarea', 'radio', 'checkbox', 'switch'].includes(type)) return false
} else {
if (!['buttonList'].includes(type)) return false
}
if (type == 'comboBox') return false
}
return true
}
const handleDragPosition = (newIndex, columnData) => {
let isGroup = false
if (columnData[newIndex]) isGroup = columnData[newIndex].type == 'layoutGroup'
let repIndex: number | undefined = undefined
columnData.forEach((item, index) => {
if (repIndex === undefined && item.type == 'layoutGroup') {
if (isGroup && index > newIndex) repIndex = index != 0 ? index - 1 : index
else if (!isGroup && index <= newIndex) repIndex = index
}
})
if (isGroup && repIndex !== newIndex) {
const column = columnData.splice(newIndex, 1)
if (repIndex === undefined) repIndex = columnData.length
if (column[0]) columnData.splice(repIndex, 0, column[0])
} else if (!isGroup && repIndex !== undefined && repIndex < newIndex) {
const column = columnData.splice(newIndex, 1)
if (column[0]) columnData.splice(repIndex, 0, column[0])
}
}
return {
onMove,
handleDragPosition
}
}

View File

@@ -0,0 +1,117 @@
import { listToTree, findNode, treeMap } from '@/utils/tree'
import { cloneDeep } from 'lodash-es'
export const useGroup = (treeRef, DataApi, resetChange, isView?, isOneLevel?) => {
const message = useMessage() // 消息弹窗
const treeForm = ref<any>({})
const treeOption = ref({
nodeKey: 'id',
defaultExpandAll: true,
filterText: '输入名称进行过滤',
props: { label: 'name', value: 'id' },
formOption: {
labelWidth: 100,
column: {
pid: { label: '上级分组', type: 'tree', value: 0, disabled: isOneLevel, dicData: [], filterable: true, defaultExpandAll: true, props: { label: 'name', value: 'id' } },
name: { label: '分组名称', rules: [{ required: true, message: '请输入 分组名称', trigger: "blur" }] }
}
}
})
if (isOneLevel) treeOption.value.formOption['filterParams'] = ['pid']
const treeData = ref<any>([])
const groupValue = ref<string | number>(0)
const currMenuNodeData = ref<any>({})
if (isView) {
treeOption.value['addBtn'] = false
treeOption.value['editBtn'] = false
treeOption.value['delBtn'] = false
treeOption.value['menu'] = false
}
const treePermission = (key, data) => {
if (key != 'addBtn' && data.id === 0) return false
return true
}
const treeNodeContextmenu = (data) => {
currMenuNodeData.value = data
}
const treeBeforeOpen = (done, type) => {
setTimeout(() => {
const treeList = cloneDeep(treeData.value)
if (type == 'edit') {
const disabledArr = [treeForm.value.id]
treeMap(treeList, {
children: 'children',
conversion: (item) => {
if (item.id == disabledArr[0]) item.disabled = true
if (disabledArr.includes(item.pid)) {
item.disabled = true
disabledArr.push(item.id)
}
return item
}
})
treeForm.value.oldPid = treeForm.value.pid
} else {
treeForm.value.pid = currMenuNodeData.value.id
}
treeOption.value.formOption.column.pid.dicData = treeList
if (isOneLevel) treeForm.value.pid = 0
}, 30)
done()
}
const treeNodeClick = (data) => {
if (data.id == groupValue.value) {
treeRef.value.setCurrentKey(null)
groupValue.value = ''
} else groupValue.value = data.id
resetChange()
}
const getTreeData = async () => {
const data = await DataApi.getGroupData({})
treeData.value = [{ name: '全部', id: 0, children: listToTree(data) }]
}
const treeUpdate = (node, data, done, loading) => {
DataApi.updateGroupData(data)
.then(() => {
if (data.oldPid != data.pid) {
const oldPNode = findNode(treeData.value, (node) => node.id == data.oldPid)
oldPNode.children = oldPNode.children.filter((item) => item.id != data.id)
const pNode = findNode(treeData.value, (node) => node.id == data.pid)
delete data.oldPid
if (pNode.children) pNode.children.push(data)
else pNode.children = [data]
}
done()
})
.catch(() => loading())
}
const treeSave = async (node, data, done, loading) => {
treeForm.value['children'] = []
await DataApi.saveGroupData(data)
.then((res) => {
treeForm.value.id = res
done()
setTimeout(() => {
if (treeData.value.length > 1) {
const currData = treeData.value.splice(1, 1)
const pNode = findNode(treeData.value, (node) => node.id == currData[0].pid)
if (pNode) pNode.children.push(currData[0])
}
}, 0)
})
.catch(() => loading())
}
const treeDel = async (node, done) => {
await message.delConfirm()
await DataApi.deleteGroupData([node.data.id])
done()
}
return { treeForm, treeOption, treeData, groupValue, treePermission, treeNodeContextmenu, treeBeforeOpen, treeNodeClick, getTreeData, treeUpdate, treeSave, treeDel }
}

View File

@@ -0,0 +1,57 @@
export default function () {
interface MEDialog {
value: boolean
title?: string
params?: object
otherParams?: object
handleClose?: any
}
interface MEData {
value: string
language?: string
editorOption?: object
params?: object
setFormValue?: (value: string) => void
}
const MEDialog = ref<MEDialog>({ value: false })
const MEData = ref<MEData>({ value: '' })
const openMEDialog = (column, tableForm) => {
const { prop, label, params } = column
const dialogParams = {}
const meParams = {}
let otherParams = {}
if (typeof params == 'object') {
for (const key in params) {
if (['title', 'width', 'fullscreen', 'headerBtn', 'footerBtn', 'dialogParams'].includes(key)) dialogParams[key] = params[key]
else if (['language', 'editorOption', 'providerType', 'oldValue'].includes(key)) meParams[key] = params[key]
if (key == 'otherParams') otherParams = params[key]
}
}
dialogParams['handleClose'] = (done) => {
if (MEData.value.setFormValue) MEData.value.setFormValue(MEData.value.value)
if (params && params.handleClose) params.handleClose(done)
else done()
}
MEDialog.value = {
value: true,
params: {
destroyOnClose:true,
title: label,
...dialogParams,
},
otherParams: Object.keys(otherParams).length ? otherParams : false
}
MEData.value = {
value: prop ? tableForm[prop] : tableForm || '',
params: meParams,
setFormValue: (value: string) => {
if (tableForm && prop) tableForm[prop] = value
},
}
}
return {
MEDialog, MEData, openMEDialog
}
}

View File

@@ -0,0 +1,341 @@
import * as monaco from 'monaco-editor'
import { ref, nextTick, onBeforeUnmount } from 'vue'
//语言
import 'monaco-editor/esm/vs/basic-languages/scss/scss.contribution';
import 'monaco-editor/esm/vs/basic-languages/java/java.contribution';
import * as MySql from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
import * as JavaScript from 'monaco-editor/esm/vs/basic-languages/javascript/javascript.js';
import * as Java from 'monaco-editor/esm/vs/basic-languages/java/java.js';
// 查找控件
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController';
import enhanceTip from '@/components/LowDesign/src/utils/enhanceTip';
import * as sqlFormatter from 'sql-formatter'
interface completions {
label: string
insertText: string
detail?: string
kind?: any
sortText?: string
}
const monacoProviderRef = ref<any>({})
const providerType = ref('')
let disposeArr: any[] = []
//清除提示
function clearProvider() {
for (const key in monacoProviderRef.value) monacoProviderRef.value[key]?.dispose()
}
function initLanguageProvider() {
clearProvider()
const sqlProvider: any = {
provideCompletionItems: (model, position) => {
const suggestions: completions[] = []
const { lineNumber, column } = position
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
})
const contents = textBeforePointer.trim().split(/\s+/)
const lastContents = contents[contents?.length - 1] // 获取最后一段非空字符串
if (lastContents) {
const sqlConfigKey = ['builtinFunctions', 'keywords', 'operators']
sqlConfigKey.forEach(key => {
MySql.language[key].forEach(sql => suggestions.push({ label: sql, insertText: sql, kind: monaco.languages.CompletionItemKind.Value }))
})
}
return { suggestions }
}
}
const javaProvider: any = {
provideCompletionItems: (model, position) => {
const suggestions: completions[] = []
const { lineNumber, column } = position
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
})
const contents = textBeforePointer.trim().split(/\s+/)
const lastContents = contents[contents?.length - 1] // 获取最后一段非空字符串
if (lastContents) {
const javaConfigKey = ['keywords', 'operators']
javaConfigKey.forEach(key => {
Java.language[key].forEach(java => suggestions.push({ label: java, insertText: java, kind: monaco.languages.CompletionItemKind.Value }))
})
}
return { suggestions }
}
}
let javaScriptDesign: any = []
const { tipList, triggerObj } = enhanceTip[providerType.value] || {}
if (tipList) javaScriptDesign = tipList
const javaScriptProvider: any = {
provideCompletionItems: (model, position) => {
if (!providerType.value) return { suggestions: [] }
const suggestions: completions[] = []
const { lineNumber, column } = position
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
})
const contents = textBeforePointer.trim().split(/\s+/)
const lastContents = contents[contents?.length - 1] // 获取最后一段非空字符串
const setTipFun = (bool) => {
javaScriptDesign.forEach((javaScript) => {
const item = { ...javaScript, sortText: bool ? '100' : '' }
suggestions.push(item)
})
}
let triggerKey = ''
if (triggerObj && lastContents) {
const lastLeng = lastContents.length
for (const key in triggerObj) {
if (triggerKey) break
const findIndex = lastContents.lastIndexOf(key)
if (findIndex != -1) {
const keyLeng = key.length
if (lastLeng - keyLeng == findIndex) triggerKey = key
}
}
}
if (triggerKey) {
javaScriptDesign = triggerObj[triggerKey]
setTipFun(true)
return { incomplete: false, suggestions }
}
javaScriptDesign = tipList || []
if (lastContents) {
const javaScriptConfigKey = ['operators']
javaScriptConfigKey.forEach(key => {
JavaScript.language[key].forEach(javaScript => suggestions.push({ label: javaScript, insertText: javaScript }))
})
setTipFun(false)
}
return { incomplete: false, suggestions }
},
triggerCharacters: ['.'],
}
monacoProviderRef.value.mysql = monaco.languages.registerCompletionItemProvider('mysql', sqlProvider);
monacoProviderRef.value.java = monaco.languages.registerCompletionItemProvider('java', javaProvider);
monacoProviderRef.value.javascript = monaco.languages.registerCompletionItemProvider('javascript', javaScriptProvider);
}
function addMySqlFormat() {
const sqlFormatDisposable = monaco.editor.addEditorAction({
id: 'format-sql',
label: '格式化 SQL',
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
run: (ed) => {
const original = ed.getValue();
const formatted = sqlFormatter.format(original, {
language: 'mysql',
params: ['#\\{[^}]+\\}']
});
ed.setValue(formatted)
}
})
const list = [...(sqlFormatDisposable['_toDispose'] || [])]
disposeArr.push(...list)
}
function emptyDispose() {
if (disposeArr.length) {
disposeArr.forEach(item => item.dispose && item.dispose())
disposeArr = []
}
}
export function useMonacoEditor(language: string = 'javascript') {
// 编辑器示例
let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
// 目标元素
const monacoEditorRef = ref<HTMLElement | null>(null)
// 创建实例
function createEditor(editorOption: monaco.editor.IStandaloneEditorConstructionOptions = {}, type = '') {
providerType.value = type
if (!monacoEditorRef.value) return
initLanguageProvider()
if (language)
monacoEditor = monaco.editor.create(monacoEditorRef.value, {
// 初始模型
model: monaco.editor.createModel('', language),
minimap: { enabled: true },
// 圆角
roundedSelection: true,
// 主题
theme: 'vs-dark',
multiCursorModifier: 'ctrlCmd',
// 滚动条
scrollbar: {
verticalScrollbarSize: 8,
horizontalScrollbarSize: 8
},
// 行号
lineNumbers: 'on',
// tab大小
tabSize: 2,
//字体大小
fontSize: 14,
// 控制编辑器在用户键入、粘贴、移动或缩进行时是否应自动调整缩进
autoIndent: 'advanced',
autoClosingBrackets: 'always',//补全括号
autoClosingQuotes: 'always', //补全冒号
// 自动布局
automaticLayout: true,
fixedOverflowWidgets: true,
...editorOption,
})
return monacoEditor
}
emptyDispose()
if (language == 'mysql') addMySqlFormat()
// 格式化
async function formatDoc() {
await monacoEditor?.getAction('editor.action.formatDocument')?.run()
}
// 数据更新
function updateVal(val: string) {
nextTick(() => {
monacoEditor?.setValue(val)
setTimeout(async () => {
await formatDoc()
}, 10)
})
}
// 配置更新
function updateOptions(opt: monaco.editor.IStandaloneEditorConstructionOptions, type = '') {
providerType.value = type
initLanguageProvider()
monacoEditor?.updateOptions(opt)
}
// 获取配置
function getOption(name: monaco.editor.EditorOption) {
return monacoEditor?.getOption(name)
}
// 获取实例
function getEditor() {
return monacoEditor
}
// 设置语言
function setLanguage(language, type = '') {
providerType.value = type
const text = monacoEditor?.getModel()?.getValue() || ''
const model = monaco.editor.createModel(text, language)
monacoEditor?.setModel(model)
emptyDispose()
if (language == 'mysql') addMySqlFormat()
}
onBeforeUnmount(() => {
if (monacoEditor) {
clearProvider()
emptyDispose()
monacoEditor.dispose()
}
})
return {
monacoEditorRef,
createEditor,
getEditor,
setLanguage,
updateVal,
updateOptions,
getOption,
formatDoc,
}
}
export function useDiffEditor(language: string = 'javascript', newValue, oldValue) {
// 编辑器示例
let diffEditor: monaco.editor.IStandaloneDiffEditor | null = null
let originalModel: any = null
let modifiedModel: any = null
// 目标元素
const deffEditorRef = ref<HTMLElement | null>(null)
// 创建实例
function createDeffEditor(editorOption: monaco.editor.IStandaloneDiffEditorConstructionOptions = {}, type = '') {
providerType.value = type
if (!deffEditorRef.value) return
initLanguageProvider()
diffEditor = monaco.editor.createDiffEditor(deffEditorRef.value, {
fontSize: 14, // 字体大小
theme: 'vs-dark', //主题
readOnly: false, // 是否只读
overviewRulerBorder: false, // 滚动是否有边框
cursorSmoothCaretAnimation: 'off', // 控制光标平滑动画的开启与关闭。当开启时,光标移动会有平滑的动画效果。
mouseWheelZoom: true, //设置是否开启鼠标滚轮缩放功能
folding: true, //控制是否开启代码折叠功能
automaticLayout: true, // 控制编辑器是否自动调整布局以适应容器大小的变化
// 是否启用预览图
minimap: { enabled: true },
// 滚动条
scrollbar: {
verticalScrollbarSize: 8,
horizontalScrollbarSize: 8
},
wordWrap: "off", // 关闭自动换行
scrollBeyondLastLine: false,
roundedSelection: true, // 右侧不显示编辑器预览框
originalEditable: false, // 是否允许修改原始文本
...editorOption,
})
originalModel = monaco.editor.createModel(oldValue, language);
modifiedModel = monaco.editor.createModel(newValue, language);
diffEditor.setModel({ original: originalModel, modified: modifiedModel });
return { diffEditor, originalModel, modifiedModel }
}
//获取实例
function getEditor(type) {
if (type == 'diff') return diffEditor
if (type == 'original') return originalModel
if (type == 'modified') return modifiedModel
}
// 格式化
async function formatDoc() {
await originalModel?.getAction('editor.action.formatDocument')?.run()
await modifiedModel?.getAction('editor.action.formatDocument')?.run()
}
// 数据更新
function updateVal(val: string, type) {
nextTick(() => {
if (type == 'original') return originalModel?.setValue(val)
if (type == 'modified') return modifiedModel?.setValue(val)
setTimeout(async () => {
await formatDoc()
}, 10)
})
}
// 设置语言
function setLanguage(language, type = '') {
providerType.value = type
const originalText = originalModel?.getValue() || ''
const modifiedText = modifiedModel?.getValue() || ''
originalModel = monaco.editor.createModel(originalText, language)
modifiedModel = monaco.editor.createModel(modifiedText, language)
diffEditor?.setModel({ original: originalModel, modified: modifiedModel })
}
return {
deffEditorRef,
createDeffEditor,
getEditor,
updateVal,
setLanguage
}
}

View File

@@ -0,0 +1,60 @@
export interface ScrollToParams {
el: HTMLElement
to: number
position: string
duration?: number
callback?: () => void
}
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
t /= d / 2
if (t < 1) {
return (c / 2) * t * t + b
}
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const move = (el: HTMLElement, position: string, amount: number) => {
el[position] = amount
}
export function useScrollTo({
el,
position = 'scrollLeft',
to,
duration = 500,
callback
}: ScrollToParams) {
const isActiveRef = ref(false)
const start = el[position]
const change = to - start
const increment = 20
let currentTime = 0
function animateScroll() {
if (!unref(isActiveRef)) {
return
}
currentTime += increment
const val = easeInOutQuad(currentTime, start, change, duration)
move(el, position, val)
if (currentTime < duration && unref(isActiveRef)) {
requestAnimationFrame(animateScroll)
} else {
if (callback) {
callback()
}
}
}
function run() {
isActiveRef.value = true
animateScroll()
}
function stop() {
isActiveRef.value = false
}
return { start: run, stop }
}

View File

@@ -0,0 +1,4 @@
export function isValidPhoneNumber(phoneNumber: string): boolean {
const regex = /^1[3-9]\d{9}$/;
return regex.test(phoneNumber);
}

30
src/hooks/web/useCache.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* 配置浏览器本地存储的方式,可直接存储对象数组。
*/
import WebStorageCache from 'web-storage-cache'
type CacheType = 'localStorage' | 'sessionStorage'
export const CACHE_KEY = {
IS_DARK: 'isDark',
DARK_BEFORE_COLOR:'darkBeforeColor',
USER: 'user',
LANG: 'lang',
THEME: 'theme',
LAYOUT: 'layout',
LOW_REGION: 'lowRegion',
ROLE_ROUTERS: 'roleRouters',
DICT_CACHE: 'dictCache',
FULLSCREEN:'fullscreen'
}
export const useCache = (type: CacheType = 'localStorage') => {
const wsCache: WebStorageCache = new WebStorageCache({
storage: type
})
return {
wsCache
}
}

View File

@@ -0,0 +1,9 @@
import { ConfigGlobalTypes } from '@/types/configGlobal'
export const useConfigGlobal = () => {
const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
return {
configGlobal
}
}

View File

@@ -0,0 +1,18 @@
import variables from '@/styles/global.module.scss'
export const useDesign = () => {
const scssVariables = variables
/**
* @param scope 类名
* @returns 返回空间名-类名
*/
const getPrefixCls = (scope: string) => {
return `${scssVariables.namespace}-${scope}`
}
return {
variables: scssVariables,
getPrefixCls
}
}

22
src/hooks/web/useEmitt.ts Normal file
View File

@@ -0,0 +1,22 @@
import mitt from 'mitt'
interface Option {
name: string // 事件名称
callback: Fn // 回调
}
const emitter = mitt()
export const useEmitt = (option?: Option) => {
if (option) {
emitter.on(option.name, option.callback)
onBeforeUnmount(() => {
emitter.off(option.name)
})
}
return {
emitter
}
}

49
src/hooks/web/useGuide.ts Normal file
View File

@@ -0,0 +1,49 @@
import { Config, driver } from 'driver.js'
import 'driver.js/dist/driver.css'
import { useDesign } from '@/hooks/web/useDesign'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const { variables } = useDesign()
export const useGuide = (options?: Config) => {
const driverObj = driver(
options || {
showProgress: true,
nextBtnText: t('common.nextLabel'),
prevBtnText: t('common.prevLabel'),
doneBtnText: t('common.doneLabel'),
steps: [
{
element: `#${variables.namespace}-menu`,
popover: {
title: t('common.menu'),
description: t('common.menuDes'),
side: 'right'
}
},
{
element: `#${variables.namespace}-tool-header`,
popover: {
title: t('common.tool'),
description: t('common.toolDes'),
side: 'left'
}
},
{
element: `#${variables.namespace}-tags-view`,
popover: {
title: t('common.tagsView'),
description: t('common.tagsViewDes'),
side: 'bottom'
}
}
]
}
)
return {
...driverObj
}
}

56
src/hooks/web/useI18n.ts Normal file
View File

@@ -0,0 +1,56 @@
import { i18n } from '@/plugins/vueI18n'
import type { Composer } from 'vue-i18n'
type I18nGlobalTranslation = {
(key: string): string
(key: string, locale: string): string
(key: string, locale: string, list: unknown[]): string
(key: string, locale: string, named: Record<string, unknown>): string
(key: string, list: unknown[]): string
(key: string, named: Record<string, unknown>): string
}
type I18nTranslationRestParameters = [string, any]
const getKey = (namespace: string | undefined, key: string) => {
if (!namespace) {
return key
}
if (key.startsWith(namespace)) {
return key
}
return `${namespace}.${key}`
}
export const useI18n = (
namespace?: string
): {
t: I18nGlobalTranslation
mergeLocaleMessage: Composer['mergeLocaleMessage']
getLocaleMessage: Composer['getLocaleMessage']
} => {
const normalFn = {
t: (key: string) => {
return getKey(namespace, key)
},
} as Composer
if (!i18n) {
return normalFn
}
const { t, ...methods } = i18n.global
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) return ''
if (!key.includes('.') && !namespace) return key
//@ts-ignore
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
}
return {
...methods,
t: tFn
}
}
export const t = (key: string) => key

8
src/hooks/web/useIcon.ts Normal file
View File

@@ -0,0 +1,8 @@
import { h } from 'vue'
import type { VNode } from 'vue'
import { Icon } from '@/components/Icon'
import { IconTypes } from '@/types/icon'
export const useIcon = (props: IconTypes): VNode => {
return h(Icon, props)
}

View File

@@ -0,0 +1,35 @@
import { i18n } from '@/plugins/vueI18n'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
const setI18nLanguage = (locale: LocaleType) => {
const localeStore = useLocaleStoreWithOut()
if (i18n.mode === 'legacy') {
i18n.global.locale = locale
} else {
;(i18n.global.locale as any).value = locale
}
localeStore.setCurrentLocale({
lang: locale
})
setHtmlPageLang(locale)
}
export const useLocale = () => {
// Switching the language will change the locale of useI18n
// And submit to configuration modification
const changeLocale = async (locale: LocaleType) => {
const globalI18n = i18n.global
const langModule = await import(`../../locales/${locale}.ts`)
globalI18n.setLocaleMessage(locale, langModule.default)
setI18nLanguage(locale)
}
return {
changeLocale
}
}

View File

@@ -0,0 +1,97 @@
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { useI18n } from './useI18n'
export const useMessage = () => {
const { t } = useI18n()
return {
// 消息提示
info(content: string) {
ElMessage.info(content)
},
// 错误消息
error(content: string) {
ElMessage.error(content)
},
// 成功消息
success(content: string) {
ElMessage.success(content)
},
// 警告消息
warning(content: string) {
ElMessage.warning(content)
},
// 弹出提示
alert(content: string, tip?: string, config: object = {}) {
ElMessageBox.alert(content, tip ? tip : t('common.confirmTitle'), config)
},
// 错误提示
alertError(content: string) {
ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'error' })
},
// 成功提示
alertSuccess(content: string) {
ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'success' })
},
// 警告提示
alertWarning(content: string) {
ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'warning' })
},
// 通知提示
notify(content: string) {
ElNotification.info(content)
},
// 错误通知
notifyError(content: string) {
ElNotification.error(content)
},
// 成功通知
notifySuccess(content: string) {
ElNotification.success(content)
},
// 警告通知
notifyWarning(content: string) {
ElNotification.warning(content)
},
// 确认窗体
confirm(content: string, tip?: string, config: object = {}) {
return ElMessageBox.confirm(content, tip ? tip : t('common.confirmTitle'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning',
...config,
})
},
// 删除窗体
delConfirm(content?: string, tip?: string) {
return ElMessageBox.confirm(
content ? content : t('common.delMessage'),
tip ? tip : t('common.confirmTitle'),
{
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
)
},
// 导出窗体
exportConfirm(content?: string, tip?: string) {
return ElMessageBox.confirm(
content ? content : t('common.exportMessage'),
tip ? tip : t('common.confirmTitle'),
{
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
)
},
// 提交内容
prompt(content: string, tip: string, config: object = {}) {
return ElMessageBox.prompt(content, tip, {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning',
...config,
})
}
}
}

View File

@@ -0,0 +1,33 @@
import { useCssVar } from '@vueuse/core'
import type { NProgressOptions } from 'nprogress'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
export const useNProgress = () => {
NProgress.configure({ showSpinner: false } as NProgressOptions)
const initColor = async () => {
await nextTick()
const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
if (bar) {
bar.style.background = unref(primaryColor.value)
}
}
initColor()
const start = () => {
NProgress.start()
}
const done = () => {
NProgress.done()
}
return {
start,
done
}
}

View File

@@ -0,0 +1,21 @@
import { ref, onBeforeUnmount } from 'vue'
const useNetwork = () => {
const online = ref(true)
const updateNetwork = () => {
online.value = navigator.onLine
}
window.addEventListener('online', updateNetwork)
window.addEventListener('offline', updateNetwork)
onBeforeUnmount(() => {
window.removeEventListener('online', updateNetwork)
window.removeEventListener('offline', updateNetwork)
})
return { online }
}
export { useNetwork }

60
src/hooks/web/useNow.ts Normal file
View File

@@ -0,0 +1,60 @@
import dayjs from 'dayjs'
import { reactive, toRefs } from 'vue'
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
export const useNow = (immediate = true) => {
let timer: IntervalHandle
const state = reactive({
year: 0,
month: 0,
week: '',
day: 0,
hour: '',
minute: '',
second: 0,
meridiem: ''
})
const update = () => {
const now = dayjs()
const h = now.format('HH')
const m = now.format('mm')
const s = now.get('s')
state.year = now.get('y')
state.month = now.get('M') + 1
state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
state.day = now.get('date')
state.hour = h
state.minute = m
state.second = s
state.meridiem = now.format('A')
}
function start() {
update()
clearInterval(timer)
timer = setInterval(() => update(), 1000)
}
function stop() {
clearInterval(timer)
}
tryOnMounted(() => {
immediate && start()
})
tryOnUnmounted(() => {
stop()
})
return {
...toRefs(state),
start,
stop
}
}

View File

@@ -0,0 +1,20 @@
import { useAppStoreWithOut } from '@/store/modules/app'
export const usePageLoading = () => {
const loadStart = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(true)
}
const loadDone = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(false)
}
return {
loadStart,
loadDone
}
}

View File

@@ -0,0 +1,63 @@
import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
import { computed, nextTick, unref } from 'vue'
export const useTagsView = () => {
const tagsViewStore = useTagsViewStoreWithOut()
const { replace, currentRoute } = useRouter()
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
const closeAll = (callback?: Fn) => {
tagsViewStore.delAllViews()
callback?.()
}
const closeLeft = (callback?: Fn) => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
callback?.()
}
const closeRight = (callback?: Fn) => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
callback?.()
}
const closeOther = (callback?: Fn) => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
callback?.()
}
const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
if (view?.meta?.affix) return
tagsViewStore.delView(view || unref(currentRoute))
callback?.()
}
const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
tagsViewStore.delCachedView()
const { path, query } = view || unref(currentRoute)
await nextTick()
replace({
path: '/redirect' + path,
query: query
})
callback?.()
}
const setTitle = (title: string, path?: string) => {
tagsViewStore.setTitle(title, path)
}
return {
closeAll,
closeLeft,
closeRight,
closeOther,
closeCurrent,
refreshPage,
setTitle
}
}

View File

@@ -0,0 +1,49 @@
import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
const TIME_AGO_MESSAGE_MAP: {
'zh-CN': UseTimeAgoMessages
en: UseTimeAgoMessages
} = {
// @ts-ignore
'zh-CN': {
justNow: '刚刚',
past: (n) => (n.match(/\d/) ? `${n}` : n),
future: (n) => (n.match(/\d/) ? `${n}` : n),
month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`),
year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n}`),
day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n}`),
week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n}`),
hour: (n) => `${n} 小时`,
minute: (n) => `${n} 分钟`,
second: (n) => `${n}`
},
// @ts-ignore
en: {
justNow: 'just now',
past: (n) => (n.match(/\d/) ? `${n} ago` : n),
future: (n) => (n.match(/\d/) ? `in ${n}` : n),
month: (n, past) =>
n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`,
year: (n, past) =>
n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`,
day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`),
week: (n, past) =>
n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`,
hour: (n) => `${n} hour${n > 1 ? 's' : ''}`,
minute: (n) => `${n} minute${n > 1 ? 's' : ''}`,
second: (n) => `${n} second${n > 1 ? 's' : ''}`
}
}
export const useTimeAgo = (time: Date | number | string) => {
const localeStore = useLocaleStoreWithOut()
const currentLocale = computed(() => localeStore.getCurrentLocale)
const timeAgo = useTimeAgoCore(time, {
messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang]
})
return timeAgo
}

25
src/hooks/web/useTitle.ts Normal file
View File

@@ -0,0 +1,25 @@
import { watch, ref } from 'vue'
import { isString } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app'
export const useTitle = (newTitle?: string) => {
const { t } = useI18n()
const appStore = useAppStoreWithOut()
const title = ref(
newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle
)
watch(
title,
(n, o) => {
if (isString(n) && n !== o && document) {
document.title = n
}
},
{ immediate: true }
)
return title
}

View File

@@ -0,0 +1,60 @@
import { useI18n } from '@/hooks/web/useI18n'
import { FormItemRule } from 'element-plus'
const { t } = useI18n()
interface LengthRange {
min: number
max: number
message?: string
}
export const useValidator = () => {
const required = (message?: string): FormItemRule => {
return {
required: true,
message: message || t('common.required')
}
}
const lengthRange = (options: LengthRange): FormItemRule => {
const { min, max, message } = options
return {
min,
max,
message: message || t('common.lengthRange', { min, max })
}
}
const notSpace = (message?: string): FormItemRule => {
return {
validator: (_, val, callback) => {
if (val?.indexOf(' ') !== -1) {
callback(new Error(message || t('common.notSpace')))
} else {
callback()
}
}
}
}
const notSpecialCharacters = (message?: string): FormItemRule => {
return {
validator: (_, val, callback) => {
if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
callback(new Error(message || t('common.notSpecialCharacters')))
} else {
callback()
}
}
}
}
return {
required,
lengthRange,
notSpace,
notSpecialCharacters
}
}

View File

@@ -0,0 +1,55 @@
const domSymbol = Symbol('watermark-dom')
export function useWatermark(appendEl: HTMLElement | null = document.body) {
let func: Fn = () => {}
const id = domSymbol.toString()
const clear = () => {
const domId = document.getElementById(id)
if (domId) {
const el = appendEl
el && el.removeChild(domId)
}
window.removeEventListener('resize', func)
}
const createWatermark = (str: string) => {
clear()
const can = document.createElement('canvas')
can.width = 300
can.height = 240
const cans = can.getContext('2d')
if (cans) {
cans.rotate((-20 * Math.PI) / 120)
cans.font = '15px Vedana'
cans.fillStyle = 'rgba(0, 0, 0, 0.15)'
cans.textAlign = 'left'
cans.textBaseline = 'middle'
cans.fillText(str, can.width / 20, can.height)
}
const div = document.createElement('div')
div.id = id
div.style.pointerEvents = 'none'
div.style.top = '0px'
div.style.left = '0px'
div.style.position = 'absolute'
div.style.zIndex = '100000000'
div.style.width = document.documentElement.clientWidth + 'px'
div.style.height = document.documentElement.clientHeight + 'px'
div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat'
const el = appendEl
el && el.appendChild(div)
return id
}
function setWatermark(str: string) {
createWatermark(str)
func = () => {
createWatermark(str)
}
window.addEventListener('resize', func)
}
return { setWatermark, clear }
}