62 Commits

Author SHA1 Message Date
b2cafbb09b chore(deps): update pnpm to v10.20.0
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 4m54s
CI/CD Pipeline / build-and-deploy (push) Failing after 4m25s
2025-11-04 01:09:37 +08:00
严浩
5bbbf488fe feat(eslint): 添加 jsonc 插件并启用 JSON 文件键排序规则
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 2m54s
CI/CD Pipeline / build-and-deploy (push) Failing after 4m9s
新增 `eslint-plugin-jsonc` 插件,并在 `.json` 文件中启用 `jsonc/sort-keys` 规则,
以确保本地化文件中的键名按字母顺序排列。此举有助于减少多人协作时的合并冲突,
同时提升代码一致性和可维护性。

此外,调整了 VS Code 配置项顺序以优化读写逻辑,并更新相关依赖版本。
2025-11-04 00:52:22 +08:00
严浩
acd7c0db13 feat(i18n): 强制路由标题键名按字母顺序排序
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m18s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m41s
2025-11-03 23:41:36 +08:00
1ad46a62fd chore(deps): update dependency vite-plugin-image-optimizer to v2.0.3
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 2m18s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m47s
2025-11-03 20:47:17 +08:00
bb02b796aa chore(deps): update dependency unplugin-vue-router to v0.16.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 2m18s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m26s
2025-11-03 17:45:06 +08:00
严浩
a4ea7ce56e fix(useMetaLayoutsMenuOptions): 移除调试日志输出
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m16s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m40s
2025-11-03 16:42:40 +08:00
严浩
166d76d980 feat(router): 支持通过 activeMenuName 指定菜单高亮路径
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m18s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m24s
2025-11-03 15:54:11 +08:00
严浩
f9f82e4d29 feat(i18n): 引入 routeI18nInstance 以支持路由菜单标题的多语言处理
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 5m9s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m48s
2025-11-03 15:03:22 +08:00
严浩
b4fcde324d feat(auth): 添加用户认证模块与登录页面
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m9s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m38s
2025-11-03 13:46:47 +08:00
严浩
b669889bb0 refactor(plugins): 重构插件自动加载逻辑
Some checks failed
CI/CD Pipeline / playwright (push) Failing after 7m17s
CI/CD Pipeline / build-and-deploy (push) Has been skipped
2025-11-03 11:52:54 +08:00
严浩
8a3b9e03fd feat(router): 添加路由就绪等待逻辑
Some checks failed
CI/CD Pipeline / playwright (push) Failing after 7m9s
CI/CD Pipeline / build-and-deploy (push) Has been skipped
2025-11-03 11:41:35 +08:00
ec02658ede chore(deps): update dependency happy-dom to v20.0.7
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 2m18s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m23s
2025-11-03 07:18:20 +08:00
严浩
bd9acc06a8 feat(vite-plugins): 添加 vant-touch-emulator 和 eruda 脚本支持
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m20s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m21s
测试最新依赖 / build-and-test (push) Successful in 2m10s
测试最新依赖 / playwright (push) Successful in 2m15s
2025-11-03 01:16:27 +08:00
严浩
f790691d5a fix(useMetaLayoutsNMenuOptions): 优化路由标题设置逻辑
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m4s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m39s
2025-11-02 23:05:20 +08:00
严浩
838d5cfb6e fix(useMetaLayoutsMenuOptions): 调整路由标题设置逻辑以支持多语言
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m17s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m27s
2025-11-02 22:33:06 +08:00
ac544a8ff5 chore(deps): update dependency wrangler to v4.45.2
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 2m18s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m25s
2025-11-02 18:20:03 +08:00
严浩
e7a4a7aff9 fix: 优化404页面返回按钮逻辑
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m49s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m27s
2025-11-02 17:19:16 +08:00
严浩
0d1a20d88d build(ci-cd): 添加构建时间环境变量支持
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m49s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m3s
2025-11-02 16:50:22 +08:00
19aaddc4e2 chore(deps): update dependency unplugin-icons to v22.5.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 3m42s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m20s
2025-11-02 12:19:18 +08:00
1e24193b84 chore(deps): update dependency primelocale to v2.2.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 3m39s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m41s
2025-11-02 09:14:20 +08:00
cdec0c1a23 chore(deps): update dependency @types/node to v22.18.13
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 2m5s
CI/CD Pipeline / build-and-deploy (push) Successful in 3m49s
2025-11-02 06:36:37 +08:00
94cdecb075 chore(deps): update all non-major dependencies
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 4m6s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m24s
2025-11-02 04:01:43 +08:00
严浩
7f0cf5dd8f feat(package): 添加 sharp 依赖,版本为 0.34.4
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 5m0s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m17s
测试最新依赖 / build-and-test (push) Successful in 1m58s
测试最新依赖 / playwright (push) Successful in 2m39s
2025-10-31 19:15:38 +08:00
严浩
c8b8a3caa4 fix(use-safe-n-form): 更改 formRef 为 formInst
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m38s
CI/CD Pipeline / build-and-deploy (push) Successful in 3m46s
2025-10-31 13:43:27 +08:00
严浩
48eb653f1a feat(demo): 添加 Naive UI 组件表单演示功能
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Has been cancelled
CI/CD Pipeline / playwright (push) Has been cancelled
2025-10-31 13:40:23 +08:00
严浩
b3fbfe2d9d feat(i18n): 添加语言变更时更新html lang属性的功能
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m30s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m2s
测试最新依赖 / build-and-test (push) Successful in 2m5s
测试最新依赖 / playwright (push) Successful in 2m27s
2025-10-30 22:43:38 +08:00
严浩
da80cad976 chore(AppNaiveUIProvider): 移除 BUILD_TIME 常量声明
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m0s
CI/CD Pipeline / build-and-deploy (push) Successful in 3m55s
2025-10-30 22:34:32 +08:00
严浩
d6e05a8b44 feat(vite.config): 添加 CI 环境变量日志输出
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Has been cancelled
CI/CD Pipeline / playwright (push) Has been cancelled
2025-10-30 22:32:53 +08:00
5a196564d0 chore(deps): update dependency vite to v7.1.12
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 3m37s
CI/CD Pipeline / build-and-deploy (push) Successful in 3m43s
2025-10-30 18:14:05 +08:00
7ec1751ba6 chore(deps): update all non-major dependencies
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
CI/CD Pipeline / playwright (push) Successful in 2m1s
CI/CD Pipeline / build-and-deploy (push) Successful in 3m59s
2025-10-30 15:24:10 +08:00
严浩
270a838185 feat(AppNaiveUIProvider): 添加日期本地化支持
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m5s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m23s
2025-10-30 14:32:12 +08:00
严浩
5b54fe5182 feat(naive-ui): 添加 Modal 命令式 API 支持并完善类型声明
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m10s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m16s
2025-10-30 14:18:06 +08:00
严浩
21676c11ff Revert "feat(AppNaiveUIProvider): 添加对 NModal 的支持"
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m58s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m21s
This reverts commit 75f461df0f.
2025-10-30 14:05:19 +08:00
严浩
75f461df0f feat(AppNaiveUIProvider): 添加对 NModal 的支持
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 4m6s
CI/CD Pipeline / build-and-deploy (push) Has been cancelled
2025-10-30 14:00:43 +08:00
严浩
b623365e38 feat(demos): 添加 Naive UI 组件演示页面
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m2s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m6s
2025-10-30 13:37:31 +08:00
严浩
8130915d0e feat(env): 添加构建时间和提交信息的环境变量
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 5m1s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m42s
2025-10-30 12:08:58 +08:00
严浩
eeb72b24b5 feat(i18n): 更新 i18n-ally 配置
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m50s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m19s
2025-10-30 10:27:29 +08:00
严浩
68e320637e feat(main): 将 import.meta.glob 的 eager 选项添加注释说明,
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m11s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m34s
2025-10-30 09:35:51 +08:00
严浩
f81c7614be feat(store): 重构应用状态管理,移除旧的 app-store 并引入 app-store-auto-imports
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 4m10s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m33s
测试最新依赖 / playwright (push) Successful in 2m16s
测试最新依赖 / build-and-test (push) Failing after 2m26s
2025-10-29 23:37:31 +08:00
严浩
2874fdfaa7 feat(locales): 避免冲突 src/locales/demo
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m49s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m24s
2025-10-29 19:07:06 +08:00
严浩
7dd7ce73bc refactor(i18n): 重构国际化模块结构
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m14s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m39s
2025-10-29 13:16:18 +08:00
严浩
94d09d0bdd feat(dependencies): 添加 svgo 依赖项以优化图像处理
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m31s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m25s
2025-10-29 10:56:36 +08:00
严浩
b0b65b454c feat(toast): 添加一键打开所有 Toast 消息功能并优化显示时间
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 4m6s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m22s
测试最新依赖 / playwright (push) Successful in 1m50s
测试最新依赖 / build-and-test (push) Failing after 2m24s
2025-10-29 00:13:08 +08:00
严浩
c490cb1c8e feat: 添加 PrimeVue 组件演示页面
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m39s
CI/CD Pipeline / build-and-deploy (push) Successful in 2m51s
2025-10-28 23:50:40 +08:00
严浩
33e8a4a5d6 refactor(env): 统一环境变量命名规范
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m46s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m5s
2025-10-28 19:18:00 +08:00
严浩
35640a2ade feat(vite-config): 扩展primevue库匹配规则
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m50s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m0s
2025-10-28 16:10:42 +08:00
严浩
8db7ded1b5 feat(dependencies): 添加 vue-component-type-helpers 依赖项 [skip ci] 2025-10-28 11:36:32 +08:00
严浩
aa8e3467d3 fix(scripts): 修复 _all 脚本的并行执行方式 [skip ci] 2025-10-28 11:20:53 +08:00
严浩
8ed289a917 fix(stylelint): 允许非 kebab-case 的类选择器 [skip ci] 2025-10-28 10:55:52 +08:00
严浩
88ca601d07 feat(i18n): 实现应用语言本地存储功能
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m46s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m6s
2025-10-28 09:55:28 +08:00
严浩
50911ada4e build(vite): 注释掉 layouts 手动分块逻辑
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 4m49s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m34s
测试最新依赖 / playwright (push) Successful in 1m51s
测试最新依赖 / build-and-test (push) Failing after 2m25s
2025-10-27 22:42:50 +08:00
严浩
d065c90e71 feat(dependencies): 添加 vue-memoize-dict 依赖项
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m24s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m29s
2025-10-27 22:09:24 +08:00
严浩
ad0a50d8c7 refactor(eslint): 添加 vue/attributes-order 规则以规范属性排序 2025-10-27 22:05:59 +08:00
严浩
d4d9620db2 refactor(useMetaLayoutsMenuOptions): 优化路由过滤与排序逻辑
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m9s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m27s
2025-10-27 14:04:58 +08:00
严浩
267bf75bc1 chore(useMetaLayoutsMenuOptions): 优化调试日志输出格式
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m55s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m11s
2025-10-27 13:46:06 +08:00
严浩
09ec2c7d12 feat(auto-import): 更新图标引入方式并升级相关依赖
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m8s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m3s
2025-10-27 12:46:08 +08:00
严浩
ad0df6b140 docs(agents): 更新 TypeScript/TSX 文件中图标自动导入的说明
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m9s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m22s
2025-10-27 12:18:51 +08:00
严浩
3269b10bfd fix(vite): 修复 rollup 手动分块逻辑
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m49s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m7s
2025-10-27 09:42:45 +08:00
严浩
047632b75f chore: lint format
Some checks failed
CI/CD Pipeline / playwright (push) Failing after 8m18s
CI/CD Pipeline / build-and-deploy (push) Has been skipped
测试最新依赖 / build-and-test (push) Failing after 1m44s
测试最新依赖 / playwright (push) Failing after 6m28s
2025-10-27 02:31:48 +08:00
严浩
7830ad0ebb Merge branch 'eslint-plugin-import' 2025-10-27 02:29:54 +08:00
严浩
a61a22569d fix(vite-config): 移除对 index.page.vue 文件的特殊处理逻辑 2025-10-27 02:29:25 +08:00
严浩
c27c8544de feat(vite-plugins): 更新cloudflare插件环境变量加载方式 2025-10-27 02:25:33 +08:00
69 changed files with 2540 additions and 1377 deletions

21
.env
View File

@@ -1,10 +1,15 @@
VITE_APP_BUILD_TIME=NOT_SET
VITE_APP_BUILD_COMMIT=NOT_SET
VITE_BUILD_SOURCE_MAP=true
VITE_BUILD_MINIFY=true
VITE_CLOUDFLARE_SERVER_ENABLED=true
VITE_APP_TITLE=vue-ts-example-2025
VITE_APP_BASE=/
VITE_APP_BUILD_SOURCE_MAP=true
VITE_APP_BUILD_MINIFY=true
VITE_APP_BUILD_COMMIT=
VITE_APP_BUILD_TIME=
VITE_ENABLE_VUE_DEVTOOLS=true
VITE_MENU_SHOW_DEMOS=true
VITE_MENU_SHOW_ORDER=true
VITE_CLOUDFLARE_SERVER_ENABLED=true
VITE_APP_ENABLE_VUE_DEVTOOLS=true
VITE_APP_MENU_SHOW_DEMOS=true
VITE_APP_MENU_SHOW_ORDER=true
VITE_APP_ENABLE_ROUTER_LOG_GUARD=true
VITE_APP_API_URL=/API
VITE_APP_PROXY=[["/API","https://jsonplaceholder.typicode.com"]]

View File

@@ -38,7 +38,9 @@ jobs:
run: pnpm run lint
- name: 📦 构建项目
run: pnpm run build-only
run: |
export VITE_APP_BUILD_TIME=$(date +"%Y-%m-%d %H:%M:%S")
pnpm run build-only
env:
VITE_APP_BUILD_COMMIT: ${{ github.sha }}

View File

@@ -2,4 +2,5 @@
echo "🧹 [Pre-commit] 正在运行 lint-staged..."
time pnpm exec lint-staged
time pnpm run lint:vue-i18n-extract
# time pnpm run type-check
echo "🧹 [Pre-commit] lint-staged 完成!"

18
.vscode/settings.json vendored
View File

@@ -6,25 +6,33 @@
"source.fixAll.oxc": "explicit",
"source.organizeImports": "never"
},
"eslint.enable": true,
"stylelint.enable": true,
"oxc.enable": true,
"editor.formatOnSave": true,
"stylelint.enable": true,
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"scss.lint.unknownAtRules": "ignore",
"css.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore",
"eslint.enable": true,
"oxc.enable": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"i18n-ally.localesPaths": ["src/locales"],
// >>>>>
"i18n-ally.readonly": true,
"i18n-ally.namespace": false /* @intlify/unplugin-vue-i18n */,
"i18n-ally.localesPaths": ["src/locales/demo", "src/locales"],
// https://github.com/lokalise/i18n-ally/wiki/Path-Matcher
// 默认: 🗃 Path Matcher Regex: /^(?<locale>[\w-_]+)(?:.*\/|^).*\.(?<ext>json|ya?ml|json5)$/
"i18n-ally.pathMatcher": "{locale}.json",
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容
"i18n-ally.displayLanguage": "zh-CN", // 显示语言 (显示文件/翻译文件)
"i18n-ally.keystyle": "nested",
// <<<<<
// https://github.com/copilot/share/8a1a019a-0180-80e7-8141-a40be02c4006
// "iconify.customCollectionJsonPaths": ["https://example.com/my-icons.json", "./local/icons.json"],

View File

@@ -22,14 +22,15 @@ Multiple auto-import systems are active:
- **Vue APIs**: Core Vue, VueUse, Pinia, Vue Router, vue-i18n
- **Components**: Auto-registered from multiple UI libraries (Naive UI, PrimeVue)
- **Icons**: Uses `unplugin-icons` with `icon-` prefix; custom SVGs from `src/assets/icons/svgs/` available via `icon-svg:filename`
- **Icons**: Uses `unplugin-icons` with `icon-` prefix; custom SVGs from `src/assets/icons/svgs/` available via `icon-svg-filename`
**IMPORTANT - Auto-Import Limitations**:
- **Dynamic components**: Auto-imported components cannot be used with `<component :is="..." />` syntax
- ❌ Avoid: `<component :is="`icon-${name}`" />`
- ✅ Use: `<icon-foo v-if="condition" />` with `v-if`/`v-else-if`/`v-else` directives
- **In TypeScript/TSX files**: Auto-import does NOT work. You must explicitly import icons using the `~icons/` prefix:
- **Icons in TypeScript/TSX files**: Auto-import for icons does NOT work. You must explicitly import them using the `~icons/` prefix:
```tsx
import IconMenuRounded from '~icons/material-symbols/menu-rounded';

338
auto-imports.d.ts vendored
View File

@@ -6,162 +6,10 @@
// biome-ignore lint: disable
export {}
declare global {
const APP_THEME_MODES: typeof import('./src/stores/app-store')['APP_THEME_MODES']
const ConfirmationService: typeof import('utils4u/primevue')['ConfirmationService']
const DialogService: typeof import('utils4u/primevue')['DialogService']
const EffectScope: typeof import('vue')['EffectScope']
const NA: typeof import('naive-ui')['NA']
const NAffix: typeof import('naive-ui')['NAffix']
const NAlert: typeof import('naive-ui')['NAlert']
const NAnchor: typeof import('naive-ui')['NAnchor']
const NAnchorLink: typeof import('naive-ui')['NAnchorLink']
const NAutoComplete: typeof import('naive-ui')['NAutoComplete']
const NAvatar: typeof import('naive-ui')['NAvatar']
const NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
const NBackTop: typeof import('naive-ui')['NBackTop']
const NBadge: typeof import('naive-ui')['NBadge']
const NBlockquote: typeof import('naive-ui')['NBlockquote']
const NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
const NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
const NButton: typeof import('naive-ui')['NButton']
const NButtonGroup: typeof import('naive-ui')['NButtonGroup']
const NCalendar: typeof import('naive-ui')['NCalendar']
const NCard: typeof import('naive-ui')['NCard']
const NCarousel: typeof import('naive-ui')['NCarousel']
const NCarouselItem: typeof import('naive-ui')['NCarouselItem']
const NCascader: typeof import('naive-ui')['NCascader']
const NCheckbox: typeof import('naive-ui')['NCheckbox']
const NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
const NCode: typeof import('naive-ui')['NCode']
const NCol: typeof import('naive-ui')['NCol']
const NCollapse: typeof import('naive-ui')['NCollapse']
const NCollapseItem: typeof import('naive-ui')['NCollapseItem']
const NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
const NColorPicker: typeof import('naive-ui')['NColorPicker']
const NConfigProvider: typeof import('naive-ui')['NConfigProvider']
const NCountdown: typeof import('naive-ui')['NCountdown']
const NDataTable: typeof import('naive-ui')['NDataTable']
const NDatePicker: typeof import('naive-ui')['NDatePicker']
const NDescriptions: typeof import('naive-ui')['NDescriptions']
const NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
const NDialog: typeof import('naive-ui')['NDialog']
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
const NDivider: typeof import('naive-ui')['NDivider']
const NDrawer: typeof import('naive-ui')['NDrawer']
const NDrawerContent: typeof import('naive-ui')['NDrawerContent']
const NDropdown: typeof import('naive-ui')['NDropdown']
const NDynamicInput: typeof import('naive-ui')['NDynamicInput']
const NDynamicTags: typeof import('naive-ui')['NDynamicTags']
const NEl: typeof import('naive-ui')['NEl']
const NElement: typeof import('naive-ui')['NElement']
const NEllipsis: typeof import('naive-ui')['NEllipsis']
const NEmpty: typeof import('naive-ui')['NEmpty']
const NEquation: typeof import('naive-ui')['NEquation']
const NFlex: typeof import('naive-ui')['NFlex']
const NFloatButton: typeof import('naive-ui')['NFloatButton']
const NFloatButtonGroup: typeof import('naive-ui')['NFloatButtonGroup']
const NForm: typeof import('naive-ui')['NForm']
const NFormItem: typeof import('naive-ui')['NFormItem']
const NFormItemCol: typeof import('naive-ui')['NFormItemCol']
const NFormItemGi: typeof import('naive-ui')['NFormItemGi']
const NFormItemGridItem: typeof import('naive-ui')['NFormItemGridItem']
const NFormItemRow: typeof import('naive-ui')['NFormItemRow']
const NGi: typeof import('naive-ui')['NGi']
const NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
const NGradientText: typeof import('naive-ui')['NGradientText']
const NGrid: typeof import('naive-ui')['NGrid']
const NGridItem: typeof import('naive-ui')['NGridItem']
const NH1: typeof import('naive-ui')['NH1']
const NH2: typeof import('naive-ui')['NH2']
const NH3: typeof import('naive-ui')['NH3']
const NH4: typeof import('naive-ui')['NH4']
const NH5: typeof import('naive-ui')['NH5']
const NH6: typeof import('naive-ui')['NH6']
const NHeatmap: typeof import('naive-ui')['NHeatmap']
const NHighlight: typeof import('naive-ui')['NHighlight']
const NHr: typeof import('naive-ui')['NHr']
const NIcon: typeof import('naive-ui')['NIcon']
const NIconWrapper: typeof import('naive-ui')['NIconWrapper']
const NImage: typeof import('naive-ui')['NImage']
const NImageGroup: typeof import('naive-ui')['NImageGroup']
const NImagePreview: typeof import('naive-ui')['NImagePreview']
const NInfiniteScroll: typeof import('naive-ui')['NInfiniteScroll']
const NInput: typeof import('naive-ui')['NInput']
const NInputGroup: typeof import('naive-ui')['NInputGroup']
const NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
const NInputNumber: typeof import('naive-ui')['NInputNumber']
const NInputOtp: typeof import('naive-ui')['NInputOtp']
const NLayout: typeof import('naive-ui')['NLayout']
const NLayoutContent: typeof import('naive-ui')['NLayoutContent']
const NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
const NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
const NLayoutSider: typeof import('naive-ui')['NLayoutSider']
const NLegacyTransfer: typeof import('naive-ui')['NLegacyTransfer']
const NLi: typeof import('naive-ui')['NLi']
const NList: typeof import('naive-ui')['NList']
const NListItem: typeof import('naive-ui')['NListItem']
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
const NLog: typeof import('naive-ui')['NLog']
const NMarquee: typeof import('naive-ui')['NMarquee']
const NMention: typeof import('naive-ui')['NMention']
const NMenu: typeof import('naive-ui')['NMenu']
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
const NModal: typeof import('naive-ui')['NModal']
const NModalProvider: typeof import('naive-ui')['NModalProvider']
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
const NOl: typeof import('naive-ui')['NOl']
const NP: typeof import('naive-ui')['NP']
const NPageHeader: typeof import('naive-ui')['NPageHeader']
const NPagination: typeof import('naive-ui')['NPagination']
const NPerformantEllipsis: typeof import('naive-ui')['NPerformantEllipsis']
const NPopconfirm: typeof import('naive-ui')['NPopconfirm']
const NPopover: typeof import('naive-ui')['NPopover']
const NPopselect: typeof import('naive-ui')['NPopselect']
const NProgress: typeof import('naive-ui')['NProgress']
const NQrCode: typeof import('naive-ui')['NQrCode']
const NRadio: typeof import('naive-ui')['NRadio']
const NRadioButton: typeof import('naive-ui')['NRadioButton']
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
const NRate: typeof import('naive-ui')['NRate']
const NResult: typeof import('naive-ui')['NResult']
const NRow: typeof import('naive-ui')['NRow']
const NScrollbar: typeof import('naive-ui')['NScrollbar']
const NSelect: typeof import('naive-ui')['NSelect']
const NSkeleton: typeof import('naive-ui')['NSkeleton']
const NSlider: typeof import('naive-ui')['NSlider']
const NSpace: typeof import('naive-ui')['NSpace']
const NSpin: typeof import('naive-ui')['NSpin']
const NSplit: typeof import('naive-ui')['NSplit']
const NStatistic: typeof import('naive-ui')['NStatistic']
const NStep: typeof import('naive-ui')['NStep']
const NSteps: typeof import('naive-ui')['NSteps']
const NSwitch: typeof import('naive-ui')['NSwitch']
const NTab: typeof import('naive-ui')['NTab']
const NTabPane: typeof import('naive-ui')['NTabPane']
const NTable: typeof import('naive-ui')['NTable']
const NTabs: typeof import('naive-ui')['NTabs']
const NTag: typeof import('naive-ui')['NTag']
const NTbody: typeof import('naive-ui')['NTbody']
const NTd: typeof import('naive-ui')['NTd']
const NText: typeof import('naive-ui')['NText']
const NTh: typeof import('naive-ui')['NTh']
const NThead: typeof import('naive-ui')['NThead']
const NThing: typeof import('naive-ui')['NThing']
const NTime: typeof import('naive-ui')['NTime']
const NTimePicker: typeof import('naive-ui')['NTimePicker']
const NTimeline: typeof import('naive-ui')['NTimeline']
const NTimelineItem: typeof import('naive-ui')['NTimelineItem']
const NTooltip: typeof import('naive-ui')['NTooltip']
const NTr: typeof import('naive-ui')['NTr']
const NTransfer: typeof import('naive-ui')['NTransfer']
const NTree: typeof import('naive-ui')['NTree']
const NTreeSelect: typeof import('naive-ui')['NTreeSelect']
const NUl: typeof import('naive-ui')['NUl']
const NUpload: typeof import('naive-ui')['NUpload']
const NUploadDragger: typeof import('naive-ui')['NUploadDragger']
const NUploadFileList: typeof import('naive-ui')['NUploadFileList']
const NUploadTrigger: typeof import('naive-ui')['NUploadTrigger']
const NVirtualList: typeof import('naive-ui')['NVirtualList']
const NWatermark: typeof import('naive-ui')['NWatermark']
const ToastService: typeof import('utils4u/primevue')['ToastService']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const arrayToTree: typeof import('utils4u/array')['arrayToTree']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
@@ -204,6 +52,8 @@ declare global {
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
const h: typeof import('vue')['h']
const i18nInstance: typeof import('./src/locales-utils/i18n-auto-imports')['i18nInstance']
const i18nRouteMessages: typeof import('./src/locales-utils/route-messages/route-messages-auto-imports')['i18nRouteMessages']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
@@ -213,7 +63,6 @@ declare global {
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const isShallow: typeof import('vue')['isShallow']
const locales4RouteMessages: typeof import('./src/locales-4-route/_messages-auto-imports')['locales4RouteMessages']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
@@ -262,6 +111,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const routeI18nInstance: typeof import('./src/locales-utils/i18n-auto-imports')['routeI18nInstance']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setViewportCSSVars: typeof import('utils4u/browser')['setViewportCSSVars']
@@ -291,7 +141,7 @@ declare global {
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAppStore: typeof import('./src/stores/app-store')['useAppStore']
const useAppStore: typeof import('./src/stores/app-store-auto-imports')['useAppStore']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@@ -307,6 +157,7 @@ declare global {
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAuthStore: typeof import('./src/stores/auth-store-auto-imports')['useAuthStore']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
@@ -409,6 +260,7 @@ declare global {
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const usePrimevueDialogRef: typeof import('utils4u/primevue')['usePrimevueDialogRef']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useRefs: typeof import('utils4u/vue-use')['useRefs']
@@ -416,6 +268,7 @@ declare global {
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useSafeNForm: typeof import('./src/utils/use-safe-n-form-auto-imports')['useSafeNForm']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
@@ -489,8 +342,8 @@ declare global {
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { AppThemeMode } from './src/stores/app-store'
import('./src/stores/app-store')
export type { AppThemeMode } from './src/stores/app-store-auto-imports'
import('./src/stores/app-store-auto-imports')
}
// for vue template auto import
@@ -498,162 +351,10 @@ import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly APP_THEME_MODES: UnwrapRef<typeof import('./src/stores/app-store')['APP_THEME_MODES']>
readonly ConfirmationService: UnwrapRef<typeof import('utils4u/primevue')['ConfirmationService']>
readonly DialogService: UnwrapRef<typeof import('utils4u/primevue')['DialogService']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly NA: UnwrapRef<typeof import('naive-ui')['NA']>
readonly NAffix: UnwrapRef<typeof import('naive-ui')['NAffix']>
readonly NAlert: UnwrapRef<typeof import('naive-ui')['NAlert']>
readonly NAnchor: UnwrapRef<typeof import('naive-ui')['NAnchor']>
readonly NAnchorLink: UnwrapRef<typeof import('naive-ui')['NAnchorLink']>
readonly NAutoComplete: UnwrapRef<typeof import('naive-ui')['NAutoComplete']>
readonly NAvatar: UnwrapRef<typeof import('naive-ui')['NAvatar']>
readonly NAvatarGroup: UnwrapRef<typeof import('naive-ui')['NAvatarGroup']>
readonly NBackTop: UnwrapRef<typeof import('naive-ui')['NBackTop']>
readonly NBadge: UnwrapRef<typeof import('naive-ui')['NBadge']>
readonly NBlockquote: UnwrapRef<typeof import('naive-ui')['NBlockquote']>
readonly NBreadcrumb: UnwrapRef<typeof import('naive-ui')['NBreadcrumb']>
readonly NBreadcrumbItem: UnwrapRef<typeof import('naive-ui')['NBreadcrumbItem']>
readonly NButton: UnwrapRef<typeof import('naive-ui')['NButton']>
readonly NButtonGroup: UnwrapRef<typeof import('naive-ui')['NButtonGroup']>
readonly NCalendar: UnwrapRef<typeof import('naive-ui')['NCalendar']>
readonly NCard: UnwrapRef<typeof import('naive-ui')['NCard']>
readonly NCarousel: UnwrapRef<typeof import('naive-ui')['NCarousel']>
readonly NCarouselItem: UnwrapRef<typeof import('naive-ui')['NCarouselItem']>
readonly NCascader: UnwrapRef<typeof import('naive-ui')['NCascader']>
readonly NCheckbox: UnwrapRef<typeof import('naive-ui')['NCheckbox']>
readonly NCheckboxGroup: UnwrapRef<typeof import('naive-ui')['NCheckboxGroup']>
readonly NCode: UnwrapRef<typeof import('naive-ui')['NCode']>
readonly NCol: UnwrapRef<typeof import('naive-ui')['NCol']>
readonly NCollapse: UnwrapRef<typeof import('naive-ui')['NCollapse']>
readonly NCollapseItem: UnwrapRef<typeof import('naive-ui')['NCollapseItem']>
readonly NCollapseTransition: UnwrapRef<typeof import('naive-ui')['NCollapseTransition']>
readonly NColorPicker: UnwrapRef<typeof import('naive-ui')['NColorPicker']>
readonly NConfigProvider: UnwrapRef<typeof import('naive-ui')['NConfigProvider']>
readonly NCountdown: UnwrapRef<typeof import('naive-ui')['NCountdown']>
readonly NDataTable: UnwrapRef<typeof import('naive-ui')['NDataTable']>
readonly NDatePicker: UnwrapRef<typeof import('naive-ui')['NDatePicker']>
readonly NDescriptions: UnwrapRef<typeof import('naive-ui')['NDescriptions']>
readonly NDescriptionsItem: UnwrapRef<typeof import('naive-ui')['NDescriptionsItem']>
readonly NDialog: UnwrapRef<typeof import('naive-ui')['NDialog']>
readonly NDialogProvider: UnwrapRef<typeof import('naive-ui')['NDialogProvider']>
readonly NDivider: UnwrapRef<typeof import('naive-ui')['NDivider']>
readonly NDrawer: UnwrapRef<typeof import('naive-ui')['NDrawer']>
readonly NDrawerContent: UnwrapRef<typeof import('naive-ui')['NDrawerContent']>
readonly NDropdown: UnwrapRef<typeof import('naive-ui')['NDropdown']>
readonly NDynamicInput: UnwrapRef<typeof import('naive-ui')['NDynamicInput']>
readonly NDynamicTags: UnwrapRef<typeof import('naive-ui')['NDynamicTags']>
readonly NEl: UnwrapRef<typeof import('naive-ui')['NEl']>
readonly NElement: UnwrapRef<typeof import('naive-ui')['NElement']>
readonly NEllipsis: UnwrapRef<typeof import('naive-ui')['NEllipsis']>
readonly NEmpty: UnwrapRef<typeof import('naive-ui')['NEmpty']>
readonly NEquation: UnwrapRef<typeof import('naive-ui')['NEquation']>
readonly NFlex: UnwrapRef<typeof import('naive-ui')['NFlex']>
readonly NFloatButton: UnwrapRef<typeof import('naive-ui')['NFloatButton']>
readonly NFloatButtonGroup: UnwrapRef<typeof import('naive-ui')['NFloatButtonGroup']>
readonly NForm: UnwrapRef<typeof import('naive-ui')['NForm']>
readonly NFormItem: UnwrapRef<typeof import('naive-ui')['NFormItem']>
readonly NFormItemCol: UnwrapRef<typeof import('naive-ui')['NFormItemCol']>
readonly NFormItemGi: UnwrapRef<typeof import('naive-ui')['NFormItemGi']>
readonly NFormItemGridItem: UnwrapRef<typeof import('naive-ui')['NFormItemGridItem']>
readonly NFormItemRow: UnwrapRef<typeof import('naive-ui')['NFormItemRow']>
readonly NGi: UnwrapRef<typeof import('naive-ui')['NGi']>
readonly NGlobalStyle: UnwrapRef<typeof import('naive-ui')['NGlobalStyle']>
readonly NGradientText: UnwrapRef<typeof import('naive-ui')['NGradientText']>
readonly NGrid: UnwrapRef<typeof import('naive-ui')['NGrid']>
readonly NGridItem: UnwrapRef<typeof import('naive-ui')['NGridItem']>
readonly NH1: UnwrapRef<typeof import('naive-ui')['NH1']>
readonly NH2: UnwrapRef<typeof import('naive-ui')['NH2']>
readonly NH3: UnwrapRef<typeof import('naive-ui')['NH3']>
readonly NH4: UnwrapRef<typeof import('naive-ui')['NH4']>
readonly NH5: UnwrapRef<typeof import('naive-ui')['NH5']>
readonly NH6: UnwrapRef<typeof import('naive-ui')['NH6']>
readonly NHeatmap: UnwrapRef<typeof import('naive-ui')['NHeatmap']>
readonly NHighlight: UnwrapRef<typeof import('naive-ui')['NHighlight']>
readonly NHr: UnwrapRef<typeof import('naive-ui')['NHr']>
readonly NIcon: UnwrapRef<typeof import('naive-ui')['NIcon']>
readonly NIconWrapper: UnwrapRef<typeof import('naive-ui')['NIconWrapper']>
readonly NImage: UnwrapRef<typeof import('naive-ui')['NImage']>
readonly NImageGroup: UnwrapRef<typeof import('naive-ui')['NImageGroup']>
readonly NImagePreview: UnwrapRef<typeof import('naive-ui')['NImagePreview']>
readonly NInfiniteScroll: UnwrapRef<typeof import('naive-ui')['NInfiniteScroll']>
readonly NInput: UnwrapRef<typeof import('naive-ui')['NInput']>
readonly NInputGroup: UnwrapRef<typeof import('naive-ui')['NInputGroup']>
readonly NInputGroupLabel: UnwrapRef<typeof import('naive-ui')['NInputGroupLabel']>
readonly NInputNumber: UnwrapRef<typeof import('naive-ui')['NInputNumber']>
readonly NInputOtp: UnwrapRef<typeof import('naive-ui')['NInputOtp']>
readonly NLayout: UnwrapRef<typeof import('naive-ui')['NLayout']>
readonly NLayoutContent: UnwrapRef<typeof import('naive-ui')['NLayoutContent']>
readonly NLayoutFooter: UnwrapRef<typeof import('naive-ui')['NLayoutFooter']>
readonly NLayoutHeader: UnwrapRef<typeof import('naive-ui')['NLayoutHeader']>
readonly NLayoutSider: UnwrapRef<typeof import('naive-ui')['NLayoutSider']>
readonly NLegacyTransfer: UnwrapRef<typeof import('naive-ui')['NLegacyTransfer']>
readonly NLi: UnwrapRef<typeof import('naive-ui')['NLi']>
readonly NList: UnwrapRef<typeof import('naive-ui')['NList']>
readonly NListItem: UnwrapRef<typeof import('naive-ui')['NListItem']>
readonly NLoadingBarProvider: UnwrapRef<typeof import('naive-ui')['NLoadingBarProvider']>
readonly NLog: UnwrapRef<typeof import('naive-ui')['NLog']>
readonly NMarquee: UnwrapRef<typeof import('naive-ui')['NMarquee']>
readonly NMention: UnwrapRef<typeof import('naive-ui')['NMention']>
readonly NMenu: UnwrapRef<typeof import('naive-ui')['NMenu']>
readonly NMessageProvider: UnwrapRef<typeof import('naive-ui')['NMessageProvider']>
readonly NModal: UnwrapRef<typeof import('naive-ui')['NModal']>
readonly NModalProvider: UnwrapRef<typeof import('naive-ui')['NModalProvider']>
readonly NNotificationProvider: UnwrapRef<typeof import('naive-ui')['NNotificationProvider']>
readonly NNumberAnimation: UnwrapRef<typeof import('naive-ui')['NNumberAnimation']>
readonly NOl: UnwrapRef<typeof import('naive-ui')['NOl']>
readonly NP: UnwrapRef<typeof import('naive-ui')['NP']>
readonly NPageHeader: UnwrapRef<typeof import('naive-ui')['NPageHeader']>
readonly NPagination: UnwrapRef<typeof import('naive-ui')['NPagination']>
readonly NPerformantEllipsis: UnwrapRef<typeof import('naive-ui')['NPerformantEllipsis']>
readonly NPopconfirm: UnwrapRef<typeof import('naive-ui')['NPopconfirm']>
readonly NPopover: UnwrapRef<typeof import('naive-ui')['NPopover']>
readonly NPopselect: UnwrapRef<typeof import('naive-ui')['NPopselect']>
readonly NProgress: UnwrapRef<typeof import('naive-ui')['NProgress']>
readonly NQrCode: UnwrapRef<typeof import('naive-ui')['NQrCode']>
readonly NRadio: UnwrapRef<typeof import('naive-ui')['NRadio']>
readonly NRadioButton: UnwrapRef<typeof import('naive-ui')['NRadioButton']>
readonly NRadioGroup: UnwrapRef<typeof import('naive-ui')['NRadioGroup']>
readonly NRate: UnwrapRef<typeof import('naive-ui')['NRate']>
readonly NResult: UnwrapRef<typeof import('naive-ui')['NResult']>
readonly NRow: UnwrapRef<typeof import('naive-ui')['NRow']>
readonly NScrollbar: UnwrapRef<typeof import('naive-ui')['NScrollbar']>
readonly NSelect: UnwrapRef<typeof import('naive-ui')['NSelect']>
readonly NSkeleton: UnwrapRef<typeof import('naive-ui')['NSkeleton']>
readonly NSlider: UnwrapRef<typeof import('naive-ui')['NSlider']>
readonly NSpace: UnwrapRef<typeof import('naive-ui')['NSpace']>
readonly NSpin: UnwrapRef<typeof import('naive-ui')['NSpin']>
readonly NSplit: UnwrapRef<typeof import('naive-ui')['NSplit']>
readonly NStatistic: UnwrapRef<typeof import('naive-ui')['NStatistic']>
readonly NStep: UnwrapRef<typeof import('naive-ui')['NStep']>
readonly NSteps: UnwrapRef<typeof import('naive-ui')['NSteps']>
readonly NSwitch: UnwrapRef<typeof import('naive-ui')['NSwitch']>
readonly NTab: UnwrapRef<typeof import('naive-ui')['NTab']>
readonly NTabPane: UnwrapRef<typeof import('naive-ui')['NTabPane']>
readonly NTable: UnwrapRef<typeof import('naive-ui')['NTable']>
readonly NTabs: UnwrapRef<typeof import('naive-ui')['NTabs']>
readonly NTag: UnwrapRef<typeof import('naive-ui')['NTag']>
readonly NTbody: UnwrapRef<typeof import('naive-ui')['NTbody']>
readonly NTd: UnwrapRef<typeof import('naive-ui')['NTd']>
readonly NText: UnwrapRef<typeof import('naive-ui')['NText']>
readonly NTh: UnwrapRef<typeof import('naive-ui')['NTh']>
readonly NThead: UnwrapRef<typeof import('naive-ui')['NThead']>
readonly NThing: UnwrapRef<typeof import('naive-ui')['NThing']>
readonly NTime: UnwrapRef<typeof import('naive-ui')['NTime']>
readonly NTimePicker: UnwrapRef<typeof import('naive-ui')['NTimePicker']>
readonly NTimeline: UnwrapRef<typeof import('naive-ui')['NTimeline']>
readonly NTimelineItem: UnwrapRef<typeof import('naive-ui')['NTimelineItem']>
readonly NTooltip: UnwrapRef<typeof import('naive-ui')['NTooltip']>
readonly NTr: UnwrapRef<typeof import('naive-ui')['NTr']>
readonly NTransfer: UnwrapRef<typeof import('naive-ui')['NTransfer']>
readonly NTree: UnwrapRef<typeof import('naive-ui')['NTree']>
readonly NTreeSelect: UnwrapRef<typeof import('naive-ui')['NTreeSelect']>
readonly NUl: UnwrapRef<typeof import('naive-ui')['NUl']>
readonly NUpload: UnwrapRef<typeof import('naive-ui')['NUpload']>
readonly NUploadDragger: UnwrapRef<typeof import('naive-ui')['NUploadDragger']>
readonly NUploadFileList: UnwrapRef<typeof import('naive-ui')['NUploadFileList']>
readonly NUploadTrigger: UnwrapRef<typeof import('naive-ui')['NUploadTrigger']>
readonly NVirtualList: UnwrapRef<typeof import('naive-ui')['NVirtualList']>
readonly NWatermark: UnwrapRef<typeof import('naive-ui')['NWatermark']>
readonly ToastService: UnwrapRef<typeof import('utils4u/primevue')['ToastService']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayToTree: UnwrapRef<typeof import('utils4u/array')['arrayToTree']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
@@ -696,6 +397,8 @@ declare module 'vue' {
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly i18nInstance: UnwrapRef<typeof import('./src/locales-utils/i18n-auto-imports')['i18nInstance']>
readonly i18nRouteMessages: UnwrapRef<typeof import('./src/locales-utils/route-messages/route-messages-auto-imports')['i18nRouteMessages']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
@@ -705,7 +408,6 @@ declare module 'vue' {
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
readonly locales4RouteMessages: UnwrapRef<typeof import('./src/locales-4-route/_messages-auto-imports')['locales4RouteMessages']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -754,6 +456,7 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly routeI18nInstance: UnwrapRef<typeof import('./src/locales-utils/i18n-auto-imports')['routeI18nInstance']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setViewportCSSVars: UnwrapRef<typeof import('utils4u/browser')['setViewportCSSVars']>
@@ -783,7 +486,7 @@ declare module 'vue' {
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppStore: UnwrapRef<typeof import('./src/stores/app-store')['useAppStore']>
readonly useAppStore: UnwrapRef<typeof import('./src/stores/app-store-auto-imports')['useAppStore']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@@ -799,6 +502,7 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
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 useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
@@ -901,6 +605,7 @@ declare module 'vue' {
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly usePrimevueDialogRef: UnwrapRef<typeof import('utils4u/primevue')['usePrimevueDialogRef']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useRefs: UnwrapRef<typeof import('utils4u/vue-use')['useRefs']>
@@ -908,6 +613,7 @@ declare module 'vue' {
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
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 useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>

View File

@@ -1,15 +1,17 @@
import pluginImport from 'eslint-plugin-import';
import { globalIgnores } from 'eslint/config';
import pluginVitest from '@vitest/eslint-plugin';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
import {
configureVueProject,
defineConfigWithVueTs,
vueTsConfigs,
configureVueProject,
} from '@vue/eslint-config-typescript';
import pluginVue from 'eslint-plugin-vue';
import pluginVitest from '@vitest/eslint-plugin';
import pluginPlaywright from 'eslint-plugin-playwright';
import pluginImport from 'eslint-plugin-import';
import pluginJsonc from 'eslint-plugin-jsonc';
import pluginOxlint from 'eslint-plugin-oxlint';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
import pluginPlaywright from 'eslint-plugin-playwright';
import pluginVue from 'eslint-plugin-vue';
import { globalIgnores } from 'eslint/config';
import jsoncParser from 'jsonc-eslint-parser';
configureVueProject({ scriptLangs: ['ts', 'tsx'] });
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
@@ -52,6 +54,19 @@ export default defineConfigWithVueTs(
},
},
{
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
files: ['src/locales/**/*.json'],
languageOptions: { parser: jsoncParser },
plugins: { jsonc: pluginJsonc },
rules: { 'jsonc/sort-keys': 'error' },
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
@@ -65,6 +80,7 @@ export default defineConfigWithVueTs(
'error',
{ order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'] },
],
'vue/attributes-order': 'error',
'vue/multi-word-component-names': 'off',
},
},

View File

@@ -11,40 +11,6 @@
name="viewport"
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="format-detection" content="telephone=no" />
@@ -132,7 +98,40 @@
<!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> -->
<!-- .min.js 是 jsDelivr 的特殊处理 -->
<!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> -->
</body>
<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 = {
@@ -163,4 +162,5 @@
s.parentNode.insertBefore(tk, s);
}); /* (document) */
</script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{
"packageManager": "pnpm@10.18.3",
"packageManager": "pnpm@10.20.0",
"name": "vue-ts-example-2025",
"version": "0.0.0",
"private": true,
@@ -8,7 +8,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"_all": "run-p build-only type-check lint format:prettier test:unit:DisableWatch",
"_all": "run-s build-only lint format:prettier type-check test:unit:DisableWatch",
"dev": "vite --port 4730 --host --strictPort",
"build": "run-p type-check \"build-only {@}\" --",
"build-only": "vite build",
@@ -17,8 +17,8 @@
"format:prettier": "prettier --write src/",
"type-check": "vue-tsc --build",
"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:stylelint": "stylelint \"**/*.{css,less,scss,vue}\" --fix --ignore-path .gitignore",
"lint:vue-i18n-extract": "vue-i18n-extract report --vueFiles './src/**/*.?(ts|tsx|vue)' --languageFiles './src/locales/**/*.?(json|yml|yaml|js)' --ci",
"lint:stylelint": "stylelint --fix --ignore-path .gitignore \"**/*.{css,less,scss,vue}\"",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix",
"test:unit:DisableWatch": "vitest --run",
@@ -27,14 +27,15 @@
"prepare": "husky"
},
"lint-staged": {
"{server,src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
"prettier --write",
"{src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
"eslint --fix",
"oxlint --fix"
"oxlint --fix",
"prettier --write"
],
"{src,packages}/**/*.{css,less,scss,vue}": [
"stylelint --fix"
]
],
"{src/locales-utils,src/locales}/**/*": "node scripts/type-check-for-lint-staged.mjs"
},
"pnpm": {
"overrides": {
@@ -59,14 +60,17 @@
"@unhead/vue": "^2.0.14",
"@vueuse/core": "^13.9.0",
"highlight.js": "^11.11.1",
"lodash-es": "^4.17.21",
"naive-ui": "^2.43.1",
"pinia": "^3.0.3",
"primeicons": "^7.0.0",
"primelocale": "^2.1.7",
"primevue": "^4.3.9",
"ts-enum-util": "^4.1.0",
"utils4u": "^4.2.3",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-memoize-dict": "^1.1.3",
"vue-router": "^4.6.3"
},
"devDependencies": {
@@ -86,6 +90,7 @@
"@tsconfig/node22": "^22.0.2",
"@types/html-minifier-terser": "^7.0.2",
"@types/jsdom": "^27.0.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.18.1",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-vue": "^6.0.1",
@@ -98,6 +103,7 @@
"consola": "^3.4.2",
"eslint": "^9.35.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsonc": "^2.21.0",
"eslint-plugin-oxlint": "~1.23.0",
"eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-vue": "~10.5.0",
@@ -113,20 +119,23 @@
"prettier": "3.6.2",
"rollup": "^4.52.5",
"sass-embedded": "^1.93.2",
"sharp": "^0.34.4",
"stylelint": "^16.25.0",
"stylelint-config-recess-order": "^7.3.0",
"stylelint-config-standard": "^39.0.1",
"stylelint-config-standard-scss": "^16.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-define-config": "^16.24.0",
"svgo": "^4.0.0",
"tinyglobby": "^0.2.15",
"type-fest": "^5.1.0",
"typescript": "~5.9.2",
"unocss": "^66.5.1",
"unocss-preset-animations": "^1.2.1",
"unplugin-auto-import": "^20.1.0",
"unplugin-icons": "^22.2.0",
"unplugin-vue-components": "^29.0.0",
"unplugin-vue-markdown": "^29.1.0",
"unplugin-vue-components": "^29.2.0",
"unplugin-vue-markdown": "^29.2.0",
"unplugin-vue-router": "^0.16.0",
"vite": "^7.1.5",
"vite-plugin-checker": "^0.11.0",
@@ -136,6 +145,7 @@
"vite-plugin-vue-meta-layouts": "^0.6.1",
"vite-plugin-webfont-dl": "^3.11.1",
"vitest": "^3.2.4",
"vue-component-type-helpers": "^3.1.2",
"vue-i18n-extract": "^2.0.7",
"vue-macros": "3.1.1",
"vue-tsc": "^3.1.0",

1689
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env node
/**
* Type check script for lint-staged
* This script ignores file arguments passed by lint-staged and runs type-check on the entire project
*/
import { spawnSync } from 'child_process';
const result = spawnSync('pnpm', ['run', 'type-check'], {
stdio: 'inherit',
shell: process.platform === 'win32',
});
process.exit(result.status ?? 1);

View File

@@ -1,26 +1,18 @@
<script setup lang="ts">
import type { GlobalThemeOverrides } from 'naive-ui';
import { darkTheme } from 'naive-ui';
import { RouterView } from 'vue-router';
const appStore = useAppStore();
// https://www.naiveui.com/zh-CN/light/docs/customize-theme
const themeOverrides: GlobalThemeOverrides = {
common: {},
};
import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
</script>
<template>
<DynamicDialog />
<ConfirmDialog />
<Toast />
<n-config-provider
:theme-overrides
preflight-style-disabled
:theme="appStore.isDark ? darkTheme : null"
abstract
>
<RouterView />
</n-config-provider>
<AppNaiveUIProvider>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</AppNaiveUIProvider>
</template>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { GlobalThemeOverrides } from 'naive-ui';
import { darkTheme, dateZhCN, zhCN } from 'naive-ui';
import type { FunctionalComponent } from 'vue';
import { createTextVNode } from 'vue';
const appStore = useAppStore();
// https://www.naiveui.com/zh-CN/light/docs/customize-theme
const themeOverrides: GlobalThemeOverrides = {
common: {},
};
const ContextHolder: FunctionalComponent = () => {
window.$nLoadingBar = useLoadingBar();
window.$nModal = useModal();
window.$nDialog = useDialog();
window.$nMessage = useMessage();
window.$nNotification = useNotification();
return createTextVNode();
};
</script>
<script lang="ts">
declare global {
export interface Window {
$nLoadingBar?: import('naive-ui').LoadingBarProviderInst;
$nModal?: import('naive-ui').ModalProviderInst;
$nDialog?: import('naive-ui').DialogProviderInst;
$nMessage?: import('naive-ui').MessageProviderInst;
$nNotification?: import('naive-ui').NotificationProviderInst;
}
}
</script>
<template>
<NConfigProvider
:locale="zhCN"
:date-locale="dateZhCN"
:theme-overrides
preflight-style-disabled
:theme="appStore.isDark ? darkTheme : null"
abstract
>
<n-loading-bar-provider>
<n-message-provider>
<n-notification-provider>
<n-modal-provider>
<n-dialog-provider>
<slot></slot>
<ContextHolder />
</n-dialog-provider>
</n-modal-provider>
</n-notification-provider>
</n-message-provider>
</n-loading-bar-provider>
</NConfigProvider>
</template>

View File

@@ -8,16 +8,7 @@ import IconMenuRounded from '~icons/material-symbols/menu-rounded';
export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) {
const router = useRouter();
const { t, te } = useI18n({
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
return key;
},
fallbackRoot: true,
messages: locales4RouteMessages,
});
const { t, te } = routeI18nInstance.global;
// 获取路由表但是不包含布局路由
const routes = createGetRoutes(router)();
@@ -26,10 +17,15 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
const selectedKey = ref('');
watch(
() => router.currentRoute.value.path,
(newPath) => {
selectedKey.value = newPath;
menuInstRef.value?.showOption(newPath); // 展开菜单,确保设定的元素被显示,如果不传入 key 会展示当前选中元素
() => router.currentRoute.value,
(route) => {
// 优先使用 activeMenuName通过路由名称解析为路径如果没有则使用当前路径
const activeMenuPath = route.meta.activeMenuName
? router.resolve({ name: route.meta.activeMenuName }).path
: route.path;
selectedKey.value = activeMenuPath;
menuInstRef.value?.showOption(activeMenuPath); // 展开菜单,确保设定的元素被显示
},
{ immediate: true },
);
@@ -48,9 +44,8 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
const menuMap = new Map<string, MenuOption>();
const rootMenus: MenuOption[] = [];
// 过滤和排序路由
const validRoutes = routes
.filter((route) => {
// 过滤路由
const validRoutes = routes.filter((route) => {
// 过滤掉不需要显示的路由
if (route.meta?.hideInMenu === true || route.meta?.layout === false) {
return false;
@@ -60,55 +55,62 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
return false;
}
// 根据环境变量判断是否显示 /demos 开头的路由
if (import.meta.env.VITE_MENU_SHOW_DEMOS !== 'true' && route.path.startsWith('/demos')) {
if (import.meta.env.VITE_APP_MENU_SHOW_DEMOS !== 'true' && route.path.startsWith('/demos')) {
return false;
}
return true;
})
// 排序路由,确保父路由总是在子路由之前,同级路由则根据 `meta.order` 排序
.sort((a: RouteRecordRaw, b: RouteRecordRaw) => {
});
// 排序路由:先按路径深度分组,再按 order 排序
const sortedRoutes = validRoutes.slice().sort((a: RouteRecordRaw, b: RouteRecordRaw) => {
const pathA = a.path;
const pathB = b.path;
// 1. 首先按路径深度排序(确保父路由在子路由之前)
const depthA = pathA.split('/').filter(Boolean).length;
const depthB = pathB.split('/').filter(Boolean).length;
if (depthA !== depthB) {
return depthA - depthB;
}
// 2. 获取父路径,判断是否为同一父级下的路由
const segmentsA = pathA.split('/').filter(Boolean);
const segmentsB = pathB.split('/').filter(Boolean);
const parentAPath = `/${segmentsA.slice(0, -1).join('/')}`;
const parentBPath = `/${segmentsB.slice(0, -1).join('/')}`;
const parentA = segmentsA.length > 1 ? `/${segmentsA.slice(0, -1).join('/')}` : '/';
const parentB = segmentsB.length > 1 ? `/${segmentsB.slice(0, -1).join('/')}` : '/';
// 如果不是同级路由,则按路径排序,确保父路由在前
if (parentAPath !== parentBPath) {
return pathA.localeCompare(pathB);
// 如果父路径不同,按父路径字母顺序排序
if (parentA !== parentB) {
return parentA.localeCompare(parentB);
}
// 同级路由,处理 `meta.order`
// 3. 同一父级下的路由,按 order 排序
const orderA = a.meta?.order;
const orderB = b.meta?.order;
const hasOrderA = orderA !== undefined;
const hasOrderB = orderB !== undefined;
const hasOrderA = typeof orderA === 'number';
const hasOrderB = typeof orderB === 'number';
// 当一个有 order 而另一个没有时,有 order 的排在前面
if (hasOrderA !== hasOrderB) {
return hasOrderA ? -1 : 1;
}
// 有 order 的排在没有 order 的前面
if (hasOrderA && !hasOrderB) return -1;
if (!hasOrderA && hasOrderB) return 1;
// 当两个都有 order 时,按 order 值升序排序
// 都有 order 时,按 order 值升序排序
if (hasOrderA && hasOrderB) {
const orderDiff = orderA - orderB;
if (orderDiff !== 0) {
return orderDiff;
}
const diff = (orderA as number) - (orderB as number);
if (diff !== 0) return diff;
}
// order 相同或都没有 order按路径字母顺序排序
// order 相同或都没有 order按路径字母顺序排序
return pathA.localeCompare(pathB);
});
// 构建菜单树
for (const route of validRoutes) {
for (const route of sortedRoutes) {
const pathSegments = route.path.split('/').filter(Boolean);
const routeName = route.name as string;
let text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
if (import.meta.env.VITE_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
let text = te(routeName) ? t(routeName) : routeName;
if (import.meta.env.VITE_APP_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
const order = String(route.meta.order).padStart(orderMaxLength, '0');
text = `${order}. ${text}`;
}
@@ -147,11 +149,33 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
}
}
// 添加调试日志
if (import.meta.env.DEV) {
console.debug(
'排序后的路由:',
sortedRoutes.map((route) => ({
path: route.path,
name: route.name,
order: route.meta?.order,
})),
);
}
return rootMenus;
}
// console.debug('原始路由:', JSON.stringify(routes, null, 0));
// console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0));
if (import.meta.env.DEV) {
console.debug(
'原始路由:',
routes.map((route) => ({
path: route.path,
name: route.name,
order: route.meta?.order,
})),
);
console.debug('转换后的菜单:', options.value);
}
return {
options,
selectedKey,

View File

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

View File

@@ -25,7 +25,7 @@ function handleSelect(key: string) {
<NDropdown trigger="hover" placement="bottom-end" :options="options" @select="handleSelect">
<NButton quaternary class="flex items-center gap-1">
<template #icon>
<icon-clarity:language-line w-4.5 h-4.5 />
<icon-clarity-language-line w-4.5 h-4.5 />
</template>
<span>{{ languageLabels[locale] }}</span>
</NButton>

View File

@@ -4,7 +4,7 @@ const appStore = useAppStore();
const themeLabels: Record<AppThemeMode, string> = {
light: '浅色',
dark: '深色',
system: '跟随系统',
auto: '跟随系统',
};
</script>
@@ -12,18 +12,18 @@ const themeLabels: Record<AppThemeMode, string> = {
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
{{ themeLabels[appStore.themeMode] }}
<template #trigger>
<NButton quaternary @click="appStore.cycleTheme">
<icon-line-md:sunny-filled-loop-to-moon-filled-loop-transition
<NButton quaternary @click="appStore.cycleTheme()">
<icon-line-md-sunny-filled-loop-to-moon-filled-loop-transition
v-if="appStore.themeMode === 'light'"
w-4.5
h-4.5
/>
<icon-line-md:moon-filled-to-sunny-filled-loop-transition
<icon-line-md-moon-filled-to-sunny-filled-loop-transition
v-else-if="appStore.themeMode === 'dark'"
w-4.5
h-4.5
/>
<icon-line-md:computer v-else w-4.5 h-4.5 />
<icon-line-md-computer v-else w-4.5 h-4.5 />
</NButton>
</template>
</NTooltip>

View File

@@ -15,8 +15,8 @@ function toggleCollapsed() {
{{ appStore.sidebarCollapsed ? '展开菜单' : '收起菜单' }}
<template #trigger>
<NButton ref="buttonRef" quaternary @click="toggleCollapsed">
<icon-line-md:menu-fold-right v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
<icon-line-md:menu-fold-left v-else w-4.5 h-4.5 />
<icon-line-md-menu-fold-right v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
<icon-line-md-menu-fold-left v-else w-4.5 h-4.5 />
</NButton>
</template>
</NTooltip>

View File

@@ -0,0 +1,45 @@
<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

@@ -1,5 +1,5 @@
<script setup lang="tsx">
import { useAppStore } from '@/stores/app-store';
import { useAppStore } from '@/stores/app-store-auto-imports';
const menuInstRef = useTemplateRef('menuInstRef');
const { options, selectedKey } = useMetaLayoutsNMenuOptions({
@@ -12,13 +12,13 @@ const appStore = useAppStore();
<template>
<!-- @update:value="handleMenuUpdate" -->
<NMenu
mode="vertical"
ref="menuInstRef"
v-model:value="selectedKey"
mode="vertical"
:collapsed="appStore.sidebarCollapsed"
:collapsed-width="64"
:icon-size="20"
:collapsed-icon-size="24"
v-model:value="selectedKey"
:options="options"
:inverted="false"
:root-indent="32"

View File

@@ -8,12 +8,12 @@ const appStore = useAppStore();
<template>
<AdminLayout
v-model:sider-collapse="appStore.sidebarCollapsed"
mode="horizontal"
:footer-visible="!false"
:tab-visible="!false"
scroll-mode="content"
:is-mobile="appStore.isMobile"
v-model:sider-collapse="appStore.sidebarCollapsed"
>
<template #header>
<BaseLayoutHeader />
@@ -49,14 +49,4 @@ const appStore = useAppStore();
#__SCROLL_EL_ID__ {
@include scrollbar;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,10 +0,0 @@
export default {
Root: 'Index',
$Path: '$Path',
Demos: 'Demos',
DemosApiDemo: 'API Demo',
DemosCounterDemo: 'Counter Demo',
DemosI18nDemo: 'i18n Demo',
DemosWebsocketDemo: 'WebSocket Demo',
Home: 'Home',
} as const satisfies PageTitleLocalizations;

View File

@@ -1,10 +0,0 @@
export default {
Root: '根 (Gēn)',
$Path: '$Path',
Demos: '示例演示',
DemosApiDemo: 'API 调用示例',
DemosCounterDemo: '点击计数器',
DemosI18nDemo: '国际化示例',
DemosWebsocketDemo: 'WebSocket 示例',
Home: '首页',
} as const satisfies PageTitleLocalizations;

View File

@@ -0,0 +1,71 @@
/* https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n#static-bundle-importing
* All i18n resources specified in the plugin `include` option can be loaded
* at once using the import syntax
*/
import { router } from '@/plugins/00.router-plugin';
import messages from '@intlify/unplugin-vue-i18n/messages';
import { createGetRoutes } from 'virtual:meta-layouts';
import { createI18n } from 'vue-i18n';
const locale = useLocalStorage<string>('app-locale', navigator.language);
watchEffect(() => {
window.document.documentElement.setAttribute('lang', locale.value);
});
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
export const i18nInstance = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: locale.value,
fallbackRoot: false,
// flatJson: true,
missing: (locale, key /* , instance, type */) => {
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
return `[${key}]`;
},
missingWarn: !true,
fallbackWarn: !true,
messages,
});
export const routeI18nInstance = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: locale.value,
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
return key;
},
fallbackRoot: true,
messages: i18nRouteMessages,
});
watchEffect(
() => {
locale.value = i18nInstance.global.locale.value;
},
{ flush: 'sync' },
);
watch(
() => i18nInstance.global.locale.value,
() => {
routeI18nInstance.global.locale.value = i18nInstance.global.locale.value;
const routes = createGetRoutes(router)(); // 获取路由表但是不包含布局路由
routes.forEach((route) => {
const { t, te } = routeI18nInstance.global;
if (router.currentRoute.value.name) {
router.currentRoute.value.meta.title = t(router.currentRoute.value.name as string);
}
route.meta = route.meta || {};
const routeName = route.name as string;
route.meta.title = te(routeName) ? t(routeName) : routeName;
});
},
{ immediate: true, flush: 'sync' },
);

View File

@@ -1,4 +1,4 @@
# `locales-4-route`
# route-messages
此目录存放专门用于**路由名称**的国际化i18n消息。这些消息通过一套自定义的编译时安全机制为应用的导航菜单提供标题。
@@ -27,7 +27,7 @@
3. **编译时检查**:此目录下的每个语言环境文件(如 `en-US.ts`)都必须使用 `satisfies PageTitleLocalizations` 来进行类型断言。
```typescript
// src/locales-4-route/en-US.ts
// ./en-US.ts
export default { ... } satisfies PageTitleLocalizations;
```
@@ -52,13 +52,13 @@
使用上一步中自动生成的 `name` 作为键,在此目录的每个语言文件中添加翻译。
```ts
// src/locales-4-route/zh-CN.ts
// ./zh-CN.ts
export default {
// ... 其他翻译
DemosApiDemo: 'API 演示',
} satisfies PageTitleLocalizations;
// src/locales-4-route/en-US.ts
// ./en-US.ts
export default {
// ... 其他翻译
DemosApiDemo: 'API Demo',

View File

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

View File

@@ -1,13 +1,13 @@
import type { I18nOptions } from 'vue-i18n';
const modules = import.meta.glob(['./*.ts', '!./_messages-auto-imports.ts'], {
eager: true,
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
eager: true /* true 为同步false 为异步 */,
import: 'default',
});
type MessageType = Record<string, string>;
export const locales4RouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
export const i18nRouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
(messages, [path, mod]) => {
const locale = path.replace(/(\.\/|\.ts)/g, '');
messages[locale] = mod as MessageType;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
import './styles/index.ts';
import { LogLevels } from 'consola';
import App from './App.vue';
import { setupPlugins } from './plugins';
consola.level = LogLevels.verbose;
/* `import.meta.glob(${g}, { eager: ${isSync} })`; */
const autoInstallModules = import.meta.glob('./plugins/!(index).ts', { eager: true });
const app = createApp(App);
setupPlugins(app);
setupPlugins(createApp(App), autoInstallModules).mount('#app');
await new Promise((resolve) => setTimeout(resolve, 280));
app.mount('#app');

87
src/pages/Login.page.vue Normal file
View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
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>
<div class="login-page">
<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>
<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,11 +1,29 @@
<script lang="ts" setup>
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>
<template>
<main flex-1 class="flex flex-col items-center justify-center h-full space-y-4">
<h1>Not Found</h1>
<p>{{ path }} does not exist.</p>
<Button @click="$router.back()">Back</Button>
<Button @click="handleBack">{{ canGoBack ? 'Back' : 'Home' }}</Button>
</main>
</template>

View File

@@ -49,10 +49,10 @@ const callApi = async () => {
</div>
<button
@click="callApi"
:disabled="loading"
:aria-label="loading ? '正在调用API' : '调用API接口'"
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-3 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
@click="callApi"
>
<span v-if="loading" class="flex items-center justify-center">
<svg

View File

@@ -91,10 +91,10 @@ const resetCount = () => {
<div class="w-full flex flex-col gap-3">
<!-- 原生按钮 ( touch 事件) -->
<button
class="w-full bg-gradient-to-br from-orange-500 via-orange-600 to-red-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-orange-600 hover:via-orange-700 hover:to-red-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
@touchstart="() => {}"
@touchend="() => {}"
@click="incrementCount"
class="w-full bg-gradient-to-br from-orange-500 via-orange-600 to-red-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-orange-600 hover:via-orange-700 hover:to-red-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
>
<span class="flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -111,8 +111,8 @@ const resetCount = () => {
<!-- 原生按钮 ( touch 事件) -->
<button
@click="incrementCount"
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
@click="incrementCount"
>
<span class="flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -129,13 +129,13 @@ const resetCount = () => {
<!-- Naive UI 按钮 -->
<n-button
@click="incrementCount"
type="warning"
size="large"
block
strong
secondary
class="text-lg"
@click="incrementCount"
>
<template #icon>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -152,9 +152,9 @@ const resetCount = () => {
<!-- 重置按钮 -->
<button
@click="resetCount"
:disabled="clickCount === 0"
class="w-full bg-gradient-to-br from-gray-500 via-gray-600 to-gray-700 text-white font-semibold py-3 px-6 rounded-xl hover:from-gray-600 hover:via-gray-700 hover:to-gray-800 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02]"
@click="resetCount"
>
<span class="flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

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

View File

@@ -1,13 +1,9 @@
<script setup lang="ts">
definePage({
meta: {
order: 1,
},
});
const { t, locale } = useI18n();
definePage({ meta: { order: 1 } });
const { t, locale } = useI18n({});
function setLocale(newLocale: 'en-US' | 'zh-CN') {
locale.value = newLocale;
i18nInstance.global.locale.value = newLocale;
}
</script>
@@ -25,6 +21,8 @@ function setLocale(newLocale: 'en-US' | 'zh-CN') {
{{ t('page.i18n-demo.hello', { name: 'Kilo' }) }}
</n-p>
<n-p> $route.meta: {{ $route.meta }} </n-p>
<n-space>
<n-button type="primary" @click="setLocale('en-US')"> English </n-button>
<n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import type { MessageType } from 'naive-ui';
import { useDialog, useMessage } from 'naive-ui';
import UseSafeNForm from './use-safe-n-form.vue';
definePage({ meta: {} });
const message = useMessage();
const dialog = useDialog();
const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[];
const dialogTypes = ['info', 'success', 'warning', 'error'] as const;
const openAllMessages = () => {
messageTypes.forEach((type, index) => {
setTimeout(() => {
message[type](`${index + 1}. 消息内容`, {
duration: 3000,
closable: true,
});
}, index * 500);
});
};
const openDialog = (type: (typeof dialogTypes)[number]) => {
dialog[type]({
title: `${type.charAt(0).toUpperCase() + type.slice(1)} 弹窗`,
content: '这是一个命令式 API 创建的弹窗示例。',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('点击了确定');
},
onNegativeClick: () => {
message.error('点击了取消');
},
});
};
const openModal = () => {
window.$nModal!.create({
title: '命令式 Modal 示例',
content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。',
preset: 'dialog',
onPositiveClick: () => {
message.success('点击了确定');
},
onNegativeClick: () => {
message.error('点击了取消');
},
positiveText: '确定',
negativeText: '取消',
});
};
</script>
<template>
<div class="naive-ui-demo-page">
<NCard>
<template #header>Naive UI 组件演示</template>
<NAlert title="信息" type="info" :bordered="false">
演示 Naive UI 各种组件的使用方法和功能特性
</NAlert>
<n-card title="SafeNForm" mt-4>
<UseSafeNForm />
</n-card>
<NCard title="Message 消息" class="mt-4">
<NSpace>
<NButton
v-for="(type, index) in messageTypes"
:key="type"
@click="
message[type](`${index + 1}. 消息内容`, {
duration: 3000,
closable: true,
})
"
>
{{ `${index + 1}. ${type}` }}
</NButton>
<NButton @click="openAllMessages"> 一键打开所有 </NButton>
</NSpace>
</NCard>
<NCard title="Dialog 弹窗 (命令式 API)" class="mt-4">
<NSpace>
<NButton v-for="type in dialogTypes" :key="type" @click="openDialog(type)">
{{ type }}
</NButton>
</NSpace>
</NCard>
<NCard title="Modal 弹窗 (命令式 API)" class="mt-4">
<NSpace>
<NButton @click="openModal"> 打开 Modal </NButton>
</NSpace>
</NCard>
</NCard>
</div>
</template>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
const { formInst, formValue, SafeNForm, SafeNFormItem } = useSafeNForm({
initialFormValue: {
/* ⚠️:
如果没使用`SafeNFormItem`
这里`user`对象没有手动初始化的话,将会报错:
`can't access property "name", $setup.formValue.user is undefined`
*/
user: {
name: '',
age: 0,
},
phone: '',
},
});
function handleValidateClick() {
formInst.value?.validate((errors) => {
if (!errors) {
window.$nMessage!.success('Valid');
} else {
console.log(errors);
window.$nMessage!.error('Invalid');
}
});
}
</script>
<template>
<div border>
<pre>formValue: {{ JSON.stringify(formValue, null, 2) }}</pre>
</div>
<SafeNForm inline label-placement="left" label-width="auto" mt-4>
<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>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import type { ToastMessageOptions } from 'primevue/toast';
definePage({
meta: {},
});
const tostSeverities = [
'secondary',
'success',
'info' /* 默认 */,
'warn',
'error',
'contrast',
undefined,
] satisfies ToastMessageOptions['severity'][];
const openAllToasts = () => {
tostSeverities.forEach((severity, index) => {
setTimeout(() => {
ToastService.add({
severity,
summary: `severity: ${severity ?? 'default'}`,
life: 3000,
detail: `${index + 1}. 消息内容`,
});
}, index * 500);
});
};
</script>
<template>
<div class="prime-vue-demo-page">
<Card>
<template #title>PrimeVue 组件演示</template>
<template #content>
<Message severity="info">演示 PrimeVue 各种组件的使用方法和功能特性</Message>
<Panel header="Toast 消息" class="mt-1.5">
<div flex="~ wrap" gap="4">
<Button
v-for="(severity, index) in tostSeverities"
:key="severity ?? 'default'"
@click="
ToastService.add({
severity: severity,
summary: `severity: ${severity ?? 'default'}`,
life: 3000,
detail: '消息内容',
})
"
>
{{ `${index + 1}. ${severity ?? 'default'}` }}
</Button>
<Button @click="openAllToasts"> 一键打开所有 </Button>
</div>
</Panel>
</template>
</Card>
</div>
</template>

View File

@@ -227,12 +227,12 @@ onUnmounted(() => {
<!-- 控制按钮 -->
<div class="grid grid-cols-2 gap-2">
<button
@click="connectWebSocket"
:disabled="wsConnected || wsLoading"
:aria-label="
wsLoading ? '正在连接WebSocket' : wsConnected ? 'WebSocket已连接' : '连接WebSocket'
"
class="flex items-center justify-center bg-gradient-to-br from-green-500 via-green-600 to-emerald-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-green-600 hover:via-green-700 hover:to-emerald-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
@click="connectWebSocket"
>
<svg
v-if="wsLoading"
@@ -259,10 +259,10 @@ onUnmounted(() => {
</button>
<button
@click="disconnectWebSocket"
:disabled="!wsConnected || wsLoading"
:aria-label="!wsConnected ? 'WebSocket未连接' : '断开WebSocket连接'"
class="flex items-center justify-center bg-gradient-to-br from-red-500 via-red-600 to-pink-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-red-600 hover:via-red-700 hover:to-pink-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
@click="disconnectWebSocket"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -284,19 +284,19 @@ onUnmounted(() => {
<input
id="messageInput"
v-model="messageInput"
@keyup.enter="sendMessage"
placeholder="输入要发送的消息..."
:disabled="!wsConnected"
:aria-describedby="!wsConnected ? 'ws-status' : undefined"
class="flex-1 w-full border-2 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:text-gray-500 transition-all duration-300 border-gray-200 dark:border-gray-600 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-100 disabled:bg-gray-100/60 dark:disabled:bg-gray-800/60 hover:border-gray-300 dark:hover:border-gray-500"
@keyup.enter="sendMessage"
/>
<button
@click="sendMessage"
:disabled="!canSendMessage"
:aria-label="
!wsConnected ? 'WebSocket未连接' : canSendMessage ? '发送消息' : '请输入消息内容'
"
class="flex items-center bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-indigo-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
@click="sendMessage"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -309,11 +309,11 @@ onUnmounted(() => {
发送
</button>
<button
@click="sendMockData"
:disabled="!wsConnected"
:aria-label="!wsConnected ? 'WebSocket未连接' : '发送随机模拟数据'"
class="flex items-center bg-gradient-to-br from-purple-500 via-purple-600 to-violet-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-purple-600 hover:via-purple-700 hover:to-violet-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
:title="!wsConnected ? 'WebSocket未连接时不可用' : '发送随机模拟数据'"
@click="sendMockData"
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -354,9 +354,9 @@ onUnmounted(() => {
</h3>
<div class="flex gap-1">
<button
@click="exportMessages"
class="text-xs text-gray-500 hover:text-blue-500 transition-colors duration-200 flex items-center"
title="导出消息"
@click="exportMessages"
>
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -369,9 +369,9 @@ onUnmounted(() => {
导出
</button>
<button
@click="clearMessages"
class="text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 flex items-center"
title="清空消息"
@click="clearMessages"
>
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path

View File

@@ -2,6 +2,6 @@
<template>
<div>
<h1>Index Page</h1>
<n-button @click="$router.push({ name: 'DemosCreate' })">DemosCreate</n-button>
</div>
</template>

View File

@@ -10,17 +10,4 @@ export function install({ app }: { app: import('vue').App<Element> }) {
// 2. 显示全局错误提示
// 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

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

View File

@@ -0,0 +1,50 @@
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

@@ -1,25 +1,3 @@
/* https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n#static-bundle-importing
* All i18n resources specified in the plugin `include` option can be loaded
* at once using the import syntax
*/
import messages from '@intlify/unplugin-vue-i18n/messages';
import { createI18n } from 'vue-i18n';
export function install({ app }: { app: import('vue').App<Element> }) {
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
const i18n = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: navigator.language,
fallbackRoot: false,
// flatJson: true,
missing: (locale, key /* , instance, type */) => {
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
return `[${key}]`;
},
missingWarn: !true,
fallbackWarn: !true,
messages,
});
app.use(i18n);
app.use(i18nInstance);
}

View File

@@ -6,6 +6,7 @@ import Aura from '@primeuix/themes/aura';
import zhCN from 'primelocale/zh-CN.json';
import PrimeVue from 'primevue/config';
import StyleClass from 'primevue/styleclass';
import ToastService from 'primevue/toastservice';
export function install({ app }: { app: import('vue').App<Element> }) {
app.directive('styleclass', StyleClass);
@@ -25,4 +26,5 @@ export function install({ app }: { app: import('vue').App<Element> }) {
preset: Aura,
},
});
app.use(ToastService);
}

View File

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

View File

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,42 @@
import { useLocalStorage, useMediaQuery } from '@vueuse/core';
import { defineStore } from 'pinia';
import { computed } from 'vue';
// >>>>>
// https://vueuse.org/core/useColorMode/#advanced-usage
const { system, store: themeMode } = useColorMode({
modes: { light: '', dark: 'app-dark', auto: '' },
disableTransition: false,
});
const { state, next: cycleTheme } = useCycleList(['light', 'dark', 'auto'] as const, {
initialValue: themeMode,
});
watchEffect(() => (themeMode.value = state.value));
export type AppThemeMode = typeof themeMode.value;
// <<<<<
export const useAppStore = defineStore('app', () => {
// 侧边栏展开/收起状态
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
const toggleSidebar = useToggle(sidebarCollapsed);
// 主题模式
const actualTheme = computed(() => (themeMode.value === 'auto' ? system.value : themeMode.value));
const isDark = computed(() => actualTheme.value === 'dark');
// 是否是移动端
const isMobile = useMediaQuery('(max-width: 768px)');
return {
themeMode,
isDark,
isMobile,
cycleTheme,
sidebarCollapsed,
toggleSidebar,
};
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
}

View File

@@ -1,64 +0,0 @@
import { useLocalStorage, useMediaQuery, usePreferredColorScheme } from '@vueuse/core';
import { defineStore } from 'pinia';
import { computed, watch } from 'vue';
export const APP_THEME_MODES = ['light', 'dark', 'system'] as const;
export type AppThemeMode = (typeof APP_THEME_MODES)[number];
const DARK_CLASS = 'app-dark';
export const useAppStore = defineStore('app', () => {
const themeMode = useLocalStorage<AppThemeMode>('app-theme-mode', 'system');
const preferredColor = usePreferredColorScheme();
// 侧边栏展开/收起状态
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
// 计算实际使用的主题
const actualTheme = computed(() =>
themeMode.value === 'system'
? preferredColor.value === 'dark'
? 'dark'
: 'light'
: themeMode.value,
);
// 是否是暗色主题
const isDark = computed(() => actualTheme.value === 'dark');
// 是否是移动端
const isMobile = useMediaQuery('(max-width: 768px)');
// 更新 DOM 类名
function updateDomClass() {
document.documentElement.classList.toggle(DARK_CLASS, isDark.value);
}
// 循环切换主题
function cycleTheme() {
const currentIndex = APP_THEME_MODES.indexOf(themeMode.value);
const nextIndex = (currentIndex + 1) % APP_THEME_MODES.length;
themeMode.value = APP_THEME_MODES[nextIndex]!;
}
// 切换侧边栏展开/收起
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value;
}
// 监听主题变化,更新 DOM
watch(isDark, updateDomClass, { immediate: true });
return {
themeMode,
isDark,
isMobile,
cycleTheme,
sidebarCollapsed,
toggleSidebar,
};
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
}

View File

@@ -0,0 +1,45 @@
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

@@ -0,0 +1,8 @@
.p-confirmdialog,
.p-toast {
max-width: calc(100% - 50px);
}
.p-toast .p-toast-message-text {
margin-top: -0.2rem;
}

View File

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

View File

@@ -1,4 +1,8 @@
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
//
import 'primeicons/primeicons.css';
import './css/reset-primevue.css';
import './css/transition.css';
import 'virtual:uno.css';

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 formInst = 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) => {
formInst.value = inst as unknown as FormInst;
}}
>
{ctx.slots.default?.({})}
</NForm>
);
},
{
name: 'SafeNForm',
inheritAttrs: true,
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,
formInst,
};
}

View File

@@ -41,5 +41,6 @@ export default defineConfig({
// SCSS 专用的 at-rule 规则会自动处理 @include, @mixin 等
// 'scss/at-rule-no-unknown': true,
// <<<<<
'selector-class-pattern': null,
},
});

52
typed-router.d.ts vendored
View File

@@ -58,6 +58,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'DemosCreate': RouteRecordInfo<
'DemosCreate',
'/demos/create',
Record<never, never>,
Record<never, never>,
| never
>,
'DemosI18nDemo': RouteRecordInfo<
'DemosI18nDemo',
'/demos/i18n-demo',
@@ -65,6 +72,20 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'DemosNaiveUiDemo': RouteRecordInfo<
'DemosNaiveUiDemo',
'/demos/naive-ui-demo',
Record<never, never>,
Record<never, never>,
| never
>,
'DemosPrimevueDemo': RouteRecordInfo<
'DemosPrimevueDemo',
'/demos/primevue-demo',
Record<never, never>,
Record<never, never>,
| never
>,
'DemosWebsocketDemo': RouteRecordInfo<
'DemosWebsocketDemo',
'/demos/websocket-demo',
@@ -79,6 +100,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'Login': RouteRecordInfo<
'Login',
'/Login',
Record<never, never>,
Record<never, never>,
| never
>,
}
/**
@@ -122,12 +150,30 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/demos/create.page.vue': {
routes:
| 'DemosCreate'
views:
| never
}
'src/pages/demos/i18n-demo.page.vue': {
routes:
| 'DemosI18nDemo'
views:
| never
}
'src/pages/demos/naive-ui-demo/index.page.vue': {
routes:
| 'DemosNaiveUiDemo'
views:
| never
}
'src/pages/demos/primevue-demo.page.vue': {
routes:
| 'DemosPrimevueDemo'
views:
| never
}
'src/pages/demos/websocket-demo.page.vue': {
routes:
| 'DemosWebsocketDemo'
@@ -140,6 +186,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/Login.page.vue': {
routes:
| 'Login'
views:
| never
}
}
/**

View File

@@ -4,5 +4,7 @@ import UnoCSS from 'unocss/vite';
export default [
// https://github.com/antfu/unocss
// see uno.config.ts for config
UnoCSS(),
UnoCSS({
checkImport: true,
}),
] satisfies PluginOption;

View File

@@ -4,6 +4,7 @@ import path from 'node:path';
import AutoImport from 'unplugin-auto-import/vite';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import IconsResolver from 'unplugin-icons/resolver';
import Icons from 'unplugin-icons/vite';
import Components from 'unplugin-vue-components/vite';
import { VueRouterAutoImports } from 'unplugin-vue-router';
@@ -22,12 +23,12 @@ import { TDesignResolver } from 'unplugin-vue-components/resolvers';
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
import IconsResolver from 'unplugin-icons/resolver';
import { VantResolver } from '@vant/auto-import-resolver';
// <<<<<
function _getNaiveUiComponentNames() {
// [dtsTsx](https://github.com/unplugin/unplugin-vue-components/pull/673/files/84e80738885cfe11298f41f070cda94a7a779276)
// 方法1: 从 web-types.json 读取(推荐)
const webTypesPath = path.resolve('node_modules/naive-ui/web-types.json');
if (fs.existsSync(webTypesPath)) {
@@ -62,7 +63,7 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
dirs: [
// 'src/utils',
'src/composables',
'src/stores',
// 'src/stores',
// 匹配所有 -auto-imports.ts / -auto-imports.tsx 结尾的文件
'src/**/*-auto-imports.{ts,tsx}',
],
@@ -72,7 +73,7 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
'pinia',
'@vueuse/core',
VueRouterAutoImports,
createUtils4uAutoImports([]),
createUtils4uAutoImports(['primevue']),
{
'consola/browser': ['consola'],
'vue-router/auto': ['useLink'],
@@ -82,7 +83,7 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
'useMessage',
'useNotification',
'useLoadingBar',
..._getNaiveUiComponentNames(),
// ..._getNaiveUiComponentNames(),
],
},
],
@@ -91,12 +92,25 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
// https://github.com/antfu/unplugin-vue-components
Components({
syncMode: 'default',
dtsTsx: true,
// `__`开头的
excludeNames: [/^__/],
// allow auto load markdown components under `./src/components/`
extensions: ['vue', 'md'],
// allow auto import and register components used in markdown
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
include: [
// https://github.com/unplugin/unplugin-vue-components/blob/c9117ae93f60f81c8b5a41890cb7fa0133f34a12/src/core/unplugin.ts#L17
/\.vue$/,
/\.vue\?vue/,
/\.vue\.[tj]sx?\?vue/, // for vue-loader with experimentalInlineMatchResource enabled
/\.vue\?v=/,
//
/\.md$/, // allow auto import and register components used in markdown
/\.tsx/,
],
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js

View File

@@ -1,5 +1,6 @@
import type { PluginOption } from 'vite';
import { minify as minifyHtml } from 'html-minifier-terser';
import { loadEnv } from 'vite';
import type { ConfigEnv, PluginOption } from 'vite';
function IndexHtmlPlugin(): PluginOption {
return {
@@ -25,4 +26,73 @@ function IndexHtmlPlugin(): PluginOption {
};
}
export default [IndexHtmlPlugin()] satisfies 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 {
// return [___()];
const env = loadEnv(_configEnv.mode, process.cwd());
if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin();
}

View File

@@ -11,7 +11,7 @@ export function loadPlugin(configEnv: ConfigEnv): PluginOption {
return [];
}
if (env.VITE_ENABLE_VUE_DEVTOOLS !== 'true') {
if (env.VITE_APP_ENABLE_VUE_DEVTOOLS !== 'true') {
consola.info('vue-devtools plugin disabled by env');
return [];
}

View File

@@ -1,13 +1,14 @@
import { cloudflare } from '@cloudflare/vite-plugin';
import { loadEnv } from 'vite';
import type {ConfigEnv, PluginOption} from 'vite';
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
const env = loadEnv(_configEnv.mode, process.cwd());
if (_configEnv.mode === 'test') {
console.log('cloudflare plugin disabled in test mode');
return [];
}
if (process.env.VITE_CLOUDFLARE_SERVER_ENABLED !== 'true') {
if (env.VITE_CLOUDFLARE_SERVER_ENABLED !== 'true') {
console.log('cloudflare plugin disabled by env');
return [];
}

View File

@@ -1,9 +1,13 @@
import type { ConfigEnv, PluginOption } from 'vite';
import { loadEnv } from 'vite';
export default [
// ...
] satisfies PluginOption;
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
return [];
const env = loadEnv(_configEnv.mode, process.cwd());
console.debug(`env :>> `, env);
// ...
return undefined;
}

View File

@@ -20,7 +20,7 @@ export const viteConfigRollupOptions: RollupOptions = {
// assetFileNames:'', // 默认: "assets/[name]-[hash][extname]"
// https://cn.rollupjs.org/configuration-options/#output-assetfilenames
assetFileNames(chunkInfo: PreRenderedAsset) {
const names = chunkInfo.names;
const names = [...new Set(chunkInfo.names)];
if (names.length !== 1) {
console.error('Multiple names for asset:', chunkInfo);
@@ -42,9 +42,13 @@ export const viteConfigRollupOptions: RollupOptions = {
},
manualChunks: (id: string, _meta: ManualChunkMeta) => {
if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
return 'layouts';
}
// https://github.com/unocss/unocss/issues/4917
// if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
// const url = new URL(id, 'file://');
// if (!url.search /* ?vue&type=script&setup=true&lang.ts */) {
// return 'layouts';
// }
// }
if (id.includes('meta-layouts')) {
// console.debug(`id :>> `, id); // id :>> virtual:meta-layouts
@@ -53,9 +57,12 @@ export const viteConfigRollupOptions: RollupOptions = {
}
if (id.includes('index.page.vue')) {
const url = new URL(id, 'file://');
if (!url.search /* ?vue&type=script&setup=true&lang.ts */) {
const parentDir = path.basename(path.dirname(id));
return `${parentDir}-index.page`;
}
}
if (!id.includes('node_modules')) return;
// 处理 pnpm 的特殊路径结构
@@ -84,15 +91,20 @@ export const viteConfigRollupOptions: RollupOptions = {
return 'lib-vendor';
}
if (['naive-ui'].includes(packageName) && id.includes('_internal')) {
return 'lib-naive-ui-internal';
}
// // 拆了有问题
// if (['naive-ui'].includes(packageName) && id.includes('_internal')) {
// return 'lib-naive-ui-internal';
// }
if (['naive-ui'].includes(packageName)) {
return 'lib-naive-ui';
}
if (['primelocale', 'primevue', '@primeuix'].some((name) => packageName!.includes(name))) {
if (
['primelocale', 'primevue', 'primeuix', 'primeicons'].some((name) =>
packageName!.includes(name),
)
) {
return 'lib-primevue';
}

View File

@@ -13,12 +13,17 @@ export default defineConfig(async (configEnv) => {
const isBuild = command === 'build';
const env = loadEnv(mode, process.cwd());
if (process.env.CI) {
for (const [key, value] of Object.entries(env)) {
consola.info(`[vite.config.ts] env: ${key}: ${value}`);
}
}
return {
base: env.VITE_APP_BASE,
build: {
minify: env.VITE_APP_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
sourcemap: env.VITE_APP_BUILD_SOURCE_MAP === 'true',
minify: env.VITE_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
sourcemap: env.VITE_BUILD_SOURCE_MAP === 'true',
rollupOptions: viteConfigRollupOptions,
},
css: {
@@ -39,8 +44,6 @@ export default defineConfig(async (configEnv) => {
},
define: {
__DEV__: JSON.stringify(!isBuild),
// // https://github.com/fi3ework/vite-plugin-checker/issues/569#issuecomment-3254311799
// 'process.env.NODE_ENV': JSON.stringify('production'),
},
server: {
allowedHosts: ['.NWCT.DEV'],

View File

@@ -1,5 +1,5 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: b487ccbfcb48074f7cafff7488cb3acb)
// Generated by Wrangler by running `wrangler types` (hash: b18d7aec4937222767b077e627f9f927)
// Runtime types generated with workerd@1.20251008.0 2025-09-09
declare namespace Cloudflare {
interface GlobalProps {
@@ -7,13 +7,19 @@ declare namespace Cloudflare {
}
interface Env {
KV: KVNamespace;
VITE_BUILD_SOURCE_MAP: string;
VITE_BUILD_MINIFY: string;
VITE_CLOUDFLARE_SERVER_ENABLED: string;
VITE_APP_TITLE: string;
VITE_APP_BASE: string;
VITE_APP_BUILD_SOURCE_MAP: string;
VITE_APP_BUILD_COMMIT: string;
VITE_APP_BUILD_TIME: string;
VITE_ENABLE_VUE_DEVTOOLS: string;
VITE_MENU_SHOW_DEMOS: string;
VITE_APP_ENABLE_VUE_DEVTOOLS: string;
VITE_APP_MENU_SHOW_DEMOS: string;
VITE_APP_MENU_SHOW_ORDER: string;
VITE_APP_ENABLE_ROUTER_LOG_GUARD: string;
VITE_APP_API_URL: string;
VITE_APP_PROXY: string;
}
}
interface Env extends Cloudflare.Env {}