diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index e8dc9ed9..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "recommendations": [ - // Vue 3 的语言支持 - "Vue.volar", - // 将 ESLint JavaScript 集成到 VS Code 中。 - "dbaeumer.vscode-eslint", - // Visual Studio Code 的官方 Stylelint 扩展 - "stylelint.vscode-stylelint", - // 使用 Prettier 的代码格式化程序 - "esbenp.prettier-vscode", - // 支持 dotenv 文件语法 - "mikestead.dotenv", - // 源代码的拼写检查器 - "streetsidesoftware.code-spell-checker", - // Tailwind CSS 的官方 VS Code 插件 - "bradlc.vscode-tailwindcss", - // iconify 图标插件 - "antfu.iconify", - // i18n 插件 - "Lokalise.i18n-ally", - // CSS 变量提示 - "vunguyentuan.vscode-css-variables", - // 在 package.json 中显示 PNPM catalog 的版本 - "antfu.pnpm-catalog-lens" - ], - "unwantedRecommendations": [ - // 和 volar 冲突 - "octref.vetur" - ] -} diff --git a/.vscode/global.code-snippets b/.vscode/global.code-snippets deleted file mode 100644 index 7604b014..00000000 --- a/.vscode/global.code-snippets +++ /dev/null @@ -1,37 +0,0 @@ -{ - "import": { - "scope": "javascript,typescript", - "prefix": "im", - "body": ["import { $2 } from '$1';"], - "description": "Import a module", - }, - "export-all": { - "scope": "javascript,typescript", - "prefix": "ex", - "body": ["export * from '$1';"], - "description": "Export a module", - }, - "vue-script-setup": { - "scope": "vue", - "prefix": "", - "const props = defineProps<{", - " modelValue?: boolean,", - "}>()", - "$1", - "", - "", - "", - ], - }, - "vue-computed": { - "scope": "javascript,typescript,vue", - "prefix": "com", - "body": ["computed(() => { $1 })"], - }, -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e9673304..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "name": "vben admin playground dev", - "request": "launch", - "url": "http://localhost:5555", - "env": { "NODE_ENV": "development" }, - "sourceMaps": true, - "webRoot": "${workspaceFolder}/playground" - }, - { - "type": "chrome", - "name": "vben admin antd dev", - "request": "launch", - "url": "http://localhost:5666", - "env": { "NODE_ENV": "development" }, - "sourceMaps": true, - "webRoot": "${workspaceFolder}/apps/web-antd" - }, - { - "type": "chrome", - "name": "vben admin ele dev", - "request": "launch", - "url": "http://localhost:5777", - "env": { "NODE_ENV": "development" }, - "sourceMaps": true, - "webRoot": "${workspaceFolder}/apps/web-ele" - }, - { - "type": "chrome", - "name": "vben admin naive dev", - "request": "launch", - "url": "http://localhost:5888", - "env": { "NODE_ENV": "development" }, - "sourceMaps": true, - "webRoot": "${workspaceFolder}/apps/web-naive" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 8b76b276..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,227 +0,0 @@ -{ - "tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts", - // workbench - "workbench.list.smoothScrolling": true, - "workbench.startupEditor": "newUntitledFile", - "workbench.tree.indent": 10, - "workbench.editor.highlightModifiedTabs": true, - "workbench.editor.closeOnFileDelete": true, - "workbench.editor.limit.enabled": true, - "workbench.editor.limit.perEditorGroup": true, - "workbench.editor.limit.value": 5, - - // editor - "editor.tabSize": 2, - "editor.detectIndentation": false, - "editor.cursorBlinking": "expand", - "editor.largeFileOptimizations": false, - "editor.accessibilitySupport": "off", - "editor.cursorSmoothCaretAnimation": "on", - "editor.guides.bracketPairs": "active", - "editor.inlineSuggest.enabled": true, - "editor.suggestSelection": "recentlyUsedByPrefix", - "editor.acceptSuggestionOnEnter": "smart", - "editor.suggest.snippetsPreventQuickSuggestions": false, - "editor.stickyScroll.enabled": true, - "editor.hover.sticky": true, - "editor.suggest.insertMode": "replace", - "editor.bracketPairColorization.enabled": true, - "editor.autoClosingBrackets": "beforeWhitespace", - "editor.autoClosingDelete": "always", - "editor.autoClosingOvertype": "always", - "editor.autoClosingQuotes": "beforeWhitespace", - "editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", - "source.fixAll.stylelint": "explicit", - "source.organizeImports": "never" - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[html]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[css]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[scss]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[vue]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - // extensions - "extensions.ignoreRecommendations": true, - - // terminal - "terminal.integrated.cursorBlinking": true, - "terminal.integrated.persistentSessionReviveProcess": "never", - "terminal.integrated.tabs.enabled": true, - "terminal.integrated.scrollback": 10000, - "terminal.integrated.stickyScroll.enabled": true, - - // files - "files.eol": "\n", - "files.insertFinalNewline": true, - "files.simpleDialog.enable": true, - "files.associations": { - "*.ejs": "html", - "*.art": "html", - "**/tsconfig.json": "jsonc", - "*.json": "jsonc", - "package.json": "json" - }, - - "files.exclude": { - "**/.eslintcache": true, - "**/bower_components": true, - "**/.turbo": true, - "**/.idea": true, - "**/tmp": true, - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.stylelintcache": true, - "**/.DS_Store": true, - "**/vite.config.mts.*": true, - "**/tea.yaml": true - }, - "files.watcherExclude": { - "**/.git/objects/**": true, - "**/.git/subtree-cache/**": true, - "**/.vscode/**": true, - "**/node_modules/**": true, - "**/tmp/**": true, - "**/bower_components/**": true, - "**/dist/**": true, - "**/yarn.lock": true - }, - - // search - "search.searchEditor.singleClickBehaviour": "peekDefinition", - "search.followSymlinks": false, - // 在使用搜索功能时,将这些文件夹/文件排除在外 - "search.exclude": { - "**/node_modules": true, - "**/*.log": true, - "**/*.log*": true, - "**/bower_components": true, - "**/dist": true, - "**/elehukouben": true, - "**/.git": true, - "**/.github": true, - "**/.gitignore": true, - "**/.svn": true, - "**/.DS_Store": true, - "**/.vitepress/cache": true, - "**/.idea": true, - "**/.vscode": false, - "**/.yarn": true, - "**/tmp": true, - "*.xml": true, - "out": true, - "dist": true, - "node_modules": true, - "CHANGELOG.md": true, - "**/pnpm-lock.yaml": true, - "**/yarn.lock": true - }, - - "debug.onTaskErrors": "debugAnyway", - "diffEditor.ignoreTrimWhitespace": false, - "npm.packageManager": "pnpm", - - "css.validate": false, - "less.validate": false, - "scss.validate": false, - - // extension - "emmet.showSuggestionsAsSnippets": true, - "emmet.triggerExpansionOnTab": false, - - "errorLens.enabledDiagnosticLevels": ["warning", "error"], - "errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"], - - "stylelint.enable": true, - "stylelint.packageManager": "pnpm", - "stylelint.validate": ["css", "less", "postcss", "scss", "vue"], - "stylelint.customSyntax": "postcss-html", - "stylelint.snippet": ["css", "less", "postcss", "scss", "vue"], - - "typescript.inlayHints.enumMemberValues.enabled": true, - "typescript.preferences.preferTypeOnlyAutoImports": true, - "typescript.preferences.includePackageJsonAutoImports": "on", - - "eslint.validate": [ - "javascript", - "typescript", - "javascriptreact", - "typescriptreact", - "vue", - "html", - "markdown", - "json", - "jsonc", - "json5" - ], - - "tailwindCSS.experimental.classRegex": [ - ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] - ], - - "github.copilot.enable": { - "*": true, - "markdown": true, - "plaintext": false, - "yaml": false - }, - - "cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"], - - "i18n-ally.localesPaths": [ - "packages/locales/src/langs", - "playground/src/locales/langs", - "apps/*/src/locales/langs" - ], - "i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}", - "i18n-ally.enabledParsers": ["json"], - "i18n-ally.sourceLanguage": "en", - "i18n-ally.displayLanguage": "zh-CN", - "i18n-ally.enabledFrameworks": ["vue", "react"], - "i18n-ally.keystyle": "nested", - "i18n-ally.sortKeys": true, - "i18n-ally.namespace": true, - - // 控制相关文件嵌套展示 - "explorer.fileNesting.enabled": true, - "explorer.fileNesting.expand": false, - "explorer.fileNesting.patterns": { - "*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts", - "*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts", - "*.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", - "tailwind.config.mjs": "postcss.*" - }, - "commentTranslate.hover.enabled": false, - "commentTranslate.multiLineMerge": true, - "vue.server.hybridMode": true, - "typescript.tsdk": "node_modules/typescript/lib", - "oxc.enable": false -} diff --git a/README.ja-JP.md b/README.ja-JP.md deleted file mode 100644 index baa4cc44..00000000 --- a/README.ja-JP.md +++ /dev/null @@ -1,153 +0,0 @@ -
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) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) - -**日本語** | [English](./README.md) | [中文](./README.zh-CN.md) - -## 紹介 - -Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術を使用して開発された、無料でオープンソースの中・後端テンプレートです。すぐに使える中・後端のフロントエンドソリューションとして、学習の参考にもなります。 - -## アップグレード通知 - -これは最新バージョン5.0であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。 - -## 特徴 - -- **最新技術スタック**: Vue 3やViteなどの最先端フロントエンド技術で開発 -- **TypeScript**: アプリケーション規模のJavaScriptのための言語 -- **テーマ**: 複数のテーマカラーが利用可能で、カスタマイズオプションも豊富 -- **国際化**: 完全な内蔵国際化サポート -- **権限管理**: 動的ルートベースの権限生成ソリューションを内蔵 - -## プレビュー - -- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト - -テストアカウント: vben/123456 - -

- VbenAdmin Logo - VbenAdmin Logo - VbenAdmin Logo -

- -### Gitpodを使用 - -Gitpod(GitHub用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始します。 - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin) - -## ドキュメント - -[ドキュメント](https://doc.vben.pro/) - -## インストールと使用 - -- プロジェクトコードを取得 - -```bash -git clone https://github.com/vbenjs/vue-vben-admin.git -``` - -- 依存関係のインストール - -```bash -cd vue-vben-admin - -corepack enable - -pnpm install - -``` - -- 実行 - -```bash -pnpm dev -``` - -- ビルド - -```bash -pnpm build -``` - -## 変更ログ - -[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases) - -## 貢献方法 - -ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。 - -**Pull Request:** - -1. コードをフォーク! -2. 自分のブランチを作成: `git checkout -b feat/xxxx` -3. 変更をコミット: `git commit -am 'feat(function): add xxxxx'` -4. ブランチをプッシュ: `git push origin feat/xxxx` -5. `pull request`を送信 - -## Git貢献提出規則 - -- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) - - - `feat` 新機能の追加 - - `fix` 問題/バグの修正 - - `style` コードスタイルに関連し、実行結果に影響しない - - `perf` 最適化/パフォーマンス向上 - - `refactor` リファクタリング - - `revert` 変更の取り消し - - `test` テスト関連 - - `docs` ドキュメント/注釈 - - `chore` 依存関係の更新/スキャフォールディング設定の変更など - - `ci` 継続的インテグレーション - - `types` 型定義ファイルの変更 - - `wip` 開発中 - -## ブラウザサポート - -ローカル開発には`Chrome 80+`ブラウザを推奨します - -モダンブラウザをサポートし、IEはサポートしません - -| [ Edge](http://godban.github.io/browsers-support-badges/)
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 | -| :-: | :-: | :-: | :-: | :-: | -| サポートしない | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン | - -## メンテナー - -[@Vben](https://github.com/anncwb) - -## スター歴史 - -[![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](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png) - -Paypal Me - -## 貢献者 - - - Contributors - - -## Discord - -- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions) - -## ライセンス - -[MIT © Vben-2020](./LICENSE) diff --git a/README.md b/README.md index e84c8392..cc951b47 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,7 @@ -
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) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) - -**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). - -## Feature - -- **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 - -- Get the project code - -```bash -git clone https://github.com/vbenjs/vue-vben-admin.git +## 按钮权限 +使用这种方式来授权 +```vue +v-access:code="['Admin'] ``` - -- Installation dependencies - -```bash -cd vue-vben-admin - -corepack enable - -pnpm install -``` - -- run - -```bash -pnpm dev -``` - -- 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:** - -1. Fork code! -2. Create your own 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 - - `wip` In development - -## Browser support - -The `Chrome 80+` browser is recommended for local development - -Support modern browsers, not IE - -| [ Edge](http://godban.github.io/browsers-support-badges/)
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 | -| :-: | :-: | :-: | :-: | :-: | -| not support | 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 - -## Contributor - - - 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 deleted file mode 100644 index e2f05bb9..00000000 --- a/README.zh-CN.md +++ /dev/null @@ -1,152 +0,0 @@ -
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) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg) - -**中文** | [English](./README.md) | [日本語](./README.ja-JP.md) - -## 简介 - -Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的中后台模板,它采用了最新的 Vue 3、Vite、TypeScript 等主流技术开发,开箱即用,可用于中后台前端开发,也适合学习参考。 - -## 升级提示 - -该版本为最新版本`5.0`, 与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2) - -## 特性 - -- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发 -- **TypeScript**: 应用程序级 JavaScript 的语言 -- **主题**:提供多套主题色彩,可配置自定义主题 -- **国际化**:内置完善的国际化方案 -- **权限** 内置完善的动态路由权限生成方案 - -## 预览 - -- [Vben Admin](https://vben.pro/) - 完整版中文站点 - -测试账号: vben/123456 - -

- VbenAdmin Logo - VbenAdmin Logo - VbenAdmin Logo -

- -### 使用 Gitpod - -在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码. - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin) - -## 文档 - -[文档地址](https://doc.vben.pro/) - -## 安装使用 - -- 获取项目代码 - -```bash -git clone https://github.com/vbenjs/vue-vben-admin.git -``` - -- 安装依赖 - -```bash -cd vue-vben-admin - -corepack enable - -pnpm install -``` - -- 运行 - -```bash -pnpm dev -``` - -- 打包 - -```bash -pnpm build -``` - -## 更新日志 - -[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases) - -## 如何贡献 - -非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。 - -**Pull Request:** - -1. Fork 代码! -2. 创建自己的分支: `git checkout -b feature/xxxx` -3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'` -4. 推送您的分支: `git push origin feature/xxxx` -5. 提交`pull request` - -## Git 贡献提交规范 - -- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) - - - `feat` 增加新功能 - - `fix` 修复问题/BUG - - `style` 代码风格相关无影响运行结果的 - - `perf` 优化/性能提升 - - `refactor` 重构 - - `revert` 撤销修改 - - `test` 测试相关 - - `docs` 文档/注释 - - `chore` 依赖更新/脚手架配置修改等 - - `ci` 持续集成 - - `types` 类型定义文件更改 - - `wip` 开发中 - -## 浏览器支持 - -本地开发推荐使用`Chrome 80+` 浏览器 - -支持现代浏览器, 不支持 IE - -| [ Edge](http://godban.github.io/browsers-support-badges/)
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 | -| :-: | :-: | :-: | :-: | :-: | -| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | - -## 维护者 - -[@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](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png) - -Paypal Me - -## Contributor - - - Contributors - - -## Discord - -- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions) - -## License - -[MIT © Vben-2020](./LICENSE) diff --git a/apps/backend-mock/README.md b/apps/backend-mock/README.md deleted file mode 100644 index 401bda76..00000000 --- a/apps/backend-mock/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# @vben/backend-mock - -## Description - -Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。 - -## Running the app - -```bash -# development -$ pnpm run start - -# production mode -$ pnpm run build -``` diff --git a/apps/backend-mock/api/auth/codes.ts b/apps/backend-mock/api/auth/codes.ts deleted file mode 100644 index 7ba01270..00000000 --- a/apps/backend-mock/api/auth/codes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; - -export default eventHandler((event) => { - const userinfo = verifyAccessToken(event); - if (!userinfo) { - return unAuthorizedResponse(event); - } - - const codes = - MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? []; - - return useResponseSuccess(codes); -}); diff --git a/apps/backend-mock/api/auth/login.post.ts b/apps/backend-mock/api/auth/login.post.ts deleted file mode 100644 index df5737a2..00000000 --- a/apps/backend-mock/api/auth/login.post.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - clearRefreshTokenCookie, - setRefreshTokenCookie, -} from '~/utils/cookie-utils'; -import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils'; -import { forbiddenResponse } from '~/utils/response'; - -export default defineEventHandler(async (event) => { - const { password, username } = await readBody(event); - if (!password || !username) { - setResponseStatus(event, 400); - return useResponseError( - 'BadRequestException', - 'Username and password are required', - ); - } - - const findUser = MOCK_USERS.find( - (item) => item.username === username && item.password === password, - ); - - if (!findUser) { - clearRefreshTokenCookie(event); - return forbiddenResponse(event, 'Username or password is incorrect.'); - } - - const accessToken = generateAccessToken(findUser); - const refreshToken = generateRefreshToken(findUser); - - setRefreshTokenCookie(event, refreshToken); - - return useResponseSuccess({ - ...findUser, - accessToken, - }); -}); diff --git a/apps/backend-mock/api/auth/logout.post.ts b/apps/backend-mock/api/auth/logout.post.ts deleted file mode 100644 index ac6afe94..00000000 --- a/apps/backend-mock/api/auth/logout.post.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - clearRefreshTokenCookie, - getRefreshTokenFromCookie, -} from '~/utils/cookie-utils'; - -export default defineEventHandler(async (event) => { - const refreshToken = getRefreshTokenFromCookie(event); - if (!refreshToken) { - return useResponseSuccess(''); - } - - clearRefreshTokenCookie(event); - - return useResponseSuccess(''); -}); diff --git a/apps/backend-mock/api/auth/refresh.post.ts b/apps/backend-mock/api/auth/refresh.post.ts deleted file mode 100644 index 7df4d34f..00000000 --- a/apps/backend-mock/api/auth/refresh.post.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - clearRefreshTokenCookie, - getRefreshTokenFromCookie, - setRefreshTokenCookie, -} from '~/utils/cookie-utils'; -import { verifyRefreshToken } from '~/utils/jwt-utils'; -import { forbiddenResponse } from '~/utils/response'; - -export default defineEventHandler(async (event) => { - const refreshToken = getRefreshTokenFromCookie(event); - if (!refreshToken) { - return forbiddenResponse(event); - } - - clearRefreshTokenCookie(event); - - const userinfo = verifyRefreshToken(refreshToken); - if (!userinfo) { - return forbiddenResponse(event); - } - - const findUser = MOCK_USERS.find( - (item) => item.username === userinfo.username, - ); - if (!findUser) { - return forbiddenResponse(event); - } - const accessToken = generateAccessToken(findUser); - - setRefreshTokenCookie(event, refreshToken); - - return accessToken; -}); diff --git a/apps/backend-mock/api/status.ts b/apps/backend-mock/api/status.ts deleted file mode 100644 index 41773e1d..00000000 --- a/apps/backend-mock/api/status.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default eventHandler((event) => { - const { status } = getQuery(event); - setResponseStatus(event, Number(status)); - return useResponseError(`${status}`); -}); diff --git a/apps/backend-mock/api/table/list.ts b/apps/backend-mock/api/table/list.ts deleted file mode 100644 index 55b88eaa..00000000 --- a/apps/backend-mock/api/table/list.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; - -function generateMockDataList(count: number) { - const dataList = []; - - for (let i = 0; i < count; i++) { - const dataItem = { - id: faker.string.uuid(), - imageUrl: faker.image.avatar(), - imageUrl2: faker.image.avatar(), - open: faker.datatype.boolean(), - status: faker.helpers.arrayElement(['success', 'error', 'warning']), - productName: faker.commerce.productName(), - price: faker.commerce.price(), - currency: faker.finance.currencyCode(), - quantity: faker.number.int({ min: 1, max: 100 }), - available: faker.datatype.boolean(), - category: faker.commerce.department(), - releaseDate: faker.date.past(), - rating: faker.number.float({ min: 1, max: 5 }), - description: faker.commerce.productDescription(), - weight: faker.number.float({ min: 0.1, max: 10 }), - color: faker.color.human(), - inProduction: faker.datatype.boolean(), - tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()), - }; - - dataList.push(dataItem); - } - - return dataList; -} - -const mockData = generateMockDataList(100); - -export default eventHandler(async (event) => { - const userinfo = verifyAccessToken(event); - if (!userinfo) { - return unAuthorizedResponse(event); - } - - await sleep(600); - - const { page, pageSize, sortBy, sortOrder } = getQuery(event); - const listData = structuredClone(mockData); - if (sortBy && Reflect.has(listData[0], sortBy as string)) { - listData.sort((a, b) => { - if (sortOrder === 'asc') { - if (sortBy === 'price') { - return ( - Number.parseFloat(a[sortBy as string]) - - Number.parseFloat(b[sortBy as string]) - ); - } else { - return a[sortBy as string] > b[sortBy as string] ? 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; - } - } - }); - } - - return usePageResponseSuccess(page as string, pageSize as string, listData); -}); diff --git a/apps/backend-mock/api/test.get.ts b/apps/backend-mock/api/test.get.ts deleted file mode 100644 index ca4a500b..00000000 --- a/apps/backend-mock/api/test.get.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(() => 'Test get handler'); diff --git a/apps/backend-mock/api/test.post.ts b/apps/backend-mock/api/test.post.ts deleted file mode 100644 index 698cf211..00000000 --- a/apps/backend-mock/api/test.post.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(() => 'Test post handler'); diff --git a/apps/backend-mock/api/user/info.ts b/apps/backend-mock/api/user/info.ts deleted file mode 100644 index cfa2346c..00000000 --- a/apps/backend-mock/api/user/info.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { verifyAccessToken } from '~/utils/jwt-utils'; -import { unAuthorizedResponse } from '~/utils/response'; - -export default eventHandler((event) => { - const userinfo = verifyAccessToken(event); - if (!userinfo) { - return unAuthorizedResponse(event); - } - return useResponseSuccess(userinfo); -}); diff --git a/apps/backend-mock/error.ts b/apps/backend-mock/error.ts deleted file mode 100644 index e20beac4..00000000 --- a/apps/backend-mock/error.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NitroErrorHandler } from 'nitropack'; - -const errorHandler: NitroErrorHandler = function (error, event) { - event.node.res.end(`[Error Handler] ${error.stack}`); -}; - -export default errorHandler; diff --git a/apps/backend-mock/nitro.config.ts b/apps/backend-mock/nitro.config.ts deleted file mode 100644 index c2d7297f..00000000 --- a/apps/backend-mock/nitro.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import errorHandler from './error'; - -process.env.COMPATIBILITY_DATE = new Date().toISOString(); -export default defineNitroConfig({ - devErrorHandler: errorHandler, - errorHandler: '~/error', - routeRules: { - '/api/**': { - cors: true, - headers: { - 'Access-Control-Allow-Credentials': 'true', - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Expose-Headers': '*', - }, - }, - }, -}); diff --git a/apps/backend-mock/package.json b/apps/backend-mock/package.json deleted file mode 100644 index cc0b8d53..00000000 --- a/apps/backend-mock/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@vben/backend-mock", - "version": "0.0.1", - "description": "", - "private": true, - "license": "MIT", - "author": "", - "scripts": { - "build": "nitro build", - "start": "nitro dev" - }, - "dependencies": { - "@faker-js/faker": "catalog:", - "jsonwebtoken": "catalog:", - "nitropack": "catalog:" - }, - "devDependencies": { - "@types/jsonwebtoken": "catalog:", - "h3": "catalog:" - } -} diff --git a/apps/backend-mock/tsconfig.build.json b/apps/backend-mock/tsconfig.build.json deleted file mode 100644 index 64f86c6b..00000000 --- a/apps/backend-mock/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] -} diff --git a/apps/backend-mock/tsconfig.json b/apps/backend-mock/tsconfig.json deleted file mode 100644 index 43008af1..00000000 --- a/apps/backend-mock/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nitro/types/tsconfig.json" -} diff --git a/apps/backend-mock/utils/cookie-utils.ts b/apps/backend-mock/utils/cookie-utils.ts deleted file mode 100644 index 0d92f577..00000000 --- a/apps/backend-mock/utils/cookie-utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { EventHandlerRequest, H3Event } from 'h3'; - -export function clearRefreshTokenCookie(event: H3Event) { - deleteCookie(event, 'jwt', { - httpOnly: true, - sameSite: 'none', - secure: true, - }); -} - -export function setRefreshTokenCookie( - event: H3Event, - refreshToken: string, -) { - setCookie(event, 'jwt', refreshToken, { - httpOnly: true, - maxAge: 24 * 60 * 60 * 1000, - sameSite: 'none', - secure: true, - }); -} - -export function getRefreshTokenFromCookie(event: H3Event) { - const refreshToken = getCookie(event, 'jwt'); - return refreshToken; -} diff --git a/apps/backend-mock/utils/jwt-utils.ts b/apps/backend-mock/utils/jwt-utils.ts deleted file mode 100644 index 8cfc6843..00000000 --- a/apps/backend-mock/utils/jwt-utils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { EventHandlerRequest, H3Event } from 'h3'; - -import jwt from 'jsonwebtoken'; - -import { UserInfo } from './mock-data'; - -// TODO: Replace with your own secret key -const ACCESS_TOKEN_SECRET = 'access_token_secret'; -const REFRESH_TOKEN_SECRET = 'refresh_token_secret'; - -export interface UserPayload extends UserInfo { - iat: number; - exp: number; -} - -export function generateAccessToken(user: UserInfo) { - return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' }); -} - -export function generateRefreshToken(user: UserInfo) { - return jwt.sign(user, REFRESH_TOKEN_SECRET, { - expiresIn: '30d', - }); -} - -export function verifyAccessToken( - event: H3Event, -): null | Omit { - const authHeader = getHeader(event, 'Authorization'); - if (!authHeader?.startsWith('Bearer')) { - return null; - } - - const token = authHeader.split(' ')[1]; - try { - const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload; - - const username = decoded.username; - const user = MOCK_USERS.find((item) => item.username === username); - const { password: _pwd, ...userinfo } = user; - return userinfo; - } catch { - return null; - } -} - -export function verifyRefreshToken( - token: string, -): null | Omit { - 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 { password: _pwd, ...userinfo } = user; - return userinfo; - } catch { - return null; - } -} diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts deleted file mode 100644 index 057588e3..00000000 --- a/apps/backend-mock/utils/mock-data.ts +++ /dev/null @@ -1,189 +0,0 @@ -export interface UserInfo { - id: number; - password: string; - realName: string; - roles: string[]; - username: string; - homePath?: string; -} - -export const MOCK_USERS: UserInfo[] = [ - { - id: 0, - password: '123456', - realName: 'Vben', - roles: ['super'], - username: 'vben', - }, - { - id: 1, - password: '123456', - realName: 'Admin', - roles: ['admin'], - username: 'admin', - homePath: '/workspace', - }, - { - id: 2, - password: '123456', - realName: 'Jack', - roles: ['user'], - username: 'jack', - homePath: '/analytics', - }, -]; - -export const MOCK_CODES = [ - // super - { - codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'], - username: 'vben', - }, - { - // admin - codes: ['AC_100010', 'AC_100020', 'AC_100030'], - username: 'admin', - }, - { - // user - codes: ['AC_1000001', 'AC_1000002'], - username: 'jack', - }, -]; - -const dashboardMenus = [ - { - component: 'BasicLayout', - meta: { - order: -1, - title: 'page.dashboard.title', - }, - name: 'Dashboard', - path: '/', - redirect: '/analytics', - children: [ - { - name: 'Analytics', - path: '/analytics', - component: '/dashboard/analytics/index', - meta: { - affixTab: true, - title: 'page.dashboard.analytics', - }, - }, - { - name: 'Workspace', - path: '/workspace', - component: '/dashboard/workspace/index', - meta: { - title: 'page.dashboard.workspace', - }, - }, - ], - }, -]; - -const createDemosMenus = (role: 'admin' | 'super' | 'user') => { - const roleWithMenus = { - admin: { - component: '/demos/access/admin-visible', - meta: { - icon: 'mdi:button-cursor', - title: 'demos.access.adminVisible', - }, - name: 'AccessAdminVisibleDemo', - path: '/demos/access/admin-visible', - }, - super: { - component: '/demos/access/super-visible', - meta: { - icon: 'mdi:button-cursor', - title: 'demos.access.superVisible', - }, - name: 'AccessSuperVisibleDemo', - path: '/demos/access/super-visible', - }, - user: { - component: '/demos/access/user-visible', - meta: { - icon: 'mdi:button-cursor', - title: 'demos.access.userVisible', - }, - name: 'AccessUserVisibleDemo', - path: '/demos/access/user-visible', - }, - }; - - return [ - { - component: 'BasicLayout', - meta: { - icon: 'ic:baseline-view-in-ar', - keepAlive: true, - order: 1000, - title: 'demos.title', - }, - name: 'Demos', - path: '/demos', - redirect: '/demos/access', - children: [ - { - name: 'AccessDemos', - path: '/demosaccess', - meta: { - icon: 'mdi:cloud-key-outline', - title: 'demos.access.backendPermissions', - }, - redirect: '/demos/access/page-control', - children: [ - { - name: 'AccessPageControlDemo', - path: '/demos/access/page-control', - component: '/demos/access/index', - meta: { - icon: 'mdi:page-previous-outline', - title: 'demos.access.pageAccess', - }, - }, - { - name: 'AccessButtonControlDemo', - path: '/demos/access/button-control', - component: '/demos/access/button-control', - meta: { - icon: 'mdi:button-cursor', - title: 'demos.access.buttonControl', - }, - }, - { - name: 'AccessMenuVisible403Demo', - path: '/demos/access/menu-visible-403', - component: '/demos/access/menu-visible-403', - meta: { - authority: ['no-body'], - icon: 'mdi:button-cursor', - menuVisibleWithForbidden: true, - title: 'demos.access.menuVisible403', - }, - }, - roleWithMenus[role], - ], - }, - ], - }, - ]; -}; - -export const MOCK_MENUS = [ - { - menus: [...dashboardMenus, ...createDemosMenus('super')], - username: 'vben', - }, - { - menus: [...dashboardMenus, ...createDemosMenus('admin')], - username: 'admin', - }, - { - menus: [...dashboardMenus, ...createDemosMenus('user')], - username: 'jack', - }, -]; diff --git a/apps/backend-mock/utils/response.ts b/apps/backend-mock/utils/response.ts deleted file mode 100644 index 2a5a908f..00000000 --- a/apps/backend-mock/utils/response.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { EventHandlerRequest, H3Event } from 'h3'; - -export function useResponseSuccess(data: T) { - return { - code: 0, - data, - error: null, - message: 'ok', - }; -} - -export function usePageResponseSuccess( - page: number | string, - pageSize: number | string, - list: T[], - { message = 'ok' } = {}, -) { - const pageData = pagination( - Number.parseInt(`${page}`), - Number.parseInt(`${pageSize}`), - list, - ); - - return { - ...useResponseSuccess({ - items: pageData, - total: list.length, - }), - message, - }; -} - -export function useResponseError(message: string, error: any = null) { - return { - code: -1, - data: null, - error, - message, - }; -} - -export function forbiddenResponse( - event: H3Event, - message = 'Forbidden Exception', -) { - setResponseStatus(event, 403); - return useResponseError(message, message); -} - -export function unAuthorizedResponse(event: H3Event) { - setResponseStatus(event, 401); - return useResponseError('Unauthorized Exception', 'Unauthorized Exception'); -} - -export function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -export function pagination( - pageNo: number, - pageSize: number, - array: T[], -): T[] { - const offset = (pageNo - 1) * Number(pageSize); - return offset + Number(pageSize) >= array.length - ? array.slice(offset) - : array.slice(offset, offset + Number(pageSize)); -} diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index bbc5593f..981d0038 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -26,6 +26,7 @@ "#/*": "./src/*" }, "dependencies": { + "@ant-design/icons-vue": "^7.0.1", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", "@vben/constants": "workspace:*", diff --git a/apps/web-antd/public/favicon.ico b/apps/web-antd/public/favicon.ico index fcf9818e..7709a74d 100644 Binary files a/apps/web-antd/public/favicon.ico and b/apps/web-antd/public/favicon.ico differ diff --git a/apps/web-antd/public/img/logo.png b/apps/web-antd/public/img/logo.png new file mode 100644 index 00000000..8d7f47ae Binary files /dev/null and b/apps/web-antd/public/img/logo.png differ diff --git a/apps/web-antd/public/img/user-default-avatar.png b/apps/web-antd/public/img/user-default-avatar.png new file mode 100644 index 00000000..4f9ebc58 Binary files /dev/null and b/apps/web-antd/public/img/user-default-avatar.png differ diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index a43a8280..3ffcbdb5 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -36,6 +36,8 @@ import { Upload, } from 'ant-design-vue'; +import { registerComponent } from '#/components/form/component-map'; + const withDefaultPlaceholder = ( component: T, type: 'input' | 'select', @@ -149,6 +151,8 @@ async function initComponentAdapter() { Upload, }; + // 自动注册自定义组件 + registerComponent(components); // 将组件注册到全局共享状态中 globalShareState.setComponents(components); diff --git a/apps/web-antd/src/api/core/menu.ts b/apps/web-antd/src/api/core/menu.ts index 9ef60b11..391dac5c 100644 --- a/apps/web-antd/src/api/core/menu.ts +++ b/apps/web-antd/src/api/core/menu.ts @@ -6,5 +6,5 @@ import { requestClient } from '#/api/request'; * 获取用户所有菜单 */ export async function getAllMenusApi() { - return requestClient.get('/menu/all'); + return requestClient.get('auth/menu'); } diff --git a/apps/web-antd/src/api/core/upload.ts b/apps/web-antd/src/api/core/upload.ts new file mode 100644 index 00000000..c57aae95 --- /dev/null +++ b/apps/web-antd/src/api/core/upload.ts @@ -0,0 +1,17 @@ +import { requestClient } from '#/api/request'; + +/** + * 文件上传 + * @param data + */ +export async function uploadFile(data: any) { + return requestClient.upload('upload/image', data); +} + +/** + * 通过文件id集合获取文件信息 + * @param data + */ +export async function getFileInfoByIds(data: any) { + return requestClient.post('/sys/fileInfo/getFileInfoByIds', data); +} diff --git a/apps/web-antd/src/api/core/user.ts b/apps/web-antd/src/api/core/user.ts index 7e28ea84..53d56027 100644 --- a/apps/web-antd/src/api/core/user.ts +++ b/apps/web-antd/src/api/core/user.ts @@ -6,5 +6,5 @@ import { requestClient } from '#/api/request'; * 获取用户信息 */ export async function getUserInfoApi() { - return requestClient.get('/user/info'); + return requestClient.get('auth/my-info'); } diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 67ef35e4..f8ae816a 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -17,9 +17,25 @@ import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; import { refreshTokenApi } from './core'; +import {Base64} from "js-base64"; const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); +const arr = new Set([ + 'accept_tel', + 'doctor', + 'express_mobile', + 'id_card', + 'idcard', + 'mobile', + 'password', + 'patient_mobile', + 'phone', +]); + +// 敏感数据 +const sensitiveData = new Set(['id_card', 'idcard']); + function createRequestClient(baseURL: string) { const client = new RequestClient({ baseURL, @@ -65,6 +81,15 @@ function createRequestClient(baseURL: string) { config.headers.Authorization = formatToken(accessStore.accessToken); config.headers['Accept-Language'] = preferences.app.locale; + // 加密 + // eslint-disable-next-line eqeqeq + if (config.data != null) { + config.data = getRes(config.data, false); + } + // eslint-disable-next-line eqeqeq + if (config.params != null) { + config.params = getRes(config.params, false); + } return config; }, }); @@ -74,9 +99,29 @@ function createRequestClient(baseURL: string) { fulfilled: (response) => { const { data: responseData, status } = response; - const { code, data } = responseData; - if (status >= 200 && status < 400 && code === 0) { - return data; + // 这里吧 data 换成了 result + const { code, result } = responseData; + // 判断返回状态码 + if (status >= 200 && status < 400) { + switch (code) { + case 0: { + // 解密 + // eslint-disable-next-line eqeqeq + if (result != null) { + return getRes(result); + } + return result; + } + case 401: { + // 401 错误,需要重新登录 + return doReAuthenticate(); + } + case 403: + case 404: + case 500: { + throw Object.assign({}, response, { response }); + } + } } throw Object.assign({}, response, { response }); @@ -112,3 +157,119 @@ function createRequestClient(baseURL: string) { export const requestClient = createRequestClient(apiURL); export const baseRequestClient = new RequestClient({ baseURL: apiURL }); + +/** + * 加解密方法 + * @param obj + * @param isDecode + */ +function getRes(obj: any, isDecode = true) { + try { + for (const key in obj) { + if (Array.isArray(obj[key])) { + obj[key] = getRes(obj[key]); + } else if (typeof obj[key] === 'object') { + obj[key] = getRes(obj[key]); + } else { + // 判断obj[key]是否在arr中 + if (arr.has(key)) { + let aseFile = ''; + aseFile = isDecode + ? customBase64Decode(obj[key]) + : customBase64Encode(obj[key]); + const isGarbled = + // eslint-disable-next-line no-control-regex + /[\u0000-\u0008\v\f\u000E-\u001F\u007F-\u00FF\u{E000}-\u{F8FF}]/u.test( + aseFile, + ); + if (isGarbled) { + aseFile = obj[key]; + } + if (isDecode && sensitiveData.has(key)) { + // 数据脱敏,自动匹配姓名、手机号、身份证 + switch (key) { + // case 'accept_name': + // case 'express_name': + // case 'express_region': + // case 'patient': { + // // 保留前两个字符,其余用星号代替 + // aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*'); + // break; + // } + // case 'express_mobile': + // case 'mobile': { + // // 保留前三位和后四位,中间用星号代替 + // aseFile = + // aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + + // aseFile.slice(-4); + // break; + // } + case 'id_card': + case 'idcard': { + // 保留前六位和后四位,中间用星号代替 + aseFile = + aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + + aseFile.slice(-4); + break; + } + default: { + // 默认情况下,全部用星号代替 + break; + } + } + } + obj[key] = aseFile; + } + } + } + return obj; + } catch (error) { + console.error('错误', error); + return obj; + } +} + +/** + * 解密 + * @param data + */ +function customBase64Decode(data: string) { + // return data + try { + // 第一次Base64解码 + const decodedFirst = Base64.decode(data); + // 截取从第30个字符开始往后的内容 + const subStr = decodedFirst.slice(30); + // 第二次Base64解码 + const base64 = Base64.decode(subStr); + return base64.length > 0 ? base64 : data; + } catch (error) { + console.error(`解码Base64字符串【${data}】时发生错误`, error); + return data; + } +} + +/** + * 加密 + * @param data + */ +function customBase64Encode(data: string) { + // eslint-disable-next-line eqeqeq + if (data == null || data === '') return data; + // return data + try { + // 和加密方法差不多但是是要生成30个随机字符 + let randomString = ''; + for (let i = 0; i < 30; i++) { + const randomChar = String.fromCodePoint( + Math.floor(Math.random() * (126 - 32 + 1)) + 32, + ); + randomString += randomChar; + } + const encoded = Base64.encode(data); + return Base64.encode(randomString + encoded); + } catch (error) { + console.error(`加密字符串【${data}】时发生错误`, error); + return data; + } +} diff --git a/apps/web-antd/src/components/description/description.vue b/apps/web-antd/src/components/description/description.vue new file mode 100644 index 00000000..ec5e220e --- /dev/null +++ b/apps/web-antd/src/components/description/description.vue @@ -0,0 +1,77 @@ + + diff --git a/apps/web-antd/src/components/description/index.ts b/apps/web-antd/src/components/description/index.ts new file mode 100644 index 00000000..745e7550 --- /dev/null +++ b/apps/web-antd/src/components/description/index.ts @@ -0,0 +1,2 @@ +export { default as Description } from './description.vue'; +export type * from './types'; diff --git a/apps/web-antd/src/components/description/types.d.ts b/apps/web-antd/src/components/description/types.d.ts new file mode 100644 index 00000000..c5120b8e --- /dev/null +++ b/apps/web-antd/src/components/description/types.d.ts @@ -0,0 +1,54 @@ +import type { CollapseContainerOptions } from '@/components/Container'; +import type { DescriptionsProps } from 'ant-design-vue/es/descriptions'; + +import type { CSSProperties, VNode } from 'vue'; + +export interface DescItem { + labelMinWidth?: number; + contentMinWidth?: number; + labelStyle?: CSSProperties; + field: string; + label: JSX.Element | string | VNode; + // Merge column + span?: number; + show?: (...arg: any) => boolean; + // render + render?: ( + val: any, + data: Recordable, + ) => Element | JSX.Element | number | string | undefined | VNode; + component: string; + componentProps?: any; + children?: DescItem[]; +} + +export interface DescriptionProps extends DescriptionsProps { + // Whether to include the collapse component + useCollapse?: boolean; + /** + * item configuration + * @type DescItem + */ + schema: DescItem[]; + /** + * 数据 + * @type object + */ + data: Recordable; + /** + * Built-in CollapseContainer component configuration + * @type CollapseContainerOptions + */ + collapseOptions?: CollapseContainerOptions; +} + +export interface DescInstance { + setDescProps(descProps: Partial): void; +} + +export type Register = (descInstance: DescInstance) => void; + +/** + * @description: + */ +export type UseDescReturnType = [Register, DescInstance]; diff --git a/apps/web-antd/src/components/dynamic-schema-form/dynamic-schema-form.vue b/apps/web-antd/src/components/dynamic-schema-form/dynamic-schema-form.vue new file mode 100644 index 00000000..df32effe --- /dev/null +++ b/apps/web-antd/src/components/dynamic-schema-form/dynamic-schema-form.vue @@ -0,0 +1,104 @@ + + + diff --git a/apps/web-antd/src/components/dynamic-schema-form/index.ts b/apps/web-antd/src/components/dynamic-schema-form/index.ts new file mode 100644 index 00000000..7965c2c8 --- /dev/null +++ b/apps/web-antd/src/components/dynamic-schema-form/index.ts @@ -0,0 +1 @@ +export { default as DynamicSchemaForm } from './dynamic-schema-form.vue'; diff --git a/apps/web-antd/src/components/form-description/form-description.vue b/apps/web-antd/src/components/form-description/form-description.vue new file mode 100644 index 00000000..c96678e8 --- /dev/null +++ b/apps/web-antd/src/components/form-description/form-description.vue @@ -0,0 +1,75 @@ + + + diff --git a/apps/web-antd/src/components/form-description/index.ts b/apps/web-antd/src/components/form-description/index.ts new file mode 100644 index 00000000..0fbe8296 --- /dev/null +++ b/apps/web-antd/src/components/form-description/index.ts @@ -0,0 +1 @@ +export { default as FormDescription } from './form-description.vue'; diff --git a/apps/web-antd/src/components/form/component-map.ts b/apps/web-antd/src/components/form/component-map.ts new file mode 100644 index 00000000..8f7353c6 --- /dev/null +++ b/apps/web-antd/src/components/form/component-map.ts @@ -0,0 +1,32 @@ +import type { CustomComponentType } from './types'; + +import type { Component } from 'vue'; + +import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool'; + +const componentMap = new Map(); +// import.meta.glob() 直接引入所有的模块 Vite 独有的功能 +const modules = import.meta.glob( + ['./components/**/*.vue', '../../views/**/components/form/*.vue'], + { eager: true }, +); +// 加入到路由集合中 +Object.keys(modules).forEach((key) => { + if (!key.includes('-ignore')) { + const mod = (modules as any)[key].default || {}; + // ./components/ApiDict.vue + // 获取ApiDict + const compName = getFileNameWithoutExtension(key); + componentMap.set(toPascalCase(compName), mod); + } +}); +/** + * 注册组件 + * @param components + */ +export const registerComponent = (components: any) => { + componentMap.forEach((value, key) => { + components[key] = value as Component; + }); +}; +export { componentMap }; diff --git a/apps/web-antd/src/components/form/components/avatar.vue b/apps/web-antd/src/components/form/components/avatar.vue new file mode 100644 index 00000000..7be437cf --- /dev/null +++ b/apps/web-antd/src/components/form/components/avatar.vue @@ -0,0 +1,62 @@ + + + diff --git a/apps/web-antd/src/components/form/components/upload.vue b/apps/web-antd/src/components/form/components/upload.vue new file mode 100644 index 00000000..02bd133d --- /dev/null +++ b/apps/web-antd/src/components/form/components/upload.vue @@ -0,0 +1,162 @@ + + diff --git a/apps/web-antd/src/components/form/index.ts b/apps/web-antd/src/components/form/index.ts new file mode 100644 index 00000000..645f73a0 --- /dev/null +++ b/apps/web-antd/src/components/form/index.ts @@ -0,0 +1,7 @@ +// export { default as ApiCheckboxGroup } from './components/api-checkbox-group.vue'; +// export { default as ApiDict } from './components/api-dict.vue'; +// export { default as ApiRadioGroup } from './components/api-radio-group.vue'; +// export { default as ApiSelect } from './components/api-select.vue'; +// export { default as ApiTreeSelect } from './components/api-tree-select.vue'; +// export { default as IconPicker } from './components/icon-picker.vue'; +export { default as Upload } from './components/upload.vue'; diff --git a/apps/web-antd/src/components/form/types/index.d.ts b/apps/web-antd/src/components/form/types/index.d.ts new file mode 100644 index 00000000..c67d6ac9 --- /dev/null +++ b/apps/web-antd/src/components/form/types/index.d.ts @@ -0,0 +1,7 @@ +export type CustomComponentType = + | 'ApiCheckboxGroup' + | 'ApiDict' + | 'ApiRadioGroup' + | 'ApiSelect' + | 'ApiTreeSelect' + | 'IconPicker'; diff --git a/apps/web-antd/src/components/icon/icon.vue b/apps/web-antd/src/components/icon/icon.vue new file mode 100644 index 00000000..e5c0c310 --- /dev/null +++ b/apps/web-antd/src/components/icon/icon.vue @@ -0,0 +1,51 @@ + + + + diff --git a/apps/web-antd/src/components/icon/index.ts b/apps/web-antd/src/components/icon/index.ts new file mode 100644 index 00000000..fc989720 --- /dev/null +++ b/apps/web-antd/src/components/icon/index.ts @@ -0,0 +1 @@ +export { default as Icon } from './icon.vue'; diff --git a/apps/web-antd/src/components/table-action/index.ts b/apps/web-antd/src/components/table-action/index.ts new file mode 100644 index 00000000..00b7e6b6 --- /dev/null +++ b/apps/web-antd/src/components/table-action/index.ts @@ -0,0 +1,2 @@ +export { default as TableAction } from './table-action.vue'; +export type * from './types.d.ts'; diff --git a/apps/web-antd/src/components/table-action/table-action.vue b/apps/web-antd/src/components/table-action/table-action.vue new file mode 100644 index 00000000..2ce2af69 --- /dev/null +++ b/apps/web-antd/src/components/table-action/table-action.vue @@ -0,0 +1,225 @@ + + + + diff --git a/apps/web-antd/src/components/table-action/types.d.ts b/apps/web-antd/src/components/table-action/types.d.ts new file mode 100644 index 00000000..29e7d4d0 --- /dev/null +++ b/apps/web-antd/src/components/table-action/types.d.ts @@ -0,0 +1,26 @@ +import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; +import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip'; + +export interface PopConfirm { + title: string; + okText?: string; + cancelText?: string; + confirm: Fn; + cancel?: Fn; + icon?: string; + disabled?: boolean; +} +export interface ActionItem extends ButtonProps { + onClick?: Fn; + label?: string; + color?: 'error' | 'success' | 'warning'; + icon?: string; + popConfirm?: PopConfirm; + disabled?: boolean; + divider?: boolean; + // 权限编码控制是否显示 + auth?: string[]; + // 业务控制是否显示 + ifShow?: ((action: ActionItem) => boolean) | boolean; + tooltip?: string | TooltipProps; +} diff --git a/apps/web-antd/src/components/view/component-map.ts b/apps/web-antd/src/components/view/component-map.ts new file mode 100644 index 00000000..1bc0dedd --- /dev/null +++ b/apps/web-antd/src/components/view/component-map.ts @@ -0,0 +1,22 @@ +import type { Component } from 'vue'; + +import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool'; + +const componentMap = new Map(); +// import.meta.glob() 直接引入所有的模块 Vite 独有的功能 +const modules = import.meta.glob( + ['./components/**/*.vue', '../../views/**/components/view/*.vue'], + { eager: true }, +); +// 加入到路由集合中 +Object.keys(modules).forEach((key) => { + if (!key.includes('-ignore')) { + const mod = (modules as any)[key].default || {}; + // ./components/ApiDict.vue + // 获取ApiDict + const compName = getFileNameWithoutExtension(key); + componentMap.set(toPascalCase(compName), mod); + } +}); + +export { componentMap }; diff --git a/apps/web-antd/src/components/view/components/api-cascader.vue b/apps/web-antd/src/components/view/components/api-cascader.vue new file mode 100644 index 00000000..3a0e26ea --- /dev/null +++ b/apps/web-antd/src/components/view/components/api-cascader.vue @@ -0,0 +1,98 @@ + + + diff --git a/apps/web-antd/src/components/view/components/api-checkbox-group.vue b/apps/web-antd/src/components/view/components/api-checkbox-group.vue new file mode 100644 index 00000000..dbca93f9 --- /dev/null +++ b/apps/web-antd/src/components/view/components/api-checkbox-group.vue @@ -0,0 +1,6 @@ + + diff --git a/apps/web-antd/src/components/view/components/api-dict.vue b/apps/web-antd/src/components/view/components/api-dict.vue new file mode 100644 index 00000000..b994408e --- /dev/null +++ b/apps/web-antd/src/components/view/components/api-dict.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/web-antd/src/components/view/components/api-radio-group.vue b/apps/web-antd/src/components/view/components/api-radio-group.vue new file mode 100644 index 00000000..dbca93f9 --- /dev/null +++ b/apps/web-antd/src/components/view/components/api-radio-group.vue @@ -0,0 +1,6 @@ + + diff --git a/apps/web-antd/src/components/view/components/api-select.vue b/apps/web-antd/src/components/view/components/api-select.vue new file mode 100644 index 00000000..7976c2e1 --- /dev/null +++ b/apps/web-antd/src/components/view/components/api-select.vue @@ -0,0 +1,153 @@ + + + diff --git a/apps/web-antd/src/components/view/components/api-tree-select.vue b/apps/web-antd/src/components/view/components/api-tree-select.vue new file mode 100644 index 00000000..4ac0ebac --- /dev/null +++ b/apps/web-antd/src/components/view/components/api-tree-select.vue @@ -0,0 +1,102 @@ + + + diff --git a/apps/web-antd/src/components/view/components/avatar.vue b/apps/web-antd/src/components/view/components/avatar.vue new file mode 100644 index 00000000..721a8cdf --- /dev/null +++ b/apps/web-antd/src/components/view/components/avatar.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/web-antd/src/components/view/components/icon-picker.vue b/apps/web-antd/src/components/view/components/icon-picker.vue new file mode 100644 index 00000000..dbeb70e8 --- /dev/null +++ b/apps/web-antd/src/components/view/components/icon-picker.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/web-antd/src/components/view/components/radio-group.vue b/apps/web-antd/src/components/view/components/radio-group.vue new file mode 100644 index 00000000..28d369f9 --- /dev/null +++ b/apps/web-antd/src/components/view/components/radio-group.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/web-antd/src/components/view/components/select.vue b/apps/web-antd/src/components/view/components/select.vue new file mode 100644 index 00000000..8724e4e7 --- /dev/null +++ b/apps/web-antd/src/components/view/components/select.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/web-antd/src/components/view/components/switch.vue b/apps/web-antd/src/components/view/components/switch.vue new file mode 100644 index 00000000..0f1eef04 --- /dev/null +++ b/apps/web-antd/src/components/view/components/switch.vue @@ -0,0 +1,25 @@ + + diff --git a/apps/web-antd/src/components/view/components/upload.vue b/apps/web-antd/src/components/view/components/upload.vue new file mode 100644 index 00000000..cc2c6ae0 --- /dev/null +++ b/apps/web-antd/src/components/view/components/upload.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 51412956..6437bc09 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -4,9 +4,9 @@ import type { NotificationItem } from '@vben/layouts'; import { computed, ref, watch } from 'vue'; import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; -import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; +// import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { useWatermark } from '@vben/hooks'; -import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; +// import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { BasicLayout, LockScreen, @@ -15,41 +15,42 @@ import { } from '@vben/layouts'; import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; -import { openWindow } from '@vben/utils'; +// import { openWindow } from '@vben/utils'; -import { $t } from '#/locales'; +// import { $t } from '#/locales'; import { useAuthStore } from '#/store'; import LoginForm from '#/views/_core/authentication/login.vue'; const notifications = ref([ - { - avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', - date: '3小时前', - isRead: true, - message: '描述信息描述信息描述信息', - title: '收到了 14 份新周报', - }, - { - avatar: 'https://avatar.vercel.sh/1', - date: '刚刚', - isRead: false, - message: '描述信息描述信息描述信息', - title: '朱偏右 回复了你', - }, - { - avatar: 'https://avatar.vercel.sh/1', - date: '2024-01-01', - isRead: false, - message: '描述信息描述信息描述信息', - title: '曲丽丽 评论了你', - }, - { - avatar: 'https://avatar.vercel.sh/satori', - date: '1天前', - isRead: false, - message: '描述信息描述信息描述信息', - title: '代办提醒', - }, + // TODO 通知列表 + // { + // avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', + // date: '3小时前', + // isRead: true, + // message: '描述信息描述信息描述信息', + // title: '收到了 14 份新周报', + // }, + // { + // avatar: 'https://avatar.vercel.sh/1', + // date: '刚刚', + // isRead: false, + // message: '描述信息描述信息描述信息', + // title: '朱偏右 回复了你', + // }, + // { + // avatar: 'https://avatar.vercel.sh/1', + // date: '2024-01-01', + // isRead: false, + // message: '描述信息描述信息描述信息', + // title: '曲丽丽 评论了你', + // }, + // { + // avatar: 'https://avatar.vercel.sh/satori', + // date: '1天前', + // isRead: false, + // message: '描述信息描述信息描述信息', + // title: '代办提醒', + // }, ]); const userStore = useUserStore(); @@ -61,33 +62,34 @@ const showDot = computed(() => ); const menus = computed(() => [ - { - handler: () => { - openWindow(VBEN_DOC_URL, { - target: '_blank', - }); - }, - icon: BookOpenText, - text: $t('ui.widgets.document'), - }, - { - handler: () => { - openWindow(VBEN_GITHUB_URL, { - target: '_blank', - }); - }, - icon: MdiGithub, - text: 'GitHub', - }, - { - handler: () => { - openWindow(`${VBEN_GITHUB_URL}/issues`, { - target: '_blank', - }); - }, - icon: CircleHelp, - text: $t('ui.widgets.qa'), - }, + // TODO 后续放个人中心 + // { + // handler: () => { + // openWindow(VBEN_DOC_URL, { + // target: '_blank', + // }); + // }, + // icon: BookOpenText, + // text: $t('ui.widgets.document'), + // }, + // { + // handler: () => { + // openWindow(VBEN_GITHUB_URL, { + // target: '_blank', + // }); + // }, + // icon: MdiGithub, + // text: 'GitHub', + // }, + // { + // handler: () => { + // openWindow(`${VBEN_GITHUB_URL}/issues`, { + // target: '_blank', + // }); + // }, + // icon: CircleHelp, + // text: $t('ui.widgets.qa'), + // }, ]); const avatar = computed(() => { @@ -96,6 +98,7 @@ const avatar = computed(() => { async function handleLogout() { await authStore.logout(false); + destroyWatermark(); } function handleNoticeClear() { @@ -110,7 +113,9 @@ watch( async (enable) => { if (enable) { await updateWatermark({ - content: `${userStore.userInfo?.username}`, + // 这里更改水印内容 + // content: `${userStore.userInfo?.nick_name || import.meta.env.VITE_APP_TITLE}`, + content: `${userStore.userInfo?.nick_name}\r\n${userStore.userInfo?.phone || import.meta.env.VITE_APP_TITLE}`, }); } else { destroyWatermark(); @@ -127,10 +132,10 @@ watch( diff --git a/apps/web-antd/src/preferences.ts b/apps/web-antd/src/preferences.ts index b2e9ace4..ae514845 100644 --- a/apps/web-antd/src/preferences.ts +++ b/apps/web-antd/src/preferences.ts @@ -9,5 +9,43 @@ export const overridesPreferences = defineOverridesPreferences({ // overrides app: { name: import.meta.env.VITE_APP_TITLE, + layout: 'sidebar-mixed-nav', + // 登录过期模式 + loginExpiredMode: 'modal', + // 权限模式 frontend 默认前端控制 backend 后端控制 + accessMode: 'backend', + enableRefreshToken: true, + defaultAvatar: + 'https://yanydy.oss-cn-hangzhou.aliyuncs.com/uploads/20241219/2b38e38414a2b9383b8643983e3f69d7.png', + watermark: true, + // 是否开启检查更新 + enableCheckUpdates: true, + // 检查更新的时间间隔,单位为分钟 + checkUpdatesInterval: 1, + version: '1.0.0', + }, + sidebar: { + extraCollapse: false, + }, + footer: { + // enable: true, + // fixed: true, + }, + logo: { + enable: true, + source: + // 'https://yanydy.oss-cn-hangzhou.aliyuncs.com/uploads/20241219/2b38e38414a2b9383b8643983e3f69d7.png', + '/img/logo.png', + }, + theme: { + mode: 'auto', + builtinType: 'sky-blue', + colorPrimary: 'hsl(231 98% 65%)', + }, + copyright: { + companyName: '萧康云医', + companySiteLink: 'https://admin.xiaokang88.com', + icp: '浙ICP备19041368号-2', + icpLink: 'https://beian.miit.gov.cn/#/Integrated/recordQuery', }, }); diff --git a/apps/web-antd/src/router/guard.ts b/apps/web-antd/src/router/guard.ts index cbb5235e..6ebbbc2a 100644 --- a/apps/web-antd/src/router/guard.ts +++ b/apps/web-antd/src/router/guard.ts @@ -93,7 +93,7 @@ function setupAccessGuard(router: Router) { // 生成路由表 // 当前登录用户拥有的角色标识列表 const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); - const userRoles = userInfo.roles ?? []; + const userRoles = userInfo?.roles ?? []; // 生成菜单和路由 const { accessibleMenus, accessibleRoutes } = await generateAccess({ diff --git a/apps/web-antd/src/router/routes/modules/demos.ts b/apps/web-antd/src/router/routes/modules/demos.ts index 32bb338e..07b03f7b 100644 --- a/apps/web-antd/src/router/routes/modules/demos.ts +++ b/apps/web-antd/src/router/routes/modules/demos.ts @@ -1,30 +1,30 @@ -import type { RouteRecordRaw } from 'vue-router'; - -import { BasicLayout } from '#/layouts'; -import { $t } from '#/locales'; - -const routes: RouteRecordRaw[] = [ - { - component: BasicLayout, - meta: { - icon: 'ic:baseline-view-in-ar', - keepAlive: true, - order: 1000, - title: $t('demos.title'), - }, - name: 'Demos', - path: '/demos', - children: [ - { - meta: { - title: $t('demos.antd'), - }, - name: 'AntDesignDemos', - path: '/demos/ant-design', - component: () => import('#/views/demos/antd/index.vue'), - }, - ], - }, -]; - -export default routes; +// import type { RouteRecordRaw } from 'vue-router'; +// +// import { BasicLayout } from '#/layouts'; +// import { $t } from '#/locales'; +// +// const routes: RouteRecordRaw[] = [ +// { +// component: BasicLayout, +// meta: { +// icon: 'ic:baseline-view-in-ar', +// keepAlive: true, +// order: 1000, +// title: $t('demos.title'), +// }, +// name: 'Demos', +// path: '/demos', +// children: [ +// { +// meta: { +// title: $t('demos.antd'), +// }, +// name: 'AntDesignDemos', +// path: '/demos/ant-design', +// component: () => import('#/views/demos/antd/index.vue'), +// }, +// ], +// }, +// ]; +// +// export default routes; diff --git a/apps/web-antd/src/router/routes/modules/sys.ts b/apps/web-antd/src/router/routes/modules/sys.ts new file mode 100644 index 00000000..6223a982 --- /dev/null +++ b/apps/web-antd/src/router/routes/modules/sys.ts @@ -0,0 +1,45 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'carbon:cics-system-group', + keepAlive: true, + order: 1000, + title: '基础管理', + }, + name: 'System', + path: '/system', + children: [ + { + meta: { + title: '管理员管理', + }, + name: 'SystemUser', + path: '/system/user', + component: () => import('#/views/system/admin/index.vue'), + }, + { + meta: { + title: '角色管理', + }, + name: 'SystemRole', + path: '/system/role', + component: () => import('#/views/system/role/index.vue'), + }, + { + meta: { + title: '菜单管理', + }, + name: 'SystemMenu', + path: '/system/menu', + component: () => import('#/views/system/menu/index.vue'), + }, + ], + }, +]; + +export default routes; diff --git a/apps/web-antd/src/util/tool.ts b/apps/web-antd/src/util/tool.ts new file mode 100644 index 00000000..8babca8a --- /dev/null +++ b/apps/web-antd/src/util/tool.ts @@ -0,0 +1,350 @@ +import type { VbenFormProps } from '#/adapter/form'; +import type { DescItem } from '#/components/description'; + +import { isFunction } from '@vben/utils'; + +export const omit = (obj: any, keysToOmit: string[]) => { + // 如果 obj 不是对象或者 keysToOmit 不是数组,则直接返回 obj + if (typeof obj !== 'object' || !Array.isArray(keysToOmit)) { + return obj; + } + + // 创建一个新的对象,避免修改原始对象 + const result = {} as any; + + // 遍历 obj 的所有键值对 + for (const key in obj) { + // 如果当前键不在要忽略的键列表中,则复制到新对象 + if (!keysToOmit.includes(key)) { + result[key] = obj[key]; + } + } + + return result; +}; +export const get = (object: any, path: string) => { + // 如果 object 不是对象或者 path 不是字符串,则直接返回 defaultValue + if ( + typeof object !== 'object' || + object === null || + typeof path !== 'string' + ) { + return object; + } + + // 将路径字符串转换为数组 + const pathArray = path.split('.').filter(Boolean); // 过滤掉空字符串 + let current = object; + + // 遍历路径数组 + for (const element of pathArray) { + // 如果当前层级不是对象或没有对应的键,则返回 defaultValue + if ( + typeof current !== 'object' || + current === null || + !(element in current) + ) { + return object; + } + // 更新 current 到下一层级 + current = current[element]; + } + + // 返回最终找到的值 + return current; +}; +export const ifDetail = (renderCallbackParams: any): boolean => { + const schema = renderCallbackParams.schema; + if (isFunction(schema.ifDetail)) { + return schema.ifDetail(renderCallbackParams); + } + return schema.ifDetail !== false; +}; + +/** + * 将表单元数据转换为详情表单 + * @param formOptions + * @param values + */ +export const schemaToDetailForm = ( + formOptions: VbenFormProps, + values: any, +): DescItem[] => { + const group: Array = []; + const formSchemas = formOptions.schema; + if (!Array.isArray(formSchemas)) return group; + for (let i = 0; i < formSchemas.length; i++) { + const item = formSchemas[i]; + // 找到分割线 + if (item?.component === 'Divider') { + group.push({ + field: item.fieldName, + label: item.label, + children: [], + } as unknown as DescItem); + // 将分割线后面的数据添加到group中 + for (let j = i + 1; j < formSchemas.length; j++) { + const nextItem = formSchemas[j]; + // 找到下面的分割线 + if (nextItem?.component === 'Divider') { + // 中断 + i = j - 1; + break; + } else { + if ( + ifDetail({ + schema: nextItem, + values, + model: values, + field: item.fieldName, + }) + ) { + group[group.length - 1]?.children?.push({ + field: nextItem?.fieldName, + label: nextItem?.label, + span: nextItem?.detailSpan || 12, + component: nextItem?.component, + componentProps: nextItem?.componentProps, + } as DescItem); + } + } + } + } + } + if (group.length > 0) { + return group; + } + return [ + { + field: 'baseinfo', + label: '基本信息', + children: formSchemas + .filter((item) => { + return ifDetail({ + schema: item, + values, + model: values, + field: item.fieldName, + }); + }) + .map((item) => { + return { + field: item.fieldName, + label: item.label, + span: item.detailSpan || 12, + component: item.component, + componentProps: item.componentProps, + } as DescItem; + }), + } as DescItem, + ]; +}; + +// 获取子节点id +export const getChildIds = (record: any) => { + const ids = [record.id]; + record.children?.forEach((item: any) => { + ids.push(...getChildIds(item)); + }); + return ids; +}; + +/** + * 获取所有叶子节点 + * @param treeData + */ +export const getLeafNodeIds = (treeData: any) => { + const leafNodeIds: any = []; + + function traverse(node: any) { + // 如果当前节点没有 children 或 children 是空数组,则它是叶子节点 + if (!node.children || node.children.length === 0) { + leafNodeIds.push(node.id); + } else { + // 否则,递归遍历每个子节点 + for (const child of node.children) { + traverse(child); + } + } + } + + // 遍历树的根节点 + for (const root of treeData) { + traverse(root); + } + return leafNodeIds; +}; + +/** + * 获取所有节点id + * @param treeData + */ +export const getAllNodeIds = (treeData: any) => { + const allNodeIds: any = []; + + function traverse(node: any) { + // 收集当前节点的 id + allNodeIds.push(node.id); + + // 如果当前节点有 children,则递归遍历每个子节点 + if (node.children && node.children.length > 0) { + for (const child of node.children) { + traverse(child); + } + } + } + + // 遍历树的根节点 + for (const root of treeData) { + traverse(root); + } + + return allNodeIds; +}; + +/** + * -转大驼峰 + * @param str + */ +export const toPascalCase = (str: any) => { + // 将连字符或下划线替换为空格,以便后续处理 + const words = str.replaceAll(/[-_]/g, ' ').split(' '); + + // 将每个单词的首字母大写,并将其余部分保持原样 + const pascalCaseWords = words.map((word: any) => { + if (word) { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + return word; + }); + + // 将处理后的单词拼接成一个新的字符串 + return pascalCaseWords.join(''); +}; + +/** + * 转-kebab + */ +export const toKebabCase = (str: string) => { + // 将每个单词的首字母小写,并在单词之间插入连字符 + return str + .replaceAll(/([A-Z])/g, (match, _p1, offset) => { + // 如果是第一个字符,直接转为小写 + if (offset === 0) { + return match.toLowerCase(); + } + // 否则,在大写字母前加上连字符,并将大写字母转为小写 + return `-${match.toLowerCase()}`; + }) + .toLowerCase(); // 确保整个字符串都是小写 +}; + +export function getFileNameWithoutExtension(path: string) { + // 使用正则表达式匹配最后一个 / 后面的所有字符,但不包括最后一个点(.)之后的内容 + const match = path.match(/[^/]+(?=\.[^./]+$|$)/); + return match ? match[0].replace(/\.[^/.]+$/, '') : null; +} + +/** + * 数据脱敏 + * @param data + * @param type + */ +export function desensitize(data: string, type: 'bankCard' | 'idCard'): string { + if (!data) return ''; + + switch (type) { + case 'bankCard': { + // 银行卡号脱敏规则:显示前6位和后4位,中间用*代替 + if (data.length < 12) { + throw new Error('Invalid bank card number length'); + } + return data.slice(0, 6) + '*'.repeat(data.length - 10) + data.slice(-4); + } + + case 'idCard': { + // 身份证号脱敏规则:显示前6位和后4位,中间用*代替 + if (data.length !== 18) { + throw new Error('Invalid ID card number length'); + } + return data.slice(0, 6) + '*'.repeat(10) + data.slice(-4); + } + + default: { + throw new Error('Unsupported desensitization type'); + } + } +} + +/** + * 获取操作系统的Icon + * @param str + */ +export function getIcon(str: string) { + switch (str) { + case 'Api Post': { + return 'logos:async-api-icon'; + } + case 'Chrome': { + return 'devicon:chrome'; + } + case 'Edge': + case 'Microsoft Edge': { + return 'logos:microsoft-edge'; + } + case 'Firefox': { + return 'devicon:firefox'; + } + case 'Linux': { + return 'logos:linux-tux'; + } + case 'Mac': { + return 'wpf:mac-os'; + } + case 'Safari': { + return 'devicon:safari'; + } + case 'Windows': { + return 'logos:microsoft-windows-icon'; + } + default: { + return 'logos:microsoft-windows-icon'; + } + } +} + +/** + * 获取接诊状态 + * @param status + */ +export function getRegisterStatus(status: number) { + switch (status) { + case 0: { + return '待支付'; + } + case 1: { + return '待接诊'; + } + case 2: { + return '接诊中'; + } + case 3: { + return '已结束'; + } + case 4: { + return '已取消'; + } + case 5: { + return '待评价'; + } + case 6: { + return '已评价'; + } + case 7: { + return '已拒诊'; + } + default: { + return '未知状态'; + } + } +} diff --git a/apps/web-antd/src/views/_core/authentication/code-login.vue b/apps/web-antd/src/views/_core/authentication/code-login.vue index acfd1fd7..19898538 100644 --- a/apps/web-antd/src/views/_core/authentication/code-login.vue +++ b/apps/web-antd/src/views/_core/authentication/code-login.vue @@ -33,11 +33,9 @@ const formSchema = computed((): VbenFormSchema[] => { componentProps: { codeLength: CODE_LENGTH, createText: (countdown: number) => { - const text = - countdown > 0 - ? $t('authentication.sendText', [countdown]) - : $t('authentication.sendCode'); - return text; + return countdown > 0 + ? $t('authentication.sendText', [countdown]) + : $t('authentication.sendCode'); }, placeholder: $t('authentication.code'), }, diff --git a/apps/web-antd/src/views/_core/authentication/login.vue b/apps/web-antd/src/views/_core/authentication/login.vue index 099e4c8c..63711140 100644 --- a/apps/web-antd/src/views/_core/authentication/login.vue +++ b/apps/web-antd/src/views/_core/authentication/login.vue @@ -1,18 +1,24 @@ + diff --git a/apps/web-antd/src/views/business/express/express-company/config/form.ts b/apps/web-antd/src/views/business/express/express-company/config/form.ts new file mode 100644 index 00000000..e3112078 --- /dev/null +++ b/apps/web-antd/src/views/business/express/express-company/config/form.ts @@ -0,0 +1,69 @@ +import type { VbenFormProps } from '#/adapter/form'; + +export const modalFormProps: VbenFormProps = { + wrapperClass: 'grid-cols-12', // 24栅格, + commonConfig: { + formItemClass: 'col-span-12', + // 所有表单项 + componentProps: { + class: 'w-full', + }, + }, + // handleSubmit: onSubmit, + layout: 'horizontal', + schema: [ + { + component: 'VbenInput', + fieldName: 'id', + label: 'ID', + formItemClass: 'col-span-6', + dependencies: { + show: false, + triggerFields: ['id'], + }, + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入快递公司昵称', + }, + fieldName: 'name', + label: '快递公司名称', + rules: 'required', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入快递公司编码', + }, + fieldName: 'code', + label: '快递公司编码', + rules: 'required', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入快递公司类型', + }, + fieldName: 'type', + label: '快递公司类型', + rules: 'required', + }, + // { + // component: 'Avatar', + // fieldName: 'logo', + // label: 'LOGO', + // rules: 'required', + // }, + { + component: 'Textarea', + componentProps: { + placeholder: '请输入快递公司介绍', + }, + fieldName: 'introduce', + label: '快递公司介绍', + rules: 'required', + }, + ], + showDefaultActions: false, +}; diff --git a/apps/web-antd/src/views/business/express/express-company/config/search.ts b/apps/web-antd/src/views/business/express/express-company/config/search.ts new file mode 100644 index 00000000..d653cfc1 --- /dev/null +++ b/apps/web-antd/src/views/business/express/express-company/config/search.ts @@ -0,0 +1,37 @@ +import type { VbenFormProps } from '#/adapter/form'; + +// import dayjs from 'dayjs'; + +export const formOptions: VbenFormProps = { + // 默认展开 + collapsed: false, + schema: [ + { + component: 'VbenInput', + componentProps: { + placeholder: '输入名称', + }, + defaultValue: '', + fieldName: 'name', + label: '快递公司名称', + }, + // { + // component: 'RangePicker', + // componentProps: { + // format: 'YYYY-MM-DD', // 确保格式与你需要的一致 + // }, + // // defaultValue: [dayjs().startOf('month'), dayjs()], + // fieldName: 'search_time', + // label: '时间范围', + // }, + ], + // 控制表单是否显示折叠按钮 + showCollapseButton: true, + submitButtonOptions: { + content: '查询', + }, + // 是否在字段值改变时提交表单 + submitOnChange: true, + // 按下回车时是否提交表单 + submitOnEnter: false, +}; diff --git a/apps/web-antd/src/views/business/express/express-company/config/table.ts b/apps/web-antd/src/views/business/express/express-company/config/table.ts new file mode 100644 index 00000000..fdcd2dae --- /dev/null +++ b/apps/web-antd/src/views/business/express/express-company/config/table.ts @@ -0,0 +1,67 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; + +import { getExpressCompaniesList } from '../api'; + +interface RowType { + id: string; + name: string; + logo: string; + introduce: string; + created_at: string; +} + +export const gridOptions: VxeGridProps = { + checkboxConfig: { + highlight: true, + labelField: '', + }, + columnConfig: { + useKey: true, + }, + rowConfig: { + useKey: true, + }, + columns: [ + { type: 'checkbox', width: 60 }, + { field: 'id', align: 'left', title: 'ID', width: 100 }, + { field: 'name', align: 'left', title: '快递公司每次' }, + { field: 'code', title: '快递公司编码' }, + { field: 'type', title: '快递公司类型' }, + { field: 'created_at', title: '上传时间' }, + { type: 'html', title: '操作', slots: { default: 'action' } }, + ], + keepSource: true, + pagerConfig: {}, + proxyConfig: { + ajax: { + // 请求后端接口方法 + query: async ({ page }, formValues) => { + return await getExpressCompaniesList({ + page: page.currentPage, + pageSize: page.pageSize, + ...formValues, + }); + }, + }, + }, + height: 'auto', + border: false, + toolbarConfig: { + // 是否显示搜索表单控制按钮 + // @ts-ignore 正式环境时有完整的类型声明 + search: true, + refresh: true, // 刷新 + print: false, // 打印 + export: false, // 导出 + // custom: true, // 自定义列 + zoom: true, // 最大化最小化 + slots: { + buttons: 'toolbar-buttons', + }, + custom: { + // 自定义列-图标 + icon: 'vxe-icon-menu', + }, + }, + showOverflow: false, +}; diff --git a/apps/web-antd/src/views/business/express/express-company/index.vue b/apps/web-antd/src/views/business/express/express-company/index.vue new file mode 100644 index 00000000..6d879242 --- /dev/null +++ b/apps/web-antd/src/views/business/express/express-company/index.vue @@ -0,0 +1,155 @@ + + + diff --git a/apps/web-antd/src/views/business/order/product-order/api/index.ts b/apps/web-antd/src/views/business/order/product-order/api/index.ts new file mode 100644 index 00000000..863e0bcb --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/api/index.ts @@ -0,0 +1,65 @@ +import { requestClient } from '#/api/request'; + +const prefix = 'order/'; +/** + * 分页查询用户列表 + * @param data + */ +export async function getOrderList(data: any) { + return requestClient.get(`${prefix}list`, { params: data }); +} +/** + * 分页查询用户列表 + * @param data + */ +export async function getOrderStatusOption(data: any) { + return requestClient.get(`${prefix}status-option`, { params: data }); +} + +/** + * 获取订单详情 + * @param id + */ +export async function getOrderInfo(id: number) { + return requestClient.get(`${prefix}detail`, { params: { id } }); +} + +/** + * 新增订单 + * @param data + */ +export async function createOrder(data: Record) { + return requestClient.post(`${prefix}create`, data); +} + +/** + * 编辑订单 + * @param data + */ +export async function updateOrder(data: Record) { + return requestClient.post(`${prefix}update`, data); +} + +/** + * 删除订单 + * @param data + */ +export async function deleteOrder(data: Record) { + return requestClient.post(`${prefix}delete`, data); +} + +/** + * 订单发货 + * @param data + */ +export async function sendOrder(data: Record) { + return requestClient.post(`${prefix}ware-send`, data); +} + +/** + * 订单物流追踪 + * @param data + */ +export async function expressDetailByOrderId(data: Record) { + return requestClient.post(`express-detail/detail-by-order`, data); +} diff --git a/apps/web-antd/src/views/business/order/product-order/components/detail.vue b/apps/web-antd/src/views/business/order/product-order/components/detail.vue new file mode 100644 index 00000000..0b3e0b65 --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/components/detail.vue @@ -0,0 +1,275 @@ + + + + diff --git a/apps/web-antd/src/views/business/order/product-order/components/modal.vue b/apps/web-antd/src/views/business/order/product-order/components/modal.vue new file mode 100644 index 00000000..1400479e --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/components/modal.vue @@ -0,0 +1,64 @@ + + diff --git a/apps/web-antd/src/views/business/order/product-order/config/form.ts b/apps/web-antd/src/views/business/order/product-order/config/form.ts new file mode 100644 index 00000000..f3f5552e --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/config/form.ts @@ -0,0 +1,71 @@ +import type { VbenFormProps } from '#/adapter/form'; + +import { getExpressCompaniesOption } from '#/views/business/express/express-company/api'; + +export const modalFormProps: VbenFormProps = { + wrapperClass: 'grid-cols-12', // 24栅格, + commonConfig: { + formItemClass: 'col-span-12', + // 所有表单项 + componentProps: { + class: 'w-full', + }, + }, + // handleSubmit: onSubmit, + layout: 'horizontal', + schema: [ + { + component: 'VbenInput', + fieldName: 'order_id', + label: 'ID', + formItemClass: 'col-span-6', + dependencies: { + show: false, + triggerFields: ['oreder_id'], + }, + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入快递单号', + }, + fieldName: 'express_no', + label: '快递单号', + rules: 'required', + }, + { + component: 'ApiSelect', + componentProps: { + allowClear: true, + // filterOption: true, + showSearch: true, + filterOption: (input: string, option: any) => { + // 自定义过滤逻辑,确保可以根据 name 进行搜索 + return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0; + }, + // 菜单接口转options格式 + afterFetch: (data: { id: number; name: string }[]) => { + return data.map((item: any) => ({ + label: item.name, + value: item.code, + })); + }, + api: getExpressCompaniesOption, + placeholder: '请选择', + }, + fieldName: 'express_company_code', + formItemClass: 'col-span-6', + label: '快递公司', + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { + placeholder: '请输入订单备注', + }, + fieldName: 'introduce', + label: '发货备注', + }, + ], + showDefaultActions: false, +}; diff --git a/apps/web-antd/src/views/business/order/product-order/config/search.ts b/apps/web-antd/src/views/business/order/product-order/config/search.ts new file mode 100644 index 00000000..a3ba96fc --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/config/search.ts @@ -0,0 +1,108 @@ +import type { VbenFormProps } from '#/adapter/form'; + +import dayjs from 'dayjs'; +import {getOrderStatusOption} from "#/views/business/order/product-order/api"; + +export const formOptions: VbenFormProps = { + // 默认展开 + collapsed: false, + schema: [ + { + component: 'VbenInput', + componentProps: { + placeholder: '输入订单号', + }, + defaultValue: '', + fieldName: 'order_no', + label: '订单号', + }, + { + component: 'ApiSelect', + componentProps: { + allowClear: true, + filterOption: true, + // showSearch: true, + // 菜单接口转options格式 + afterFetch: (data: { id: number; name: string }[]) => { + return data.map((item: any) => ({ + label: item.name, + value: item.id, + })); + }, + api: getOrderStatusOption, + placeholder: '请选择', + }, + fieldName: 'status', + label: '订单状态', + }, + { + component: 'VbenSelect', + componentProps: { + allowClear: true, + filterOption: true, + showSearch: true, + options: [ + { + label: '快递到家', + value: 0, + }, + { + label: '诊所自提', + value: 1, + }, + ], + placeholder: '请选择', + }, + fieldName: 'delivery_method', + label: '发货方式', + }, + { + component: 'VbenSelect', + componentProps: { + allowClear: true, + filterOption: true, + showSearch: true, + options: [ + { + label: '中药', + value: 1, + }, + { + label: '西药', + value: 2, + }, + { + label: '中成药', + value: 3, + }, + { + label: '产品服务包', + value: 5, + }, + ], + placeholder: '请选择', + }, + fieldName: 'prescription_type', + label: '订单类型', + }, + { + component: 'RangePicker', + componentProps: { + format: 'YYYY-MM-DD', // 确保格式与你需要的一致 + }, + defaultValue: [dayjs().startOf('month'), dayjs()], + fieldName: 'search_time', + label: '时间范围', + }, + ], + // 控制表单是否显示折叠按钮 + showCollapseButton: true, + submitButtonOptions: { + content: '查询', + }, + // 是否在字段值改变时提交表单 + // submitOnChange: true, + // submitOnChange: true, + // 按下回车时是否提交表单 + submitOnEnter: false, +}; diff --git a/apps/web-antd/src/views/business/order/product-order/config/table.ts b/apps/web-antd/src/views/business/order/product-order/config/table.ts new file mode 100644 index 00000000..b6f0ac7c --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/config/table.ts @@ -0,0 +1,103 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; + +import { getOrderList } from '#/views/business/order/product-order/api'; + +interface RowType { + id: string; + name: string; + logo: string; + introduce: string; + created_at: string; +} + +export const gridOptions: VxeGridProps = { + checkboxConfig: { + highlight: true, + labelField: '', + }, + columns: [ + // { type: 'checkbox', width: 60 }, + { type: 'expand', width: 80, slots: { content: 'expand-content' } }, + { field: 'id', align: 'left', title: 'ID', width: 100 }, + { field: 'order_no', align: 'left', title: '订单号' }, + { + field: 'user.avatarUrl', + title: '下单用户信息', + slots: { default: 'avatar' }, + width: 100, + }, + { field: 'store.name', title: '诊所名称' }, + { + field: 'address.name', + title: '收货人姓名', + slots: { default: 'address-name' }, + }, + { + field: 'address.mobile', + title: '收货人联系方式', + slots: { default: 'address-mobile' }, + }, + { + field: 'delivery_method', + title: '订单类型&邮寄方式&订单状态', + slots: { default: 'delivery-method' }, + }, + { field: 'items_price', title: '总价' }, + { field: 'pay_time', title: '支付时间', slots: { default: 'pay-time' } }, + { field: 'created_at', title: '下单时间' }, + { + type: 'html', + title: '操作', + align: 'right', + slots: { default: 'action' }, + width: 200, + }, + ], + keepSource: true, + pagerConfig: {}, + columnConfig: { + useKey: true, + }, + rowConfig: { + useKey: true, + }, + // scrollY: { + // enabled: true, + // gt: 0, + // }, + proxyConfig: { + ajax: { + // 请求后端接口方法 + query: async ({ page }, formValues) => { + return await getOrderList({ + page: page.currentPage, + pageSize: page.pageSize, + ...formValues, + }); + }, + }, + }, + height: 'auto', + border: false, + toolbarConfig: { + // 是否显示搜索表单控制按钮 + // @ts-ignore 正式环境时有完整的类型声明 + search: true, + refresh: true, // 刷新 + print: false, // 打印 + export: false, // 导出 + // custom: true, // 自定义列 + zoom: true, // 最大化最小化 + slots: { + buttons: 'toolbar-buttons', + }, + custom: { + // 自定义列-图标 + icon: 'vxe-icon-menu', + }, + }, + expandConfig: { + // expandAll: true, + }, + showOverflow: false, +}; diff --git a/apps/web-antd/src/views/business/order/product-order/index.vue b/apps/web-antd/src/views/business/order/product-order/index.vue new file mode 100644 index 00000000..f7aa3897 --- /dev/null +++ b/apps/web-antd/src/views/business/order/product-order/index.vue @@ -0,0 +1,315 @@ + + + + diff --git a/apps/web-antd/src/views/business/product/service-pack/api/index.ts b/apps/web-antd/src/views/business/product/service-pack/api/index.ts new file mode 100644 index 00000000..65e6f535 --- /dev/null +++ b/apps/web-antd/src/views/business/product/service-pack/api/index.ts @@ -0,0 +1,49 @@ +import { requestClient } from '#/api/request'; + +const prefix = 'service-pack/'; +/** + * 分页查询用户列表 + * @param data + */ +export async function getServicePackList(data: any) { + return requestClient.get(`${prefix}list`, { params: data }); +} +/** + * 分页查询用户列表 + * @param data + */ +export async function getServicePackOption(data: any) { + return requestClient.get(`${prefix}option`, { params: data }); +} + +/** + * 获取产品服务包详情 + * @param id + */ +export async function getServicePackInfo(id: number) { + return requestClient.get(`${prefix}detail`, { params: { id } }); +} + +/** + * 新增产品服务包 + * @param data + */ +export async function createServicePack(data: Record) { + return requestClient.post(`${prefix}create`, data); +} + +/** + * 编辑产品服务包 + * @param data + */ +export async function updateServicePack(data: Record) { + return requestClient.post(`${prefix}update`, data); +} + +/** + * 删除产品服务包 + * @param data + */ +export async function deleteServicePack(data: Record) { + return requestClient.post(`${prefix}delete`, data); +} diff --git a/apps/web-antd/src/views/business/product/service-pack/components/modal.vue b/apps/web-antd/src/views/business/product/service-pack/components/modal.vue new file mode 100644 index 00000000..d1f64f8e --- /dev/null +++ b/apps/web-antd/src/views/business/product/service-pack/components/modal.vue @@ -0,0 +1,75 @@ + + diff --git a/apps/web-antd/src/views/business/product/service-pack/config/form.ts b/apps/web-antd/src/views/business/product/service-pack/config/form.ts new file mode 100644 index 00000000..b052affb --- /dev/null +++ b/apps/web-antd/src/views/business/product/service-pack/config/form.ts @@ -0,0 +1,243 @@ +import type { VbenFormProps } from '#/adapter/form'; + +import { useUserStore } from '@vben/stores'; + +import { getSupplierOption } from '#/views/system/supplier/api'; + +const userStore = useUserStore(); + +export const modalFormProps: VbenFormProps = { + wrapperClass: 'grid-cols-12', // 24栅格, + commonConfig: { + formItemClass: 'col-span-12', + // 所有表单项 + componentProps: { + class: 'w-full', + }, + }, + // handleSubmit: onSubmit, + layout: 'horizontal', + schema: [ + { + component: 'VbenInput', + fieldName: 'id', + label: 'ID', + formItemClass: 'col-span-6', + dependencies: { + show: false, + triggerFields: ['id'], + }, + }, + { + component: 'VbenInput', + fieldName: 'showSelect', + label: 'ID', + formItemClass: 'col-span-6', + dependencies: { + show: false, + triggerFields: ['id'], + }, + }, + { + component: 'Avatar', + fieldName: 'image', + label: '产品图片', + rules: 'required', + formItemClass: 'col-span-6', + }, + { + component: 'ApiSelect', + componentProps: { + allowClear: true, + showSearch: true, + filterOption: (input: string, option: any) => { + // 自定义过滤逻辑,确保可以根据 name 进行搜索 + return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0; + }, + // 菜单接口转options格式 + afterFetch: (data: { id: number; name: string }[]) => { + return data.map((item: any) => ({ + label: item.name, + value: item.id, + })); + }, + api: getSupplierOption, + placeholder: '请选择', + }, + dependencies: { + disabled() { + return userStore?.userInfo?.roles?.user_type === 3; + }, + triggerFields: ['supplier_id'], + }, + fieldName: 'supplier_id', + formItemClass: 'col-span-6', + label: '所属供应商', + rules: 'required', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入服务包昵称', + }, + fieldName: 'drug_name', + formItemClass: 'col-span-6', + label: '服务包昵称', + rules: 'required', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入服务包别名', + }, + fieldName: 'drug_alias', + formItemClass: 'col-span-6', + label: '服务包别名', + rules: 'required', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入服务包编号', + }, + fieldName: 'drug_number', + formItemClass: 'col-span-6', + label: '服务包编号', + rules: 'required', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入功能', + }, + fieldName: 'function', + formItemClass: 'col-span-6', + label: '功能', + rules: 'required', + }, + // { + // component: 'VbenInput', + // componentProps: { + // placeholder: '请输入规格', + // }, + // fieldName: 'specification', + // formItemClass: 'col-span-6', + // label: '规格', + // rules: 'required', + // }, + { + component: 'VbenSelect', + componentProps: { + placeholder: '请选择规格', + options: [ + { + label: '7天', + value: '7天', + }, + { + label: '14天', + value: '14天', + }, + { + label: '30天', + value: '30天', + }, + { + label: '60天', + value: '60天', + }, + { + label: '90天', + value: '90天', + }, + { + label: '180天', + value: '180天', + }, + ], + }, + fieldName: 'specification', + formItemClass: 'col-span-6', + label: '规格', + rules: 'required', + }, + { + component: 'RadioGroup', + componentProps: { + placeholder: '请选择', + options: [ + { + label: '草稿', + value: 1, + }, + { + label: '下架', + value: 2, + }, + { + label: '上架', + value: 3, + }, + ], + }, + defaultValue: 1, + formItemClass: 'col-span-6', + fieldName: 'status', + label: '商品状态', + rules: 'required', + }, + { + component: 'InputNumber', + componentProps: { + placeholder: '请输入平台分成', + }, + suffix: () => '%', + defaultValue: 15, + fieldName: 'platform_ledger', + formItemClass: 'col-span-4', + label: '平台分成', + rules: 'required', + }, + { + component: 'InputNumber', + componentProps: { + placeholder: '请输入诊所分成', + }, + suffix: () => '%', + defaultValue: 35, + fieldName: 'store_ledger', + formItemClass: 'col-span-4', + label: '诊所分成', + rules: 'required', + }, + { + component: 'InputNumber', + componentProps: { + placeholder: '请输入供应商分成', + }, + suffix: () => '%', + defaultValue: 50, + fieldName: 'supplier_ledger', + formItemClass: 'col-span-4', + label: '供应商分成', + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { + placeholder: '请输入服务包介绍', + }, + fieldName: 'small_info', + label: '服务包介绍', + rules: 'required', + }, + { + component: 'Avatar', + fieldName: 'instruction', + label: '产品说明书', + rules: 'required', + formItemClass: 'col-span-6', + }, + ], + showDefaultActions: false, +}; diff --git a/apps/web-antd/src/views/business/product/service-pack/config/search.ts b/apps/web-antd/src/views/business/product/service-pack/config/search.ts new file mode 100644 index 00000000..834bb63c --- /dev/null +++ b/apps/web-antd/src/views/business/product/service-pack/config/search.ts @@ -0,0 +1,37 @@ +import type { VbenFormProps } from '#/adapter/form'; + +// import dayjs from 'dayjs'; + +export const formOptions: VbenFormProps = { + // 默认展开 + collapsed: false, + schema: [ + { + component: 'VbenInput', + componentProps: { + placeholder: '输入名称', + }, + defaultValue: '', + fieldName: 'name', + label: '产品服务包名称', + }, + // { + // component: 'RangePicker', + // componentProps: { + // format: 'YYYY-MM-DD', // 确保格式与你需要的一致 + // }, + // // defaultValue: [dayjs().startOf('month'), dayjs()], + // fieldName: 'search_time', + // label: '时间范围', + // }, + ], + // 控制表单是否显示折叠按钮 + showCollapseButton: true, + submitButtonOptions: { + content: '查询', + }, + // 是否在字段值改变时提交表单 + submitOnChange: true, + // 按下回车时是否提交表单 + submitOnEnter: false, +}; diff --git a/apps/web-antd/src/views/business/product/service-pack/config/table.ts b/apps/web-antd/src/views/business/product/service-pack/config/table.ts new file mode 100644 index 00000000..5d1085f8 --- /dev/null +++ b/apps/web-antd/src/views/business/product/service-pack/config/table.ts @@ -0,0 +1,84 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; + +import { getServicePackList } from '../api'; + +interface RowType { + id: string; + name: string; + logo: string; + introduce: string; + created_at: string; +} + +export const gridOptions: VxeGridProps = { + checkboxConfig: { + highlight: true, + labelField: '', + }, + columnConfig: { + useKey: true, + }, + rowConfig: { + useKey: true, + }, + columns: [ + { type: 'checkbox', width: 60 }, + { field: 'id', align: 'left', title: 'ID', width: 100 }, + { field: 'drug_name', align: 'left', title: '服务包名称' }, + { field: 'pinyin_simple', title: '拼音' }, + { field: 'supplier.name', title: '供应商' }, + { + field: 'image', + align: 'left', + title: '产品图片', + slots: { default: 'image' }, + width: 130, + }, + { + field: 'instruction', + align: 'left', + title: '说明书', + slots: { default: 'instruction' }, + width: 130, + }, + { field: 'function', title: '主要功能' }, + { field: 'specification', title: '规格' }, + { field: 'status', title: '状态', slots: { default: 'status' } }, + { field: 'created_at', title: '发布时间' }, + { type: 'html', title: '操作', slots: { default: 'action' } }, + ], + keepSource: true, + pagerConfig: {}, + proxyConfig: { + ajax: { + // 请求后端接口方法 + query: async ({ page }, formValues) => { + return await getServicePackList({ + page: page.currentPage, + pageSize: page.pageSize, + ...formValues, + }); + }, + }, + }, + height: 'auto', + border: false, + toolbarConfig: { + // 是否显示搜索表单控制按钮 + // @ts-ignore 正式环境时有完整的类型声明 + search: true, + refresh: true, // 刷新 + print: false, // 打印 + export: false, // 导出 + // custom: true, // 自定义列 + zoom: true, // 最大化最小化 + slots: { + buttons: 'toolbar-buttons', + }, + custom: { + // 自定义列-图标 + icon: 'vxe-icon-menu', + }, + }, + showOverflow: false, +}; diff --git a/apps/web-antd/src/views/business/product/service-pack/index.vue b/apps/web-antd/src/views/business/product/service-pack/index.vue new file mode 100644 index 00000000..b67a04d5 --- /dev/null +++ b/apps/web-antd/src/views/business/product/service-pack/index.vue @@ -0,0 +1,163 @@ + + + diff --git a/apps/web-antd/src/views/dashboard/workspace/index.vue b/apps/web-antd/src/views/dashboard/workspace/index.vue index b95d6138..2c532c02 100644 --- a/apps/web-antd/src/views/dashboard/workspace/index.vue +++ b/apps/web-antd/src/views/dashboard/workspace/index.vue @@ -17,6 +17,7 @@ import { WorkbenchTodo, WorkbenchTrends, } from '@vben/common-ui'; +import { LogoSvgICON } from '@vben/icons'; import { preferences } from '@vben/preferences'; import { useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; @@ -30,59 +31,77 @@ const userStore = useUserStore(); // 例如:url: /dashboard/workspace const projectItems: WorkbenchProjectItem[] = [ { - color: '', + color: 'blue', content: '不要等待机会,而要创造机会。', date: '2021-04-01', - group: '开源组', - icon: 'carbon:logo-github', - title: 'Github', - url: 'https://github.com', + group: '萧康云医科技', + icon: LogoSvgICON, + title: '萧康后台管理系统-旧版本', + url: 'https://admin.xiaokang88.com', }, { - color: '#3fb27f', - content: '现在的你决定将来的你。', + color: 'blue', + content: '数据可视化平台。', date: '2021-04-01', - group: '算法组', - icon: 'ion:logo-vue', - title: 'Vue', - url: 'https://vuejs.org', - }, - { - color: '#e18525', - content: '没有什么才能比努力更重要。', - date: '2021-04-01', - group: '上班摸鱼', - icon: 'ion:logo-html5', - title: 'Html5', - url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML', - }, - { - color: '#bf0c2c', - content: '热情和欲望可以突破一切难关。', - date: '2021-04-01', - group: 'UI', - icon: 'ion:logo-angular', - title: 'Angular', - url: 'https://angular.io', - }, - { - color: '#00d8ff', - content: '健康的身体是实现目标的基石。', - date: '2021-04-01', - group: '技术牛', - icon: 'bx:bxl-react', - title: 'React', - url: 'https://reactjs.org', - }, - { - color: '#EBD94E', - content: '路是走出来的,而不是空想出来的。', - date: '2021-04-01', - group: '架构组', - icon: 'ion:logo-javascript', - title: 'Js', - url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript', + group: '萧康云医科技', + icon: 'fluent-color:data-bar-vertical-ascending-20', + title: '数据大屏', + url: 'http://dls.xiaokang88.com', }, + // { + // color: '', + // content: '不要等待机会,而要创造机会。', + // date: '2021-04-01', + // group: '开源组', + // icon: 'carbon:logo-github', + // title: 'Github', + // url: 'https://github.com', + // }, + // { + // color: '#3fb27f', + // content: '现在的你决定将来的你。', + // date: '2021-04-01', + // group: '算法组', + // icon: 'ion:logo-vue', + // title: 'Vue', + // url: 'https://vuejs.org', + // }, + // { + // color: '#e18525', + // content: '没有什么才能比努力更重要。', + // date: '2021-04-01', + // group: '上班摸鱼', + // icon: 'ion:logo-html5', + // title: 'Html5', + // url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML', + // }, + // { + // color: '#bf0c2c', + // content: '热情和欲望可以突破一切难关。', + // date: '2021-04-01', + // group: 'UI', + // icon: 'ion:logo-angular', + // title: 'Angular', + // url: 'https://angular.io', + // }, + // { + // color: '#00d8ff', + // content: '健康的身体是实现目标的基石。', + // date: '2021-04-01', + // group: '技术牛', + // icon: 'bx:bxl-react', + // title: 'React', + // url: 'https://reactjs.org', + // }, + // { + // color: '#EBD94E', + // content: '路是走出来的,而不是空想出来的。', + // date: '2021-04-01', + // group: '架构组', + // icon: 'ion:logo-javascript', + // title: 'Js', + // url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript', + // }, ]; // 同样,这里的 url 也可以使用以 http 开头的外部链接 @@ -102,26 +121,26 @@ const quickNavItems: WorkbenchQuickNavItem[] = [ { color: '#e18525', icon: 'ion:layers-outline', - title: '组件', - url: '/demos/features/icons', - }, - { - color: '#3fb27f', - icon: 'ion:settings-outline', - title: '系统管理', - url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整 + title: '管理员管理', + url: '/system/user', }, + // { + // color: '#3fb27f', + // icon: 'ion:settings-outline', + // title: '系统管理', + // url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整 + // }, { color: '#4daf1bc9', icon: 'ion:key-outline', - title: '权限管理', - url: '/demos/access/page-control', + title: '平台管理', + url: '/platform', }, { color: '#00d8ff', icon: 'ion:bar-chart-outline', - title: '图表', - url: '/analytics', + title: '供应商管理', + url: '/supplier', }, ]; @@ -159,59 +178,59 @@ const todoItems = ref([ ]); const trendItems: WorkbenchTrendItem[] = [ { - avatar: 'svg:avatar-1', - content: `在 开源组 创建了项目 Vue`, - date: '刚刚', - title: '威廉', - }, - { - avatar: 'svg:avatar-2', - content: `关注了 威廉 `, - date: '1个小时前', - title: '艾文', - }, - { - avatar: 'svg:avatar-3', - content: `发布了 个人动态 `, - date: '1天前', - title: '克里斯', - }, - { - avatar: 'svg:avatar-4', - content: `发表文章 如何编写一个Vite插件 `, - date: '2天前', - title: 'Vben', - }, - { - avatar: 'svg:avatar-1', - content: `回复了 杰克 的问题 如何进行项目优化?`, - date: '3天前', - title: '皮特', - }, - { - avatar: 'svg:avatar-2', - content: `关闭了问题 如何运行项目 `, - date: '1周前', - title: '杰克', - }, - { - avatar: 'svg:avatar-3', - content: `发布了 个人动态 `, - date: '1周前', - title: '威廉', - }, - { - avatar: 'svg:avatar-4', - content: `推送了代码到 Github`, - date: '2021-04-01 20:00', - title: '威廉', - }, - { - avatar: 'svg:avatar-4', - content: `发表文章 如何编写使用 Admin Vben `, - date: '2021-03-01 20:00', - title: 'Vben', + avatar: 'svg:logo', + content: `萧康云医上线啦!`, + date: '2025-1-7', + title: '萧康云医', }, + // { + // avatar: 'svg:avatar-2', + // content: `关注了 威廉 `, + // date: '1个小时前', + // title: '艾文', + // }, + // { + // avatar: 'svg:avatar-3', + // content: `发布了 个人动态 `, + // date: '1天前', + // title: '克里斯', + // }, + // { + // avatar: 'svg:avatar-4', + // content: `发表文章 如何编写一个Vite插件 `, + // date: '2天前', + // title: 'Vben', + // }, + // { + // avatar: 'svg:avatar-1', + // content: `回复了 杰克 的问题 如何进行项目优化?`, + // date: '3天前', + // title: '皮特', + // }, + // { + // avatar: 'svg:avatar-2', + // content: `关闭了问题 如何运行项目 `, + // date: '1周前', + // title: '杰克', + // }, + // { + // avatar: 'svg:avatar-3', + // content: `发布了 个人动态 `, + // date: '1周前', + // title: '威廉', + // }, + // { + // avatar: 'svg:avatar-4', + // content: `推送了代码到 Github`, + // date: '2021-04-01 20:00', + // title: '威廉', + // }, + // { + // avatar: 'svg:avatar-4', + // content: `发表文章 如何编写使用 Admin Vben `, + // date: '2021-03-01 20:00', + // title: 'Vben', + // }, ]; const router = useRouter(); @@ -231,6 +250,20 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); } } + +// 获取当天的日期和天气 +function getTodayDate() { + const today = new Date(); + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }; + return today.toLocaleDateString('zh-CN', options); +} +const todayDate = getTodayDate(); +