feat: 搭建后台管理框架

This commit is contained in:
Vicky committed 2026-10-13 13:58:05 +08:00
1 parent 6d28bab72b
commit 40546bfd01
238 files changed
+18160 -3837

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
import mitt from 'mitt'
import { onBeforeUnmount } from 'vue'
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
}
}
+62
View File
@@ -0,0 +1,62 @@
import { ref, unref } from 'vue'
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 }
}
+10
View File
@@ -0,0 +1,10 @@
import { ConfigGlobalTypes } from '@/components/ConfigGlobal'
import { inject } from 'vue'
export const useConfigGlobal = () => {
const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
return {
configGlobal
}
}
+158
View File
@@ -0,0 +1,158 @@
import { reactive } from 'vue'
import { eachTree, treeMap, filter } from '@/utils/tree'
import { FormSchema } from '@/components/Form'
import { TableColumn } from '@/components/Table'
import { DescriptionsSchema } from '@/components/Descriptions'
export type CrudSchema = Omit<TableColumn, 'children'> & {
search?: CrudSearchParams
table?: CrudTableParams
form?: CrudFormParams
detail?: CrudDescriptionsParams
children?: CrudSchema[]
}
interface CrudSearchParams extends Omit<FormSchema, 'field'> {
// 是否隐藏在查询项
hidden?: boolean
}
interface CrudTableParams extends Omit<TableColumn, 'field'> {
// 是否隐藏表头
hidden?: boolean
}
interface CrudFormParams extends Omit<FormSchema, 'field'> {
// 是否隐藏表单项
hidden?: boolean
}
interface CrudDescriptionsParams extends Omit<DescriptionsSchema, 'field'> {
// 是否隐藏表单项
hidden?: boolean
}
interface AllSchemas {
searchSchema: FormSchema[]
tableColumns: TableColumn[]
formSchema: FormSchema[]
detailSchema: DescriptionsSchema[]
}
// 过滤所有结构
export const useCrudSchemas = (
crudSchema: CrudSchema[]
): {
allSchemas: AllSchemas
} => {
// 所有结构数据
const allSchemas = reactive<AllSchemas>({
searchSchema: [],
tableColumns: [],
formSchema: [],
detailSchema: []
})
const searchSchema = filterSearchSchema(crudSchema)
// @ts-ignore
allSchemas.searchSchema = searchSchema || []
const tableColumns = filterTableSchema(crudSchema)
allSchemas.tableColumns = tableColumns || []
const formSchema = filterFormSchema(crudSchema)
allSchemas.formSchema = formSchema
const detailSchema = filterDescriptionsSchema(crudSchema)
allSchemas.detailSchema = detailSchema
return {
allSchemas
}
}
// 过滤 Search 结构
const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
const searchSchema: FormSchema[] = []
const length = crudSchema.length
for (let i = 0; i < length; i++) {
const schemaItem = crudSchema[i]
const searchSchemaItem = {
component: schemaItem?.search?.component || 'Input',
...schemaItem.search,
field: schemaItem.field,
label: schemaItem.label
}
searchSchema.push(searchSchemaItem)
}
return searchSchema
}
// 过滤 table 结构
const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
const tableColumns = treeMap<CrudSchema>(crudSchema, {
conversion: (schema: CrudSchema) => {
if (!schema?.table?.hidden) {
return {
...schema.table,
...schema
}
}
}
})
// 第一次过滤会有 undefined 所以需要二次过滤
return filter<TableColumn>(tableColumns as TableColumn[], (data) => {
if (data.children === void 0) {
delete data.children
}
return !!data.field
})
}
// 过滤 form 结构
const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
const formSchema: FormSchema[] = []
const length = crudSchema.length
for (let i = 0; i < length; i++) {
const formItem = crudSchema[i]
// 判断是否隐藏
const formSchemaItem = {
component: formItem?.form?.component || 'Input',
...formItem.form,
field: formItem.field,
label: formItem.label
}
formSchema.push(formSchemaItem)
}
return formSchema
}
// 过滤 descriptions 结构
const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => {
const descriptionsSchema: FormSchema[] = []
eachTree(crudSchema, (schemaItem: CrudSchema) => {
// 判断是否隐藏
if (!schemaItem?.detail?.hidden) {
const descriptionsSchemaItem = {
...schemaItem.detail,
field: schemaItem.field,
label: schemaItem.detail?.label || schemaItem.label
}
// 删除不必要的字段
delete descriptionsSchemaItem.hidden
descriptionsSchema.push(descriptionsSchemaItem)
}
})
return descriptionsSchema
}
+18
View File
@@ -0,0 +1,18 @@
import variables from '@/styles/variables.module.less'
export const useDesign = () => {
const lessVariables = variables
/**
* @param scope 类名
* @returns 返回空间名-类名
*/
const getPrefixCls = (scope: string) => {
return `${lessVariables.namespace}-${scope}`
}
return {
variables: lessVariables,
getPrefixCls
}
}
+130
View File
@@ -0,0 +1,130 @@
import type { Form, FormExpose } from '@/components/Form'
import type { ElForm, ElFormItem } from 'element-plus'
import { ref, unref, nextTick } from 'vue'
import { FormSchema, FormSetProps, FormProps } from '@/components/Form'
export const useForm = () => {
// From实例
const formRef = ref<typeof Form & FormExpose>()
// ElForm实例
const elFormRef = ref<ComponentRef<typeof ElForm>>()
/**
* @param ref Form实例
* @param elRef ElForm实例
*/
const register = (ref: typeof Form & FormExpose, elRef: ComponentRef<typeof ElForm>) => {
formRef.value = ref
elFormRef.value = elRef
}
const getForm = async () => {
await nextTick()
const form = unref(formRef)
if (!form) {
console.error('The form is not registered. Please use the register method to register')
}
return form
}
// 一些内置的方法
const methods = {
/**
* @description 设置form组件的props
* @param props form组件的props
*/
setProps: async (props: FormProps = {}) => {
const form = await getForm()
form?.setProps(props)
if (props.model) {
form?.setValues(props.model)
}
},
/**
* @description 设置form的值
* @param data 需要设置的数据
*/
setValues: async (data: Recordable) => {
const form = await getForm()
form?.setValues(data)
},
/**
* @description 设置schema
* @param schemaProps 需要设置的schemaProps
*/
setSchema: async (schemaProps: FormSetProps[]) => {
const form = await getForm()
form?.setSchema(schemaProps)
},
/**
* @description 新增schema
* @param formSchema 需要新增数据
* @param index 在哪里新增
*/
addSchema: async (formSchema: FormSchema, index?: number) => {
const form = await getForm()
form?.addSchema(formSchema, index)
},
/**
* @description 删除schema
* @param field 删除哪个数据
*/
delSchema: async (field: string) => {
const form = await getForm()
form?.delSchema(field)
},
/**
* @description 获取表单数据
* @returns form data
*/
getFormData: async <T = Recordable>(): Promise<T> => {
const form = await getForm()
return form?.formModel as T
},
/**
* @description 获取表单组件的实例
* @param field 表单项唯一标识
* @returns component instance
*/
getComponentExpose: async (field: string) => {
const form = await getForm()
return form?.getComponentExpose(field)
},
/**
* @description 获取formItem组件的实例
* @param field 表单项唯一标识
* @returns formItem instance
*/
getFormItemExpose: async (field: string) => {
const form = await getForm()
return form?.getFormItemExpose(field) as ComponentRef<typeof ElFormItem>
},
/**
* @description 获取ElForm组件的实例
* @returns ElForm instance
*/
getElFormExpose: async () => {
await getForm()
return unref(elFormRef)
},
getFormExpose: async () => {
await getForm()
return unref(formRef)
}
}
return {
formRegister: register,
formMethods: methods
}
}
+52
View File
@@ -0,0 +1,52 @@
import { i18n } from '@/plugins/vueI18n'
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
} => {
const normalFn = {
t: (key: string) => {
return getKey(namespace, key)
}
}
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
return (t as any)(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
}
return {
...methods,
t: tFn
}
}
export const t = (key: string) => key
+7
View File
@@ -0,0 +1,7 @@
import { h } from 'vue'
import type { VNode } from 'vue'
import { Icon, IconTypes } from '@/components/Icon'
export const useIcon = (props: IconTypes): VNode => {
return h(Icon, props)
}
+35
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
}
}
+34
View File
@@ -0,0 +1,34 @@
import { nextTick, unref } from 'vue'
import type { NProgressOptions } from 'nprogress'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { useCssVar } from '@vueuse/core'
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
}
}
+60
View File
@@ -0,0 +1,60 @@
import { dateUtil } from '@/utils/dateUtil'
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 = dateUtil()
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
}
}
+18
View File
@@ -0,0 +1,18 @@
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
}
}
+91
View File
@@ -0,0 +1,91 @@
import { ref, unref, nextTick } from 'vue'
import { FormSchema, FormSetProps } from '@/components/Form'
import { SearchExpose, SearchProps } from '@/components/Search'
export const useSearch = () => {
// Search实例
const searchRef = ref<SearchExpose>()
/**
* @param ref Search实例
* @param elRef ElForm实例
*/
const register = (ref: SearchExpose) => {
searchRef.value = ref
}
const getSearch = async () => {
await nextTick()
const search = unref(searchRef)
if (!search) {
console.error('The Search is not registered. Please use the register method to register')
}
return search
}
// 一些内置的方法
const methods = {
/**
* @description 设置search组件的props
* @param field FormItem的field
*/
setProps: async (props: SearchProps = {}) => {
const search = await getSearch()
search?.setProps(props)
if (props.model) {
search?.setValues(props.model)
}
},
/**
* @description 设置form的值
* @param data 需要设置的数据
*/
setValues: async (data: Recordable) => {
const search = await getSearch()
search?.setValues(data)
},
/**
* @description 设置schema
* @param schemaProps 需要设置的schemaProps
*/
setSchema: async (schemaProps: FormSetProps[]) => {
const search = await getSearch()
search?.setSchema(schemaProps)
},
/**
* @description 新增schema
* @param formSchema 需要新增数据
* @param index 在哪里新增
*/
addSchema: async (formSchema: FormSchema, index?: number) => {
const search = await getSearch()
search?.addSchema(formSchema, index)
},
/**
* @description 删除schema
* @param field 删除哪个数据
*/
delSchema: async (field: string) => {
const search = await getSearch()
search?.delSchema(field)
},
/**
* @description 获取表单数据
* @returns form data
*/
getFormData: async <T = Recordable>(): Promise<T> => {
const search = await getSearch()
return search?.formModel as T
}
}
return {
searchRegister: register,
searchMethods: methods
}
}
+31
View File
@@ -0,0 +1,31 @@
import { isArray, isObject } from '@/utils/is'
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
const setStorage = (key: string, value: any) => {
window[type].setItem(key, isArray(value) || isObject(value) ? JSON.stringify(value) : value)
}
const getStorage = (key: string) => {
const value = window[type].getItem(key)
try {
return JSON.parse(value || '')
} catch (error) {
return value
}
}
const removeStorage = (key: string) => {
window[type].removeItem(key)
}
const clear = () => {
window[type].clear()
}
return {
setStorage,
getStorage,
removeStorage,
clear
}
}
+193
View File
@@ -0,0 +1,193 @@
import { useI18n } from '@/hooks/web/useI18n'
import { Table, TableExpose, TableProps, TableSetProps, TableColumn } from '@/components/Table'
import { ElTable, ElMessageBox, ElMessage } from 'element-plus'
import { ref, watch, unref, nextTick, onMounted } from 'vue'
const { t } = useI18n()
interface UseTableConfig {
/**
* 是否初始化的时候请求一次
*/
immediate?: boolean
fetchDataApi: () => Promise<{
list: any[]
total?: number
}>
fetchDelApi?: () => Promise<boolean>
}
export const useTable = (config: UseTableConfig) => {
const { immediate = true } = config
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const dataList = ref<any[]>([])
watch(
() => currentPage.value,
() => {
methods.getList()
}
)
watch(
() => pageSize.value,
() => {
// 当前页不为1时,修改页数后会导致多次调用getList方法
if (unref(currentPage) === 1) {
methods.getList()
} else {
currentPage.value = 1
methods.getList()
}
}
)
onMounted(() => {
if (immediate) {
methods.getList()
}
})
// Table实例
const tableRef = ref<typeof Table & TableExpose>()
// ElTable实例
const elTableRef = ref<ComponentRef<typeof ElTable>>()
const register = (ref: typeof Table & TableExpose, elRef: ComponentRef<typeof ElTable>) => {
tableRef.value = ref
elTableRef.value = unref(elRef)
}
const getTable = async () => {
await nextTick()
const table = unref(tableRef)
if (!table) {
console.error('The table is not registered. Please use the register method to register')
}
return table
}
const methods = {
/**
* 获取表单数据
*/
getList: async () => {
loading.value = true
try {
const res = await config?.fetchDataApi()
console.log('fetchDataApi res', res)
if (res) {
dataList.value = res.list
total.value = res.total || 0
}
} catch (err) {
console.log('fetchDataApi error')
} finally {
loading.value = false
}
},
/**
* @description 设置table组件的props
* @param props table组件的props
*/
setProps: async (props: TableProps = {}) => {
const table = await getTable()
table?.setProps(props)
},
/**
* @description 设置column
* @param columnProps 需要设置的列
*/
setColumn: async (columnProps: TableSetProps[]) => {
const table = await getTable()
table?.setColumn(columnProps)
},
/**
* @description 新增column
* @param tableColumn 需要新增数据
* @param index 在哪里新增
*/
addColumn: async (tableColumn: TableColumn, index?: number) => {
const table = await getTable()
table?.addColumn(tableColumn, index)
},
/**
* @description 删除column
* @param field 删除哪个数据
*/
delColumn: async (field: string) => {
const table = await getTable()
table?.delColumn(field)
},
/**
* @description 获取ElTable组件的实例
* @returns ElTable instance
*/
getElTableExpose: async () => {
await getTable()
return unref(elTableRef)
},
refresh: () => {
methods.getList()
},
// sortableChange: (e: any) => {
// console.log('sortableChange', e)
// const { oldIndex, newIndex } = e
// dataList.value.splice(newIndex, 0, dataList.value.splice(oldIndex, 1)[0])
// // to do something
// }
// 删除数据
delList: async (idsLength: number) => {
const { fetchDelApi } = config
if (!fetchDelApi) {
console.warn('fetchDelApi is undefined')
return
}
ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), {
confirmButtonText: t('common.delOk'),
cancelButtonText: t('common.delCancel'),
type: 'warning'
}).then(async () => {
const res = await fetchDelApi()
if (res) {
ElMessage.success(t('common.delSuccess'))
// 计算出临界点
const current =
unref(total) % unref(pageSize) === idsLength || unref(pageSize) === 1
? unref(currentPage) > 1
? unref(currentPage) - 1
: unref(currentPage)
: unref(currentPage)
currentPage.value = current
methods.getList()
}
})
}
}
return {
tableRegister: register,
tableMethods: methods,
tableState: {
currentPage,
pageSize,
total,
dataList,
loading
}
}
}
+63
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
}
}
+50
View File
@@ -0,0 +1,50 @@
import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core'
import { computed, unref } from 'vue'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
const TIME_AGO_MESSAGE_MAP: {
'zh-CN': UseTimeAgoMessages
en: UseTimeAgoMessages
} = {
'zh-CN': {
justNow: '刚刚',
invalid: '无效时间',
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} 秒`
},
en: {
justNow: '刚刚',
invalid: 'Invalid Date',
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
View File
@@ -0,0 +1,25 @@
import { watch, ref } from 'vue'
import { isString } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useI18n } from '@/hooks/web/useI18n'
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
}
+64
View File
@@ -0,0 +1,64 @@
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
type Callback = (error?: string | Error | undefined) => void
interface LengthRange {
min: number
max: number
message: string
}
export const useValidator = () => {
const required = (message?: string) => {
return {
required: true,
message: message || t('common.required')
}
}
const lengthRange = (val: any, callback: Callback, options: LengthRange) => {
const { min, max, message } = options
if (val.length < min || val.length > max) {
callback(new Error(message))
} else {
callback()
}
}
const notSpace = (val: any, callback: Callback, message: string) => {
// 用户名不能有空格
if (val.indexOf(' ') !== -1) {
callback(new Error(message))
} else {
callback()
}
}
const notSpecialCharacters = (val: any, callback: Callback, message: string) => {
// 密码不能是特殊字符
if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
callback(new Error(message))
} else {
callback()
}
}
// 两个字符串是否想等
const isEqual = (val1: string, val2: string, callback: Callback, message: string) => {
if (val1 === val2) {
callback()
} else {
callback(new Error(message))
}
}
return {
required,
lengthRange,
notSpace,
notSpecialCharacters,
isEqual
}
}
+55
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 }
}