Compare commits
202 Commits
eslint-plu
...
a3b79c132e
| Author | SHA1 | Date | |
|---|---|---|---|
| a3b79c132e | |||
| 17730cfb10 | |||
| c4b93140f0 | |||
| 1a9257249f | |||
|
|
66280dc6a1 | ||
| 16c93a82a2 | |||
| 16d4b3f749 | |||
| 42f63f7134 | |||
| 9d12927793 | |||
| bb82a6ab83 | |||
| 76d2a45015 | |||
|
|
935251ee53 | ||
| 5472f1c9b9 | |||
| 744703811a | |||
| be3d59691b | |||
| f6bccd7536 | |||
| 4673d622b6 | |||
| 0e467a1e9d | |||
| 7a5610c0dc | |||
| 80d989f6d0 | |||
| 6097d39752 | |||
| 4bc62221b6 | |||
| c405bc9481 | |||
| 54cc46b33a | |||
| 65ee7cccb3 | |||
| 6da6e59b6d | |||
| 8e6d7488f4 | |||
|
|
9e050306bb | ||
| 7f1811098f | |||
| 40b3686f2e | |||
| dfc64ecece | |||
| 1df5110034 | |||
| 731a3da55d | |||
| 4e5b3cb96c | |||
| 5e8df2572f | |||
|
|
7d6474c92b | ||
|
|
86920da611 | ||
| 3828f12a2d | |||
| f269a25968 | |||
| 4a97eca056 | |||
| e91476f55c | |||
| afd08f05fc | |||
| b2f9b9d3b3 | |||
|
|
9cb70b7827 | ||
| 52b1e4fb6b | |||
| f101237792 | |||
| c84380836a | |||
| aca2c29946 | |||
| 9d1374d53c | |||
|
|
814130af34 | ||
| 4a5ae8d2de | |||
| 18fd9cb226 | |||
| 188c813447 | |||
| 737da9c585 | |||
| 80090fdb90 | |||
| abcf706acc | |||
| 760b75083d | |||
| 0b4575b181 | |||
| 57fb6d5256 | |||
| e516dd4ba6 | |||
| 6fc619315a | |||
| a5082ef4e4 | |||
| 9be884be20 | |||
|
|
bbec2a3ff9 | ||
|
|
aa851bb920 | ||
|
|
5610d3ea20 | ||
| 7a20a45035 | |||
| db3f77d6d3 | |||
|
|
e6731afff5 | ||
| 3affb21d99 | |||
|
|
2585b70f6b | ||
|
|
a4e162cab0 | ||
|
|
88cc365935 | ||
|
|
33801ca211 | ||
|
|
4f79426206 | ||
|
|
1185843982 | ||
| 0620651b12 | |||
|
|
a82190d4aa | ||
|
|
d0a7afd5df | ||
|
|
10fcb40d1d | ||
|
|
c7dccb0664 | ||
|
|
1bcecabf7b | ||
|
|
b7b508af08 | ||
|
|
917b3c7324 | ||
|
|
6a73a274bd | ||
|
|
82897ea690 | ||
|
|
e04310c2c9 | ||
| d2a21581ff | |||
|
|
a2c49cf600 | ||
|
|
3429c75600 | ||
| 0f57624cdf | |||
|
|
68fc6ad1a3 | ||
|
|
b4727a1946 | ||
|
|
f757b70174 | ||
|
|
3033a8a707 | ||
|
|
fa77b772be | ||
|
|
2440605fce | ||
|
|
386b65cd05 | ||
|
|
b6fd7c698c | ||
|
|
9376f8e68e | ||
|
|
3a3e9aead5 | ||
|
|
97009158e8 | ||
|
|
a9b916221c | ||
|
|
ac7cb6a31c | ||
|
|
fdf928028c | ||
|
|
3f31d188f6 | ||
| c200206487 | |||
|
|
b7aab0a6a5 | ||
|
|
37a0d59f13 | ||
|
|
163af5e80c | ||
| 8e148c6408 | |||
|
|
1f78af72fc | ||
| 169e0984a4 | |||
|
|
be9093fa77 | ||
|
|
5a6fdc0efd | ||
| 7551eed484 | |||
|
|
a4068df0be | ||
|
|
ccf61ee6c3 | ||
|
|
3170fe889b | ||
|
|
aa55508ed7 | ||
|
|
8d052bcac8 | ||
|
|
ce2f5338de | ||
| 966c40a338 | |||
|
|
fd42a4d6d5 | ||
|
|
f883e0bec1 | ||
|
|
17bdbce8c8 | ||
|
|
61e86e731d | ||
|
|
ff790c7a3a | ||
| 076b8f3407 | |||
| 269d93187c | |||
| 33676dcc8b | |||
|
|
290aed74f2 | ||
|
|
fd0543dcc2 | ||
|
|
fcd840d4fe | ||
|
|
2ba453bbed | ||
|
|
4b645bb916 | ||
| 0a8f1f8248 | |||
| eaafbef793 | |||
| 9f57c8fe0b | |||
|
|
e912b2c710 | ||
|
|
c0c5e9763e | ||
|
|
6e915a5977 | ||
|
|
acd7c0db13 | ||
| 1ad46a62fd | |||
| bb02b796aa | |||
|
|
a4ea7ce56e | ||
|
|
166d76d980 | ||
|
|
f9f82e4d29 | ||
|
|
b4fcde324d | ||
|
|
b669889bb0 | ||
|
|
8a3b9e03fd | ||
| ec02658ede | |||
|
|
bd9acc06a8 | ||
|
|
f790691d5a | ||
|
|
838d5cfb6e | ||
| ac544a8ff5 | |||
|
|
e7a4a7aff9 | ||
|
|
0d1a20d88d | ||
| 19aaddc4e2 | |||
| 1e24193b84 | |||
| cdec0c1a23 | |||
| 94cdecb075 | |||
|
|
7f0cf5dd8f | ||
|
|
c8b8a3caa4 | ||
|
|
48eb653f1a | ||
|
|
b3fbfe2d9d | ||
|
|
da80cad976 | ||
|
|
d6e05a8b44 | ||
| 5a196564d0 | |||
| 7ec1751ba6 | |||
|
|
270a838185 | ||
|
|
5b54fe5182 | ||
|
|
21676c11ff | ||
|
|
75f461df0f | ||
|
|
b623365e38 | ||
|
|
8130915d0e | ||
|
|
eeb72b24b5 | ||
|
|
68e320637e | ||
|
|
f81c7614be | ||
|
|
2874fdfaa7 | ||
|
|
7dd7ce73bc | ||
|
|
94d09d0bdd | ||
|
|
b0b65b454c | ||
|
|
c490cb1c8e | ||
|
|
33e8a4a5d6 | ||
|
|
35640a2ade | ||
|
|
8db7ded1b5 | ||
|
|
aa8e3467d3 | ||
|
|
8ed289a917 | ||
|
|
88ca601d07 | ||
|
|
50911ada4e | ||
|
|
d065c90e71 | ||
|
|
ad0a50d8c7 | ||
|
|
d4d9620db2 | ||
|
|
267bf75bc1 | ||
|
|
09ec2c7d12 | ||
|
|
ad0df6b140 | ||
|
|
3269b10bfd | ||
|
|
047632b75f | ||
|
|
7830ad0ebb | ||
|
|
a61a22569d | ||
|
|
c27c8544de |
39
.devcontainer/devcontainer.json
Normal file
39
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"image": "ghcr.io/yanhao98/h-devcontainer:main",
|
||||||
|
"runArgs": [
|
||||||
|
"--hostname=devcontainer-host",
|
||||||
|
"--name=${localWorkspaceFolderBasename}-devcontainer"
|
||||||
|
],
|
||||||
|
"forwardPorts": [4730, 4731, 5901],
|
||||||
|
"portsAttributes": {
|
||||||
|
"4730": { "label": "开发服务器端口", "onAutoForward": "notify" },
|
||||||
|
"4731": { "label": "预览服务器端口", "onAutoForward": "notify" }
|
||||||
|
},
|
||||||
|
"containerEnv": {
|
||||||
|
"TZ": "${localEnv:TZ:Asia/Shanghai}"
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["prettier.prettier-vscode", "vue.volar"],
|
||||||
|
"settings": {
|
||||||
|
// "tasks": { "version": "2.0.0", "tasks": [] },
|
||||||
|
"github.copilot.chat.codeGeneration.instructions": [
|
||||||
|
{
|
||||||
|
"text": "This dev container includes a lightweight Fluxbox based desktop that can be accessed using a VNC viewer or the web. GUI-based commands executed from the built-in VS Code terminal will open on the desktop automatically."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mounts": [
|
||||||
|
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
||||||
|
"source=${localWorkspaceFolder}/.devcontainer/lifecycle-scripts.d,target=/usr/local/etc/lifecycle-scripts.d,type=bind,consistency=delegated"
|
||||||
|
],
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/${localWorkspaceFolderBasename},type=bind,consistency=delegated",
|
||||||
|
"initializeCommand": "docker pull ghcr.io/yanhao98/h-devcontainer:main;",
|
||||||
|
"onCreateCommand": "/usr/local/bin/run-lifecycle-scripts.sh onCreateCommand",
|
||||||
|
"updateContentCommand": "/usr/local/bin/run-lifecycle-scripts.sh updateContentCommand",
|
||||||
|
"postCreateCommand": "/usr/local/bin/run-lifecycle-scripts.sh postCreateCommand",
|
||||||
|
"postStartCommand": "/usr/local/bin/run-lifecycle-scripts.sh postStartCommand",
|
||||||
|
"postAttachCommand": "/usr/local/bin/run-lifecycle-scripts.sh postAttachCommand"
|
||||||
|
}
|
||||||
0
.devcontainer/lifecycle-scripts.d/.gitkeep
Normal file
0
.devcontainer/lifecycle-scripts.d/.gitkeep
Normal file
14
.devcontainer/lifecycle-scripts.d/04-postStartCommand.d/-
Executable file
14
.devcontainer/lifecycle-scripts.d/04-postStartCommand.d/-
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/zsh -eu
|
||||||
|
h-setup-zh-locale
|
||||||
|
|
||||||
|
h-setup-bun-bin
|
||||||
|
h-setup-pnpm-bin
|
||||||
|
|
||||||
|
h-setup-chromium
|
||||||
|
h-setup-desktop-lite
|
||||||
|
|
||||||
|
h-install-node-modules
|
||||||
|
|
||||||
|
echo "-----------------------------"
|
||||||
|
echo "开发容器已启动并配置完成!"
|
||||||
|
echo "-----------------------------"
|
||||||
21
.env
21
.env
@@ -1,10 +1,15 @@
|
|||||||
|
VITE_APP_BUILD_TIME=NOT_SET
|
||||||
|
VITE_APP_BUILD_COMMIT=NOT_SET
|
||||||
|
|
||||||
|
VITE_BUILD_SOURCE_MAP=true
|
||||||
|
VITE_BUILD_MINIFY=true
|
||||||
|
VITE_CLOUDFLARE_SERVER_ENABLED=true
|
||||||
|
|
||||||
VITE_APP_TITLE=vue-ts-example-2025
|
VITE_APP_TITLE=vue-ts-example-2025
|
||||||
VITE_APP_BASE=/
|
VITE_APP_BASE=/
|
||||||
VITE_APP_BUILD_SOURCE_MAP=true
|
VITE_APP_ENABLE_VUE_DEVTOOLS=true
|
||||||
VITE_APP_BUILD_MINIFY=true
|
VITE_APP_MENU_SHOW_DEMOS=true
|
||||||
VITE_APP_BUILD_COMMIT=
|
VITE_APP_MENU_SHOW_ORDER=true
|
||||||
VITE_APP_BUILD_TIME=
|
VITE_APP_ENABLE_ROUTER_LOG_GUARD=true
|
||||||
VITE_ENABLE_VUE_DEVTOOLS=true
|
VITE_APP_API_URL=/API
|
||||||
VITE_MENU_SHOW_DEMOS=true
|
VITE_APP_PROXY=[["/API","https://jsonplaceholder.typicode.com"]]
|
||||||
VITE_MENU_SHOW_ORDER=true
|
|
||||||
VITE_CLOUDFLARE_SERVER_ENABLED=true
|
|
||||||
|
|||||||
10
.github/workflows/ci-cd.yaml
vendored
10
.github/workflows/ci-cd.yaml
vendored
@@ -14,10 +14,10 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
playwright:
|
playwright:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: mcr.microsoft.com/playwright:v1.56.1-noble
|
container: mcr.microsoft.com/playwright:v1.57.0-noble
|
||||||
steps:
|
steps:
|
||||||
- name: ⚙️ 设置 Node 环境
|
- name: ⚙️ 设置 Node 环境
|
||||||
uses: yanhao98/composite-actions/setup-node-environment@25eb4dc0c134cc9df2b7c569aa54140a366b45a8
|
uses: yanhao98/composite-actions/setup-node-environment@faab20ac2f9c85dfce1a4147fca493bf632bd744
|
||||||
# - name: 📥 安装 Playwright 浏览器
|
# - name: 📥 安装 Playwright 浏览器
|
||||||
# run: pnpm exec playwright install --with-deps
|
# run: pnpm exec playwright install --with-deps
|
||||||
- name: 📦 构建项目
|
- name: 📦 构建项目
|
||||||
@@ -32,13 +32,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 🛠️ 设置Node环境
|
- name: 🛠️ 设置Node环境
|
||||||
uses: yanhao98/composite-actions/setup-node-environment@25eb4dc0c134cc9df2b7c569aa54140a366b45a8
|
uses: yanhao98/composite-actions/setup-node-environment@faab20ac2f9c85dfce1a4147fca493bf632bd744
|
||||||
|
|
||||||
- name: 🔍 静态代码分析
|
- name: 🔍 静态代码分析
|
||||||
run: pnpm run lint
|
run: pnpm run lint
|
||||||
|
|
||||||
- name: 📦 构建项目
|
- name: 📦 构建项目
|
||||||
run: pnpm run build-only
|
run: |
|
||||||
|
export VITE_APP_BUILD_TIME=$(date +"%Y-%m-%d %H:%M:%S")
|
||||||
|
pnpm run build-only
|
||||||
env:
|
env:
|
||||||
VITE_APP_BUILD_COMMIT: ${{ github.sha }}
|
VITE_APP_BUILD_COMMIT: ${{ github.sha }}
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/测试最新依赖.yaml
vendored
2
.github/workflows/测试最新依赖.yaml
vendored
@@ -60,7 +60,7 @@ jobs:
|
|||||||
|
|
||||||
playwright:
|
playwright:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: mcr.microsoft.com/playwright:v1.56.1-noble
|
container: mcr.microsoft.com/playwright:v1.57.0-noble
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@main
|
- uses: actions/checkout@main
|
||||||
with:
|
with:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
|
.pnpm-store
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ cat $1
|
|||||||
echo "🟢 [Commit-msg] Node 版本:$(node -v)"
|
echo "🟢 [Commit-msg] Node 版本:$(node -v)"
|
||||||
|
|
||||||
# pnpm exec commitlint --edit $1
|
# pnpm exec commitlint --edit $1
|
||||||
time node node_modules/@commitlint/cli/cli.js --edit $1
|
node node_modules/@commitlint/cli/cli.js --edit $1
|
||||||
|
|
||||||
echo "📝 [Commit-msg] commit-msg 钩子完成!"
|
echo "📝 [Commit-msg] commit-msg 钩子完成!"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 此钩子在 git merge 或 git pull 成功完成后运行。
|
# 此钩子在 git merge 或 git pull 成功完成后运行。
|
||||||
echo "🔗 [Post-merge] 正在安装依赖..."
|
echo "🔗 [Post-merge] 正在安装依赖..."
|
||||||
time pnpm install
|
pnpm install
|
||||||
echo "🔗 [Post-merge] 依赖安装完成!"
|
echo "🔗 [Post-merge] 依赖安装完成!"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# 此钩子在执行 git commit 命令时,在创建提交之前运行。
|
# 此钩子在执行 git commit 命令时,在创建提交之前运行。
|
||||||
echo "🧹 [Pre-commit] 正在运行 lint-staged..."
|
echo "🧹 [Pre-commit] 正在运行 lint-staged..."
|
||||||
time pnpm exec lint-staged
|
pnpm exec lint-staged
|
||||||
time pnpm run lint:vue-i18n-extract
|
pnpm run lint:vue-i18n-extract
|
||||||
|
# pnpm run type-check
|
||||||
echo "🧹 [Pre-commit] lint-staged 完成!"
|
echo "🧹 [Pre-commit] lint-staged 完成!"
|
||||||
|
|||||||
8
.npmrc
8
.npmrc
@@ -1,9 +1 @@
|
|||||||
# registry=https://registry.npmmirror.com/
|
# registry=https://registry.npmmirror.com/
|
||||||
|
|
||||||
# https://pnpm.io/zh/npmrc#node-mirrorltreleasedir
|
|
||||||
use-node-version=24.7.0
|
|
||||||
node-mirror:release=https://npmmirror.com/mirrors/node/ # pnpm config set node-mirror:release=https://npmmirror.com/mirrors/node/
|
|
||||||
node-mirror:rc=https://npmmirror.com/mirrors/node-rc/
|
|
||||||
node-mirror:nightly=https://npmmirror.com/mirrors/node-nightly/
|
|
||||||
|
|
||||||
# shamefully-hoist=true
|
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all",
|
||||||
|
"plugins": ["@prettier/plugin-oxc"]
|
||||||
}
|
}
|
||||||
|
|||||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -2,10 +2,11 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"Vue.volar",
|
"Vue.volar",
|
||||||
"vitest.explorer",
|
"vitest.explorer",
|
||||||
"ms-playwright.playwright",
|
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"oxc.oxc-vscode",
|
"oxc.oxc-vscode",
|
||||||
"esbenp.prettier-vscode"
|
"prettier.prettier-vscode",
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
"lokalise.i18n-ally"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
69
.vscode/settings.json
vendored
69
.vscode/settings.json
vendored
@@ -1,34 +1,65 @@
|
|||||||
{
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||||
|
"vue-router$",
|
||||||
|
"**/src/composables/**",
|
||||||
|
"**/*-auto-imports.ts",
|
||||||
|
"**/*-auto-imports.types.ts"
|
||||||
|
],
|
||||||
|
"search.exclude": {
|
||||||
|
"public/report-ui-dist": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 代码格式化相关配置
|
||||||
|
// ============================================================
|
||||||
|
"prettier.enable": true,
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"dist/**": true
|
||||||
|
},
|
||||||
|
"eslint.enable": true,
|
||||||
|
"oxc.enable": true,
|
||||||
|
"stylelint.enable": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit",
|
"source.fixAll": "explicit",
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.fixAll.stylelint": "explicit",
|
|
||||||
"source.fixAll.oxc": "explicit",
|
"source.fixAll.oxc": "explicit",
|
||||||
"source.organizeImports": "never"
|
"source.fixAll.stylelint": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
|
||||||
|
|
||||||
"stylelint.enable": true,
|
|
||||||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
|
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
|
||||||
"scss.lint.unknownAtRules": "ignore",
|
"scss.lint.unknownAtRules": "ignore",
|
||||||
"css.lint.unknownAtRules": "ignore",
|
"css.lint.unknownAtRules": "ignore",
|
||||||
"less.lint.unknownAtRules": "ignore",
|
"less.lint.unknownAtRules": "ignore",
|
||||||
|
"editor.defaultFormatter": "prettier.prettier-vscode",
|
||||||
"eslint.enable": true,
|
"[typescript]": {
|
||||||
"oxc.enable": true,
|
"editor.defaultFormatter": "prettier.prettier-vscode"
|
||||||
"[vue]": {
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "prettier.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "prettier.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "prettier.prettier-vscode"
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
|
|
||||||
"i18n-ally.localesPaths": ["src/locales"],
|
// ============================================================
|
||||||
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容
|
// i18n-ally 配置
|
||||||
"i18n-ally.displayLanguage": "zh-CN", // 显示语言 (显示文件/翻译文件)
|
// ============================================================
|
||||||
|
// >>>>>
|
||||||
|
"i18n-ally.readonly": false,
|
||||||
|
"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.keystyle": "nested",
|
||||||
|
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言 (源文件) 根据此语言文件翻译其他语言文件的变量和内容
|
||||||
// https://github.com/copilot/share/8a1a019a-0180-80e7-8141-a40be02c4006
|
"i18n-ally.displayLanguage": "zh-CN" // 显示语言 (显示文件/翻译文件)
|
||||||
|
// <<<<<
|
||||||
// "iconify.customCollectionJsonPaths": ["https://example.com/my-icons.json", "./local/icons.json"],
|
// "iconify.customCollectionJsonPaths": ["https://example.com/my-icons.json", "./local/icons.json"],
|
||||||
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"typescript.preferences.autoImportFileExcludePatterns": ["vue-router/auto$"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
.vscode/tasks.json
vendored
24
.vscode/tasks.json
vendored
@@ -6,16 +6,12 @@
|
|||||||
"script": "dev",
|
"script": "dev",
|
||||||
"label": "🚀 dev",
|
"label": "🚀 dev",
|
||||||
"detail": "启动开发服务器",
|
"detail": "启动开发服务器",
|
||||||
"problemMatcher": {
|
"problemMatcher": [],
|
||||||
"pattern": { "regexp": "." },
|
|
||||||
"background": {
|
|
||||||
"activeOnStart": true,
|
|
||||||
"beginsPattern": "VITE.*ready in",
|
|
||||||
"endsPattern": "(Local|Network):.*http"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
"presentation": {
|
||||||
|
"reveal": "always", // 控制运行任务的终端是否显示。可按选项 "revealProblems" 进行替代。默认设置为“始终”。
|
||||||
|
"panel": "dedicated" // dedicated:一个任务独占一个终端,方便查看特定任务的日志,不会被其他任务干扰。
|
||||||
|
},
|
||||||
"group": { "kind": "build", "isDefault": false }
|
"group": { "kind": "build", "isDefault": false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23,7 +19,6 @@
|
|||||||
"script": "build-only",
|
"script": "build-only",
|
||||||
"label": "🔨 build-only",
|
"label": "🔨 build-only",
|
||||||
"detail": "" /* 如果为空或省略,VSCode 将自动使用 package.json 中 scripts[scriptName] 的值作为 detail */,
|
"detail": "" /* 如果为空或省略,VSCode 将自动使用 package.json 中 scripts[scriptName] 的值作为 detail */,
|
||||||
"problemMatcher": ["$tsc"],
|
|
||||||
"presentation": { "reveal": "always", "panel": "shared" },
|
"presentation": { "reveal": "always", "panel": "shared" },
|
||||||
"group": { "kind": "none", "isDefault": false }
|
"group": { "kind": "none", "isDefault": false }
|
||||||
},
|
},
|
||||||
@@ -33,14 +28,7 @@
|
|||||||
"label": "☁️ wrangler:dev",
|
"label": "☁️ wrangler:dev",
|
||||||
"detail": "启动 Cloudflare Workers 开发服务器,相当于预览",
|
"detail": "启动 Cloudflare Workers 开发服务器,相当于预览",
|
||||||
"dependsOn": ["🔨 build-only"],
|
"dependsOn": ["🔨 build-only"],
|
||||||
"problemMatcher": {
|
"problemMatcher": [],
|
||||||
"pattern": { "regexp": "." },
|
|
||||||
"background": {
|
|
||||||
"activeOnStart": true,
|
|
||||||
"beginsPattern": "wrangler dev",
|
|
||||||
"endsPattern": "Ready on"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
"presentation": { "reveal": "always", "panel": "dedicated" },
|
||||||
"group": { "kind": "build", "isDefault": false }
|
"group": { "kind": "build", "isDefault": false }
|
||||||
|
|||||||
25
AGENTS.md
25
AGENTS.md
@@ -9,6 +9,7 @@ Vue 3 TypeScript application with Vite.
|
|||||||
## 开发服务器
|
## 开发服务器
|
||||||
|
|
||||||
- **不要启动开发服务器**: 开发服务器通常已经由用户启动。除非特别要求,否则不要执行 `pnpm dev` 之类的命令。
|
- **不要启动开发服务器**: 开发服务器通常已经由用户启动。除非特别要求,否则不要执行 `pnpm dev` 之类的命令。
|
||||||
|
- **不要执行 Playwright 测试**: 除非用户明确要求,否则不要运行 Playwright 端到端测试。
|
||||||
|
|
||||||
### Routing & Layouts
|
### Routing & Layouts
|
||||||
|
|
||||||
@@ -22,14 +23,15 @@ Multiple auto-import systems are active:
|
|||||||
|
|
||||||
- **Vue APIs**: Core Vue, VueUse, Pinia, Vue Router, vue-i18n
|
- **Vue APIs**: Core Vue, VueUse, Pinia, Vue Router, vue-i18n
|
||||||
- **Components**: Auto-registered from multiple UI libraries (Naive UI, PrimeVue)
|
- **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**:
|
**IMPORTANT - Auto-Import Limitations**:
|
||||||
|
|
||||||
- **Dynamic components**: Auto-imported components cannot be used with `<component :is="..." />` syntax
|
- **Dynamic components**: Auto-imported components cannot be used with `<component :is="..." />` syntax
|
||||||
- ❌ Avoid: `<component :is="`icon-${name}`" />`
|
- ❌ Avoid: `<component :is="`icon-${name}`" />`
|
||||||
- ✅ Use: `<icon-foo v-if="condition" />` with `v-if`/`v-else-if`/`v-else` directives
|
- ✅ 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
|
```tsx
|
||||||
import IconMenuRounded from '~icons/material-symbols/menu-rounded';
|
import IconMenuRounded from '~icons/material-symbols/menu-rounded';
|
||||||
@@ -45,25 +47,6 @@ Multiple auto-import systems are active:
|
|||||||
Project has multiple UI frameworks configured:
|
Project has multiple UI frameworks configured:
|
||||||
|
|
||||||
- **Naive UI**
|
- **Naive UI**
|
||||||
- **Form Layout**: When using `NGrid` for form layouts, prefer `NFormItemGi` over nesting `NFormItem` inside `NGridItem` for more concise code.
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- ❌ Avoid: Verbose nesting -->
|
|
||||||
<NGrid :cols="4">
|
|
||||||
<NGridItem>
|
|
||||||
<NFormItem label="Username">
|
|
||||||
<NInput />
|
|
||||||
</NFormItem>
|
|
||||||
</NGridItem>
|
|
||||||
</NGrid>
|
|
||||||
|
|
||||||
<!-- ✅ Use: Concise and direct -->
|
|
||||||
<NGrid :cols="4">
|
|
||||||
<NFormItemGi label="Username">
|
|
||||||
<NInput />
|
|
||||||
</NFormItemGi>
|
|
||||||
</NGrid>
|
|
||||||
```
|
|
||||||
|
|
||||||
- **PrimeVue**:
|
- **PrimeVue**:
|
||||||
|
|
||||||
|
|||||||
980
auto-imports.d.ts
vendored
980
auto-imports.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
|||||||
import type { UserConfig } from '@commitlint/types';
|
import type { UserConfig } from '@commitlint/types';
|
||||||
|
|
||||||
|
|
||||||
const Configuration: UserConfig = {
|
const Configuration: UserConfig = {
|
||||||
extends: ['@commitlint/config-conventional'],
|
extends: ['@commitlint/config-conventional'],
|
||||||
formatter: '@commitlint/format',
|
formatter: '@commitlint/format',
|
||||||
|
rules: {
|
||||||
|
'body-max-line-length': [2, 'always', 500],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Configuration;
|
export default Configuration;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/node22/tsconfig.json",
|
"extends": "@tsconfig/node24/tsconfig.json",
|
||||||
"include": ["./**/*"]
|
"include": ["./**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import pluginImport from 'eslint-plugin-import';
|
import vueI18n from '@intlify/eslint-plugin-vue-i18n';
|
||||||
import { globalIgnores } from 'eslint/config';
|
// import stylistic from '@stylistic/eslint-plugin';
|
||||||
|
import pluginVitest from '@vitest/eslint-plugin';
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
|
||||||
import {
|
import {
|
||||||
|
configureVueProject,
|
||||||
defineConfigWithVueTs,
|
defineConfigWithVueTs,
|
||||||
vueTsConfigs,
|
vueTsConfigs,
|
||||||
configureVueProject,
|
|
||||||
} from '@vue/eslint-config-typescript';
|
} from '@vue/eslint-config-typescript';
|
||||||
import pluginVue from 'eslint-plugin-vue';
|
import pluginImport from 'eslint-plugin-import';
|
||||||
import pluginVitest from '@vitest/eslint-plugin';
|
import pluginJsonc from 'eslint-plugin-jsonc';
|
||||||
import pluginPlaywright from 'eslint-plugin-playwright';
|
|
||||||
import pluginOxlint from 'eslint-plugin-oxlint';
|
import pluginOxlint from 'eslint-plugin-oxlint';
|
||||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
|
import pluginPerfectionist from 'eslint-plugin-perfectionist';
|
||||||
|
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'] });
|
configureVueProject({ scriptLangs: ['ts', 'tsx'] });
|
||||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
@@ -20,7 +25,14 @@ export default defineConfigWithVueTs(
|
|||||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||||
},
|
},
|
||||||
|
|
||||||
globalIgnores(['worker-configuration.d.ts', '**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
globalIgnores([
|
||||||
|
'worker-configuration.d.ts',
|
||||||
|
'**/dist/**',
|
||||||
|
'**/dist-ssr/**',
|
||||||
|
'**/coverage/**',
|
||||||
|
'**/public/**',
|
||||||
|
'**/-----TEMP-----/**',
|
||||||
|
]),
|
||||||
|
|
||||||
pluginVue.configs['flat/essential'],
|
pluginVue.configs['flat/essential'],
|
||||||
vueTsConfigs.recommended,
|
vueTsConfigs.recommended,
|
||||||
@@ -35,10 +47,29 @@ export default defineConfigWithVueTs(
|
|||||||
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
|
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
|
||||||
},
|
},
|
||||||
...pluginOxlint.configs['flat/recommended'],
|
...pluginOxlint.configs['flat/recommended'],
|
||||||
|
|
||||||
|
// https://eslint-plugin-vue-i18n.intlify.dev/started.html#getting-started
|
||||||
|
...vueI18n.configs.recommended,
|
||||||
{
|
{
|
||||||
plugins: {
|
rules: {
|
||||||
import: pluginImport,
|
'@intlify/vue-i18n/no-raw-text': 'off',
|
||||||
|
'@intlify/vue-i18n/no-unused-keys': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
src: './src',
|
||||||
|
extensions: ['.js', '.ts', '.tsx', '.vue'],
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'vue-i18n': {
|
||||||
|
localeDir: './src/locales/**/*.json',
|
||||||
|
messageSyntaxVersion: '^11.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugins: { import: pluginImport },
|
||||||
rules: {
|
rules: {
|
||||||
'import/first': 'error',
|
'import/first': 'error',
|
||||||
'import/no-duplicates': 'error',
|
'import/no-duplicates': 'error',
|
||||||
@@ -52,7 +83,25 @@ export default defineConfigWithVueTs(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{ plugins: { perfectionist: pluginPerfectionist } },
|
||||||
|
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 启用 sort-keys 规则以强制对象键按字母顺序排序
|
||||||
|
* 原因:
|
||||||
|
* 1. 减少多人协作时的合并冲突
|
||||||
|
* 2. 保持代码一致性,提高可维护性
|
||||||
|
*/
|
||||||
|
files: ['src/locales/**/*.json'],
|
||||||
|
languageOptions: { parser: jsoncParser },
|
||||||
|
plugins: { jsonc: pluginJsonc },
|
||||||
|
rules: { 'jsonc/sort-keys': 'error' },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// plugins: {
|
||||||
|
// '@stylistic': stylistic,
|
||||||
|
// },
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'vue/block-order': [
|
'vue/block-order': [
|
||||||
@@ -65,7 +114,20 @@ export default defineConfigWithVueTs(
|
|||||||
'error',
|
'error',
|
||||||
{ order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'] },
|
{ order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'] },
|
||||||
],
|
],
|
||||||
|
'vue/attributes-order': 'error',
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/padding-line-between-blocks': ['error', 'always'],
|
||||||
|
'vue/component-name-in-template-casing': [
|
||||||
|
'error',
|
||||||
|
'PascalCase',
|
||||||
|
{ registeredComponentsOnly: false, ignores: [] },
|
||||||
|
],
|
||||||
|
// '@stylistic/padding-line-between-statements': [
|
||||||
|
// 'error',
|
||||||
|
// { blankLine: 'always', prev: '*', next: ['const', 'let', 'var'] },
|
||||||
|
// { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
|
||||||
|
// { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
|
||||||
|
// ],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
70
index.html
70
index.html
@@ -11,40 +11,6 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
/>
|
/>
|
||||||
<script>
|
|
||||||
window.addEventListener('DOMContentLoaded', function () {
|
|
||||||
window.ontouchstart = function () {};
|
|
||||||
window.ontouchend = function () {};
|
|
||||||
});
|
|
||||||
|
|
||||||
window.onloadX = function () {
|
|
||||||
// 禁止双指缩放
|
|
||||||
document.addEventListener('touchstart', function (event) {
|
|
||||||
if (event.touches.length > 1) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 禁止双击放大
|
|
||||||
var lastTouchEnd = 0;
|
|
||||||
document.addEventListener(
|
|
||||||
'touchend',
|
|
||||||
function (event) {
|
|
||||||
var now = new Date().getTime();
|
|
||||||
if (now - lastTouchEnd <= 300) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
lastTouchEnd = now;
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 禁止手势事件
|
|
||||||
document.addEventListener('gesturestart', function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
@@ -132,7 +98,40 @@
|
|||||||
<!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> -->
|
<!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> -->
|
||||||
<!-- .min.js 是 jsDelivr 的特殊处理 -->
|
<!-- .min.js 是 jsDelivr 的特殊处理 -->
|
||||||
<!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> -->
|
<!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> -->
|
||||||
</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>
|
<script>
|
||||||
(function (d) {
|
(function (d) {
|
||||||
var config = {
|
var config = {
|
||||||
@@ -163,4 +162,5 @@
|
|||||||
s.parentNode.insertBefore(tk, s);
|
s.parentNode.insertBefore(tk, s);
|
||||||
}); /* (document) */
|
}); /* (document) */
|
||||||
</script>
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
161
package.json
161
package.json
@@ -1,45 +1,49 @@
|
|||||||
{
|
{
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.25.0",
|
||||||
"name": "vue-ts-example-2025",
|
"name": "vue-ts-example-2025",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"_devenginesruntime_docs": "https://pnpm.io/zh/package_json#devenginesruntime",
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"devEngines": {
|
||||||
|
"runtime": {
|
||||||
|
"name": "node",
|
||||||
|
"version": "^24.11.1",
|
||||||
|
"onFail": "download"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"_all": "run-p build-only type-check lint format:prettier test:unit:DisableWatch",
|
"all": "run-s lint format:prettier build-only type-check test:unit:DisableWatch",
|
||||||
"dev": "vite --port 4730 --host --strictPort",
|
"dev": "nodemon --delay 280ms --watch vite-plugins --ext ts -x vite -- --port 4730 --host --strictPort",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"preview": "vite preview --port 4731 --host --strictPort",
|
"preview": "vite preview --port 4731 --host --strictPort",
|
||||||
"wrangler:dev": "wrangler dev --port 4732",
|
"wrangler:dev": "wrangler dev --port 4732",
|
||||||
"format:prettier": "prettier --write src/",
|
"format:prettier": "prettier --config=.prettierrc.json --cache --write --log-level=warn src/",
|
||||||
"type-check": "vue-tsc --build",
|
"type-check": "vue-tsc --build",
|
||||||
"lint": "run-s lint:*",
|
"lint": "run-s lint:*",
|
||||||
"lint:vue-i18n-extract": "vue-i18n-extract report --vueFiles './src/**/*.?(ts|tsx|vue)' --languageFiles './src/locales/*.?(json|yml|yaml|js)' --ci",
|
"lint:vue-i18n-extract": "vue-i18n-extract report --vueFiles './src/**/*.?(ts|tsx|vue)' --languageFiles './src/locales/**/*.?(json|yml|yaml|js)' --ci",
|
||||||
"lint:stylelint": "stylelint \"**/*.{css,less,scss,vue}\" --fix --ignore-path .gitignore",
|
"lint:stylelint": "stylelint --config=stylelint.config.mjs --cache --output-file=node_modules/.cache/stylelint/stylelint-report.json --cache-location=node_modules/.cache/stylelint/.stylelintcache --fix --ignore-path=.gitignore '**/*.{css,less,scss,vue}'",
|
||||||
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
||||||
"lint:eslint": "eslint . --fix",
|
"lint:eslint": "eslint . --fix --config=eslint.config.ts --concurrency=auto --env-info --cache --cache-location=node_modules/.cache/eslint/.eslintcache",
|
||||||
"test:unit:DisableWatch": "vitest --run",
|
"test:unit:DisableWatch": "vitest --run",
|
||||||
"test:playwright:headless": "HEADLESS=true playwright test --quiet",
|
"test:playwright:headless": "HEADLESS=true playwright test --quiet",
|
||||||
|
"_stylelint-config": "stylelint --config=stylelint.config.mjs --print-config src/styles/scss/global.scss",
|
||||||
"postinstall": "wrangler types",
|
"postinstall": "wrangler types",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"{server,src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
|
"{src,e2e}/**/*.{js,jsx,ts,tsx,vue}": [
|
||||||
"prettier --write",
|
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"oxlint --fix"
|
"oxlint --fix",
|
||||||
|
"prettier --write"
|
||||||
],
|
],
|
||||||
"{src,packages}/**/*.{css,less,scss,vue}": [
|
"{src,packages}/**/*.{css,less,scss,vue}": [
|
||||||
"stylelint --fix"
|
"stylelint --fix"
|
||||||
]
|
],
|
||||||
|
"{src/locales-utils,src/locales}/**/*": "node scripts/type-check-for-lint-staged.mjs"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
|
||||||
"vue-tsc": "$vue-tsc"
|
|
||||||
},
|
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
@@ -50,95 +54,110 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@commitlint/cli": "^20.0.0",
|
"@commitlint/cli": "^20.1.0",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
"@formkit/auto-animate": "^0.9.0",
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
"@pinia/colada": "^0.17.4",
|
"@pinia/colada": "^0.21.0",
|
||||||
"@primeuix/themes": "^1.2.3",
|
"@primeuix/themes": "^2.0.0",
|
||||||
"@sa/materials": "workspace:*",
|
"@sa/materials": "workspace:*",
|
||||||
"@unhead/vue": "^2.0.14",
|
"@unhead/vue": "^2.0.19",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"naive-ui": "^2.43.1",
|
"jsonc-eslint-parser": "^2.4.1",
|
||||||
"pinia": "^3.0.3",
|
"lodash-es": "^4.17.21",
|
||||||
"primelocale": "^2.1.7",
|
"naive-ui": "^2.43.2",
|
||||||
"primevue": "^4.3.9",
|
"pinia": "^3.0.4",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primelocale": "^2.2.2",
|
||||||
|
"primevue": "^4.4.1",
|
||||||
"ts-enum-util": "^4.1.0",
|
"ts-enum-util": "^4.1.0",
|
||||||
"utils4u": "^4.2.3",
|
"utils4u": "^5",
|
||||||
"vue": "^3.5.21",
|
"vue": "^3.5.24",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.2.1",
|
||||||
|
"vue-memoize-dict": "^1.1.3",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.13.2",
|
"@cloudflare/vite-plugin": "^1.15.2",
|
||||||
"@commitlint/types": "^20.0.0",
|
"@commitlint/types": "^20.0.0",
|
||||||
"@iconify-json/carbon": "^1.2.13",
|
"@iconify-json/carbon": "^1.2.14",
|
||||||
"@iconify-json/clarity": "^1.2.4",
|
"@iconify-json/clarity": "^1.2.4",
|
||||||
"@iconify-json/line-md": "^1.2.11",
|
"@iconify-json/line-md": "^1.2.11",
|
||||||
"@iconify-json/material-symbols": "^1.2.42",
|
"@iconify-json/material-symbols": "^1.2.47",
|
||||||
"@intlify/unplugin-vue-i18n": "^11.0.0",
|
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
|
||||||
"@playwright/test": "^1.55.0",
|
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||||
"@prettier/plugin-oxc": "^0.0.4",
|
"@playwright/test": "^1.57.0",
|
||||||
"@primevue/auto-import-resolver": "^4.3.9",
|
"@prettier/plugin-oxc": "^0.1.3",
|
||||||
"@primevue/metadata": "^4.3.9",
|
"@primevue/auto-import-resolver": "^4.4.1",
|
||||||
|
"@primevue/metadata": "^4.4.1",
|
||||||
"@stylelint-types/stylelint-order": "^7.0.0",
|
"@stylelint-types/stylelint-order": "^7.0.0",
|
||||||
"@stylelint-types/stylelint-scss": "^6.11.0",
|
"@stylelint-types/stylelint-scss": "^6.11.0",
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@stylistic/eslint-plugin": "^5.6.1",
|
||||||
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/html-minifier-terser": "^7.0.2",
|
"@types/html-minifier-terser": "^7.0.2",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/node": "^22.18.1",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
"@vant/auto-import-resolver": "^1.3.0",
|
"@vant/auto-import-resolver": "^1.3.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
"@vitejs/plugin-vue-jsx": "^5.1.2",
|
||||||
"@vitest/eslint-plugin": "^1.3.9",
|
"@vitest/eslint-plugin": "^1.4.3",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"boxen": "^8.0.1",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-oxlint": "~1.23.0",
|
"eslint-plugin-jsonc": "^2.21.0",
|
||||||
"eslint-plugin-playwright": "^2.2.2",
|
"eslint-plugin-oxlint": "~1.32.0",
|
||||||
"eslint-plugin-vue": "~10.5.0",
|
"eslint-plugin-perfectionist": "^5.0.0",
|
||||||
"happy-dom": "^20.0.1",
|
"eslint-plugin-playwright": "^2.3.0",
|
||||||
|
"eslint-plugin-vue": "~10.6.0",
|
||||||
|
"happy-dom": "^20.0.10",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.2.0",
|
||||||
"lint-staged": "^16.1.6",
|
"lint-staged": "^16.2.7",
|
||||||
|
"nodemon": "^3.1.11",
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"oxlint": "~1.23.0",
|
"oxlint": "~1.29.0",
|
||||||
"postcss-html": "^1.8.0",
|
"postcss-html": "^1.8.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.7.4",
|
||||||
"rollup": "^4.52.5",
|
"rollup": "^4.53.3",
|
||||||
"sass-embedded": "^1.93.2",
|
"sass-embedded": "^1.93.3",
|
||||||
"stylelint": "^16.25.0",
|
"sharp": "^0.34.5",
|
||||||
"stylelint-config-recess-order": "^7.3.0",
|
"stylelint": "^16.26.0",
|
||||||
|
"stylelint-config-recess-order": "^7.4.0",
|
||||||
"stylelint-config-standard": "^39.0.1",
|
"stylelint-config-standard": "^39.0.1",
|
||||||
"stylelint-config-standard-scss": "^16.0.0",
|
"stylelint-config-standard-scss": "^16.0.0",
|
||||||
"stylelint-config-standard-vue": "^1.0.0",
|
"stylelint-config-standard-vue": "^1.0.0",
|
||||||
"stylelint-define-config": "^16.24.0",
|
"stylelint-define-config": "^16.24.0",
|
||||||
|
"svgo": "^4.0.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"typescript": "~5.9.2",
|
"type-fest": "^5.2.0",
|
||||||
"unocss": "^66.5.1",
|
"typescript": "~5.9.3",
|
||||||
"unocss-preset-animations": "^1.2.1",
|
"unocss": "^66.5.9",
|
||||||
"unplugin-auto-import": "^20.1.0",
|
"unocss-preset-animations": "^1.3.0",
|
||||||
"unplugin-icons": "^22.2.0",
|
"unplugin-auto-import": "^20.2.0",
|
||||||
"unplugin-vue-components": "^29.0.0",
|
"unplugin-icons": "^22.5.0",
|
||||||
"unplugin-vue-markdown": "^29.1.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"unplugin-vue-router": "^0.16.0",
|
"unplugin-vue-markdown": "^29.2.0",
|
||||||
"vite": "^7.1.5",
|
"unplugin-vue-router": "^0.19.0",
|
||||||
"vite-plugin-checker": "^0.11.0",
|
"vite": "^7.2.4",
|
||||||
"vite-plugin-fake-server": "^2.2.0",
|
"vite-plugin-checker": "^0.12.0",
|
||||||
"vite-plugin-image-optimizer": "^2.0.2",
|
"vite-plugin-fake-server": "^2.2.2",
|
||||||
"vite-plugin-vue-devtools": "^8.0.1",
|
"vite-plugin-image-optimizer": "^2.0.3",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
"vite-plugin-vue-meta-layouts": "^0.6.1",
|
"vite-plugin-vue-meta-layouts": "^0.6.1",
|
||||||
"vite-plugin-webfont-dl": "^3.11.1",
|
"vite-plugin-webfont-dl": "^3.11.1",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^4.0.13",
|
||||||
|
"vue-component-type-helpers": "^3.1.4",
|
||||||
"vue-i18n-extract": "^2.0.7",
|
"vue-i18n-extract": "^2.0.7",
|
||||||
"vue-macros": "3.1.1",
|
"vue-macros": "3.1.2",
|
||||||
"vue-tsc": "^3.1.0",
|
"vue-tsc": "^3.1.8",
|
||||||
"wrangler": "^4.37.1"
|
"wrangler": "^4.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6309
pnpm-lock.yaml
generated
6309
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,6 @@
|
|||||||
packages:
|
packages:
|
||||||
- "packages/*"
|
- 'packages/*'
|
||||||
|
# shamefullyHoist: false # https://pnpm.io/zh/settings#shamefullyhoist
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
vue-tsc: $vue-tsc
|
||||||
|
|||||||
15
scripts/type-check-for-lint-staged.mjs
Executable file
15
scripts/type-check-for-lint-staged.mjs
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type check script for lint-staged
|
||||||
|
* This script ignores file arguments passed by lint-staged and runs type-check on the entire project
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
|
const result = spawnSync('pnpm', ['run', 'type-check'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
28
src/App.vue
28
src/App.vue
@@ -1,26 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
|
||||||
import { darkTheme } from 'naive-ui';
|
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
|
import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
// https://www.naiveui.com/zh-CN/light/docs/customize-theme
|
|
||||||
const themeOverrides: GlobalThemeOverrides = {
|
|
||||||
common: {},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DynamicDialog />
|
<DynamicDialog />
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
<Toast />
|
<Toast style="z-index: 5000" />
|
||||||
<n-config-provider
|
|
||||||
:theme-overrides
|
<AppNaiveUIProvider>
|
||||||
preflight-style-disabled
|
<RouterView v-slot="{ Component }">
|
||||||
:theme="appStore.isDark ? darkTheme : null"
|
<Transition name="fade" mode="out-in">
|
||||||
abstract
|
<component :is="Component" />
|
||||||
>
|
</Transition>
|
||||||
<RouterView />
|
</RouterView>
|
||||||
</n-config-provider>
|
</AppNaiveUIProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
58
src/AppNaiveUIProvider.vue
Normal file
58
src/AppNaiveUIProvider.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
|
import { darkTheme, dateZhCN, zhCN } from 'naive-ui';
|
||||||
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
import { createTextVNode } from 'vue';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
// https://www.naiveui.com/zh-CN/light/docs/customize-theme
|
||||||
|
const themeOverrides: GlobalThemeOverrides = {
|
||||||
|
common: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextHolder: FunctionalComponent = () => {
|
||||||
|
window.$nLoadingBar = useLoadingBar();
|
||||||
|
window.$nModal = useModal();
|
||||||
|
window.$nDialog = useDialog();
|
||||||
|
window.$nMessage = useMessage();
|
||||||
|
window.$nNotification = useNotification();
|
||||||
|
return createTextVNode();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
declare global {
|
||||||
|
export interface Window {
|
||||||
|
$nLoadingBar?: import('naive-ui').LoadingBarProviderInst;
|
||||||
|
$nModal?: import('naive-ui').ModalProviderInst;
|
||||||
|
$nDialog?: import('naive-ui').DialogProviderInst;
|
||||||
|
$nMessage?: import('naive-ui').MessageProviderInst;
|
||||||
|
$nNotification?: import('naive-ui').NotificationProviderInst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NConfigProvider
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:theme-overrides
|
||||||
|
preflight-style-disabled
|
||||||
|
:theme="appStore.isDark ? darkTheme : null"
|
||||||
|
abstract
|
||||||
|
>
|
||||||
|
<NLoadingBarProvider>
|
||||||
|
<NMessageProvider>
|
||||||
|
<NNotificationProvider>
|
||||||
|
<NModalProvider>
|
||||||
|
<NDialogProvider>
|
||||||
|
<slot></slot>
|
||||||
|
<ContextHolder />
|
||||||
|
</NDialogProvider>
|
||||||
|
</NModalProvider>
|
||||||
|
</NNotificationProvider>
|
||||||
|
</NMessageProvider>
|
||||||
|
</NLoadingBarProvider>
|
||||||
|
</NConfigProvider>
|
||||||
|
</template>
|
||||||
@@ -8,16 +8,7 @@ import IconMenuRounded from '~icons/material-symbols/menu-rounded';
|
|||||||
export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) {
|
export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t, te } = useI18n({
|
const { t, te } = routeI18nInstance.global;
|
||||||
inheritLocale: true,
|
|
||||||
useScope: 'local',
|
|
||||||
missing: (locale, key) => {
|
|
||||||
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
|
|
||||||
return key;
|
|
||||||
},
|
|
||||||
fallbackRoot: true,
|
|
||||||
messages: locales4RouteMessages,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取路由表但是不包含布局路由
|
// 获取路由表但是不包含布局路由
|
||||||
const routes = createGetRoutes(router)();
|
const routes = createGetRoutes(router)();
|
||||||
@@ -26,10 +17,15 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
const selectedKey = ref('');
|
const selectedKey = ref('');
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => router.currentRoute.value.path,
|
() => router.currentRoute.value,
|
||||||
(newPath) => {
|
(route) => {
|
||||||
selectedKey.value = newPath;
|
// 优先使用 activeMenuName(通过路由名称解析为路径),如果没有则使用当前路径
|
||||||
menuInstRef.value?.showOption(newPath); // 展开菜单,确保设定的元素被显示,如果不传入 key 会展示当前选中元素
|
const activeMenuPath = route.meta.activeMenuName
|
||||||
|
? router.resolve({ name: route.meta.activeMenuName }).path
|
||||||
|
: route.path;
|
||||||
|
|
||||||
|
selectedKey.value = activeMenuPath;
|
||||||
|
menuInstRef.value?.showOption(activeMenuPath); // 展开菜单,确保设定的元素被显示
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -48,9 +44,8 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
const menuMap = new Map<string, MenuOption>();
|
const menuMap = new Map<string, MenuOption>();
|
||||||
const rootMenus: MenuOption[] = [];
|
const rootMenus: MenuOption[] = [];
|
||||||
|
|
||||||
// 过滤和排序路由
|
// 过滤路由
|
||||||
const validRoutes = routes
|
const validRoutes = routes.filter((route) => {
|
||||||
.filter((route) => {
|
|
||||||
// 过滤掉不需要显示的路由
|
// 过滤掉不需要显示的路由
|
||||||
if (route.meta?.hideInMenu === true || route.meta?.layout === false) {
|
if (route.meta?.hideInMenu === true || route.meta?.layout === false) {
|
||||||
return false;
|
return false;
|
||||||
@@ -60,55 +55,62 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 根据环境变量判断是否显示 /demos 开头的路由
|
// 根据环境变量判断是否显示 /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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
});
|
||||||
// 排序路由,确保父路由总是在子路由之前,同级路由则根据 `meta.order` 排序
|
|
||||||
.sort((a: RouteRecordRaw, b: RouteRecordRaw) => {
|
// 排序路由:先按路径深度分组,再按 order 排序
|
||||||
|
const sortedRoutes = validRoutes.slice().sort((a: RouteRecordRaw, b: RouteRecordRaw) => {
|
||||||
const pathA = a.path;
|
const pathA = a.path;
|
||||||
const pathB = b.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 segmentsA = pathA.split('/').filter(Boolean);
|
||||||
const segmentsB = pathB.split('/').filter(Boolean);
|
const segmentsB = pathB.split('/').filter(Boolean);
|
||||||
const parentAPath = `/${segmentsA.slice(0, -1).join('/')}`;
|
const parentA = segmentsA.length > 1 ? `/${segmentsA.slice(0, -1).join('/')}` : '/';
|
||||||
const parentBPath = `/${segmentsB.slice(0, -1).join('/')}`;
|
const parentB = segmentsB.length > 1 ? `/${segmentsB.slice(0, -1).join('/')}` : '/';
|
||||||
|
|
||||||
// 如果不是同级路由,则按路径排序,确保父路由在前
|
// 如果父路径不同,按父路径字母顺序排序
|
||||||
if (parentAPath !== parentBPath) {
|
if (parentA !== parentB) {
|
||||||
return pathA.localeCompare(pathB);
|
return parentA.localeCompare(parentB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同级路由,处理 `meta.order`
|
// 3. 同一父级下的路由,按 order 排序
|
||||||
const orderA = a.meta?.order;
|
const orderA = a.meta?.order;
|
||||||
const orderB = b.meta?.order;
|
const orderB = b.meta?.order;
|
||||||
const hasOrderA = orderA !== undefined;
|
const hasOrderA = typeof orderA === 'number';
|
||||||
const hasOrderB = orderB !== undefined;
|
const hasOrderB = typeof orderB === 'number';
|
||||||
|
|
||||||
// 当一个有 order 而另一个没有时,有 order 的排在前面
|
// 有 order 的排在没有 order 的前面
|
||||||
if (hasOrderA !== hasOrderB) {
|
if (hasOrderA && !hasOrderB) return -1;
|
||||||
return hasOrderA ? -1 : 1;
|
if (!hasOrderA && hasOrderB) return 1;
|
||||||
}
|
|
||||||
|
|
||||||
// 当两个都有 order 时,按 order 值升序排序
|
// 都有 order 时,按 order 数值升序排序
|
||||||
if (hasOrderA && hasOrderB) {
|
if (hasOrderA && hasOrderB) {
|
||||||
const orderDiff = orderA - orderB;
|
const diff = (orderA as number) - (orderB as number);
|
||||||
if (orderDiff !== 0) {
|
if (diff !== 0) return diff;
|
||||||
return orderDiff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// order 相同或都没有 order,按路径字母顺序排序
|
// order 相同或都没有 order,按路径名字母顺序排序
|
||||||
return pathA.localeCompare(pathB);
|
return pathA.localeCompare(pathB);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 构建菜单树
|
// 构建菜单树
|
||||||
for (const route of validRoutes) {
|
for (const route of sortedRoutes) {
|
||||||
const pathSegments = route.path.split('/').filter(Boolean);
|
const pathSegments = route.path.split('/').filter(Boolean);
|
||||||
const routeName = route.name as string;
|
const routeName = route.name as string;
|
||||||
|
|
||||||
let text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
|
let text = te(routeName) ? t(routeName) : routeName;
|
||||||
if (import.meta.env.VITE_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
|
if (import.meta.env.VITE_APP_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
|
||||||
const order = String(route.meta.order).padStart(orderMaxLength, '0');
|
const order = String(route.meta.order).padStart(orderMaxLength, '0');
|
||||||
text = `${order}. ${text}`;
|
text = `${order}. ${text}`;
|
||||||
}
|
}
|
||||||
@@ -147,11 +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;
|
return rootMenus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('原始路由:', JSON.stringify(routes, null, 0));
|
if (import.meta.env.DEV) {
|
||||||
// console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0));
|
console.debug(
|
||||||
|
'原始路由:',
|
||||||
|
routes.map((route) => ({
|
||||||
|
path: route.path,
|
||||||
|
name: route.name,
|
||||||
|
order: route.meta?.order,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
console.debug('转换后的菜单:', options.value);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
selectedKey,
|
selectedKey,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
|
import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
|
||||||
import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
|
import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
|
||||||
import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
||||||
|
import UserDropdown from './components/UserDropdown.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,6 +12,7 @@ import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<LanguageSwitchButton />
|
<LanguageSwitchButton />
|
||||||
<ThemeSwitchButton />
|
<ThemeSwitchButton />
|
||||||
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function handleSelect(key: string) {
|
|||||||
<NDropdown trigger="hover" placement="bottom-end" :options="options" @select="handleSelect">
|
<NDropdown trigger="hover" placement="bottom-end" :options="options" @select="handleSelect">
|
||||||
<NButton quaternary class="flex items-center gap-1">
|
<NButton quaternary class="flex items-center gap-1">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-clarity:language-line w-4.5 h-4.5 />
|
<IconClarityLanguageLine w-4.5 h-4.5 />
|
||||||
</template>
|
</template>
|
||||||
<span>{{ languageLabels[locale] }}</span>
|
<span>{{ languageLabels[locale] }}</span>
|
||||||
</NButton>
|
</NButton>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const appStore = useAppStore();
|
|||||||
const themeLabels: Record<AppThemeMode, string> = {
|
const themeLabels: Record<AppThemeMode, string> = {
|
||||||
light: '浅色',
|
light: '浅色',
|
||||||
dark: '深色',
|
dark: '深色',
|
||||||
system: '跟随系统',
|
auto: '跟随系统',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -12,18 +12,18 @@ const themeLabels: Record<AppThemeMode, string> = {
|
|||||||
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
||||||
{{ themeLabels[appStore.themeMode] }}
|
{{ themeLabels[appStore.themeMode] }}
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton quaternary @click="appStore.cycleTheme">
|
<NButton quaternary @click="appStore.cycleTheme()">
|
||||||
<icon-line-md:sunny-filled-loop-to-moon-filled-loop-transition
|
<IconLineMdSunnyFilledLoopToMoonFilledLoopTransition
|
||||||
v-if="appStore.themeMode === 'light'"
|
v-if="appStore.themeMode === 'light'"
|
||||||
w-4.5
|
w-4.5
|
||||||
h-4.5
|
h-4.5
|
||||||
/>
|
/>
|
||||||
<icon-line-md:moon-filled-to-sunny-filled-loop-transition
|
<IconLineMdMoonFilledToSunnyFilledLoopTransition
|
||||||
v-else-if="appStore.themeMode === 'dark'"
|
v-else-if="appStore.themeMode === 'dark'"
|
||||||
w-4.5
|
w-4.5
|
||||||
h-4.5
|
h-4.5
|
||||||
/>
|
/>
|
||||||
<icon-line-md:computer v-else w-4.5 h-4.5 />
|
<IconLineMdComputer v-else w-4.5 h-4.5 />
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ function toggleCollapsed() {
|
|||||||
{{ appStore.sidebarCollapsed ? '展开菜单' : '收起菜单' }}
|
{{ appStore.sidebarCollapsed ? '展开菜单' : '收起菜单' }}
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton ref="buttonRef" quaternary @click="toggleCollapsed">
|
<NButton ref="buttonRef" quaternary @click="toggleCollapsed">
|
||||||
<icon-line-md:menu-fold-right v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
|
<IconLineMdMenuFoldRight v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
|
||||||
<icon-line-md:menu-fold-left v-else w-4.5 h-4.5 />
|
<IconLineMdMenuFoldLeft v-else w-4.5 h-4.5 />
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<IconMaterialSymbolsAccountCircle w-5 h-5 />
|
||||||
|
</NButton>
|
||||||
|
</NDropdown>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store-auto-imports';
|
||||||
|
|
||||||
const menuInstRef = useTemplateRef('menuInstRef');
|
const menuInstRef = useTemplateRef('menuInstRef');
|
||||||
const { options, selectedKey } = useMetaLayoutsNMenuOptions({
|
const { options, selectedKey } = useMetaLayoutsNMenuOptions({
|
||||||
@@ -12,13 +12,13 @@ const appStore = useAppStore();
|
|||||||
<template>
|
<template>
|
||||||
<!-- @update:value="handleMenuUpdate" -->
|
<!-- @update:value="handleMenuUpdate" -->
|
||||||
<NMenu
|
<NMenu
|
||||||
mode="vertical"
|
|
||||||
ref="menuInstRef"
|
ref="menuInstRef"
|
||||||
|
v-model:value="selectedKey"
|
||||||
|
mode="vertical"
|
||||||
:collapsed="appStore.sidebarCollapsed"
|
:collapsed="appStore.sidebarCollapsed"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:icon-size="20"
|
:icon-size="20"
|
||||||
:collapsed-icon-size="24"
|
:collapsed-icon-size="24"
|
||||||
v-model:value="selectedKey"
|
|
||||||
:options="options"
|
:options="options"
|
||||||
:inverted="false"
|
:inverted="false"
|
||||||
:root-indent="32"
|
:root-indent="32"
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ const appStore = useAppStore();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
|
v-model:sider-collapse="appStore.sidebarCollapsed"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:footer-visible="!false"
|
:footer-visible="!false"
|
||||||
:tab-visible="!false"
|
:tab-visible="!false"
|
||||||
scroll-mode="content"
|
scroll-mode="content"
|
||||||
:is-mobile="appStore.isMobile"
|
:is-mobile="appStore.isMobile"
|
||||||
v-model:sider-collapse="appStore.sidebarCollapsed"
|
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<BaseLayoutHeader />
|
<BaseLayoutHeader />
|
||||||
@@ -29,11 +29,11 @@ const appStore = useAppStore();
|
|||||||
<BaseLayoutSider />
|
<BaseLayoutSider />
|
||||||
</template>
|
</template>
|
||||||
<!-- <div>GlobalContent</div> -->
|
<!-- <div>GlobalContent</div> -->
|
||||||
<router-view v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</transition>
|
</Transition>
|
||||||
</router-view>
|
</RouterView>
|
||||||
<!-- <div>ThemeDrawer</div> -->
|
<!-- <div>ThemeDrawer</div> -->
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div
|
<div
|
||||||
@@ -49,14 +49,4 @@ const appStore = useAppStore();
|
|||||||
#__SCROLL_EL_ID__ {
|
#__SCROLL_EL_ID__ {
|
||||||
@include scrollbar;
|
@include scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.25s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export default {
|
|
||||||
Root: '根 (Gēn)',
|
|
||||||
$Path: '$Path',
|
|
||||||
Demos: '示例演示',
|
|
||||||
DemosApiDemo: 'API 调用示例',
|
|
||||||
DemosCounterDemo: '点击计数器',
|
|
||||||
DemosI18nDemo: '国际化示例',
|
|
||||||
DemosWebsocketDemo: 'WebSocket 示例',
|
|
||||||
Home: '首页',
|
|
||||||
} as const satisfies PageTitleLocalizations;
|
|
||||||
80
src/locales-utils/i18n-auto-imports.ts
Normal file
80
src/locales-utils/i18n-auto-imports.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/* 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 { router } from '@/plugins/00.router-plugin';
|
||||||
|
import { createGetRoutes } from 'virtual:meta-layouts';
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import { START_LOCATION } from 'vue-router';
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
if (__DEV__) {
|
||||||
|
ToastService.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: '菜单翻译缺失',
|
||||||
|
detail: `菜单翻译缺失: locale=${locale}, key=${key}`,
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
fallbackRoot: true,
|
||||||
|
messages: i18nRouteMessages,
|
||||||
|
});
|
||||||
|
export const routeI18nT = routeI18nInstance.global.t;
|
||||||
|
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
locale.value = i18nInstance.global.locale.value;
|
||||||
|
},
|
||||||
|
{ flush: 'sync' },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => i18nInstance.global.locale.value,
|
||||||
|
() => {
|
||||||
|
const { t, te } = routeI18nInstance.global;
|
||||||
|
|
||||||
|
routeI18nInstance.global.locale.value = i18nInstance.global.locale.value;
|
||||||
|
|
||||||
|
if (router.currentRoute.value.name && router.currentRoute.value !== START_LOCATION) {
|
||||||
|
router.currentRoute.value.meta.title = t(router.currentRoute.value.name as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = createGetRoutes(router)(); // 获取路由表但是不包含布局路由
|
||||||
|
|
||||||
|
routes.forEach((route) => {
|
||||||
|
const routeName = route.name as string;
|
||||||
|
route.meta.title = te(routeName) ? t(routeName) : routeName;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true, flush: 'sync' },
|
||||||
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# `locales-4-route`
|
# route-messages
|
||||||
|
|
||||||
此目录存放专门用于**路由名称**的国际化(i18n)消息。这些消息通过一套自定义的编译时安全机制,为应用的导航菜单提供标题。
|
此目录存放专门用于**路由名称**的国际化(i18n)消息。这些消息通过一套自定义的编译时安全机制,为应用的导航菜单提供标题。
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
3. **编译时检查**:此目录下的每个语言环境文件(如 `en-US.ts`)都必须使用 `satisfies PageTitleLocalizations` 来进行类型断言。
|
3. **编译时检查**:此目录下的每个语言环境文件(如 `en-US.ts`)都必须使用 `satisfies PageTitleLocalizations` 来进行类型断言。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/locales-4-route/en-US.ts
|
// ./en-US.ts
|
||||||
export default { ... } satisfies PageTitleLocalizations;
|
export default { ... } satisfies PageTitleLocalizations;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,13 +52,13 @@
|
|||||||
使用上一步中自动生成的 `name` 作为键,在此目录的每个语言文件中添加翻译。
|
使用上一步中自动生成的 `name` 作为键,在此目录的每个语言文件中添加翻译。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// src/locales-4-route/zh-CN.ts
|
// ./zh-CN.ts
|
||||||
export default {
|
export default {
|
||||||
// ... 其他翻译
|
// ... 其他翻译
|
||||||
DemosApiDemo: 'API 演示',
|
DemosApiDemo: 'API 演示',
|
||||||
} satisfies PageTitleLocalizations;
|
} satisfies PageTitleLocalizations;
|
||||||
|
|
||||||
// src/locales-4-route/en-US.ts
|
// ./en-US.ts
|
||||||
export default {
|
export default {
|
||||||
// ... 其他翻译
|
// ... 其他翻译
|
||||||
DemosApiDemo: 'API Demo',
|
DemosApiDemo: 'API Demo',
|
||||||
25
src/locales-utils/route-messages/en-US.ts
Normal file
25
src/locales-utils/route-messages/en-US.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*eslint perfectionist/sort-objects: "error"*/
|
||||||
|
/**
|
||||||
|
* 启用 perfectionist/sort-objects 规则以强制对象键按字母顺序排序
|
||||||
|
* 原因:
|
||||||
|
* 1. 减少多人协作时的合并冲突
|
||||||
|
* 2. 保持代码一致性,提高可维护性
|
||||||
|
*
|
||||||
|
* 运行以下命令自动修复排序:
|
||||||
|
* pnpm exec eslint --fix --no-ignore src/locales-utils/route-messages/
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { I18nOptions } from 'vue-i18n';
|
import type { I18nOptions } from 'vue-i18n';
|
||||||
|
|
||||||
const modules = import.meta.glob(['./*.ts', '!./_messages-auto-imports.ts'], {
|
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
|
||||||
eager: true,
|
eager: true /* true 为同步,false 为异步 */,
|
||||||
import: 'default',
|
import: 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
type MessageType = Record<string, string>;
|
type MessageType = Record<string, string>;
|
||||||
|
|
||||||
export const locales4RouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
|
export const i18nRouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
|
||||||
(messages, [path, mod]) => {
|
(messages, [path, mod]) => {
|
||||||
const locale = path.replace(/(\.\/|\.ts)/g, '');
|
const locale = path.replace(/(\.\/|\.ts)/g, '');
|
||||||
messages[locale] = mod as MessageType;
|
messages[locale] = mod as MessageType;
|
||||||
25
src/locales-utils/route-messages/zh-CN.ts
Normal file
25
src/locales-utils/route-messages/zh-CN.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*eslint perfectionist/sort-objects: "error"*/
|
||||||
|
/**
|
||||||
|
* 启用 perfectionist/sort-objects 规则以强制对象键按字母顺序排序
|
||||||
|
* 原因:
|
||||||
|
* 1. 减少多人协作时的合并冲突
|
||||||
|
* 2. 保持代码一致性,提高可维护性
|
||||||
|
*
|
||||||
|
* 运行以下命令自动修复排序:
|
||||||
|
* pnpm exec eslint --fix --no-ignore src/locales-utils/route-messages/
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
$Path: '$Path',
|
||||||
|
Demos: '示例演示',
|
||||||
|
DemosApiDemo: 'API 调用示例',
|
||||||
|
DemosCounterDemo: '点击计数器',
|
||||||
|
DemosCreate: '创建示例',
|
||||||
|
DemosI18nDemo: '国际化示例',
|
||||||
|
DemosNaiveUiDemo: 'Naive UI 组件示例',
|
||||||
|
DemosPrimevueDemo: 'PrimeVue 组件示例',
|
||||||
|
DemosWebsocketDemo: 'WebSocket 示例',
|
||||||
|
Home: '首页',
|
||||||
|
Login: '登录',
|
||||||
|
Root: '根 (Gēn)',
|
||||||
|
} satisfies PageTitleLocalizations;
|
||||||
10
src/locales/demo/en-US.json
Normal file
10
src/locales/demo/en-US.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"i18n-demo": {
|
||||||
|
"change-language": "Change Language",
|
||||||
|
"current-language": "Current Language",
|
||||||
|
"hello": "Hello, {name}!",
|
||||||
|
"title": "Vue I18n Demo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/locales/demo/zh-CN.json
Normal file
10
src/locales/demo/zh-CN.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"i18n-demo": {
|
||||||
|
"change-language": "切换语言",
|
||||||
|
"current-language": "当前语言",
|
||||||
|
"hello": "你好, {name}!",
|
||||||
|
"title": "Vue I18n 示例"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1 @@
|
|||||||
{
|
{}
|
||||||
"page": {
|
|
||||||
"i18n-demo": {
|
|
||||||
"title": "Vue I18n Demo",
|
|
||||||
"current-language": "Current Language",
|
|
||||||
"change-language": "Change Language",
|
|
||||||
"hello": "Hello, {name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
{
|
{}
|
||||||
"page": {
|
|
||||||
"i18n-demo": {
|
|
||||||
"title": "Vue I18n 示例",
|
|
||||||
"current-language": "当前语言",
|
|
||||||
"change-language": "切换语言",
|
|
||||||
"hello": "你好, {name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import './styles/index.ts';
|
import './styles/index.ts';
|
||||||
|
|
||||||
import { LogLevels } from 'consola';
|
import { LogLevels } from 'consola';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { setupPlugins } from './plugins';
|
import { setupPlugins } from './plugins';
|
||||||
|
|
||||||
consola.level = LogLevels.verbose;
|
consola.level = LogLevels.verbose;
|
||||||
|
|
||||||
/* `import.meta.glob(${g}, { eager: ${isSync} })`; */
|
const app = createApp(App);
|
||||||
const autoInstallModules = import.meta.glob('./plugins/!(index).ts', { eager: true });
|
if (__DEV__) Object.defineProperty(window, '__APP__', { value: 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
87
src/pages/Login.page.vue
Normal 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>
|
||||||
@@ -1,11 +1,29 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ path: string }>();
|
defineProps<{ path: string }>();
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
stack?: ReturnType<typeof createStackGuard>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = window?.stack;
|
||||||
|
const canGoBack = stack && stack.length > 1;
|
||||||
|
const router = useRouter();
|
||||||
|
function handleBack() {
|
||||||
|
if (canGoBack) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main flex-1 class="flex flex-col items-center justify-center h-full space-y-4">
|
<main flex-1 class="flex flex-col items-center justify-center h-full space-y-4">
|
||||||
<h1>Not Found</h1>
|
<h1>Not Found</h1>
|
||||||
<p>{{ path }} does not exist.</p>
|
<p>{{ path }} does not exist.</p>
|
||||||
<Button @click="$router.back()">Back</Button>
|
<Button @click="handleBack">{{ canGoBack ? 'Back' : 'Home' }}</Button>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ const callApi = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="callApi"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:aria-label="loading ? '正在调用API' : '调用API接口'"
|
:aria-label="loading ? '正在调用API' : '调用API接口'"
|
||||||
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-3 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-3 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="callApi"
|
||||||
>
|
>
|
||||||
<span v-if="loading" class="flex items-center justify-center">
|
<span v-if="loading" class="flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ const resetCount = () => {
|
|||||||
<div class="w-full flex flex-col gap-3">
|
<div class="w-full flex flex-col gap-3">
|
||||||
<!-- 原生按钮 (带 touch 事件) -->
|
<!-- 原生按钮 (带 touch 事件) -->
|
||||||
<button
|
<button
|
||||||
|
class="w-full bg-gradient-to-br from-orange-500 via-orange-600 to-red-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-orange-600 hover:via-orange-700 hover:to-red-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
||||||
@touchstart="() => {}"
|
@touchstart="() => {}"
|
||||||
@touchend="() => {}"
|
@touchend="() => {}"
|
||||||
@click="incrementCount"
|
@click="incrementCount"
|
||||||
class="w-full bg-gradient-to-br from-orange-500 via-orange-600 to-red-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-orange-600 hover:via-orange-700 hover:to-red-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
|
||||||
>
|
>
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -111,8 +111,8 @@ const resetCount = () => {
|
|||||||
|
|
||||||
<!-- 原生按钮 (无 touch 事件) -->
|
<!-- 原生按钮 (无 touch 事件) -->
|
||||||
<button
|
<button
|
||||||
@click="incrementCount"
|
|
||||||
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
class="w-full bg-gradient-to-br from-blue-500 via-blue-600 to-purple-600 text-white font-semibold py-4 px-6 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 transition-all duration-500 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-lg"
|
||||||
|
@click="incrementCount"
|
||||||
>
|
>
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -128,14 +128,14 @@ const resetCount = () => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Naive UI 按钮 -->
|
<!-- Naive UI 按钮 -->
|
||||||
<n-button
|
<NButton
|
||||||
@click="incrementCount"
|
|
||||||
type="warning"
|
type="warning"
|
||||||
size="large"
|
size="large"
|
||||||
block
|
block
|
||||||
strong
|
strong
|
||||||
secondary
|
secondary
|
||||||
class="text-lg"
|
class="text-lg"
|
||||||
|
@click="incrementCount"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -148,13 +148,13 @@ const resetCount = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
点击 +1 (Naive UI)
|
点击 +1 (Naive UI)
|
||||||
</n-button>
|
</NButton>
|
||||||
|
|
||||||
<!-- 重置按钮 -->
|
<!-- 重置按钮 -->
|
||||||
<button
|
<button
|
||||||
@click="resetCount"
|
|
||||||
:disabled="clickCount === 0"
|
:disabled="clickCount === 0"
|
||||||
class="w-full bg-gradient-to-br from-gray-500 via-gray-600 to-gray-700 text-white font-semibold py-3 px-6 rounded-xl hover:from-gray-600 hover:via-gray-700 hover:to-gray-800 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02]"
|
class="w-full bg-gradient-to-br from-gray-500 via-gray-600 to-gray-700 text-white font-semibold py-3 px-6 rounded-xl hover:from-gray-600 hover:via-gray-700 hover:to-gray-800 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02]"
|
||||||
|
@click="resetCount"
|
||||||
>
|
>
|
||||||
<span class="flex items-center justify-center">
|
<span class="flex items-center justify-center">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
12
src/pages/demos/create.page.vue
Normal file
12
src/pages/demos/create.page.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenuName: 'Demos',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePage({
|
definePage({ meta: { order: 1 } });
|
||||||
meta: {
|
const { t, locale } = useI18n({});
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { t, locale } = useI18n();
|
|
||||||
|
|
||||||
function setLocale(newLocale: 'en-US' | 'zh-CN') {
|
function setLocale(newLocale: 'en-US' | 'zh-CN') {
|
||||||
locale.value = newLocale;
|
i18nInstance.global.locale.value = newLocale;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<n-h1>{{ t('page.i18n-demo.title') }}</n-h1>
|
<NH1>{{ t('page.i18n-demo.title') }}</NH1>
|
||||||
|
|
||||||
<n-card :title="t('page.i18n-demo.change-language')">
|
<NCard :title="t('page.i18n-demo.change-language')">
|
||||||
<n-p>
|
<NP>
|
||||||
{{ t('page.i18n-demo.current-language') }}:
|
{{ t('page.i18n-demo.current-language') }}:
|
||||||
<span class="font-bold">{{ locale }}</span>
|
<span class="font-bold">{{ locale }}</span>
|
||||||
</n-p>
|
</NP>
|
||||||
|
|
||||||
<n-p>
|
<NP>
|
||||||
{{ t('page.i18n-demo.hello', { name: 'Kilo' }) }}
|
{{ t('page.i18n-demo.hello', { name: 'Kilo' }) }}
|
||||||
</n-p>
|
</NP>
|
||||||
|
|
||||||
<n-space>
|
<NSpace>
|
||||||
<n-button type="primary" @click="setLocale('en-US')"> English </n-button>
|
<NButton type="primary" @click="setLocale('en-US')"> English </NButton>
|
||||||
<n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button>
|
<NButton type="success" @click="setLocale('zh-CN')"> 简体中文 </NButton>
|
||||||
</n-space>
|
</NSpace>
|
||||||
</n-card>
|
</NCard>
|
||||||
|
|
||||||
|
<!-- 这里响应式有问题: -->
|
||||||
|
<NP> $route.meta.title: {{ $route.meta.title }} </NP>
|
||||||
|
<!-- 这样才正常 -->
|
||||||
|
<NP>
|
||||||
|
routeI18nInstance.global.t($route.name): {{ routeI18nInstance.global.t($route.name) }}
|
||||||
|
</NP>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const FComponent: FunctionalComponent<{ prop: string }> = (props /* context */)
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NCard title="函数式组件(TSX)示例">
|
<NCard title="函数式组件(TSX)示例">
|
||||||
<FComponent prop="some-prop-value" />
|
<FComponent prop="some-prop-value" />
|
||||||
|
|||||||
96
src/pages/demos/naive-ui-demo/index.page.vue
Normal file
96
src/pages/demos/naive-ui-demo/index.page.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { MessageType } from 'naive-ui';
|
||||||
|
import { useDialog, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
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>
|
||||||
|
<NCard title="Message 消息" class="mt-4">
|
||||||
|
<NSpace>
|
||||||
|
<NButton
|
||||||
|
v-for="(type, index) in messageTypes"
|
||||||
|
:key="type"
|
||||||
|
@click="
|
||||||
|
message[type](`${index + 1}. 消息内容`, {
|
||||||
|
duration: 3000,
|
||||||
|
closable: true,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ `${index + 1}. ${type}` }}
|
||||||
|
</NButton>
|
||||||
|
<NButton @click="openAllMessages"> 一键打开所有 </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<NCard title="Dialog 弹窗 (命令式 API)" class="mt-4">
|
||||||
|
<NSpace>
|
||||||
|
<NButton v-for="type in dialogTypes" :key="type" @click="openDialog(type)">
|
||||||
|
{{ type }}
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<NCard title="Modal 弹窗 (命令式 API)" class="mt-4">
|
||||||
|
<NSpace>
|
||||||
|
<NButton @click="openModal"> 打开 Modal </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
61
src/pages/demos/primevue-demo.page.vue
Normal file
61
src/pages/demos/primevue-demo.page.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ToastMessageOptions } from 'primevue/toast';
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tostSeverities = [
|
||||||
|
'secondary',
|
||||||
|
'success',
|
||||||
|
'info' /* 默认 */,
|
||||||
|
'warn',
|
||||||
|
'error',
|
||||||
|
'contrast',
|
||||||
|
undefined,
|
||||||
|
] satisfies ToastMessageOptions['severity'][];
|
||||||
|
|
||||||
|
const openAllToasts = () => {
|
||||||
|
tostSeverities.forEach((severity, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
ToastService.add({
|
||||||
|
severity,
|
||||||
|
summary: `severity: ${severity ?? 'default'}`,
|
||||||
|
life: 3000,
|
||||||
|
detail: `${index + 1}. 消息内容`,
|
||||||
|
});
|
||||||
|
}, index * 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="prime-vue-demo-page">
|
||||||
|
<Card>
|
||||||
|
<template #title>PrimeVue 组件演示</template>
|
||||||
|
<template #content>
|
||||||
|
<Message severity="info">演示 PrimeVue 各种组件的使用方法和功能特性</Message>
|
||||||
|
|
||||||
|
<Panel header="Toast 消息" class="mt-1.5">
|
||||||
|
<div flex="~ wrap" gap="4">
|
||||||
|
<Button
|
||||||
|
v-for="(severity, index) in tostSeverities"
|
||||||
|
:key="severity ?? 'default'"
|
||||||
|
@click="
|
||||||
|
ToastService.add({
|
||||||
|
severity: severity,
|
||||||
|
summary: `severity: ${severity ?? 'default'}`,
|
||||||
|
life: 3000,
|
||||||
|
detail: '消息内容',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ `${index + 1}. ${severity ?? 'default'}` }}
|
||||||
|
</Button>
|
||||||
|
<Button @click="openAllToasts"> 一键打开所有 </Button>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -227,12 +227,12 @@ onUnmounted(() => {
|
|||||||
<!-- 控制按钮 -->
|
<!-- 控制按钮 -->
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="connectWebSocket"
|
|
||||||
:disabled="wsConnected || wsLoading"
|
:disabled="wsConnected || wsLoading"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
wsLoading ? '正在连接WebSocket' : wsConnected ? 'WebSocket已连接' : '连接WebSocket'
|
wsLoading ? '正在连接WebSocket' : wsConnected ? 'WebSocket已连接' : '连接WebSocket'
|
||||||
"
|
"
|
||||||
class="flex items-center justify-center bg-gradient-to-br from-green-500 via-green-600 to-emerald-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-green-600 hover:via-green-700 hover:to-emerald-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center justify-center bg-gradient-to-br from-green-500 via-green-600 to-emerald-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-green-600 hover:via-green-700 hover:to-emerald-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="connectWebSocket"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="wsLoading"
|
v-if="wsLoading"
|
||||||
@@ -259,10 +259,10 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="disconnectWebSocket"
|
|
||||||
:disabled="!wsConnected || wsLoading"
|
:disabled="!wsConnected || wsLoading"
|
||||||
:aria-label="!wsConnected ? 'WebSocket未连接' : '断开WebSocket连接'"
|
:aria-label="!wsConnected ? 'WebSocket未连接' : '断开WebSocket连接'"
|
||||||
class="flex items-center justify-center bg-gradient-to-br from-red-500 via-red-600 to-pink-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-red-600 hover:via-red-700 hover:to-pink-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center justify-center bg-gradient-to-br from-red-500 via-red-600 to-pink-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-red-600 hover:via-red-700 hover:to-pink-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="disconnectWebSocket"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -284,19 +284,19 @@ onUnmounted(() => {
|
|||||||
<input
|
<input
|
||||||
id="messageInput"
|
id="messageInput"
|
||||||
v-model="messageInput"
|
v-model="messageInput"
|
||||||
@keyup.enter="sendMessage"
|
|
||||||
placeholder="输入要发送的消息..."
|
placeholder="输入要发送的消息..."
|
||||||
:disabled="!wsConnected"
|
:disabled="!wsConnected"
|
||||||
:aria-describedby="!wsConnected ? 'ws-status' : undefined"
|
:aria-describedby="!wsConnected ? 'ws-status' : undefined"
|
||||||
class="flex-1 w-full border-2 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:text-gray-500 transition-all duration-300 border-gray-200 dark:border-gray-600 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-100 disabled:bg-gray-100/60 dark:disabled:bg-gray-800/60 hover:border-gray-300 dark:hover:border-gray-500"
|
class="flex-1 w-full border-2 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:text-gray-500 transition-all duration-300 border-gray-200 dark:border-gray-600 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-100 disabled:bg-gray-100/60 dark:disabled:bg-gray-800/60 hover:border-gray-300 dark:hover:border-gray-500"
|
||||||
|
@keyup.enter="sendMessage"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="sendMessage"
|
|
||||||
:disabled="!canSendMessage"
|
:disabled="!canSendMessage"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
!wsConnected ? 'WebSocket未连接' : canSendMessage ? '发送消息' : '请输入消息内容'
|
!wsConnected ? 'WebSocket未连接' : canSendMessage ? '发送消息' : '请输入消息内容'
|
||||||
"
|
"
|
||||||
class="flex items-center bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-indigo-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-blue-600 hover:via-blue-700 hover:to-indigo-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
|
@click="sendMessage"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -309,11 +309,11 @@ onUnmounted(() => {
|
|||||||
发送
|
发送
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="sendMockData"
|
|
||||||
:disabled="!wsConnected"
|
:disabled="!wsConnected"
|
||||||
:aria-label="!wsConnected ? 'WebSocket未连接' : '发送随机模拟数据'"
|
:aria-label="!wsConnected ? 'WebSocket未连接' : '发送随机模拟数据'"
|
||||||
class="flex items-center bg-gradient-to-br from-purple-500 via-purple-600 to-violet-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-purple-600 hover:via-purple-700 hover:to-violet-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
class="flex items-center bg-gradient-to-br from-purple-500 via-purple-600 to-violet-600 text-white font-semibold py-2.5 px-4 rounded-xl hover:from-purple-600 hover:via-purple-700 hover:to-violet-700 transition-all duration-500 disabled:opacity-50 shadow-lg hover:shadow-2xl transform hover:-translate-y-1 hover:scale-[1.02] text-sm"
|
||||||
:title="!wsConnected ? 'WebSocket未连接时不可用' : '发送随机模拟数据'"
|
:title="!wsConnected ? 'WebSocket未连接时不可用' : '发送随机模拟数据'"
|
||||||
|
@click="sendMockData"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -354,9 +354,9 @@ onUnmounted(() => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<button
|
||||||
@click="exportMessages"
|
|
||||||
class="text-xs text-gray-500 hover:text-blue-500 transition-colors duration-200 flex items-center"
|
class="text-xs text-gray-500 hover:text-blue-500 transition-colors duration-200 flex items-center"
|
||||||
title="导出消息"
|
title="导出消息"
|
||||||
|
@click="exportMessages"
|
||||||
>
|
>
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -369,9 +369,9 @@ onUnmounted(() => {
|
|||||||
导出
|
导出
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="clearMessages"
|
|
||||||
class="text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 flex items-center"
|
class="text-xs text-gray-500 hover:text-red-500 transition-colors duration-200 flex items-center"
|
||||||
title="清空消息"
|
title="清空消息"
|
||||||
|
@click="clearMessages"
|
||||||
>
|
>
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Index Page</h1>
|
<NButton @click="$router.push({ name: 'DemosCreate' })">DemosCreate</NButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
app.config.globalProperties.__DEV__ = __DEV__;
|
app.config.globalProperties.__DEV__ =
|
||||||
|
__DEV__; /* vite.config.ts: define: { __DEV__: JSON.stringify(!isBuild) } */
|
||||||
|
|
||||||
app.config.errorHandler = (error, instance, info) => {
|
app.config.errorHandler = (error, instance, info) => {
|
||||||
console.error('Global error:', error);
|
console.error('Global error:', error);
|
||||||
@@ -10,17 +11,4 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
|||||||
// 2. 显示全局错误提示
|
// 2. 显示全局错误提示
|
||||||
// 3. 进行错误分析和处理
|
// 3. 进行错误分析和处理
|
||||||
};
|
};
|
||||||
|
|
||||||
// if (import.meta.env.MODE === 'development' && '1' === ('2' as never)) {
|
|
||||||
// // TODO: https://github.com/hu3dao/vite-plugin-debug/
|
|
||||||
// // https://eruda.liriliri.io/zh/docs/#快速上手
|
|
||||||
// import('eruda').then(({ default: eruda }) => {
|
|
||||||
// eruda.init({
|
|
||||||
// defaults: {
|
|
||||||
// transparency: 0.9,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// /* eruda.show(); */
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
|
|||||||
import { setupLayouts } from 'virtual:meta-layouts';
|
import { setupLayouts } from 'virtual:meta-layouts';
|
||||||
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
|
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import type { RouteNamedMap } from 'vue-router/auto-routes';
|
import type { Router } from 'vue-router';
|
||||||
import { routes, handleHotUpdate } from 'vue-router/auto-routes';
|
import { handleHotUpdate, routes } from 'vue-router/auto-routes';
|
||||||
|
|
||||||
const setupLayoutsResult = setupLayouts(routes);
|
const setupLayoutsResult = setupLayouts(routes);
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -15,6 +15,10 @@ const router = createRouter({
|
|||||||
strict: true,
|
strict: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.isReady().then(() => {
|
||||||
|
console.debug('✅ [router is ready]');
|
||||||
|
});
|
||||||
|
|
||||||
router.onError((error) => {
|
router.onError((error) => {
|
||||||
console.debug('🚨 [router error]:', error);
|
console.debug('🚨 [router error]:', error);
|
||||||
});
|
});
|
||||||
@@ -32,51 +36,25 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
|||||||
{
|
{
|
||||||
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
||||||
createNProgressGuard(router);
|
createNProgressGuard(router);
|
||||||
createLogGuard(router);
|
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
|
||||||
Object.assign(globalThis, { stack: createStackGuard(router) });
|
Object.assign(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' {
|
if (__DEV__) Object.assign(window, { router });
|
||||||
/* definePage({ meta: { title: '示例演示' } }); */
|
|
||||||
interface RouteMeta {
|
|
||||||
/**
|
|
||||||
* @description 是否在菜单中隐藏
|
|
||||||
*/
|
|
||||||
hideInMenu?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 菜单标题
|
|
||||||
* @deprecated //!⚠️请通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
|
|
||||||
*/
|
|
||||||
title?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 使用的布局,设置为 false 则表示不使用布局
|
|
||||||
*/
|
|
||||||
layout?: string | false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 菜单项是否渲染为可点击链接,默认为 true
|
|
||||||
* - true: 使用 RouterLink 包装,可点击跳转
|
|
||||||
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
|
|
||||||
*/
|
|
||||||
link?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
|
|
||||||
*/
|
|
||||||
order?: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { router, setupLayoutsResult };
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__DEV__) Object.assign(globalThis, { router });
|
|
||||||
// This will update routes at runtime without reloading the page
|
// This will update routes at runtime without reloading the page
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
handleHotUpdate(router);
|
handleHotUpdate(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { router, setupLayoutsResult };
|
||||||
|
|||||||
50
src/plugins/00.router-plugin.types.ts
Normal file
50
src/plugins/00.router-plugin.types.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,3 @@
|
|||||||
/* https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n#static-bundle-importing
|
|
||||||
* All i18n resources specified in the plugin `include` option can be loaded
|
|
||||||
* at once using the import syntax
|
|
||||||
*/
|
|
||||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
|
app.use(i18nInstance);
|
||||||
const i18n = createI18n({
|
|
||||||
legacy: false, // you must set `false`, to use Composition API
|
|
||||||
locale: navigator.language,
|
|
||||||
fallbackRoot: false,
|
|
||||||
// flatJson: true,
|
|
||||||
missing: (locale, key /* , instance, type */) => {
|
|
||||||
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
|
|
||||||
return `[${key}]`;
|
|
||||||
},
|
|
||||||
missingWarn: !true,
|
|
||||||
fallbackWarn: !true,
|
|
||||||
messages,
|
|
||||||
});
|
|
||||||
app.use(i18n);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,21 @@
|
|||||||
import Aura from '@primeuix/themes/aura';
|
import Aura from '@primeuix/themes/aura';
|
||||||
import zhCN from 'primelocale/zh-CN.json';
|
import zhCN from 'primelocale/zh-CN.json';
|
||||||
import PrimeVue from 'primevue/config';
|
import PrimeVue from 'primevue/config';
|
||||||
|
import type { PrimeVueConfiguration } from 'primevue/config';
|
||||||
import StyleClass from 'primevue/styleclass';
|
import StyleClass from 'primevue/styleclass';
|
||||||
|
import ToastService from 'primevue/toastservice';
|
||||||
|
|
||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
app.directive('styleclass', StyleClass);
|
app.directive('styleclass', StyleClass);
|
||||||
|
|
||||||
|
// https://github.com/primefaces/primevue/blob/afe6f58ae55e9caf7f9bc094cd453a21a6113001/packages/core/src/config/PrimeVue.js
|
||||||
app.use(PrimeVue, {
|
app.use(PrimeVue, {
|
||||||
|
zIndex: {
|
||||||
|
modal: 5100,
|
||||||
|
overlay: 5000,
|
||||||
|
menu: 5000,
|
||||||
|
tooltip: 5100,
|
||||||
|
},
|
||||||
locale: {
|
locale: {
|
||||||
...zhCN['zh-CN'],
|
...zhCN['zh-CN'],
|
||||||
completed: '已上传',
|
completed: '已上传',
|
||||||
@@ -24,5 +34,6 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
|||||||
},
|
},
|
||||||
preset: Aura,
|
preset: Aura,
|
||||||
},
|
},
|
||||||
});
|
} satisfies PrimeVueConfiguration);
|
||||||
|
app.use(ToastService);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,18 @@
|
|||||||
type UserPlugin = (ctx: UserPluginContext) => void;
|
type UserPlugin = (ctx: UserPluginContext) => void;
|
||||||
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
|
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
|
||||||
type UserPluginContext = { app: import('vue').App<Element> };
|
type UserPluginContext = { app: import('vue').App<Element> };
|
||||||
export function setupPlugins(
|
|
||||||
app: import('vue').App,
|
const autoInstallModules: AutoInstallModule = import.meta.glob(
|
||||||
modules: AutoInstallModule | Record<string, unknown>,
|
['./*.ts', '!./**/*.types.ts', '!./index.ts'],
|
||||||
) {
|
{
|
||||||
console.group('🔌 Plugins');
|
eager: true /* true 为同步,false 为异步 */,
|
||||||
for (const path in modules) {
|
},
|
||||||
const module = modules[path] as AutoInstallModule;
|
);
|
||||||
|
|
||||||
|
export function setupPlugins(app: import('vue').App) {
|
||||||
|
console.group(`🔌 Installing ${Object.keys(autoInstallModules).length} plugins`);
|
||||||
|
for (const path in autoInstallModules) {
|
||||||
|
const module = autoInstallModules[path] as AutoInstallModule;
|
||||||
if (module.install) {
|
if (module.install) {
|
||||||
module.install({ app });
|
module.install({ app });
|
||||||
console.debug(`%c✔ ${path}`, 'color: #07a');
|
console.debug(`%c✔ ${path}`, 'color: #07a');
|
||||||
|
|||||||
28
src/plugins/router-guard/router-permission-guard.ts
Normal file
28
src/plugins/router-guard/router-permission-guard.ts
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
46
src/stores/app-store-auto-imports.ts
Normal file
46
src/stores/app-store-auto-imports.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { BasicColorSchema } from '@vueuse/core';
|
||||||
|
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<BasicColorSchema>({
|
||||||
|
selector: 'html',
|
||||||
|
attribute: 'class',
|
||||||
|
modes: { light: '', dark: 'app-dark', auto: '' },
|
||||||
|
disableTransition: false,
|
||||||
|
initialValue: 'auto',
|
||||||
|
});
|
||||||
|
const { state, next: cycleTheme } = useCycleList(['light', 'dark', 'auto'] as const, {
|
||||||
|
initialValue: themeMode,
|
||||||
|
});
|
||||||
|
watchEffect(() => (themeMode.value = state.value));
|
||||||
|
export type AppThemeMode = typeof themeMode.value;
|
||||||
|
// <<<<<
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', () => {
|
||||||
|
// 侧边栏展开/收起状态
|
||||||
|
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
|
||||||
|
const toggleSidebar = useToggle(sidebarCollapsed);
|
||||||
|
|
||||||
|
// 主题模式
|
||||||
|
const actualTheme = computed(() => (themeMode.value === 'auto' ? system.value : themeMode.value));
|
||||||
|
const isDark = computed(() => actualTheme.value === 'dark');
|
||||||
|
|
||||||
|
// 是否是移动端
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeMode,
|
||||||
|
isDark,
|
||||||
|
isMobile,
|
||||||
|
cycleTheme,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { useLocalStorage, useMediaQuery, usePreferredColorScheme } from '@vueuse/core';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { computed, watch } from 'vue';
|
|
||||||
|
|
||||||
export const APP_THEME_MODES = ['light', 'dark', 'system'] as const;
|
|
||||||
export type AppThemeMode = (typeof APP_THEME_MODES)[number];
|
|
||||||
|
|
||||||
const DARK_CLASS = 'app-dark';
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', () => {
|
|
||||||
const themeMode = useLocalStorage<AppThemeMode>('app-theme-mode', 'system');
|
|
||||||
const preferredColor = usePreferredColorScheme();
|
|
||||||
|
|
||||||
// 侧边栏展开/收起状态
|
|
||||||
const sidebarCollapsed = useLocalStorage<boolean>('app-sidebar-collapsed', false);
|
|
||||||
|
|
||||||
// 计算实际使用的主题
|
|
||||||
const actualTheme = computed(() =>
|
|
||||||
themeMode.value === 'system'
|
|
||||||
? preferredColor.value === 'dark'
|
|
||||||
? 'dark'
|
|
||||||
: 'light'
|
|
||||||
: themeMode.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 是否是暗色主题
|
|
||||||
const isDark = computed(() => actualTheme.value === 'dark');
|
|
||||||
|
|
||||||
// 是否是移动端
|
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
||||||
|
|
||||||
// 更新 DOM 类名
|
|
||||||
function updateDomClass() {
|
|
||||||
document.documentElement.classList.toggle(DARK_CLASS, isDark.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 循环切换主题
|
|
||||||
function cycleTheme() {
|
|
||||||
const currentIndex = APP_THEME_MODES.indexOf(themeMode.value);
|
|
||||||
const nextIndex = (currentIndex + 1) % APP_THEME_MODES.length;
|
|
||||||
themeMode.value = APP_THEME_MODES[nextIndex]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换侧边栏展开/收起
|
|
||||||
function toggleSidebar() {
|
|
||||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听主题变化,更新 DOM
|
|
||||||
watch(isDark, updateDomClass, { immediate: true });
|
|
||||||
|
|
||||||
return {
|
|
||||||
themeMode,
|
|
||||||
isDark,
|
|
||||||
isMobile,
|
|
||||||
cycleTheme,
|
|
||||||
sidebarCollapsed,
|
|
||||||
toggleSidebar,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
|
|
||||||
}
|
|
||||||
45
src/stores/auth-store-auto-imports.ts
Normal file
45
src/stores/auth-store-auto-imports.ts
Normal 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 };
|
||||||
|
});
|
||||||
8
src/styles/css/reset-primevue.css
Normal file
8
src/styles/css/reset-primevue.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.p-confirmdialog,
|
||||||
|
.p-toast {
|
||||||
|
max-width: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-toast .p-toast-message-text {
|
||||||
|
margin-top: -0.2rem;
|
||||||
|
}
|
||||||
9
src/styles/css/transition.css
Normal file
9
src/styles/css/transition.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
||||||
|
|
||||||
//
|
import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
|
import './css/reset-primevue.css';
|
||||||
|
import './css/transition.css';
|
||||||
|
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
|
|||||||
@@ -41,5 +41,6 @@ export default defineConfig({
|
|||||||
// SCSS 专用的 at-rule 规则会自动处理 @include, @mixin 等
|
// SCSS 专用的 at-rule 规则会自动处理 @include, @mixin 等
|
||||||
// 'scss/at-rule-no-unknown': true,
|
// 'scss/at-rule-no-unknown': true,
|
||||||
// <<<<<
|
// <<<<<
|
||||||
|
'selector-class-pattern': null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,15 @@
|
|||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"~/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"vueCompilerOptions": {
|
||||||
|
"plugins": [
|
||||||
|
"vue-macros/volar",
|
||||||
|
"unplugin-vue-router/volar/sfc-route-blocks",
|
||||||
|
"unplugin-vue-router/volar/sfc-typed-router"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/node22/tsconfig.json",
|
"extends": "@tsconfig/node24/tsconfig.json",
|
||||||
"include": [
|
"include": [
|
||||||
"vite.config.*",
|
"vite.config.*",
|
||||||
"vitest.config.*",
|
"vitest.config.*",
|
||||||
|
|||||||
54
typed-router.d.ts
vendored
54
typed-router.d.ts
vendored
@@ -2,7 +2,7 @@
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// noinspection ES6UnusedImports
|
// noinspection ES6UnusedImports
|
||||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !!
|
||||||
// It's recommended to commit this file.
|
// It's recommended to commit this file.
|
||||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||||
|
|
||||||
@@ -58,6 +58,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'DemosCreate': RouteRecordInfo<
|
||||||
|
'DemosCreate',
|
||||||
|
'/demos/create',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'DemosI18nDemo': RouteRecordInfo<
|
'DemosI18nDemo': RouteRecordInfo<
|
||||||
'DemosI18nDemo',
|
'DemosI18nDemo',
|
||||||
'/demos/i18n-demo',
|
'/demos/i18n-demo',
|
||||||
@@ -65,6 +72,20 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'DemosNaiveUiDemo': RouteRecordInfo<
|
||||||
|
'DemosNaiveUiDemo',
|
||||||
|
'/demos/naive-ui-demo',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'DemosPrimevueDemo': RouteRecordInfo<
|
||||||
|
'DemosPrimevueDemo',
|
||||||
|
'/demos/primevue-demo',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'DemosWebsocketDemo': RouteRecordInfo<
|
'DemosWebsocketDemo': RouteRecordInfo<
|
||||||
'DemosWebsocketDemo',
|
'DemosWebsocketDemo',
|
||||||
'/demos/websocket-demo',
|
'/demos/websocket-demo',
|
||||||
@@ -79,6 +100,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'Login': RouteRecordInfo<
|
||||||
|
'Login',
|
||||||
|
'/Login',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,12 +150,30 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/demos/create.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'DemosCreate'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/demos/i18n-demo.page.vue': {
|
'src/pages/demos/i18n-demo.page.vue': {
|
||||||
routes:
|
routes:
|
||||||
| 'DemosI18nDemo'
|
| 'DemosI18nDemo'
|
||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/demos/naive-ui-demo/index.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'DemosNaiveUiDemo'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
|
'src/pages/demos/primevue-demo.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'DemosPrimevueDemo'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/demos/websocket-demo.page.vue': {
|
'src/pages/demos/websocket-demo.page.vue': {
|
||||||
routes:
|
routes:
|
||||||
| 'DemosWebsocketDemo'
|
| 'DemosWebsocketDemo'
|
||||||
@@ -140,6 +186,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/Login.page.vue': {
|
||||||
|
routes:
|
||||||
|
| 'Login'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import { getPascalCaseRouteName } from 'unplugin-vue-router';
|
|
||||||
import vueRouter from 'unplugin-vue-router/vite';
|
import vueRouter from 'unplugin-vue-router/vite';
|
||||||
import type { PluginOption } from 'vite';
|
|
||||||
import VueMacros from 'vue-macros/vite';
|
import VueMacros from 'vue-macros/vite';
|
||||||
|
|
||||||
export default [
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
|
export const loadPlugin: LoadPluginFunction = async (_pluginLoadOptions) => {
|
||||||
|
return [
|
||||||
VueMacros({
|
VueMacros({
|
||||||
plugins: {
|
plugins: {
|
||||||
vue: vue({ include: [/\.vue$/, /\.md$/] }),
|
vue: vue({ include: [/\.vue$/, /\.md$/] }),
|
||||||
@@ -15,12 +16,18 @@ export default [
|
|||||||
// https://github.com/posva/unplugin-vue-router
|
// https://github.com/posva/unplugin-vue-router
|
||||||
// ⚠️ Vue must be placed after VueRouter()
|
// ⚠️ Vue must be placed after VueRouter()
|
||||||
vueRouter: vueRouter({
|
vueRouter: vueRouter({
|
||||||
exclude: ['**/__*', '**/__*/**/*'],
|
|
||||||
extensions: ['.page.vue', '.page.md'],
|
|
||||||
getRouteName: (routeNode) => getPascalCaseRouteName(routeNode),
|
|
||||||
logs: false,
|
|
||||||
routesFolder: 'src/pages',
|
routesFolder: 'src/pages',
|
||||||
|
extensions: ['.page.vue', '.page.md'],
|
||||||
|
exclude: ['**/__*', '**/__*/**/*'],
|
||||||
|
getRouteName: (await import('unplugin-vue-router')).getPascalCaseRouteName,
|
||||||
|
beforeWriteFiles(rootRoute) {
|
||||||
|
for (/* 深度优先遍历 */ const route of rootRoute.traverseDFS()) {
|
||||||
|
route.addToMeta({ _: route.fullPath });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logs: !true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
] satisfies PluginOption;
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
import UnoCSS from 'unocss/vite';
|
import UnoCSS from 'unocss/vite';
|
||||||
|
|
||||||
export default [
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
|
return [
|
||||||
// https://github.com/antfu/unocss
|
// https://github.com/antfu/unocss
|
||||||
// see uno.config.ts for config
|
// see uno.config.ts for config
|
||||||
UnoCSS(),
|
UnoCSS({
|
||||||
] satisfies PluginOption;
|
checkImport: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
|
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
|
||||||
import type { PluginOption } from 'vite';
|
|
||||||
|
|
||||||
export default [
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
|
return [
|
||||||
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
|
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
|
||||||
VueI18nPlugin({
|
VueI18nPlugin({
|
||||||
/* options */
|
/* options */
|
||||||
@@ -15,4 +17,5 @@ export default [
|
|||||||
// return src as string;
|
// return src as string;
|
||||||
// },
|
// },
|
||||||
}),
|
}),
|
||||||
] satisfies PluginOption;
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
import Markdown from 'unplugin-vue-markdown/vite';
|
import Markdown from 'unplugin-vue-markdown/vite';
|
||||||
|
|
||||||
export default [
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
|
return [
|
||||||
// https://github.com/unplugin/unplugin-vue-markdown
|
// https://github.com/unplugin/unplugin-vue-markdown
|
||||||
Markdown({
|
Markdown({
|
||||||
headEnabled: true,
|
headEnabled: true,
|
||||||
}),
|
}),
|
||||||
] satisfies PluginOption;
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
import MetaLayouts from 'vite-plugin-vue-meta-layouts';
|
import MetaLayouts from 'vite-plugin-vue-meta-layouts';
|
||||||
|
|
||||||
export default [
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
|
return [
|
||||||
// https://github.com/dishait/vite-plugin-vue-meta-layouts
|
// https://github.com/dishait/vite-plugin-vue-meta-layouts
|
||||||
MetaLayouts({
|
MetaLayouts({
|
||||||
target: 'src/layouts',
|
target: 'src/layouts',
|
||||||
@@ -14,4 +16,5 @@ export default [
|
|||||||
// importMode: 'sync', // 默认为自动处理,SSG 时为 sync,非 SSG 时为 async
|
// importMode: 'sync', // 默认为自动处理,SSG 时为 sync,非 SSG 时为 async
|
||||||
skipTopLevelRouteLayout: true, // 打开修复 https://github.com/JohnCampionJr/vite-plugin-vue-layouts/issues/134,默认为 false 关闭
|
skipTopLevelRouteLayout: true, // 打开修复 https://github.com/JohnCampionJr/vite-plugin-vue-layouts/issues/134,默认为 false 关闭
|
||||||
}),
|
}),
|
||||||
] satisfies PluginOption;
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import AutoImport from 'unplugin-auto-import/vite';
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver';
|
||||||
import Icons from 'unplugin-icons/vite';
|
import Icons from 'unplugin-icons/vite';
|
||||||
import Components from 'unplugin-vue-components/vite';
|
import Components from 'unplugin-vue-components/vite';
|
||||||
import { VueRouterAutoImports } from 'unplugin-vue-router';
|
import { VueRouterAutoImports } from 'unplugin-vue-router';
|
||||||
import { createUtils4uAutoImports } from 'utils4u/auto-imports';
|
import { createUtils4uAutoImports } from 'utils4u/auto-imports';
|
||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
|
||||||
|
|
||||||
// >>>>>
|
// >>>>>
|
||||||
// eslint-disable-next-line import/no-duplicates
|
// eslint-disable-next-line import/no-duplicates
|
||||||
@@ -22,12 +22,14 @@ import { TDesignResolver } from 'unplugin-vue-components/resolvers';
|
|||||||
|
|
||||||
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
|
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
|
||||||
|
|
||||||
import IconsResolver from 'unplugin-icons/resolver';
|
|
||||||
|
|
||||||
import { VantResolver } from '@vant/auto-import-resolver';
|
import { VantResolver } from '@vant/auto-import-resolver';
|
||||||
// <<<<<
|
// <<<<<
|
||||||
|
|
||||||
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
function _getNaiveUiComponentNames() {
|
function _getNaiveUiComponentNames() {
|
||||||
|
// [dtsTsx](https://github.com/unplugin/unplugin-vue-components/pull/673/files/84e80738885cfe11298f41f070cda94a7a779276)
|
||||||
|
|
||||||
// 方法1: 从 web-types.json 读取(推荐)
|
// 方法1: 从 web-types.json 读取(推荐)
|
||||||
const webTypesPath = path.resolve('node_modules/naive-ui/web-types.json');
|
const webTypesPath = path.resolve('node_modules/naive-ui/web-types.json');
|
||||||
if (fs.existsSync(webTypesPath)) {
|
if (fs.existsSync(webTypesPath)) {
|
||||||
@@ -54,7 +56,7 @@ function _getNaiveUiComponentNames() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
return [
|
return [
|
||||||
// https://github.com/antfu/unplugin-auto-import
|
// https://github.com/antfu/unplugin-auto-import
|
||||||
AutoImport({
|
AutoImport({
|
||||||
@@ -62,7 +64,7 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
dirs: [
|
dirs: [
|
||||||
// 'src/utils',
|
// 'src/utils',
|
||||||
'src/composables',
|
'src/composables',
|
||||||
'src/stores',
|
// 'src/stores',
|
||||||
// 匹配所有 -auto-imports.ts / -auto-imports.tsx 结尾的文件
|
// 匹配所有 -auto-imports.ts / -auto-imports.tsx 结尾的文件
|
||||||
'src/**/*-auto-imports.{ts,tsx}',
|
'src/**/*-auto-imports.{ts,tsx}',
|
||||||
],
|
],
|
||||||
@@ -72,17 +74,17 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
'pinia',
|
'pinia',
|
||||||
'@vueuse/core',
|
'@vueuse/core',
|
||||||
VueRouterAutoImports,
|
VueRouterAutoImports,
|
||||||
createUtils4uAutoImports([]),
|
createUtils4uAutoImports(['primevue']),
|
||||||
{
|
{
|
||||||
'consola/browser': ['consola'],
|
'consola/browser': ['consola'],
|
||||||
'vue-router/auto': ['useLink'],
|
'unplugin-vue-router/data-loaders/basic': ['defineBasicLoader'],
|
||||||
'naive-ui': [
|
'naive-ui': [
|
||||||
'useModal',
|
'useModal',
|
||||||
'useDialog',
|
'useDialog',
|
||||||
'useMessage',
|
'useMessage',
|
||||||
'useNotification',
|
'useNotification',
|
||||||
'useLoadingBar',
|
'useLoadingBar',
|
||||||
..._getNaiveUiComponentNames(),
|
// ..._getNaiveUiComponentNames(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -91,12 +93,25 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
// https://github.com/antfu/unplugin-vue-components
|
// https://github.com/antfu/unplugin-vue-components
|
||||||
Components({
|
Components({
|
||||||
syncMode: 'default',
|
syncMode: 'default',
|
||||||
|
dtsTsx: true,
|
||||||
|
|
||||||
// `__`开头的
|
// `__`开头的
|
||||||
excludeNames: [/^__/],
|
excludeNames: [/^__/],
|
||||||
// allow auto load markdown components under `./src/components/`
|
// allow auto load markdown components under `./src/components/`
|
||||||
extensions: ['vue', 'md'],
|
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: [
|
resolvers: [
|
||||||
AntDesignVueResolver({
|
AntDesignVueResolver({
|
||||||
importStyle: false, // css in js
|
importStyle: false, // css in js
|
||||||
@@ -127,4 +142,4 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
import { minify as minifyHtml } from 'html-minifier-terser';
|
import { minify as minifyHtml } from 'html-minifier-terser';
|
||||||
|
import type { PluginOption } from 'vite';
|
||||||
|
|
||||||
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
function IndexHtmlPlugin(): PluginOption {
|
function IndexHtmlPlugin(): PluginOption {
|
||||||
return {
|
return {
|
||||||
@@ -25,4 +27,79 @@ 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 const loadPlugin: LoadPluginFunction = (pluginLoadOptions) => {
|
||||||
|
const { mode, env } = pluginLoadOptions;
|
||||||
|
// return [___()];
|
||||||
|
if (mode !== 'production') {
|
||||||
|
return { plugins: [], message: '仅在生产模式下启用' };
|
||||||
|
}
|
||||||
|
if (env.VITE_BUILD_MINIFY !== 'true') {
|
||||||
|
return { plugins: [], message: `已通过环境变量禁用: VITE_BUILD_MINIFY=${env.VITE_BUILD_MINIFY}` };
|
||||||
|
}
|
||||||
|
return IndexHtmlPlugin();
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { consola } from 'consola';
|
|
||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
|
||||||
import { vitePluginFakeServer } from 'vite-plugin-fake-server';
|
import { vitePluginFakeServer } from 'vite-plugin-fake-server';
|
||||||
// https://github.com/condorheroblog/vite-plugin-fake-server?tab=readme-ov-file#usage
|
|
||||||
|
|
||||||
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
if (_configEnv.mode !== 'development') {
|
|
||||||
consola.info('fake server plugin is disabled in non-development mode.');
|
// https://github.com/condorheroblog/vite-plugin-fake-server?tab=readme-ov-file#usage
|
||||||
return [];
|
export const loadPlugin: LoadPluginFunction = (pluginLoadOptions) => {
|
||||||
|
const { mode } = pluginLoadOptions;
|
||||||
|
if (mode !== 'development') {
|
||||||
|
return { plugins: [], message: '仅在开发模式下启用' };
|
||||||
}
|
}
|
||||||
return vitePluginFakeServer({
|
return vitePluginFakeServer({
|
||||||
basename: 'fake-api',
|
basename: 'fake-api',
|
||||||
enableProd: true,
|
enableProd: true,
|
||||||
include: 'fake',
|
include: 'fake',
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||||
|
|
||||||
export default [
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
|
return [
|
||||||
// https://github.com/FatehAK/vite-plugin-image-optimizer?tab=readme-ov-file#default-configuration
|
// https://github.com/FatehAK/vite-plugin-image-optimizer?tab=readme-ov-file#default-configuration
|
||||||
ViteImageOptimizer({
|
ViteImageOptimizer({
|
||||||
/* pass your config */
|
/* pass your config */
|
||||||
}),
|
}),
|
||||||
] satisfies PluginOption;
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import consola from 'consola';
|
|
||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
|
||||||
import { loadEnv } from 'vite';
|
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools';
|
import vueDevTools from 'vite-plugin-vue-devtools';
|
||||||
|
|
||||||
export function loadPlugin(configEnv: ConfigEnv): PluginOption {
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
const env = loadEnv(configEnv.mode, process.cwd());
|
|
||||||
|
|
||||||
if (configEnv.command === 'build') {
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
consola.info('vue-devtools plugin is not used in build mode.');
|
const { mode } = _pluginLoadOptions;
|
||||||
return [];
|
if (mode !== 'development') {
|
||||||
|
return { plugins: [], message: '仅在开发模式下启用' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.VITE_ENABLE_VUE_DEVTOOLS !== 'true') {
|
let launchEditor = 'code';
|
||||||
consola.info('vue-devtools plugin disabled by env');
|
let message: string | undefined;
|
||||||
return [];
|
|
||||||
|
if (process.env.TERM_PROGRAM_VERSION?.toLowerCase()?.includes('insider')) {
|
||||||
|
launchEditor = 'code-insiders';
|
||||||
|
message = '检测到 VSCode Insiders 环境';
|
||||||
}
|
}
|
||||||
|
|
||||||
return [vueDevTools()];
|
return {
|
||||||
}
|
plugins: [
|
||||||
|
vueDevTools({
|
||||||
|
launchEditor: launchEditor,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { cloudflare } from '@cloudflare/vite-plugin';
|
import { cloudflare } from '@cloudflare/vite-plugin';
|
||||||
|
|
||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
export const loadPlugin: LoadPluginFunction = (pluginLoadOptions) => {
|
||||||
if (_configEnv.mode === 'test') {
|
const { mode, env } = pluginLoadOptions;
|
||||||
console.log('cloudflare plugin disabled in test mode');
|
if (mode === 'test') {
|
||||||
return [];
|
return { plugins: [], message: '在测试模式下禁用' };
|
||||||
}
|
}
|
||||||
if (process.env.VITE_CLOUDFLARE_SERVER_ENABLED !== 'true') {
|
if (env.VITE_CLOUDFLARE_SERVER_ENABLED !== 'true') {
|
||||||
console.log('cloudflare plugin disabled by env');
|
return {
|
||||||
return [];
|
plugins: [],
|
||||||
|
message: `已通过环境变量禁用: VITE_CLOUDFLARE_SERVER_ENABLED=${env.VITE_CLOUDFLARE_SERVER_ENABLED}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return [cloudflare()];
|
return [cloudflare()];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,28 +1,47 @@
|
|||||||
|
import boxen from 'boxen';
|
||||||
|
import consola from 'consola';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
import consola from 'consola';
|
|
||||||
import { glob } from 'tinyglobby';
|
import { glob } from 'tinyglobby';
|
||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
import type { ConfigEnv, PluginOption } from 'vite';
|
||||||
|
import { loadEnv } from 'vite';
|
||||||
|
|
||||||
|
export type LoadPluginFunction = (
|
||||||
|
configEnv: ConfigEnv & {
|
||||||
|
env: Record<string, string>;
|
||||||
|
},
|
||||||
|
) => PluginOption | LoadPluginResult;
|
||||||
|
export interface LoadPluginResult {
|
||||||
|
plugins: PluginOption;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type LoadPluginFunction = (configEnv: ConfigEnv) => PluginOption;
|
|
||||||
export async function loadPlugins(configEnv: ConfigEnv): Promise<PluginOption[]> {
|
export async function loadPlugins(configEnv: ConfigEnv): Promise<PluginOption[]> {
|
||||||
const plugins: PluginOption[] = [];
|
const plugins: PluginOption[] = [];
|
||||||
|
|
||||||
consola.start('开始加载 Vite 插件...');
|
const cwd = path.resolve(import.meta.dirname);
|
||||||
|
|
||||||
const pluginEntries = await glob('**/*.ts', {
|
const pluginEntries = await glob('**/*.ts', {
|
||||||
absolute: true,
|
absolute: true,
|
||||||
cwd: path.resolve(import.meta.dirname),
|
cwd,
|
||||||
ignore: [
|
ignore: [
|
||||||
'**/*.d.ts',
|
'**/*.d.ts',
|
||||||
'**/*.disabled.ts',
|
'**/*.disabled.ts',
|
||||||
'**/x-*.ts', // 禁用以 x- 开头的插件文件
|
'**/x-*.ts', // 禁用以 x- 开头的插件文件
|
||||||
|
'**/*-x.ts', // 禁用以 -x 结尾的插件文件
|
||||||
|
'**/*-X.ts', // 禁用以 -X 结尾的插件文件
|
||||||
'**/_*',
|
'**/_*',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
consola.info(`找到 ${pluginEntries.length} 个插件文件`);
|
const relativeCwd = path.relative(process.cwd(), cwd);
|
||||||
|
console.time('加载插件');
|
||||||
|
consola.log(
|
||||||
|
boxen(`正在加载 Vite 插件... (./${relativeCwd})`, {
|
||||||
|
borderStyle: 'classic',
|
||||||
|
borderColor: 'cyan',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 计算最长的文件名长度,用于对齐输出
|
// 计算最长的文件名长度,用于对齐输出
|
||||||
const maxNameLength = Math.max(...pluginEntries.map((entry) => path.basename(entry).length));
|
const maxNameLength = Math.max(...pluginEntries.map((entry) => path.basename(entry).length));
|
||||||
@@ -33,36 +52,39 @@ export async function loadPlugins(configEnv: ConfigEnv): Promise<PluginOption[]>
|
|||||||
const imported = await import(pathToFileURL(entry).href);
|
const imported = await import(pathToFileURL(entry).href);
|
||||||
|
|
||||||
const loadPlugin = imported.loadPlugin as LoadPluginFunction | undefined;
|
const loadPlugin = imported.loadPlugin as LoadPluginFunction | undefined;
|
||||||
let plugin: PluginOption | undefined;
|
|
||||||
let loadMethod = '';
|
|
||||||
|
|
||||||
// 优先使用 loadPlugin 函数(接收 configEnv 参数)
|
if (!loadPlugin || typeof loadPlugin !== 'function') {
|
||||||
if (loadPlugin && typeof loadPlugin === 'function') {
|
consola.warn(`插件未导出 loadPlugin 函数: ${paddedName}`);
|
||||||
plugin = loadPlugin(configEnv);
|
continue;
|
||||||
loadMethod = 'loadPlugin';
|
|
||||||
} else if (imported.default) {
|
|
||||||
plugin = imported.default;
|
|
||||||
loadMethod = 'default';
|
|
||||||
} else {
|
|
||||||
consola.warn(`插件未导出有效内容: ${paddedName}`);
|
|
||||||
continue; // 跳过无效插件
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin) {
|
const env = loadEnv(configEnv.mode, process.cwd());
|
||||||
|
const result = loadPlugin({ ...configEnv, env });
|
||||||
|
|
||||||
|
// 判断是否是 LoadPluginResult 对象
|
||||||
|
const isResultObject = (val: unknown): val is LoadPluginResult =>
|
||||||
|
typeof val === 'object' && val !== null && 'plugins' in val;
|
||||||
|
|
||||||
|
const plugin = isResultObject(result) ? result.plugins : result;
|
||||||
|
const message = isResultObject(result) ? result.message : undefined;
|
||||||
|
|
||||||
const pluginArray = Array.isArray(plugin) ? plugin : [plugin];
|
const pluginArray = Array.isArray(plugin) ? plugin : [plugin];
|
||||||
const validPlugins = pluginArray.filter(Boolean); // 过滤掉 null/undefined
|
const validPlugins = pluginArray.filter(Boolean);
|
||||||
const pluginCount = validPlugins.length;
|
const pluginCount = validPlugins.length;
|
||||||
|
|
||||||
if (pluginCount > 0) {
|
if (pluginCount > 0) {
|
||||||
plugins.push(...validPlugins);
|
plugins.push(...validPlugins);
|
||||||
consola.success(`${paddedName} → ${pluginCount} 个实例 (${loadMethod})`);
|
const suffix = message ? ` (${message})` : '';
|
||||||
|
consola.info(`${paddedName} → ${pluginCount} 个实例${suffix}`);
|
||||||
|
} else if (message) {
|
||||||
|
consola.info(`${paddedName} → ${message}`);
|
||||||
} else {
|
} else {
|
||||||
consola.info(`${paddedName} 返回了空数组或无效值`);
|
consola.info(`${paddedName} 返回了空数组或无效值`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
consola.success(`✅ 总共加载了 ${plugins.length} 个插件实例`);
|
consola.success(`共 ${pluginEntries.length} 个插件文件,已加载 ${plugins.length} 个实例`);
|
||||||
|
console.timeEnd('加载插件');
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import type { ConfigEnv, PluginOption } from 'vite';
|
import type { LoadPluginFunction } from './_loadPlugins';
|
||||||
|
|
||||||
export default [
|
export const loadPlugin: LoadPluginFunction = (_pluginLoadOptions) => {
|
||||||
// ...
|
const env = _pluginLoadOptions.env;
|
||||||
] satisfies PluginOption;
|
|
||||||
|
|
||||||
export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
// 示例:根据环境变量禁用插件并返回消息
|
||||||
|
if (env.VITE_DEMO_ENABLED !== 'true') {
|
||||||
|
return {
|
||||||
|
plugins: [],
|
||||||
|
message: `已通过环境变量禁用: VITE_DEMO_ENABLED=${env.VITE_DEMO_ENABLED}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常返回插件
|
||||||
return [];
|
return [];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// // 检查是否在VS Code终端中运行
|
|
||||||
// if (process.env.TERM_PROGRAM === 'vscode' || process.env.VSCODE_PID) {
|
|
||||||
// // plugins.push(
|
|
||||||
// // // 构建后自动将dist目录打包成zip文件
|
|
||||||
// // viteArchiverPlugin({
|
|
||||||
// // addTimestamp: false, // 是否添加时间戳到输出文件名
|
|
||||||
// // format: 'zip', // 输出的压缩文件格式
|
|
||||||
// // outputDir: '', // 输出目录,默认为项目根目录
|
|
||||||
// // outputFileName: 'dist', // 输出的zip文件名(不含扩展名)
|
|
||||||
// // sourceDir: 'dist', // 要打包的源目录
|
|
||||||
// // }),
|
|
||||||
// // )
|
|
||||||
// }
|
|
||||||
] satisfies PluginOption;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ManualChunkMeta, PreRenderedAsset, RollupOptions } from 'rollup';
|
import type { /* ManualChunkMeta, PreRenderedAsset, */ RollupOptions } from 'rollup';
|
||||||
|
|
||||||
import path from 'node:path';
|
// import path from 'node:path';
|
||||||
|
|
||||||
// https://www.npmjs.com/package/utils4u/v/2.19.2?activeTab=code
|
// https://www.npmjs.com/package/utils4u/v/2.19.2?activeTab=code
|
||||||
|
|
||||||
@@ -12,94 +12,95 @@ export const viteConfigRollupOptions: RollupOptions = {
|
|||||||
if (warning.code === 'EVAL' && warning.id?.includes('node_modules/protobufjs')) return;
|
if (warning.code === 'EVAL' && warning.id?.includes('node_modules/protobufjs')) return;
|
||||||
warn(warning);
|
warn(warning);
|
||||||
}, */
|
}, */
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
// Keep hashed file names predictable across entry, chunk, and asset outputs.
|
// 如果一个 chunk 小于 10KB,Rollup 会尝试将它合并到其他 chunk 中。这样可以避免产生大量碎片文件
|
||||||
entryFileNames: 'entry/[name].[hash].js', // 默认: "[name].js"
|
experimentalMinChunkSize: 10 * 1024,
|
||||||
chunkFileNames: 'chunk/[name].[hash].js', // 默认: "[name]-[hash].js"
|
// // Keep hashed file names predictable across entry, chunk, and asset outputs.
|
||||||
// assetFileNames:'', // 默认: "assets/[name]-[hash][extname]"
|
// entryFileNames: 'entry/[name].[hash].js', // 默认: "[name].js"
|
||||||
// https://cn.rollupjs.org/configuration-options/#output-assetfilenames
|
// chunkFileNames: 'chunk/[name].[hash].js', // 默认: "[name]-[hash].js"
|
||||||
assetFileNames(chunkInfo: PreRenderedAsset) {
|
// // assetFileNames:'', // 默认: "assets/[name]-[hash][extname]"
|
||||||
const names = chunkInfo.names;
|
// // https://cn.rollupjs.org/configuration-options/#output-assetfilenames
|
||||||
|
// assetFileNames(chunkInfo: PreRenderedAsset) {
|
||||||
if (names.length !== 1) {
|
// const names = [...new Set(chunkInfo.names)];
|
||||||
console.error('Multiple names for asset:', chunkInfo);
|
// if (names.length !== 1) {
|
||||||
process.exit(1);
|
// console.error('Multiple names for asset:', chunkInfo);
|
||||||
}
|
// process.exit(1);
|
||||||
|
// }
|
||||||
const assetName = names[0];
|
// const assetName = names[0];
|
||||||
const ext = assetName.split('.').pop()?.toLowerCase();
|
// const ext = assetName.split('.').pop()?.toLowerCase();
|
||||||
if (ext && /png|jpe?g|gif|svg|webp|avif/.test(ext)) {
|
// if (ext && /png|jpe?g|gif|svg|webp|avif/.test(ext)) {
|
||||||
return 'chunks/images/[name].[hash][extname]';
|
// return 'chunks/images/[name].[hash][extname]';
|
||||||
}
|
// }
|
||||||
if (ext && /woff2?|ttf|otf/.test(ext)) {
|
// if (ext && /woff2?|ttf|otf/.test(ext)) {
|
||||||
return 'chunks/fonts/[name].[hash][extname]';
|
// return 'chunks/fonts/[name].[hash][extname]';
|
||||||
}
|
// }
|
||||||
if (ext === 'css') {
|
// if (ext === 'css') {
|
||||||
return 'chunks/css/[name].[hash][extname]';
|
// return 'chunks/css/[name].[hash][extname]';
|
||||||
}
|
// }
|
||||||
return '_chunks/[name].[hash][extname]';
|
// return '_chunks/[name].[hash][extname]';
|
||||||
},
|
// },
|
||||||
|
// manualChunks: (id: string, _meta: ManualChunkMeta) => {
|
||||||
manualChunks: (id: string, _meta: ManualChunkMeta) => {
|
// // https://github.com/unocss/unocss/issues/4917
|
||||||
if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
|
// // if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
|
||||||
return 'layouts';
|
// // 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
|
// // }
|
||||||
// 这里很奇怪,打印 id 是`virtual:meta-layouts`,但是 `'virtual:meta-layouts' === id` 却是 false
|
// if (id.includes('meta-layouts')) {
|
||||||
return 'lib-meta-layouts';
|
// // console.debug(`id :>> `, id); // id :>> virtual:meta-layouts
|
||||||
}
|
// // 这里很奇怪,打印 id 是`virtual:meta-layouts`,但是 `'virtual:meta-layouts' === id` 却是 false
|
||||||
|
// return 'lib-meta-layouts';
|
||||||
if (id.includes('index.page.vue')) {
|
// }
|
||||||
const parentDir = path.basename(path.dirname(id));
|
// if (id.includes('index.page.vue')) {
|
||||||
return `${parentDir}-index.page`;
|
// const url = new URL(id, 'file://');
|
||||||
}
|
// if (!url.search /* ?vue&type=script&setup=true&lang.ts */) {
|
||||||
|
// const parentDir = path.basename(path.dirname(id));
|
||||||
if (!id.includes('node_modules')) return;
|
// return `${parentDir}-index.page`;
|
||||||
// 处理 pnpm 的特殊路径结构
|
// }
|
||||||
let packageName;
|
// }
|
||||||
if (id.includes('.pnpm')) {
|
// if (!id.includes('node_modules')) return;
|
||||||
// pnpm 路径: .pnpm/naive-ui@2.43.1_vue@3.5.22/node_modules/naive-ui/...
|
// // 处理 pnpm 的特殊路径结构
|
||||||
const pnpmMatch = id.match(/\.pnpm\/(.+?)@/);
|
// let packageName;
|
||||||
if (pnpmMatch) {
|
// if (id.includes('.pnpm')) {
|
||||||
packageName = pnpmMatch[1];
|
// // pnpm 路径: .pnpm/naive-ui@2.43.1_vue@3.5.22/node_modules/naive-ui/...
|
||||||
}
|
// const pnpmMatch = id.match(/\.pnpm\/(.+?)@/);
|
||||||
} else {
|
// if (pnpmMatch) {
|
||||||
// 普通路径: node_modules/lodash/...
|
// packageName = pnpmMatch[1];
|
||||||
const normalMatch = id.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
|
// }
|
||||||
if (normalMatch) {
|
// } else {
|
||||||
packageName = normalMatch[1];
|
// // 普通路径: node_modules/lodash/...
|
||||||
}
|
// const normalMatch = id.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
|
||||||
}
|
// if (normalMatch) {
|
||||||
|
// packageName = normalMatch[1];
|
||||||
if (packageName) {
|
// }
|
||||||
if (['highlight.js'].includes(packageName)) {
|
// }
|
||||||
return 'lib-hljs';
|
// if (packageName) {
|
||||||
}
|
// if (['highlight.js'].includes(packageName)) {
|
||||||
|
// return 'lib-hljs';
|
||||||
// 根据包名分组
|
// }
|
||||||
if (['consola', 'lodash', '@juggle+resize-observer', 'vueuc'].includes(packageName)) {
|
// // 根据包名分组
|
||||||
return 'lib-vendor';
|
// if (['consola', 'lodash', '@juggle+resize-observer', 'vueuc'].includes(packageName)) {
|
||||||
}
|
// 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)) {
|
// if (['naive-ui'].includes(packageName)) {
|
||||||
return 'lib-naive-ui';
|
// return 'lib-naive-ui';
|
||||||
}
|
// }
|
||||||
|
// if (
|
||||||
if (['primelocale', 'primevue', '@primeuix'].some((name) => packageName!.includes(name))) {
|
// ['primelocale', 'primevue', 'primeuix', 'primeicons'].some((name) =>
|
||||||
return 'lib-primevue';
|
// packageName!.includes(name),
|
||||||
}
|
// )
|
||||||
|
// ) {
|
||||||
if (['vue', 'vue-router', 'pinia', 'vue-demi', 'vue-i18n'].includes(packageName)) {
|
// return 'lib-primevue';
|
||||||
return 'lib-vue-vendor';
|
// }
|
||||||
}
|
// if (['vue', 'vue-router', 'pinia', 'vue-demi', 'vue-i18n'].includes(packageName)) {
|
||||||
}
|
// return 'lib-vue-vendor';
|
||||||
},
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ export default defineConfig(async (configEnv) => {
|
|||||||
|
|
||||||
const isBuild = command === 'build';
|
const isBuild = command === 'build';
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
if (process.env.CI) {
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
consola.info(`[vite.config.ts] env: ${key}: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: env.VITE_APP_BASE,
|
base: env.VITE_APP_BASE,
|
||||||
build: {
|
build: {
|
||||||
minify: env.VITE_APP_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
|
minify: env.VITE_BUILD_MINIFY === 'true' ? undefined /* 即默认 */ : false, // 默认: 'terser'
|
||||||
sourcemap: env.VITE_APP_BUILD_SOURCE_MAP === 'true',
|
sourcemap: env.VITE_BUILD_SOURCE_MAP === 'true',
|
||||||
rollupOptions: viteConfigRollupOptions,
|
rollupOptions: viteConfigRollupOptions,
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
@@ -35,12 +40,11 @@ export default defineConfig(async (configEnv) => {
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'~': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__DEV__: JSON.stringify(!isBuild),
|
__DEV__: JSON.stringify(!isBuild),
|
||||||
// // https://github.com/fi3ework/vite-plugin-checker/issues/569#issuecomment-3254311799
|
|
||||||
// 'process.env.NODE_ENV': JSON.stringify('production'),
|
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['.NWCT.DEV'],
|
allowedHosts: ['.NWCT.DEV'],
|
||||||
|
|||||||
1415
worker-configuration.d.ts
vendored
1415
worker-configuration.d.ts
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user