5 Commits

Author SHA1 Message Date
严浩
96728d87fd feat(src/utils/a2use.vue): 添加 SafeNFormItem 组件用于电话号码验证
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 3m28s
CI/CD Pipeline / build-and-deploy (push) Failing after 3m17s
2025-10-31 13:16:22 +08:00
严浩
66419d22d4 refactor: 移除未使用的 RouterView 组件,优化 a2use 组件的模板和 SafeNFormItem 的实现
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 3m25s
CI/CD Pipeline / build-and-deploy (push) Failing after 3m29s
2025-10-31 12:53:59 +08:00
严浩
18cb623730 refactor: 优化 SafeNFormItem 组件的类型定义和插槽处理
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 2m38s
CI/CD Pipeline / build-and-deploy (push) Failing after 3m13s
2025-10-31 11:14:13 +08:00
严浩
4c7f052ea1 feat: 更新 useSafeNForm 组件,优化类型定义 [skip ci] 2025-10-31 10:28:12 +08:00
严浩
13b535c530 feat: 添加 useSafeNForm 组件及相关功能,更新依赖项,修复类型定义 [skip ci] 2025-10-31 01:12:55 +08:00
67 changed files with 3398 additions and 5063 deletions

View File

@@ -1,75 +0,0 @@
{
"image": "ghcr.io/yanhao98/h-devcontainer:main",
"runArgs": ["--name=${localWorkspaceFolderBasename}-devcontainer"],
"forwardPorts": [4730, 4731], // vscode://settings/remote.localPortHost -> 默认只监听 localhost
"portsAttributes": {
"4730": { "label": "开发服务器端口", "onAutoForward": "notify" },
"4731": { "label": "预览服务器端口", "onAutoForward": "notify" }
},
"remoteEnv": {
"ANTHROPIC_AUTH_TOKEN": "${localEnv:ANTHROPIC_AUTH_TOKEN}",
"ANTHROPIC_BASE_URL": "${localEnv:ANTHROPIC_BASE_URL}",
"GEMINI_API_KEY": "${localEnv:GEMINI_API_KEY}",
"GOOGLE_GEMINI_BASE_URL": "${localEnv:GOOGLE_GEMINI_BASE_URL}"
},
"containerEnv": {
"NODE_OPTIONS": "--max-old-space-size=4096",
"TZ": "${localEnv:TZ:Asia/Shanghai}"
},
"customizations": {
"vscode": {
"extensions": [
// AI
"github.copilot-chat",
"anthropic.claude-code",
"google.gemini-cli-vscode-ide-companion",
"vicanent.gcmp",
// >>>>>
// "eamodio.gitlens",
"tu6ge.naive-ui-intelligence",
"gruntfuggly.todo-tree",
"lokalise.i18n-ally",
"vitest.explorer",
"antfu.unocss",
"vue.volar",
// <<<<<
// 代码质量 / 格式化 / Lint
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"chat.extensionUnification.enabled": true,
"chat.tools.terminal.autoApprove": {
"/.*/": true,
"git push": false
},
// * 尽管使用了“/.*/”,但有些还是会失败,因为有几个错误的默认值:
// * https://github.com/microsoft/vscode/issues/266651#issuecomment-3292581459
"chat.tools.terminal.ignoreDefaultAutoApproveRules": true,
"tasks": { "version": "2.0.0", "tasks": [] },
"terminal.integrated.defaultProfile.linux": "zsh"
}
}
},
"mounts": [
// 不挂载还可能会遇到:`Cannot run macOS (Mach-O) executable in Docker: Exec format error`
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
"source=${localWorkspaceFolder}/.devcontainer/lifecycle-scripts.d,target=/usr/local/etc/lifecycle-scripts.d,type=bind,consistency=delegated"
],
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/${localWorkspaceFolderBasename},type=bind,consistency=delegated",
"initializeCommand": "docker pull ghcr.io/yanhao98/h-devcontainer:main;",
"onCreateCommand": "/usr/local/bin/run-lifecycle-scripts.sh onCreateCommand",
"updateContentCommand": "/usr/local/bin/run-lifecycle-scripts.sh updateContentCommand",
"postCreateCommand": "/usr/local/bin/run-lifecycle-scripts.sh postCreateCommand",
"postStartCommand": "/usr/local/bin/run-lifecycle-scripts.sh postStartCommand",
"postAttachCommand": "/usr/local/bin/run-lifecycle-scripts.sh postAttachCommand",
"waitFor": "updateContentCommand",
"remoteUser": "usr_vscode"
}

View File

@@ -1,13 +0,0 @@
#!/bin/zsh -eu
# 打印带有颜色的欢迎信息
echo -e "\033[1;32m↘ 容器首次创建!\033[0m"
# 修复权限问题(比如 node_modules 目录)
sudo chown -R $(whoami):$(whoami) /workspaces || true
# 调用内置命令 (这些命令在 _build-context/builtin-scripts 目录中的脚本中定义)
h-setup-locale
h-setup-zsh
h-setup-bun-bin
h-setup-pnpm-bin

View File

@@ -1,13 +0,0 @@
#!/bin/bash -eu
# 容器内容更新时的消息和依赖安装
echo '↘️ 容器内容已更新!'
# 安装依赖
if command -v pnpm >/dev/null 2>&1; then
# 跳过: The modules directory at "/workspaces/h-devcontainers/node_modules" will be removed and reinstalled from scratch. Proceed? (Y/n) ·
time pnpm install --config.confirmModulesPurge=false
else
echo '❌错误: pnpm 未安装'
exit 0
fi

View File

@@ -17,7 +17,7 @@ jobs:
container: mcr.microsoft.com/playwright:v1.56.1-noble container: mcr.microsoft.com/playwright:v1.56.1-noble
steps: steps:
- name: ⚙️ 设置 Node 环境 - name: ⚙️ 设置 Node 环境
uses: yanhao98/composite-actions/setup-node-environment@faab20ac2f9c85dfce1a4147fca493bf632bd744 uses: yanhao98/composite-actions/setup-node-environment@25eb4dc0c134cc9df2b7c569aa54140a366b45a8
# - name: 📥 安装 Playwright 浏览器 # - name: 📥 安装 Playwright 浏览器
# run: pnpm exec playwright install --with-deps # run: pnpm exec playwright install --with-deps
- name: 📦 构建项目 - name: 📦 构建项目
@@ -32,15 +32,13 @@ jobs:
steps: steps:
- name: 🛠️ 设置Node环境 - name: 🛠️ 设置Node环境
uses: yanhao98/composite-actions/setup-node-environment@faab20ac2f9c85dfce1a4147fca493bf632bd744 uses: yanhao98/composite-actions/setup-node-environment@25eb4dc0c134cc9df2b7c569aa54140a366b45a8
- name: 🔍 静态代码分析 - name: 🔍 静态代码分析
run: pnpm run lint run: pnpm run lint
- name: 📦 构建项目 - name: 📦 构建项目
run: | run: pnpm run build-only
export VITE_APP_BUILD_TIME=$(date +"%Y-%m-%d %H:%M:%S")
pnpm run build-only
env: env:
VITE_APP_BUILD_COMMIT: ${{ github.sha }} VITE_APP_BUILD_COMMIT: ${{ github.sha }}

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
.pnpm-store
node_modules node_modules
.DS_Store .DS_Store
dist dist

View File

@@ -8,6 +8,6 @@ cat $1
echo "🟢 [Commit-msg] Node 版本:$(node -v)" echo "🟢 [Commit-msg] Node 版本:$(node -v)"
# pnpm exec commitlint --edit $1 # pnpm exec commitlint --edit $1
node node_modules/@commitlint/cli/cli.js --edit $1 time node node_modules/@commitlint/cli/cli.js --edit $1
echo "📝 [Commit-msg] commit-msg 钩子完成!" echo "📝 [Commit-msg] commit-msg 钩子完成!"

View File

@@ -1,4 +1,4 @@
# 此钩子在 git merge 或 git pull 成功完成后运行。 # 此钩子在 git merge 或 git pull 成功完成后运行。
echo "🔗 [Post-merge] 正在安装依赖..." echo "🔗 [Post-merge] 正在安装依赖..."
pnpm install time pnpm install
echo "🔗 [Post-merge] 依赖安装完成!" echo "🔗 [Post-merge] 依赖安装完成!"

View File

@@ -1,6 +1,6 @@
# 此钩子在执行 git commit 命令时,在创建提交之前运行。 # 此钩子在执行 git commit 命令时,在创建提交之前运行。
echo "🧹 [Pre-commit] 正在运行 lint-staged..." echo "🧹 [Pre-commit] 正在运行 lint-staged..."
pnpm exec lint-staged time pnpm exec lint-staged
pnpm run lint:vue-i18n-extract time pnpm run lint:vue-i18n-extract
# pnpm run type-check # time pnpm run type-check
echo "🧹 [Pre-commit] lint-staged 完成!" echo "🧹 [Pre-commit] lint-staged 完成!"

8
.npmrc
View File

@@ -1 +1,9 @@
# registry=https://registry.npmmirror.com/ # registry=https://registry.npmmirror.com/
# https://pnpm.io/zh/npmrc#node-mirrorltreleasedir
use-node-version=24.7.0
node-mirror:release=https://npmmirror.com/mirrors/node/ # pnpm config set node-mirror:release=https://npmmirror.com/mirrors/node/
node-mirror:rc=https://npmmirror.com/mirrors/node-rc/
node-mirror:nightly=https://npmmirror.com/mirrors/node-nightly/
# shamefully-hoist=true

View File

@@ -2,11 +2,10 @@
"recommendations": [ "recommendations": [
"Vue.volar", "Vue.volar",
"vitest.explorer", "vitest.explorer",
"ms-playwright.playwright",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"oxc.oxc-vscode", "oxc.oxc-vscode",
"esbenp.prettier-vscode", "esbenp.prettier-vscode"
"stylelint.vscode-stylelint",
"lokalise.i18n-ally"
] ]
} }

18
.vscode/settings.json vendored
View File

@@ -6,26 +6,22 @@
"source.fixAll.oxc": "explicit", "source.fixAll.oxc": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"eslint.enable": true,
"stylelint.enable": true,
"oxc.enable": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"stylelint.enable": true,
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"], "stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"scss.lint.unknownAtRules": "ignore", "scss.lint.unknownAtRules": "ignore",
"css.lint.unknownAtRules": "ignore", "css.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore", "less.lint.unknownAtRules": "ignore",
"eslint.enable": true,
"oxc.enable": true,
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
// >>>>> // >>>>>
"i18n-ally.readonly": false,
"i18n-ally.namespace": false /* @intlify/unplugin-vue-i18n */, "i18n-ally.namespace": false /* @intlify/unplugin-vue-i18n */,
"i18n-ally.localesPaths": ["src/locales/demo", "src/locales"], "i18n-ally.localesPaths": ["src/locales/demo", "src/locales"],
// https://github.com/lokalise/i18n-ally/wiki/Path-Matcher // https://github.com/lokalise/i18n-ally/wiki/Path-Matcher
@@ -36,8 +32,10 @@
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容 "i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容
"i18n-ally.displayLanguage": "zh-CN", // 显示语言 (显示文件/翻译文件) "i18n-ally.displayLanguage": "zh-CN", // 显示语言 (显示文件/翻译文件)
// <<<<< // <<<<<
// 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"],
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": ["vue-router/auto$"] "typescript.preferences.autoImportFileExcludePatterns": ["vue-router/auto$"]
} }

24
.vscode/tasks.json vendored
View File

@@ -6,12 +6,16 @@
"script": "dev", "script": "dev",
"label": "🚀 dev", "label": "🚀 dev",
"detail": "启动开发服务器", "detail": "启动开发服务器",
"problemMatcher": [], "problemMatcher": {
"isBackground": true, "pattern": { "regexp": "." },
"presentation": { "background": {
"reveal": "always", // 控制运行任务的终端是否显示。可按选项 "revealProblems" 进行替代。默认设置为“始终”。 "activeOnStart": true,
"panel": "dedicated" // dedicated一个任务独占一个终端方便查看特定任务的日志不会被其他任务干扰。 "beginsPattern": "VITE.*ready in",
"endsPattern": "(Local|Network):.*http"
}
}, },
"isBackground": true,
"presentation": { "reveal": "always", "panel": "dedicated" },
"group": { "kind": "build", "isDefault": false } "group": { "kind": "build", "isDefault": false }
}, },
{ {
@@ -19,6 +23,7 @@
"script": "build-only", "script": "build-only",
"label": "🔨 build-only", "label": "🔨 build-only",
"detail": "" /* VSCode 使 package.json scripts[scriptName] detail */, "detail": "" /* VSCode 使 package.json scripts[scriptName] detail */,
"problemMatcher": ["$tsc"],
"presentation": { "reveal": "always", "panel": "shared" }, "presentation": { "reveal": "always", "panel": "shared" },
"group": { "kind": "none", "isDefault": false } "group": { "kind": "none", "isDefault": false }
}, },
@@ -28,7 +33,14 @@
"label": "☁️ wrangler:dev", "label": "☁️ wrangler:dev",
"detail": "启动 Cloudflare Workers 开发服务器,相当于预览", "detail": "启动 Cloudflare Workers 开发服务器,相当于预览",
"dependsOn": ["🔨 build-only"], "dependsOn": ["🔨 build-only"],
"problemMatcher": [], "problemMatcher": {
"pattern": { "regexp": "." },
"background": {
"activeOnStart": true,
"beginsPattern": "wrangler dev",
"endsPattern": "Ready on"
}
},
"isBackground": true, "isBackground": true,
"presentation": { "reveal": "always", "panel": "dedicated" }, "presentation": { "reveal": "always", "panel": "dedicated" },
"group": { "kind": "build", "isDefault": false } "group": { "kind": "build", "isDefault": false }

View File

@@ -9,7 +9,6 @@ Vue 3 TypeScript application with Vite.
## 开发服务器 ## 开发服务器
- **不要启动开发服务器**: 开发服务器通常已经由用户启动。除非特别要求,否则不要执行 `pnpm dev` 之类的命令。 - **不要启动开发服务器**: 开发服务器通常已经由用户启动。除非特别要求,否则不要执行 `pnpm dev` 之类的命令。
- **不要执行 Playwright 测试**: 除非用户明确要求,否则不要运行 Playwright 端到端测试。
### Routing & Layouts ### Routing & Layouts
@@ -47,6 +46,25 @@ Multiple auto-import systems are active:
Project has multiple UI frameworks configured: Project has multiple UI frameworks configured:
- **Naive UI** - **Naive UI**
- **Form Layout**: When using `NGrid` for form layouts, prefer `NFormItemGi` over nesting `NFormItem` inside `NGridItem` for more concise code.
```vue
<!-- ❌ Avoid: Verbose nesting -->
<NGrid :cols="4">
<NGridItem>
<NFormItem label="Username">
<NInput />
</NFormItem>
</NGridItem>
</NGrid>
<!-- ✅ Use: Concise and direct -->
<NGrid :cols="4">
<NFormItemGi label="Username">
<NInput />
</NFormItemGi>
</NGrid>
```
- **PrimeVue**: - **PrimeVue**:

664
auto-imports.d.ts vendored
View File

@@ -6,336 +6,333 @@
// biome-ignore lint: disable // biome-ignore lint: disable
export {} export {}
declare global { declare global {
const ConfirmationService: typeof import('utils4u/primevue').ConfirmationService const ConfirmationService: typeof import('utils4u/primevue')['ConfirmationService']
const DialogService: typeof import('utils4u/primevue').DialogService 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 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']
const autoResetRef: typeof import('@vueuse/core').autoResetRef const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue').computed const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core').computedAsync const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core').computedEager const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core').computedInject const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core').computedWithControl const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const consola: typeof import('consola/browser').consola const consola: typeof import('consola/browser')['consola']
const controlledComputed: typeof import('@vueuse/core').controlledComputed const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core').controlledRef const controlledRef: typeof import('@vueuse/core')['controlledRef']
const convertFileToBase64: typeof import('utils4u/browser').convertFileToBase64 const convertFileToBase64: typeof import('utils4u/browser')['convertFileToBase64']
const createApp: typeof import('vue').createApp const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core').createEventHook const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core').createGlobalState const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core').createInjectionState const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createLogGuard: typeof import('utils4u/vue-router').createLogGuard const createLogGuard: typeof import('utils4u/vue-router')['createLogGuard']
const createNProgressGuard: typeof import('utils4u/vue-router').createNProgressGuard const createNProgressGuard: typeof import('utils4u/vue-router')['createNProgressGuard']
const createPinia: typeof import('pinia').createPinia const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core').createRef const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createStackGuard: typeof import('utils4u/vue-router').createStackGuard const createStackGuard: typeof import('utils4u/vue-router')['createStackGuard']
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue').customRef const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core').debouncedRef const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const deepFreeze: typeof import('deep-freeze-es6').default const deepFreeze: typeof import('deep-freeze-es6')['default']
const defineAsyncComponent: typeof import('vue').defineAsyncComponent const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue').defineComponent const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia').defineStore const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core').eagerComputed const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue').effectScope const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core').extendRef const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia').getActivePinia const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue').getCurrentInstance const getCurrentInstance: typeof import('vue')['getCurrentInstance']
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 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 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']
const isDefined: typeof import('@vueuse/core').isDefined const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue').isProxy const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue').isReactive const isReactive: typeof import('vue')['isReactive']
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 makeDestructurable: typeof import('@vueuse/core').makeDestructurable const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const manualResetRef: typeof import('@vueuse/core').manualResetRef const mapActions: typeof import('pinia')['mapActions']
const mapActions: typeof import('pinia').mapActions const mapGetters: typeof import('pinia')['mapGetters']
const mapGetters: typeof import('pinia').mapGetters const mapState: typeof import('pinia')['mapState']
const mapState: typeof import('pinia').mapState const mapStores: typeof import('pinia')['mapStores']
const mapStores: typeof import('pinia').mapStores const mapWritableState: typeof import('pinia')['mapWritableState']
const mapWritableState: typeof import('pinia').mapWritableState const markRaw: typeof import('vue')['markRaw']
const markRaw: typeof import('vue').markRaw const nextTick: typeof import('vue')['nextTick']
const nextTick: typeof import('vue').nextTick const onActivated: typeof import('vue')['onActivated']
const onActivated: typeof import('vue').onActivated const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeMount: typeof import('vue').onBeforeMount const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUnmount: typeof import('vue').onBeforeUnmount const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onBeforeUpdate: typeof import('vue').onBeforeUpdate const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onClickOutside: typeof import('@vueuse/core').onClickOutside const onDeactivated: typeof import('vue')['onDeactivated']
const onDeactivated: typeof import('vue').onDeactivated const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onErrorCaptured: typeof import('vue').onErrorCaptured const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onLongPress: typeof import('@vueuse/core').onLongPress const onMounted: typeof import('vue')['onMounted']
const onMounted: typeof import('vue').onMounted const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTracked: typeof import('vue').onRenderTracked const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onRenderTriggered: typeof import('vue').onRenderTriggered const onScopeDispose: typeof import('vue')['onScopeDispose']
const onScopeDispose: typeof import('vue').onScopeDispose const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onServerPrefetch: typeof import('vue').onServerPrefetch const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onStartTyping: typeof import('@vueuse/core').onStartTyping const onUnmounted: typeof import('vue')['onUnmounted']
const onUnmounted: typeof import('vue').onUnmounted const onUpdated: typeof import('vue')['onUpdated']
const onUpdated: typeof import('vue').onUpdated const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const onWatcherCleanup: typeof import('vue').onWatcherCleanup const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const pausableWatch: typeof import('@vueuse/core').pausableWatch const provide: typeof import('vue')['provide']
const provide: typeof import('vue').provide const provideLocal: typeof import('@vueuse/core')['provideLocal']
const provideLocal: typeof import('@vueuse/core').provideLocal const reactify: typeof import('@vueuse/core')['reactify']
const reactify: typeof import('@vueuse/core').reactify const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactifyObject: typeof import('@vueuse/core').reactifyObject const reactive: typeof import('vue')['reactive']
const reactive: typeof import('vue').reactive const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit const reactivePick: typeof import('@vueuse/core')['reactivePick']
const reactivePick: typeof import('@vueuse/core').reactivePick const readonly: typeof import('vue')['readonly']
const readonly: typeof import('vue').readonly const ref: typeof import('vue')['ref']
const ref: typeof import('vue').ref const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refAutoReset: typeof import('@vueuse/core').refAutoReset const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDebounced: typeof import('@vueuse/core').refDebounced const refDefault: typeof import('@vueuse/core')['refDefault']
const refDefault: typeof import('@vueuse/core').refDefault const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refManualReset: typeof import('@vueuse/core').refManualReset const refWithControl: typeof import('@vueuse/core')['refWithControl']
const refThrottled: typeof import('@vueuse/core').refThrottled const resolveComponent: typeof import('vue')['resolveComponent']
const refWithControl: typeof import('@vueuse/core').refWithControl const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveComponent: typeof import('vue').resolveComponent const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const resolveRef: typeof import('@vueuse/core').resolveRef const setActivePinia: typeof import('pinia')['setActivePinia']
const routeI18nInstance: typeof import('./src/locales-utils/i18n-auto-imports').routeI18nInstance const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const routeI18nT: typeof import('./src/locales-utils/i18n-auto-imports').routeI18nT const setViewportCSSVars: typeof import('utils4u/browser')['setViewportCSSVars']
const setActivePinia: typeof import('pinia').setActivePinia const shallowReactive: typeof import('vue')['shallowReactive']
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix const shallowReadonly: typeof import('vue')['shallowReadonly']
const setViewportCSSVars: typeof import('utils4u/browser').setViewportCSSVars const shallowRef: typeof import('vue')['shallowRef']
const shallowReactive: typeof import('vue').shallowReactive const showOpenFilePicker: typeof import('utils4u/browser')['showOpenFilePicker']
const shallowReadonly: typeof import('vue').shallowReadonly const storeToRefs: typeof import('pinia')['storeToRefs']
const shallowRef: typeof import('vue').shallowRef const syncRef: typeof import('@vueuse/core')['syncRef']
const showOpenFilePicker: typeof import('utils4u/browser').showOpenFilePicker const syncRefs: typeof import('@vueuse/core')['syncRefs']
const storeToRefs: typeof import('pinia').storeToRefs const templateRef: typeof import('@vueuse/core')['templateRef']
const syncRef: typeof import('@vueuse/core').syncRef const throttledRef: typeof import('@vueuse/core')['throttledRef']
const syncRefs: typeof import('@vueuse/core').syncRefs const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const templateRef: typeof import('@vueuse/core').templateRef const toRaw: typeof import('vue')['toRaw']
const throttledRef: typeof import('@vueuse/core').throttledRef const toReactive: typeof import('@vueuse/core')['toReactive']
const throttledWatch: typeof import('@vueuse/core').throttledWatch const toRef: typeof import('vue')['toRef']
const toRaw: typeof import('vue').toRaw const toRefs: typeof import('vue')['toRefs']
const toReactive: typeof import('@vueuse/core').toReactive const toValue: typeof import('vue')['toValue']
const toRef: typeof import('vue').toRef const triggerRef: typeof import('vue')['triggerRef']
const toRefs: typeof import('vue').toRefs const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const toValue: typeof import('vue').toValue const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const triggerRef: typeof import('vue').triggerRef const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted const unref: typeof import('vue')['unref']
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose const unrefElement: typeof import('@vueuse/core')['unrefElement']
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted const until: typeof import('@vueuse/core')['until']
const unref: typeof import('vue').unref const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const unrefElement: typeof import('@vueuse/core').unrefElement const useAnimate: typeof import('@vueuse/core')['useAnimate']
const until: typeof import('@vueuse/core').until const useAppStore: typeof import('./src/stores/app-store-auto-imports')['useAppStore']
const useActiveElement: typeof import('@vueuse/core').useActiveElement const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useAnimate: typeof import('@vueuse/core').useAnimate const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useAppStore: typeof import('./src/stores/app-store-auto-imports').useAppStore const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayFind: typeof import('@vueuse/core').useArrayFind const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayMap: typeof import('@vueuse/core').useArrayMap const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useArraySome: typeof import('@vueuse/core').useArraySome const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique const useAttrs: typeof import('vue')['useAttrs']
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue const useBase64: typeof import('@vueuse/core')['useBase64']
const useAsyncState: typeof import('@vueuse/core').useAsyncState const useBattery: typeof import('@vueuse/core')['useBattery']
const useAttrs: typeof import('vue').useAttrs const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useAuthStore: typeof import('./src/stores/auth-store-auto-imports').useAuthStore const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBase64: typeof import('@vueuse/core').useBase64 const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBattery: typeof import('@vueuse/core').useBattery const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useBluetooth: typeof import('@vueuse/core').useBluetooth const useCached: typeof import('@vueuse/core')['useCached']
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation const useCloned: typeof import('@vueuse/core')['useCloned']
const useCached: typeof import('@vueuse/core').useCached const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useClipboard: typeof import('@vueuse/core').useClipboard const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCloned: typeof import('@vueuse/core').useCloned const useCounter: typeof import('@vueuse/core')['useCounter']
const useColorMode: typeof import('@vueuse/core').useColorMode const useCssModule: typeof import('vue')['useCssModule']
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCountdown: typeof import('@vueuse/core').useCountdown const useCssVars: typeof import('vue')['useCssVars']
const useCounter: typeof import('@vueuse/core').useCounter const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCssModule: typeof import('vue').useCssModule const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useCssVar: typeof import('@vueuse/core').useCssVar const useDark: typeof import('@vueuse/core')['useDark']
const useCssVars: typeof import('vue').useCssVars const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useCycleList: typeof import('@vueuse/core').useCycleList const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDark: typeof import('@vueuse/core').useDark const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDateFormat: typeof import('@vueuse/core').useDateFormat const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDebounce: typeof import('@vueuse/core').useDebounce const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion const useDialog: typeof import('naive-ui')['useDialog']
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDevicesList: typeof import('@vueuse/core').useDevicesList const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDialog: typeof import('naive-ui').useDialog const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useDraggable: typeof import('@vueuse/core').useDraggable const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useDropZone: typeof import('@vueuse/core').useDropZone const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementBounding: typeof import('@vueuse/core').useElementBounding const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useElementHover: typeof import('@vueuse/core').useElementHover const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useElementSize: typeof import('@vueuse/core').useElementSize const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useEventBus: typeof import('@vueuse/core').useEventBus const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useEventListener: typeof import('@vueuse/core').useEventListener const useFetch: typeof import('@vueuse/core')['useFetch']
const useEventSource: typeof import('@vueuse/core').useEventSource const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFavicon: typeof import('@vueuse/core').useFavicon const useFocus: typeof import('@vueuse/core')['useFocus']
const useFetch: typeof import('@vueuse/core').useFetch const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFileDialog: typeof import('@vueuse/core').useFileDialog const useFps: typeof import('@vueuse/core')['useFps']
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useFocus: typeof import('@vueuse/core').useFocus const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useFps: typeof import('@vueuse/core').useFps const useI18n: typeof import('vue-i18n')['useI18n']
const useFullscreen: typeof import('@vueuse/core').useFullscreen const useId: typeof import('vue')['useId']
const useGamepad: typeof import('@vueuse/core').useGamepad const useIdle: typeof import('@vueuse/core')['useIdle']
const useGeolocation: typeof import('@vueuse/core').useGeolocation const useImage: typeof import('@vueuse/core')['useImage']
const useI18n: typeof import('vue-i18n').useI18n const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useId: typeof import('vue').useId const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useIdle: typeof import('@vueuse/core').useIdle const useInterval: typeof import('@vueuse/core')['useInterval']
const useImage: typeof import('@vueuse/core').useImage const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useInterval: typeof import('@vueuse/core').useInterval const useLink: typeof import('vue-router/auto')['useLink']
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLastChanged: typeof import('@vueuse/core').useLastChanged const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useLink: typeof import('vue-router/auto').useLink const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useLoadingBar: typeof import('naive-ui').useLoadingBar const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory const useMemory: typeof import('@vueuse/core')['useMemory']
const useMediaControls: typeof import('@vueuse/core').useMediaControls const useMessage: typeof import('naive-ui')['useMessage']
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery const useMetaLayoutsNMenuOptions: typeof import('./src/composables/useMetaLayoutsMenuOptions')['useMetaLayoutsNMenuOptions']
const useMemoize: typeof import('@vueuse/core').useMemoize const useModal: typeof import('naive-ui')['useModal']
const useMemory: typeof import('@vueuse/core').useMemory const useModel: typeof import('vue')['useModel']
const useMessage: typeof import('naive-ui').useMessage const useMounted: typeof import('@vueuse/core')['useMounted']
const useMetaLayoutsNMenuOptions: typeof import('./src/composables/useMetaLayoutsMenuOptions').useMetaLayoutsNMenuOptions const useMouse: typeof import('@vueuse/core')['useMouse']
const useModal: typeof import('naive-ui').useModal const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useModel: typeof import('vue').useModel const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMounted: typeof import('@vueuse/core').useMounted const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useMouse: typeof import('@vueuse/core').useMouse const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useMousePressed: typeof import('@vueuse/core').useMousePressed const useNotification: typeof import('naive-ui')['useNotification']
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver const useNow: typeof import('@vueuse/core')['useNow']
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useNetwork: typeof import('@vueuse/core').useNetwork const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useNotification: typeof import('naive-ui').useNotification const useOnline: typeof import('@vueuse/core')['useOnline']
const useNow: typeof import('@vueuse/core').useNow const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl const useParallax: typeof import('@vueuse/core')['useParallax']
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination const useParentElement: typeof import('@vueuse/core')['useParentElement']
const useOnline: typeof import('@vueuse/core').useOnline const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePageLeave: typeof import('@vueuse/core').usePageLeave const usePermission: typeof import('@vueuse/core')['usePermission']
const useParallax: typeof import('@vueuse/core').useParallax const usePointer: typeof import('@vueuse/core')['usePointer']
const useParentElement: typeof import('@vueuse/core').useParentElement const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePermission: typeof import('@vueuse/core').usePermission const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePointer: typeof import('@vueuse/core').usePointer const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePointerLock: typeof import('@vueuse/core').usePointerLock const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark const usePrevious: typeof import('@vueuse/core')['usePrevious']
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages const usePrimevueDialogRef: typeof import('utils4u/primevue')['usePrimevueDialogRef']
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion const useRafFn: typeof import('@vueuse/core')['useRafFn']
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const usePrevious: typeof import('@vueuse/core').usePrevious const useRefs: typeof import('utils4u/vue-use')['useRefs']
const usePrimevueDialogRef: typeof import('utils4u/primevue').usePrimevueDialogRef const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRafFn: typeof import('@vueuse/core').useRafFn const useRoute: typeof import('vue-router')['useRoute']
const useRefHistory: typeof import('@vueuse/core').useRefHistory const useRouter: typeof import('vue-router')['useRouter']
const useRefs: typeof import('utils4u/vue-use').useRefs const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver const useSafeNForm: typeof import('./src/utils/use-safe-n-form-auto-imports')['useSafeNForm']
const useRoute: typeof import('vue-router').useRoute const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useRouter: typeof import('vue-router').useRouter const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation const useScroll: typeof import('@vueuse/core')['useScroll']
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useScriptTag: typeof import('@vueuse/core').useScriptTag const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useScroll: typeof import('@vueuse/core').useScroll const useShare: typeof import('@vueuse/core')['useShare']
const useScrollLock: typeof import('@vueuse/core').useScrollLock const useSlots: typeof import('vue')['useSlots']
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage const useSorted: typeof import('@vueuse/core')['useSorted']
const useShare: typeof import('@vueuse/core').useShare const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSlots: typeof import('vue').useSlots const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useSorted: typeof import('@vueuse/core').useSorted const useStepper: typeof import('@vueuse/core')['useStepper']
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition const useStorage: typeof import('@vueuse/core')['useStorage']
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStepper: typeof import('@vueuse/core').useStepper const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useStorage: typeof import('@vueuse/core').useStorage const useSupported: typeof import('@vueuse/core')['useSupported']
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useStyleTag: typeof import('@vueuse/core').useStyleTag const useTemplateRef: typeof import('vue')['useTemplateRef']
const useSupported: typeof import('@vueuse/core').useSupported const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useSwipe: typeof import('@vueuse/core').useSwipe const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTemplateRef: typeof import('vue').useTemplateRef const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useTextDirection: typeof import('@vueuse/core').useTextDirection const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useTextSelection: typeof import('@vueuse/core').useTextSelection const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useThrottle: typeof import('@vueuse/core').useThrottle const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimeout: typeof import('@vueuse/core').useTimeout const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn const useTitle: typeof import('@vueuse/core')['useTitle']
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useTimestamp: typeof import('@vueuse/core').useTimestamp const useToString: typeof import('@vueuse/core')['useToString']
const useTitle: typeof import('@vueuse/core').useTitle const useToggle: typeof import('@vueuse/core')['useToggle']
const useToNumber: typeof import('@vueuse/core').useToNumber const useTransition: typeof import('@vueuse/core')['useTransition']
const useToString: typeof import('@vueuse/core').useToString const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useToggle: typeof import('@vueuse/core').useToggle const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useTransition: typeof import('@vueuse/core').useTransition const useVModel: typeof import('@vueuse/core')['useVModel']
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams const useVModels: typeof import('@vueuse/core')['useVModels']
const useUserMedia: typeof import('@vueuse/core').useUserMedia const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVModel: typeof import('@vueuse/core').useVModel const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVModels: typeof import('@vueuse/core').useVModels const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useVibrate: typeof import('@vueuse/core').useVibrate const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useVirtualList: typeof import('@vueuse/core').useVirtualList const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWakeLock: typeof import('@vueuse/core').useWakeLock const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebNotification: typeof import('@vueuse/core').useWebNotification const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWebSocket: typeof import('@vueuse/core').useWebSocket const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWebWorker: typeof import('@vueuse/core').useWebWorker const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus const watch: typeof import('vue')['watch']
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll const watchArray: typeof import('@vueuse/core')['watchArray']
const useWindowSize: typeof import('@vueuse/core').useWindowSize const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watch: typeof import('vue').watch const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchArray: typeof import('@vueuse/core').watchArray const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchAtMost: typeof import('@vueuse/core').watchAtMost const watchEffect: typeof import('vue')['watchEffect']
const watchDebounced: typeof import('@vueuse/core').watchDebounced const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchDeep: typeof import('@vueuse/core').watchDeep const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchEffect: typeof import('vue').watchEffect const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchImmediate: typeof import('@vueuse/core').watchImmediate const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchOnce: typeof import('@vueuse/core').watchOnce const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchPausable: typeof import('@vueuse/core').watchPausable const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchPostEffect: typeof import('vue').watchPostEffect const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchSyncEffect: typeof import('vue').watchSyncEffect const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const watchThrottled: typeof import('@vueuse/core').watchThrottled const whenever: typeof import('@vueuse/core')['whenever']
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
} }
// for type re-export // for type re-export
declare global { declare global {
@@ -410,7 +407,6 @@ declare module 'vue' {
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 makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly manualResetRef: UnwrapRef<typeof import('@vueuse/core')['manualResetRef']>
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']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']> readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
@@ -453,13 +449,11 @@ declare module 'vue' {
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']> readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']> readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']> readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']> readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']> readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']> readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']> readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly routeI18nInstance: UnwrapRef<typeof import('./src/locales-utils/i18n-auto-imports')['routeI18nInstance']> readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly routeI18nT: UnwrapRef<typeof import('./src/locales-utils/i18n-auto-imports')['routeI18nT']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']> readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']> readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setViewportCSSVars: UnwrapRef<typeof import('utils4u/browser')['setViewportCSSVars']> readonly setViewportCSSVars: UnwrapRef<typeof import('utils4u/browser')['setViewportCSSVars']>
@@ -505,7 +499,6 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']> readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuthStore: UnwrapRef<typeof import('./src/stores/auth-store-auto-imports')['useAuthStore']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']> readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']> readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']> readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
@@ -616,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']>

View File

@@ -1,19 +1,15 @@
import vueI18n from '@intlify/eslint-plugin-vue-i18n'; import pluginImport from 'eslint-plugin-import';
// import stylistic from '@stylistic/eslint-plugin'; import { globalIgnores } from 'eslint/config';
import pluginVitest from '@vitest/eslint-plugin';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
import { import {
configureVueProject,
defineConfigWithVueTs, defineConfigWithVueTs,
vueTsConfigs, vueTsConfigs,
configureVueProject,
} from '@vue/eslint-config-typescript'; } from '@vue/eslint-config-typescript';
import pluginImport from 'eslint-plugin-import';
import pluginJsonc from 'eslint-plugin-jsonc';
import pluginOxlint from 'eslint-plugin-oxlint';
import pluginPlaywright from 'eslint-plugin-playwright';
import pluginVue from 'eslint-plugin-vue'; import pluginVue from 'eslint-plugin-vue';
import { globalIgnores } from 'eslint/config'; import pluginVitest from '@vitest/eslint-plugin';
import jsoncParser from 'jsonc-eslint-parser'; import pluginPlaywright from 'eslint-plugin-playwright';
import pluginOxlint from 'eslint-plugin-oxlint';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
configureVueProject({ scriptLangs: ['ts', 'tsx'] }); configureVueProject({ scriptLangs: ['ts', 'tsx'] });
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
@@ -39,20 +35,6 @@ export default defineConfigWithVueTs(
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'], files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
}, },
...pluginOxlint.configs['flat/recommended'], ...pluginOxlint.configs['flat/recommended'],
// https://eslint-plugin-vue-i18n.intlify.dev/started.html#getting-started
...vueI18n.configs.recommended,
{
rules: {
'@intlify/vue-i18n/no-raw-text': 'off',
},
settings: {
'vue-i18n': {
localeDir: './src/locales/**/*.json',
messageSyntaxVersion: '^11.0.0',
},
},
},
{ {
plugins: { plugins: {
import: pluginImport, import: pluginImport,
@@ -71,22 +53,6 @@ export default defineConfigWithVueTs(
}, },
{ {
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
files: ['src/locales/**/*.json'],
languageOptions: { parser: jsoncParser },
plugins: { jsonc: pluginJsonc },
rules: { 'jsonc/sort-keys': 'error' },
},
{
// plugins: {
// '@stylistic': stylistic,
// },
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'vue/block-order': [ 'vue/block-order': [
@@ -101,14 +67,6 @@ export default defineConfigWithVueTs(
], ],
'vue/attributes-order': 'error', 'vue/attributes-order': 'error',
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/padding-line-between-blocks': ['error', 'always'],
// '@stylistic/padding-line-between-statements': [
// 'error',
// { blankLine: 'always', prev: '*', next: ['const', 'let', 'var'] },
// { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
// { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
// ],
}, },
}, },

View File

@@ -11,6 +11,40 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<script>
window.addEventListener('DOMContentLoaded', function () {
window.ontouchstart = function () {};
window.ontouchend = function () {};
});
window.onloadX = function () {
// 禁止双指缩放
document.addEventListener('touchstart', function (event) {
if (event.touches.length > 1) {
event.preventDefault();
}
});
// 禁止双击放大
var lastTouchEnd = 0;
document.addEventListener(
'touchend',
function (event) {
var now = new Date().getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
},
false,
);
// 禁止手势事件
document.addEventListener('gesturestart', function (event) {
event.preventDefault();
});
};
</script>
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
@@ -98,69 +132,35 @@
<!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> --> <!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> -->
<!-- .min.js 是 jsDelivr 的特殊处理 --> <!-- .min.js 是 jsDelivr 的特殊处理 -->
<!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> --> <!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> -->
<script>
window.addEventListener('DOMContentLoaded', function () {
window.ontouchstart = function () {};
window.ontouchend = function () {};
});
window.onloadX = function () {
// 禁止双指缩放
document.addEventListener('touchstart', function (event) {
if (event.touches.length > 1) {
event.preventDefault();
}
});
// 禁止双击放大
var lastTouchEnd = 0;
document.addEventListener(
'touchend',
function (event) {
var now = new Date().getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
},
false,
);
// 禁止手势事件
document.addEventListener('gesturestart', function (event) {
event.preventDefault();
});
};
</script>
<script>
(function (d) {
var config = {
kitId: 'whk2tto',
scriptTimeout: 3000,
async: true,
},
h = d.documentElement,
t = setTimeout(function () {
h.className = h.className.replace(/\bwf-loading\b/g, '') + ' wf-inactive';
}, config.scriptTimeout),
tk = d.createElement('script'),
f = false,
s = d.getElementsByTagName('script')[0],
a;
h.className += ' wf-loading';
tk.src = 'https://use.typekit.net/' + config.kitId + '.js';
tk.async = true;
tk.onload = tk.onreadystatechange = function () {
a = this.readyState;
if (f || (a && a != 'complete' && a != 'loaded')) return;
f = true;
clearTimeout(t);
try {
Typekit.load(config);
} catch (e) {}
};
s.parentNode.insertBefore(tk, s);
}); /* (document) */
</script>
</body> </body>
<script>
(function (d) {
var config = {
kitId: 'whk2tto',
scriptTimeout: 3000,
async: true,
},
h = d.documentElement,
t = setTimeout(function () {
h.className = h.className.replace(/\bwf-loading\b/g, '') + ' wf-inactive';
}, config.scriptTimeout),
tk = d.createElement('script'),
f = false,
s = d.getElementsByTagName('script')[0],
a;
h.className += ' wf-loading';
tk.src = 'https://use.typekit.net/' + config.kitId + '.js';
tk.async = true;
tk.onload = tk.onreadystatechange = function () {
a = this.readyState;
if (f || (a && a != 'complete' && a != 'loaded')) return;
f = true;
clearTimeout(t);
try {
Typekit.load(config);
} catch (e) {}
};
s.parentNode.insertBefore(tk, s);
}); /* (document) */
</script>
</html> </html>

View File

@@ -1,34 +1,28 @@
{ {
"packageManager": "pnpm@10.23.0", "packageManager": "pnpm@10.18.3",
"name": "vue-ts-example-2025", "name": "vue-ts-example-2025",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"_devenginesruntime_docs": "https://pnpm.io/zh/package_json#devenginesruntime", "engines": {
"devEngines": { "node": "^20.19.0 || >=22.12.0"
"runtime": {
"name": "node",
"version": "^24.11.1",
"onFail": "download"
}
}, },
"scripts": { "scripts": {
"all": "run-s lint format:prettier build-only type-check 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",
"preview": "vite preview --port 4731 --host --strictPort", "preview": "vite preview --port 4731 --host --strictPort",
"wrangler:dev": "wrangler dev --port 4732", "wrangler:dev": "wrangler dev --port 4732",
"format:prettier": "prettier --config=.prettierrc.json --cache --write --log-level=warn 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 --config=stylelint.config.mjs --cache --output-file=node_modules/.cache/stylelint/stylelint-report.json --cache-location=node_modules/.cache/stylelint/.stylelintcache --fix --ignore-path=.gitignore '**/*.{css,less,scss,vue}'", "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 --config=eslint.config.ts --concurrency=auto --env-info --cache --cache-location=node_modules/.cache/eslint/.eslintcache", "lint:eslint": "eslint . --fix",
"test:unit:DisableWatch": "vitest --run", "test:unit:DisableWatch": "vitest --run",
"test:playwright:headless": "HEADLESS=true playwright test --quiet", "test:playwright:headless": "HEADLESS=true playwright test --quiet",
"_stylelint-config": "stylelint --config=stylelint.config.mjs --print-config src/styles/scss/global.scss",
"postinstall": "wrangler types", "postinstall": "wrangler types",
"prepare": "husky" "prepare": "husky"
}, },
@@ -44,6 +38,9 @@
"{src/locales-utils,src/locales}/**/*": "node scripts/type-check-for-lint-staged.mjs" "{src/locales-utils,src/locales}/**/*": "node scripts/type-check-for-lint-staged.mjs"
}, },
"pnpm": { "pnpm": {
"overrides": {
"vue-tsc": "$vue-tsc"
},
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",
"esbuild", "esbuild",
@@ -54,113 +51,102 @@
] ]
}, },
"dependencies": { "dependencies": {
"@commitlint/cli": "^20.1.0", "@commitlint/cli": "^20.0.0",
"@commitlint/config-conventional": "^20.0.0", "@commitlint/config-conventional": "^20.0.0",
"@formkit/auto-animate": "^0.9.0", "@formkit/auto-animate": "^0.9.0",
"@pinia/colada": "^0.17.8", "@pinia/colada": "^0.17.4",
"@primeuix/themes": "^1.2.5", "@primeuix/themes": "^1.2.3",
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",
"@unhead/vue": "^2.0.19", "@unhead/vue": "^2.0.14",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^13.9.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jsonc-eslint-parser": "^2.4.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"naive-ui": "^2.43.2", "naive-ui": "^2.43.1",
"pinia": "^3.0.4", "pinia": "^3.0.3",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primelocale": "^2.2.2", "primelocale": "^2.1.7",
"primevue": "^4.4.1", "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.24", "vue": "^3.5.21",
"vue-i18n": "^11.2.1", "vue-i18n": "^11.1.12",
"vue-memoize-dict": "^1.1.3", "vue-memoize-dict": "^1.1.3",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vite-plugin": "^1.15.2", "@cloudflare/vite-plugin": "^1.13.2",
"@commitlint/types": "^20.0.0", "@commitlint/types": "^20.0.0",
"@iconify-json/carbon": "^1.2.14", "@iconify-json/carbon": "^1.2.13",
"@iconify-json/clarity": "^1.2.4", "@iconify-json/clarity": "^1.2.4",
"@iconify-json/line-md": "^1.2.11", "@iconify-json/line-md": "^1.2.11",
"@iconify-json/material-symbols": "^1.2.47", "@iconify-json/material-symbols": "^1.2.42",
"@intlify/eslint-plugin-vue-i18n": "^4.1.0", "@intlify/unplugin-vue-i18n": "^11.0.0",
"@intlify/unplugin-vue-i18n": "^11.0.1", "@playwright/test": "^1.55.0",
"@playwright/test": "^1.56.1", "@prettier/plugin-oxc": "^0.0.4",
"@prettier/plugin-oxc": "^0.0.5", "@primevue/auto-import-resolver": "^4.3.9",
"@primevue/auto-import-resolver": "^4.4.1", "@primevue/metadata": "^4.3.9",
"@primevue/metadata": "^4.4.1",
"@stylelint-types/stylelint-order": "^7.0.0", "@stylelint-types/stylelint-order": "^7.0.0",
"@stylelint-types/stylelint-scss": "^6.11.0", "@stylelint-types/stylelint-scss": "^6.11.0",
"@stylistic/eslint-plugin": "^5.6.1", "@tsconfig/node22": "^22.0.2",
"@tsconfig/node22": "^22.0.5",
"@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/lodash-es": "^4.17.12",
"@types/node": "^24.10.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.2", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.2", "@vitejs/plugin-vue-jsx": "^5.1.1",
"@vitest/eslint-plugin": "^1.4.3", "@vitest/eslint-plugin": "^1.3.9",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"consola": "^3.4.2", "consola": "^3.4.2",
"eslint": "^9.39.1", "eslint": "^9.35.0",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsonc": "^2.21.0", "eslint-plugin-oxlint": "~1.23.0",
"eslint-plugin-oxlint": "~1.29.0", "eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-playwright": "^2.3.0", "eslint-plugin-vue": "~10.5.0",
"eslint-plugin-vue": "~10.6.0", "happy-dom": "^20.0.1",
"happy-dom": "^20.0.10",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^27.2.0", "jsdom": "^27.0.0",
"lint-staged": "^16.2.7", "lint-staged": "^16.1.6",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"oxlint": "~1.29.0", "oxlint": "~1.23.0",
"postcss-html": "^1.8.0", "postcss-html": "^1.8.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"rollup": "^4.53.3", "rollup": "^4.52.5",
"sass-embedded": "^1.93.3", "sass-embedded": "^1.93.2",
"sharp": "^0.34.5", "stylelint": "^16.25.0",
"stylelint": "^16.26.0", "stylelint-config-recess-order": "^7.3.0",
"stylelint-config-recess-order": "^7.4.0",
"stylelint-config-standard": "^39.0.1", "stylelint-config-standard": "^39.0.1",
"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", "svgo": "^4.0.0",
"tinyglobby": "^0.2.15", "tinyglobby": "^0.2.15",
"type-fest": "^5.2.0", "type-fest": "^5.1.0",
"typescript": "~5.9.3", "typescript": "~5.9.2",
"unocss": "^66.5.9", "unocss": "^66.5.1",
"unocss-preset-animations": "^1.3.0", "unocss-preset-animations": "^1.2.1",
"unplugin-auto-import": "^20.2.0", "unplugin-auto-import": "^20.1.0",
"unplugin-icons": "^22.5.0", "unplugin-icons": "^22.2.0",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^29.2.0",
"unplugin-vue-markdown": "^29.2.0", "unplugin-vue-markdown": "^29.2.0",
"unplugin-vue-router": "^0.19.0", "unplugin-vue-router": "^0.16.0",
"vite": "^7.2.4", "vite": "^7.1.5",
"vite-plugin-checker": "^0.11.0", "vite-plugin-checker": "^0.11.0",
"vite-plugin-fake-server": "^2.2.2", "vite-plugin-fake-server": "^2.2.0",
"vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-image-optimizer": "^2.0.2",
"vite-plugin-vue-devtools": "^8.0.5", "vite-plugin-vue-devtools": "^8.0.1",
"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": "^4.0.13", "vitest": "^3.2.4",
"vue-component-type-helpers": "^3.1.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.4", "vue-tsc": "^3.1.0",
"wrangler": "^4.50.0" "wrangler": "^4.37.1"
}, }
"overrides": {
"vue-tsc": "$vue-tsc"
},
"workspaces": [
"packages/*"
]
} }

4818
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,2 @@
packages: packages:
- 'packages/*' - "packages/*"
# shamefullyHoist: false # https://pnpm.io/zh/settings#shamefullyhoist

View File

@@ -1,18 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router';
import AppNaiveUIProvider from './AppNaiveUIProvider.vue'; import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
import A2use from './utils/a2use.vue';
</script> </script>
<template> <template>
<DynamicDialog /> <DynamicDialog />
<ConfirmDialog /> <ConfirmDialog />
<Toast style="z-index: 5000" /> <Toast />
<AppNaiveUIProvider> <AppNaiveUIProvider>
<router-view v-slot="{ Component }"> <!-- <RouterView /> -->
<transition name="fade" mode="out-in">
<component :is="Component" /> <A2use></A2use>
</transition>
</router-view>
</AppNaiveUIProvider> </AppNaiveUIProvider>
</template> </template>

View File

@@ -1,6 +1,5 @@
import { createGetRoutes } from '@/plugins/00.router-plugin';
import type { MenuInst, MenuOption } from 'naive-ui'; import type { MenuInst, MenuOption } from 'naive-ui';
import { createGetRoutes } from 'virtual:meta-layouts';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
@@ -9,7 +8,16 @@ import IconMenuRounded from '~icons/material-symbols/menu-rounded';
export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) { export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) {
const router = useRouter(); const router = useRouter();
const { t, te } = routeI18nInstance.global; const { t, te } = useI18n({
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
return key;
},
fallbackRoot: true,
messages: i18nRouteMessages,
});
// 获取路由表但是不包含布局路由 // 获取路由表但是不包含布局路由
const routes = createGetRoutes(router)(); const routes = createGetRoutes(router)();
@@ -18,15 +26,10 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
const selectedKey = ref(''); const selectedKey = ref('');
watch( watch(
() => router.currentRoute.value, () => router.currentRoute.value.path,
(route) => { (newPath) => {
// 优先使用 activeMenuName通过路由名称解析为路径如果没有则使用当前路径 selectedKey.value = newPath;
const activeMenuPath = route.meta.activeMenuName menuInstRef.value?.showOption(newPath); // 展开菜单,确保设定的元素被显示,如果不传入 key 会展示当前选中元素
? router.resolve({ name: route.meta.activeMenuName }).path
: route.path;
selectedKey.value = activeMenuPath;
menuInstRef.value?.showOption(activeMenuPath); // 展开菜单,确保设定的元素被显示
}, },
{ immediate: true }, { immediate: true },
); );
@@ -110,7 +113,7 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
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) : routeName; let text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
if (import.meta.env.VITE_APP_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}`;

View File

@@ -2,7 +2,6 @@
import LanguageSwitchButton from './components/LanguageSwitchButton.vue'; import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
import ThemeSwitchButton from './components/ThemeSwitchButton.vue'; import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
import ToggleSiderButton from './components/ToggleSiderButton.vue'; import ToggleSiderButton from './components/ToggleSiderButton.vue';
import UserDropdown from './components/UserDropdown.vue';
</script> </script>
<template> <template>
@@ -12,7 +11,6 @@ import UserDropdown from './components/UserDropdown.vue';
<div class="flex items-center"> <div class="flex items-center">
<LanguageSwitchButton /> <LanguageSwitchButton />
<ThemeSwitchButton /> <ThemeSwitchButton />
<UserDropdown />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,45 +0,0 @@
<script setup lang="tsx">
const router = useRouter();
const userStore = useAuthStore();
const dialog = useDialog();
const options = computed(() => [
{
label: userStore.userInfo?.nickname || userStore.userInfo?.username || '用户',
key: 'user',
disabled: true,
},
{
type: 'divider',
key: 'd1',
},
{
label: '退出登录',
key: 'logout',
icon: () => <icon-material-symbols-logout class="w-4 h-4" />,
},
]);
function handleSelect(key: string) {
if (key === 'logout') {
dialog.warning({
title: '退出登录',
content: '确定要退出登录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
userStore.clearToken('用户退出登录');
router.push('/login');
},
});
}
}
</script>
<template>
<NDropdown :options="options" placement="bottom-end" @select="handleSelect">
<NButton quaternary circle>
<icon-material-symbols-account-circle w-5 h-5 />
</NButton>
</NDropdown>
</template>

View File

@@ -49,4 +49,14 @@ const appStore = useAppStore();
#__SCROLL_EL_ID__ { #__SCROLL_EL_ID__ {
@include scrollbar; @include scrollbar;
} }
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style> </style>

View File

@@ -4,9 +4,7 @@
*/ */
import messages from '@intlify/unplugin-vue-i18n/messages'; import messages from '@intlify/unplugin-vue-i18n/messages';
import { createGetRoutes, router } from '@/plugins/00.router-plugin';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { START_LOCATION } from 'vue-router';
const locale = useLocalStorage<string>('app-locale', navigator.language); const locale = useLocalStorage<string>('app-locale', navigator.language);
watchEffect(() => { watchEffect(() => {
@@ -28,52 +26,6 @@ export const i18nInstance = createI18n({
messages, messages,
}); });
export const routeI18nInstance = createI18n({ watchEffect(() => {
legacy: false, // you must set `false`, to use Composition API locale.value = i18nInstance.global.locale.value;
locale: locale.value,
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
if (__DEV__) {
ToastService.add({
severity: 'warn',
summary: '菜单翻译缺失',
detail: `菜单翻译缺失: locale=${locale}, key=${key}`,
life: 5000,
});
}
return key;
},
fallbackRoot: true,
messages: i18nRouteMessages,
}); });
export const routeI18nT = routeI18nInstance.global.t;
watchEffect(
() => {
locale.value = i18nInstance.global.locale.value;
},
{ flush: 'sync' },
);
watch(
() => i18nInstance.global.locale.value,
() => {
const { t, te } = routeI18nInstance.global;
routeI18nInstance.global.locale.value = i18nInstance.global.locale.value;
if (router.currentRoute.value.name && router.currentRoute.value !== START_LOCATION) {
router.currentRoute.value.meta.title = t(router.currentRoute.value.name as string);
}
const routes = createGetRoutes(router)(); // 获取路由表但是不包含布局路由
routes.forEach((route) => {
const routeName = route.name as string;
route.meta.title = te(routeName) ? t(routeName) : routeName;
});
},
{ immediate: true, flush: 'sync' },
);

View File

@@ -1,22 +1,13 @@
/*eslint sort-keys: "error"*/
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
export default { export default {
Root: 'Index',
$Path: '$Path', $Path: '$Path',
Demos: 'Demos', Demos: 'Demos',
DemosApiDemo: 'API Demo', DemosApiDemo: 'API Demo',
DemosCounterDemo: 'Counter Demo', DemosCounterDemo: 'Counter Demo',
DemosCreate: 'Create Demo',
DemosI18nDemo: 'i18n Demo', DemosI18nDemo: 'i18n Demo',
DemosNaiveUiDemo: 'Naive UI Demo', DemosNaiveUiDemo: 'Naive UI Demo',
DemosPrimevueDemo: 'PrimeVue Demo', DemosPrimevueDemo: 'PrimeVue Demo',
DemosWebsocketDemo: 'WebSocket Demo', DemosWebsocketDemo: 'WebSocket Demo',
Home: 'Home', Home: 'Home',
Login: 'Login', Login: 'Login',
Root: 'Index',
} satisfies PageTitleLocalizations; } satisfies PageTitleLocalizations;

View File

@@ -1,7 +1,7 @@
import type { I18nOptions } from 'vue-i18n'; import type { I18nOptions } from 'vue-i18n';
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], { const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
eager: true /* true 为同步false 为异步 */, eager: true,
import: 'default', import: 'default',
}); });

View File

@@ -1,22 +1,13 @@
/*eslint sort-keys: "error"*/
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
export default { export default {
Root: '根 (Gēn)',
$Path: '$Path', $Path: '$Path',
Demos: '示例演示', Demos: '示例演示',
DemosApiDemo: 'API 调用示例', DemosApiDemo: 'API 调用示例',
DemosCounterDemo: '点击计数器', DemosCounterDemo: '点击计数器',
DemosCreate: '创建示例',
DemosI18nDemo: '国际化示例', DemosI18nDemo: '国际化示例',
DemosNaiveUiDemo: 'Naive UI 组件示例', DemosNaiveUiDemo: 'Naive UI 组件示例',
DemosPrimevueDemo: 'PrimeVue 组件示例', DemosPrimevueDemo: 'PrimeVue 组件示例',
DemosWebsocketDemo: 'WebSocket 示例', DemosWebsocketDemo: 'WebSocket 示例',
Home: '首页', Home: '首页',
Login: '登录', Login: '登录',
Root: '根 (Gēn)',
} satisfies PageTitleLocalizations; } satisfies PageTitleLocalizations;

View File

@@ -1,10 +1,10 @@
{ {
"page": { "page": {
"i18n-demo": { "i18n-demo": {
"change-language": "Change Language", "title": "Vue I18n Demo",
"current-language": "Current Language", "current-language": "Current Language",
"hello": "Hello, {name}!", "change-language": "Change Language",
"title": "Vue I18n Demo" "hello": "Hello, {name}!"
} }
} }
} }

View File

@@ -1,10 +1,10 @@
{ {
"page": { "page": {
"i18n-demo": { "i18n-demo": {
"change-language": "切换语言", "title": "Vue I18n 示例",
"current-language": "当前语言", "current-language": "当前语言",
"hello": "你好, {name}", "change-language": "切换语言",
"title": "Vue I18n 示例" "hello": "你好, {name}"
} }
} }
} }

View File

@@ -1,14 +1,14 @@
import './styles/index.ts'; import './styles/index.ts';
import { LogLevels } from 'consola'; import { LogLevels } from 'consola';
import App from './App.vue'; import App from './App.vue';
import { setupPlugins } from './plugins'; import { setupPlugins } from './plugins';
consola.level = LogLevels.verbose; consola.level = LogLevels.verbose;
const app = createApp(App); const autoInstallModules = import.meta.glob('./plugins/!(index).ts', {
if (__DEV__) Object.defineProperty(window, '__APP__', { value: app }); eager: true /* true 为同步false 为异步 */,
setupPlugins(app); });
const app = setupPlugins(createApp(App), autoInstallModules);
await new Promise((resolve) => setTimeout(resolve, 280)); await new Promise((resolve) => setTimeout(resolve, 280));
app.mount('#app'); app.mount('#app');

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
definePage({
// name: false,
meta: {
_file: '(layout-group).page.vue',
},
});
</script>
<template>
<div>
<div> b:</div>
<RouterView />
</div>
</template>

View File

@@ -1,12 +0,0 @@
<script setup lang="ts">
definePage({
alias: '/',
meta: {
_file: '(layout)/a.page.vue',
},
});
</script>
<template>
<div>a.page.vue</div>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
definePage({
meta: {
_file: '(layout)/b.page.vue',
},
});
</script>
<template>
<div>b.page.vue</div>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
definePage({
meta: {
_file: '(ccc)/c.page.vue',
},
});
</script>
<template>
<div>c.page.vue</div>
</template>

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
definePage({
meta: {
_file: '(home).page.vue',
},
});
</script>
<template>
<div>
<n-button @click="$router.push({ name: 'DemosCreate' })">DemosCreate</n-button>
</div>
</template>

View File

@@ -1,87 +1,5 @@
<script setup lang="ts"> <script setup lang="ts"></script>
definePage({ meta: { ignoreAuth: true, layout: false } });
const router = useRouter();
const userStore = useAuthStore();
const message = useMessage();
const formValue = ref({
username: 'admin',
password: 'admin',
});
const loading = ref(false);
async function handleLogin() {
if (!formValue.value.username || !formValue.value.password) {
message.warning('请输入用户名和密码');
return;
}
loading.value = true;
try {
const result = await userStore.login(formValue.value.username, formValue.value.password);
if (result.success) {
message.success('登录成功');
router.push('/');
} else {
message.error(result.message || '登录失败');
}
} catch {
message.error('登录异常');
} finally {
loading.value = false;
}
}
</script>
<template> <template>
<div class="login-page"> <div>Login</div>
<NCard class="login-card" title="用户登录">
<NForm :model="formValue" label-placement="left" label-width="80">
<NFormItem label="用户名" path="username">
<NInput
v-model:value="formValue.username"
placeholder="请输入用户名"
@keyup.enter="handleLogin"
/>
</NFormItem>
<NFormItem label="密码" path="password">
<NInput
v-model:value="formValue.password"
type="password"
show-password-on="click"
placeholder="请输入密码"
@keyup.enter="handleLogin"
/>
</NFormItem>
<NFormItem :show-label="false">
<NButton type="primary" block :loading="loading" @click="handleLogin"> 登录 </NButton>
</NFormItem>
</NForm>
<div class="login-hint">
<NText depth="3">提示用户名和密码均为 admin</NText>
</div>
</NCard>
</div>
</template> </template>
<style scoped lang="scss">
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
width: 400px;
max-width: 90%;
}
.login-hint {
margin-top: 16px;
text-align: center;
}
</style>

View File

@@ -1,29 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ path: string }>(); defineProps<{ path: string }>();
declare global {
interface Window {
stack?: ReturnType<typeof createStackGuard>;
}
}
const stack = window?.stack;
const canGoBack = stack && stack.length > 1;
const router = useRouter();
function handleBack() {
if (canGoBack) {
router.back();
} else {
router.push('/');
}
}
</script> </script>
<template> <template>
<main flex-1 class="flex flex-col items-center justify-center h-full space-y-4"> <main flex-1 class="flex flex-col items-center justify-center h-full space-y-4">
<h1>Not Found</h1> <h1>Not Found</h1>
<p>{{ path }} does not exist.</p> <p>{{ path }} does not exist.</p>
<Button @click="handleBack">{{ canGoBack ? 'Back' : 'Home' }}</Button> <Button @click="$router.back()">Back</Button>
</main> </main>
</template> </template>

View File

@@ -1,7 +0,0 @@
<script setup lang="ts">
import TheBaseLayout from '@/layouts/base-layout/the-base-layout.vue';
</script>
<template>
<TheBaseLayout></TheBaseLayout>
</template>

View File

@@ -1,12 +0,0 @@
<script setup lang="ts">
definePage({
meta: {
hideInMenu: true,
activeMenuName: 'Demos',
},
});
</script>
<template>
<div>create.page.vue</div>
</template>

View File

@@ -26,12 +26,5 @@ function setLocale(newLocale: 'en-US' | 'zh-CN') {
<n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button> <n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button>
</n-space> </n-space>
</n-card> </n-card>
<!-- 这里响应式有问题: -->
<n-p> $route.meta.title: {{ $route.meta.title }} </n-p>
<!-- 这样才正常 -->
<n-p>
routeI18nInstance.global.t($route.name): {{ routeI18nInstance.global.t($route.name) }}
</n-p>
</div> </div>
</template> </template>

View File

@@ -24,7 +24,6 @@ const FComponent: FunctionalComponent<{ prop: string }> = (props /* context */)
</> </>
); );
</script> </script>
<template> <template>
<NCard title="函数式组件TSX示例"> <NCard title="函数式组件TSX示例">
<FComponent prop="some-prop-value" /> <FComponent prop="some-prop-value" />

View File

@@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialog, useMessage, useModal } from 'naive-ui';
import type { MessageType } from 'naive-ui'; import type { MessageType } from 'naive-ui';
import { useDialog, useMessage } from 'naive-ui';
definePage({ meta: {} }); definePage({ meta: {} });
const message = useMessage(); const message = useMessage();
const dialog = useDialog(); const dialog = useDialog();
const modal = useModal();
const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[]; const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[];
const dialogTypes = ['info', 'success', 'warning', 'error'] as const; const dialogTypes = ['info', 'success', 'warning', 'error'] as const;
@@ -37,10 +38,11 @@ const openDialog = (type: (typeof dialogTypes)[number]) => {
}; };
const openModal = () => { const openModal = () => {
window.$nModal!.create({ modal.create({
title: '命令式 Modal 示例', title: '命令式 Modal 示例',
content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。', content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。',
preset: 'dialog', preset: 'dialog',
maskClosable: false,
onPositiveClick: () => { onPositiveClick: () => {
message.success('点击了确定'); message.success('点击了确定');
}, },
@@ -60,6 +62,7 @@ const openModal = () => {
<NAlert title="信息" type="info" :bordered="false"> <NAlert title="信息" type="info" :bordered="false">
演示 Naive UI 各种组件的使用方法和功能特性 演示 Naive UI 各种组件的使用方法和功能特性
</NAlert> </NAlert>
<NCard title="Message 消息" class="mt-4"> <NCard title="Message 消息" class="mt-4">
<NSpace> <NSpace>
<NButton <NButton

7
src/pages/index.page.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div>
<h1>Index Page</h1>
</div>
</template>

View File

@@ -1,6 +1,5 @@
export function install({ app }: { app: import('vue').App<Element> }) { export function install({ app }: { app: import('vue').App<Element> }) {
app.config.globalProperties.__DEV__ = app.config.globalProperties.__DEV__ = __DEV__;
__DEV__; /* vite.config.ts: define: { __DEV__: JSON.stringify(!isBuild) } */
app.config.errorHandler = (error, instance, info) => { app.config.errorHandler = (error, instance, info) => {
console.error('Global error:', error); console.error('Global error:', error);
@@ -11,4 +10,17 @@ export function install({ app }: { app: import('vue').App<Element> }) {
// 2. 显示全局错误提示 // 2. 显示全局错误提示
// 3. 进行错误分析和处理 // 3. 进行错误分析和处理
}; };
// if (import.meta.env.MODE === 'development' && '1' === ('2' as never)) {
// // TODO: https://github.com/hu3dao/vite-plugin-debug/
// // https://eruda.liriliri.io/zh/docs/#快速上手
// import('eruda').then(({ default: eruda }) => {
// eruda.init({
// defaults: {
// transparency: 0.9,
// },
// })
// /* eruda.show(); */
// })
// }
} }

View File

@@ -1,23 +1,20 @@
import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders'; import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
// import { setupLayouts } from 'virtual:meta-layouts'; import { setupLayouts } from 'virtual:meta-layouts';
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import type { Router } from 'vue-router'; import type { RouteNamedMap } from 'vue-router/auto-routes';
import { handleHotUpdate, routes } from 'vue-router/auto-routes'; import { routes, handleHotUpdate } from 'vue-router/auto-routes';
// const setupLayoutsResult = setupLayouts(routes); const setupLayoutsResult = setupLayouts(routes);
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: routes /* ?? setupLayoutsResult */, routes: /* routes ?? */ setupLayoutsResult,
scrollBehavior: (_to, _from, savedPosition) => { scrollBehavior: (_to, _from, savedPosition) => {
return savedPosition ?? { left: 0, top: 0 }; return savedPosition ?? { left: 0, top: 0 };
}, },
strict: true, strict: true,
}); });
router.isReady().then(() => {
console.debug('✅ [router is ready]');
});
router.onError((error) => { router.onError((error) => {
console.debug('🚨 [router error]:', error); console.debug('🚨 [router error]:', error);
}); });
@@ -36,29 +33,50 @@ export function install({ app }: { app: import('vue').App<Element> }) {
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整 // 警告:路由守卫的创建顺序会影响执行流程,请勿调整
createNProgressGuard(router); createNProgressGuard(router);
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router); if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
Object.assign(window, { stack: createStackGuard(router) }); Object.assign(globalThis, { stack: createStackGuard(router) });
// >>>
Object.values(
import.meta.glob<{
createGuard?: (router: Router) => void;
}>('./router-guard/*.ts', { eager: true /* true 为同步false 为异步 */ }),
).forEach((module) => {
module.createGuard?.(router);
});
// <<<
} }
export function createGetRoutes(router: Router) { declare module 'vue-router' {
const routes = router.getRoutes(); /* definePage({ meta: { title: '示例演示' } }); */
return () => routes.filter((route) => !route.meta.isLayout); interface RouteMeta {
/**
* @description 是否在菜单中隐藏
*/
hideInMenu?: boolean;
/**
* @description 菜单标题
* @deprecated //!⚠️请通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
*/
title?: string;
/**
* @description 使用的布局,设置为 false 则表示不使用布局
*/
layout?: string | false;
/**
* @description 菜单项是否渲染为可点击链接,默认为 true
* - true: 使用 RouterLink 包装,可点击跳转
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
*/
link?: boolean;
/**
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
*/
order?: number;
}
} }
if (__DEV__) Object.assign(window, { router }); export { router, setupLayoutsResult };
declare global {
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
}
if (__DEV__) Object.assign(globalThis, { router });
// This will update routes at runtime without reloading the page // This will update routes at runtime without reloading the page
if (import.meta.hot) { if (import.meta.hot) {
handleHotUpdate(router); handleHotUpdate(router);
} }
export { router };

View File

@@ -1,50 +0,0 @@
import type { RouteNamedMap } from 'vue-router/auto-routes';
declare global {
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
}
declare module 'vue-router' {
/* definePage({ meta: { title: '示例演示' } }); */
interface RouteMeta {
/**
* @description 是否在菜单中隐藏
*/
hideInMenu?: boolean;
/**
* @description 菜单标题 // !⚠️通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
*/
title?: string;
/**
* @description 使用的布局,设置为 false 则表示不使用布局
*/
layout?: string | false;
/**
* @description 菜单项是否渲染为可点击链接,默认为 true
* - true: 使用 RouterLink 包装,可点击跳转
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
*/
link?: boolean;
/**
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
*/
order?: number;
/**
* @description 是否忽略权限,默认为 false
*/
ignoreAuth?: boolean;
/**
* @description 当前路由激活时应该高亮的菜单项(通过路由名称指定)
* - 用于隐藏在菜单中的子页面,指定其父级菜单项应该高亮
* - 使用路由名称而非路径,提供更好的类型安全和重构友好性
* - 例如:`activeMenuName: 'Demos'` 会高亮 Demos 菜单项
*/
activeMenuName?: keyof RouteNamedMap;
}
}

View File

@@ -5,21 +5,12 @@
import Aura from '@primeuix/themes/aura'; 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 type { PrimeVueConfiguration } from 'primevue/config';
import StyleClass from 'primevue/styleclass'; import StyleClass from 'primevue/styleclass';
import ToastService from 'primevue/toastservice'; 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);
// https://github.com/primefaces/primevue/blob/afe6f58ae55e9caf7f9bc094cd453a21a6113001/packages/core/src/config/PrimeVue.js
app.use(PrimeVue, { app.use(PrimeVue, {
zIndex: {
modal: 5100,
overlay: 5000,
menu: 5000,
tooltip: 5100,
},
locale: { locale: {
...zhCN['zh-CN'], ...zhCN['zh-CN'],
completed: '已上传', completed: '已上传',
@@ -34,6 +25,6 @@ export function install({ app }: { app: import('vue').App<Element> }) {
}, },
preset: Aura, preset: Aura,
}, },
} satisfies PrimeVueConfiguration); });
app.use(ToastService); app.use(ToastService);
} }

View File

@@ -4,18 +4,13 @@
type UserPlugin = (ctx: UserPluginContext) => void; type UserPlugin = (ctx: UserPluginContext) => void;
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin }; type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
type UserPluginContext = { app: import('vue').App<Element> }; type UserPluginContext = { app: import('vue').App<Element> };
export function setupPlugins(
const autoInstallModules: AutoInstallModule = import.meta.glob( app: import('vue').App,
['./*.ts', '!./**/*.types.ts', '!./index.ts'], modules: AutoInstallModule | Record<string, unknown>,
{ ) {
eager: true /* true 为同步false 为异步 */, console.group('🔌 Plugins');
}, for (const path in modules) {
); const module = modules[path] as AutoInstallModule;
export function setupPlugins(app: import('vue').App) {
console.group(`🔌 Installing ${Object.keys(autoInstallModules).length} plugins`);
for (const path in autoInstallModules) {
const module = autoInstallModules[path] as AutoInstallModule;
if (module.install) { if (module.install) {
module.install({ app }); module.install({ app });
console.debug(`%c✔ ${path}`, 'color: #07a'); console.debug(`%c✔ ${path}`, 'color: #07a');

View File

@@ -1,28 +0,0 @@
import type { Router } from 'vue-router';
export function createGuard(router: Router) {
router.beforeEach(async (to /* , from */) => {
const userStore = useAuthStore();
if (to.name === 'Login') {
userStore.clearToken('User navigated to login page');
}
if (to.meta.ignoreAuth) {
return true;
}
if (!userStore.isLoggedIn) {
console.debug('🔑 [permission-guard] 用户未登录,重定向到登录页');
return { name: 'Login' };
}
});
router.beforeResolve(async (/* to, from */) => {
const userStore = useAuthStore();
if (userStore.isLoggedIn && !userStore.userInfo) {
console.debug('🔑 [permission-guard] 用户信息不存在,尝试获取用户信息');
await userStore.fetchUserInfo();
}
});
}

View File

@@ -1,16 +1,12 @@
import type { BasicColorSchema } from '@vueuse/core';
import { useLocalStorage, useMediaQuery } from '@vueuse/core'; import { useLocalStorage, useMediaQuery } from '@vueuse/core';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed } from 'vue'; import { computed } from 'vue';
// >>>>> // >>>>>
// https://vueuse.org/core/useColorMode/#advanced-usage // https://vueuse.org/core/useColorMode/#advanced-usage
const { system, store: themeMode } = useColorMode<BasicColorSchema>({ const { system, store: themeMode } = useColorMode({
selector: 'html',
attribute: 'class',
modes: { light: '', dark: 'app-dark', auto: '' }, modes: { light: '', dark: 'app-dark', auto: '' },
disableTransition: false, disableTransition: false,
initialValue: 'auto',
}); });
const { state, next: cycleTheme } = useCycleList(['light', 'dark', 'auto'] as const, { const { state, next: cycleTheme } = useCycleList(['light', 'dark', 'auto'] as const, {
initialValue: themeMode, initialValue: themeMode,

View File

@@ -1,45 +0,0 @@
export const useAuthStore = defineStore('auth', () => {
const token = useLocalStorage<string | null>('auth-token', null);
const userInfo = ref<Record<string, any> | null>(null);
const isLoggedIn = computed(() => !!token.value);
function clearToken(reason?: string) {
consola.info('🚮 [auth-store] clear: ', reason);
token.value = null;
userInfo.value = null;
}
async function login(username: string, password: string) {
// 模拟登录延迟
await new Promise((resolve) => setTimeout(resolve, 500));
// 模拟验证
if (username === 'admin' && password === 'admin') {
token.value = `mock-token-${Date.now()}`;
await fetchUserInfo();
return { success: true };
}
return { success: false, message: '用户名或密码错误' };
}
async function fetchUserInfo() {
if (!token.value) {
return;
}
// 模拟获取用户信息延迟
await new Promise((resolve) => setTimeout(resolve, 300));
// 模拟从服务器获取用户信息
userInfo.value = {
id: 1,
username: 'admin',
nickname: '管理员',
roles: ['admin'],
};
}
return { token, isLoggedIn, userInfo, clearToken, login, fetchUserInfo };
});

View File

@@ -1,9 +0,0 @@
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@@ -1,8 +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 'primeicons/primeicons.css';
import './reset-primevue.css';
import './css/reset-primevue.css';
import './css/transition.css';
import 'virtual:uno.css'; import 'virtual:uno.css';

90
src/utils/a2use.vue Normal file
View 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>

View 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,
};
}

View File

@@ -14,14 +14,7 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"]
"~/*": ["./src/*"]
} }
},
"vueCompilerOptions": {
"plugins": [
"unplugin-vue-router/volar/sfc-route-blocks",
"unplugin-vue-router/volar/sfc-typed-router"
]
} }
} }

89
typed-router.d.ts vendored
View File

@@ -2,7 +2,7 @@
/* prettier-ignore */ /* prettier-ignore */
// @ts-nocheck // @ts-nocheck
// noinspection ES6UnusedImports // noinspection ES6UnusedImports
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !! // Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file. // It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
@@ -28,28 +28,6 @@ declare module 'vue-router/auto-routes' {
'/', '/',
Record<never, never>, Record<never, never>,
Record<never, never>, Record<never, never>,
| 'RootA'
| 'RootB'
>,
'RootA': RouteRecordInfo<
'RootA',
'/a',
Record<never, never>,
Record<never, never>,
| never
>,
'RootB': RouteRecordInfo<
'RootB',
'/b',
Record<never, never>,
Record<never, never>,
| never
>,
'RootC': RouteRecordInfo<
'RootC',
'/c',
Record<never, never>,
Record<never, never>,
| never | never
>, >,
'$Path': RouteRecordInfo< '$Path': RouteRecordInfo<
@@ -59,20 +37,6 @@ declare module 'vue-router/auto-routes' {
{ path: ParamValue<false> }, { path: ParamValue<false> },
| never | never
>, >,
'DemosParent': RouteRecordInfo<
'DemosParent',
'/demos',
Record<never, never>,
Record<never, never>,
| 'Demos'
| 'DemosApiDemo'
| 'DemosCounterDemo'
| 'DemosCreate'
| 'DemosI18nDemo'
| 'DemosNaiveUiDemo'
| 'DemosPrimevueDemo'
| 'DemosWebsocketDemo'
>,
'Demos': RouteRecordInfo< 'Demos': RouteRecordInfo<
'Demos', 'Demos',
'/demos', '/demos',
@@ -94,13 +58,6 @@ declare module 'vue-router/auto-routes' {
Record<never, never>, Record<never, never>,
| never | never
>, >,
'DemosCreate': RouteRecordInfo<
'DemosCreate',
'/demos/create',
Record<never, never>,
Record<never, never>,
| never
>,
'DemosI18nDemo': RouteRecordInfo< 'DemosI18nDemo': RouteRecordInfo<
'DemosI18nDemo', 'DemosI18nDemo',
'/demos/i18n-demo', '/demos/i18n-demo',
@@ -156,29 +113,9 @@ declare module 'vue-router/auto-routes' {
* @internal * @internal
*/ */
export interface _RouteFileInfoMap { export interface _RouteFileInfoMap {
'src/pages-with-layout/(layout-group).page.vue': { 'src/pages/index.page.vue': {
routes: routes:
| 'Root' | 'Root'
| 'RootA'
| 'RootB'
views:
| 'default'
}
'src/pages-with-layout/(layout-group)/a.page.vue': {
routes:
| 'RootA'
views:
| never
}
'src/pages-with-layout/(layout-group)/b.page.vue': {
routes:
| 'RootB'
views:
| never
}
'src/pages/(ccc)/c.page.vue': {
routes:
| 'RootC'
views: views:
| never | never
} }
@@ -188,20 +125,6 @@ declare module 'vue-router/auto-routes' {
views: views:
| never | never
} }
'src/pages/demos.page.vue': {
routes:
| 'Demos'
| 'DemosApiDemo'
| 'DemosCounterDemo'
| 'DemosCreate'
| 'DemosI18nDemo'
| 'DemosNaiveUiDemo'
| 'DemosParent'
| 'DemosPrimevueDemo'
| 'DemosWebsocketDemo'
views:
| 'default'
}
'src/pages/demos/index.page.vue': { 'src/pages/demos/index.page.vue': {
routes: routes:
| 'Demos' | 'Demos'
@@ -220,19 +143,13 @@ declare module 'vue-router/auto-routes' {
views: views:
| never | never
} }
'src/pages/demos/create.page.vue': {
routes:
| 'DemosCreate'
views:
| never
}
'src/pages/demos/i18n-demo.page.vue': { 'src/pages/demos/i18n-demo.page.vue': {
routes: routes:
| 'DemosI18nDemo' | 'DemosI18nDemo'
views: views:
| never | never
} }
'src/pages/demos/naive-ui-demo/index.page.vue': { 'src/pages/demos/naive-ui-demo.page.vue': {
routes: routes:
| 'DemosNaiveUiDemo' | 'DemosNaiveUiDemo'
views: views:

View File

@@ -1,6 +1,5 @@
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'; import vueJsx from '@vitejs/plugin-vue-jsx';
import consola from 'consola';
import { getPascalCaseRouteName } from 'unplugin-vue-router'; import { getPascalCaseRouteName } from 'unplugin-vue-router';
import vueRouter from 'unplugin-vue-router/vite'; import vueRouter from 'unplugin-vue-router/vite';
import type { PluginOption } from 'vite'; import type { PluginOption } from 'vite';
@@ -19,13 +18,8 @@ export default [
exclude: ['**/__*', '**/__*/**/*'], exclude: ['**/__*', '**/__*/**/*'],
extensions: ['.page.vue', '.page.md'], extensions: ['.page.vue', '.page.md'],
getRouteName: (routeNode) => getPascalCaseRouteName(routeNode), getRouteName: (routeNode) => getPascalCaseRouteName(routeNode),
logs: true, logs: false,
routesFolder: ['src/pages', 'src/pages-with-layout'], routesFolder: 'src/pages',
extendRoute(route) {
consola.info(`route.name :>> `, route.name);
console.debug(`route.meta :>> `, route.meta);
console.debug(`route.path :>> `, route.path);
},
}), }),
}, },
}), }),

View File

@@ -26,73 +26,7 @@ function IndexHtmlPlugin(): PluginOption {
}; };
} }
function ___(): PluginOption {
// https://github.com/hu3dao/vite-plugin-debug/blob/2935025e8ce082b9a5aef04766bcae3e996b3e55/src/index.ts
return {
name: 'vant-touch-emulator-online',
apply: 'build',
transformIndexHtml(html) {
return {
html,
tags: [
{
tag: 'script',
attrs: {
src: 'https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js',
// 这里的 `.min.js` 是 jsDelivr 的特殊处理
// src: 'https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js',
},
injectTo: 'body',
},
// >>>>> eruda
{
tag: 'script',
attrs: {
src: 'https://testingcf.jsdelivr.net/npm/eruda/eruda.js',
},
injectTo: 'body',
},
{
tag: 'script',
children: `eruda.init();`,
injectTo: 'body',
},
// https://eruda.liriliri.io/zh/docs/#快速上手
// import('eruda').then(({ default: eruda }) => {
// eruda.init({
// defaults: {
// transparency: 0.9,
// },
// })
// /* eruda.show(); */
// })
// }
// <<<<<
// >>>>> vConsole
{
tag: 'script',
attrs: {
src: 'https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js',
},
injectTo: 'body',
},
{
tag: 'script',
children: `new window.VConsole();`,
injectTo: 'body',
},
// <<<<<
],
};
},
};
}
export function loadPlugin(_configEnv: ConfigEnv): PluginOption { export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
// return [___()];
const env = loadEnv(_configEnv.mode, process.cwd()); const env = loadEnv(_configEnv.mode, process.cwd());
if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin(); if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin();
} }

View File

@@ -1,23 +1,20 @@
import consola from 'consola'; import consola from 'consola';
import type { ConfigEnv, PluginOption } from 'vite'; import type { ConfigEnv, PluginOption } from 'vite';
import { loadEnv } from 'vite';
import vueDevTools from 'vite-plugin-vue-devtools'; import vueDevTools from 'vite-plugin-vue-devtools';
export function loadPlugin(configEnv: ConfigEnv): PluginOption { export function loadPlugin(configEnv: ConfigEnv): PluginOption {
if (configEnv.mode !== 'development') { const env = loadEnv(configEnv.mode, process.cwd());
consola.info('vue-devtools 插件仅在开发模式下使用。');
if (configEnv.command === 'build') {
consola.info('vue-devtools plugin is not used in build mode.');
return []; return [];
} }
let launchEditor = 'code'; if (env.VITE_APP_ENABLE_VUE_DEVTOOLS !== 'true') {
consola.info('vue-devtools plugin disabled by env');
if (process.env.TERM_PROGRAM_VERSION?.toLowerCase()?.includes('insider')) { return [];
consola.info('检测到 VSCode Insiders 环境。');
launchEditor = 'code-insiders';
} }
return [ return [vueDevTools()];
vueDevTools({
launchEditor: launchEditor,
}),
];
} }

View File

@@ -17,8 +17,6 @@ export async function loadPlugins(configEnv: ConfigEnv): Promise<PluginOption[]>
ignore: [ ignore: [
'**/*.d.ts', '**/*.d.ts',
'**/*.disabled.ts', '**/*.disabled.ts',
'**/*.x.ts',
'**/*.X.ts',
'**/x-*.ts', // 禁用以 x- 开头的插件文件 '**/x-*.ts', // 禁用以 x- 开头的插件文件
'**/_*', '**/_*',
], ],

View File

@@ -3,7 +3,7 @@ import { createViteProxy } from 'utils4u/vite';
import { defineConfig, loadEnv } from 'vite'; import { defineConfig, loadEnv } from 'vite';
import { loadPlugins } from './vite-plugins/_loadPlugins'; import { loadPlugins } from './vite-plugins/_loadPlugins';
import { optimizeDeps } from './vite.config.optimizeDeps'; import { optimizeDeps } from './vite.config.optimizeDeps';
// import { viteConfigRollupOptions } from './vite.config.rollup'; import { viteConfigRollupOptions } from './vite.config.rollup';
import consola from 'consola'; import consola from 'consola';
// https://vite.dev/config/ // https://vite.dev/config/
@@ -24,7 +24,7 @@ export default defineConfig(async (configEnv) => {
build: { build: {
minify: env.VITE_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser' minify: env.VITE_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
sourcemap: env.VITE_BUILD_SOURCE_MAP === 'true', sourcemap: env.VITE_BUILD_SOURCE_MAP === 'true',
// rollupOptions: viteConfigRollupOptions, rollupOptions: viteConfigRollupOptions,
}, },
css: { css: {
devSourcemap: true, devSourcemap: true,
@@ -40,7 +40,6 @@ export default defineConfig(async (configEnv) => {
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'~': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
define: { define: {

File diff suppressed because it is too large Load Diff