diff --git a/.changeset/.prettierrc.json b/.changeset/.prettierrc.json new file mode 100644 index 00000000..544138be --- /dev/null +++ b/.changeset/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/.changeset/fancy-ears-walk.md b/.changeset/fancy-ears-walk.md new file mode 100644 index 00000000..f73cb596 --- /dev/null +++ b/.changeset/fancy-ears-walk.md @@ -0,0 +1,7 @@ +--- +'@vben/styles': patch +'@vben-core/form-ui': patch +'@vben/web-naive': patch +--- + +feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray diff --git a/.gitattributes b/.gitattributes index d4e5bd3e..775caa34 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,8 +4,18 @@ * text=auto eol=lf # Declare files that will always have CRLF line endings on checkout. -*.{cmd,[cC][mM][dD]} text eol=crlf -*.{bat,[bB][aA][tT]} text eol=crlf +*.cmd text eol=crlf +*.[cC][mM][dD] text eol=crlf +*.bat text eol=crlf +*.[bB][aA][tT] text eol=crlf # Denote all files that are truly binary and should not be modified. -*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary \ No newline at end of file +*.ico binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.svg binary +*.woff binary +*.woff2 binary \ No newline at end of file diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 35fa41c8..445f30be 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -6,10 +6,10 @@ runs: using: 'composite' steps: - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: .node-version cache: 'pnpm' diff --git a/.github/config.yml b/.github/config.yml index 52454840..248da6a5 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -21,12 +21,10 @@ newPRWelcomeComment: | firstPRMergeComment: > Thanks for your contribution! 🎉🎉🎉 - # Comment to be posted to on first time issues newIssueWelcomeComment: > Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers! - # *OPTIONAL* default titles to check against for lack of descriptiveness # MUST BE ALL LOWERCASE requestInfoDefaultTitles: @@ -36,4 +34,3 @@ requestInfoDefaultTitles: # *Required* Comment to reply with requestInfoReplyComment: > Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context. - diff --git a/.github/contributing.md b/.github/contributing.md index f22370f3..304c5192 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject - Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch. - If adding a new feature: - - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it. - If fixing bug: - - Provide a detailed description of the bug in the PR. Live demo preferred. - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9f48e4a..b81bc4ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - windows-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/changeset-version.yml b/.github/workflows/changeset-version.yml index 373d9af2..32421e26 100644 --- a/.github/workflows/changeset-version.yml +++ b/.github/workflows/changeset-version.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00750949..254ffd8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,12 +28,12 @@ jobs: timeout-minutes: 20 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -90,7 +90,7 @@ jobs: - windows-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 69a990f0..4824318e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,11 +57,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -89,6 +89,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b4afdd93..8c12d408 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -30,7 +30,7 @@ jobs: run: pnpm build:play - name: Sync Playground files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -54,7 +54,7 @@ jobs: run: pnpm build:docs - name: Sync Docs files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEBSITE_FTP_ACCOUNT }} @@ -67,7 +67,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -85,7 +85,7 @@ jobs: run: pnpm run build:antd - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }} @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -116,7 +116,7 @@ jobs: run: pnpm run build:ele - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }} @@ -129,7 +129,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -147,7 +147,7 @@ jobs: run: pnpm run build:naive - name: Sync files - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.PRO_FTP_HOST }} username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }} diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index 20b4148a..babca2d3 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -20,6 +20,6 @@ jobs: if: github.repository == 'vbenjs/vue-vben-admin' runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml index 18c007bf..794292f4 100644 --- a/.github/workflows/issue-close-require.yml +++ b/.github/workflows/issue-close-require.yml @@ -19,7 +19,7 @@ jobs: steps: # 关闭未活动的 Issues - name: Close Inactive Issues - uses: actions/stale@v9 + uses: actions/stale@v11 with: days-before-stale: -1 # Issues and PR will never be flagged stale automatically. stale-issue-label: needs-reproduction # Label that flags an issue as stale. diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index 2feda4ef..7a461e3b 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -18,7 +18,7 @@ jobs: steps: - name: remove enhancement pending if: github.event.label.name == 'enhancement' - uses: actions-cool/issues-helper@v3 + uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69 with: actions: 'remove-labels' token: ${{ secrets.GITHUB_TOKEN }} @@ -27,7 +27,7 @@ jobs: - name: remove bug pending if: github.event.label.name == 'bug' - uses: actions-cool/issues-helper@v3 + uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69 with: actions: 'remove-labels' token: ${{ secrets.GITHUB_TOKEN }} @@ -36,7 +36,7 @@ jobs: - name: needs reproduction if: github.event.label.name == 'needs reproduction' - uses: actions-cool/issues-helper@v3 + uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69 with: actions: 'create-comment, remove-labels' token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index bd73d533..a12227ef 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -14,7 +14,7 @@ jobs: if: github.repository == 'vbenjs/vue-vben-admin' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} issue-inactive-days: '14' diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 7f524f90..d87a7011 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -3,78 +3,48 @@ name: Create Release Tag on: push: tags: - - 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 - -env: - HUSKY: '0' + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to create (e.g. v1.2.3)' + required: true + type: string permissions: - pull-requests: write contents: write jobs: - build: + release: name: Create Release if: github.repository == 'vbenjs/vue-vben-admin' runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - # - name: Checkout code - # uses: actions/checkout@v4 - # with: - # fetch-depth: 0 - - # - name: Install pnpm - # uses: pnpm/action-setup@v4 - - # - name: Use Node.js ${{ matrix.node-version }} - # uses: actions/setup-node@v4 - # with: - # node-version: ${{ matrix.node-version }} - # cache: "pnpm" - - # - name: Install dependencies - # run: pnpm install --frozen-lockfile - - # - name: Test and Build - # run: | - # pnpm run test - # pnpm run build - - - name: version + - name: Extract version id: version run: | - tag=${GITHUB_REF/refs\/tags\//} - version=${tag#v} - major=${version%%.*} - echo "tag=${tag}" >> $GITHUB_OUTPUT - echo "version=${version}" >> $GITHUB_OUTPUT - echo "major=${major}" >> $GITHUB_OUTPUT + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + raw_tag="${{ inputs.tag }}" + else + raw_tag="${GITHUB_REF_NAME}" + fi + # Normalize: ensure v prefix + tag="${raw_tag}" + [[ "${tag:0:1}" != "v" ]] && tag="v${tag}" + version="${tag#v}" + major="${version%%.*}" + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "version=${version}" >> "${GITHUB_OUTPUT}" + echo "major=${major}" >> "${GITHUB_OUTPUT}" - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@v7 with: version: ${{ steps.version.outputs.version }} publish: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # - name: force update major tag - # run: | - # git tag v${{ steps.version.outputs.major }} ${{ steps.version.outputs.tag }} -f - # git push origin refs/tags/v${{ steps.version.outputs.major }} -f - - # - name: Create Release for Tag - # id: release_tag - # uses: ncipollo/release-action@v1 - # with: - # token: ${{ secrets.GITHUB_TOKEN }} - # generateReleaseNotes: "true" - # body: | - # > Please refer to [CHANGELOG.md](https://github.com/vbenjs/vue-vben-admin/blob/main/CHANGELOG.md) for details. diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index db8e9e09..02db08de 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Validate PR title - uses: amannn/action-semantic-pull-request@v5 + uses: amannn/action-semantic-pull-request@v6 with: wip: true subjectPattern: ^(?![A-Z]).+$ diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 991e1331..a6fd1c0e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: if: github.repository == 'vbenjs/vue-vben-admin' runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v11 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' diff --git a/.gitignore b/.gitignore index c2a8a771..3a7e7ced 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,8 @@ yarn.lock package-lock.json .VSCodeCounter **/backend-mock/data - +.omx +.pnpm-store # local env files .env.local .env.*.local @@ -49,3 +50,14 @@ vite.config.ts.* *.sln *.sw? .history +.cursor + +# AI +.agent +.agents +.claude +.codex +skills-lock.json +.atomcode +datalog +.playwright-mcp diff --git a/.node-version b/.node-version index ee5c2446..b832e400 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22.1.0 +24.16.0 diff --git a/.npmrc b/.npmrc index 21147aff..7549542d 100644 --- a/.npmrc +++ b/.npmrc @@ -1,13 +1 @@ -registry = "https://registry.npmmirror.com" -public-hoist-pattern[]=lefthook -public-hoist-pattern[]=eslint -public-hoist-pattern[]=prettier -public-hoist-pattern[]=prettier-plugin-tailwindcss -public-hoist-pattern[]=stylelint -public-hoist-pattern[]=*postcss* -public-hoist-pattern[]=@commitlint/* -public-hoist-pattern[]=czg - -strict-peer-dependencies=false -auto-install-peers=true -dedupe-peer-dependents=true +registry=https://registry.npmmirror.com diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index d0b0ca13..00000000 --- a/.prettierignore +++ /dev/null @@ -1,18 +0,0 @@ -dist -dev-dist -.local -.output.js -node_modules -.nvmrc -coverage -CODEOWNERS -.nitro -.output - - -**/*.svg -**/*.sh - -public -.npmrc -*-lock.yaml diff --git a/.prettierrc.mjs b/.prettierrc.mjs deleted file mode 100644 index 3e25d2cf..00000000 --- a/.prettierrc.mjs +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@vben/prettier-config'; diff --git a/.stylelintignore b/.stylelintignore index f4b2db2c..3adb33b2 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -2,3 +2,7 @@ dist public __tests__ coverage +.codex +.claude +.agent +.agents diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e8dc9ed9..d4e28662 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,14 +2,18 @@ "recommendations": [ // Vue 3 的语言支持 "Vue.volar", - // 将 ESLint JavaScript 集成到 VS Code 中。 + // 将 eslint 集成到 VS Code 中。 "dbaeumer.vscode-eslint", + // 将 oxlint 集成到 VS Code 中。 + "oxc.oxc-vscode", // Visual Studio Code 的官方 Stylelint 扩展 "stylelint.vscode-stylelint", - // 使用 Prettier 的代码格式化程序 - "esbenp.prettier-vscode", + // 使用 oxfmt 的代码格式化程序 + "oxc.oxc-vscode", // 支持 dotenv 文件语法 "mikestead.dotenv", + // YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件 + "redhat.vscode-yaml", // 源代码的拼写检查器 "streetsidesoftware.code-spell-checker", // Tailwind CSS 的官方 VS Code 插件 diff --git a/.vscode/settings.json b/.vscode/settings.json index f38c4278..993b5203 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { - "tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts", + "tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css", + "tailwindCSS.lint.suggestCanonicalClasses": "ignore", // workbench "workbench.list.smoothScrolling": true, "workbench.startupEditor": "newUntitledFile", @@ -31,39 +32,51 @@ "editor.autoClosingOvertype": "always", "editor.autoClosingQuotes": "beforeWhitespace", "editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", + "editor.quickSuggestions": { + "strings": "on" + }, + + // lint && format + "oxc.enable": true, + "oxc.typeAware": true, + "oxc.configPath": "oxlint.config.ts", + "oxc.fmt.configPath": "oxfmt.config.ts", + "eslint.useFlatConfig": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", + "source.fixAll.oxc": "explicit", "source.fixAll.stylelint": "explicit", "source.organizeImports": "never" }, - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "[html]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[css]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[scss]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[vue]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode" }, + // extensions "extensions.ignoreRecommendations": true, @@ -79,6 +92,7 @@ "files.insertFinalNewline": true, "files.simpleDialog.enable": true, "files.associations": { + "*.css": "tailwindcss", "*.ejs": "html", "*.art": "html", "**/tsconfig.json": "jsonc", @@ -118,7 +132,7 @@ // search "search.searchEditor.singleClickBehaviour": "peekDefinition", "search.followSymlinks": false, - // 在使用搜索功能时,将这些文件夹/文件排除在外 + // 使用搜索功能时,将这些文件和文件夹排除在外 "search.exclude": { "**/node_modules": true, "**/*.log": true, @@ -158,7 +172,7 @@ "emmet.triggerExpansionOnTab": false, "errorLens.enabledDiagnosticLevels": ["warning", "error"], - "errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"], + "errorLens.excludeBySource": ["cSpell", "Grammarly"], "stylelint.enable": true, "stylelint.packageManager": "pnpm", @@ -166,9 +180,10 @@ "stylelint.customSyntax": "postcss-html", "stylelint.snippet": ["css", "less", "postcss", "scss", "vue"], - "typescript.inlayHints.enumMemberValues.enabled": true, - "typescript.preferences.preferTypeOnlyAutoImports": true, - "typescript.preferences.includePackageJsonAutoImports": "on", + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.inlayHints.enumMemberValues.enabled": true, + "js/ts.preferences.preferTypeOnlyAutoImports": true, + "js/ts.preferences.includePackageJsonAutoImports": "on", "eslint.validate": [ "javascript", @@ -180,7 +195,8 @@ "markdown", "json", "jsonc", - "json5" + "json5", + "yaml" ], "tailwindCSS.experimental.classRegex": [ @@ -194,7 +210,7 @@ "yaml": false }, - "cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"], + "cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"], "i18n-ally.localesPaths": [ "packages/locales/src/langs", @@ -219,23 +235,9 @@ "*.env": "$(capture).env.*", "README.md": "README*,CHANGELOG*,LICENSE,CNAME", "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json", - "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml", - "tailwind.config.mjs": "postcss.*" + "oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*" }, "commentTranslate.hover.enabled": false, "commentTranslate.multiLineMerge": true, - "vue.server.hybridMode": true, - "typescript.tsdk": "node_modules/typescript/lib", - "oxc.enable": false, - "cSpell.words": [ - "archiver", - "axios", - "dotenv", - "isequal", - "jspm", - "napi", - "nolebase", - "rollup", - "vitest" - ] + "vue.server.hybridMode": true } diff --git a/README.ja-JP.md b/README.ja-JP.md index f7847a1d..ca0a9775 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -114,7 +114,7 @@ pnpm build ## ブラウザサポート -ローカル開発には `Chrome 80+` ブラウザを推奨します +Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+ モダンブラウザをサポートし、IEはサポートしません @@ -140,8 +140,12 @@ pnpm build ## 貢献者 + + Contribution Leaderboard + + - Contributors + Contributors ## Discord diff --git a/README.md b/README.md index 0580d49d..e1c9f29c 100644 --- a/README.md +++ b/README.md @@ -1 +1,157 @@ -# 奶酪云后台管理脚手架 +
+ + VbenAdmin Logo + +
+
+ +[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE) + +

Vue Vben Admin

+
+ +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml) + +**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md) + +## Introduction + +Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference. + +## Upgrade Notice + +This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2). + +## Features + +- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite +- **TypeScript**: A language for application-scale JavaScript +- **Themes**: Multiple theme colors available with customizable options +- **Internationalization**: Comprehensive built-in internationalization support +- **Permissions**: Built-in solution for dynamic route-based permission generation + +## Preview + +- [Vben Admin](https://vben.pro/) - Full version Chinese site + +Test Account: vben/123456 + +
+ VbenAdmin Logo + VbenAdmin Logo + VbenAdmin Logo +
+ +### Use Gitpod + +Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin) + +## Documentation + +[Document](https://doc.vben.pro/) + +## Install and Use + +1. Get the project code + +```bash +git clone https://github.com/vbenjs/vue-vben-admin.git +``` + +2. Install dependencies + +```bash +cd vue-vben-admin +npm i -g corepack +pnpm install +``` + +3. Run + +```bash +pnpm dev +``` + +4. Build + +```bash +pnpm build +``` + +## Change Log + +[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases) + +## How to Contribute + +You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request. + +**Pull Request Process:** + +1. Fork the code +2. Create your branch: `git checkout -b feat/xxxx` +3. Submit your changes: `git commit -am 'feat(function): add xxxxx'` +4. Push your branch: `git push origin feat/xxxx` +5. Submit `pull request` + +## Git Contribution Submission Specification + +Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) + +- `feat` Add new features +- `fix` Fix the problem/BUG +- `style` The code style is related and does not affect the running result +- `perf` Optimization/performance improvement +- `refactor` Refactor +- `revert` Undo edit +- `test` Test related +- `docs` Documentation/notes +- `chore` Dependency update/scaffolding configuration modification etc. +- `ci` Continuous integration +- `types` Type definition file changes + +## Browser Support + +Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+ + +Support modern browsers, not IE + +| [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| :-: | :-: | :-: | :-: | +| last 2 versions | last 2 versions | last 2 versions | last 2 versions | + +## Maintainer + +[@Vben](https://github.com/anncwb) + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date) + +## Donate + +If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support! + +![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png) + +Paypal Me + +## Contributors + + + Contribution Leaderboard + + + + Contributors + + +## Discord + +- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions) + +## License + +[MIT © Vben-2020](./LICENSE) diff --git a/README.zh-CN.md b/README.zh-CN.md index 5a6b191b..da7ec5fa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -114,7 +114,7 @@ pnpm build ## 浏览器支持 -本地开发推荐使用 `Chrome 80+` 浏览器 +Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+ 支持现代浏览器,不支持 IE @@ -140,8 +140,12 @@ pnpm build ## 贡献者 + + Contribution Leaderboard + + - Contributors + Contributors ## Discord diff --git a/apps/backend-mock/api/auth/codes.ts b/apps/backend-mock/api/auth/codes.ts index 7ba01270..e610b338 100644 --- a/apps/backend-mock/api/auth/codes.ts +++ b/apps/backend-mock/api/auth/codes.ts @@ -1,5 +1,7 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; +import { MOCK_CODES } from '~/utils/mock-data'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; export default eventHandler((event) => { const userinfo = verifyAccessToken(event); diff --git a/apps/backend-mock/api/auth/login.post.ts b/apps/backend-mock/api/auth/login.post.ts index df5737a2..e23942c4 100644 --- a/apps/backend-mock/api/auth/login.post.ts +++ b/apps/backend-mock/api/auth/login.post.ts @@ -1,9 +1,15 @@ +import { defineEventHandler, readBody, setResponseStatus } from 'h3'; import { clearRefreshTokenCookie, setRefreshTokenCookie, } from '~/utils/cookie-utils'; import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils'; -import { forbiddenResponse } from '~/utils/response'; +import { MOCK_USERS } from '~/utils/mock-data'; +import { + forbiddenResponse, + useResponseError, + useResponseSuccess, +} from '~/utils/response'; export default defineEventHandler(async (event) => { const { password, username } = await readBody(event); diff --git a/apps/backend-mock/api/auth/logout.post.ts b/apps/backend-mock/api/auth/logout.post.ts index ac6afe94..74c8d315 100644 --- a/apps/backend-mock/api/auth/logout.post.ts +++ b/apps/backend-mock/api/auth/logout.post.ts @@ -1,7 +1,9 @@ +import { defineEventHandler } from 'h3'; import { clearRefreshTokenCookie, getRefreshTokenFromCookie, } from '~/utils/cookie-utils'; +import { useResponseSuccess } from '~/utils/response'; export default defineEventHandler(async (event) => { const refreshToken = getRefreshTokenFromCookie(event); diff --git a/apps/backend-mock/api/auth/refresh.post.ts b/apps/backend-mock/api/auth/refresh.post.ts index 7df4d34f..7d8d3a51 100644 --- a/apps/backend-mock/api/auth/refresh.post.ts +++ b/apps/backend-mock/api/auth/refresh.post.ts @@ -1,9 +1,11 @@ +import { defineEventHandler } from 'h3'; import { clearRefreshTokenCookie, getRefreshTokenFromCookie, setRefreshTokenCookie, } from '~/utils/cookie-utils'; -import { verifyRefreshToken } from '~/utils/jwt-utils'; +import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils'; +import { MOCK_USERS } from '~/utils/mock-data'; import { forbiddenResponse } from '~/utils/response'; export default defineEventHandler(async (event) => { diff --git a/apps/backend-mock/api/demo/bigint.ts b/apps/backend-mock/api/demo/bigint.ts new file mode 100644 index 00000000..00d6c28c --- /dev/null +++ b/apps/backend-mock/api/demo/bigint.ts @@ -0,0 +1,32 @@ +import { eventHandler, setHeader } from 'h3'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse } from '~/utils/response'; + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const data = ` + { + "code": 0, + "message": "success", + "data": [ + { + "id": 123456789012345678901234567890123456789012345678901234567890, + "name": "John Doe", + "age": 30, + "email": "john-doe@demo.com" + }, + { + "id": 987654321098765432109876543210987654321098765432109876543210, + "name": "Jane Smith", + "age": 25, + "email": "jane@demo.com" + } + ] + } + `; + setHeader(event, 'Content-Type', 'application/json'); + return data; +}); diff --git a/apps/backend-mock/api/menu/all.ts b/apps/backend-mock/api/menu/all.ts index 580cee4f..7923f7ca 100644 --- a/apps/backend-mock/api/menu/all.ts +++ b/apps/backend-mock/api/menu/all.ts @@ -1,5 +1,7 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; +import { MOCK_MENUS } from '~/utils/mock-data'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; export default eventHandler(async (event) => { const userinfo = verifyAccessToken(event); diff --git a/apps/backend-mock/api/status.ts b/apps/backend-mock/api/status.ts index 41773e1d..43782095 100644 --- a/apps/backend-mock/api/status.ts +++ b/apps/backend-mock/api/status.ts @@ -1,3 +1,6 @@ +import { eventHandler, getQuery, setResponseStatus } from 'h3'; +import { useResponseError } from '~/utils/response'; + export default eventHandler((event) => { const { status } = getQuery(event); setResponseStatus(event, Number(status)); diff --git a/apps/backend-mock/api/system/dept/.post.ts b/apps/backend-mock/api/system/dept/.post.ts index c529ea1b..9a4896af 100644 --- a/apps/backend-mock/api/system/dept/.post.ts +++ b/apps/backend-mock/api/system/dept/.post.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { sleep, diff --git a/apps/backend-mock/api/system/dept/[id].delete.ts b/apps/backend-mock/api/system/dept/[id].delete.ts index e48f051c..eac0f584 100644 --- a/apps/backend-mock/api/system/dept/[id].delete.ts +++ b/apps/backend-mock/api/system/dept/[id].delete.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { sleep, diff --git a/apps/backend-mock/api/system/dept/[id].put.ts b/apps/backend-mock/api/system/dept/[id].put.ts index aa55c085..6805e139 100644 --- a/apps/backend-mock/api/system/dept/[id].put.ts +++ b/apps/backend-mock/api/system/dept/[id].put.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { sleep, diff --git a/apps/backend-mock/api/system/dept/list.ts b/apps/backend-mock/api/system/dept/list.ts index ae819b62..a649a0d2 100644 --- a/apps/backend-mock/api/system/dept/list.ts +++ b/apps/backend-mock/api/system/dept/list.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend-mock/api/system/menu/list.ts b/apps/backend-mock/api/system/menu/list.ts index 5328b2fd..ce96bb14 100644 --- a/apps/backend-mock/api/system/menu/list.ts +++ b/apps/backend-mock/api/system/menu/list.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { MOCK_MENU_LIST } from '~/utils/mock-data'; import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend-mock/api/system/menu/name-exists.ts b/apps/backend-mock/api/system/menu/name-exists.ts index 5599c22b..7d5551b3 100644 --- a/apps/backend-mock/api/system/menu/name-exists.ts +++ b/apps/backend-mock/api/system/menu/name-exists.ts @@ -1,6 +1,7 @@ +import { eventHandler, getQuery } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { MOCK_MENU_LIST } from '~/utils/mock-data'; -import { unAuthorizedResponse } from '~/utils/response'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; const namesMap: Record = {}; diff --git a/apps/backend-mock/api/system/menu/path-exists.ts b/apps/backend-mock/api/system/menu/path-exists.ts index 64774f79..f3c3be99 100644 --- a/apps/backend-mock/api/system/menu/path-exists.ts +++ b/apps/backend-mock/api/system/menu/path-exists.ts @@ -1,6 +1,7 @@ +import { eventHandler, getQuery } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { MOCK_MENU_LIST } from '~/utils/mock-data'; -import { unAuthorizedResponse } from '~/utils/response'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; const pathMap: Record = { '/': 0 }; diff --git a/apps/backend-mock/api/system/role/list.ts b/apps/backend-mock/api/system/role/list.ts index 4d5f923e..bad29a51 100644 --- a/apps/backend-mock/api/system/role/list.ts +++ b/apps/backend-mock/api/system/role/list.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import { eventHandler, getQuery } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data'; import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; diff --git a/apps/backend-mock/api/system/user/list.ts b/apps/backend-mock/api/system/user/list.ts new file mode 100644 index 00000000..21d44c8b --- /dev/null +++ b/apps/backend-mock/api/system/user/list.ts @@ -0,0 +1,85 @@ +import { faker } from '@faker-js/faker'; +import { eventHandler, getQuery } from 'h3'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; + +const formatterCN = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +function generateMockDataList(count: number) { + const dataList = []; + + for (let i = 0; i < count; i++) { + const dataItem: Record = { + id: faker.string.uuid(), + name: faker.commerce.product(), + status: faker.helpers.arrayElement([0, 1]), + createTime: formatterCN.format( + faker.date.between({ from: '2022-01-01', to: '2025-01-01' }), + ), + deptId: faker.string.uuid(), + remark: faker.lorem.sentence(), + }; + + dataList.push(dataItem); + } + + return dataList; +} + +const mockData = generateMockDataList(100); + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + + const { + page = 1, + pageSize = 20, + name, + id, + remark, + startTime, + endTime, + deptId, + status, + } = getQuery(event); + let listData = structuredClone(mockData); + if (name) { + listData = listData.filter((item) => + item.name.toLowerCase().includes(String(name).toLowerCase()), + ); + } + if (id) { + listData = listData.filter((item) => + item.id.toLowerCase().includes(String(id).toLowerCase()), + ); + } + if (remark) { + listData = listData.filter((item) => + item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()), + ); + } + if (startTime) { + listData = listData.filter((item) => item.createTime >= startTime); + } + if (endTime) { + listData = listData.filter((item) => item.createTime <= endTime); + } + if (['0', '1'].includes(status as string)) { + listData = listData.filter((item) => item.status === Number(status)); + } + if (deptId) { + listData = listData.filter((item) => item.deptId === deptId); + } + return usePageResponseSuccess(page as string, pageSize as string, listData); +}); diff --git a/apps/backend-mock/api/table/list.ts b/apps/backend-mock/api/table/list.ts index 3e6f705b..f81571e3 100644 --- a/apps/backend-mock/api/table/list.ts +++ b/apps/backend-mock/api/table/list.ts @@ -1,6 +1,11 @@ import { faker } from '@faker-js/faker'; +import { eventHandler, getQuery } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; +import { + sleep, + unAuthorizedResponse, + usePageResponseSuccess, +} from '~/utils/response'; function generateMockDataList(count: number) { const dataList = []; @@ -44,30 +49,69 @@ export default eventHandler(async (event) => { await sleep(600); const { page, pageSize, sortBy, sortOrder } = getQuery(event); + // 规范化分页参数,处理 string[] + const pageRaw = Array.isArray(page) ? page[0] : page; + const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize; + const pageNumber = Math.max( + 1, + Number.parseInt(String(pageRaw ?? '1'), 10) || 1, + ); + const pageSizeNumber = Math.min( + 100, + Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10), + ); const listData = structuredClone(mockData); - if (sortBy && Reflect.has(listData[0], sortBy as string)) { + + // 规范化 query 入参,兼容 string[] + const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy; + const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder; + // 检查 sortBy 是否是 listData 元素的合法属性键 + if ( + typeof sortKeyRaw === 'string' && + listData[0] && + Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw) + ) { + // 定义数组元素的类型 + type ItemType = (typeof listData)[0]; + const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键 + const isDesc = sortOrderRaw === 'desc'; listData.sort((a, b) => { - if (sortOrder === 'asc') { - if (sortBy === 'price') { - return ( - Number.parseFloat(a[sortBy as string]) - - Number.parseFloat(b[sortBy as string]) - ); + const aValue = a[sortKey] as unknown; + const bValue = b[sortKey] as unknown; + + let result: number; + + if (typeof aValue === 'number' && typeof bValue === 'number') { + result = aValue - bValue; + } else if (aValue instanceof Date && bValue instanceof Date) { + result = aValue.getTime() - bValue.getTime(); + } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { + if (aValue === bValue) { + result = 0; } else { - return a[sortBy as string] > b[sortBy as string] ? 1 : -1; + result = aValue ? 1 : -1; } } else { - if (sortBy === 'price') { - return ( - Number.parseFloat(b[sortBy as string]) - - Number.parseFloat(a[sortBy as string]) - ); - } else { - return a[sortBy as string] < b[sortBy as string] ? 1 : -1; - } + const aStr = String(aValue); + const bStr = String(bValue); + const aNum = Number(aStr); + const bNum = Number(bStr); + result = + Number.isFinite(aNum) && Number.isFinite(bNum) + ? aNum - bNum + : aStr.localeCompare(bStr, undefined, { + numeric: true, + sensitivity: 'base', + }); } + + return isDesc ? -result : result; }); } - return usePageResponseSuccess(page as string, pageSize as string, listData); + return usePageResponseSuccess( + String(pageNumber), + String(pageSizeNumber), + listData, + ); }); diff --git a/apps/backend-mock/api/test.get.ts b/apps/backend-mock/api/test.get.ts index ca4a500b..dc2ceef7 100644 --- a/apps/backend-mock/api/test.get.ts +++ b/apps/backend-mock/api/test.get.ts @@ -1 +1,3 @@ +import { defineEventHandler } from 'h3'; + export default defineEventHandler(() => 'Test get handler'); diff --git a/apps/backend-mock/api/test.post.ts b/apps/backend-mock/api/test.post.ts index 698cf211..0e9e337a 100644 --- a/apps/backend-mock/api/test.post.ts +++ b/apps/backend-mock/api/test.post.ts @@ -1 +1,3 @@ +import { defineEventHandler } from 'h3'; + export default defineEventHandler(() => 'Test post handler'); diff --git a/apps/backend-mock/api/timezone/getTimezone.ts b/apps/backend-mock/api/timezone/getTimezone.ts new file mode 100644 index 00000000..0cbcb6ec --- /dev/null +++ b/apps/backend-mock/api/timezone/getTimezone.ts @@ -0,0 +1,12 @@ +import { eventHandler } from 'h3'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; +import { getTimezone } from '~/utils/timezone-utils'; + +export default eventHandler((event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + return useResponseSuccess(getTimezone()); +}); diff --git a/apps/backend-mock/api/timezone/getTimezoneOptions.ts b/apps/backend-mock/api/timezone/getTimezoneOptions.ts new file mode 100644 index 00000000..6c241864 --- /dev/null +++ b/apps/backend-mock/api/timezone/getTimezoneOptions.ts @@ -0,0 +1,11 @@ +import { eventHandler } from 'h3'; +import { TIME_ZONE_OPTIONS } from '~/utils/mock-data'; +import { useResponseSuccess } from '~/utils/response'; + +export default eventHandler(() => { + const data = TIME_ZONE_OPTIONS.map((o) => ({ + label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`, + value: o.timezone, + })); + return useResponseSuccess(data); +}); diff --git a/apps/backend-mock/api/timezone/setTimezone.ts b/apps/backend-mock/api/timezone/setTimezone.ts new file mode 100644 index 00000000..851e4351 --- /dev/null +++ b/apps/backend-mock/api/timezone/setTimezone.ts @@ -0,0 +1,26 @@ +import { eventHandler, readBody, setResponseStatus } from 'h3'; +import { verifyAccessToken } from '~/utils/jwt-utils'; +import { TIME_ZONE_OPTIONS } from '~/utils/mock-data'; +import { + unAuthorizedResponse, + useResponseError, + useResponseSuccess, +} from '~/utils/response'; +import { setTimezone } from '~/utils/timezone-utils'; + +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const body = await readBody<{ timezone?: unknown }>(event); + const timezone = + typeof body?.timezone === 'string' ? body.timezone : undefined; + const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone); + if (!timezone || !allowed) { + setResponseStatus(event, 400); + return useResponseError('Bad Request', 'Invalid timezone'); + } + setTimezone(timezone); + return useResponseSuccess({}); +}); diff --git a/apps/backend-mock/api/upload.ts b/apps/backend-mock/api/upload.ts index 1bb9e602..436b63cb 100644 --- a/apps/backend-mock/api/upload.ts +++ b/apps/backend-mock/api/upload.ts @@ -1,5 +1,6 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; export default eventHandler((event) => { const userinfo = verifyAccessToken(event); diff --git a/apps/backend-mock/api/user/info.ts b/apps/backend-mock/api/user/info.ts index cfa2346c..138cb433 100644 --- a/apps/backend-mock/api/user/info.ts +++ b/apps/backend-mock/api/user/info.ts @@ -1,5 +1,6 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; +import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response'; export default eventHandler((event) => { const userinfo = verifyAccessToken(event); diff --git a/apps/backend-mock/middleware/1.api.ts b/apps/backend-mock/middleware/1.api.ts index bad9a41a..339cda4d 100644 --- a/apps/backend-mock/middleware/1.api.ts +++ b/apps/backend-mock/middleware/1.api.ts @@ -1,3 +1,4 @@ +import { defineEventHandler } from 'h3'; import { forbiddenResponse, sleep } from '~/utils/response'; export default defineEventHandler(async (event) => { diff --git a/apps/backend-mock/nitro.config.ts b/apps/backend-mock/nitro.config.ts index c0fc13e2..9cc3acdb 100644 --- a/apps/backend-mock/nitro.config.ts +++ b/apps/backend-mock/nitro.config.ts @@ -1,3 +1,5 @@ +import { defineNitroConfig } from 'nitropack/config'; + import errorHandler from './error'; process.env.COMPATIBILITY_DATE = new Date().toISOString(); diff --git a/apps/backend-mock/package.json b/apps/backend-mock/package.json index cc0b8d53..66961d4d 100644 --- a/apps/backend-mock/package.json +++ b/apps/backend-mock/package.json @@ -1,6 +1,6 @@ { "name": "@vben/backend-mock", - "version": "0.0.1", + "version": "5.7.0", "description": "", "private": true, "license": "MIT", diff --git a/apps/backend-mock/routes/[...].ts b/apps/backend-mock/routes/[...].ts index 99f544b6..44038cf7 100644 --- a/apps/backend-mock/routes/[...].ts +++ b/apps/backend-mock/routes/[...].ts @@ -1,13 +1,15 @@ +import { defineEventHandler } from 'h3'; + export default defineEventHandler(() => { return `

Hello Vben Admin

-

Mock service is starting

+

Mock service is running

`; }); diff --git a/apps/backend-mock/tsconfig.json b/apps/backend-mock/tsconfig.json index 43008af1..4417b73d 100644 --- a/apps/backend-mock/tsconfig.json +++ b/apps/backend-mock/tsconfig.json @@ -1,3 +1,11 @@ { - "extends": "./.nitro/types/tsconfig.json" + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/node.json", + "compilerOptions": { + "paths": { + "~/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/.*.ts"], + "exclude": ["node_modules", "dist", ".nitro"] } diff --git a/apps/backend-mock/utils/cookie-utils.ts b/apps/backend-mock/utils/cookie-utils.ts index 78f3aab7..187ce2f0 100644 --- a/apps/backend-mock/utils/cookie-utils.ts +++ b/apps/backend-mock/utils/cookie-utils.ts @@ -1,5 +1,7 @@ import type { EventHandlerRequest, H3Event } from 'h3'; +import { deleteCookie, getCookie, setCookie } from 'h3'; + export function clearRefreshTokenCookie(event: H3Event) { deleteCookie(event, 'jwt', { httpOnly: true, diff --git a/apps/backend-mock/utils/jwt-utils.ts b/apps/backend-mock/utils/jwt-utils.ts index 8cfc6843..71858307 100644 --- a/apps/backend-mock/utils/jwt-utils.ts +++ b/apps/backend-mock/utils/jwt-utils.ts @@ -1,8 +1,11 @@ import type { EventHandlerRequest, H3Event } from 'h3'; +import type { UserInfo } from './mock-data'; + +import { getHeader } from 'h3'; import jwt from 'jsonwebtoken'; -import { UserInfo } from './mock-data'; +import { MOCK_USERS } from './mock-data'; // TODO: Replace with your own secret key const ACCESS_TOKEN_SECRET = 'access_token_secret'; @@ -31,12 +34,22 @@ export function verifyAccessToken( return null; } - const token = authHeader.split(' ')[1]; + const tokenParts = authHeader.split(' '); + if (tokenParts.length !== 2) { + return null; + } + const token = tokenParts[1] as string; try { - const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload; + const decoded = jwt.verify( + token, + ACCESS_TOKEN_SECRET, + ) as unknown as UserPayload; const username = decoded.username; const user = MOCK_USERS.find((item) => item.username === username); + if (!user) { + return null; + } const { password: _pwd, ...userinfo } = user; return userinfo; } catch { @@ -50,7 +63,12 @@ export function verifyRefreshToken( try { const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload; const username = decoded.username; - const user = MOCK_USERS.find((item) => item.username === username); + const user = MOCK_USERS.find( + (item) => item.username === username, + ) as UserInfo; + if (!user) { + return null; + } const { password: _pwd, ...userinfo } = user; return userinfo; } catch { diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts index 192f30a0..82a50dd6 100644 --- a/apps/backend-mock/utils/mock-data.ts +++ b/apps/backend-mock/utils/mock-data.ts @@ -7,6 +7,11 @@ export interface UserInfo { homePath?: string; } +export interface TimezoneOption { + offset: number; + timezone: string; +} + export const MOCK_USERS: UserInfo[] = [ { id: 0, @@ -14,6 +19,7 @@ export const MOCK_USERS: UserInfo[] = [ realName: 'Vben', roles: ['super'], username: 'vben', + homePath: '/dashboard/workspace', }, { id: 1, @@ -21,7 +27,7 @@ export const MOCK_USERS: UserInfo[] = [ realName: 'Admin', roles: ['admin'], username: 'admin', - homePath: '/workspace', + homePath: '/dashboard/workspace', }, { id: 2, @@ -29,7 +35,7 @@ export const MOCK_USERS: UserInfo[] = [ realName: 'Jack', roles: ['user'], username: 'jack', - homePath: '/analytics', + homePath: '/dashboard/analytics', }, ]; @@ -59,11 +65,11 @@ const dashboardMenus = [ }, name: 'Dashboard', path: '/dashboard', - redirect: '/analytics', + redirect: '/dashboard/analytics', children: [ { name: 'Analytics', - path: '/analytics', + path: 'analytics', component: '/dashboard/analytics/index', meta: { affixTab: true, @@ -72,7 +78,7 @@ const dashboardMenus = [ }, { name: 'Workspace', - path: '/workspace', + path: 'workspace', component: '/dashboard/workspace/index', meta: { title: 'page.dashboard.workspace', @@ -91,7 +97,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { title: 'demos.access.adminVisible', }, name: 'AccessAdminVisibleDemo', - path: '/demos/access/admin-visible', + path: 'admin-visible', }, super: { component: '/demos/access/super-visible', @@ -100,7 +106,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { title: 'demos.access.superVisible', }, name: 'AccessSuperVisibleDemo', - path: '/demos/access/super-visible', + path: 'super-visible', }, user: { component: '/demos/access/user-visible', @@ -109,7 +115,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { title: 'demos.access.userVisible', }, name: 'AccessUserVisibleDemo', - path: '/demos/access/user-visible', + path: 'user-visible', }, }; @@ -127,7 +133,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { children: [ { name: 'AccessDemos', - path: '/demosaccess', + path: 'access', meta: { icon: 'mdi:cloud-key-outline', title: 'demos.access.backendPermissions', @@ -136,7 +142,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { children: [ { name: 'AccessPageControlDemo', - path: '/demos/access/page-control', + path: 'page-control', component: '/demos/access/index', meta: { icon: 'mdi:page-previous-outline', @@ -145,7 +151,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { }, { name: 'AccessButtonControlDemo', - path: '/demos/access/button-control', + path: 'button-control', component: '/demos/access/button-control', meta: { icon: 'mdi:button-cursor', @@ -154,7 +160,7 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => { }, { name: 'AccessMenuVisible403Demo', - path: '/demos/access/menu-visible-403', + path: 'menu-visible-403', component: '/demos/access/menu-visible-403', meta: { authority: ['no-body'], @@ -189,18 +195,46 @@ export const MOCK_MENUS = [ export const MOCK_MENU_LIST = [ { id: 1, - name: 'Workspace', + name: 'Dashboard', status: 1, - type: 'menu', - icon: 'mdi:dashboard', - path: '/workspace', - component: '/dashboard/workspace/index', + type: 'catalog', + icon: 'lucide:layout-dashboard', + path: '/dashboard', meta: { - icon: 'carbon:workspace', - title: 'page.dashboard.workspace', - affixTab: true, - order: 0, + icon: 'lucide:layout-dashboard', + order: -1, + title: 'page.dashboard.title', }, + children: [ + { + id: 101, + pid: 1, + status: 1, + type: 'menu', + name: 'Analytics', + path: 'analytics', + component: '/dashboard/analytics/index', + meta: { + affixTab: true, + icon: 'lucide:area-chart', + title: 'page.dashboard.analytics', + keepAlive: true, + }, + }, + { + id: 102, + pid: 1, + status: 1, + type: 'menu', + name: 'Workspace', + path: 'workspace', + component: '/views/dashboard/workspace/index', + meta: { + icon: 'carbon:workspace', + title: 'page.dashboard.workspace', + }, + }, + ], }, { id: 2, @@ -276,7 +310,7 @@ export const MOCK_MENU_LIST = [ children: [ { id: 20_401, - pid: 201, + pid: 202, name: 'SystemDeptCreate', status: 1, type: 'button', @@ -285,7 +319,7 @@ export const MOCK_MENU_LIST = [ }, { id: 20_402, - pid: 201, + pid: 202, name: 'SystemDeptEdit', status: 1, type: 'button', @@ -294,7 +328,7 @@ export const MOCK_MENU_LIST = [ }, { id: 20_403, - pid: 201, + pid: 202, name: 'SystemDeptDelete', status: 1, type: 'button', @@ -388,3 +422,29 @@ export function getMenuIds(menus: any[]) { }); return ids; } + +/** + * 时区选项 + */ +export const TIME_ZONE_OPTIONS: TimezoneOption[] = [ + { + offset: -5, + timezone: 'America/New_York', + }, + { + offset: 0, + timezone: 'Europe/London', + }, + { + offset: 8, + timezone: 'Asia/Shanghai', + }, + { + offset: 9, + timezone: 'Asia/Tokyo', + }, + { + offset: 9, + timezone: 'Asia/Seoul', + }, +]; diff --git a/apps/backend-mock/utils/response.ts b/apps/backend-mock/utils/response.ts index 2a5a908f..2d4242e9 100644 --- a/apps/backend-mock/utils/response.ts +++ b/apps/backend-mock/utils/response.ts @@ -1,5 +1,7 @@ import type { EventHandlerRequest, H3Event } from 'h3'; +import { setResponseStatus } from 'h3'; + export function useResponseSuccess(data: T) { return { code: 0, diff --git a/apps/backend-mock/utils/timezone-utils.ts b/apps/backend-mock/utils/timezone-utils.ts new file mode 100644 index 00000000..da35f920 --- /dev/null +++ b/apps/backend-mock/utils/timezone-utils.ts @@ -0,0 +1,9 @@ +let mockTimeZone: null | string = null; + +export const setTimezone = (timeZone: string) => { + mockTimeZone = timeZone; +}; + +export const getTimezone = () => { + return mockTimeZone; +}; diff --git a/apps/web-antd/index.html b/apps/web-antd/index.html index 480eb84d..e3b9ae5c 100644 --- a/apps/web-antd/index.html +++ b/apps/web-antd/index.html @@ -12,7 +12,7 @@ content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" /> - <%= VITE_APP_TITLE %> + %VITE_APP_TITLE%