diff --git a/.vscode/settings.json b/.vscode/settings.json index 1d3f0aa8..34310929 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -102,6 +102,7 @@ "codemirror", "commitlint", "cropperjs", + "echart", "echarts", "esnext", "esno", @@ -116,10 +117,12 @@ "sider", "sortablejs", "stylelint", + "svgs", "unocss", "unplugin", "unref", "videojs", + "VITE", "vitejs", "vueuse", "wangeditor", diff --git a/package.json b/package.json index 21a731a8..fb0f23ad 100644 --- a/package.json +++ b/package.json @@ -36,20 +36,20 @@ "@wangeditor/editor-for-vue": "^5.1.10", "@zxcvbn-ts/core": "^3.0.4", "animate.css": "^4.1.1", - "axios": "^1.5.1", + "axios": "^1.6.0", "benz-amr-recorder": "^1.1.5", "bpmn-js-token-simulation": "^0.10.0", "camunda-bpmn-moddle": "^7.0.1", "cropperjs": "^1.6.1", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", - "diagram-js": "^12.5.0", + "diagram-js": "^12.6.0", + "driver.js": "^1.3.0", "echarts": "^5.4.3", "echarts-wordcloud": "^2.1.0", "element-plus": "2.4.1", "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", - "intro.js": "^7.2.0", "jsencrypt": "^3.3.2", "lodash-es": "^4.17.21", "min-dash": "^4.1.1", @@ -64,7 +64,7 @@ "video.js": "^7.21.5", "vue": "^3.3.7", "vue-dompurify-html": "^4.1.4", - "vue-i18n": "^9.5.0", + "vue-i18n": "^9.6.2", "vue-router": "^4.2.5", "vue-types": "^5.1.1", "vuedraggable": "^4.1.0", @@ -72,20 +72,19 @@ "xml-js": "^1.6.11" }, "devDependencies": { - "@commitlint/cli": "^18.0.0", - "@commitlint/config-conventional": "^18.0.0", - "@iconify/json": "^2.2.132", + "@commitlint/cli": "^18.2.0", + "@commitlint/config-conventional": "^18.1.0", + "@iconify/json": "^2.2.135", "@intlify/unplugin-vue-i18n": "^1.4.0", "@purge-icons/generated": "^0.9.0", - "@types/intro.js": "^5.1.3", "@types/lodash-es": "^4.17.10", - "@types/node": "^20.8.8", + "@types/node": "^20.8.9", "@types/nprogress": "^0.2.2", "@types/qrcode": "^1.5.4", "@types/qs": "^6.9.9", "@types/sortablejs": "^1.15.4", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", "@unocss/transformer-variant-group": "^0.57.1", "@unocss/eslint-config": "^0.57.1", "@vitejs/plugin-legacy": "^4.1.1", @@ -99,24 +98,24 @@ "eslint-config-prettier": "^9.0.0", "eslint-define-config": "^1.24.1", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-vue": "^9.18.0", + "eslint-plugin-vue": "^9.18.1", "lint-staged": "^15.0.2", "postcss": "^8.4.31", "postcss-html": "^1.5.0", "postcss-scss": "^4.0.9", "prettier": "^3.0.3", "rimraf": "^5.0.5", - "rollup": "^4.1.4", - "sass": "^1.69.4", + "rollup": "^4.1.5", + "sass": "^1.69.5", "stylelint": "^15.11.0", "stylelint-config-html": "^1.1.0", "stylelint-config-recommended": "^13.0.0", "stylelint-config-standard": "^34.0.0", "stylelint-order": "^6.0.3", - "terser": "^5.22.0", + "terser": "^5.23.0", "typescript": "5.2.2", "unocss": "^0.57.1", - "unplugin-auto-import": "^0.16.6", + "unplugin-auto-import": "^0.16.7", "unplugin-element-plus": "^0.8.0", "unplugin-vue-components": "^0.25.2", "vite": "4.5.0", @@ -128,7 +127,7 @@ "vite-plugin-svg-icons": "^2.0.1", "vite-plugin-top-level-await": "^1.3.1", "vue-eslint-parser": "^9.3.2", - "vue-tsc": "^1.8.20" + "vue-tsc": "^1.8.22" }, "license": "MIT", "repository": { diff --git a/src/components/Card/src/CardTitle.vue b/src/components/Card/src/CardTitle.vue index 5b122f49..76a83564 100644 --- a/src/components/Card/src/CardTitle.vue +++ b/src/components/Card/src/CardTitle.vue @@ -3,7 +3,7 @@ defineComponent({ name: 'CardTitle' }) -const { title } = defineProps({ +defineProps({ title: { type: String, required: true diff --git a/src/components/ConfigGlobal/src/ConfigGlobal.vue b/src/components/ConfigGlobal/src/ConfigGlobal.vue index a0873967..5bd90cf1 100644 --- a/src/components/ConfigGlobal/src/ConfigGlobal.vue +++ b/src/components/ConfigGlobal/src/ConfigGlobal.vue @@ -1,20 +1,19 @@ -<script lang="ts" setup> +<script setup lang="ts"> +import { provide, computed, watch, onMounted } from 'vue' import { propTypes } from '@/utils/propTypes' +import { ComponentSize, ElConfigProvider } from 'element-plus' import { useLocaleStore } from '@/store/modules/locale' +import { useWindowSize } from '@vueuse/core' import { useAppStore } from '@/store/modules/app' import { setCssVar } from '@/utils' import { useDesign } from '@/hooks/web/useDesign' -import { ElementPlusSize } from '@/types/elementPlus' -import { useWindowSize } from '@vueuse/core' - -defineOptions({ name: 'ConfigGlobal' }) const { variables } = useDesign() const appStore = useAppStore() const props = defineProps({ - size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default') + size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default') }) provide('configGlobal', props) @@ -53,9 +52,9 @@ const currentLocale = computed(() => localeStore.currentLocale) <template> <ElConfigProvider + :namespace="variables.elNamespace" :locale="currentLocale.elLocale" :message="{ max: 1 }" - :namespace="variables.elNamespace" :size="size" > <slot></slot> diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue index d7fa90cb..9f268a3f 100644 --- a/src/components/ShortcutDateRangePicker/index.vue +++ b/src/components/ShortcutDateRangePicker/index.vue @@ -27,7 +27,7 @@ import * as DateUtil from '@/utils/formatTime' defineOptions({ name: 'ShortcutDateRangePicker' }) const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天 -const times = ref<[dayjs.ConfigType, dayjs.ConfigType]>(['', '']) // 时间范围参数 +const times = ref<[string, string]>(['', '']) // 时间范围参数 defineExpose({ times }) // 暴露时间范围参数 /** 日期快捷选择 */ const shortcuts = [ diff --git a/src/components/Sticky/src/Sticky.vue b/src/components/Sticky/src/Sticky.vue index b958544a..28ecbcb8 100644 --- a/src/components/Sticky/src/Sticky.vue +++ b/src/components/Sticky/src/Sticky.vue @@ -32,7 +32,7 @@ onMounted(() => { scrollContainer.value = getScrollContainer(refSticky.value!, true) useEventListener(scrollContainer, 'scroll', handleScroll) - useEventListener('resize', handleReize) + useEventListener('resize', handleResize) handleScroll() }) onActivated(() => { @@ -103,7 +103,7 @@ const handleScroll = () => { reset() } } -const handleReize = () => { +const handleResize = () => { if (isSticky.value && refSticky.value) { width.value = refSticky.value.getBoundingClientRect().width + 'px' } diff --git a/src/components/Tooltip/src/Tooltip.vue b/src/components/Tooltip/src/Tooltip.vue index 7490bd70..1a2e09cc 100644 --- a/src/components/Tooltip/src/Tooltip.vue +++ b/src/components/Tooltip/src/Tooltip.vue @@ -4,13 +4,13 @@ import { propTypes } from '@/utils/propTypes' defineOptions({ name: 'Tooltip' }) defineProps({ - titel: propTypes.string.def(''), + title: propTypes.string.def(''), message: propTypes.string.def(''), icon: propTypes.string.def('ep:question-filled') }) </script> <template> - <span>{{ titel }}</span> + <span>{{ title }}</span> <ElTooltip :content="message" placement="top"> <Icon :icon="icon" class="relative top-1px ml-1px" /> </ElTooltip> diff --git a/src/hooks/web/useGuide.ts b/src/hooks/web/useGuide.ts new file mode 100644 index 00000000..7fd2fb09 --- /dev/null +++ b/src/hooks/web/useGuide.ts @@ -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 + } +} diff --git a/src/hooks/web/useIntro.ts b/src/hooks/web/useIntro.ts deleted file mode 100644 index 7fe00845..00000000 --- a/src/hooks/web/useIntro.ts +++ /dev/null @@ -1,47 +0,0 @@ -import introJs from 'intro.js' -import { IntroJs, Step, Options } from 'intro.js' -import 'intro.js/introjs.css' - -import { useDesign } from '@/hooks/web/useDesign' - -export const useIntro = (setps?: Step[], options?: Options) => { - const { t } = useI18n() - - const { variables } = useDesign() - - const defaultSetps: Step[] = setps || [ - { - element: `#${variables.namespace}-menu`, - title: t('common.menu'), - intro: t('common.menuDes'), - position: 'right' - }, - { - element: `#${variables.namespace}-tool-header`, - title: t('common.tool'), - intro: t('common.toolDes'), - position: 'left' - }, - { - element: `#${variables.namespace}-tags-view`, - title: t('common.tagsView'), - intro: t('common.tagsViewDes'), - position: 'bottom' - } - ] - - const defaultOptions: Options = options || { - prevLabel: t('common.prevLabel'), - nextLabel: t('common.nextLabel'), - skipLabel: t('common.skipLabel'), - doneLabel: t('common.doneLabel') - } - - const introRef: IntroJs = introJs() - - introRef.addSteps(defaultSetps).setOptions(defaultOptions) - - return { - introRef - } -} diff --git a/src/hooks/web/useNetwork.ts b/src/hooks/web/useNetwork.ts new file mode 100644 index 00000000..66fa4464 --- /dev/null +++ b/src/hooks/web/useNetwork.ts @@ -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 } diff --git a/src/hooks/web/useNow.ts b/src/hooks/web/useNow.ts new file mode 100644 index 00000000..09d3176b --- /dev/null +++ b/src/hooks/web/useNow.ts @@ -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 + } +} diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts new file mode 100644 index 00000000..31eadb02 --- /dev/null +++ b/src/hooks/web/useTagsView.ts @@ -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 + } +} diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts index 0c16fa31..151e35b2 100644 --- a/src/hooks/web/useValidator.ts +++ b/src/hooks/web/useValidator.ts @@ -1,54 +1,53 @@ -const { t } = useI18n() +import { useI18n } from '@/hooks/web/useI18n' +import { FormItemRule } from 'element-plus' -type Callback = (error?: string | Error | undefined) => void +const { t } = useI18n() interface LengthRange { min: number max: number - message: string + message?: string } export const useValidator = () => { - const required = (message?: string) => { + const required = (message?: string): FormItemRule => { return { required: true, message: message || t('common.required') } } - const lengthRange = (val: any, callback: Callback, options: LengthRange) => { + const lengthRange = (options: LengthRange): FormItemRule => { const { min, max, message } = options - if (val.length < min || val.length > max) { - callback(new Error(message)) - } else { - callback() + + return { + min, + max, + message: message || t('common.lengthRange', { min, max }) } } - const notSpace = (val: any, callback: Callback, message: string) => { - // 用户名不能有空格 - if (val.indexOf(' ') !== -1) { - callback(new Error(message)) - } else { - callback() + const notSpace = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (val?.indexOf(' ') !== -1) { + callback(new Error(message || t('common.notSpace'))) + } 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)) + const notSpecialCharacters = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { + callback(new Error(message || t('common.notSpecialCharacters'))) + } else { + callback() + } + } } } @@ -56,7 +55,6 @@ export const useValidator = () => { required, lengthRange, notSpace, - notSpecialCharacters, - isEqual + notSpecialCharacters } } diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index 53ccda11..e2ffcadd 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -334,6 +334,6 @@ export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] { export function getDateRange( beginDate: dayjs.ConfigType, endDate: dayjs.ConfigType -): [dayjs.ConfigType, dayjs.ConfigType] { - return [dayjs(beginDate).startOf('d'), dayjs(endDate).endOf('d')] +): [string, string] { + return [dayjs(beginDate).startOf('d').toString(), dayjs(endDate).endOf('d').toString()] } diff --git a/src/utils/index.ts b/src/utils/index.ts index d5301ddb..10f57567 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -34,6 +34,13 @@ export const underlineToHump = (str: string): string => { }) } +/** + * 驼峰转横杠 + */ +export const humpToDash = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { dom.style.setProperty(prop, val) } @@ -67,7 +74,7 @@ export const trim = (str: string) => { * @param {Date | number | string} time 需要转换的时间 * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss */ -export const formatTime = (time: Date | number | string, fmt: string) => { +export function formatTime(time: Date | number | string, fmt: string) { if (!time) return '' else { const date = new Date(time) @@ -98,7 +105,7 @@ export const formatTime = (time: Date | number | string, fmt: string) => { /** * 生成随机字符串 */ -export const toAnyString = () => { +export function toAnyString() { const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { const r: number = (Math.random() * 16) | 0 const v: number = c === 'x' ? r : (r & 0x3) | 0x8 @@ -107,6 +114,13 @@ export const toAnyString = () => { return str } +/** + * 首字母大写 + */ +export function firstUpperCase(str: string) { + return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +} + export const generateUUID = () => { if (typeof crypto === 'object') { if (typeof crypto.randomUUID === 'function') { diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue index 78debc55..7eaf6ab6 100644 --- a/src/views/system/menu/MenuForm.vue +++ b/src/views/system/menu/MenuForm.vue @@ -38,7 +38,7 @@ <template #label> <Tooltip message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头" - titel="路由地址" + title="路由地址" /> </template> <el-input v-model="formData.path" clearable placeholder="请输入路由地址" /> @@ -53,7 +53,7 @@ <template #label> <Tooltip message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)" - titel="权限标识" + title="权限标识" /> </template> <el-input v-model="formData.permission" clearable placeholder="请输入权限标识" /> @@ -74,7 +74,7 @@ </el-form-item> <el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible"> <template #label> - <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" titel="显示状态" /> + <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" /> </template> <el-radio-group v-model="formData.visible"> <el-radio key="true" :label="true" border>显示</el-radio> @@ -85,7 +85,7 @@ <template #label> <Tooltip message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单" - titel="总是显示" + title="总是显示" /> </template> <el-radio-group v-model="formData.alwaysShow"> @@ -97,7 +97,7 @@ <template #label> <Tooltip message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段" - titel="缓存状态" + title="缓存状态" /> </template> <el-radio-group v-model="formData.keepAlive"> diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 2f564dd9..bf64a807 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -1,6 +1,6 @@ <template> <doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" /> - <doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue2/route/" /> + <doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" /> <!-- 搜索工作栏 --> <ContentWrap> diff --git a/tsconfig.json b/tsconfig.json index 1ee23774..182852ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,6 @@ "@intlify/unplugin-vue-i18n/types", "vite/client", "element-plus/global", - "@types/intro.js", "@types/qrcode", "vite-plugin-svg-icons/client" ], diff --git a/uno.config.ts b/uno.config.ts index 0645fe68..8b874a4a 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -101,5 +101,8 @@ ${selector}:after { ] ], presets: [presetUno({ dark: 'class', attributify: false })], - transformers: [transformerVariantGroup()] + transformers: [transformerVariantGroup()], + shortcuts: { + 'wh-full': 'w-full h-full' + } })