Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96728d87fd | ||
|
|
66419d22d4 | ||
|
|
18cb623730 | ||
|
|
4c7f052ea1 | ||
|
|
13b535c530 | ||
|
|
b3fbfe2d9d | ||
|
|
da80cad976 | ||
|
|
d6e05a8b44 | ||
| 5a196564d0 | |||
| 7ec1751ba6 | |||
|
|
270a838185 | ||
|
|
5b54fe5182 | ||
|
|
21676c11ff | ||
|
|
75f461df0f | ||
|
|
b623365e38 | ||
|
|
8130915d0e | ||
|
|
eeb72b24b5 | ||
|
|
68e320637e | ||
|
|
f81c7614be | ||
|
|
2874fdfaa7 | ||
|
|
7dd7ce73bc | ||
|
|
94d09d0bdd | ||
|
|
b0b65b454c | ||
|
|
c490cb1c8e | ||
|
|
33e8a4a5d6 | ||
|
|
35640a2ade | ||
|
|
8db7ded1b5 | ||
|
|
aa8e3467d3 | ||
|
|
8ed289a917 | ||
|
|
88ca601d07 | ||
|
|
50911ada4e | ||
|
|
d065c90e71 | ||
|
|
ad0a50d8c7 | ||
|
|
d4d9620db2 | ||
|
|
267bf75bc1 |
21
.env
21
.env
@@ -1,10 +1,15 @@
|
|||||||
|
VITE_APP_BUILD_TIME=NOT_SET
|
||||||
|
VITE_APP_BUILD_COMMIT=NOT_SET
|
||||||
|
|
||||||
|
VITE_BUILD_SOURCE_MAP=true
|
||||||
|
VITE_BUILD_MINIFY=true
|
||||||
|
VITE_CLOUDFLARE_SERVER_ENABLED=true
|
||||||
|
|
||||||
VITE_APP_TITLE=vue-ts-example-2025
|
VITE_APP_TITLE=vue-ts-example-2025
|
||||||
VITE_APP_BASE=/
|
VITE_APP_BASE=/
|
||||||
VITE_APP_BUILD_SOURCE_MAP=true
|
VITE_APP_ENABLE_VUE_DEVTOOLS=true
|
||||||
VITE_APP_BUILD_MINIFY=true
|
VITE_APP_MENU_SHOW_DEMOS=true
|
||||||
VITE_APP_BUILD_COMMIT=
|
VITE_APP_MENU_SHOW_ORDER=true
|
||||||
VITE_APP_BUILD_TIME=
|
VITE_APP_ENABLE_ROUTER_LOG_GUARD=true
|
||||||
VITE_ENABLE_VUE_DEVTOOLS=true
|
VITE_APP_API_URL=/API
|
||||||
VITE_MENU_SHOW_DEMOS=true
|
VITE_APP_PROXY=[["/API","https://jsonplaceholder.typicode.com"]]
|
||||||
VITE_MENU_SHOW_ORDER=true
|
|
||||||
VITE_CLOUDFLARE_SERVER_ENABLED=true
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
echo "🧹 [Pre-commit] 正在运行 lint-staged..."
|
echo "🧹 [Pre-commit] 正在运行 lint-staged..."
|
||||||
time pnpm exec lint-staged
|
time pnpm exec lint-staged
|
||||||
time pnpm run lint:vue-i18n-extract
|
time pnpm run lint:vue-i18n-extract
|
||||||
|
# time pnpm run type-check
|
||||||
echo "🧹 [Pre-commit] lint-staged 完成!"
|
echo "🧹 [Pre-commit] lint-staged 完成!"
|
||||||
|
|||||||
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -21,10 +21,17 @@
|
|||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
|
||||||
"i18n-ally.localesPaths": ["src/locales"],
|
// >>>>>
|
||||||
|
"i18n-ally.namespace": false /* 禁用命名空间(@intlify/unplugin-vue-i18n不支持吧?) */,
|
||||||
|
"i18n-ally.localesPaths": ["src/locales/demo", "src/locales"],
|
||||||
|
// https://github.com/lokalise/i18n-ally/wiki/Path-Matcher
|
||||||
|
// 默认: 🗃 Path Matcher Regex: /^(?<locale>[\w-_]+)(?:.*\/|^).*\.(?<ext>json|ya?ml|json5)$/
|
||||||
|
"i18n-ally.pathMatcher": "{locale}.json",
|
||||||
|
"i18n-ally.enabledParsers": ["json"],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容
|
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容
|
||||||
"i18n-ally.displayLanguage": "zh-CN", // 显示语言 (显示文件/翻译文件)
|
"i18n-ally.displayLanguage": "zh-CN", // 显示语言 (显示文件/翻译文件)
|
||||||
"i18n-ally.keystyle": "nested",
|
// <<<<<
|
||||||
|
|
||||||
// https://github.com/copilot/share/8a1a019a-0180-80e7-8141-a40be02c4006
|
// https://github.com/copilot/share/8a1a019a-0180-80e7-8141-a40be02c4006
|
||||||
// "iconify.customCollectionJsonPaths": ["https://example.com/my-icons.json", "./local/icons.json"],
|
// "iconify.customCollectionJsonPaths": ["https://example.com/my-icons.json", "./local/icons.json"],
|
||||||
|
|||||||
26
auto-imports.d.ts
vendored
26
auto-imports.d.ts
vendored
@@ -6,8 +6,10 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const APP_THEME_MODES: typeof import('./src/stores/app-store')['APP_THEME_MODES']
|
const ConfirmationService: typeof import('utils4u/primevue')['ConfirmationService']
|
||||||
|
const DialogService: typeof import('utils4u/primevue')['DialogService']
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const ToastService: typeof import('utils4u/primevue')['ToastService']
|
||||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
const arrayToTree: typeof import('utils4u/array')['arrayToTree']
|
const arrayToTree: typeof import('utils4u/array')['arrayToTree']
|
||||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
@@ -50,6 +52,8 @@ declare global {
|
|||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue')['h']
|
||||||
|
const i18nInstance: typeof import('./src/locales-utils/i18n-auto-imports')['i18nInstance']
|
||||||
|
const i18nRouteMessages: typeof import('./src/locales-utils/route-messages/route-messages-auto-imports')['i18nRouteMessages']
|
||||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: typeof import('vue')['inject']
|
||||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||||
@@ -59,7 +63,6 @@ declare global {
|
|||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
const isShallow: typeof import('vue')['isShallow']
|
const isShallow: typeof import('vue')['isShallow']
|
||||||
const locales4RouteMessages: typeof import('./src/locales-4-route/_messages-auto-imports')['locales4RouteMessages']
|
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
const mapActions: typeof import('pinia')['mapActions']
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
const mapGetters: typeof import('pinia')['mapGetters']
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
@@ -137,7 +140,7 @@ declare global {
|
|||||||
const until: typeof import('@vueuse/core')['until']
|
const until: typeof import('@vueuse/core')['until']
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
const useAppStore: typeof import('./src/stores/app-store')['useAppStore']
|
const useAppStore: typeof import('./src/stores/app-store-auto-imports')['useAppStore']
|
||||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||||
@@ -255,6 +258,7 @@ declare global {
|
|||||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
|
const usePrimevueDialogRef: typeof import('utils4u/primevue')['usePrimevueDialogRef']
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
const useRefs: typeof import('utils4u/vue-use')['useRefs']
|
const useRefs: typeof import('utils4u/vue-use')['useRefs']
|
||||||
@@ -262,6 +266,7 @@ declare global {
|
|||||||
const useRoute: typeof import('vue-router')['useRoute']
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
const useRouter: typeof import('vue-router')['useRouter']
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||||
|
const useSafeNForm: typeof import('./src/utils/use-safe-n-form-auto-imports')['useSafeNForm']
|
||||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
@@ -335,8 +340,8 @@ declare global {
|
|||||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { AppThemeMode } from './src/stores/app-store'
|
export type { AppThemeMode } from './src/stores/app-store-auto-imports'
|
||||||
import('./src/stores/app-store')
|
import('./src/stores/app-store-auto-imports')
|
||||||
}
|
}
|
||||||
|
|
||||||
// for vue template auto import
|
// for vue template auto import
|
||||||
@@ -344,8 +349,10 @@ import { UnwrapRef } from 'vue'
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface GlobalComponents {}
|
interface GlobalComponents {}
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
readonly APP_THEME_MODES: UnwrapRef<typeof import('./src/stores/app-store')['APP_THEME_MODES']>
|
readonly ConfirmationService: UnwrapRef<typeof import('utils4u/primevue')['ConfirmationService']>
|
||||||
|
readonly DialogService: UnwrapRef<typeof import('utils4u/primevue')['DialogService']>
|
||||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
|
readonly ToastService: UnwrapRef<typeof import('utils4u/primevue')['ToastService']>
|
||||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||||
readonly arrayToTree: UnwrapRef<typeof import('utils4u/array')['arrayToTree']>
|
readonly arrayToTree: UnwrapRef<typeof import('utils4u/array')['arrayToTree']>
|
||||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||||
@@ -388,6 +395,8 @@ declare module 'vue' {
|
|||||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
||||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly i18nInstance: UnwrapRef<typeof import('./src/locales-utils/i18n-auto-imports')['i18nInstance']>
|
||||||
|
readonly i18nRouteMessages: UnwrapRef<typeof import('./src/locales-utils/route-messages/route-messages-auto-imports')['i18nRouteMessages']>
|
||||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||||
@@ -397,7 +406,6 @@ declare module 'vue' {
|
|||||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
|
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
|
||||||
readonly locales4RouteMessages: UnwrapRef<typeof import('./src/locales-4-route/_messages-auto-imports')['locales4RouteMessages']>
|
|
||||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||||
@@ -475,7 +483,7 @@ declare module 'vue' {
|
|||||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||||
readonly useAppStore: UnwrapRef<typeof import('./src/stores/app-store')['useAppStore']>
|
readonly useAppStore: UnwrapRef<typeof import('./src/stores/app-store-auto-imports')['useAppStore']>
|
||||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||||
@@ -593,6 +601,7 @@ declare module 'vue' {
|
|||||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||||
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
||||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||||
|
readonly usePrimevueDialogRef: UnwrapRef<typeof import('utils4u/primevue')['usePrimevueDialogRef']>
|
||||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||||
readonly useRefs: UnwrapRef<typeof import('utils4u/vue-use')['useRefs']>
|
readonly useRefs: UnwrapRef<typeof import('utils4u/vue-use')['useRefs']>
|
||||||
@@ -600,6 +609,7 @@ declare module 'vue' {
|
|||||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||||
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||||
|
readonly useSafeNForm: UnwrapRef<typeof import('./src/utils/use-safe-n-form-auto-imports')['useSafeNForm']>
|
||||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export default defineConfigWithVueTs(
|
|||||||
'error',
|
'error',
|
||||||
{ order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'] },
|
{ order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'] },
|
||||||
],
|
],
|
||||||
|
'vue/attributes-order': 'error',
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -8,7 +8,7 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"_all": "run-p build-only type-check lint format:prettier test:unit:DisableWatch",
|
"_all": "run-s build-only lint format:prettier type-check test:unit:DisableWatch",
|
||||||
"dev": "vite --port 4730 --host --strictPort",
|
"dev": "vite --port 4730 --host --strictPort",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
"format:prettier": "prettier --write src/",
|
"format:prettier": "prettier --write src/",
|
||||||
"type-check": "vue-tsc --build",
|
"type-check": "vue-tsc --build",
|
||||||
"lint": "run-s lint:*",
|
"lint": "run-s lint:*",
|
||||||
"lint:vue-i18n-extract": "vue-i18n-extract report --vueFiles './src/**/*.?(ts|tsx|vue)' --languageFiles './src/locales/*.?(json|yml|yaml|js)' --ci",
|
"lint:vue-i18n-extract": "vue-i18n-extract report --vueFiles './src/**/*.?(ts|tsx|vue)' --languageFiles './src/locales/**/*.?(json|yml|yaml|js)' --ci",
|
||||||
"lint:stylelint": "stylelint \"**/*.{css,less,scss,vue}\" --fix --ignore-path .gitignore",
|
"lint:stylelint": "stylelint --fix --ignore-path .gitignore \"**/*.{css,less,scss,vue}\"",
|
||||||
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
||||||
"lint:eslint": "eslint . --fix",
|
"lint:eslint": "eslint . --fix",
|
||||||
"test:unit:DisableWatch": "vitest --run",
|
"test:unit:DisableWatch": "vitest --run",
|
||||||
@@ -27,14 +27,15 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"{server,src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
|
"{src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
|
||||||
"prettier --write",
|
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"oxlint --fix"
|
"oxlint --fix",
|
||||||
|
"prettier --write"
|
||||||
],
|
],
|
||||||
"{src,packages}/**/*.{css,less,scss,vue}": [
|
"{src,packages}/**/*.{css,less,scss,vue}": [
|
||||||
"stylelint --fix"
|
"stylelint --fix"
|
||||||
]
|
],
|
||||||
|
"{src/locales-utils,src/locales}/**/*": "node scripts/type-check-for-lint-staged.mjs"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
@@ -59,14 +60,17 @@
|
|||||||
"@unhead/vue": "^2.0.14",
|
"@unhead/vue": "^2.0.14",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"naive-ui": "^2.43.1",
|
"naive-ui": "^2.43.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primelocale": "^2.1.7",
|
"primelocale": "^2.1.7",
|
||||||
"primevue": "^4.3.9",
|
"primevue": "^4.3.9",
|
||||||
"ts-enum-util": "^4.1.0",
|
"ts-enum-util": "^4.1.0",
|
||||||
"utils4u": "^4.2.3",
|
"utils4u": "^4.2.3",
|
||||||
"vue": "^3.5.21",
|
"vue": "^3.5.21",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.1.12",
|
||||||
|
"vue-memoize-dict": "^1.1.3",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -86,6 +90,7 @@
|
|||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/html-minifier-terser": "^7.0.2",
|
"@types/html-minifier-terser": "^7.0.2",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.18.1",
|
"@types/node": "^22.18.1",
|
||||||
"@vant/auto-import-resolver": "^1.3.0",
|
"@vant/auto-import-resolver": "^1.3.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
@@ -119,7 +124,9 @@
|
|||||||
"stylelint-config-standard-scss": "^16.0.0",
|
"stylelint-config-standard-scss": "^16.0.0",
|
||||||
"stylelint-config-standard-vue": "^1.0.0",
|
"stylelint-config-standard-vue": "^1.0.0",
|
||||||
"stylelint-define-config": "^16.24.0",
|
"stylelint-define-config": "^16.24.0",
|
||||||
|
"svgo": "^4.0.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
|
"type-fest": "^5.1.0",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"unocss": "^66.5.1",
|
"unocss": "^66.5.1",
|
||||||
"unocss-preset-animations": "^1.2.1",
|
"unocss-preset-animations": "^1.2.1",
|
||||||
@@ -136,6 +143,7 @@
|
|||||||
"vite-plugin-vue-meta-layouts": "^0.6.1",
|
"vite-plugin-vue-meta-layouts": "^0.6.1",
|
||||||
"vite-plugin-webfont-dl": "^3.11.1",
|
"vite-plugin-webfont-dl": "^3.11.1",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
|
"vue-component-type-helpers": "^3.1.2",
|
||||||
"vue-i18n-extract": "^2.0.7",
|
"vue-i18n-extract": "^2.0.7",
|
||||||
"vue-macros": "3.1.1",
|
"vue-macros": "3.1.1",
|
||||||
"vue-tsc": "^3.1.0",
|
"vue-tsc": "^3.1.0",
|
||||||
|
|||||||
852
pnpm-lock.yaml
generated
852
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
15
scripts/type-check-for-lint-staged.mjs
Executable file
15
scripts/type-check-for-lint-staged.mjs
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type check script for lint-staged
|
||||||
|
* This script ignores file arguments passed by lint-staged and runs type-check on the entire project
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
|
const result = spawnSync('pnpm', ['run', 'type-check'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
26
src/App.vue
26
src/App.vue
@@ -1,26 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
|
||||||
import { darkTheme } from 'naive-ui';
|
import A2use from './utils/a2use.vue';
|
||||||
import { RouterView } from 'vue-router';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
// https://www.naiveui.com/zh-CN/light/docs/customize-theme
|
|
||||||
const themeOverrides: GlobalThemeOverrides = {
|
|
||||||
common: {},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DynamicDialog />
|
<DynamicDialog />
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
<Toast />
|
<Toast />
|
||||||
<n-config-provider
|
|
||||||
:theme-overrides
|
<AppNaiveUIProvider>
|
||||||
preflight-style-disabled
|
<!-- <RouterView /> -->
|
||||||
:theme="appStore.isDark ? darkTheme : null"
|
|
||||||
abstract
|
<A2use></A2use>
|
||||||
>
|
</AppNaiveUIProvider>
|
||||||
<RouterView />
|
|
||||||
</n-config-provider>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
58
src/AppNaiveUIProvider.vue
Normal file
58
src/AppNaiveUIProvider.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
|
import { darkTheme, dateZhCN, zhCN } from 'naive-ui';
|
||||||
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
import { createTextVNode } from 'vue';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
// https://www.naiveui.com/zh-CN/light/docs/customize-theme
|
||||||
|
const themeOverrides: GlobalThemeOverrides = {
|
||||||
|
common: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextHolder: FunctionalComponent = () => {
|
||||||
|
window.$nLoadingBar = useLoadingBar();
|
||||||
|
window.$nModal = useModal();
|
||||||
|
window.$nDialog = useDialog();
|
||||||
|
window.$nMessage = useMessage();
|
||||||
|
window.$nNotification = useNotification();
|
||||||
|
return createTextVNode();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
declare global {
|
||||||
|
export interface Window {
|
||||||
|
$nLoadingBar?: import('naive-ui').LoadingBarProviderInst;
|
||||||
|
$nModal?: import('naive-ui').ModalProviderInst;
|
||||||
|
$nDialog?: import('naive-ui').DialogProviderInst;
|
||||||
|
$nMessage?: import('naive-ui').MessageProviderInst;
|
||||||
|
$nNotification?: import('naive-ui').NotificationProviderInst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NConfigProvider
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:theme-overrides
|
||||||
|
preflight-style-disabled
|
||||||
|
:theme="appStore.isDark ? darkTheme : null"
|
||||||
|
abstract
|
||||||
|
>
|
||||||
|
<n-loading-bar-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<n-modal-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<slot></slot>
|
||||||
|
<ContextHolder />
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-modal-provider>
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
|
</NConfigProvider>
|
||||||
|
</template>
|
||||||
@@ -16,7 +16,7 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
return key;
|
return key;
|
||||||
},
|
},
|
||||||
fallbackRoot: true,
|
fallbackRoot: true,
|
||||||
messages: locales4RouteMessages,
|
messages: i18nRouteMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取路由表但是不包含布局路由
|
// 获取路由表但是不包含布局路由
|
||||||
@@ -48,67 +48,73 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
const menuMap = new Map<string, MenuOption>();
|
const menuMap = new Map<string, MenuOption>();
|
||||||
const rootMenus: MenuOption[] = [];
|
const rootMenus: MenuOption[] = [];
|
||||||
|
|
||||||
// 过滤和排序路由
|
// 过滤路由
|
||||||
const validRoutes = routes
|
const validRoutes = routes.filter((route) => {
|
||||||
.filter((route) => {
|
// 过滤掉不需要显示的路由
|
||||||
// 过滤掉不需要显示的路由
|
if (route.meta?.hideInMenu === true || route.meta?.layout === false) {
|
||||||
if (route.meta?.hideInMenu === true || route.meta?.layout === false) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
// 过滤掉通配符路径
|
||||||
// 过滤掉通配符路径
|
if (route.path.includes('*')) {
|
||||||
if (route.path.includes('*')) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
// 根据环境变量判断是否显示 /demos 开头的路由
|
||||||
// 根据环境变量判断是否显示 /demos 开头的路由
|
if (import.meta.env.VITE_APP_MENU_SHOW_DEMOS !== 'true' && route.path.startsWith('/demos')) {
|
||||||
if (import.meta.env.VITE_MENU_SHOW_DEMOS !== 'true' && route.path.startsWith('/demos')) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
});
|
||||||
})
|
|
||||||
// 排序路由,确保父路由总是在子路由之前,同级路由则根据 `meta.order` 排序
|
|
||||||
.sort((a: RouteRecordRaw, b: RouteRecordRaw) => {
|
|
||||||
const pathA = a.path;
|
|
||||||
const pathB = b.path;
|
|
||||||
const segmentsA = pathA.split('/').filter(Boolean);
|
|
||||||
const segmentsB = pathB.split('/').filter(Boolean);
|
|
||||||
const parentAPath = `/${segmentsA.slice(0, -1).join('/')}`;
|
|
||||||
const parentBPath = `/${segmentsB.slice(0, -1).join('/')}`;
|
|
||||||
|
|
||||||
// 如果不是同级路由,则按路径排序,确保父路由在前
|
// 排序路由:先按路径深度分组,再按 order 排序
|
||||||
if (parentAPath !== parentBPath) {
|
const sortedRoutes = validRoutes.slice().sort((a: RouteRecordRaw, b: RouteRecordRaw) => {
|
||||||
return pathA.localeCompare(pathB);
|
const pathA = a.path;
|
||||||
}
|
const pathB = b.path;
|
||||||
|
|
||||||
// 同级路由,处理 `meta.order`
|
// 1. 首先按路径深度排序(确保父路由在子路由之前)
|
||||||
const orderA = a.meta?.order;
|
const depthA = pathA.split('/').filter(Boolean).length;
|
||||||
const orderB = b.meta?.order;
|
const depthB = pathB.split('/').filter(Boolean).length;
|
||||||
const hasOrderA = orderA !== undefined;
|
if (depthA !== depthB) {
|
||||||
const hasOrderB = orderB !== undefined;
|
return depthA - depthB;
|
||||||
|
}
|
||||||
|
|
||||||
// 当一个有 order 而另一个没有时,有 order 的排在前面
|
// 2. 获取父路径,判断是否为同一父级下的路由
|
||||||
if (hasOrderA !== hasOrderB) {
|
const segmentsA = pathA.split('/').filter(Boolean);
|
||||||
return hasOrderA ? -1 : 1;
|
const segmentsB = pathB.split('/').filter(Boolean);
|
||||||
}
|
const parentA = segmentsA.length > 1 ? `/${segmentsA.slice(0, -1).join('/')}` : '/';
|
||||||
|
const parentB = segmentsB.length > 1 ? `/${segmentsB.slice(0, -1).join('/')}` : '/';
|
||||||
|
|
||||||
// 当两个都有 order 时,按 order 值升序排序
|
// 如果父路径不同,按父路径字母顺序排序
|
||||||
if (hasOrderA && hasOrderB) {
|
if (parentA !== parentB) {
|
||||||
const orderDiff = orderA - orderB;
|
return parentA.localeCompare(parentB);
|
||||||
if (orderDiff !== 0) {
|
}
|
||||||
return orderDiff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// order 相同或都没有 order,按路径字母顺序排序
|
// 3. 同一父级下的路由,按 order 排序
|
||||||
return pathA.localeCompare(pathB);
|
const orderA = a.meta?.order;
|
||||||
});
|
const orderB = b.meta?.order;
|
||||||
|
const hasOrderA = typeof orderA === 'number';
|
||||||
|
const hasOrderB = typeof orderB === 'number';
|
||||||
|
|
||||||
|
// 有 order 的排在没有 order 的前面
|
||||||
|
if (hasOrderA && !hasOrderB) return -1;
|
||||||
|
if (!hasOrderA && hasOrderB) return 1;
|
||||||
|
|
||||||
|
// 都有 order 时,按 order 数值升序排序
|
||||||
|
if (hasOrderA && hasOrderB) {
|
||||||
|
const diff = (orderA as number) - (orderB as number);
|
||||||
|
if (diff !== 0) return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// order 相同或都没有 order,按路径名字母顺序排序
|
||||||
|
return pathA.localeCompare(pathB);
|
||||||
|
});
|
||||||
|
|
||||||
// 构建菜单树
|
// 构建菜单树
|
||||||
for (const route of validRoutes) {
|
for (const route of sortedRoutes) {
|
||||||
const pathSegments = route.path.split('/').filter(Boolean);
|
const pathSegments = route.path.split('/').filter(Boolean);
|
||||||
const routeName = route.name as string;
|
const routeName = route.name as string;
|
||||||
|
|
||||||
let text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
|
let text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
|
||||||
if (import.meta.env.VITE_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
|
if (import.meta.env.VITE_APP_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
|
||||||
const order = String(route.meta.order).padStart(orderMaxLength, '0');
|
const order = String(route.meta.order).padStart(orderMaxLength, '0');
|
||||||
text = `${order}. ${text}`;
|
text = `${order}. ${text}`;
|
||||||
}
|
}
|
||||||
@@ -147,11 +153,33 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加调试日志
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.debug(
|
||||||
|
'排序后的路由:',
|
||||||
|
sortedRoutes.map((route) => ({
|
||||||
|
path: route.path,
|
||||||
|
name: route.name,
|
||||||
|
order: route.meta?.order,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return rootMenus;
|
return rootMenus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('原始路由:', JSON.stringify(routes, null, 0));
|
if (import.meta.env.DEV) {
|
||||||
// console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0));
|
console.debug(
|
||||||
|
'原始路由:',
|
||||||
|
routes.map((route) => ({
|
||||||
|
path: route.path,
|
||||||
|
name: route.name,
|
||||||
|
order: route.meta?.order,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
console.debug('转换后的菜单:', options.value);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
selectedKey,
|
selectedKey,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const appStore = useAppStore();
|
|||||||
const themeLabels: Record<AppThemeMode, string> = {
|
const themeLabels: Record<AppThemeMode, string> = {
|
||||||
light: '浅色',
|
light: '浅色',
|
||||||
dark: '深色',
|
dark: '深色',
|
||||||
system: '跟随系统',
|
auto: '跟随系统',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const themeLabels: Record<AppThemeMode, string> = {
|
|||||||
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
||||||
{{ themeLabels[appStore.themeMode] }}
|
{{ themeLabels[appStore.themeMode] }}
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton quaternary @click="appStore.cycleTheme">
|
<NButton quaternary @click="appStore.cycleTheme()">
|
||||||
<icon-line-md-sunny-filled-loop-to-moon-filled-loop-transition
|
<icon-line-md-sunny-filled-loop-to-moon-filled-loop-transition
|
||||||
v-if="appStore.themeMode === 'light'"
|
v-if="appStore.themeMode === 'light'"
|
||||||
w-4.5
|
w-4.5
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store-auto-imports';
|
||||||
|
|
||||||
const menuInstRef = useTemplateRef('menuInstRef');
|
const menuInstRef = useTemplateRef('menuInstRef');
|
||||||
const { options, selectedKey } = useMetaLayoutsNMenuOptions({
|
const { options, selectedKey } = useMetaLayoutsNMenuOptions({
|
||||||
@@ -12,13 +12,13 @@ const appStore = useAppStore();
|
|||||||
<template>
|
<template>
|
||||||
<!-- @update:value="handleMenuUpdate" -->
|
<!-- @update:value="handleMenuUpdate" -->
|
||||||
<NMenu
|
<NMenu
|
||||||
mode="vertical"
|
|
||||||
ref="menuInstRef"
|
ref="menuInstRef"
|
||||||
|
v-model:value="selectedKey"
|
||||||
|
mode="vertical"
|
||||||
:collapsed="appStore.sidebarCollapsed"
|
:collapsed="appStore.sidebarCollapsed"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:icon-size="20"
|
:icon-size="20"
|
||||||
:collapsed-icon-size="24"
|
:collapsed-icon-size="24"
|
||||||
v-model:value="selectedKey"
|
|
||||||
:options="options"
|
:options="options"
|
||||||
:inverted="false"
|
:inverted="false"
|
||||||
:root-indent="32"
|
:root-indent="32"
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ const appStore = useAppStore();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
|
v-model:sider-collapse="appStore.sidebarCollapsed"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:footer-visible="!false"
|
:footer-visible="!false"
|
||||||
:tab-visible="!false"
|
:tab-visible="!false"
|
||||||
scroll-mode="content"
|
scroll-mode="content"
|
||||||
:is-mobile="appStore.isMobile"
|
:is-mobile="appStore.isMobile"
|
||||||
v-model:sider-collapse="appStore.sidebarCollapsed"
|
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<BaseLayoutHeader />
|
<BaseLayoutHeader />
|
||||||
|
|||||||
31
src/locales-utils/i18n-auto-imports.ts
Normal file
31
src/locales-utils/i18n-auto-imports.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n#static-bundle-importing
|
||||||
|
* All i18n resources specified in the plugin `include` option can be loaded
|
||||||
|
* at once using the import syntax
|
||||||
|
*/
|
||||||
|
import messages from '@intlify/unplugin-vue-i18n/messages';
|
||||||
|
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const locale = useLocalStorage<string>('app-locale', navigator.language);
|
||||||
|
watchEffect(() => {
|
||||||
|
window.document.documentElement.setAttribute('lang', locale.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
|
||||||
|
export const i18nInstance = createI18n({
|
||||||
|
legacy: false, // you must set `false`, to use Composition API
|
||||||
|
locale: locale.value,
|
||||||
|
fallbackRoot: false,
|
||||||
|
// flatJson: true,
|
||||||
|
missing: (locale, key /* , instance, type */) => {
|
||||||
|
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
|
||||||
|
return `[${key}]`;
|
||||||
|
},
|
||||||
|
missingWarn: !true,
|
||||||
|
fallbackWarn: !true,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
locale.value = i18nInstance.global.locale.value;
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# `locales-4-route`
|
# route-messages
|
||||||
|
|
||||||
此目录存放专门用于**路由名称**的国际化(i18n)消息。这些消息通过一套自定义的编译时安全机制,为应用的导航菜单提供标题。
|
此目录存放专门用于**路由名称**的国际化(i18n)消息。这些消息通过一套自定义的编译时安全机制,为应用的导航菜单提供标题。
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
3. **编译时检查**:此目录下的每个语言环境文件(如 `en-US.ts`)都必须使用 `satisfies PageTitleLocalizations` 来进行类型断言。
|
3. **编译时检查**:此目录下的每个语言环境文件(如 `en-US.ts`)都必须使用 `satisfies PageTitleLocalizations` 来进行类型断言。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/locales-4-route/en-US.ts
|
// ./en-US.ts
|
||||||
export default { ... } satisfies PageTitleLocalizations;
|
export default { ... } satisfies PageTitleLocalizations;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,13 +52,13 @@
|
|||||||
使用上一步中自动生成的 `name` 作为键,在此目录的每个语言文件中添加翻译。
|
使用上一步中自动生成的 `name` 作为键,在此目录的每个语言文件中添加翻译。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// src/locales-4-route/zh-CN.ts
|
// ./zh-CN.ts
|
||||||
export default {
|
export default {
|
||||||
// ... 其他翻译
|
// ... 其他翻译
|
||||||
DemosApiDemo: 'API 演示',
|
DemosApiDemo: 'API 演示',
|
||||||
} satisfies PageTitleLocalizations;
|
} satisfies PageTitleLocalizations;
|
||||||
|
|
||||||
// src/locales-4-route/en-US.ts
|
// ./en-US.ts
|
||||||
export default {
|
export default {
|
||||||
// ... 其他翻译
|
// ... 其他翻译
|
||||||
DemosApiDemo: 'API Demo',
|
DemosApiDemo: 'API Demo',
|
||||||
@@ -5,6 +5,9 @@ export default {
|
|||||||
DemosApiDemo: 'API Demo',
|
DemosApiDemo: 'API Demo',
|
||||||
DemosCounterDemo: 'Counter Demo',
|
DemosCounterDemo: 'Counter Demo',
|
||||||
DemosI18nDemo: 'i18n Demo',
|
DemosI18nDemo: 'i18n Demo',
|
||||||
|
DemosNaiveUiDemo: 'Naive UI Demo',
|
||||||
|
DemosPrimevueDemo: 'PrimeVue Demo',
|
||||||
DemosWebsocketDemo: 'WebSocket Demo',
|
DemosWebsocketDemo: 'WebSocket Demo',
|
||||||
Home: 'Home',
|
Home: 'Home',
|
||||||
} as const satisfies PageTitleLocalizations;
|
Login: 'Login',
|
||||||
|
} satisfies PageTitleLocalizations;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { I18nOptions } from 'vue-i18n';
|
import type { I18nOptions } from 'vue-i18n';
|
||||||
|
|
||||||
const modules = import.meta.glob(['./*.ts', '!./_messages-auto-imports.ts'], {
|
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
|
||||||
eager: true,
|
eager: true,
|
||||||
import: 'default',
|
import: 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
type MessageType = Record<string, string>;
|
type MessageType = Record<string, string>;
|
||||||
|
|
||||||
export const locales4RouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
|
export const i18nRouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
|
||||||
(messages, [path, mod]) => {
|
(messages, [path, mod]) => {
|
||||||
const locale = path.replace(/(\.\/|\.ts)/g, '');
|
const locale = path.replace(/(\.\/|\.ts)/g, '');
|
||||||
messages[locale] = mod as MessageType;
|
messages[locale] = mod as MessageType;
|
||||||
@@ -5,6 +5,9 @@ export default {
|
|||||||
DemosApiDemo: 'API 调用示例',
|
DemosApiDemo: 'API 调用示例',
|
||||||
DemosCounterDemo: '点击计数器',
|
DemosCounterDemo: '点击计数器',
|
||||||
DemosI18nDemo: '国际化示例',
|
DemosI18nDemo: '国际化示例',
|
||||||
|
DemosNaiveUiDemo: 'Naive UI 组件示例',
|
||||||
|
DemosPrimevueDemo: 'PrimeVue 组件示例',
|
||||||
DemosWebsocketDemo: 'WebSocket 示例',
|
DemosWebsocketDemo: 'WebSocket 示例',
|
||||||
Home: '首页',
|
Home: '首页',
|
||||||
} as const satisfies PageTitleLocalizations;
|
Login: '登录',
|
||||||
|
} satisfies PageTitleLocalizations;
|
||||||
10
src/locales/demo/en-US.json
Normal file
10
src/locales/demo/en-US.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"i18n-demo": {
|
||||||
|
"title": "Vue I18n Demo",
|
||||||
|
"current-language": "Current Language",
|
||||||
|
"change-language": "Change Language",
|
||||||
|
"hello": "Hello, {name}!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/locales/demo/zh-CN.json
Normal file
10
src/locales/demo/zh-CN.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"i18n-demo": {
|
||||||
|
"title": "Vue I18n 示例",
|
||||||
|
"current-language": "当前语言",
|
||||||
|
"change-language": "切换语言",
|
||||||
|
"hello": "你好, {name}!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1 @@
|
|||||||
{
|
{}
|
||||||
"page": {
|
|
||||||
"i18n-demo": {
|
|
||||||
"title": "Vue I18n Demo",
|
|
||||||
"current-language": "Current Language",
|
|
||||||
"change-language": "Change Language",
|
|
||||||
"hello": "Hello, {name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
{
|
{}
|
||||||
"page": {
|
|
||||||
"i18n-demo": {
|
|
||||||
"title": "Vue I18n 示例",
|
|
||||||
"current-language": "当前语言",
|
|
||||||
"change-language": "切换语言",
|
|
||||||
"hello": "你好, {name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { setupPlugins } from './plugins';
|
|||||||
|
|
||||||
consola.level = LogLevels.verbose;
|
consola.level = LogLevels.verbose;
|
||||||
|
|
||||||
/* `import.meta.glob(${g}, { eager: ${isSync} })`; */
|
const autoInstallModules = import.meta.glob('./plugins/!(index).ts', {
|
||||||
const autoInstallModules = import.meta.glob('./plugins/!(index).ts', { eager: true });
|
eager: true /* true 为同步,false 为异步 */,
|
||||||
|
});
|
||||||
|
|
||||||
setupPlugins(createApp(App), autoInstallModules).mount('#app');
|
const app = setupPlugins(createApp(App), autoInstallModules);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 280));
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
5
src/pages/Login.page.vue
Normal file
5
src/pages/Login.page.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>Login</div>
|
||||||
|
</template>
|
||||||
@@ -49,10 +49,10 @@ const callApi = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="callApi"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:aria-label="loading ? '正在调用API' : '调用API接口'"
|
:aria-label="loading ? '正在调用API' : '调用API接口'"
|
||||||
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-3 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-3 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="callApi"
|
||||||
>
|
>
|
||||||
<span v-if="loading" class="flex items-center justify-center">
|
<span v-if="loading" class="flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ const resetCount = () => {
|
|||||||
<div class="w-full flex flex-col gap-3">
|
<div class="w-full flex flex-col gap-3">
|
||||||
<!-- 原生按钮 (带 touch 事件) -->
|
<!-- 原生按钮 (带 touch 事件) -->
|
||||||
<button
|
<button
|
||||||
|
class="w-full bg-gradient-to-br from-orange-500 via-orange-600 to-red-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-orange-600 hover:via-orange-700 hover:to-red-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
||||||
@touchstart="() => {}"
|
@touchstart="() => {}"
|
||||||
@touchend="() => {}"
|
@touchend="() => {}"
|
||||||
@click="incrementCount"
|
@click="incrementCount"
|
||||||
class="w-full bg-gradient-to-br from-orange-500 via-orange-600 to-red-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-orange-600 hover:via-orange-700 hover:to-red-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
|
||||||
>
|
>
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -111,8 +111,8 @@ const resetCount = () => {
|
|||||||
|
|
||||||
<!-- 原生按钮 (无 touch 事件) -->
|
<!-- 原生按钮 (无 touch 事件) -->
|
||||||
<button
|
<button
|
||||||
@click="incrementCount"
|
|
||||||
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
||||||
|
@click="incrementCount"
|
||||||
>
|
>
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -129,13 +129,13 @@ const resetCount = () => {
|
|||||||
|
|
||||||
<!-- Naive UI 按钮 -->
|
<!-- Naive UI 按钮 -->
|
||||||
<n-button
|
<n-button
|
||||||
@click="incrementCount"
|
|
||||||
type="warning"
|
type="warning"
|
||||||
size="large"
|
size="large"
|
||||||
block
|
block
|
||||||
strong
|
strong
|
||||||
secondary
|
secondary
|
||||||
class="text-lg"
|
class="text-lg"
|
||||||
|
@click="incrementCount"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -152,9 +152,9 @@ const resetCount = () => {
|
|||||||
|
|
||||||
<!-- 重置按钮 -->
|
<!-- 重置按钮 -->
|
||||||
<button
|
<button
|
||||||
@click="resetCount"
|
|
||||||
:disabled="clickCount === 0"
|
:disabled="clickCount === 0"
|
||||||
class="w-full bg-gradient-to-br from-gray-500 via-gray-600 to-gray-700 text-white font-semibold py-3 px-6 rounded-xl hover:from-gray-600 hover:via-gray-700 hover:to-gray-800 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02]"
|
class="w-full bg-gradient-to-br from-gray-500 via-gray-600 to-gray-700 text-white font-semibold py-3 px-6 rounded-xl hover:from-gray-600 hover:via-gray-700 hover:to-gray-800 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02]"
|
||||||
|
@click="resetCount"
|
||||||
>
|
>
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePage({
|
definePage({ meta: { order: 1 } });
|
||||||
meta: {
|
const { t, locale } = useI18n({});
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { t, locale } = useI18n();
|
|
||||||
|
|
||||||
function setLocale(newLocale: 'en-US' | 'zh-CN') {
|
function setLocale(newLocale: 'en-US' | 'zh-CN') {
|
||||||
locale.value = newLocale;
|
i18nInstance.global.locale.value = newLocale;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
99
src/pages/demos/naive-ui-demo.page.vue
Normal file
99
src/pages/demos/naive-ui-demo.page.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDialog, useMessage, useModal } from 'naive-ui';
|
||||||
|
import type { MessageType } from 'naive-ui';
|
||||||
|
|
||||||
|
definePage({ meta: {} });
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[];
|
||||||
|
const dialogTypes = ['info', 'success', 'warning', 'error'] as const;
|
||||||
|
|
||||||
|
const openAllMessages = () => {
|
||||||
|
messageTypes.forEach((type, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
message[type](`${index + 1}. 消息内容`, {
|
||||||
|
duration: 3000,
|
||||||
|
closable: true,
|
||||||
|
});
|
||||||
|
}, index * 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDialog = (type: (typeof dialogTypes)[number]) => {
|
||||||
|
dialog[type]({
|
||||||
|
title: `${type.charAt(0).toUpperCase() + type.slice(1)} 弹窗`,
|
||||||
|
content: '这是一个命令式 API 创建的弹窗示例。',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
message.success('点击了确定');
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
message.error('点击了取消');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
modal.create({
|
||||||
|
title: '命令式 Modal 示例',
|
||||||
|
content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。',
|
||||||
|
preset: 'dialog',
|
||||||
|
maskClosable: false,
|
||||||
|
onPositiveClick: () => {
|
||||||
|
message.success('点击了确定');
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
message.error('点击了取消');
|
||||||
|
},
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="naive-ui-demo-page">
|
||||||
|
<NCard>
|
||||||
|
<template #header>Naive UI 组件演示</template>
|
||||||
|
<NAlert title="信息" type="info" :bordered="false">
|
||||||
|
演示 Naive UI 各种组件的使用方法和功能特性
|
||||||
|
</NAlert>
|
||||||
|
|
||||||
|
<NCard title="Message 消息" class="mt-4">
|
||||||
|
<NSpace>
|
||||||
|
<NButton
|
||||||
|
v-for="(type, index) in messageTypes"
|
||||||
|
:key="type"
|
||||||
|
@click="
|
||||||
|
message[type](`${index + 1}. 消息内容`, {
|
||||||
|
duration: 3000,
|
||||||
|
closable: true,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ `${index + 1}. ${type}` }}
|
||||||
|
</NButton>
|
||||||
|
<NButton @click="openAllMessages"> 一键打开所有 </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<NCard title="Dialog 弹窗 (命令式 API)" class="mt-4">
|
||||||
|
<NSpace>
|
||||||
|
<NButton v-for="type in dialogTypes" :key="type" @click="openDialog(type)">
|
||||||
|
{{ type }}
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<NCard title="Modal 弹窗 (命令式 API)" class="mt-4">
|
||||||
|
<NSpace>
|
||||||
|
<NButton @click="openModal"> 打开 Modal </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
61
src/pages/demos/primevue-demo.page.vue
Normal file
61
src/pages/demos/primevue-demo.page.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ToastMessageOptions } from 'primevue/toast';
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tostSeverities = [
|
||||||
|
'secondary',
|
||||||
|
'success',
|
||||||
|
'info' /* 默认 */,
|
||||||
|
'warn',
|
||||||
|
'error',
|
||||||
|
'contrast',
|
||||||
|
undefined,
|
||||||
|
] satisfies ToastMessageOptions['severity'][];
|
||||||
|
|
||||||
|
const openAllToasts = () => {
|
||||||
|
tostSeverities.forEach((severity, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
ToastService.add({
|
||||||
|
severity,
|
||||||
|
summary: `severity: ${severity ?? 'default'}`,
|
||||||
|
life: 3000,
|
||||||
|
detail: `${index + 1}. 消息内容`,
|
||||||
|
});
|
||||||
|
}, index * 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="prime-vue-demo-page">
|
||||||
|
<Card>
|
||||||
|
<template #title>PrimeVue 组件演示</template>
|
||||||
|
<template #content>
|
||||||
|
<Message severity="info">演示 PrimeVue 各种组件的使用方法和功能特性</Message>
|
||||||
|
|
||||||
|
<Panel header="Toast 消息" class="mt-1.5">
|
||||||
|
<div flex="~ wrap" gap="4">
|
||||||
|
<Button
|
||||||
|
v-for="(severity, index) in tostSeverities"
|
||||||
|
:key="severity ?? 'default'"
|
||||||
|
@click="
|
||||||
|
ToastService.add({
|
||||||
|
severity: severity,
|
||||||
|
summary: `severity: ${severity ?? 'default'}`,
|
||||||
|
life: 3000,
|
||||||
|
detail: '消息内容',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ `${index + 1}. ${severity ?? 'default'}` }}
|
||||||
|
</Button>
|
||||||
|
<Button @click="openAllToasts"> 一键打开所有 </Button>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -227,12 +227,12 @@ onUnmounted(() => {
|
|||||||
<!-- 控制按钮 -->
|
<!-- 控制按钮 -->
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="connectWebSocket"
|
|
||||||
:disabled="wsConnected || wsLoading"
|
:disabled="wsConnected || wsLoading"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
wsLoading ? '正在连接WebSocket' : wsConnected ? 'WebSocket已连接' : '连接WebSocket'
|
wsLoading ? '正在连接WebSocket' : wsConnected ? 'WebSocket已连接' : '连接WebSocket'
|
||||||
"
|
"
|
||||||
class="flex items-center justify-center bg-gradient-to-br from-green-500 via-green-600 to-emerald-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-green-600 hover:via-green-700 hover:to-emerald-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center justify-center bg-gradient-to-br from-green-500 via-green-600 to-emerald-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-green-600 hover:via-green-700 hover:to-emerald-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="connectWebSocket"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="wsLoading"
|
v-if="wsLoading"
|
||||||
@@ -259,10 +259,10 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="disconnectWebSocket"
|
|
||||||
:disabled="!wsConnected || wsLoading"
|
:disabled="!wsConnected || wsLoading"
|
||||||
:aria-label="!wsConnected ? 'WebSocket未连接' : '断开WebSocket连接'"
|
:aria-label="!wsConnected ? 'WebSocket未连接' : '断开WebSocket连接'"
|
||||||
class="flex items-center justify-center bg-gradient-to-br from-red-500 via-red-600 to-pink-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-red-600 hover:via-red-700 hover:to-pink-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center justify-center bg-gradient-to-br from-red-500 via-red-600 to-pink-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-red-600 hover:via-red-700 hover:to-pink-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="disconnectWebSocket"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -284,19 +284,19 @@ onUnmounted(() => {
|
|||||||
<input
|
<input
|
||||||
id="messageInput"
|
id="messageInput"
|
||||||
v-model="messageInput"
|
v-model="messageInput"
|
||||||
@keyup.enter="sendMessage"
|
|
||||||
placeholder="输入要发送的消息..."
|
placeholder="输入要发送的消息..."
|
||||||
:disabled="!wsConnected"
|
:disabled="!wsConnected"
|
||||||
:aria-describedby="!wsConnected ? 'ws-status' : undefined"
|
:aria-describedby="!wsConnected ? 'ws-status' : undefined"
|
||||||
class="flex-1 w-full border-2 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:text-gray-500 transition-all duration-300 border-gray-200 dark:border-gray-600 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-100 disabled:bg-gray-100/60 dark:disabled:bg-gray-800/60 hover:border-gray-300 dark:hover:border-gray-500"
|
class="flex-1 w-full border-2 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:text-gray-500 transition-all duration-300 border-gray-200 dark:border-gray-600 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-100 disabled:bg-gray-100/60 dark:disabled:bg-gray-800/60 hover:border-gray-300 dark:hover:border-gray-500"
|
||||||
|
@keyup.enter="sendMessage"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="sendMessage"
|
|
||||||
:disabled="!canSendMessage"
|
:disabled="!canSendMessage"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
!wsConnected ? 'WebSocket未连接' : canSendMessage ? '发送消息' : '请输入消息内容'
|
!wsConnected ? 'WebSocket未连接' : canSendMessage ? '发送消息' : '请输入消息内容'
|
||||||
"
|
"
|
||||||
class="flex items-center bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-indigo-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-indigo-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="sendMessage"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -309,11 +309,11 @@ onUnmounted(() => {
|
|||||||
发送
|
发送
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="sendMockData"
|
|
||||||
:disabled="!wsConnected"
|
:disabled="!wsConnected"
|
||||||
:aria-label="!wsConnected ? 'WebSocket未连接' : '发送随机模拟数据'"
|
:aria-label="!wsConnected ? 'WebSocket未连接' : '发送随机模拟数据'"
|
||||||
class="flex items-center bg-gradient-to-br from-purple-500 via-purple-600 to-violet-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-purple-600 hover:via-purple-700 hover:to-violet-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center bg-gradient-to-br from-purple-500 via-purple-600 to-violet-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-purple-600 hover:via-purple-700 hover:to-violet-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
:title="!wsConnected ? 'WebSocket未连接时不可用' : '发送随机模拟数据'"
|
:title="!wsConnected ? 'WebSocket未连接时不可用' : '发送随机模拟数据'"
|
||||||
|
@click="sendMockData"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -354,9 +354,9 @@ onUnmounted(() => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<button
|
||||||
@click="exportMessages"
|
|
||||||
class="text-xs text-gray-500 hover:text-blue-500 transition-colors duration-200 flex items-center"
|
class="text-xs text-gray-500 hover:text-blue-500 transition-colors duration-200 flex items-center"
|
||||||
title="导出消息"
|
title="导出消息"
|
||||||
|
@click="exportMessages"
|
||||||
>
|
>
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -369,9 +369,9 @@ onUnmounted(() => {
|
|||||||
导出
|
导出
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="clearMessages"
|
|
||||||
class="text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 flex items-center"
|
class="text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 flex items-center"
|
||||||
title="清空消息"
|
title="清空消息"
|
||||||
|
@click="clearMessages"
|
||||||
>
|
>
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
|||||||
{
|
{
|
||||||
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
||||||
createNProgressGuard(router);
|
createNProgressGuard(router);
|
||||||
createLogGuard(router);
|
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
|
||||||
Object.assign(globalThis, { stack: createStackGuard(router) });
|
Object.assign(globalThis, { stack: createStackGuard(router) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,3 @@
|
|||||||
/* https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n#static-bundle-importing
|
|
||||||
* All i18n resources specified in the plugin `include` option can be loaded
|
|
||||||
* at once using the import syntax
|
|
||||||
*/
|
|
||||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
|
app.use(i18nInstance);
|
||||||
const i18n = createI18n({
|
|
||||||
legacy: false, // you must set `false`, to use Composition API
|
|
||||||
locale: navigator.language,
|
|
||||||
fallbackRoot: false,
|
|
||||||
// flatJson: true,
|
|
||||||
missing: (locale, key /* , instance, type */) => {
|
|
||||||
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
|
|
||||||
return `[${key}]`;
|
|
||||||
},
|
|
||||||
missingWarn: !true,
|
|
||||||
fallbackWarn: !true,
|
|
||||||
messages,
|
|
||||||
});
|
|
||||||
app.use(i18n);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Aura from '@primeuix/themes/aura';
|
|||||||
import zhCN from 'primelocale/zh-CN.json';
|
import zhCN from 'primelocale/zh-CN.json';
|
||||||
import PrimeVue from 'primevue/config';
|
import PrimeVue from 'primevue/config';
|
||||||
import StyleClass from 'primevue/styleclass';
|
import StyleClass from 'primevue/styleclass';
|
||||||
|
import ToastService from 'primevue/toastservice';
|
||||||
|
|
||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
app.directive('styleclass', StyleClass);
|
app.directive('styleclass', StyleClass);
|
||||||
@@ -25,4 +26,5 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
|||||||
preset: Aura,
|
preset: Aura,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
app.use(ToastService);
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/stores/app-store-auto-imports.ts
Normal file
42
src/stores/app-store-auto-imports.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useLocalStorage, useMediaQuery } from '@vueuse/core';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
// >>>>>
|
||||||
|
// https://vueuse.org/core/useColorMode/#advanced-usage
|
||||||
|
const { system, store: themeMode } = useColorMode({
|
||||||
|
modes: { light: '', dark: 'app-dark', auto: '' },
|
||||||
|
disableTransition: false,
|
||||||
|
});
|
||||||
|
const { state, next: cycleTheme } = useCycleList(['light', 'dark', 'auto'] as const, {
|
||||||
|
initialValue: themeMode,
|
||||||
|
});
|
||||||
|
watchEffect(() => (themeMode.value = state.value));
|
||||||
|
export type AppThemeMode = typeof themeMode.value;
|
||||||
|
// <<<<<
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', () => {
|
||||||
|
// 侧边栏展开/收起状态
|
||||||
|
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
|
||||||
|
const toggleSidebar = useToggle(sidebarCollapsed);
|
||||||
|
|
||||||
|
// 主题模式
|
||||||
|
const actualTheme = computed(() => (themeMode.value === 'auto' ? system.value : themeMode.value));
|
||||||
|
const isDark = computed(() => actualTheme.value === 'dark');
|
||||||
|
|
||||||
|
// 是否是移动端
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeMode,
|
||||||
|
isDark,
|
||||||
|
isMobile,
|
||||||
|
cycleTheme,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { useLocalStorage, useMediaQuery, usePreferredColorScheme } from '@vueuse/core';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { computed, watch } from 'vue';
|
|
||||||
|
|
||||||
export const APP_THEME_MODES = ['light', 'dark', 'system'] as const;
|
|
||||||
export type AppThemeMode = (typeof APP_THEME_MODES)[number];
|
|
||||||
|
|
||||||
const DARK_CLASS = 'app-dark';
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', () => {
|
|
||||||
const themeMode = useLocalStorage<AppThemeMode>('app-theme-mode', 'system');
|
|
||||||
const preferredColor = usePreferredColorScheme();
|
|
||||||
|
|
||||||
// 侧边栏展开/收起状态
|
|
||||||
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
|
|
||||||
|
|
||||||
// 计算实际使用的主题
|
|
||||||
const actualTheme = computed(() =>
|
|
||||||
themeMode.value === 'system'
|
|
||||||
? preferredColor.value === 'dark'
|
|
||||||
? 'dark'
|
|
||||||
: 'light'
|
|
||||||
: themeMode.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 是否是暗色主题
|
|
||||||
const isDark = computed(() => actualTheme.value === 'dark');
|
|
||||||
|
|
||||||
// 是否是移动端
|
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
||||||
|
|
||||||
// 更新 DOM 类名
|
|
||||||
function updateDomClass() {
|
|
||||||
document.documentElement.classList.toggle(DARK_CLASS, isDark.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 循环切换主题
|
|
||||||
function cycleTheme() {
|
|
||||||
const currentIndex = APP_THEME_MODES.indexOf(themeMode.value);
|
|
||||||
const nextIndex = (currentIndex + 1) % APP_THEME_MODES.length;
|
|
||||||
themeMode.value = APP_THEME_MODES[nextIndex]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换侧边栏展开/收起
|
|
||||||
function toggleSidebar() {
|
|
||||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听主题变化,更新 DOM
|
|
||||||
watch(isDark, updateDomClass, { immediate: true });
|
|
||||||
|
|
||||||
return {
|
|
||||||
themeMode,
|
|
||||||
isDark,
|
|
||||||
isMobile,
|
|
||||||
cycleTheme,
|
|
||||||
sidebarCollapsed,
|
|
||||||
toggleSidebar,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
||||||
|
|
||||||
//
|
import 'primeicons/primeicons.css';
|
||||||
|
import './reset-primevue.css';
|
||||||
|
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
|
|||||||
8
src/styles/reset-primevue.css
Normal file
8
src/styles/reset-primevue.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.p-confirmdialog,
|
||||||
|
.p-toast {
|
||||||
|
max-width: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-toast .p-toast-message-text {
|
||||||
|
margin-top: -0.2rem;
|
||||||
|
}
|
||||||
90
src/utils/a2use.vue
Normal file
90
src/utils/a2use.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { get, set } from 'lodash-es';
|
||||||
|
|
||||||
|
const { formValue, SafeNForm, SafeNFormItem, formRef } = useSafeNForm({
|
||||||
|
initialFormValue: {
|
||||||
|
/* ⚠️:
|
||||||
|
如果没使用`SafeNFormItem`,
|
||||||
|
这里`user`对象没有手动初始化的话,将会报错:
|
||||||
|
`can't access property "name", $setup.formValue.user is undefined`
|
||||||
|
*/
|
||||||
|
user: {
|
||||||
|
name: '',
|
||||||
|
age: 0,
|
||||||
|
},
|
||||||
|
phone: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSetUserName() {
|
||||||
|
set(formValue.value, 'user.name', 'Alice');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleValidateClick() {
|
||||||
|
formRef.value?.validate((errors) => {
|
||||||
|
if (!errors) {
|
||||||
|
window.$nMessage!.success('Valid');
|
||||||
|
} else {
|
||||||
|
console.log(errors);
|
||||||
|
window.$nMessage!.error('Invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div p-4>
|
||||||
|
<div border>
|
||||||
|
<div>
|
||||||
|
<pre>formValue: {{ JSON.stringify(formValue, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
<div>get(formValue, 'user.name'): {{ get(formValue, 'user.name') }}</div>
|
||||||
|
</div>
|
||||||
|
<n-space mt-4>
|
||||||
|
<n-button @click="handleSetUserName">Set user.name</n-button>
|
||||||
|
</n-space>
|
||||||
|
|
||||||
|
<n-space item-class="flex-1">
|
||||||
|
<n-card title="SafeForm" mt-4>
|
||||||
|
<SafeNForm label-placement="left" label-width="auto">
|
||||||
|
<n-form-item
|
||||||
|
label="姓名"
|
||||||
|
path="user.name"
|
||||||
|
:rule="{ required: true, message: '请输入姓名', trigger: ['input'] }"
|
||||||
|
>
|
||||||
|
<n-input v-model:value="formValue.user.name" placeholder="输入姓名" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<SafeNFormItem
|
||||||
|
#default="{ value, setValue }"
|
||||||
|
:rule="{ required: true, message: '请输入姓名', trigger: ['input'] }"
|
||||||
|
label="姓名"
|
||||||
|
path="user.name"
|
||||||
|
>
|
||||||
|
<NInput :value="value" placeholder="SafeNFormItem" @update:value="setValue" />
|
||||||
|
</SafeNFormItem>
|
||||||
|
|
||||||
|
<n-form-item
|
||||||
|
label="电话号码"
|
||||||
|
path="phone"
|
||||||
|
:rule="{ required: true, message: '请输入电话号码', trigger: ['blur'] }"
|
||||||
|
>
|
||||||
|
<n-input v-model:value="formValue.phone" placeholder="电话号码" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<SafeNFormItem
|
||||||
|
label="电话号码"
|
||||||
|
path="phone"
|
||||||
|
:rule="{ required: true, message: '请输入电话号码', trigger: ['blur'] }"
|
||||||
|
>
|
||||||
|
<!-- 如果没有提供插槽,会默认渲染一个`<NInput>` -->
|
||||||
|
</SafeNFormItem>
|
||||||
|
|
||||||
|
<n-form-item>
|
||||||
|
<n-button attr-type="button" @click="handleValidateClick"> 验证 </n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</SafeNForm>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
121
src/utils/use-safe-n-form-auto-imports.tsx
Normal file
121
src/utils/use-safe-n-form-auto-imports.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* https://www.naiveui.com/zh-CN/os-theme/components/form
|
||||||
|
*
|
||||||
|
* FIXME: `NForm` 和 `NFormItem` 的 slots 还没有实现。`NFormItemGi`组件。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get, set } from 'lodash-es';
|
||||||
|
import type { FormInst, FormItemProps, FormProps } from 'naive-ui';
|
||||||
|
import { NForm, NFormItem, formItemProps, NInput, formProps } from 'naive-ui';
|
||||||
|
import type { Get, Paths } from 'type-fest';
|
||||||
|
import type { SlotsType } from 'vue';
|
||||||
|
import { Comment } from 'vue';
|
||||||
|
|
||||||
|
type UseSafeNFormOptions<FormValue> = {
|
||||||
|
initialFormValue?: FormValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSafeNForm<T extends Record<string, any> = Record<string, unknown>>(
|
||||||
|
options: UseSafeNFormOptions<T> = {},
|
||||||
|
) {
|
||||||
|
const formRef = ref<FormInst | null>(null);
|
||||||
|
const formValue = ref<T>(structuredClone(toRaw(options.initialFormValue)) || ({} as T));
|
||||||
|
|
||||||
|
// 创建类型安全的 Form 组件
|
||||||
|
type SafeNFormProps = FormProps;
|
||||||
|
|
||||||
|
type SafeNFormSlots = SlotsType<{
|
||||||
|
default?: { count?: number };
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const SafeNForm = defineComponent<SafeNFormProps, /* Emits */ [], /* EE */ never, SafeNFormSlots>(
|
||||||
|
(props, ctx) => {
|
||||||
|
return () => (
|
||||||
|
<NForm
|
||||||
|
{...props}
|
||||||
|
model={formValue.value}
|
||||||
|
ref={(inst) => {
|
||||||
|
formRef.value = inst as unknown as FormInst;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ctx.slots.default?.({})}
|
||||||
|
</NForm>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SafeNForm',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: formProps,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// <<<<<
|
||||||
|
|
||||||
|
// >>>>> 创建类型安全的 FormItem 组件
|
||||||
|
type SafeNFormItemProps<P extends Paths<T> & string> = FormItemProps & {
|
||||||
|
path: P;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SafeNFormItemDefaultSlot<P extends Paths<T> & string> = {
|
||||||
|
value: Get<T, P>;
|
||||||
|
setValue: (val: Get<T, P>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SafeNFormItemImpl = defineComponent<
|
||||||
|
SafeNFormItemProps<Paths<T> & string>,
|
||||||
|
/* Emits */ [],
|
||||||
|
/* EE */ never,
|
||||||
|
SlotsType<{ default: SafeNFormItemDefaultSlot<Paths<T> & string> }>
|
||||||
|
>(
|
||||||
|
(props, ctx) => {
|
||||||
|
return () => {
|
||||||
|
const value = get(formValue.value, props.path);
|
||||||
|
function setValue(val: typeof value) {
|
||||||
|
set(formValue.value, props.path, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSlotContent = ctx.slots.default?.({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有提供默认 slot 内容,则渲染一个 NInput 作为默认输入组件
|
||||||
|
const renderDefaultNInput = defaultSlotContent?.some((v) => v.type !== Comment) ? null : (
|
||||||
|
<NInput value={value} onUpdate:value={setValue} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NFormItem {...props} path={props.path}>
|
||||||
|
{defaultSlotContent}
|
||||||
|
{renderDefaultNInput}
|
||||||
|
</NFormItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SafeNFormItem',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: Object.keys(formItemProps) as unknown as [keyof FormItemProps],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose a generic constructor so template literals narrow `path`.
|
||||||
|
type SafeNFormItemComponent = {
|
||||||
|
new <P extends Paths<T> & string>(
|
||||||
|
props: SafeNFormItemProps<P>,
|
||||||
|
): {
|
||||||
|
$props: SafeNFormItemProps<P>;
|
||||||
|
$slots: {
|
||||||
|
default?: (scope: SafeNFormItemDefaultSlot<P>) => VNode[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const SafeNFormItem = SafeNFormItemImpl as SafeNFormItemComponent;
|
||||||
|
// <<<<<
|
||||||
|
|
||||||
|
return {
|
||||||
|
formValue,
|
||||||
|
SafeNForm,
|
||||||
|
SafeNFormItem,
|
||||||
|
formRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -41,5 +41,6 @@ export default defineConfig({
|
|||||||
// SCSS 专用的 at-rule 规则会自动处理 @include, @mixin 等
|
// SCSS 专用的 at-rule 规则会自动处理 @include, @mixin 等
|
||||||
// 'scss/at-rule-no-unknown': true,
|
// 'scss/at-rule-no-unknown': true,
|
||||||
// <<<<<
|
// <<<<<
|
||||||
|
'selector-class-pattern': null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
39
typed-router.d.ts
vendored
39
typed-router.d.ts
vendored
@@ -65,6 +65,20 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'DemosNaiveUiDemo': RouteRecordInfo<
|
||||||
|
'DemosNaiveUiDemo',
|
||||||
|
'/demos/naive-ui-demo',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'DemosPrimevueDemo': RouteRecordInfo<
|
||||||
|
'DemosPrimevueDemo',
|
||||||
|
'/demos/primevue-demo',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'DemosWebsocketDemo': RouteRecordInfo<
|
'DemosWebsocketDemo': RouteRecordInfo<
|
||||||
'DemosWebsocketDemo',
|
'DemosWebsocketDemo',
|
||||||
'/demos/websocket-demo',
|
'/demos/websocket-demo',
|
||||||
@@ -79,6 +93,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'Login': RouteRecordInfo<
|
||||||
|
'Login',
|
||||||
|
'/Login',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,6 +149,18 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/demos/naive-ui-demo.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'DemosNaiveUiDemo'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
|
'src/pages/demos/primevue-demo.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'DemosPrimevueDemo'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/demos/websocket-demo.page.vue': {
|
'src/pages/demos/websocket-demo.page.vue': {
|
||||||
routes:
|
routes:
|
||||||
| 'DemosWebsocketDemo'
|
| 'DemosWebsocketDemo'
|
||||||
@@ -140,6 +173,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/Login.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'Login'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ import UnoCSS from 'unocss/vite';
|
|||||||
export default [
|
export default [
|
||||||
// https://github.com/antfu/unocss
|
// https://github.com/antfu/unocss
|
||||||
// see uno.config.ts for config
|
// see uno.config.ts for config
|
||||||
UnoCSS(),
|
UnoCSS({
|
||||||
|
checkImport: true,
|
||||||
|
}),
|
||||||
] satisfies PluginOption;
|
] satisfies PluginOption;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
dirs: [
|
dirs: [
|
||||||
// 'src/utils',
|
// 'src/utils',
|
||||||
'src/composables',
|
'src/composables',
|
||||||
'src/stores',
|
// 'src/stores',
|
||||||
// 匹配所有 -auto-imports.ts / -auto-imports.tsx 结尾的文件
|
// 匹配所有 -auto-imports.ts / -auto-imports.tsx 结尾的文件
|
||||||
'src/**/*-auto-imports.{ts,tsx}',
|
'src/**/*-auto-imports.{ts,tsx}',
|
||||||
],
|
],
|
||||||
@@ -73,7 +73,7 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
'pinia',
|
'pinia',
|
||||||
'@vueuse/core',
|
'@vueuse/core',
|
||||||
VueRouterAutoImports,
|
VueRouterAutoImports,
|
||||||
createUtils4uAutoImports([]),
|
createUtils4uAutoImports(['primevue']),
|
||||||
{
|
{
|
||||||
'consola/browser': ['consola'],
|
'consola/browser': ['consola'],
|
||||||
'vue-router/auto': ['useLink'],
|
'vue-router/auto': ['useLink'],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
import { minify as minifyHtml } from 'html-minifier-terser';
|
import { minify as minifyHtml } from 'html-minifier-terser';
|
||||||
|
import { loadEnv } from 'vite';
|
||||||
|
import type { ConfigEnv, PluginOption } from 'vite';
|
||||||
|
|
||||||
function IndexHtmlPlugin(): PluginOption {
|
function IndexHtmlPlugin(): PluginOption {
|
||||||
return {
|
return {
|
||||||
@@ -25,4 +26,7 @@ function IndexHtmlPlugin(): PluginOption {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [IndexHtmlPlugin()] satisfies PluginOption[];
|
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
||||||
|
const env = loadEnv(_configEnv.mode, process.cwd());
|
||||||
|
if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin();
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function loadPlugin(configEnv: ConfigEnv): PluginOption {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.VITE_ENABLE_VUE_DEVTOOLS !== 'true') {
|
if (env.VITE_APP_ENABLE_VUE_DEVTOOLS !== 'true') {
|
||||||
consola.info('vue-devtools plugin disabled by env');
|
consola.info('vue-devtools plugin disabled by env');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
import type { ConfigEnv, PluginOption } from 'vite';
|
||||||
|
import { loadEnv } from 'vite';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// ...
|
// ...
|
||||||
] satisfies PluginOption;
|
] satisfies PluginOption;
|
||||||
|
|
||||||
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
||||||
return [];
|
const env = loadEnv(_configEnv.mode, process.cwd());
|
||||||
|
console.debug(`env :>> `, env);
|
||||||
|
// ...
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const viteConfigRollupOptions: RollupOptions = {
|
|||||||
// assetFileNames:'', // 默认: "assets/[name]-[hash][extname]"
|
// assetFileNames:'', // 默认: "assets/[name]-[hash][extname]"
|
||||||
// https://cn.rollupjs.org/configuration-options/#output-assetfilenames
|
// https://cn.rollupjs.org/configuration-options/#output-assetfilenames
|
||||||
assetFileNames(chunkInfo: PreRenderedAsset) {
|
assetFileNames(chunkInfo: PreRenderedAsset) {
|
||||||
const names = chunkInfo.names;
|
const names = [...new Set(chunkInfo.names)];
|
||||||
|
|
||||||
if (names.length !== 1) {
|
if (names.length !== 1) {
|
||||||
console.error('Multiple names for asset:', chunkInfo);
|
console.error('Multiple names for asset:', chunkInfo);
|
||||||
@@ -42,12 +42,13 @@ export const viteConfigRollupOptions: RollupOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
manualChunks: (id: string, _meta: ManualChunkMeta) => {
|
manualChunks: (id: string, _meta: ManualChunkMeta) => {
|
||||||
if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
|
// https://github.com/unocss/unocss/issues/4917
|
||||||
const url = new URL(id, 'file://');
|
// if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
|
||||||
if (!url.search /* ?vue&type=script&setup=true&lang.ts */) {
|
// const url = new URL(id, 'file://');
|
||||||
return 'layouts';
|
// if (!url.search /* ?vue&type=script&setup=true&lang.ts */) {
|
||||||
}
|
// return 'layouts';
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (id.includes('meta-layouts')) {
|
if (id.includes('meta-layouts')) {
|
||||||
// console.debug(`id :>> `, id); // id :>> virtual:meta-layouts
|
// console.debug(`id :>> `, id); // id :>> virtual:meta-layouts
|
||||||
@@ -99,7 +100,11 @@ export const viteConfigRollupOptions: RollupOptions = {
|
|||||||
return 'lib-naive-ui';
|
return 'lib-naive-ui';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['primelocale', 'primevue', '@primeuix'].some((name) => packageName!.includes(name))) {
|
if (
|
||||||
|
['primelocale', 'primevue', 'primeuix', 'primeicons'].some((name) =>
|
||||||
|
packageName!.includes(name),
|
||||||
|
)
|
||||||
|
) {
|
||||||
return 'lib-primevue';
|
return 'lib-primevue';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ export default defineConfig(async (configEnv) => {
|
|||||||
|
|
||||||
const isBuild = command === 'build';
|
const isBuild = command === 'build';
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
if (process.env.CI) {
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
consola.info(`[vite.config.ts] env: ${key}: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: env.VITE_APP_BASE,
|
base: env.VITE_APP_BASE,
|
||||||
build: {
|
build: {
|
||||||
minify: env.VITE_APP_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
|
minify: env.VITE_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
|
||||||
sourcemap: env.VITE_APP_BUILD_SOURCE_MAP === 'true',
|
sourcemap: env.VITE_BUILD_SOURCE_MAP === 'true',
|
||||||
rollupOptions: viteConfigRollupOptions,
|
rollupOptions: viteConfigRollupOptions,
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
21
worker-configuration.d.ts
vendored
21
worker-configuration.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// Generated by Wrangler by running `wrangler types` (hash: 84692d03acd392dfe81b48b26e3d156f)
|
// Generated by Wrangler by running `wrangler types` (hash: 0003c1b3cd56e3cc5549efef6b4c6d3d)
|
||||||
// Runtime types generated with workerd@1.20251008.0 2025-09-09
|
// Runtime types generated with workerd@1.20251008.0 2025-09-09
|
||||||
declare namespace Cloudflare {
|
declare namespace Cloudflare {
|
||||||
interface GlobalProps {
|
interface GlobalProps {
|
||||||
@@ -7,16 +7,19 @@ declare namespace Cloudflare {
|
|||||||
}
|
}
|
||||||
interface Env {
|
interface Env {
|
||||||
KV: KVNamespace;
|
KV: KVNamespace;
|
||||||
|
VITE_APP_BUILD_TIME: string;
|
||||||
|
VITE_APP_BUILD_COMMIT: string;
|
||||||
|
VITE_BUILD_SOURCE_MAP: string;
|
||||||
|
VITE_BUILD_MINIFY: string;
|
||||||
|
VITE_CLOUDFLARE_SERVER_ENABLED: string;
|
||||||
VITE_APP_TITLE: string;
|
VITE_APP_TITLE: string;
|
||||||
VITE_APP_BASE: string;
|
VITE_APP_BASE: string;
|
||||||
VITE_APP_BUILD_SOURCE_MAP: string;
|
VITE_APP_ENABLE_VUE_DEVTOOLS: string;
|
||||||
VITE_APP_BUILD_MINIFY: string;
|
VITE_APP_MENU_SHOW_DEMOS: string;
|
||||||
VITE_APP_BUILD_COMMIT: string;
|
VITE_APP_MENU_SHOW_ORDER: string;
|
||||||
VITE_APP_BUILD_TIME: string;
|
VITE_APP_ENABLE_ROUTER_LOG_GUARD: string;
|
||||||
VITE_ENABLE_VUE_DEVTOOLS: string;
|
VITE_APP_API_URL: string;
|
||||||
VITE_MENU_SHOW_DEMOS: string;
|
VITE_APP_PROXY: string;
|
||||||
VITE_MENU_SHOW_ORDER: string;
|
|
||||||
VITE_CLOUDFLARE_SERVER_ENABLED: string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface Env extends Cloudflare.Env {}
|
interface Env extends Cloudflare.Env {}
|
||||||
|
|||||||
Reference in New Issue
Block a user