Initial commit
This commit is contained in:
26
promo-ui2/src/hooks/event/useEventBus.ts
Normal file
26
promo-ui2/src/hooks/event/useEventBus.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import mitt from 'mitt'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
|
||||
interface Option {
|
||||
name: string // 事件名称
|
||||
callback: Fn // 回调
|
||||
}
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
export const useEventBus = (option?: Option) => {
|
||||
if (option) {
|
||||
emitter.on(option.name, option.callback)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
emitter.off(option.name)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
on: emitter.on,
|
||||
off: emitter.off,
|
||||
emit: emitter.emit,
|
||||
all: emitter.all
|
||||
}
|
||||
}
|
||||
62
promo-ui2/src/hooks/event/useScrollTo.ts
Normal file
62
promo-ui2/src/hooks/event/useScrollTo.ts
Normal 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 }
|
||||
}
|
||||
47
promo-ui2/src/hooks/web/useClipboard.ts
Normal file
47
promo-ui2/src/hooks/web/useClipboard.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const useClipboard = () => {
|
||||
const copied = ref(false)
|
||||
const text = ref('')
|
||||
const isSupported = ref(false)
|
||||
|
||||
if (!navigator.clipboard && !document.execCommand) {
|
||||
isSupported.value = false
|
||||
} else {
|
||||
isSupported.value = true
|
||||
}
|
||||
|
||||
const copy = (str: string) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(str).then(() => {
|
||||
text.value = str
|
||||
copied.value = true
|
||||
resetCopied()
|
||||
})
|
||||
return
|
||||
}
|
||||
const input = document.createElement('input')
|
||||
input.setAttribute('readonly', 'readonly')
|
||||
input.setAttribute('value', str)
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
input.setSelectionRange(0, 9999)
|
||||
if (document.execCommand('copy')) {
|
||||
text.value = str
|
||||
document.execCommand('copy')
|
||||
copied.value = true
|
||||
resetCopied()
|
||||
}
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
|
||||
const resetCopied = () => {
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
return { copy, text, copied, isSupported }
|
||||
}
|
||||
|
||||
export { useClipboard }
|
||||
10
promo-ui2/src/hooks/web/useConfigGlobal.ts
Normal file
10
promo-ui2/src/hooks/web/useConfigGlobal.ts
Normal 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
|
||||
}
|
||||
}
|
||||
163
promo-ui2/src/hooks/web/useCrudSchemas.ts
Normal file
163
promo-ui2/src/hooks/web/useCrudSchemas.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 不推荐使用,感觉过于繁琐,不是很灵活 可能会在某个版本中删除
|
||||
*/
|
||||
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]
|
||||
if (schemaItem.search?.hidden === true) {
|
||||
continue
|
||||
}
|
||||
// 判断是否隐藏
|
||||
const searchSchemaItem = {
|
||||
component: schemaItem?.search?.component || 'Input',
|
||||
...schemaItem.search,
|
||||
field: schemaItem.field,
|
||||
label: schemaItem.search?.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,
|
||||
...schema.table
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 第一次过滤会有 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.form?.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
promo-ui2/src/hooks/web/useDesign.ts
Normal file
18
promo-ui2/src/hooks/web/useDesign.ts
Normal 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
|
||||
}
|
||||
}
|
||||
149
promo-ui2/src/hooks/web/useForm.ts
Normal file
149
promo-ui2/src/hooks/web/useForm.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
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'
|
||||
import { isEmptyVal, isObject } from '@/utils/is'
|
||||
|
||||
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>(filterEmptyVal = true): Promise<T> => {
|
||||
const form = await getForm()
|
||||
const model = form?.formModel as any
|
||||
if (filterEmptyVal) {
|
||||
// 使用reduce过滤空值,并返回一个新对象
|
||||
return Object.keys(model).reduce((prev, next) => {
|
||||
const value = model[next]
|
||||
if (!isEmptyVal(value)) {
|
||||
if (isObject(value)) {
|
||||
if (Object.keys(value).length > 0) {
|
||||
prev[next] = value
|
||||
}
|
||||
} else {
|
||||
prev[next] = value
|
||||
}
|
||||
}
|
||||
return prev
|
||||
}, {}) as T
|
||||
} else {
|
||||
return model 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
|
||||
}
|
||||
}
|
||||
49
promo-ui2/src/hooks/web/useGuide.ts
Normal file
49
promo-ui2/src/hooks/web/useGuide.ts
Normal 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
|
||||
}
|
||||
}
|
||||
52
promo-ui2/src/hooks/web/useI18n.ts
Normal file
52
promo-ui2/src/hooks/web/useI18n.ts
Normal 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
promo-ui2/src/hooks/web/useIcon.ts
Normal file
7
promo-ui2/src/hooks/web/useIcon.ts
Normal 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
promo-ui2/src/hooks/web/useLocale.ts
Normal file
35
promo-ui2/src/hooks/web/useLocale.ts
Normal 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
|
||||
}
|
||||
}
|
||||
129
promo-ui2/src/hooks/web/useMonacoEditor.ts
Normal file
129
promo-ui2/src/hooks/web/useMonacoEditor.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { ref, nextTick, onBeforeUnmount } from 'vue'
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
|
||||
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
if (label === 'json') {
|
||||
return new jsonWorker()
|
||||
}
|
||||
if (label === 'css' || label === 'scss' || label === 'less') {
|
||||
return new cssWorker()
|
||||
}
|
||||
if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
||||
return new htmlWorker()
|
||||
}
|
||||
if (label === 'typescript' || label === 'javascript') {
|
||||
return new tsWorker()
|
||||
}
|
||||
return new editorWorker()
|
||||
}
|
||||
}
|
||||
|
||||
export function useMonacoEditor(language: string = 'javascript') {
|
||||
// 编辑器示例
|
||||
let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
|
||||
// 目标元素
|
||||
const monacoEditorRef = ref<HTMLElement>()
|
||||
|
||||
// 创建实例
|
||||
function createEditor(editorOption: monaco.editor.IStandaloneEditorConstructionOptions = {}) {
|
||||
if (!monacoEditorRef.value) return
|
||||
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',
|
||||
// 自动布局
|
||||
automaticLayout: true,
|
||||
...editorOption
|
||||
})
|
||||
return monacoEditor
|
||||
}
|
||||
|
||||
// 格式化
|
||||
async function formatDoc() {
|
||||
await monacoEditor?.getAction('editor.action.formatDocument')?.run()
|
||||
}
|
||||
|
||||
// 数据更新
|
||||
function updateVal(val: string) {
|
||||
nextTick(() => {
|
||||
if (getOption(monaco.editor.EditorOption.readOnly)) {
|
||||
updateOptions({ readOnly: false })
|
||||
}
|
||||
monacoEditor?.setValue(val)
|
||||
setTimeout(async () => {
|
||||
await formatDoc()
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
// 配置更新
|
||||
function updateOptions(opt: monaco.editor.IStandaloneEditorConstructionOptions) {
|
||||
monacoEditor?.updateOptions(opt)
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
function getOption(name: monaco.editor.EditorOption) {
|
||||
return monacoEditor?.getOption(name)
|
||||
}
|
||||
|
||||
// 获取实例
|
||||
function getEditor() {
|
||||
return monacoEditor
|
||||
}
|
||||
|
||||
function changeLanguage(newLanguage: string) {
|
||||
const model = monacoEditor?.getModel()
|
||||
if (model) {
|
||||
monaco.editor.setModelLanguage(model, newLanguage)
|
||||
}
|
||||
}
|
||||
|
||||
function changeTheme(newTheme: string) {
|
||||
monaco.editor.setTheme(newTheme)
|
||||
}
|
||||
|
||||
// 页面离开 销毁
|
||||
onBeforeUnmount(() => {
|
||||
if (monacoEditor) {
|
||||
monacoEditor.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
monacoEditorRef,
|
||||
createEditor,
|
||||
getEditor,
|
||||
updateVal,
|
||||
updateOptions,
|
||||
getOption,
|
||||
formatDoc,
|
||||
changeLanguage,
|
||||
changeTheme
|
||||
}
|
||||
}
|
||||
34
promo-ui2/src/hooks/web/useNProgress.ts
Normal file
34
promo-ui2/src/hooks/web/useNProgress.ts
Normal 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) as string
|
||||
}
|
||||
}
|
||||
|
||||
initColor()
|
||||
|
||||
const start = () => {
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
const done = () => {
|
||||
NProgress.done()
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
done
|
||||
}
|
||||
}
|
||||
21
promo-ui2/src/hooks/web/useNetwork.ts
Normal file
21
promo-ui2/src/hooks/web/useNetwork.ts
Normal 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
promo-ui2/src/hooks/web/useNow.ts
Normal file
60
promo-ui2/src/hooks/web/useNow.ts
Normal 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
|
||||
}
|
||||
}
|
||||
20
promo-ui2/src/hooks/web/usePageLoading.ts
Normal file
20
promo-ui2/src/hooks/web/usePageLoading.ts
Normal 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
|
||||
}
|
||||
}
|
||||
91
promo-ui2/src/hooks/web/useSearch.ts
Normal file
91
promo-ui2/src/hooks/web/useSearch.ts
Normal 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?.getFormData() as T
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
searchRegister: register,
|
||||
searchMethods: methods
|
||||
}
|
||||
}
|
||||
46
promo-ui2/src/hooks/web/useStorage.ts
Normal file
46
promo-ui2/src/hooks/web/useStorage.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// 获取传入的值的类型
|
||||
const getValueType = (value: any) => {
|
||||
const type = Object.prototype.toString.call(value)
|
||||
return type.slice(8, -1)
|
||||
}
|
||||
|
||||
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
|
||||
const setStorage = (key: string, value: any) => {
|
||||
const valueType = getValueType(value)
|
||||
window[type].setItem(key, JSON.stringify({ type: valueType, value }))
|
||||
}
|
||||
|
||||
const getStorage = (key: string) => {
|
||||
const value = window[type].getItem(key)
|
||||
if (value) {
|
||||
const { value: val } = JSON.parse(value)
|
||||
return val
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const removeStorage = (key: string) => {
|
||||
window[type].removeItem(key)
|
||||
}
|
||||
|
||||
const clear = (excludes?: string[]) => {
|
||||
// 获取排除项
|
||||
const keys = Object.keys(window[type])
|
||||
const defaultExcludes = ['dynamicRouter', 'serverDynamicRouter']
|
||||
const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes
|
||||
const excludesKeys = excludesArr ? keys.filter((key) => !excludesArr.includes(key)) : keys
|
||||
// 排除项不清除
|
||||
excludesKeys.forEach((key) => {
|
||||
window[type].removeItem(key)
|
||||
})
|
||||
// window[type].clear()
|
||||
}
|
||||
|
||||
return {
|
||||
setStorage,
|
||||
getStorage,
|
||||
removeStorage,
|
||||
clear
|
||||
}
|
||||
}
|
||||
195
promo-ui2/src/hooks/web/useTable.ts
Normal file
195
promo-ui2/src/hooks/web/useTable.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
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[]>([])
|
||||
let isPageSizeChange = false
|
||||
|
||||
watch(
|
||||
() => currentPage.value,
|
||||
() => {
|
||||
if (!isPageSizeChange) methods.getList()
|
||||
isPageSizeChange = false
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => pageSize.value,
|
||||
() => {
|
||||
if (unref(currentPage) === 1) {
|
||||
methods.getList()
|
||||
} else {
|
||||
currentPage.value = 1
|
||||
isPageSizeChange = true
|
||||
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
promo-ui2/src/hooks/web/useTagsView.ts
Normal file
63
promo-ui2/src/hooks/web/useTagsView.ts
Normal 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
promo-ui2/src/hooks/web/useTimeAgo.ts
Normal file
50
promo-ui2/src/hooks/web/useTimeAgo.ts
Normal 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
promo-ui2/src/hooks/web/useTitle.ts
Normal file
25
promo-ui2/src/hooks/web/useTitle.ts
Normal 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
|
||||
}
|
||||
109
promo-ui2/src/hooks/web/useValidator.ts
Normal file
109
promo-ui2/src/hooks/web/useValidator.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const phone = (message?: string): FormItemRule => {
|
||||
return {
|
||||
validator: (_, val, callback) => {
|
||||
if (!val) return callback()
|
||||
if (!/^1[3456789]\d{9}$/.test(val)) {
|
||||
callback(new Error(message || '请输入正确的手机号码'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const email = (message?: string): FormItemRule => {
|
||||
return {
|
||||
validator: (_, val, callback) => {
|
||||
if (!val) return callback()
|
||||
if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(val)) {
|
||||
callback(new Error(message || '请输入正确的邮箱'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxlength = (max: number): FormItemRule => {
|
||||
return {
|
||||
max,
|
||||
message: '长度不能超过' + max + '个字符'
|
||||
}
|
||||
}
|
||||
|
||||
const check = (message?: string): FormItemRule => {
|
||||
return {
|
||||
validator: (_, val, callback) => {
|
||||
if (!val) {
|
||||
callback(new Error(message || t('common.required')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
required,
|
||||
lengthRange,
|
||||
notSpace,
|
||||
notSpecialCharacters,
|
||||
phone,
|
||||
email,
|
||||
maxlength,
|
||||
check
|
||||
}
|
||||
}
|
||||
55
promo-ui2/src/hooks/web/useWatermark.ts
Normal file
55
promo-ui2/src/hooks/web/useWatermark.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user