commit 5ce39851367ddb25674f7e0eb6773bcdb4dddda9
Author: li875147827 <3122907793@qq.com>
Date: Thu Feb 1 17:11:16 2024 +0800
初始化
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4fc13c7
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+root = true
+
+[*]
+indent_style = space
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{ts,js,vue,css}]
+indent_size = 2
diff --git a/.env b/.env
new file mode 100644
index 0000000..2dd71fd
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+# 打包路径 根据项目不同按需配置
+VITE_BASE_URL = /
+VITE_IS_REQUEST_PROXY = true
+VITE_API_URL = https://service-bv448zsw-1257786608.gz.apigw.tencentcs.com
+VITE_API_URL_PREFIX = /api
\ No newline at end of file
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..d475e5c
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,5 @@
+# 打包路径
+VITE_BASE_URL = /
+VITE_IS_REQUEST_PROXY = true
+VITE_API_URL = https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com
+VITE_API_URL_PREFIX = /api
\ No newline at end of file
diff --git a/.env.site b/.env.site
new file mode 100644
index 0000000..7e1e855
--- /dev/null
+++ b/.env.site
@@ -0,0 +1,5 @@
+# 打包路径 根据项目不同按需配置
+VITE_BASE_URL = https://static.tdesign.tencent.com/starter/vue-next/
+VITE_IS_REQUEST_PROXY = true
+VITE_API_URL = https://service-bv448zsw-1257786608.gz.apigw.tencentcs.com
+VITE_API_URL_PREFIX = /api
\ No newline at end of file
diff --git a/.env.test b/.env.test
new file mode 100644
index 0000000..d278ac2
--- /dev/null
+++ b/.env.test
@@ -0,0 +1,5 @@
+# 打包路径 根据项目不同按需配置
+VITE_BASE_URL = /
+VITE_IS_REQUEST_PROXY = true
+VITE_API_URL = https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com
+VITE_API_URL_PREFIX = /api
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..66eb01a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,14 @@
+snapshot*
+dist
+lib
+es
+esm
+node_modules
+src/_common
+static
+cypress
+script/test/cypress
+_site
+temp*
+static/
+!.prettierrc.js
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..0ebc597
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,112 @@
+{
+ "extends": [
+ "plugin:@typescript-eslint/recommended",
+ "eslint-config-airbnb-base",
+ "@vue/typescript/recommended",
+ "plugin:vue/vue3-recommended",
+ "plugin:vue-scoped-css/base",
+ "plugin:prettier/recommended"
+ ],
+ "env": {
+ "browser": true,
+ "node": true,
+ "jest": true,
+ "es6": true
+ },
+ "globals": {
+ "defineProps": "readonly",
+ "defineEmits": "readonly"
+ },
+ "plugins": ["vue", "@typescript-eslint", "simple-import-sort"],
+ "parserOptions": {
+ "parser": "@typescript-eslint/parser",
+ "sourceType": "module",
+ "allowImportExportEverywhere": true,
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "settings": {
+ "import/extensions": [".js", ".jsx", ".ts", ".tsx"]
+ },
+ "rules": {
+ "no-console": "off",
+ "no-continue": "off",
+ "no-restricted-syntax": "off",
+ "no-plusplus": "off",
+ "no-param-reassign": "off",
+ "no-shadow": "off",
+ "guard-for-in": "off",
+
+ "import/extensions": "off",
+ "import/no-unresolved": "off",
+ "import/no-extraneous-dependencies": "off",
+ "import/prefer-default-export": "off",
+ "import/first": "off", // https://github.com/vuejs/vue-eslint-parser/issues/58
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "vue/first-attribute-linebreak": 0,
+
+
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ "argsIgnorePattern": "^_",
+ "varsIgnorePattern": "^_"
+ }
+ ],
+ "no-unused-vars": [
+ "error",
+ {
+ "argsIgnorePattern": "^_",
+ "varsIgnorePattern": "^_"
+ }
+ ],
+ "no-use-before-define": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/ban-types": "off",
+ "class-methods-use-this": "off", // 因为AxiosCancel必须实例化而能静态化所以加的规则,如果有办法解决可以取消
+ "simple-import-sort/imports": "error",
+ "simple-import-sort/exports": "error"
+ },
+ "overrides": [
+ {
+ "files": ["*.vue"],
+ "rules": {
+ "vue/component-name-in-template-casing": [2, "kebab-case"],
+ "vue/require-default-prop": 0,
+ "vue/multi-word-component-names": 0,
+ "vue/no-reserved-props": 0,
+ "vue/no-v-html": 0,
+ "vue-scoped-css/enforce-style-type": ["error", { "allows": ["scoped"] }]
+ }
+ },
+ {
+ "files": ["*.ts", "*.tsx"], // https://github.com/typescript-eslint eslint-recommended
+ "rules": {
+ "constructor-super": "off", // ts(2335) & ts(2377)
+ "getter-return": "off", // ts(2378)
+ "no-const-assign": "off", // ts(2588)
+ "no-dupe-args": "off", // ts(2300)
+ "no-dupe-class-members": "off", // ts(2393) & ts(2300)
+ "no-dupe-keys": "off", // ts(1117)
+ "no-func-assign": "off", // ts(2539)
+ "no-import-assign": "off", // ts(2539) & ts(2540)
+ "no-new-symbol": "off", // ts(2588)
+ "no-obj-calls": "off", // ts(2349)
+ "no-redeclare": "off", // ts(2451)
+ "no-setter-return": "off", // ts(2408)
+ "no-this-before-super": "off", // ts(2376)
+ "no-undef": "off", // ts(2304)
+ "no-unreachable": "off", // ts(7027)
+ "no-unsafe-negation": "off", // ts(2365) & ts(2360) & ts(2358)
+ "no-var": "error", // ts transpiles let/const to var, so no need for vars any more
+ "prefer-const": "error", // ts provides better types with const
+ "prefer-rest-params": "error", // ts provides better types with rest args over arguments
+ "prefer-spread": "error", // ts transpiles spread to apply, so no need for manual apply
+ "valid-typeof": "off" // ts(2367)
+ }
+ }
+ ]
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..326caac
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+*.ts text eol=lf
+*.vue text eol=lf
+*.tsx text eol=lf
+*.jsx text eol=lf
+*.html text eol=lf
+*.json text eol=lf
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug-report.zh-CN.yml b/.github/ISSUE_TEMPLATE/bug-report.zh-CN.yml
new file mode 100644
index 0000000..549408b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.zh-CN.yml
@@ -0,0 +1,79 @@
+name: 反馈 Bug
+description: 通过 github 模板进行 Bug 反馈。
+title: "[组件名称] 描述问题的标题"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # 欢迎你的参与
+ tdesign-vue-next-starter 的 Issue 列表接受 bug 报告或是新功能请求。也可加入官方社区:
+
+ 在发布一个 Issue 前,请确保:
+ - 在 [常见问题](https://tdesign.tencent.com/about/faq)、[更新日志](https://github.com/Tencent/tdesign-vue-next-starter/blob/main/CHANGELOG.md) 和 [旧Issue列表](https://github.com/Tencent/tdesign-vue-next-starter/issues?q=is%3Aissue) 中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正)
+ - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。
+
+ - type: input
+ id: version
+ attributes:
+ label: tdesign-vue-next-starter 版本
+ description: 请检查在最新项目版本中能否重现此 issue。
+ placeholder: 请填写
+ validations:
+ required: true
+
+ - type: input
+ id: reproduce
+ attributes:
+ label: 重现链接
+ description: 请提供尽可能精简的 CodePen、CodeSandbox 或 GitHub 仓库的链接。请不要填无关链接,否则你的 Issue 将被关闭。
+ placeholder: 请填写
+
+ - type: textarea
+ id: reproduceSteps
+ attributes:
+ label: 重现步骤
+ description: 请清晰的描述重现该 Issue 的步骤,这能帮助我们快速定位问题。没有清晰重现步骤将不会被修复,标有 'need reproduction' 的 Issue 在 7 天内不提供相关步骤,将被关闭。
+ placeholder: 请填写
+
+ - type: textarea
+ id: expect
+ attributes:
+ label: 期望结果
+ placeholder: 请填写
+
+ - type: textarea
+ id: actual
+ attributes:
+ label: 实际结果
+ placeholder: 请填写
+
+ - type: input
+ id: frameworkVersion
+ attributes:
+ label: 框架版本
+ placeholder: Vue(3.2.0)
+
+ - type: input
+ id: browsersVersion
+ attributes:
+ label: 浏览器版本
+ placeholder: Chrome(8.213.231.123)
+
+ - type: input
+ id: systemVersion
+ attributes:
+ label: 系统版本
+ placeholder: MacOS(11.2.3)
+
+ - type: input
+ id: nodeVersion
+ attributes:
+ label: Node版本
+ placeholder: 请填写
+
+ - type: textarea
+ id: remarks
+ attributes:
+ label: 补充说明
+ description: 可以是遇到这个 bug 的业务场景、上下文等信息。
+ placeholder: 请填写
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..c2d6d05
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+ - name: 使用 issue-helper 新建
+ url: https://Tencent.github.io/tdesign/issue-helper/?lang=zh-CN&repo=Tencent/tdesign-vue-next-starter
+ about: 使用 https://Tencent.github.io/tdesign/issue-helper/ 创建 issue,其中包含 bug 和 feature,表单提交更加严格。
diff --git a/.github/ISSUE_TEMPLATE/feature-report.zh-CN.yml b/.github/ISSUE_TEMPLATE/feature-report.zh-CN.yml
new file mode 100644
index 0000000..4409353
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-report.zh-CN.yml
@@ -0,0 +1,30 @@
+name: 反馈新功能
+description: 通过 github 模板进行新功能反馈。
+title: "[组件名称] 描述问题的标题"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # 欢迎你的参与
+ tdesign-vue-next-starter 的 Issue 列表接受 bug 报告或是新功能请求。也可加入官方社区:
+
+ 在发布一个 Issue 前,请确保:
+ - 在 [常见问题](https://tdesign.tencent.com/about/faq)、[更新日志](https://github.com/Tencent/tdesign-vue-next-starter/blob/main/CHANGELOG.md) 和 [旧Issue列表](https://github.com/Tencent/tdesign-vue-next-starter/issues?q=is%3Aissue) 中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正)
+ - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。
+
+ - type: textarea
+ id: functionContent
+ attributes:
+ label: 这个功能解决了什么问题
+ description: 请详尽说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。我们将考虑添加在现有 API 无法轻松实现的功能。新功能的用例也应当足够常见。
+ placeholder: 请填写
+ validations:
+ required: true
+
+ - type: textarea
+ id: functionalExpectations
+ attributes:
+ label: 你建议的方案是什么
+ placeholder: 请填写
+ validations:
+ required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..a97d15e
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,52 @@
+
+
+### 🤔 这个 PR 的性质是?
+
+- [ ] 日常 bug 修复
+- [ ] 新特性提交
+- [ ] 文档改进
+- [ ] 演示代码改进
+- [ ] 组件样式/交互改进
+- [ ] CI/CD 改进
+- [ ] 重构
+- [ ] 代码风格优化
+- [ ] 测试用例
+- [ ] 分支合并
+- [ ] 其他
+
+### 🔗 相关 Issue
+
+
+
+### 💡 需求背景和解决方案
+
+
+
+### 📝 更新日志
+
+
+
+- fix(组件名称): 处理问题或特性描述 ...
+
+- [ ] 本条 PR 不需要纳入 Changelog
+
+### ☑️ 请求合并前的自查清单
+
+⚠️ 请自检并全部**勾选全部选项**。⚠️
+
+- [ ] 文档已补充或无须补充
+- [ ] 代码演示已提供或无须提供
+- [ ] TypeScript 定义已补充或无须补充
+- [ ] Changelog 已提供或无须提供
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..c37a937
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,20 @@
+# Basic dependabot.yml file with
+# minimum configuration for two package managers
+
+version: 2
+updates:
+ # Enable version updates for npm
+ - package-ecosystem: "npm"
+ # Look for `package.json` and `lock` files in the `root` directory
+ directory: "/"
+ # Check the npm registry for updates every day (weekdays)
+ schedule:
+ interval: "monthly"
+
+ # Enable version updates for Docker
+ - package-ecosystem: "docker"
+ # Look for a `Dockerfile` in the `root` directory
+ directory: "/"
+ # Check for updates once a week
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/issue-assignees.temp.yml b/.github/workflows/issue-assignees.temp.yml
new file mode 100644
index 0000000..83d5d6b
--- /dev/null
+++ b/.github/workflows/issue-assignees.temp.yml
@@ -0,0 +1,52 @@
+# force copy from tencent/tdesign
+name: Issue Add Assigness
+
+on:
+ issues:
+ types: [opened, edited]
+
+jobs:
+ mark-duplicate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: wow-actions/auto-comment@v1
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ issuesOpened: |
+ 👋 @{{ author }},感谢给 TDesign 提出了 issue。
+ 请根据 issue 模版确保背景信息的完善,我们将调查并尽快回复你。
+
+ # https://docs.github.com/cn/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issues
+ - uses: 94dreamer/issue-assignees@main
+ id: assignees
+ with:
+ project_name: ${{github.event.repository.name}}
+ issue_title: ${{github.event.issue.title}}
+
+ - run: echo ${{ steps.assignees.outputs.contributors }}
+ - name: Add assigness
+ if: steps.assignees.outputs.contributors != ''
+ uses: actions-cool/issues-helper@v3
+ with:
+ actions: 'add-assignees'
+ token: ${{ secrets.GITHUB_TOKEN }}
+ issue-number: ${{ github.event.issue.number }}
+ assignees: ${{ steps.assignees.outputs.contributors }}
+
+ - run: |
+ contributors=${{ steps.assignees.outputs.contributors }}
+ contributorstring=${contributors//,/ @}
+ echo "::set-output name=string::@$contributorstring"
+ id: contributors
+
+ - name: 通知贡献者
+ if: steps.assignees.outputs.contributors != ''
+ uses: actions-cool/maintain-one-comment@v2.0.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ body: |
+ ♥️ 有劳 ${{ steps.contributors.outputs.string }} 尽快确认问题。
+ 确认有效后将下一步计划和可能需要的时间回复给 @${{ github.event.issue.user.login }} 。
+
+ number: ${{ github.event.issue.number }}
+ body-include: ""
diff --git a/.github/workflows/issue-help-wanted.temp.yml b/.github/workflows/issue-help-wanted.temp.yml
new file mode 100644
index 0000000..94e9bbd
--- /dev/null
+++ b/.github/workflows/issue-help-wanted.temp.yml
@@ -0,0 +1,22 @@
+# force copy from tencent/tdesign
+name: Issue Help wanted
+on:
+ issues:
+ types:
+ - labeled
+jobs:
+ add-comment:
+ if: github.event.label.name == 'help wanted'
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - name: Add comment
+ uses: peter-evans/create-or-update-comment@v1
+ with:
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ 任何人都可以处理此问题。
+ **请务必在您的 `pull request` 中引用此问题。** :sparkles:
+ 感谢你的贡献! :sparkles:
+ reactions: heart
\ No newline at end of file
diff --git a/.github/workflows/issue-mark-duplicate.temp.yml b/.github/workflows/issue-mark-duplicate.temp.yml
new file mode 100644
index 0000000..3017723
--- /dev/null
+++ b/.github/workflows/issue-mark-duplicate.temp.yml
@@ -0,0 +1,19 @@
+# force copy from tencent/tdesign
+# 当在 issue 的 comment 回复类似 `Duplicate of #111` 这样的话,issue 将被自动打上 重复提交标签 并且 cloese
+name: Issue Mark Duplicate
+
+on:
+ issue_comment:
+ types: [created, edited]
+
+jobs:
+ mark-duplicate:
+ runs-on: ubuntu-latest
+ steps:
+ - name: mark-duplicate
+ uses: actions-cool/issues-helper@v2
+ with:
+ actions: "mark-duplicate"
+ token: ${{ secrets.GITHUB_TOKEN }}
+ duplicate-labels: "duplicate"
+ close-issue: true
diff --git a/.github/workflows/issue-reply.temp.yml b/.github/workflows/issue-reply.temp.yml
new file mode 100644
index 0000000..271a94f
--- /dev/null
+++ b/.github/workflows/issue-reply.temp.yml
@@ -0,0 +1,21 @@
+# force copy from tencent/tdesign
+# 当被打上 Need Reproduce 标签时候,自动提示需要重现实例
+
+name: ISSUE_REPLY
+
+on:
+ issues:
+ types: [labeled]
+
+jobs:
+ issue-reply:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Need Reproduce
+ if: github.event.label.name == 'Need Reproduce'
+ uses: actions-cool/issues-helper@v2
+ with:
+ actions: 'create-comment'
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ 你好 @${{ github.event.issue.user.login }}, 我们需要你提供一个在线的重现实例以便于我们帮你排查问题。你可以通过点击 [此处](https://codesandbox.io/) 创建一个 codesandbox 或者提供一个最小化的 GitHub 仓库。请确保选择准确的版本。
diff --git a/.github/workflows/issue-synchronize.temp.yml b/.github/workflows/issue-synchronize.temp.yml
new file mode 100644
index 0000000..4ca7de1
--- /dev/null
+++ b/.github/workflows/issue-synchronize.temp.yml
@@ -0,0 +1,17 @@
+# force copy from tencent/tdesign
+name: Issue Add Assigness
+
+on:
+ issues:
+ types: [opened, reopened]
+
+jobs:
+ mark-duplicate:
+ runs-on: ubuntu-latest
+ steps:
+ # https://docs.github.com/cn/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issues
+ - uses: 94dreamer/create-report@main
+ with:
+ wxhook: ${{ secrets.WX_HOOK_URL }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ type: 'issue'
\ No newline at end of file
diff --git a/.github/workflows/pr-spelling.template.yml b/.github/workflows/pr-spelling.template.yml
new file mode 100644
index 0000000..93e138e
--- /dev/null
+++ b/.github/workflows/pr-spelling.template.yml
@@ -0,0 +1,12 @@
+# force copy from tencent/tdesign
+name: pr-spell-check
+on: [pull_request]
+
+jobs:
+ run:
+ name: Spell Check with Typos
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Check spelling
+ uses: crate-ci/typos@master
diff --git a/.github/workflows/preview-publish.yml b/.github/workflows/preview-publish.yml
new file mode 100644
index 0000000..364f99c
--- /dev/null
+++ b/.github/workflows/preview-publish.yml
@@ -0,0 +1,15 @@
+# 文件名建议统一为 preview-publish
+# 应用 preview.yml 的 demo
+name: PREVIEW_PUBLISH
+
+on:
+ workflow_run:
+ workflows: ["MAIN_PULL_REQUEST"]
+ types:
+ - completed
+
+jobs:
+ call-preview:
+ uses: Tencent/tdesign/.github/workflows/preview.yml@main
+ secrets:
+ TDESIGN_SURGE_TOKEN: ${{ secrets.TDESIGN_SURGE_TOKEN }}
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 0000000..280a82f
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,14 @@
+# 文件名建议统一为 pull-request.yml
+# 应用 test-build.yml 的 demo
+
+name: MAIN_PULL_REQUEST
+
+on:
+ pull_request:
+ branches: [develop, main, site]
+ types: [opened, synchronize, reopened]
+
+jobs:
+ call-test-build:
+ uses: Tencent/tdesign/.github/workflows/test-build.yml@main
+# install lint
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..35bcab0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+node_modules
+.DS_Store
+
+# build files
+es/
+lib/
+dist/
+typings/
+
+_site
+package
+tmp*
+temp*
+coverage
+test-report.html
+.idea/
+yarn-error.log
+*.zip
+.history
+.stylelintcache
+
+.env.local
+.env.*.local
+
+# lock文件 请根据自身项目或团队需求选择具体的包管理工具 并移除具体的ignore的lock文件
+yarn.lock
+package-lock.json
+pnpm-lock.yaml
\ No newline at end of file
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 0000000..b02e0a7
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,8 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+if [[ "$OS" == "Windows_NT" ]]; then
+ npx.cmd --no-install commitlint -e $GIT_PARAMS
+else
+ npx --no-install commitlint -e $GIT_PARAMS
+fi
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..11709a7
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,8 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+if [[ "$OS" == "Windows_NT" ]]; then
+ npx.cmd lint-staged
+else
+ npx lint-staged
+fi
diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg
new file mode 100644
index 0000000..ab3a6e8
--- /dev/null
+++ b/.husky/prepare-commit-msg
@@ -0,0 +1,10 @@
+#!/bin/sh
+[[ "$(uname -a)" = *"MINGW64"* ]] && exit 0
+[ -n "$CI" ] && exit 0
+. "$(dirname "$0")/_/husky.sh"
+
+if [[ "$OS" == "Windows_NT" ]]; then
+ exec < /dev/tty && npx.cmd git-cz --hook || true
+else
+ exec < /dev/tty && npx git-cz --hook || true
+fi
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..d43847c
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+shamefully-hoist = true
+hoist = true
+engine-strict =true
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000..3c58064
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,39 @@
+module.exports = {
+ // 一行最多 120 字符..
+ printWidth: 120,
+ // 使用 2 个空格缩进
+ tabWidth: 2,
+ // 不使用缩进符,而使用空格
+ useTabs: false,
+ // 行尾需要有分号
+ semi: true,
+ // 使用单引号
+ singleQuote: true,
+ // 对象的 key 仅在必要时用引号
+ quoteProps: 'as-needed',
+ // jsx 不使用单引号,而使用双引号
+ jsxSingleQuote: false,
+ // 末尾需要有逗号
+ trailingComma: 'all',
+ // 大括号内的首尾需要空格
+ bracketSpacing: true,
+ // jsx 标签的反尖括号需要换行
+ jsxBracketSameLine: false,
+ // 箭头函数,只有一个参数的时候,也需要括号
+ arrowParens: 'always',
+ // 每个文件格式化的范围是文件的全部内容
+ rangeStart: 0,
+ rangeEnd: Infinity,
+ // 不需要写文件开头的 @prettier
+ requirePragma: false,
+ // 不需要自动在文件开头插入 @prettier
+ insertPragma: false,
+ // 使用默认的折行标准
+ proseWrap: 'preserve',
+ // 根据显示样式决定 html 要不要折行
+ htmlWhitespaceSensitivity: 'css',
+ // vue 文件中的 script 和 style 内不用缩进
+ vueIndentScriptAndStyle: false,
+ // 换行符使用 lf
+ endOfLine: 'lf',
+};
diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644
index 0000000..1b7da3c
--- /dev/null
+++ b/.stylelintignore
@@ -0,0 +1,8 @@
+# .stylelintignore
+# 旧的不需打包的样式库
+*.min.css
+
+# 其他类型文件
+*.js
+*.jpg
+*.woff
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..940260d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["dbaeumer.vscode-eslint"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..02a0913
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,36 @@
+{
+ "files.eol":"\n",
+ "editor.tabSize": 2,
+ "eslint.format.enable": true,
+ "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"],
+ "[vue]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[typescriptreact]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[javascriptreact]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[typescript]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[javascript]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "cSpell.words": [
+ "tdesign",
+ "tvision",
+ "echarts",
+ "nprogress",
+ "commitlint",
+ "stylelint",
+ "pinia",
+ "qrcode"
+ ],
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..81af071
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 TDesign
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README-zh_CN.md b/README-zh_CN.md
new file mode 100644
index 0000000..63b71ae
--- /dev/null
+++ b/README-zh_CN.md
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+简体中文 | [English](./README.md)
+
+### 项目简介
+
+TDesign Vue Next Starter 是一个基于 TDesign,使用 `Vue3`、`Vite`、`Pinia`、`TypeScript` 开发,可进行个性化主题配置,旨在提供项目开箱即用的、配置式的中后台项目。
+
+
+ 在线预览
+ ·
+ 使用文档
+
+
+
+
+
+### 特性
+
+- 内置多种常用的中后台页面
+- 完善的目录结构
+- 完善的代码规范配置
+- 支持暗黑模式
+- 自定义主题颜色
+- 多种空间布局
+- 内置 Mock 数据方案
+
+### 使用
+
+> 通过 `tdesign-starter-cli` 初始化项目仓库
+
+```bash
+## 1、安装 tdesign-starter-cli
+npm i tdesign-starter-cli@latest -g
+
+## 2、创建项目
+td-starter init
+```
+
+### 开发
+
+``` bash
+## 安装依赖
+npm install
+
+## 启动项目
+npm run dev
+```
+
+### 构建
+
+```bash
+## 构建正式环境
+npm run build
+
+## 构建测试环境
+npm run build:test
+```
+
+### 其他
+
+```bash
+## 预览构建产物
+npm run preview
+
+## 代码格式检查
+npm run lint
+
+## 代码格式检查与自动修复
+npm run lint:fix
+
+## style格式检查
+npm run stylelint
+
+## style格式检查与自动修复
+npm run stylelint:fix
+```
+
+### 如何贡献
+
+非常欢迎您的贡献!提交您的 [Issue](https://github.com/tencent/tdesign-vue-next-starter/issues/new/choose) 或者提交 [Pull Request](https://github.com/Tencent/tdesign-vue-next-starter/pulls)。
+
+#### 贡献提交规范
+
+- [Angular Convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
+- [Vue Style Guide](https://v3.vuejs.org/style-guide/#rule-categories)
+
+### 兼容性
+
+| [ ](http://godban.github.io/browsers-support-badges/) IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
+| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Edge >=84 | Firefox >=83 | Chrome >=84 | Safari >=14.1 |
+
+### 社区版本
+
+基于 TDesign Vue Next 的 starter-kit 有多种社区版本,访问 [社区链接](https://tdesign.tencent.com/starter/docs/vue-next/community-link) 可以访问更多版本。
+如果您也开发了 TDesign Starter 的社区版本,可以提交 Issue 或者直接给我们提Pull Request 😊。
+
+### 开源协议
+
+TDesign 遵循 [MIT 协议](https://github.com/Tencent/tdesign-vue-next-starter/LICENSE)。
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..823d502
--- /dev/null
+++ b/README.md
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+English | [简体中文](./README-zh_CN.md)
+### Introduction
+
+TDesign Vue Next Starter is a TDesign-based developed with `Vue 3`, `Vite`, `Pinia`, `TypeScript`. It can be customized theme configuration, and aims to provide project out-of-the-box, configuration-style middle and background projects.
+
+
+ Live Preview
+ ·
+ Documentation
+
+
+
+
+### Features
+
+- Various provided pages for develop
+- Complete directory structure for develop
+- Code specification configuration
+- Support dark mode
+- Custom theme colors
+- Various space layouts
+- Mock data scheme
+
+### Usage
+
+> Initialize project with our CLI tool `tdesign-starter-cli`
+
+```bash
+## install tdesign-starter-cli
+npm i tdesign-starter-cli@latest -g
+
+## create project
+td-starter init
+```
+
+### Develop
+
+```bash
+## install dependencies
+npm install
+
+## set up
+npm run dev
+```
+
+### Build
+
+```bash
+## build
+npm run build
+
+## build for test
+npm run build:test
+```
+
+
+### Contributing Guide
+
+We welcome contributions to our project. Create your [Issue](https://github.com/tencent/tdesign-vue-next-starter/issues/new/choose) or Submit your [Pull Request](https://github.com/Tencent/tdesign-vue-next-starter/pulls).
+
+#### Commit Specification
+
+- [Angular Convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
+- [Vue Style Guide](https://v3.vuejs.org/style-guide/#rule-categories)
+
+### Browser Support
+
+| [ ](http://godban.github.io/browsers-support-badges/) IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
+| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Edge >=84 | Firefox >=83 | Chrome >=84 | Safari >=14.1 |
+
+### Community Versions
+
+There are kinds of community versions of starter-kit based on TDesign Vue Next, visit [community-link](https://tdesign.tencent.com/starter/docs/vue-next/community-link) for more detail. If you developed a community versions of tdesign starter, please create a issue or submit a pull request to let us know 😊.
+
+### License
+
+The MIT License. Please see [the license file](LICENSE) for more information.
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 0000000..20116c9
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1,11 @@
+// commit-lint config
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'type-enum': [
+ 2,
+ 'always',
+ ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'types'],
+ ],
+ },
+};
diff --git a/docs/starter.png b/docs/starter.png
new file mode 100644
index 0000000..6c8d7e7
Binary files /dev/null and b/docs/starter.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..587b9b1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ TDesign Vue Next Starter
+
+
+
+
+
+
+
+
+
diff --git a/mock/index.ts b/mock/index.ts
new file mode 100644
index 0000000..d898fcc
--- /dev/null
+++ b/mock/index.ts
@@ -0,0 +1,368 @@
+import Mock from 'mockjs';
+import { MockMethod } from 'vite-plugin-mock';
+
+export default [
+ {
+ url: '/api/get-purchase-list',
+ method: 'get',
+ response: () => ({
+ code: 0,
+ data: {
+ ...Mock.mock({
+ 'list|1-100': [
+ {
+ index: /S20201228115950[0-9][0-9][0-9]/,
+ pdName: 'Macbook',
+ pdNum: 'p_tmp_60a637cd0d',
+ 'purchaseNum|1-100': 100,
+ adminName: '财务部111',
+ updateTime: '2020-05-20@date("HH:mm:ss")',
+ pdType: '电子产品',
+ },
+ {
+ index: /S20201228115950[0-9][0-9][0-9]/,
+ pdName: 'Macbook',
+ pdNum: 'p_tmp_60a637cd0d',
+ 'purchaseNum|1-100': 100,
+ adminName: '财务部',
+ updateTime: '2020-05-20@date("HH:mm:ss")',
+ },
+ ],
+ }),
+ },
+ }),
+ },
+ {
+ url: '/api/get-list',
+ method: 'get',
+ response: () => ({
+ code: 0,
+ data: {
+ ...Mock.mock({
+ 'list|1-100': [
+ {
+ 'index|+1': 1,
+ 'status|1': '@natural(0, 4)',
+ no: 'BH00@natural(01, 100)',
+ name: '@city()办公用品采购项目',
+ 'paymentType|1': '@natural(0, 1)',
+ 'contractType|1': '@natural(0, 2)',
+ updateTime: '2020-05-30 @date("HH:mm:ss")',
+ amount: '@natural(10, 500),000,000',
+ adminName: '@cname()',
+ },
+ ],
+ }),
+ },
+ }),
+ },
+ {
+ url: '/api/detail-basic',
+ method: 'get',
+ response: () => ({
+ code: 0,
+ data: {
+ ...Mock.mock({
+ name: 'td_20023747',
+ loginType: 'Web',
+ currentRole: 'Admin',
+ rightsList: '通用权限',
+ userStatus: '启用',
+ language: '简体中文',
+ timeZone: '(GMT+08:00)中国时区—北京(Asia/Beijing)',
+ }),
+ },
+ }),
+ },
+ {
+ url: '/api/get-card-list',
+ method: 'get',
+ response: () => ({
+ code: 0,
+ data: {
+ ...Mock.mock({
+ 'list|48-50': [
+ {
+ 'index|+1': 1,
+ isSetup: '@boolean',
+ 'type|1': '@natural(1, 5)',
+ 'banner|1': [
+ 'https://tdesign.gtimg.com/starter/cloud-db.jpg',
+ 'https://tdesign.gtimg.com/starter/cloud-server.jpg',
+ 'https://tdesign.gtimg.com/starter/ssl.jpg',
+ 'https://tdesign.gtimg.com/starter/t-sec.jpg',
+ 'https://tdesign.gtimg.com/starter/face-recognition.jpg',
+ ],
+ 'name|1': ['人脸识别', 'SSL证书', 'CVM', '云数据库', 'T-Sec 云防火墙'],
+ 'description|1': [
+ '基于腾讯优图强大的面部分析技术,提供包括人脸检测与分析、五官定位、人脸搜索、人脸比对、人脸',
+ '云硬盘为您提供用于CVM的持久性数据块级存储服务。云硬盘中的数据自动地可用区内以多副本冗',
+ 'SSL证书又叫服务器证书,腾讯云为您提供证书的一站式服务,包括免费、付费证书的申请、管理及部',
+ '腾讯安全云防火墙产品,是腾讯云安全团队结合云原生的优势,自主研发的SaaS化防火墙产品,无需客无需客无需客无需客无需客无需客无需客',
+ '云数据库MySQL为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。',
+ ],
+ },
+ ],
+ }),
+ },
+ }),
+ },
+ {
+ url: '/api/get-project-list',
+ method: 'get',
+ response: () => ({
+ code: 0,
+ data: {
+ ...Mock.mock({
+ 'list|1-50': [
+ {
+ 'index|+1': 1,
+ adminPhone: '+86 13587609955',
+ updateTime: '2020-05-30 @date("HH:mm:ss")',
+ 'adminName|1': ['顾娟 ', '常刚', '郑洋'],
+ 'name|1': [
+ '沧州市办公用品采购项目',
+ '红河哈尼族彝族自治州办公用品采购项目 ',
+ '铜川市办公用品采购项目',
+ '陇南市办公用品采购项目 ',
+ '六安市办公用品采购项目 ',
+ ],
+ },
+ ],
+ }),
+ },
+ }),
+ },
+ {
+ url: '/api/post',
+ method: 'post',
+ timeout: 2000,
+ response: {
+ code: 0,
+ data: {
+ name: 'vben',
+ },
+ },
+ },
+ {
+ url: '/api/get-menu-list-i18n',
+ method: 'get',
+ timeout: 2000,
+ response: {
+ code: 0,
+ data: {
+ ...Mock.mock({
+ list: [
+ {
+ path: '/list',
+ name: 'list',
+ component: 'LAYOUT',
+ redirect: '/list/base',
+ meta: {
+ title: {
+ zh_CN: '列表页',
+ en_US: 'List',
+ },
+ icon: 'view-list',
+ },
+ children: [
+ {
+ path: 'base',
+ name: 'ListBase',
+ component: '/list/base/index',
+ meta: {
+ title: {
+ zh_CN: '基础列表页',
+ en_US: 'Base List',
+ },
+ },
+ },
+ {
+ path: 'card',
+ name: 'ListCard',
+ component: '/list/card/index',
+ meta: {
+ title: {
+ zh_CN: '卡片列表页',
+ en_US: 'Card List',
+ },
+ },
+ },
+ {
+ path: 'filter',
+ name: 'ListFilter',
+ component: '/list/filter/index',
+ meta: {
+ title: {
+ zh_CN: '筛选列表页',
+ en_US: 'Filter List',
+ },
+ },
+ },
+ {
+ path: 'tree',
+ name: 'ListTree',
+ component: '/list/tree/index',
+ meta: {
+ title: {
+ zh_CN: '树状筛选列表页',
+ en_US: 'Tree List',
+ },
+ },
+ },
+ ],
+ },
+ {
+ path: '/form',
+ name: 'form',
+ component: 'LAYOUT',
+ redirect: '/form/base',
+ meta: {
+ title: {
+ zh_CN: '表单页',
+ en_US: 'Form',
+ },
+ icon: 'edit-1',
+ },
+ children: [
+ {
+ path: 'base',
+ name: 'FormBase',
+ component: '/form/base/index',
+ meta: {
+ title: {
+ zh_CN: '基础表单页',
+ en_US: 'Base Form',
+ },
+ },
+ },
+ {
+ path: 'step',
+ name: 'FormStep',
+ component: '/form/step/index',
+ meta: {
+ title: {
+ zh_CN: '分步表单页',
+ en_US: 'Step Form',
+ },
+ },
+ },
+ ],
+ },
+ {
+ path: '/detail',
+ name: 'detail',
+ component: 'LAYOUT',
+ redirect: '/detail/base',
+ meta: {
+ title: {
+ zh_CN: '详情页',
+ en_US: 'Detail',
+ },
+ icon: 'layers',
+ },
+ children: [
+ {
+ path: 'base',
+ name: 'DetailBase',
+ component: '/detail/base/index',
+ meta: {
+ title: {
+ zh_CN: '基础详情页',
+ en_US: 'Base Detail',
+ },
+ },
+ },
+ {
+ path: 'advanced',
+ name: 'DetailAdvanced',
+ component: '/detail/advanced/index',
+ meta: {
+ title: {
+ zh_CN: '多卡片详情页',
+ en_US: 'Card Detail',
+ },
+ },
+ },
+ {
+ path: 'deploy',
+ name: 'DetailDeploy',
+ component: '/detail/deploy/index',
+ meta: {
+ title: {
+ zh_CN: '数据详情页',
+ en_US: 'Data Detail',
+ },
+ },
+ },
+ {
+ path: 'secondary',
+ name: 'DetailSecondary',
+ component: '/detail/secondary/index',
+ meta: {
+ title: {
+ zh_CN: '二级详情页',
+ en_US: 'Secondary Detail',
+ },
+ },
+ },
+ ],
+ },
+ {
+ path: '/frame',
+ name: 'Frame',
+ component: 'Layout',
+ redirect: '/frame/doc',
+ meta: {
+ icon: 'internet',
+ title: {
+ zh_CN: '外部页面',
+ en_US: 'External',
+ },
+ },
+ children: [
+ {
+ path: 'doc',
+ name: 'Doc',
+ component: 'IFrame',
+ meta: {
+ frameSrc: 'https://tdesign.tencent.com/starter/docs/vue-next/get-started',
+ title: {
+ zh_CN: '使用文档(内嵌)',
+ en_US: 'Documentation(IFrame)',
+ },
+ },
+ },
+ {
+ path: 'TDesign',
+ name: 'TDesign',
+ component: 'IFrame',
+ meta: {
+ frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
+ title: {
+ zh_CN: 'TDesign 文档(内嵌)',
+ en_US: 'TDesign (IFrame)',
+ },
+ },
+ },
+ {
+ path: 'TDesign2',
+ name: 'TDesign2',
+ component: 'IFrame',
+ meta: {
+ frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
+ frameBlank: true,
+ title: {
+ zh_CN: 'TDesign 文档(外链',
+ en_US: 'TDesign Doc(Link)',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ },
+ },
+ },
+] as MockMethod[];
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..69f8dc3
--- /dev/null
+++ b/package.json
@@ -0,0 +1,95 @@
+{
+ "name": "@tencent/tdesign-vue-next-starter",
+ "version": "0.9.0",
+ "scripts": {
+ "dev:mock": "vite --open --mode mock",
+ "dev": "vite --open --mode development",
+ "dev:linux": "vite --mode development",
+ "build:test": "vite build --mode test",
+ "build": "vue-tsc --noEmit && vite build --mode release",
+ "build:site": "vue-tsc --noEmit && vite build --mode site",
+ "preview": "vite preview",
+ "lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
+ "lint:fix": "eslint --ext .vue,.js,jsx,.ts,.tsx ./ --max-warnings 0 --fix",
+ "stylelint": "stylelint src/**/*.{html,vue,sass,less}",
+ "stylelint:fix": "stylelint --fix src/**/*.{html,vue,css,sass,less}",
+ "prepare": "husky install",
+ "site:preview": "npm run build && cp -r dist _site",
+ "test": "echo \"no test specified,work in process\"",
+ "test:coverage": "echo \"no test:coverage specified,work in process\""
+ },
+ "dependencies": {
+ "@vueuse/core": "^10.6.1",
+ "axios": "^1.6.2",
+ "dayjs": "^1.11.10",
+ "echarts": "5.1.2",
+ "lodash": "^4.17.21",
+ "nprogress": "^0.2.0",
+ "pinia": "^2.1.7",
+ "pinia-plugin-persistedstate": "^3.2.0",
+ "qrcode.vue": "^3.4.1",
+ "qs": "^6.11.2",
+ "tdesign-icons-vue-next": "^0.2.2",
+ "tdesign-vue-next": "^1.6.8",
+ "tvision-color": "^1.6.0",
+ "vue": "~3.3.8",
+ "vue-i18n": "^9.6.5",
+ "vue-router": "~4.2.4"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^18.4.1",
+ "@commitlint/config-conventional": "^18.4.0",
+ "@types/echarts": "^4.9.21",
+ "@types/lodash": "^4.14.201",
+ "@types/nprogress": "^0.2.3",
+ "@types/qs": "^6.9.10",
+ "@typescript-eslint/eslint-plugin": "^6.11.0",
+ "@typescript-eslint/parser": "^6.11.0",
+ "@vitejs/plugin-vue": "^4.4.1",
+ "@vitejs/plugin-vue-jsx": "^3.0.2",
+ "@vue/compiler-sfc": "^3.3.8",
+ "@vue/eslint-config-typescript": "^12.0.0",
+ "commitizen": "^4.3.0",
+ "cz-conventional-changelog": "^3.3.0",
+ "eslint": "^8.53.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-import": "^2.29.0",
+ "eslint-plugin-prettier": "^5.0.1",
+ "eslint-plugin-simple-import-sort": "^10.0.0",
+ "eslint-plugin-vue": "^9.18.1",
+ "eslint-plugin-vue-scoped-css": "^2.5.1",
+ "husky": "^8.0.3",
+ "less": "^4.2.0",
+ "lint-staged": "^15.1.0",
+ "mockjs": "^1.1.0",
+ "postcss-html": "^1.5.0",
+ "postcss-less": "^6.0.0",
+ "prettier": "^3.1.0",
+ "stylelint": "~15.11.0",
+ "stylelint-config-standard": "^34.0.0",
+ "stylelint-order": "~6.0.3",
+ "typescript": "~5.3.2",
+ "vite": "^4.5.0",
+ "vite-plugin-mock": "^3.0.0",
+ "vite-svg-loader": "^4.0.0",
+ "vue-tsc": "^1.8.22"
+ },
+ "config": {
+ "commitizen": {
+ "path": "./node_modules/cz-conventional-changelog"
+ }
+ },
+ "lint-staged": {
+ "*.{js,jsx,vue,ts,tsx}": [
+ "prettier --write",
+ "npm run lint:fix"
+ ],
+ "*.{html,vue,css,sass,less}": [
+ "npm run stylelint:fix"
+ ]
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+}
\ No newline at end of file
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..086ac80
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..b291f91
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/src/api/detail.ts b/src/api/detail.ts
new file mode 100644
index 0000000..858c7ff
--- /dev/null
+++ b/src/api/detail.ts
@@ -0,0 +1,19 @@
+import type { ProjectListResult, PurchaseListResult } from '@/api/model/detailModel';
+import { request } from '@/utils/request';
+
+const Api = {
+ PurchaseList: '/get-purchase-list',
+ ProjectList: '/get-project-list',
+};
+
+export function getPurchaseList() {
+ return request.get({
+ url: Api.PurchaseList,
+ });
+}
+
+export function getProjectList() {
+ return request.get({
+ url: Api.ProjectList,
+ });
+}
diff --git a/src/api/list.ts b/src/api/list.ts
new file mode 100644
index 0000000..ab30e2b
--- /dev/null
+++ b/src/api/list.ts
@@ -0,0 +1,19 @@
+import type { CardListResult, ListResult } from '@/api/model/listModel';
+import { request } from '@/utils/request';
+
+const Api = {
+ BaseList: '/get-list',
+ CardList: '/get-card-list',
+};
+
+export function getList() {
+ return request.get({
+ url: Api.BaseList,
+ });
+}
+
+export function getCardList() {
+ return request.get({
+ url: Api.CardList,
+ });
+}
diff --git a/src/api/model/detailModel.ts b/src/api/model/detailModel.ts
new file mode 100644
index 0000000..c144b48
--- /dev/null
+++ b/src/api/model/detailModel.ts
@@ -0,0 +1,23 @@
+export interface PurchaseListResult {
+ list: Array;
+}
+export interface PurchaseInfo {
+ adminName: string;
+ index: string;
+ pdName: string;
+ pdNum: string;
+ pdType: string;
+ purchaseNum: number;
+ updateTime: Date;
+}
+
+export interface ProjectListResult {
+ list: Array;
+}
+export interface ProjectInfo {
+ adminName: string;
+ adminPhone: string;
+ index: number;
+ name: string;
+ updateTime: Date;
+}
diff --git a/src/api/model/listModel.ts b/src/api/model/listModel.ts
new file mode 100644
index 0000000..453114d
--- /dev/null
+++ b/src/api/model/listModel.ts
@@ -0,0 +1,26 @@
+export interface ListResult {
+ list: Array;
+}
+export interface ListModel {
+ adminName: string;
+ amount: string;
+ contractType: number;
+ index: number;
+ name: string;
+ no: string;
+ paymentType: number;
+ status: number;
+ updateTime: Date;
+}
+
+export interface CardListResult {
+ list: Array;
+}
+export interface CardList {
+ banner: string;
+ description: string;
+ index: number;
+ isSetup: boolean;
+ name: string;
+ type: number;
+}
diff --git a/src/api/model/permissionModel.ts b/src/api/model/permissionModel.ts
new file mode 100644
index 0000000..dad83b9
--- /dev/null
+++ b/src/api/model/permissionModel.ts
@@ -0,0 +1,22 @@
+import { defineComponent } from 'vue';
+
+import { RouteMeta } from '@/types/interface';
+
+export interface MenuListResult {
+ list: Array;
+}
+
+export type Component =
+ | ReturnType
+ | (() => Promise)
+ | (() => Promise);
+
+export interface RouteItem {
+ path: string;
+ name: string;
+ component?: Component | string;
+ components?: Component;
+ redirect?: string;
+ meta: RouteMeta;
+ children?: Array;
+}
diff --git a/src/api/permission.ts b/src/api/permission.ts
new file mode 100644
index 0000000..a6f50d8
--- /dev/null
+++ b/src/api/permission.ts
@@ -0,0 +1,12 @@
+import type { MenuListResult } from '@/api/model/permissionModel';
+import { request } from '@/utils/request';
+
+const Api = {
+ MenuList: '/get-menu-list-i18n',
+};
+
+export function getMenuList() {
+ return request.get({
+ url: Api.MenuList,
+ });
+}
diff --git a/src/assets/assets-empty.svg b/src/assets/assets-empty.svg
new file mode 100644
index 0000000..374d27d
--- /dev/null
+++ b/src/assets/assets-empty.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/assets-login-bg-black.png b/src/assets/assets-login-bg-black.png
new file mode 100644
index 0000000..b4dde4b
Binary files /dev/null and b/src/assets/assets-login-bg-black.png differ
diff --git a/src/assets/assets-login-bg-white.png b/src/assets/assets-login-bg-white.png
new file mode 100644
index 0000000..4275549
Binary files /dev/null and b/src/assets/assets-login-bg-white.png differ
diff --git a/src/assets/assets-logo-full.svg b/src/assets/assets-logo-full.svg
new file mode 100644
index 0000000..94f0049
--- /dev/null
+++ b/src/assets/assets-logo-full.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/assets-product-1.svg b/src/assets/assets-product-1.svg
new file mode 100644
index 0000000..07c105e
--- /dev/null
+++ b/src/assets/assets-product-1.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-product-2.svg b/src/assets/assets-product-2.svg
new file mode 100644
index 0000000..f6a13ac
--- /dev/null
+++ b/src/assets/assets-product-2.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-product-3.svg b/src/assets/assets-product-3.svg
new file mode 100644
index 0000000..96f8c0e
--- /dev/null
+++ b/src/assets/assets-product-3.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-product-4.svg b/src/assets/assets-product-4.svg
new file mode 100644
index 0000000..6005e4c
--- /dev/null
+++ b/src/assets/assets-product-4.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-result-403.svg b/src/assets/assets-result-403.svg
new file mode 100644
index 0000000..343a25e
--- /dev/null
+++ b/src/assets/assets-result-403.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-result-404.svg b/src/assets/assets-result-404.svg
new file mode 100644
index 0000000..db1cb36
--- /dev/null
+++ b/src/assets/assets-result-404.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-result-500.svg b/src/assets/assets-result-500.svg
new file mode 100644
index 0000000..08784af
--- /dev/null
+++ b/src/assets/assets-result-500.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-result-ie.svg b/src/assets/assets-result-ie.svg
new file mode 100644
index 0000000..ebaf5ba
--- /dev/null
+++ b/src/assets/assets-result-ie.svg
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-result-maintenance.svg b/src/assets/assets-result-maintenance.svg
new file mode 100644
index 0000000..388fd0b
--- /dev/null
+++ b/src/assets/assets-result-maintenance.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-result-wifi.svg b/src/assets/assets-result-wifi.svg
new file mode 100644
index 0000000..eeee8b5
--- /dev/null
+++ b/src/assets/assets-result-wifi.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-setting-auto.svg b/src/assets/assets-setting-auto.svg
new file mode 100644
index 0000000..66f57de
--- /dev/null
+++ b/src/assets/assets-setting-auto.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-setting-dark.svg b/src/assets/assets-setting-dark.svg
new file mode 100644
index 0000000..0f15649
--- /dev/null
+++ b/src/assets/assets-setting-dark.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-setting-light.svg b/src/assets/assets-setting-light.svg
new file mode 100644
index 0000000..ffa8f34
--- /dev/null
+++ b/src/assets/assets-setting-light.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/assets-t-logo.svg b/src/assets/assets-t-logo.svg
new file mode 100644
index 0000000..3f95cba
--- /dev/null
+++ b/src/assets/assets-t-logo.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/assets-tencent-logo.png b/src/assets/assets-tencent-logo.png
new file mode 100644
index 0000000..26fdb92
Binary files /dev/null and b/src/assets/assets-tencent-logo.png differ
diff --git a/src/components/color/index.vue b/src/components/color/index.vue
new file mode 100644
index 0000000..f62207a
--- /dev/null
+++ b/src/components/color/index.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
diff --git a/src/components/common-table/index.vue b/src/components/common-table/index.vue
new file mode 100644
index 0000000..05cd4a8
--- /dev/null
+++ b/src/components/common-table/index.vue
@@ -0,0 +1,338 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('components.commonTable.query') }}
+
+ {{ $t('components.commonTable.reset') }}
+
+
+
+
+
+
+
+
+ {{ $t('components.commonTable.contractStatusEnum.fail') }}
+
+
+ {{ $t('components.commonTable.contractStatusEnum.audit') }}
+
+
+ {{ $t('components.commonTable.contractStatusEnum.pending') }}
+
+
+ {{ $t('components.commonTable.contractStatusEnum.executing') }}
+
+
+ {{ $t('components.commonTable.contractStatusEnum.finish') }}
+
+
+
+ {{ $t('pages.listBase.contractStatusEnum.fail') }}
+ {{ $t('pages.listBase.contractStatusEnum.audit') }}
+
+ {{ $t('pages.listBase.contractStatusEnum.pending') }}
+
+
+
+
+ {{ $t('pages.listBase.pay') }}
+
+
+ {{ $t('pages.listBase.receive') }}
+
+
+
+
+ {{ $t('pages.listBase.detail') }}
+ {{ $t('pages.listBase.delete') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/product-card/index.vue b/src/components/product-card/index.vue
new file mode 100644
index 0000000..e27eb31
--- /dev/null
+++ b/src/components/product-card/index.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ product.isSetup ? $t('components.isSetup.on') : $t('components.isSetup.off')
+ }}
+
+
+ {{ product.name }}
+ {{ product.description }}
+
+
+
+ {{ typeMap[product.type - 1] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/result/index.vue b/src/components/result/index.vue
new file mode 100644
index 0000000..74045b8
--- /dev/null
+++ b/src/components/result/index.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
{{ title }}
+
{{ tip }}
+
+
+
+
+
diff --git a/src/components/thumbnail/index.vue b/src/components/thumbnail/index.vue
new file mode 100644
index 0000000..370774d
--- /dev/null
+++ b/src/components/thumbnail/index.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/src/components/trend/index.vue b/src/components/trend/index.vue
new file mode 100644
index 0000000..3ce7c31
--- /dev/null
+++ b/src/components/trend/index.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ describe }}
+
+
+
+
+
diff --git a/src/config/color.ts b/src/config/color.ts
new file mode 100644
index 0000000..300ac1d
--- /dev/null
+++ b/src/config/color.ts
@@ -0,0 +1,30 @@
+export type TColorToken = Record;
+export type TColorSeries = Record;
+
+// TODO: 中性色暂时固定 待tvision-color生成带色彩倾向的中性色
+export const LIGHT_CHART_COLORS = {
+ textColor: 'rgba(0, 0, 0, 0.9)',
+ placeholderColor: 'rgba(0, 0, 0, 0.35)',
+ borderColor: '#dcdcdc',
+ containerColor: '#fff',
+};
+
+export const DARK_CHART_COLORS = {
+ textColor: 'rgba(255, 255, 255, 0.9)',
+ placeholderColor: 'rgba(255, 255, 255, 0.35)',
+ borderColor: '#5e5e5e',
+ containerColor: '#242424',
+};
+
+export type TChartColor = typeof LIGHT_CHART_COLORS;
+
+export const DEFAULT_COLOR_OPTIONS = [
+ '#0052D9',
+ '#0594FA',
+ '#00A870',
+ '#EBB105',
+ '#ED7B2F',
+ '#E34D59',
+ '#ED49B4',
+ '#834EC2',
+];
diff --git a/src/config/global.ts b/src/config/global.ts
new file mode 100644
index 0000000..67aba60
--- /dev/null
+++ b/src/config/global.ts
@@ -0,0 +1 @@
+export const prefix = 'tdesign-starter';
diff --git a/src/config/style.ts b/src/config/style.ts
new file mode 100644
index 0000000..b652f5a
--- /dev/null
+++ b/src/config/style.ts
@@ -0,0 +1,14 @@
+export default {
+ showFooter: true,
+ isSidebarCompact: false,
+ showBreadcrumb: false,
+ mode: 'light',
+ layout: 'side',
+ splitMenu: false,
+ isFooterAside: false,
+ isSidebarFixed: true,
+ isHeaderFixed: true,
+ isUseTabsRouter: false,
+ showHeader: true,
+ brandTheme: '#0052D9',
+};
diff --git a/src/constants/index.ts b/src/constants/index.ts
new file mode 100644
index 0000000..6cea6f4
--- /dev/null
+++ b/src/constants/index.ts
@@ -0,0 +1,37 @@
+// 合同状态枚举
+export const CONTRACT_STATUS = {
+ FAIL: 0,
+ AUDIT_PENDING: 1,
+ EXEC_PENDING: 2,
+ EXECUTING: 3,
+ FINISH: 4,
+};
+
+// 合同类型枚举
+export const CONTRACT_TYPES = {
+ MAIN: 0,
+ SUB: 1,
+ SUPPLEMENT: 2,
+};
+
+// 合同收付类型枚举
+export const CONTRACT_PAYMENT_TYPES = {
+ PAYMENT: 0,
+ RECEIPT: 1,
+};
+
+// 标签类型
+type TagTheme = 'default' | 'success' | 'primary' | 'warning' | 'danger';
+// 通知的优先级对应的标签类型
+export const NOTIFICATION_TYPES: Map = new Map([
+ ['low', 'primary'],
+ ['middle', 'warning'],
+ ['high', 'danger'],
+]);
+
+// 通用请求头
+export enum ContentTypeEnum {
+ Json = 'application/json;charset=UTF-8',
+ FormURLEncoded = 'application/x-www-form-urlencoded;charset=UTF-8',
+ FormData = 'multipart/form-data;charset=UTF-8',
+}
diff --git a/src/hooks/event/useWindowSizeFn.ts b/src/hooks/event/useWindowSizeFn.ts
new file mode 100644
index 0000000..74e92fe
--- /dev/null
+++ b/src/hooks/event/useWindowSizeFn.ts
@@ -0,0 +1,34 @@
+import debounce from 'lodash/debounce';
+import { onMounted, onUnmounted } from 'vue';
+
+interface WindowSizeOptions {
+ immediate?: boolean;
+}
+
+interface Fn {
+ (...arg: T[]): R;
+}
+
+export function useWindowSizeFn(fn: Fn, options?: WindowSizeOptions, wait = 150) {
+ const handleSize: () => void = debounce(fn, wait);
+
+ const start = () => {
+ if (options && options.immediate) {
+ fn();
+ }
+ window.addEventListener('resize', handleSize);
+ };
+
+ const stop = () => {
+ window.removeEventListener('resize', handleSize);
+ };
+
+ onMounted(() => {
+ start();
+ });
+
+ onUnmounted(() => {
+ stop();
+ });
+ return [start, stop];
+}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..cd5a8d0
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,60 @@
+import * as echarts from 'echarts/core';
+import { onMounted, onUnmounted, Ref, ref, ShallowRef, shallowRef } from 'vue';
+
+/**
+ * eChart hook
+ * @param domId
+ */
+export const useChart = (domId: string): ShallowRef => {
+ let chartContainer: HTMLCanvasElement;
+ const selfChart = shallowRef();
+ const updateContainer = () => {
+ selfChart.value.resize({
+ width: chartContainer.clientWidth,
+ height: chartContainer.clientHeight,
+ });
+ };
+
+ onMounted(() => {
+ if (!chartContainer) {
+ chartContainer = document.getElementById(domId) as HTMLCanvasElement;
+ }
+ selfChart.value = echarts.init(chartContainer);
+ });
+
+ window.addEventListener('resize', updateContainer, false);
+
+ onUnmounted(() => {
+ window.removeEventListener('resize', updateContainer);
+ });
+
+ return selfChart;
+};
+
+/**
+ * counter utils
+ * @param duration
+ * @returns
+ */
+export const useCounter = (duration = 60): [Ref, () => void] => {
+ let intervalTimer: ReturnType;
+ onUnmounted(() => {
+ clearInterval(intervalTimer);
+ });
+ const countDown = ref(0);
+
+ return [
+ countDown,
+ () => {
+ countDown.value = duration;
+ intervalTimer = setInterval(() => {
+ if (countDown.value > 0) {
+ countDown.value -= 1;
+ } else {
+ clearInterval(intervalTimer);
+ countDown.value = 0;
+ }
+ }, 1000);
+ },
+ ];
+};
diff --git a/src/layouts/blank.vue b/src/layouts/blank.vue
new file mode 100644
index 0000000..dcdc217
--- /dev/null
+++ b/src/layouts/blank.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/src/layouts/components/Breadcrumb.vue b/src/layouts/components/Breadcrumb.vue
new file mode 100644
index 0000000..2c48f79
--- /dev/null
+++ b/src/layouts/components/Breadcrumb.vue
@@ -0,0 +1,52 @@
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
diff --git a/src/layouts/components/Content.vue b/src/layouts/components/Content.vue
new file mode 100644
index 0000000..f5b27d5
--- /dev/null
+++ b/src/layouts/components/Content.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue
new file mode 100644
index 0000000..04eeccd
--- /dev/null
+++ b/src/layouts/components/Footer.vue
@@ -0,0 +1,15 @@
+
+ Copyright © 2021-{{ new Date().getFullYear() }} Tencent. All Rights Reserved
+
+
+
+
+
diff --git a/src/layouts/components/FrameBlank.vue b/src/layouts/components/FrameBlank.vue
new file mode 100644
index 0000000..b284429
--- /dev/null
+++ b/src/layouts/components/FrameBlank.vue
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/layouts/components/FrameContent.vue b/src/layouts/components/FrameContent.vue
new file mode 100644
index 0000000..5c88928
--- /dev/null
+++ b/src/layouts/components/FrameContent.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Header.vue b/src/layouts/components/Header.vue
new file mode 100644
index 0000000..4ee196f
--- /dev/null
+++ b/src/layouts/components/Header.vue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ lang.content
+ }}
+
+
+
+
+
+ {{ $t('layout.header.user') }}
+
+
+ {{ $t('layout.header.signOut') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/LayoutContent.vue b/src/layouts/components/LayoutContent.vue
new file mode 100644
index 0000000..ba8b034
--- /dev/null
+++ b/src/layouts/components/LayoutContent.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+ {{ renderTitle(routeItem.title) }}
+
+
+
+
+ handleRefresh(routeItem, index)">
+
+ {{ $t('layout.tagTabs.refresh') }}
+
+ handleCloseAhead(routeItem.path, index)">
+
+ {{ $t('layout.tagTabs.closeLeft') }}
+
+ handleCloseBehind(routeItem.path, index)"
+ >
+
+ {{ $t('layout.tagTabs.closeRight') }}
+
+ handleCloseOther(routeItem.path, index)">
+
+ {{ $t('layout.tagTabs.closeOther') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/LayoutHeader.vue b/src/layouts/components/LayoutHeader.vue
new file mode 100644
index 0000000..13864ed
--- /dev/null
+++ b/src/layouts/components/LayoutHeader.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
diff --git a/src/layouts/components/LayoutSideNav.vue b/src/layouts/components/LayoutSideNav.vue
new file mode 100644
index 0000000..112db44
--- /dev/null
+++ b/src/layouts/components/LayoutSideNav.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/src/layouts/components/MenuContent.vue b/src/layouts/components/MenuContent.vue
new file mode 100644
index 0000000..9d3c27c
--- /dev/null
+++ b/src/layouts/components/MenuContent.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+ {{ renderMenuTitle(item.title) }}
+
+
+
+
+
+ {{ renderMenuTitle(item.title) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Notice.vue b/src/layouts/components/Notice.vue
new file mode 100644
index 0000000..e79c71a
--- /dev/null
+++ b/src/layouts/components/Notice.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/Search.vue b/src/layouts/components/Search.vue
new file mode 100644
index 0000000..fc83e65
--- /dev/null
+++ b/src/layouts/components/Search.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
diff --git a/src/layouts/components/SideNav.vue b/src/layouts/components/SideNav.vue
new file mode 100644
index 0000000..a8b627f
--- /dev/null
+++ b/src/layouts/components/SideNav.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+ {{ !collapsed ? 'TDesign Starter' : '' }} {{ pgk.version }}
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/frame/index.vue b/src/layouts/frame/index.vue
new file mode 100644
index 0000000..1938847
--- /dev/null
+++ b/src/layouts/frame/index.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/src/layouts/frame/useFrameKeepAlive.ts b/src/layouts/frame/useFrameKeepAlive.ts
new file mode 100644
index 0000000..1fd228d
--- /dev/null
+++ b/src/layouts/frame/useFrameKeepAlive.ts
@@ -0,0 +1,54 @@
+import uniqBy from 'lodash/uniqBy';
+import { computed, toRaw, unref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { useSettingStore, useTabsRouterStore } from '@/store';
+import type { MenuRoute } from '@/types/interface';
+
+export function useFrameKeepAlive() {
+ const router = useRouter();
+ const { currentRoute } = router;
+ const { isUseTabsRouter } = useSettingStore();
+ const tabStore = useTabsRouterStore();
+ const getFramePages = computed(() => {
+ const ret = getAllFramePages(toRaw(router.getRoutes()) as unknown as MenuRoute[]) || [];
+ return ret;
+ });
+
+ const getOpenTabList = computed((): string[] => {
+ return tabStore.tabRouters.reduce((prev: string[], next) => {
+ if (next.meta && Reflect.has(next.meta, 'frameSrc')) {
+ prev.push(next.name as string);
+ }
+ return prev;
+ }, []);
+ });
+
+ function getAllFramePages(routes: MenuRoute[]): MenuRoute[] {
+ let res: MenuRoute[] = [];
+ for (const route of routes) {
+ const { meta: { frameSrc, frameBlank } = {}, children } = route;
+ if (frameSrc && !frameBlank) {
+ res.push(route);
+ }
+ if (children && children.length) {
+ res.push(...getAllFramePages(children));
+ }
+ }
+ res = uniqBy(res, 'name');
+ return res;
+ }
+
+ function showIframe(item: MenuRoute) {
+ return item.name === unref(currentRoute).name;
+ }
+
+ function hasRenderFrame(name: string) {
+ if (!unref(isUseTabsRouter)) {
+ return router.currentRoute.value.name === name;
+ }
+ return unref(getOpenTabList).includes(name);
+ }
+
+ return { hasRenderFrame, getFramePages, showIframe, getAllFramePages };
+}
diff --git a/src/layouts/index.vue b/src/layouts/index.vue
new file mode 100644
index 0000000..82e8961
--- /dev/null
+++ b/src/layouts/index.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/setting.vue b/src/layouts/setting.vue
new file mode 100644
index 0000000..0d32f4a
--- /dev/null
+++ b/src/layouts/setting.vue
@@ -0,0 +1,354 @@
+
+
+
+
+ {{ $t('layout.setting.theme.mode') }}
+
+
+
+ {{ $t('layout.setting.theme.color') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('layout.setting.navigationLayout') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('layout.setting.element.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('layout.setting.tips') }}
+
+ {{ $t('layout.setting.copy.title') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/locales/index.ts b/src/locales/index.ts
new file mode 100644
index 0000000..33ba482
--- /dev/null
+++ b/src/locales/index.ts
@@ -0,0 +1,67 @@
+import { useLocalStorage, usePreferredLanguages } from '@vueuse/core';
+import { DropdownOption } from 'tdesign-vue-next';
+import { computed } from 'vue';
+import { createI18n } from 'vue-i18n';
+
+// 导入语言文件
+const langModules = import.meta.glob('./lang/*/index.ts', { eager: true });
+
+const langModuleMap = new Map();
+
+export const langCode: Array = [];
+
+export const localeConfigKey = 'tdesign-starter-locale';
+
+// 获取浏览器默认语言环境
+const languages = usePreferredLanguages();
+
+// 生成语言模块列表
+const generateLangModuleMap = () => {
+ const fullPaths = Object.keys(langModules);
+ fullPaths.forEach((fullPath) => {
+ const k = fullPath.replace('./lang', '');
+ const startIndex = 1;
+ const lastIndex = k.lastIndexOf('/');
+ const code = k.substring(startIndex, lastIndex);
+ langCode.push(code);
+ langModuleMap.set(code, langModules[fullPath]);
+ });
+};
+
+// 导出 Message
+const importMessages = computed(() => {
+ generateLangModuleMap();
+
+ const message: Recordable = {};
+ langModuleMap.forEach((value: any, key) => {
+ message[key] = value.default;
+ });
+ return message;
+});
+
+export const i18n = createI18n({
+ legacy: false,
+ locale: useLocalStorage(localeConfigKey, 'zh_CN').value || languages.value[0] || 'zh_CN',
+ fallbackLocale: 'zh_CN',
+ messages: importMessages.value,
+ globalInjection: true,
+});
+
+export const langList = computed(() => {
+ if (langModuleMap.size === 0) generateLangModuleMap();
+
+ const list: DropdownOption[] = [];
+ langModuleMap.forEach((value: any, key) => {
+ list.push({
+ content: value.default.lang,
+ value: key,
+ });
+ });
+
+ return list;
+});
+
+// @ts-ignore
+export const { t } = i18n.global;
+
+export default i18n;
diff --git a/src/locales/lang/en_US/components.ts b/src/locales/lang/en_US/components.ts
new file mode 100644
index 0000000..e4e09d5
--- /dev/null
+++ b/src/locales/lang/en_US/components.ts
@@ -0,0 +1,37 @@
+export default {
+ isSetup: {
+ on: 'Enabled',
+ off: 'Disabled',
+ },
+ manage: 'Manage',
+ delete: 'Delete',
+ commonTable: {
+ contractName: 'Name',
+ contractStatus: 'Status',
+ contractNum: 'Number',
+ contractType: 'Type',
+ contractPayType: 'Pay Type',
+ contractAmount: 'Amount',
+ contractNamePlaceholder: 'enter contract name',
+ contractStatusPlaceholder: 'enter contract status',
+ contractNumPlaceholder: 'enter contract number',
+ contractTypePlaceholder: 'enter contract type',
+ operation: 'Operation',
+ detail: 'detail',
+ delete: 'delete',
+ contractStatusEnum: {
+ fail: 'fail',
+ audit: 'audit',
+ executing: 'executing',
+ pending: 'pending',
+ finish: 'finish',
+ },
+ contractTypeEnum: {
+ main: 'main',
+ sub: 'sub',
+ supplement: 'supplement',
+ },
+ reset: 'reset',
+ query: 'query',
+ },
+};
diff --git a/src/locales/lang/en_US/index.ts b/src/locales/lang/en_US/index.ts
new file mode 100644
index 0000000..62d2f7c
--- /dev/null
+++ b/src/locales/lang/en_US/index.ts
@@ -0,0 +1,54 @@
+import merge from 'lodash/merge';
+import componentsLocale from 'tdesign-vue-next/es/locale/en_US';
+
+import components from './components';
+import layout from './layout';
+import pages from './pages';
+
+export default {
+ lang: 'English',
+ layout,
+ pages,
+ components,
+ constants: {
+ contract: {
+ name: 'Name',
+ status: 'Status',
+ num: 'Number',
+ type: 'Type',
+ typePlaceholder: 'Please enter type',
+ payType: 'Pay Type',
+ amount: 'Amount',
+ amountPlaceholder: 'Please enter amount',
+ signDate: 'Sign Date',
+ effectiveDate: 'Effective Date',
+ endDate: 'End Date',
+ createDate: 'Create Date',
+ attachment: 'Attachment',
+ company: 'Company',
+ employee: 'Employee',
+ pay: 'pay',
+ receive: 'received',
+ remark: 'remark',
+ statusOptions: {
+ fail: 'Failure',
+ auditPending: 'Pending audit',
+ execPending: 'Pending performance',
+ executing: 'Successful',
+ finish: 'Finish',
+ },
+ typeOptions: {
+ main: 'Master contract',
+ sub: 'Subcontract',
+ supplement: 'Supplementary contract',
+ },
+ },
+ },
+ componentsLocale: merge({}, componentsLocale, {
+ // 可以在此处定义更多自定义配置,具体可配置内容参看 API 文档
+ // https://tdesign.tencent.com/vue-next/config?tab=api
+ // pagination: {
+ // jumpTo: 'xxx'
+ // },
+ }),
+};
diff --git a/src/locales/lang/en_US/layout.ts b/src/locales/lang/en_US/layout.ts
new file mode 100644
index 0000000..5768171
--- /dev/null
+++ b/src/locales/lang/en_US/layout.ts
@@ -0,0 +1,52 @@
+export default {
+ header: {
+ code: 'Code Repository',
+ help: 'Document',
+ user: 'Profile',
+ signOut: 'Sign Out',
+ setting: 'Setting',
+ },
+ notice: {
+ title: 'Notification Center',
+ clear: 'Clear',
+ setRead: 'Set Read',
+ empty: 'Empty',
+ emptyNotice: 'No Notice',
+ viewAll: 'View All',
+ },
+ tagTabs: {
+ closeOther: 'close other',
+ closeLeft: 'close left',
+ closeRight: 'close right',
+ refresh: 'refresh',
+ },
+ searchPlaceholder: 'Enter search content',
+ setting: {
+ title: 'Setting',
+ theme: {
+ mode: 'Theme Mode',
+ color: 'Theme Color',
+ options: {
+ light: 'Light',
+ dark: 'Dark ',
+ auto: 'Follow System',
+ },
+ },
+ navigationLayout: 'Navigation Layout',
+ splitMenu: 'Split Menu(Only Mix mode)',
+ fixedSidebar: 'Fixed Sidebar',
+ element: {
+ title: 'Element Switch',
+ showHeader: 'Show Header',
+ showBreadcrumb: 'Show Breadcrumb',
+ showFooter: 'Show Footer',
+ useTagTabs: 'Use Tag Tabs',
+ },
+ tips: 'Please copy and manually modify the configuration file: /src/config/style.ts',
+ copy: {
+ title: 'Copy',
+ success: 'copied',
+ fail: 'fail to copy',
+ },
+ },
+};
diff --git a/src/locales/lang/en_US/pages/dashboard-base.ts b/src/locales/lang/en_US/pages/dashboard-base.ts
new file mode 100644
index 0000000..920c88f
--- /dev/null
+++ b/src/locales/lang/en_US/pages/dashboard-base.ts
@@ -0,0 +1,62 @@
+export default {
+ outputOverview: {
+ title: 'In/Out Overview',
+ subtitle: '(pieces)',
+ export: 'Export data',
+ month: {
+ input: 'Total in store this month',
+ output: 'Total out store this month',
+ },
+ since: 'Since last week',
+ },
+ rankList: {
+ title: 'Sales order ranking',
+ week: 'This week',
+ month: 'Latest 3 months',
+ info: 'Detail',
+ },
+ topPanel: {
+ card1: 'Total Revenue',
+ card2: 'Total Refund',
+ card3: 'Active User(s)',
+ card4: 'Generate Order(s)',
+ cardTips: 'since last week',
+ analysis: {
+ title: 'Analysis Data',
+ unit: 'ten thousand yuan',
+ series1: 'this month',
+ series2: 'last month',
+ channels: 'Sales Channels',
+ channel1: 'online',
+ channel2: 'shop',
+ channelTips: ' sales ratio',
+ },
+ },
+ saleColumns: {
+ index: 'Ranking',
+ productName: 'Customer',
+ growUp: 'Grow up',
+ count: 'Count',
+ operation: 'Operation',
+ },
+ buyColumns: {
+ index: 'Ranking',
+ productName: 'Supplier',
+ growUp: 'Grow up',
+ count: 'Count',
+ operation: 'Operation',
+ },
+ chart: {
+ week1: 'MON',
+ week2: 'TUE',
+ week3: 'WED',
+ week4: 'THU',
+ week5: 'FRI',
+ week6: 'SAT',
+ week7: 'SUN',
+ max: 'Max',
+ min: 'Min',
+ thisMonth: 'this month',
+ lastMonth: 'last month',
+ },
+};
diff --git a/src/locales/lang/en_US/pages/dashboard-detail.ts b/src/locales/lang/en_US/pages/dashboard-detail.ts
new file mode 100644
index 0000000..2d5a479
--- /dev/null
+++ b/src/locales/lang/en_US/pages/dashboard-detail.ts
@@ -0,0 +1,45 @@
+export default {
+ topPanel: {
+ title: 'Purchase applications for this month',
+ quarter: 'Quarter on quarter',
+ paneList: {
+ totalRequest: 'Apply count',
+ suppliers: 'Number of Suppliers',
+ productCategory: 'Product Category',
+ applicant: 'Number of Application',
+ completionRate: 'Completion Rate(%)',
+ arrivalRate: 'Arrival Rate(%)',
+ },
+ },
+ procurement: {
+ title: 'Trends in purchase requisitions for goods',
+ goods: {
+ cup: 'cup',
+ tea: 'tea',
+ honey: 'honey',
+ flour: 'flour',
+ coffeeMachine: 'coffee machine',
+ massageMachine: 'massage machine',
+ },
+ },
+ ssl: 'SSL certificate',
+ sslDescription:
+ 'SSL certificate, also known as a server certificate, is a digital certificate that authenticates the identity of a website and encrypts information sent to the server using SSL technology. Tencent Cloud provides you with a one-stop service for SSL certificates, including application, management, and deployment of both free and paid certificates.',
+ satisfaction: {
+ title: 'distribution of satisfaction levels for purchased goods',
+ export: 'export data',
+ },
+ chart: {
+ week1: 'MON',
+ week2: 'TUE',
+ week3: 'WED',
+ week4: 'THU',
+ week5: 'FRI',
+ week6: 'SAT',
+ week7: 'SUN',
+ max: 'Max',
+ min: 'Min',
+ thisMonth: 'this month',
+ lastMonth: 'last month',
+ },
+};
diff --git a/src/locales/lang/en_US/pages/detail-base.ts b/src/locales/lang/en_US/pages/detail-base.ts
new file mode 100644
index 0000000..d40d1e1
--- /dev/null
+++ b/src/locales/lang/en_US/pages/detail-base.ts
@@ -0,0 +1,20 @@
+export default {
+ baseInfo: {
+ title: 'Base Info',
+ },
+ changelog: {
+ title: 'Changelog',
+ step1: {
+ title: 'upload file',
+ subtitle: 'subtitle',
+ },
+ step2: {
+ title: 'modify contract amount',
+ subtitle: 'subtitle',
+ },
+ step3: {
+ title: 'create contract',
+ desc: 'administrator',
+ },
+ },
+};
diff --git a/src/locales/lang/en_US/pages/detail-card.ts b/src/locales/lang/en_US/pages/detail-card.ts
new file mode 100644
index 0000000..116a239
--- /dev/null
+++ b/src/locales/lang/en_US/pages/detail-card.ts
@@ -0,0 +1,43 @@
+export default {
+ baseInfo: {
+ title: 'Base Info',
+ },
+ invoice: {
+ title: 'Invoice Progress',
+ step1: {
+ title: 'Apply',
+ content: 'The electronic invoice has been submitted on December 21st',
+ },
+ step2: {
+ title: 'Electronic invoice',
+ content: 'expected to be processed within 1-3 business days.',
+ },
+ step3: {
+ title: 'Invoice is send',
+ content: 'we will contact you within 7 business days.',
+ },
+ step4: {
+ title: 'Finish',
+ },
+ },
+ product: {
+ title: 'Product Category',
+ add: 'add production',
+ month: 'month',
+ quarter: 'quarter',
+ },
+ detail: {
+ title: 'Product Procurement Detail',
+ form: {
+ applyNo: 'Apply no',
+ product: 'Name',
+ productNo: 'No.',
+ department: 'Department',
+ num: 'Num',
+ createTime: 'Create time',
+ operation: 'Operation',
+ manage: 'manage',
+ delete: 'delete',
+ },
+ },
+};
diff --git a/src/locales/lang/en_US/pages/detail-deploy.ts b/src/locales/lang/en_US/pages/detail-deploy.ts
new file mode 100644
index 0000000..d639b3e
--- /dev/null
+++ b/src/locales/lang/en_US/pages/detail-deploy.ts
@@ -0,0 +1,32 @@
+export default {
+ deployTrend: {
+ title: 'Deploy Trend',
+ warning: 'Warning',
+ thisMonth: 'this month',
+ thisWeek: 'this week',
+ lastMonth: 'last month',
+ thisYear: 'this year',
+ lastYear: 'last year',
+ week1: 'MON',
+ week2: 'TUE',
+ week3: 'WED',
+ week4: 'THU',
+ week5: 'FRI',
+ week6: 'SAT',
+ week7: 'SUN',
+ },
+ projectList: {
+ title: 'Project List',
+ dialog: {
+ title: 'Base Info',
+ },
+ table: {
+ name: 'Name',
+ admin: 'Admin',
+ createTime: 'Create time',
+ operation: 'Operation',
+ manage: 'manage',
+ delete: 'delete',
+ },
+ },
+};
diff --git a/src/locales/lang/en_US/pages/detail-secondary.ts b/src/locales/lang/en_US/pages/detail-secondary.ts
new file mode 100644
index 0000000..b6b28b6
--- /dev/null
+++ b/src/locales/lang/en_US/pages/detail-secondary.ts
@@ -0,0 +1,9 @@
+export default {
+ read: 'Read',
+ unread: 'Unread',
+ all: 'All',
+ setRead: 'set as read',
+ setUnread: 'set as unread',
+ delete: 'delete',
+ empty: 'Empty',
+};
diff --git a/src/locales/lang/en_US/pages/form-base.ts b/src/locales/lang/en_US/pages/form-base.ts
new file mode 100644
index 0000000..7a26ce9
--- /dev/null
+++ b/src/locales/lang/en_US/pages/form-base.ts
@@ -0,0 +1,27 @@
+export default {
+ title: 'Contract Info',
+ contractName: 'Name',
+ contractStatus: 'Status',
+ contractNum: 'Number',
+ contractType: 'Type',
+ contractTypePlaceholder: 'Please enter type',
+ contractPayType: 'Pay Type',
+ contractAmount: 'Amount',
+ contractAmountPlaceholder: 'Please enter amount',
+ contractSignDate: 'Sign Date',
+ contractEffectiveDate: 'Effective Date',
+ contractEndDate: 'End Date',
+ company: 'Company',
+ employee: 'Employee',
+ pay: 'pay',
+ receive: 'received',
+ upload: 'upload file',
+ uploadFile: 'upload contract file',
+ uploadTips: 'Please upload a PDF file, with a size limit of 60MB.',
+ otherInfo: 'Other Info',
+ remark: 'Remark',
+ remarkPlaceholder: 'please enter remark',
+ notaryPublic: 'Notary Public',
+ confirm: 'confirm',
+ cancel: 'cancel',
+};
diff --git a/src/locales/lang/en_US/pages/form-step.ts b/src/locales/lang/en_US/pages/form-step.ts
new file mode 100644
index 0000000..3805f12
--- /dev/null
+++ b/src/locales/lang/en_US/pages/form-step.ts
@@ -0,0 +1,53 @@
+export default {
+ step1: {
+ title: 'Submit Application',
+ subtitle: 'Submitted on December 21st',
+ rules: 'rules',
+ rule1:
+ '1. After applying for an invoice, the electronic invoice will be issued within 1-3 working days. If the qualification review is passed, the VAT special invoice (paper) will be mailed to you within 10 working days after the electronic invoice is issued;',
+ rule2: '2. The invoiced amount will be the actual amount you paid;',
+ rule3: '3. For any questions, please contact us directly at 13300001111.',
+ contractName: 'Name',
+ contractTips: 'Please select a contract name',
+ invoiceType: 'Invoice type',
+ amount: 'Amount',
+ submit: 'Submit Application',
+ },
+ step2: {
+ title: 'Electronic Information',
+ subtitle: 'Contact Customer Service if you have any questions',
+ invoiceTitle: 'Invoice Title',
+ taxNum: 'Tax Num',
+ address: 'Address',
+ bank: 'Bank',
+ bankAccount: 'Bank Account',
+ email: 'Email',
+ tel: 'Tel',
+ invoiceTitlePlaceholder: 'please enter invoice title',
+ taxNumPlaceholder: 'please enter tax num',
+ addressPlaceholder: 'please enter address',
+ bankPlaceholder: 'please enter bank',
+ bankAccountPlaceholder: 'please enter bank account',
+ emailPlaceholder: 'please enter email',
+ telPlaceholder: 'please enter tel',
+ },
+ step3: {
+ title: 'Invoice Mailed',
+ subtitle: 'Contact us after the electronic invoice is issued',
+ consignee: 'Consignee',
+ mobileNum: 'Mobile Num',
+ deliveryAddress: 'Address',
+ fullAddress: 'Full Address',
+ },
+ step4: {
+ title: 'Application Completed',
+ subtitle: 'Contact Customer Service if you have any questions',
+ finishTitle: 'Application is completed.',
+ finishTips:
+ 'The electronic invoice is expected to be sent to your email within 1-3 working days. Please be patient for the delivery of the paper invoice.',
+ reapply: 'reapply',
+ check: 'check progress',
+ },
+ preStep: 'pre step',
+ nextStep: 'next step',
+};
diff --git a/src/locales/lang/en_US/pages/index.ts b/src/locales/lang/en_US/pages/index.ts
new file mode 100644
index 0000000..47da178
--- /dev/null
+++ b/src/locales/lang/en_US/pages/index.ts
@@ -0,0 +1,33 @@
+import dashboardBase from './dashboard-base';
+import dashboardDetail from './dashboard-detail';
+import detailBase from './detail-base';
+import detailCard from './detail-card';
+import detailDeploy from './detail-deploy';
+import detailSecondary from './detail-secondary';
+import formBase from './form-base';
+import formStep from './form-step';
+import listBase from './list-base';
+import listCard from './list-card';
+import listFilter from './list-filter';
+import listTree from './list-tree';
+import login from './login';
+import result from './result';
+import user from './user';
+
+export default {
+ dashboardBase,
+ dashboardDetail,
+ listBase,
+ listCard,
+ listFilter,
+ listTree,
+ detailBase,
+ detailCard,
+ detailDeploy,
+ detailSecondary,
+ formBase,
+ formStep,
+ user,
+ login,
+ result,
+};
diff --git a/src/locales/lang/en_US/pages/list-base.ts b/src/locales/lang/en_US/pages/list-base.ts
new file mode 100644
index 0000000..c5de9ae
--- /dev/null
+++ b/src/locales/lang/en_US/pages/list-base.ts
@@ -0,0 +1,25 @@
+export default {
+ export: 'export',
+ create: 'create',
+ select: 'select',
+ items: 'items',
+ contractName: 'Name',
+ contractStatus: 'Status',
+ contractNum: 'Number',
+ contractType: 'Type',
+ contractPayType: 'Pay Type',
+ contractAmount: 'Amount',
+ operation: 'Operation',
+ detail: 'detail',
+ delete: 'delete',
+ pay: 'pay',
+ receive: 'received',
+ placeholder: 'please enter to search',
+ contractStatusEnum: {
+ fail: 'fail',
+ audit: 'audit',
+ executing: 'executing',
+ pending: 'pending',
+ finish: 'finish',
+ },
+};
diff --git a/src/locales/lang/en_US/pages/list-card.ts b/src/locales/lang/en_US/pages/list-card.ts
new file mode 100644
index 0000000..ac86551
--- /dev/null
+++ b/src/locales/lang/en_US/pages/list-card.ts
@@ -0,0 +1,13 @@
+export default {
+ create: 'Create Product',
+ placeholder: 'please enter to search',
+ productName: 'Name',
+ productStatus: 'Status',
+ productDescription: 'Description',
+ productType: 'Type',
+ productRemark: 'Remark',
+ productStatusEnum: {
+ off: 'off',
+ on: 'on',
+ },
+};
diff --git a/src/locales/lang/en_US/pages/list-filter.ts b/src/locales/lang/en_US/pages/list-filter.ts
new file mode 100644
index 0000000..ff8b4c5
--- /dev/null
+++ b/src/locales/lang/en_US/pages/list-filter.ts
@@ -0,0 +1 @@
+export default {};
diff --git a/src/locales/lang/en_US/pages/list-tree.ts b/src/locales/lang/en_US/pages/list-tree.ts
new file mode 100644
index 0000000..1186fd6
--- /dev/null
+++ b/src/locales/lang/en_US/pages/list-tree.ts
@@ -0,0 +1,3 @@
+export default {
+ placeholder: 'please enter to search',
+};
diff --git a/src/locales/lang/en_US/pages/login.ts b/src/locales/lang/en_US/pages/login.ts
new file mode 100644
index 0000000..c52a048
--- /dev/null
+++ b/src/locales/lang/en_US/pages/login.ts
@@ -0,0 +1,26 @@
+export default {
+ loginTitle: 'Login in',
+ noAccount: 'No Account?',
+ createAccount: 'Create Account',
+ remember: 'Remember Account',
+ forget: 'Forget Account',
+ signIn: 'Sign in',
+ existAccount: 'Exist Account?',
+ refresh: 'refresh',
+ wechatLogin: 'Login with WeChat',
+ accountLogin: 'Login with Account',
+ phoneLogin: 'Login with Mobile Phone',
+ input: {
+ account: 'please enter account',
+ password: 'please enter password',
+ phone: 'please enter phone',
+ verification: 'please enter verification code',
+ },
+ required: {
+ account: 'account is required',
+ phone: 'phone is required',
+ password: 'password is required',
+ verification: 'verification code is require',
+ },
+ sendVerification: 'send',
+};
diff --git a/src/locales/lang/en_US/pages/result.ts b/src/locales/lang/en_US/pages/result.ts
new file mode 100644
index 0000000..df5fb51
--- /dev/null
+++ b/src/locales/lang/en_US/pages/result.ts
@@ -0,0 +1,43 @@
+export default {
+ 403: {
+ tips: 'sorry, you don not have permission to access this page. Please contact the creator through WeCom',
+ back: 'Back to homepage',
+ },
+ 404: {
+ subtitle: 'Sorry, the page is not found',
+ back: 'Back to homepage',
+ },
+ 500: {
+ subtitle: 'Sorry, the server is error',
+ back: 'Back to homepage',
+ },
+ fail: {
+ title: 'Create fail',
+ subtitle: 'Sorry, your project creation has failed',
+ back: 'Back to homepage',
+ modify: 'Back to modify',
+ },
+ success: {
+ title: 'Project is created',
+ subtitle: 'Contact the person in charge of distributing the application',
+ back: 'Back to homepage',
+ progress: 'Check Progress',
+ },
+ maintenance: {
+ title: 'System maintenance',
+ subtitle: 'Please try again later',
+ back: 'Back to homepage',
+ },
+ browserIncompatible: {
+ title: 'browser is incompatible',
+ subtitle: 'the browser version you are using is too outdated to open the current webpage.',
+ back: 'Back to homepage',
+ recommend: 'TDesign Starter recommend the following browser',
+ },
+ networkError: {
+ title: 'Network Error',
+ subtitle: 'Network error, please try again later',
+ back: 'Back to homepage',
+ reload: 'Reload',
+ },
+};
diff --git a/src/locales/lang/en_US/pages/user.ts b/src/locales/lang/en_US/pages/user.ts
new file mode 100644
index 0000000..5c49b18
--- /dev/null
+++ b/src/locales/lang/en_US/pages/user.ts
@@ -0,0 +1,23 @@
+export default {
+ markDay: 'Good afternoon, today marks your 100th day at Tencent',
+ personalInfo: {
+ title: 'Personal Info',
+ position: 'Employee of the Hong Kong and Macau Business Expansion team',
+
+ desc: {
+ phone: 'Phone',
+ mobile: 'Mobile',
+ seat: 'Seat',
+ email: 'Email',
+ position: 'Position',
+ leader: 'Leader',
+ entity: 'Entity',
+ joinDay: 'Day of join',
+ group: 'Group',
+ },
+ },
+ contentList: 'Content List',
+ visitData: 'Visit Data',
+ teamMember: 'Team Member',
+ serviceProduction: 'Service Product',
+};
diff --git a/src/locales/lang/zh_CN/components.ts b/src/locales/lang/zh_CN/components.ts
new file mode 100644
index 0000000..ffdf918
--- /dev/null
+++ b/src/locales/lang/zh_CN/components.ts
@@ -0,0 +1,38 @@
+export default {
+ isSetup: {
+ on: '已启用',
+ off: '已停用',
+ },
+ manage: '管理',
+ delete: '删除',
+ commonTable: {
+ contractName: '合同名称',
+ contractNamePlaceholder: '请输入合同名称',
+ contractStatus: '合同状态',
+ contractStatusPlaceholder: '请输入合同状态',
+ contractNum: '合同编号',
+ contractNumPlaceholder: '请输入合同编号',
+ contractType: '合同类型',
+ contractTypePlaceholder: '请选择合同类型',
+ contractPayType: '合同支付类型',
+ contractAmount: '合同金额',
+ operation: '操作',
+ detail: '详情',
+ delete: '删除',
+ placeholder: '请输入内容搜索',
+ contractStatusEnum: {
+ fail: '审核失败',
+ audit: '待审核',
+ executing: '履行中',
+ pending: '待履行',
+ finish: '已完成',
+ },
+ contractTypeEnum: {
+ main: '主合同',
+ sub: '子合同',
+ supplement: '补充合同',
+ },
+ reset: '重置',
+ query: '查询',
+ },
+};
diff --git a/src/locales/lang/zh_CN/index.ts b/src/locales/lang/zh_CN/index.ts
new file mode 100644
index 0000000..f27b2dc
--- /dev/null
+++ b/src/locales/lang/zh_CN/index.ts
@@ -0,0 +1,47 @@
+import componentsLocale from 'tdesign-vue-next/es/locale/zh_CN';
+
+import components from './components';
+import layout from './layout';
+import pages from './pages';
+
+export default {
+ lang: '简体中文',
+ layout,
+ pages,
+ components,
+ constants: {
+ contract: {
+ name: '合同名称',
+ status: '合同状态',
+ num: '合同编号',
+ type: '合同类型',
+ typePlaceholder: '请输入类型',
+ payType: '合同收支类型',
+ amount: '合同金额',
+ amountPlaceholder: '请输入金额',
+ signDate: '合同签订日期',
+ effectiveDate: '合同生效日期',
+ endDate: '合同结束日期',
+ createDate: '合同创建时间',
+ company: '甲方',
+ employee: '乙方',
+ pay: '付款',
+ receive: '收款',
+ remark: '备注',
+ attachment: '附件',
+ statusOptions: {
+ fail: '审核失败',
+ auditPending: '待审核',
+ execPending: '待履行',
+ executing: '审核成功',
+ finish: '已完成',
+ },
+ typeOptions: {
+ main: '主合同',
+ sub: '子合同',
+ supplement: '补充合同',
+ },
+ },
+ },
+ componentsLocale,
+};
diff --git a/src/locales/lang/zh_CN/layout.ts b/src/locales/lang/zh_CN/layout.ts
new file mode 100644
index 0000000..b648b25
--- /dev/null
+++ b/src/locales/lang/zh_CN/layout.ts
@@ -0,0 +1,52 @@
+export default {
+ header: {
+ code: '代码仓库',
+ help: '帮助文档',
+ user: '个人中心',
+ signOut: '退出登录',
+ setting: '系统设置',
+ },
+ notice: {
+ title: '通知中心',
+ clear: '清空',
+ setRead: '设为已读',
+ empty: '空',
+ emptyNotice: '暂无通知',
+ viewAll: '查看全部',
+ },
+ searchPlaceholder: '请输入搜索内容',
+ tagTabs: {
+ closeOther: '关闭其他',
+ closeLeft: '关闭左侧',
+ closeRight: '关闭右侧',
+ refresh: '刷新',
+ },
+ setting: {
+ title: '页面配置',
+ theme: {
+ mode: '主题模式',
+ color: '主题色',
+ options: {
+ light: '明亮',
+ dark: '暗黑',
+ auto: '跟随系统',
+ },
+ },
+ navigationLayout: '导航布局',
+ splitMenu: '分割菜单(混合模式下有效)',
+ fixedSidebar: '固定侧边栏',
+ element: {
+ title: '元素开关',
+ showHeader: '显示顶栏',
+ showBreadcrumb: '显示面包屑',
+ showFooter: '显示页脚',
+ useTagTabs: '展示多标签Tab页',
+ },
+ tips: '请复制后手动修改配置文件: /src/config/style.ts',
+ copy: {
+ title: '复制配置项',
+ success: '复制成功',
+ fail: '复制失败',
+ },
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/dashboard-base.ts b/src/locales/lang/zh_CN/pages/dashboard-base.ts
new file mode 100644
index 0000000..64cf01b
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/dashboard-base.ts
@@ -0,0 +1,63 @@
+export default {
+ title: '概览仪表盘',
+ outputOverview: {
+ title: '出入库概览',
+ subtitle: '(件)',
+ export: '导出数据',
+ month: {
+ input: '本月入库总计(件)',
+ output: '本月出库总计(件)',
+ },
+ since: '自从上周以来',
+ },
+ rankList: {
+ title: '销售订单排名',
+ week: '本周',
+ month: '近三月',
+ info: '详情',
+ },
+ topPanel: {
+ card1: '总收入',
+ card2: '总退款',
+ card3: '活跃用户(个)',
+ card4: '产生订单(个)',
+ cardTips: '自从上周以来',
+ analysis: {
+ title: '统计数据',
+ unit: '万元',
+ series1: '本月',
+ series2: '上月',
+ channels: '销售渠道',
+ channel1: '线上',
+ channel2: '门店',
+ channelTips: '销售占比',
+ },
+ },
+ saleColumns: {
+ index: '排名',
+ productName: '客户名称',
+ growUp: '较上周',
+ count: '订单量',
+ operation: '操作',
+ },
+ buyColumns: {
+ index: '排名',
+ productName: '供应商名称',
+ growUp: '较上周',
+ count: '订单量',
+ operation: '操作',
+ },
+ chart: {
+ week1: '周一',
+ week2: '周二',
+ week3: '周三',
+ week4: '周四',
+ week5: '周五',
+ week6: '周六',
+ week7: '周日',
+ max: '最大值',
+ min: '最小值',
+ thisMonth: '本月',
+ lastMonth: '上月',
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/dashboard-detail.ts b/src/locales/lang/zh_CN/pages/dashboard-detail.ts
new file mode 100644
index 0000000..5e47ad8
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/dashboard-detail.ts
@@ -0,0 +1,44 @@
+export default {
+ topPanel: {
+ title: '本月采购申请情况',
+ quarter: '环比',
+ paneList: {
+ totalRequest: '总申请数(次)',
+ suppliers: '供应商数量(个)',
+ productCategory: '采购商品品类(类)',
+ applicant: '申请人数量(人)',
+ completionRate: '申请完成率(%)',
+ arrivalRate: '到货及时率(%)',
+ },
+ },
+ procurement: {
+ title: '采购商品申请趋势',
+ goods: {
+ cup: '杯子',
+ tea: '茶叶',
+ honey: '蜂蜜',
+ flour: '面粉',
+ coffeeMachine: '咖啡机',
+ massageMachine: '按摩仪',
+ },
+ },
+ ssl: 'SSL证书',
+ sslDescription: 'SSL证书又叫服务器证书,腾讯云为您提供证书的一站式服务,包括免费、付费证书的申请、管理及部署',
+ satisfaction: {
+ title: '采购商品满意度分布',
+ export: '导出数据',
+ },
+ chart: {
+ week1: '周一',
+ week2: '周二',
+ week3: '周三',
+ week4: '周四',
+ week5: '周五',
+ week6: '周六',
+ week7: '周日',
+ max: '最大值',
+ min: '最小值',
+ thisMonth: '本月',
+ lastMonth: '上月',
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/detail-base.ts b/src/locales/lang/zh_CN/pages/detail-base.ts
new file mode 100644
index 0000000..65ed5cd
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/detail-base.ts
@@ -0,0 +1,20 @@
+export default {
+ baseInfo: {
+ title: '基本信息',
+ },
+ changelog: {
+ title: '变更记录',
+ step1: {
+ title: '上传合同附件',
+ subtitle: '这里是提示文字',
+ },
+ step2: {
+ title: '修改合同金额',
+ subtitle: '这里是提示文字',
+ },
+ step3: {
+ title: '新建合同',
+ desc: '管理员-李川操作',
+ },
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/detail-card.ts b/src/locales/lang/zh_CN/pages/detail-card.ts
new file mode 100644
index 0000000..88f81ce
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/detail-card.ts
@@ -0,0 +1,43 @@
+export default {
+ baseInfo: {
+ title: '基本信息',
+ },
+ invoice: {
+ title: '发票进度',
+ step1: {
+ title: '申请提交',
+ content: '已于12月21日提交',
+ },
+ step2: {
+ title: '电子发票',
+ content: '预计1~3个工作日',
+ },
+ step3: {
+ title: '发票已邮寄',
+ content: '电子发票开出后7个工作日联系',
+ },
+ step4: {
+ title: '完成',
+ },
+ },
+ product: {
+ title: '产品目录',
+ add: '新增产品',
+ month: '月份',
+ quarter: '季度',
+ },
+ detail: {
+ title: '产品采购明细',
+ form: {
+ applyNo: '申请号',
+ product: '产品名称',
+ productNo: '产品编号',
+ operation: '操作',
+ department: '申请部门',
+ num: '采购数量',
+ createTime: '创建时间',
+ manage: '管理',
+ delete: '删除',
+ },
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/detail-deploy.ts b/src/locales/lang/zh_CN/pages/detail-deploy.ts
new file mode 100644
index 0000000..12fc15a
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/detail-deploy.ts
@@ -0,0 +1,32 @@
+export default {
+ deployTrend: {
+ title: '部署趋势',
+ warning: '告警情况',
+ thisMonth: '本月',
+ thisWeek: '本周',
+ lastMonth: '上月',
+ thisYear: '今年',
+ lastYear: '去年',
+ week1: '周一',
+ week2: '周二',
+ week3: '周三',
+ week4: '周四',
+ week5: '周五',
+ week6: '周六',
+ week7: '周日',
+ },
+ projectList: {
+ title: '项目列表',
+ dialog: {
+ title: '基本信息',
+ },
+ table: {
+ name: '名称',
+ admin: '管理员',
+ createTime: '创建时间',
+ operation: '操作',
+ manage: '管理',
+ delete: '删除',
+ },
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/detail-secondary.ts b/src/locales/lang/zh_CN/pages/detail-secondary.ts
new file mode 100644
index 0000000..c66270d
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/detail-secondary.ts
@@ -0,0 +1,9 @@
+export default {
+ read: '已读通知',
+ unread: '未读通知',
+ all: '全部通知',
+ setRead: '设为已读',
+ setUnread: '设为未读',
+ delete: '删除通知',
+ empty: '暂无通知',
+};
diff --git a/src/locales/lang/zh_CN/pages/form-base.ts b/src/locales/lang/zh_CN/pages/form-base.ts
new file mode 100644
index 0000000..1b119f8
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/form-base.ts
@@ -0,0 +1,27 @@
+export default {
+ title: '合同信息',
+ contractName: '合同名称',
+ contractStatus: '合同状态',
+ contractNum: '合同编号',
+ contractType: '合同类型',
+ contractTypePlaceholder: '请输入类型',
+ contractPayType: '合同收支类型',
+ contractAmount: '合同金额',
+ contractAmountPlaceholder: '请输入金额',
+ contractSignDate: '合同签订日期',
+ contractEffectiveDate: '合同生效日期',
+ contractEndDate: '合同结束日期',
+ company: '甲方',
+ employee: '乙方',
+ pay: '付款',
+ receive: '收款',
+ upload: '上传文件',
+ uploadFile: '上传合同',
+ uploadTips: '请上传pdf文件,大小在60M以内',
+ otherInfo: '其他信息',
+ remark: '备注',
+ remarkPlaceholder: '请输入备注',
+ notaryPublic: '公证人',
+ confirm: '确认提交',
+ cancel: '取消',
+};
diff --git a/src/locales/lang/zh_CN/pages/form-step.ts b/src/locales/lang/zh_CN/pages/form-step.ts
new file mode 100644
index 0000000..bad33a9
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/form-step.ts
@@ -0,0 +1,54 @@
+export default {
+ step1: {
+ title: '提交申请',
+ subtitle: '已于12月21日提交',
+ rules: '发票开具规则',
+ rule1:
+ '1、申请开票后,电子发票在1~3个工作日内开具;增值税专用发票(纸质)如资质审核通过,将在电子发票开具后10个工作日内为您寄出;',
+ rule2: '2、开票金额为您实际支付金额;',
+ rule3: '3、如有疑问请直接联系:13300001111。',
+ contractName: '合同名称',
+ contractTips: '请选择合同名称',
+ invoiceType: '发票类型',
+ amount: '开票金额',
+ submit: '提交申请',
+ },
+ step2: {
+ title: '电子信息',
+ subtitle: '如有疑问联系客服',
+ invoiceTitle: '发票抬头',
+ taxNum: '纳税人识别号',
+ address: '地址',
+ bank: '开户行',
+ bankAccount: '银行账号',
+ email: '邮箱',
+ tel: '手机号',
+ titlePlaceholder: '请输入电子信息',
+ subtitlePlaceholder: '请输入如有疑问联系客服',
+ invoiceTitlePlaceholder: '请输入发票抬头',
+ taxNumPlaceholder: '请输入纳税人识别号',
+ addressPlaceholder: '请输入地址',
+ bankPlaceholder: '请输入开户行',
+ bankAccountPlaceholder: '请输入银行账号',
+ emailPlaceholder: '请输入邮箱',
+ telPlaceholder: '请输入手机号',
+ },
+ step3: {
+ title: '发票已邮寄',
+ subtitle: '电子发票开出后联系',
+ consignee: '收货人',
+ mobileNum: '手机号码',
+ deliveryAddress: '收货地址',
+ fullAddress: '详细地址',
+ },
+ step4: {
+ title: '完成申请',
+ subtitle: '如有疑问联系客服',
+ finishTitle: '完成开票申请',
+ finishTips: '预计1~3个工作日会将电子发票发至邮箱,发票邮寄请耐心等待',
+ reapply: '再次申请',
+ check: '查看进度',
+ },
+ preStep: '上一步',
+ nextStep: '下一步',
+};
diff --git a/src/locales/lang/zh_CN/pages/index.ts b/src/locales/lang/zh_CN/pages/index.ts
new file mode 100644
index 0000000..47da178
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/index.ts
@@ -0,0 +1,33 @@
+import dashboardBase from './dashboard-base';
+import dashboardDetail from './dashboard-detail';
+import detailBase from './detail-base';
+import detailCard from './detail-card';
+import detailDeploy from './detail-deploy';
+import detailSecondary from './detail-secondary';
+import formBase from './form-base';
+import formStep from './form-step';
+import listBase from './list-base';
+import listCard from './list-card';
+import listFilter from './list-filter';
+import listTree from './list-tree';
+import login from './login';
+import result from './result';
+import user from './user';
+
+export default {
+ dashboardBase,
+ dashboardDetail,
+ listBase,
+ listCard,
+ listFilter,
+ listTree,
+ detailBase,
+ detailCard,
+ detailDeploy,
+ detailSecondary,
+ formBase,
+ formStep,
+ user,
+ login,
+ result,
+};
diff --git a/src/locales/lang/zh_CN/pages/list-base.ts b/src/locales/lang/zh_CN/pages/list-base.ts
new file mode 100644
index 0000000..5a96799
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/list-base.ts
@@ -0,0 +1,25 @@
+export default {
+ export: '导出合同',
+ create: '新建合同',
+ select: '已选',
+ items: '项',
+ contractName: '合同名称',
+ contractStatus: '合同状态',
+ contractNum: '合同编号',
+ contractType: '合同类型',
+ contractPayType: '合同收支类型',
+ contractAmount: '合同金额',
+ operation: '操作',
+ detail: '详情',
+ delete: '删除',
+ pay: '付款',
+ receive: '收款',
+ placeholder: '请输入内容搜索',
+ contractStatusEnum: {
+ fail: '审核失败',
+ audit: '待审核',
+ executing: '履行中',
+ pending: '待履行',
+ finish: '已完成',
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/list-card.ts b/src/locales/lang/zh_CN/pages/list-card.ts
new file mode 100644
index 0000000..38323f1
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/list-card.ts
@@ -0,0 +1,13 @@
+export default {
+ create: '新建产品',
+ placeholder: '请输入内容搜索',
+ productName: '产品名称',
+ productStatus: '产品状态',
+ productDescription: '产品描述',
+ productType: '产品类型',
+ productRemark: '备注',
+ productStatusEnum: {
+ off: '停用',
+ on: '启用',
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/list-filter.ts b/src/locales/lang/zh_CN/pages/list-filter.ts
new file mode 100644
index 0000000..ff8b4c5
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/list-filter.ts
@@ -0,0 +1 @@
+export default {};
diff --git a/src/locales/lang/zh_CN/pages/list-tree.ts b/src/locales/lang/zh_CN/pages/list-tree.ts
new file mode 100644
index 0000000..c7d68a7
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/list-tree.ts
@@ -0,0 +1,3 @@
+export default {
+ placeholder: '请输入内容进行搜索',
+};
diff --git a/src/locales/lang/zh_CN/pages/login.ts b/src/locales/lang/zh_CN/pages/login.ts
new file mode 100644
index 0000000..3cbe5f2
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/login.ts
@@ -0,0 +1,27 @@
+export default {
+ loginTitle: '登录到',
+ noAccount: '没有账号吗?',
+ existAccount: '已有账号?',
+ createAccount: '注册新账号',
+ remember: '记住账号',
+ forget: '忘记账号',
+ signIn: '登录',
+ register: '注册',
+ refresh: '刷新',
+ wechatLogin: '使用微信扫一扫登录',
+ accountLogin: '使用账号登录',
+ phoneLogin: '使用手机号登录',
+ input: {
+ account: '请输入账号',
+ password: '请输入登录密码',
+ phone: '请输入手机号',
+ verification: '请输入验证码',
+ },
+ required: {
+ account: '账号必填',
+ phone: '手机号必填',
+ password: '密码必填',
+ verification: '验证码必填',
+ },
+ sendVerification: '发送验证码',
+};
diff --git a/src/locales/lang/zh_CN/pages/result.ts b/src/locales/lang/zh_CN/pages/result.ts
new file mode 100644
index 0000000..e00487c
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/result.ts
@@ -0,0 +1,43 @@
+export default {
+ 403: {
+ tips: '抱歉,您无权限访问此页面,企业微信联系创建者',
+ back: '返回首页',
+ },
+ 404: {
+ subtitle: '抱歉,您访问的页面不存在',
+ back: '返回首页',
+ },
+ 500: {
+ subtitle: '抱歉,服务器出错啦',
+ back: '返回首页',
+ },
+ fail: {
+ title: '创建失败',
+ subtitle: '抱歉,您的项目创建失败,企业微信联系检查创建者权限,或返回修改。',
+ back: '回到首页',
+ modify: '返回修改',
+ },
+ success: {
+ title: '项目已创建成功',
+ subtitle: '可以联系负责人分发应用',
+ back: '回到首页',
+ progress: '查看进度',
+ },
+ maintenance: {
+ title: '系统维护中',
+ subtitle: '系统维护中,请稍后再试',
+ back: '回到首页',
+ },
+ browserIncompatible: {
+ title: '浏览器不兼容',
+ subtitle: '抱歉,您正在使用的浏览器版本过低,无法打开当前网页。',
+ back: '回到首页',
+ recommend: 'TDesign Starter 推荐以下主流浏览器',
+ },
+ networkError: {
+ title: '网络异常',
+ subtitle: '网络异常, 请稍后重试',
+ back: '回到首页',
+ reload: '重新加载',
+ },
+};
diff --git a/src/locales/lang/zh_CN/pages/user.ts b/src/locales/lang/zh_CN/pages/user.ts
new file mode 100644
index 0000000..c1a8b2f
--- /dev/null
+++ b/src/locales/lang/zh_CN/pages/user.ts
@@ -0,0 +1,22 @@
+export default {
+ markDay: '下午好,今天是你加入鹅厂的第 100 天',
+ personalInfo: {
+ title: '个人信息',
+ position: '港澳业务拓展组员工 直客销售 ',
+ desc: {
+ phone: '座机',
+ mobile: '手机',
+ seat: '座位',
+ email: '邮箱',
+ position: '职位',
+ leader: '上级',
+ entity: '主体',
+ joinDay: '入职时间',
+ group: '所属团队',
+ },
+ },
+ visitData: '首页访问数据',
+ contentList: '内容列表',
+ teamMember: '团队成员',
+ serviceProduction: '服务产品',
+};
diff --git a/src/locales/useLocale.ts b/src/locales/useLocale.ts
new file mode 100644
index 0000000..0e7c517
--- /dev/null
+++ b/src/locales/useLocale.ts
@@ -0,0 +1,28 @@
+import { useLocalStorage } from '@vueuse/core';
+import { computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+import { i18n, langCode, localeConfigKey } from '@/locales/index';
+
+export function useLocale() {
+ const { locale } = useI18n({ useScope: 'global' });
+ function changeLocale(lang: string) {
+ // 如果切换的语言不在对应语言文件里则默认为简体中文
+ if (!langCode.includes(lang)) {
+ lang = 'zh_CN';
+ }
+
+ locale.value = lang;
+ useLocalStorage(localeConfigKey, 'zh_CN').value = lang;
+ }
+
+ const getComponentsLocale = computed(() => {
+ return i18n.global.getLocaleMessage(locale.value).componentsLocale;
+ });
+
+ return {
+ changeLocale,
+ getComponentsLocale,
+ locale,
+ };
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..58ec99a
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,21 @@
+/* eslint-disable simple-import-sort/imports */
+import TDesign from 'tdesign-vue-next';
+import { createApp } from 'vue';
+
+import App from './App.vue';
+import router from './router';
+import { store } from './store';
+import i18n from './locales';
+
+import 'tdesign-vue-next/es/style/index.css';
+import '@/style/index.less';
+import './permission';
+
+const app = createApp(App);
+
+app.use(TDesign);
+app.use(store);
+app.use(router);
+app.use(i18n);
+
+app.mount('#app');
diff --git a/src/pages/dashboard/base/components/MiddleChart.vue b/src/pages/dashboard/base/components/MiddleChart.vue
new file mode 100644
index 0000000..11d546d
--- /dev/null
+++ b/src/pages/dashboard/base/components/MiddleChart.vue
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/dashboard/base/components/OutputOverview.vue b/src/pages/dashboard/base/components/OutputOverview.vue
new file mode 100644
index 0000000..764feee
--- /dev/null
+++ b/src/pages/dashboard/base/components/OutputOverview.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('pages.dashboardBase.outputOverview.export') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/dashboard/base/components/RankList.vue b/src/pages/dashboard/base/components/RankList.vue
new file mode 100644
index 0000000..9f7049f
--- /dev/null
+++ b/src/pages/dashboard/base/components/RankList.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+ {{ $t('pages.dashboardBase.rankList.week') }}
+ {{ $t('pages.dashboardBase.rankList.month') }}
+
+
+
+
+
+ {{ rowIndex + 1 }}
+
+
+
+
+
+
+
+
+ {{
+ $t('pages.dashboardBase.rankList.info')
+ }}
+
+
+
+
+
+
+
+
+ {{ $t('pages.dashboardBase.rankList.week') }}
+ {{ $t('pages.dashboardBase.rankList.month') }}
+
+
+
+
+
+ {{ rowIndex + 1 }}
+
+
+
+
+
+
+ {{
+ $t('pages.dashboardBase.rankList.info')
+ }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/dashboard/base/components/TopPanel.vue b/src/pages/dashboard/base/components/TopPanel.vue
new file mode 100644
index 0000000..005bf7c
--- /dev/null
+++ b/src/pages/dashboard/base/components/TopPanel.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+ {{ item.number }}
+
+
+
+
+
+ {{ $t('pages.dashboardBase.topPanel.cardTips') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/dashboard/base/constants.ts b/src/pages/dashboard/base/constants.ts
new file mode 100644
index 0000000..48b401a
--- /dev/null
+++ b/src/pages/dashboard/base/constants.ts
@@ -0,0 +1,84 @@
+interface TendItem {
+ growUp?: number;
+ productName: string;
+ count: number;
+ date: string;
+}
+
+export const SALE_TEND_LIST: Array = [
+ {
+ growUp: 1,
+ productName: '国家电网有限公司',
+ count: 7059,
+ date: '2021-09-01',
+ },
+ {
+ growUp: -1,
+ productName: '深圳燃气集团股份有限公司',
+ count: 6437,
+ date: '2021-09-01',
+ },
+ {
+ growUp: 4,
+ productName: '国家烟草专卖局',
+ count: 4221,
+ date: '2021-09-01',
+ },
+ {
+ growUp: 3,
+ productName: '中国电信集团有限公司',
+ count: 3317,
+ date: '2021-09-01',
+ },
+ {
+ growUp: -3,
+ productName: '中国移动通信集团有限公司',
+ count: 3015,
+ date: '2021-09-01',
+ },
+ {
+ growUp: -3,
+ productName: '新余市办公用户采购项目',
+ count: 2015,
+ date: '2021-09-12',
+ },
+];
+
+export const BUY_TEND_LIST: Array = [
+ {
+ growUp: 1,
+ productName: '腾讯科技(深圳)有限公司',
+ count: 3015,
+ date: '2021-09-01',
+ },
+ {
+ growUp: -1,
+ productName: '大润发有限公司',
+ count: 2015,
+ date: '2021-09-01',
+ },
+ {
+ growUp: 6,
+ productName: '四川海底捞股份有限公司',
+ count: 1815,
+ date: '2021-09-11',
+ },
+ {
+ growUp: -3,
+ productName: '索尼(中国)有限公司',
+ count: 1015,
+ date: '2021-09-21',
+ },
+ {
+ growUp: -4,
+ productName: '松下电器(中国)有限公司',
+ count: 445,
+ date: '2021-09-19',
+ },
+ {
+ growUp: -3,
+ productName: '新余市办公用户采购项目',
+ count: 2015,
+ date: '2021-09-12',
+ },
+];
diff --git a/src/pages/dashboard/base/index.ts b/src/pages/dashboard/base/index.ts
new file mode 100644
index 0000000..35a768e
--- /dev/null
+++ b/src/pages/dashboard/base/index.ts
@@ -0,0 +1,400 @@
+import dayjs from 'dayjs';
+import { EChartsOption } from 'echarts';
+
+import { TChartColor } from '@/config/color';
+import { t } from '@/locales/index';
+import { getRandomArray } from '@/utils/charts';
+import { getChartListColor } from '@/utils/color';
+
+/** 首页 dashboard 折线图 */
+export function constructInitDashboardDataset(type: string) {
+ const dateArray: Array = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
+
+ const datasetAxis = {
+ xAxis: {
+ type: 'category',
+ show: false,
+ data: dateArray,
+ },
+ yAxis: {
+ show: false,
+ type: 'value',
+ },
+ grid: {
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+ };
+
+ if (type === 'line') {
+ const lineDataset = {
+ ...datasetAxis,
+ color: ['#fff'],
+ series: [
+ {
+ data: [150, 230, 224, 218, 135, 147, 260],
+ type,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 0,
+ markPoint: {
+ data: [
+ { type: 'max', name: '最大值' },
+ { type: 'min', name: '最小值' },
+ ],
+ },
+ lineStyle: {
+ width: 2,
+ },
+ },
+ ],
+ };
+ return lineDataset;
+ }
+ const barDataset = {
+ ...datasetAxis,
+ color: getChartListColor(),
+ series: [
+ {
+ data: [
+ 100,
+ 130,
+ 184,
+ 218,
+ {
+ value: 135,
+ itemStyle: {
+ opacity: 0.2,
+ },
+ },
+ {
+ value: 118,
+ itemStyle: {
+ opacity: 0.2,
+ },
+ },
+ {
+ value: 60,
+ itemStyle: {
+ opacity: 0.2,
+ },
+ },
+ ],
+ type,
+ barWidth: 9,
+ },
+ ],
+ };
+ return barDataset;
+}
+
+/** 柱状图数据源 */
+export function constructInitDataset({
+ dateTime = [],
+ placeholderColor,
+ borderColor,
+}: { dateTime: Array } & TChartColor) {
+ const divideNum = 10;
+ const timeArray = [];
+ const inArray = [];
+ const outArray = [];
+ for (let i = 0; i < divideNum; i++) {
+ if (dateTime.length > 0) {
+ const dateAbsTime: number = (new Date(dateTime[1]).getTime() - new Date(dateTime[0]).getTime()) / divideNum;
+ const enhandTime: number = new Date(dateTime[0]).getTime() + dateAbsTime * i;
+ timeArray.push(dayjs(enhandTime).format('YYYY-MM-DD'));
+ } else {
+ timeArray.push(
+ dayjs()
+ .subtract(divideNum - i, 'day')
+ .format('YYYY-MM-DD'),
+ );
+ }
+
+ inArray.push(getRandomArray().toString());
+ outArray.push(getRandomArray().toString());
+ }
+
+ const dataset = {
+ color: getChartListColor(),
+ tooltip: {
+ trigger: 'item',
+ },
+ xAxis: {
+ type: 'category',
+ data: timeArray,
+ axisLabel: {
+ color: placeholderColor,
+ },
+ axisLine: {
+ lineStyle: {
+ color: getChartListColor()[1],
+ width: 1,
+ },
+ },
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ color: placeholderColor,
+ },
+ splitLine: {
+ lineStyle: {
+ color: borderColor,
+ },
+ },
+ },
+ grid: {
+ top: '5%',
+ left: '25px',
+ right: 0,
+ bottom: '60px',
+ },
+ legend: {
+ icon: 'rect',
+ itemWidth: 12,
+ itemHeight: 4,
+ itemGap: 48,
+ textStyle: {
+ fontSize: 12,
+ color: placeholderColor,
+ },
+ left: 'center',
+ bottom: '0',
+ orient: 'horizontal',
+ data: [t('pages.dashboardBase.chart.thisMonth'), t('pages.dashboardBase.chart.lastMonth')],
+ },
+ series: [
+ {
+ name: t('pages.dashboardBase.chart.thisMonth'),
+ data: outArray,
+ type: 'bar',
+ },
+ {
+ name: t('pages.dashboardBase.chart.lastMonth'),
+ data: inArray,
+ type: 'bar',
+ },
+ ],
+ };
+
+ return dataset;
+}
+
+/**
+ * 线性图表数据源
+ *
+ * @export
+ * @param {Array} [dateTime=[]]
+ * @returns {*}
+ */
+export function getLineChartDataSet({
+ dateTime = [],
+ placeholderColor,
+ borderColor,
+}: { dateTime?: Array } & TChartColor) {
+ const divideNum = 10;
+ const timeArray = [];
+ const inArray = [];
+ const outArray = [];
+ for (let i = 0; i < divideNum; i++) {
+ if (dateTime.length > 0) {
+ const dateAbsTime: number = (new Date(dateTime[1]).getTime() - new Date(dateTime[0]).getTime()) / divideNum;
+ const enhandTime: number = new Date(dateTime[0]).getTime() + dateAbsTime * i;
+ // console.log('dateAbsTime..', dateAbsTime, enhandTime);
+ timeArray.push(dayjs(enhandTime).format('MM-DD'));
+ } else {
+ timeArray.push(
+ dayjs()
+ .subtract(divideNum - i, 'day')
+ .format('MM-DD'),
+ );
+ }
+
+ inArray.push(getRandomArray().toString());
+ outArray.push(getRandomArray().toString());
+ }
+
+ const dataSet = {
+ color: getChartListColor(),
+ tooltip: {
+ trigger: 'item',
+ },
+ grid: {
+ left: '0',
+ right: '20px',
+ top: '5px',
+ bottom: '36px',
+ containLabel: true,
+ },
+ legend: {
+ left: 'center',
+ bottom: '0',
+ orient: 'horizontal', // legend 横向布局。
+ data: [t('pages.dashboardBase.chart.thisMonth'), t('pages.dashboardBase.chart.lastMonth')],
+ textStyle: {
+ fontSize: 12,
+ color: placeholderColor,
+ },
+ },
+ xAxis: {
+ type: 'category',
+ data: timeArray,
+ boundaryGap: false,
+ axisLabel: {
+ color: placeholderColor,
+ },
+ axisLine: {
+ lineStyle: {
+ width: 1,
+ },
+ },
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ color: placeholderColor,
+ },
+ splitLine: {
+ lineStyle: {
+ color: borderColor,
+ },
+ },
+ },
+ series: [
+ {
+ name: t('pages.dashboardBase.chart.thisMonth'),
+ data: outArray,
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ areaStyle: {
+ opacity: 0.1,
+ },
+ },
+ {
+ name: t('pages.dashboardBase.chart.lastMonth'),
+ data: inArray,
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ ],
+ };
+ return dataSet;
+}
+
+/**
+ * 获取饼图数据
+ *
+ * @export
+ * @param {number} [radius=1]
+ * @returns {*}
+ */
+export function getPieChartDataSet({
+ radius = 42,
+ textColor,
+ placeholderColor,
+ containerColor,
+}: { radius?: number } & Record): EChartsOption {
+ return {
+ color: getChartListColor(),
+ tooltip: {
+ show: false,
+ trigger: 'axis',
+ position: null,
+ },
+ grid: {
+ top: '0',
+ right: '0',
+ },
+ legend: {
+ selectedMode: false,
+ itemWidth: 12,
+ itemHeight: 4,
+ textStyle: {
+ fontSize: 12,
+ color: placeholderColor,
+ },
+ left: 'center',
+ bottom: '0',
+ orient: 'horizontal', // legend 横向布局。
+ },
+ series: [
+ {
+ name: '销售渠道',
+ type: 'pie',
+ radius: ['48%', '60%'],
+ avoidLabelOverlap: true,
+ selectedMode: true,
+ silent: true,
+ itemStyle: {
+ borderColor: containerColor,
+ borderWidth: 1,
+ },
+ label: {
+ show: true,
+ position: 'center',
+ formatter: ['{value|{d}%}', '{name|{b}}'].join('\n'),
+ rich: {
+ value: {
+ color: textColor,
+ fontSize: 28,
+ fontWeight: 'normal',
+ lineHeight: 46,
+ },
+ name: {
+ color: '#909399',
+ fontSize: 12,
+ lineHeight: 14,
+ },
+ },
+ },
+ emphasis: {
+ scale: true,
+ label: {
+ show: true,
+ formatter: ['{value|{d}%}', '{name|{b}渠道占比}'].join('\n'),
+ rich: {
+ value: {
+ color: textColor,
+ fontSize: 28,
+ fontWeight: 'normal',
+ lineHeight: 46,
+ },
+ name: {
+ color: '#909399',
+ fontSize: 14,
+ lineHeight: 14,
+ },
+ },
+ },
+ },
+ labelLine: {
+ show: false,
+ },
+ data: [
+ {
+ value: 1048,
+ name: t('pages.dashboardBase.topPanel.analysis.channel1'),
+ },
+ { value: radius * 7, name: t('pages.dashboardBase.topPanel.analysis.channel2') },
+ ],
+ },
+ ],
+ };
+}
diff --git a/src/pages/dashboard/base/index.vue b/src/pages/dashboard/base/index.vue
new file mode 100644
index 0000000..0f791af
--- /dev/null
+++ b/src/pages/dashboard/base/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/dashboard/detail/constants.ts b/src/pages/dashboard/detail/constants.ts
new file mode 100644
index 0000000..e9782b9
--- /dev/null
+++ b/src/pages/dashboard/detail/constants.ts
@@ -0,0 +1,52 @@
+import { t } from '@/locales';
+
+export const PANE_LIST_DATA = [
+ {
+ title: t('pages.dashboardDetail.topPanel.paneList.totalRequest'),
+ number: '1126',
+ upTrend: '10%',
+ },
+ {
+ title: t('pages.dashboardDetail.topPanel.paneList.suppliers'),
+ number: '13',
+ downTrend: '13%',
+ },
+ {
+ title: t('pages.dashboardDetail.topPanel.paneList.productCategory'),
+ number: '4',
+ upTrend: '10%',
+ },
+ {
+ title: t('pages.dashboardDetail.topPanel.paneList.applicant'),
+ number: 90,
+ downTrend: '44%',
+ leftType: 'icon-file-paste',
+ },
+ {
+ title: t('pages.dashboardDetail.topPanel.paneList.completionRate'),
+ number: 80.5,
+ upTrend: '70%',
+ },
+ {
+ title: t('pages.dashboardDetail.topPanel.paneList.arrivalRate'),
+ number: 78,
+ upTrend: '16%',
+ },
+];
+
+export const PRODUCT_LIST = [
+ {
+ description: t('pages.dashboardDetail.sslDescription'),
+ index: 1,
+ isSetup: true,
+ name: t('pages.dashboardDetail.ssl'),
+ type: 4,
+ },
+ {
+ description: t('pages.dashboardDetail.sslDescription'),
+ index: 1,
+ isSetup: true,
+ name: t('pages.dashboardDetail.ssl'),
+ type: 4,
+ },
+];
diff --git a/src/pages/dashboard/detail/index.ts b/src/pages/dashboard/detail/index.ts
new file mode 100644
index 0000000..e13587e
--- /dev/null
+++ b/src/pages/dashboard/detail/index.ts
@@ -0,0 +1,271 @@
+import dayjs from 'dayjs';
+
+import { TChartColor } from '@/config/color';
+import { t } from '@/locales';
+import { getDateArray, getRandomArray } from '@/utils/charts';
+import { getChartListColor } from '@/utils/color';
+/**
+ * 散点图数据
+ *
+ * @export
+ * @returns {}
+ */
+export function getScatterDataSet({
+ dateTime = [],
+ placeholderColor,
+ borderColor,
+}: { dateTime?: Array } & TChartColor) {
+ const divideNum = 40;
+ const timeArray = [];
+ const inArray = [];
+ const outArray = [];
+ for (let i = 0; i < divideNum; i++) {
+ // const [timeArray, inArray, outArray] = dataset;
+ if (dateTime.length > 0) {
+ const dateAbsTime: number = (new Date(dateTime[1]).getTime() - new Date(dateTime[0]).getTime()) / divideNum;
+ const endTime: number = new Date(dateTime[0]).getTime() + dateAbsTime * i;
+ timeArray.push(dayjs(endTime).format('MM-DD'));
+ } else {
+ timeArray.push(
+ dayjs()
+ .subtract(divideNum - i, 'day')
+ .format('MM-DD'),
+ );
+ }
+
+ inArray.push(getRandomArray().toString());
+ outArray.push(getRandomArray().toString());
+ }
+
+ return {
+ color: getChartListColor(),
+ xAxis: {
+ data: timeArray,
+ axisLabel: {
+ color: placeholderColor,
+ },
+ splitLine: { show: false },
+ axisLine: {
+ lineStyle: {
+ color: borderColor,
+ width: 1,
+ },
+ },
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ color: placeholderColor,
+ },
+ nameTextStyle: {
+ padding: [0, 0, 0, 60],
+ },
+ axisTick: {
+ show: false,
+ axisLine: {
+ show: false,
+ },
+ },
+ axisLine: {
+ show: false,
+ },
+ splitLine: {
+ lineStyle: {
+ color: borderColor,
+ },
+ },
+ },
+ tooltip: {
+ trigger: 'item',
+ },
+ grid: {
+ top: '5px',
+ left: '25px',
+ right: '5px',
+ bottom: '60px',
+ },
+ legend: {
+ left: 'center',
+ bottom: '0',
+ orient: 'horizontal', // legend 横向布局。
+ data: [
+ t(`pages.dashboardDetail.procurement.goods.massageMachine`),
+ t(`pages.dashboardDetail.procurement.goods.coffeeMachine`),
+ ],
+ itemHeight: 8,
+ itemWidth: 8,
+ textStyle: {
+ fontSize: 12,
+ color: placeholderColor,
+ },
+ },
+ series: [
+ {
+ name: t(`pages.dashboardDetail.procurement.goods.massageMachine`),
+ symbolSize: 10,
+ data: outArray.reverse(),
+ type: 'scatter',
+ },
+ {
+ name: t(`pages.dashboardDetail.procurement.goods.coffeeMachine`),
+ symbolSize: 10,
+ data: inArray.concat(inArray.reverse()),
+ type: 'scatter',
+ },
+ ],
+ };
+}
+
+/** 折线图数据 */
+export function getFolderLineDataSet({
+ dateTime = [],
+ placeholderColor,
+ borderColor,
+}: { dateTime?: Array } & TChartColor) {
+ let dateArray = [];
+ for (let i = 1; i < 7; i++) {
+ dateArray.push(t(`pages.dashboardDetail.chart.week${i}`));
+ }
+ if (dateTime.length > 0) {
+ const divideNum = 7;
+ dateArray = getDateArray(dateTime, divideNum);
+ }
+ return {
+ color: getChartListColor(),
+ grid: {
+ top: '5%',
+ right: '10px',
+ left: '30px',
+ bottom: '60px',
+ },
+ legend: {
+ left: 'center',
+ bottom: '0',
+ orient: 'horizontal', // legend 横向布局。
+ data: [
+ t(`pages.dashboardDetail.procurement.goods.cup`),
+ t(`pages.dashboardDetail.procurement.goods.tea`),
+ t(`pages.dashboardDetail.procurement.goods.honey`),
+ t(`pages.dashboardDetail.procurement.goods.flour`),
+ ],
+ textStyle: {
+ fontSize: 12,
+ color: placeholderColor,
+ },
+ },
+ xAxis: {
+ type: 'category',
+ data: dateArray,
+ boundaryGap: false,
+ axisLabel: {
+ color: placeholderColor,
+ },
+ axisLine: {
+ lineStyle: {
+ color: borderColor,
+ width: 1,
+ },
+ },
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ color: placeholderColor,
+ },
+ splitLine: {
+ lineStyle: {
+ color: borderColor,
+ },
+ },
+ },
+ tooltip: {
+ trigger: 'item',
+ },
+ series: [
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: t(`pages.dashboardDetail.procurement.goods.cup`),
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: t(`pages.dashboardDetail.procurement.goods.tea`),
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: t(`pages.dashboardDetail.procurement.goods.honey`),
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: t(`pages.dashboardDetail.procurement.goods.flour`),
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ ],
+ };
+}
diff --git a/src/pages/dashboard/detail/index.vue b/src/pages/dashboard/detail/index.vue
new file mode 100644
index 0000000..fd245cd
--- /dev/null
+++ b/src/pages/dashboard/detail/index.vue
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+ {{ item.number }}
+
+
+ {{ $t('pages.dashboardDetail.topPanel.quarter') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('pages.dashboardDetail.satisfaction.export') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/list/base/index.vue b/src/pages/list/base/index.vue
new file mode 100644
index 0000000..84eeb74
--- /dev/null
+++ b/src/pages/list/base/index.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
{{ $t('pages.listBase.create') }}
+
+ {{ $t('pages.listBase.export') }}
+
+ {{ $t('pages.listBase.select') }} {{ selectedRowKeys.length }} {{ $t('pages.listBase.items') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('pages.listBase.contractStatusEnum.fail') }}
+
+ {{ $t('pages.listBase.contractStatusEnum.audit') }}
+
+
+ {{ $t('pages.listBase.contractStatusEnum.pending') }}
+
+
+ {{ $t('pages.listBase.contractStatusEnum.executing') }}
+
+
+ {{ $t('pages.listBase.contractStatusEnum.finish') }}
+
+
+
+ {{ $t('pages.listBase.contractStatusEnum.fail') }}
+ {{ $t('pages.listBase.contractStatusEnum.audit') }}
+
+ {{ $t('pages.listBase.contractStatusEnum.pending') }}
+
+
+
+
+ {{ $t('pages.listBase.pay') }}
+
+
+ {{ $t('pages.listBase.receive') }}
+
+
+
+
+
+ {{ $t('pages.listBase.detail') }}
+ {{ $t('pages.listBase.delete') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/list/tree/constants.ts b/src/pages/list/tree/constants.ts
new file mode 100644
index 0000000..33dc05c
--- /dev/null
+++ b/src/pages/list/tree/constants.ts
@@ -0,0 +1,86 @@
+export const TREE_DATA = [
+ {
+ label: '深圳总部',
+ value: 0,
+ children: [
+ {
+ label: '总办',
+ value: '0-0',
+ },
+ {
+ label: '市场部',
+ value: '0-1',
+ children: [
+ {
+ label: '采购1组',
+ value: '0-1-0',
+ },
+ {
+ label: '采购2组',
+ value: '0-1-1',
+ },
+ ],
+ },
+ {
+ label: '技术部',
+ value: '0-2',
+ },
+ ],
+ },
+ {
+ label: '北京总部',
+ value: 1,
+ children: [
+ {
+ label: '总办',
+ value: '1-0',
+ },
+ {
+ label: '市场部',
+ value: '1-1',
+ children: [
+ {
+ label: '采购1组',
+ value: '1-1-0',
+ },
+ {
+ label: '采购2组',
+ value: '1-1-1',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ label: '上海总部',
+ value: 2,
+ children: [
+ {
+ label: '市场部',
+ value: '2-0',
+ },
+ {
+ label: '财务部',
+ value: '2-1',
+ children: [
+ {
+ label: '财务1组',
+ value: '2-1-0',
+ },
+ {
+ label: '财务2组',
+ value: '2-1-1',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ label: '湖南',
+ value: 3,
+ },
+ {
+ label: '湖北',
+ value: 4,
+ },
+];
diff --git a/src/pages/list/tree/index.vue b/src/pages/list/tree/index.vue
new file mode 100644
index 0000000..9c5a6df
--- /dev/null
+++ b/src/pages/list/tree/index.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/login/components/Header.vue b/src/pages/login/components/Header.vue
new file mode 100644
index 0000000..2bf17af
--- /dev/null
+++ b/src/pages/login/components/Header.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
diff --git a/src/pages/login/components/Login.vue b/src/pages/login/components/Login.vue
new file mode 100644
index 0000000..cf55f87
--- /dev/null
+++ b/src/pages/login/components/Login.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('pages.login.remember') }}
+ {{ $t('pages.login.forget') }}
+
+
+
+
+
+
+ {{ $t('pages.login.wechatLogin') }}
+ {{ $t('pages.login.refresh') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ countDown == 0 ? $t('pages.login.sendVerification') : `${countDown}秒后可重发` }}
+
+
+
+
+
+ {{ $t('pages.login.signIn') }}
+
+
+
+ {{
+ $t('pages.login.accountLogin')
+ }}
+ {{
+ $t('pages.login.wechatLogin')
+ }}
+ {{ $t('pages.login.phoneLogin') }}
+
+
+
+
+
+
+
diff --git a/src/pages/login/components/Register.vue b/src/pages/login/components/Register.vue
new file mode 100644
index 0000000..a329577
--- /dev/null
+++ b/src/pages/login/components/Register.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ countDown == 0 ? '发送验证码' : `${countDown}秒后可重发` }}
+
+
+
+
+
+ 我已阅读并同意 TDesign服务协议 和
+ TDesign 隐私声明
+
+
+
+ 注册
+
+
+
+ {{
+ type == 'phone' ? '使用邮箱注册' : '使用手机号注册'
+ }}
+
+
+
+
+
+
+
diff --git a/src/pages/login/index.less b/src/pages/login/index.less
new file mode 100644
index 0000000..b978dd5
--- /dev/null
+++ b/src/pages/login/index.less
@@ -0,0 +1,194 @@
+.light {
+ &.login-wrapper {
+ background-color: white;
+ background-image: url('@/assets/assets-login-bg-white.png');
+ }
+}
+
+.dark {
+ &.login-wrapper {
+ background-color: var(--td-bg-color-page);
+ background-image: url('@/assets/assets-login-bg-black.png');
+ }
+}
+
+.login-wrapper {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-size: cover;
+ background-position: 100%;
+ position: relative;
+}
+
+.login-container {
+ position: absolute;
+ top: 22%;
+ left: 5%;
+ min-height: 500px;
+}
+
+.title-container {
+ .title {
+ font: var(--td-font-headline-large);
+ color: var(--td-text-color-primary);
+ margin-top: var(--td-comp-margin-xs);
+
+ &.margin-no {
+ margin-top: 0;
+ }
+ }
+
+ .sub-title {
+ margin-top: var(--td-comp-margin-xxl);
+
+ .tip {
+ display: inline-block;
+ margin-right: var(--td-comp-margin-s);
+ font: var(--td-font-body-medium);
+
+ &:first-child {
+ color: var(--td-text-color-secondary);
+ }
+
+ &:last-child {
+ color: var(--td-text-color-primary);
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.item-container {
+ width: 400px;
+ margin-top: var(--td-comp-margin-xxxxl);
+
+ &.login-qrcode {
+ .tip-container {
+ margin-bottom: var(--td-comp-margin-l);
+ font: var(--td-font-body-medium);
+ display: flex;
+ align-items: flex-start;
+
+ .tip {
+ color: var(--td-text-color-primary);
+ margin-right: var(--td-comp-margin-s);
+ }
+
+ .refresh {
+ display: flex;
+ align-items: center;
+ color: var(--td-brand-color);
+
+ .t-icon {
+ font-size: var(--td-comp-size-xxxs);
+ margin-left: var(--td-comp-margin-xs);
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+ }
+
+ .bottom-container {
+ margin-top: 32px;
+ }
+ }
+
+ &.login-phone {
+ .bottom-container {
+ margin-top: 66px;
+ }
+ }
+
+ .check-container {
+ display: flex;
+ align-items: center;
+
+ &.remember-pwd {
+ margin-bottom: var(--td-comp-margin-l);
+ justify-content: space-between;
+ }
+
+ span {
+ color: var(--td-brand-color);
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+ }
+
+ .verification-code {
+ display: flex;
+ align-items: center;
+
+ :deep(.t-form__controls) {
+ width: 100%;
+
+ button {
+ flex-shrink: 0;
+ margin-left: var(--td-comp-margin-l);
+ width: 128px;
+ }
+ }
+ }
+
+ .btn-container {
+ margin-top: var(--td-comp-margin-xxxxl);
+ }
+}
+
+.switch-container {
+ margin-top: var(--td-comp-margin-xxl);
+
+ .tip {
+ font: var(--td-font-body-medium);
+ color: var(--td-brand-color);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ margin-right: var(--td-comp-margin-l);
+
+ &:last-child {
+ &::after {
+ display: none;
+ }
+ }
+
+ &::after {
+ content: '';
+ display: block;
+ width: 1px;
+ height: 12px;
+ background: var(--td-component-stroke);
+ margin-left: var(--td-comp-margin-l);
+ }
+ }
+}
+
+.check-container {
+ font: var(--td-font-body-medium);
+ color: var(--td-text-color-secondary);
+
+ .tip {
+ float: right;
+ font: var(--td-font-body-medium);
+ color: var(--td-brand-color);
+ }
+}
+
+.copyright {
+ font: var(--td-font-body-medium);
+ position: absolute;
+ left: 5%;
+ bottom: 64px;
+ color: var(--td-text-color-secondary);
+}
+
+@media screen and (height <= 700px) {
+ .copyright {
+ display: none;
+ }
+}
diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue
new file mode 100644
index 0000000..b7b0144
--- /dev/null
+++ b/src/pages/login/index.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
{{ $t('pages.login.loginTitle') }}
+
TDesign Starter
+
+
{{ type == 'register' ? $t('pages.login.existAccount') : $t('pages.login.noAccount') }}
+
+ {{ type == 'register' ? $t('pages.login.signIn') : $t('pages.login.createAccount') }}
+
+
+
+
+
+
+
+
+
+
Copyright @ 2021-2023 Tencent. All Rights Reserved
+
+
+
+
+
+
diff --git a/src/pages/user/constants.ts b/src/pages/user/constants.ts
new file mode 100644
index 0000000..74f03ad
--- /dev/null
+++ b/src/pages/user/constants.ts
@@ -0,0 +1,70 @@
+export interface UserInfoListType {
+ title: string;
+ content: string;
+ span?: number;
+}
+
+export const USER_INFO_LIST: Array = [
+ {
+ title: 'pages.user.personalInfo.desc.mobile',
+ content: '+86 13923734567',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.phone',
+ content: '734567',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.email',
+ content: 'Account@qq.com',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.seat',
+ content: 'T32F 012',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.entity',
+ content: '腾讯集团',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.leader',
+ content: 'Michael Wang',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.position',
+ content: '高级 UI 设计师',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.joinDay',
+ content: '2021-07-01',
+ },
+ {
+ title: 'pages.user.personalInfo.desc.group',
+ content: '腾讯/腾讯公司/某事业群/某产品部/某运营中心/商户服务组',
+ span: 6,
+ },
+];
+
+export const TEAM_MEMBERS = [
+ {
+ avatar: 'https://avatars.githubusercontent.com/Wen1kang',
+ title: 'Lovellzhong 钟某某',
+ description: '直客销售 港澳拓展组员工',
+ },
+ {
+ avatar: 'https://avatars.githubusercontent.com/pengYYYYY',
+ title: 'Jiajingwang 彭某某',
+ description: '前端开发 前台研发组员工',
+ },
+ {
+ avatar: 'https://avatars.githubusercontent.com/u/24469546?s=96&v=4',
+ title: 'cruisezhang 林某某',
+ description: '技术产品 产品组员工',
+ },
+ {
+ avatar: 'https://avatars.githubusercontent.com/u/88708072?s=96&v=4',
+ title: 'Lovellzhang 商某某',
+ description: '产品运营 港澳拓展组员工',
+ },
+];
+
+export const PRODUCT_LIST = ['a', 'b', 'c', 'd'];
diff --git a/src/pages/user/index.less b/src/pages/user/index.less
new file mode 100644
index 0000000..33bf30e
--- /dev/null
+++ b/src/pages/user/index.less
@@ -0,0 +1,191 @@
+:deep(.t-card__title) {
+ font: var(--td-font-title-large);
+ font-weight: 400;
+}
+
+.user-left-greeting {
+ padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
+ font: var(--td-font-title-large);
+ background: var(--td-bg-color-container);
+ color: var(--td-text-color-primary);
+ text-align: left;
+ border-radius: var(--td-radius-medium);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .regular {
+ margin-left: var(--td-comp-margin-xl);
+ font: var(--td-font-body-medium);
+ }
+
+ .logo {
+ width: 168px;
+ }
+}
+
+.user-info-list {
+ margin-top: var(--td-comp-margin-l);
+ padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
+
+ .content {
+ width: 90%;
+ }
+
+ :deep(.t-card__header) {
+ padding: 0;
+ }
+
+ :deep(.t-card__body) {
+ padding: 0;
+ }
+
+ .contract {
+ &-title {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin: var(--td-comp-margin-xxxl) 0 var(--td-comp-margin-l);
+ font: var(--td-font-body-medium);
+ color: var(--td-text-color-placeholder);
+ }
+
+ &-detail {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font: var(--td-font-body-medium);
+ color: var(--td-text-color-primary);
+ }
+ }
+
+ .contract:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.user-intro {
+ padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
+ background: var(--td-brand-color);
+ border-radius: var(--td-radius-medium);
+ color: var(--td-text-color-primary);
+
+ :deep(.t-card__body) {
+ padding: 0;
+ }
+
+ .name {
+ font: var(--td-font-title-large);
+ margin-top: var(--td-comp-margin-xxxl);
+ color: var(--td-text-color-anti);
+ }
+
+ .position {
+ font: var(--td-font-body-medium);
+ margin-top: var(--td-comp-margin-s);
+ color: var(--td-text-color-anti);
+ }
+
+ .user-info {
+ line-height: 24px;
+ font-size: 14px;
+ color: var(--td-text-color-primary);
+
+ .hiredate,
+ .del,
+ .mail {
+ display: flex;
+ }
+
+ .t-icon {
+ height: 24px;
+ margin-right: 8px;
+ }
+
+ .del {
+ margin: 16px 0;
+ }
+ }
+}
+
+.product-container {
+ margin-top: var(--td-comp-margin-l);
+ border-radius: var(--td-radius-medium);
+ padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
+
+ :deep(.t-card__header) {
+ padding: 0;
+ margin-bottom: var(--td-comp-margin-m);
+ }
+
+ :deep(.t-card__body) {
+ padding: 0;
+ }
+
+ .content {
+ width: 100%;
+ margin: var(--td-comp-margin-xxxl) 0 0;
+ }
+
+ .logo {
+ width: var(--td-comp-size-xxl);
+ }
+}
+
+.content-container {
+ margin-top: var(--td-comp-margin-l);
+ background: var(--td-bg-color-container);
+ border-radius: var(--td-radius-medium);
+ padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
+
+ :deep(.t-card__header) {
+ padding: 0;
+ }
+
+ :deep(.t-card__body) {
+ padding: 0;
+ }
+
+ .card-padding-no {
+ margin-top: var(--td-comp-margin-xxxl);
+
+ :deep(.t-card__body) {
+ margin-top: var(--td-comp-margin-xxl);
+ }
+ }
+}
+
+.user-team {
+ margin-top: var(--td-comp-margin-l);
+ padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
+
+ :deep(.t-card__header) {
+ padding: 0;
+ margin-bottom: var(--td-comp-margin-m);
+ }
+
+ :deep(.t-card__body) {
+ padding: 0;
+ }
+
+ .t-list-item {
+ margin-top: var(--td-comp-margin-xxl);
+ padding: 0;
+
+ :deep(.t-list-item__meta-avatar) {
+ height: var(--td-comp-size-xxl);
+ width: var(--td-comp-size-xxl);
+ margin-right: var(--td-comp-margin-xxl);
+ }
+
+ :deep(.t-list-item__meta-title) {
+ margin: 0 0 var(--td-comp-margin-xs);
+ }
+
+ :deep(.t-list-item__meta-description) {
+ display: inline-block;
+ color: var(--td-text-color-placeholder);
+ font: var(--td-font-body-medium);
+ }
+ }
+}
diff --git a/src/pages/user/index.ts b/src/pages/user/index.ts
new file mode 100644
index 0000000..3d501cf
--- /dev/null
+++ b/src/pages/user/index.ts
@@ -0,0 +1,149 @@
+import { TChartColor } from '@/config/color';
+import { getDateArray, getRandomArray } from '@/utils/charts';
+import { getChartListColor } from '@/utils/color';
+
+/** 折线图数据 */
+export function getFolderLineDataSet({
+ dateTime = [],
+ placeholderColor,
+ borderColor,
+}: { dateTime?: Array } & TChartColor) {
+ let dateArray: Array = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
+ if (dateTime.length > 0) {
+ const divideNum = 7;
+ dateArray = getDateArray(dateTime, divideNum);
+ }
+ return {
+ color: getChartListColor(),
+ grid: {
+ top: '5%',
+ right: '10px',
+ left: '30px',
+ bottom: '60px',
+ },
+ legend: {
+ left: 'center',
+ bottom: '0',
+ orient: 'horizontal', // legend 横向布局。
+ data: ['杯子', '茶叶', '蜂蜜', '面粉'],
+ textStyle: {
+ fontSize: 12,
+ color: placeholderColor,
+ },
+ },
+ xAxis: {
+ type: 'category',
+ data: dateArray,
+ boundaryGap: false,
+ axisLabel: {
+ color: placeholderColor,
+ },
+ axisLine: {
+ lineStyle: {
+ color: borderColor,
+ width: 1,
+ },
+ },
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ color: placeholderColor,
+ },
+ splitLine: {
+ lineStyle: {
+ color: borderColor,
+ },
+ },
+ },
+ tooltip: {
+ trigger: 'item',
+ },
+ series: [
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: '杯子',
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: '茶叶',
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: '蜂蜜',
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ {
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ name: '面粉',
+ stack: '总量',
+ data: [
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ getRandomArray(),
+ ],
+ type: 'line',
+ itemStyle: {
+ borderColor,
+ borderWidth: 1,
+ },
+ },
+ ],
+ };
+}
diff --git a/src/pages/user/index.vue b/src/pages/user/index.vue
new file mode 100644
index 0000000..5ceecb4
--- /dev/null
+++ b/src/pages/user/index.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+ Hi,Image
+ {{ $t('pages.user.markDay') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t(item.title) }}
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+ {{ $t('pages.user.contentList') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('pages.user.contentList') }}
+
+
+
+
+
+
+
+ T
+ My Account
+ {{ $t('pages.user.personalInfo.position') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/permission.ts b/src/permission.ts
new file mode 100644
index 0000000..ab7b7d3
--- /dev/null
+++ b/src/permission.ts
@@ -0,0 +1,82 @@
+import 'nprogress/nprogress.css'; // progress bar style
+
+import NProgress from 'nprogress'; // progress bar
+import { MessagePlugin } from 'tdesign-vue-next';
+import { RouteRecordRaw } from 'vue-router';
+
+import router from '@/router';
+import { getPermissionStore, useUserStore } from '@/store';
+import { PAGE_NOT_FOUND_ROUTE } from '@/utils/route/constant';
+
+NProgress.configure({ showSpinner: false });
+
+router.beforeEach(async (to, from, next) => {
+ NProgress.start();
+
+ const permissionStore = getPermissionStore();
+ const { whiteListRouters } = permissionStore;
+
+ const userStore = useUserStore();
+
+ if (userStore.token) {
+ if (to.path === '/login') {
+ next();
+ return;
+ }
+ try {
+ await userStore.getUserInfo();
+
+ const { asyncRoutes } = permissionStore;
+
+ if (asyncRoutes && asyncRoutes.length === 0) {
+ const routeList = await permissionStore.buildAsyncRoutes();
+ routeList.forEach((item: RouteRecordRaw) => {
+ router.addRoute(item);
+ });
+
+ if (to.name === PAGE_NOT_FOUND_ROUTE.name) {
+ // 动态添加路由后,此处应当重定向到fullPath,否则会加载404页面内容
+ next({ path: to.fullPath, replace: true, query: to.query });
+ } else {
+ const redirect = decodeURIComponent((from.query.redirect || to.path) as string);
+ next(to.path === redirect ? { ...to, replace: true } : { path: redirect });
+ return;
+ }
+ }
+ if (router.hasRoute(to.name)) {
+ next();
+ } else {
+ next(`/`);
+ }
+ } catch (error) {
+ MessagePlugin.error(error.message);
+ next({
+ path: '/login',
+ query: { redirect: encodeURIComponent(to.fullPath) },
+ });
+ NProgress.done();
+ }
+ } else {
+ /* white list router */
+ if (whiteListRouters.indexOf(to.path) !== -1) {
+ next();
+ } else {
+ next({
+ path: '/login',
+ query: { redirect: encodeURIComponent(to.fullPath) },
+ });
+ }
+ NProgress.done();
+ }
+});
+
+router.afterEach((to) => {
+ if (to.path === '/login') {
+ const userStore = useUserStore();
+ const permissionStore = getPermissionStore();
+
+ userStore.logout();
+ permissionStore.restoreRoutes();
+ }
+ NProgress.done();
+});
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..5076429
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,87 @@
+import uniq from 'lodash/uniq';
+import { createRouter, createWebHistory, RouteRecordRaw, useRoute } from 'vue-router';
+
+const env = import.meta.env.MODE || 'development';
+
+// 导入homepage相关固定路由
+const homepageModules = import.meta.glob('./modules/**/homepage.ts', { eager: true });
+
+// 导入modules非homepage相关固定路由
+const fixedModules = import.meta.glob('./modules/**/!(homepage).ts', { eager: true });
+
+// 其他固定路由
+const defaultRouterList: Array = [
+ {
+ path: '/login',
+ name: 'login',
+ component: () => import('@/pages/login/index.vue'),
+ },
+ {
+ path: '/',
+ redirect: '/dashboard/base',
+ },
+];
+// 存放固定路由
+export const homepageRouterList: Array = mapModuleRouterList(homepageModules);
+export const fixedRouterList: Array = mapModuleRouterList(fixedModules);
+
+export const allRoutes = [...homepageRouterList, ...fixedRouterList, ...defaultRouterList];
+
+// 固定路由模块转换为路由
+export function mapModuleRouterList(modules: Record): Array {
+ const routerList: Array = [];
+ Object.keys(modules).forEach((key) => {
+ // @ts-ignore
+ const mod = modules[key].default || {};
+ const modList = Array.isArray(mod) ? [...mod] : [mod];
+ routerList.push(...modList);
+ });
+ return routerList;
+}
+
+export const getRoutesExpanded = () => {
+ const expandedRoutes: Array = [];
+
+ fixedRouterList.forEach((item) => {
+ if (item.meta && item.meta.expanded) {
+ expandedRoutes.push(item.path);
+ }
+ if (item.children && item.children.length > 0) {
+ item.children
+ .filter((child) => child.meta && child.meta.expanded)
+ .forEach((child: RouteRecordRaw) => {
+ expandedRoutes.push(item.path);
+ expandedRoutes.push(`${item.path}/${child.path}`);
+ });
+ }
+ });
+ return uniq(expandedRoutes);
+};
+
+export const getActive = (maxLevel = 3): string => {
+ const route = useRoute();
+
+ if (!route.path) {
+ return '';
+ }
+
+ return route.path
+ .split('/')
+ .filter((_item: string, index: number) => index <= maxLevel && index > 0)
+ .map((item: string) => `/${item}`)
+ .join('');
+};
+
+const router = createRouter({
+ history: createWebHistory(env === 'site' ? '/starter/vue-next/' : import.meta.env.VITE_BASE_URL),
+ routes: allRoutes,
+ scrollBehavior() {
+ return {
+ el: '#app',
+ top: 0,
+ behavior: 'smooth',
+ };
+ },
+});
+
+export default router;
diff --git a/src/router/modules/homepage.ts b/src/router/modules/homepage.ts
new file mode 100644
index 0000000..883738a
--- /dev/null
+++ b/src/router/modules/homepage.ts
@@ -0,0 +1,45 @@
+import { DashboardIcon } from 'tdesign-icons-vue-next';
+import { shallowRef } from 'vue';
+
+import Layout from '@/layouts/index.vue';
+
+export default [
+ {
+ path: '/dashboard',
+ component: Layout,
+ redirect: '/dashboard/base',
+ name: 'dashboard',
+ meta: {
+ title: {
+ zh_CN: '仪表盘',
+ en_US: 'Dashboard',
+ },
+ icon: shallowRef(DashboardIcon),
+ orderNo: 0,
+ },
+ children: [
+ {
+ path: 'base',
+ name: 'DashboardBase',
+ component: () => import('@/pages/dashboard/base/index.vue'),
+ meta: {
+ title: {
+ zh_CN: '概览仪表盘',
+ en_US: 'Overview',
+ },
+ },
+ },
+ {
+ path: 'detail',
+ name: 'DashboardDetail',
+ component: () => import('@/pages/dashboard/detail/index.vue'),
+ meta: {
+ title: {
+ zh_CN: '统计报表',
+ en_US: 'Dashboard Detail',
+ },
+ },
+ },
+ ],
+ },
+];
diff --git a/src/router/modules/list.ts b/src/router/modules/list.ts
new file mode 100644
index 0000000..540cb58
--- /dev/null
+++ b/src/router/modules/list.ts
@@ -0,0 +1,44 @@
+import { ViewListIcon } from 'tdesign-icons-vue-next';
+import { shallowRef } from 'vue';
+
+import Layout from '@/layouts/index.vue';
+
+export default [
+ {
+ path: '/list',
+ component: Layout,
+ redirect: '/list/base',
+ name: 'list',
+ meta: {
+ title: {
+ zh_CN: '列表页',
+ en_US: 'List',
+ },
+ icon: shallowRef(ViewListIcon),
+ },
+ children: [
+ {
+ path: 'base',
+ name: 'ListBase',
+ component: () => import('@/pages/list/base/index.vue'),
+ meta: {
+ title: {
+ zh_CN: '基础列表页',
+ en_US: 'Base List',
+ },
+ },
+ },
+ {
+ path: 'tree',
+ name: 'ListTree',
+ component: () => import('@/pages/list/tree/index.vue'),
+ meta: {
+ title: {
+ zh_CN: '树状筛选列表页',
+ en_US: 'Tree List',
+ },
+ },
+ },
+ ],
+ },
+];
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..862341a
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,15 @@
+import { createPinia } from 'pinia';
+import { createPersistedState } from 'pinia-plugin-persistedstate';
+
+const store = createPinia();
+store.use(createPersistedState());
+
+export { store };
+
+export * from './modules/notification';
+export * from './modules/permission';
+export * from './modules/setting';
+export * from './modules/tabs-router';
+export * from './modules/user';
+
+export default store;
diff --git a/src/store/modules/notification.ts b/src/store/modules/notification.ts
new file mode 100644
index 0000000..c5fabd3
--- /dev/null
+++ b/src/store/modules/notification.ts
@@ -0,0 +1,60 @@
+import { defineStore } from 'pinia';
+
+import type { NotificationItem } from '@/types/interface';
+
+const msgData = [
+ {
+ id: '123',
+ content: '腾讯大厦一楼改造施工项目 已通过审核!',
+ type: '合同动态',
+ status: true,
+ collected: false,
+ date: '2021-01-01 08:00',
+ quality: 'high',
+ },
+ {
+ id: '124',
+ content: '三季度生产原材料采购项目 开票成功!',
+ type: '票务动态',
+ status: true,
+ collected: false,
+ date: '2021-01-01 08:00',
+ quality: 'low',
+ },
+ {
+ id: '125',
+ content: '2021-01-01 10:00的【国家电网线下签约】会议即将开始,请提前10分钟前往 会议室1 进行签到!',
+ type: '会议通知',
+ status: true,
+ collected: false,
+ date: '2021-01-01 08:00',
+ quality: 'middle',
+ },
+ {
+ id: '126',
+ content: '一季度生产原材料采购项目 开票成功!',
+ type: '票务动态',
+ status: true,
+ collected: false,
+ date: '2021-01-01 08:00',
+ quality: 'low',
+ },
+];
+
+type MsgDataType = typeof msgData;
+
+export const useNotificationStore = defineStore('notification', {
+ state: () => ({
+ msgData,
+ }),
+ getters: {
+ unreadMsg: (state) => state.msgData.filter((item: NotificationItem) => item.status),
+ readMsg: (state) => state.msgData.filter((item: NotificationItem) => !item.status),
+ },
+ actions: {
+ setMsgData(data: MsgDataType) {
+ this.msgData = data;
+ },
+ },
+ persist: true,
+});
diff --git a/src/store/modules/permission-fe.ts b/src/store/modules/permission-fe.ts
new file mode 100644
index 0000000..2c08b61
--- /dev/null
+++ b/src/store/modules/permission-fe.ts
@@ -0,0 +1,70 @@
+// 前端 roles 控制菜单权限 通过登录后的角色对菜单就行过滤处理
+// 如果需要前端 roles 控制菜单权限 请使用此文件代码替换 permission.ts 的内容
+
+import { defineStore } from 'pinia';
+import { RouteRecordRaw } from 'vue-router';
+
+import router, { allRoutes } from '@/router';
+import { store } from '@/store';
+
+function filterPermissionsRouters(routes: Array, roles: Array) {
+ const res: Array = [];
+ const removeRoutes: Array = [];
+ routes.forEach((route) => {
+ const children: Array = [];
+ route.children?.forEach((childRouter) => {
+ const roleCode = childRouter.meta?.roleCode || childRouter.name;
+ if (roles.indexOf(roleCode) !== -1) {
+ children.push(childRouter);
+ } else {
+ removeRoutes.push(childRouter);
+ }
+ });
+ if (children.length > 0) {
+ route.children = children;
+ res.push(route);
+ }
+ });
+ return { accessedRouters: res, removeRoutes };
+}
+
+export const usePermissionStore = defineStore('permission', {
+ state: () => ({
+ whiteListRouters: ['/login'],
+ routers: [],
+ removeRoutes: [],
+ }),
+ actions: {
+ async initRoutes(roles: Array) {
+ let accessedRouters = [];
+
+ let removeRoutes: Array = [];
+ // special token
+ if (roles.includes('all')) {
+ accessedRouters = allRoutes;
+ } else {
+ const res = filterPermissionsRouters(allRoutes, roles);
+ accessedRouters = res.accessedRouters;
+ removeRoutes = res.removeRoutes;
+ }
+
+ this.routers = accessedRouters;
+ this.removeRoutes = removeRoutes;
+
+ removeRoutes.forEach((item: RouteRecordRaw) => {
+ if (router.hasRoute(item.name)) {
+ router.removeRoute(item.name);
+ }
+ });
+ },
+ async restore() {
+ this.removeRoutes.forEach((item: RouteRecordRaw) => {
+ router.addRoute(item);
+ });
+ },
+ },
+});
+
+export function getPermissionStore() {
+ return usePermissionStore(store);
+}
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
new file mode 100644
index 0000000..07fd7a5
--- /dev/null
+++ b/src/store/modules/permission.ts
@@ -0,0 +1,124 @@
+// 前端 roles 控制菜单权限 通过登录后的角色对菜单就行过滤处理
+// 如果需要前端 roles 控制菜单权限 请使用此文件代码替换 permission.ts 的内容
+
+import { defineStore } from 'pinia';
+import { RouteRecordRaw } from 'vue-router';
+
+import router, { allRoutes } from '@/router';
+import { store } from '@/store';
+
+function filterPermissionsRouters(routes: Array, roles: Array) {
+ const res: Array = [];
+ const removeRoutes: Array = [];
+ routes.forEach((route) => {
+ const children: Array = [];
+ route.children?.forEach((childRouter) => {
+ const roleCode = childRouter.meta?.roleCode || childRouter.name;
+ if (roles.indexOf(roleCode) !== -1) {
+ children.push(childRouter);
+ } else {
+ removeRoutes.push(childRouter);
+ }
+ });
+ if (children.length > 0) {
+ route.children = children;
+ res.push(route);
+ }
+ });
+ return { accessedRouters: res, removeRoutes };
+}
+
+export const usePermissionStore = defineStore('permission', {
+ state: () => ({
+ whiteListRouters: ['/login'],
+ routers: [],
+ removeRoutes: [],
+ }),
+ actions: {
+ async initRoutes(roles: Array) {
+ let accessedRouters = [];
+
+ let removeRoutes: Array = [];
+ // special token
+ if (roles.includes('all')) {
+ accessedRouters = allRoutes;
+ } else {
+ const res = filterPermissionsRouters(allRoutes, roles);
+ accessedRouters = res.accessedRouters;
+ removeRoutes = res.removeRoutes;
+ }
+
+ this.routers = accessedRouters;
+ this.removeRoutes = removeRoutes;
+
+ removeRoutes.forEach((item: RouteRecordRaw) => {
+ if (router.hasRoute(item.name)) {
+ router.removeRoute(item.name);
+ }
+ });
+ },
+ async restore() {
+ this.removeRoutes.forEach((item: RouteRecordRaw) => {
+ router.addRoute(item);
+ });
+ },
+ },
+});
+
+export function getPermissionStore() {
+ return usePermissionStore(store);
+}
+
+// import { defineStore } from 'pinia';
+// import { RouteRecordRaw } from 'vue-router';
+
+// import { RouteItem } from '@/api/model/permissionModel';
+// import { getMenuList } from '@/api/permission';
+// import router, { fixedRouterList, homepageRouterList } from '@/router';
+// import { store } from '@/store';
+// import { transformObjectToRoute } from '@/utils/route';
+
+// export const usePermissionStore = defineStore('permission', {
+// state: () => ({
+// whiteListRouters: ['/login'],
+// routers: [],
+// removeRoutes: [],
+// asyncRoutes: [],
+// }),
+// actions: {
+// async initRoutes() {
+// const accessedRouters = this.asyncRoutes;
+
+// // 在菜单展示全部路由
+// // this.routers = [...homepageRouterList, ...accessedRouters, ...fixedRouterList];
+// // 在菜单只展示动态路由和首页
+// this.routers = [...homepageRouterList, ...accessedRouters];
+// // 在菜单只展示动态路由
+// // this.routers = [...accessedRouters];
+// },
+// async buildAsyncRoutes() {
+// try {
+// // 发起菜单权限请求 获取菜单列表
+// const asyncRoutes: Array = (await getMenuList()).list;
+// this.asyncRoutes = transformObjectToRoute(asyncRoutes);
+// await this.initRoutes();
+// return this.asyncRoutes;
+// } catch (error) {
+// throw new Error("Can't build routes");
+// }
+// },
+// async restoreRoutes() {
+// // 不需要在此额外调用initRoutes更新侧边导肮内容,在登录后asyncRoutes为空会调用
+// this.asyncRoutes.forEach((item: RouteRecordRaw) => {
+// if (item.name) {
+// router.removeRoute(item.name);
+// }
+// });
+// this.asyncRoutes = [];
+// },
+// },
+// });
+
+// export function getPermissionStore() {
+// return usePermissionStore(store);
+// }
diff --git a/src/store/modules/setting.ts b/src/store/modules/setting.ts
new file mode 100644
index 0000000..346d4cb
--- /dev/null
+++ b/src/store/modules/setting.ts
@@ -0,0 +1,95 @@
+import keys from 'lodash/keys';
+import { defineStore } from 'pinia';
+import { Color } from 'tvision-color';
+
+import { DARK_CHART_COLORS, LIGHT_CHART_COLORS } from '@/config/color';
+import STYLE_CONFIG from '@/config/style';
+import { store } from '@/store';
+import { generateColorMap, insertThemeStylesheet } from '@/utils/color';
+
+const state = {
+ ...STYLE_CONFIG,
+ showSettingPanel: false,
+ colorList: {},
+ chartColors: LIGHT_CHART_COLORS,
+};
+
+export type TState = typeof state;
+export type TStateKey = keyof typeof state;
+
+export const useSettingStore = defineStore('setting', {
+ state: () => state,
+ getters: {
+ showSidebar: (state) => state.layout !== 'top',
+ showSidebarLogo: (state) => state.layout === 'side',
+ showHeaderLogo: (state) => state.layout !== 'side',
+ displayMode: (state): 'dark' | 'light' => {
+ if (state.mode === 'auto') {
+ const media = window.matchMedia('(prefers-color-scheme:dark)');
+ if (media.matches) {
+ return 'dark';
+ }
+ return 'light';
+ }
+ return state.mode as 'dark' | 'light';
+ },
+ },
+ actions: {
+ async changeMode(mode: 'dark' | 'light' | 'auto') {
+ let theme = mode;
+
+ if (mode === 'auto') {
+ const media = window.matchMedia('(prefers-color-scheme:dark)');
+ if (media.matches) {
+ theme = 'dark';
+ } else {
+ theme = 'light';
+ }
+ }
+ const isDarkMode = theme === 'dark';
+
+ document.documentElement.setAttribute('theme-mode', isDarkMode ? 'dark' : '');
+
+ this.chartColors = isDarkMode ? DARK_CHART_COLORS : LIGHT_CHART_COLORS;
+ },
+ changeBrandTheme(brandTheme: string) {
+ const mode = this.displayMode;
+ // 以主题色加显示模式作为键
+ const colorKey = `${brandTheme}[${mode}]`;
+ let colorMap = this.colorList[colorKey];
+ // 如果不存在色阶,就需要计算
+ if (colorMap === undefined) {
+ const [{ colors: newPalette, primary: brandColorIndex }] = Color.getColorGradations({
+ colors: [brandTheme],
+ step: 10,
+ remainInput: false, // 是否保留输入 不保留会矫正不合适的主题色
+ });
+ colorMap = generateColorMap(brandTheme, newPalette, mode as 'light' | 'dark', brandColorIndex);
+ this.colorList[colorKey] = colorMap;
+ }
+ // TODO 需要解决不停切换时有反复插入 style 的问题
+ insertThemeStylesheet(brandTheme, colorMap, mode as 'light' | 'dark');
+ document.documentElement.setAttribute('theme-color', brandTheme);
+ },
+ updateConfig(payload: Partial) {
+ for (const key in payload) {
+ if (payload[key as TStateKey] !== undefined) {
+ this[key] = payload[key as TStateKey];
+ }
+ if (key === 'mode') {
+ this.changeMode(payload[key]);
+ }
+ if (key === 'brandTheme') {
+ this.changeBrandTheme(payload[key]);
+ }
+ }
+ },
+ },
+ persist: {
+ paths: [...keys(STYLE_CONFIG), 'colorList', 'chartColors'],
+ },
+});
+
+export function getSettingStore() {
+ return useSettingStore(store);
+}
diff --git a/src/store/modules/tabs-router.ts b/src/store/modules/tabs-router.ts
new file mode 100644
index 0000000..00f7f51
--- /dev/null
+++ b/src/store/modules/tabs-router.ts
@@ -0,0 +1,89 @@
+import { defineStore } from 'pinia';
+
+import { store } from '@/store';
+import type { TRouterInfo, TTabRouterType } from '@/types/interface';
+
+const homeRoute: Array = [
+ {
+ path: '/dashboard/base',
+ routeIdx: 0,
+ title: '仪表盘',
+ name: 'DashboardBase',
+ isHome: true,
+ },
+];
+
+const state = {
+ tabRouterList: homeRoute,
+ isRefreshing: false,
+};
+
+// 不需要做多标签tabs页缓存的列表 值为每个页面对应的name 如 DashboardDetail
+// const ignoreCacheRoutes = ['DashboardDetail'];
+const ignoreCacheRoutes = ['login'];
+
+export const useTabsRouterStore = defineStore('tabsRouter', {
+ state: () => state,
+ getters: {
+ tabRouters: (state: TTabRouterType) => state.tabRouterList,
+ refreshing: (state: TTabRouterType) => state.isRefreshing,
+ },
+ actions: {
+ // 处理刷新
+ toggleTabRouterAlive(routeIdx: number) {
+ this.isRefreshing = !this.isRefreshing;
+ this.tabRouters[routeIdx].isAlive = !this.tabRouters[routeIdx].isAlive;
+ },
+ // 处理新增
+ appendTabRouterList(newRoute: TRouterInfo) {
+ // 不要将判断条件newRoute.meta.keepAlive !== false修改为newRoute.meta.keepAlive,starter默认开启保活,所以meta.keepAlive未定义时也需要进行保活,只有显式说明false才禁用保活。
+ const needAlive = !ignoreCacheRoutes.includes(newRoute.name as string) && newRoute.meta?.keepAlive !== false;
+ if (!this.tabRouters.find((route: TRouterInfo) => route.path === newRoute.path)) {
+ // eslint-disable-next-line no-param-reassign
+ this.tabRouterList = this.tabRouterList.concat({ ...newRoute, isAlive: needAlive });
+ }
+ },
+ // 处理关闭当前
+ subtractCurrentTabRouter(newRoute: TRouterInfo) {
+ const { routeIdx } = newRoute;
+ this.tabRouterList = this.tabRouterList.slice(0, routeIdx).concat(this.tabRouterList.slice(routeIdx + 1));
+ },
+ // 处理关闭右侧
+ subtractTabRouterBehind(newRoute: TRouterInfo) {
+ const { routeIdx } = newRoute;
+ const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
+ let tabRouterList: Array = this.tabRouterList.slice(0, routeIdx + 1);
+ if (routeIdx < homeIdx) {
+ tabRouterList = tabRouterList.concat(homeRoute);
+ }
+ this.tabRouterList = tabRouterList;
+ },
+ // 处理关闭左侧
+ subtractTabRouterAhead(newRoute: TRouterInfo) {
+ const { routeIdx } = newRoute;
+ const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
+ let tabRouterList: Array = this.tabRouterList.slice(routeIdx);
+ if (routeIdx > homeIdx) {
+ tabRouterList = homeRoute.concat(tabRouterList);
+ }
+ this.tabRouterList = tabRouterList;
+ },
+ // 处理关闭其他
+ subtractTabRouterOther(newRoute: TRouterInfo) {
+ const { routeIdx } = newRoute;
+ const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
+ this.tabRouterList = routeIdx === homeIdx ? homeRoute : homeRoute.concat([this.tabRouterList?.[routeIdx]]);
+ },
+ removeTabRouterList() {
+ this.tabRouterList = [];
+ },
+ initTabRouterList(newRoutes: TRouterInfo[]) {
+ newRoutes?.forEach((route: TRouterInfo) => this.appendTabRouterList(route));
+ },
+ },
+ persist: true,
+});
+
+export function getTabsRouterStore() {
+ return useTabsRouterStore(store);
+}
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
new file mode 100644
index 0000000..5712298
--- /dev/null
+++ b/src/store/modules/user.ts
@@ -0,0 +1,87 @@
+import { defineStore } from 'pinia';
+
+import { usePermissionStore } from '@/store';
+import type { UserInfo } from '@/types/interface';
+
+const InitUserInfo: UserInfo = {
+ name: '', // 用户名,用于展示在页面右上角头像处
+ roles: [], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
+};
+
+export const useUserStore = defineStore('user', {
+ state: () => ({
+ token: 'main_token', // 默认token不走权限
+ userInfo: { ...InitUserInfo },
+ }),
+ getters: {
+ roles: (state) => {
+ return state.userInfo?.roles;
+ },
+ },
+ actions: {
+ async login(userInfo: Record) {
+ const mockLogin = async (userInfo: Record) => {
+ // 登录请求流程
+ console.log(`用户信息:`, userInfo);
+ // const { account, password } = userInfo;
+ // if (account !== 'td') {
+ // return {
+ // code: 401,
+ // message: '账号不存在',
+ // };
+ // }
+ // if (['main_', 'dev_'].indexOf(password) === -1) {
+ // return {
+ // code: 401,
+ // message: '密码错误',
+ // };
+ // }
+ // const token = {
+ // main_: 'main_token',
+ // dev_: 'dev_token',
+ // }[password];
+ return {
+ code: 200,
+ message: '登录成功',
+ data: 'main_token',
+ };
+ };
+
+ const res = await mockLogin(userInfo);
+ if (res.code === 200) {
+ this.token = res.data;
+ } else {
+ throw res;
+ }
+ },
+ async getUserInfo() {
+ const mockRemoteUserInfo = async (token: string) => {
+ if (token === 'main_token') {
+ return {
+ name: 'Tencent',
+ roles: ['all'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
+ };
+ }
+ return {
+ name: 'td_dev',
+ roles: ['UserIndex', 'DashboardBase', 'login'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
+ };
+ };
+ const res = await mockRemoteUserInfo(this.token);
+
+ this.userInfo = res;
+ },
+ async logout() {
+ this.token = '';
+ this.userInfo = { ...InitUserInfo };
+ },
+ },
+ persist: {
+ afterRestore: () => {
+ const permissionStore = usePermissionStore();
+ permissionStore.initRoutes(['all']);
+ },
+ key: 'user',
+ paths: ['token'],
+ },
+});
diff --git a/src/style/font-family.less b/src/style/font-family.less
new file mode 100644
index 0000000..67a31b4
--- /dev/null
+++ b/src/style/font-family.less
@@ -0,0 +1,7 @@
+@font-face {
+ font-family: 'TencentSansW7';
+ src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAusAA4AAAAAEJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAALkAAAABwAAAAchqPqzUdERUYAAAtwAAAAHgAAAB4AKQAbT1MvMgAAAbgAAABZAAAAYGmceoNjbWFwAAACYAAAAJcAAAHsPmfPZmdhc3AAAAtkAAAADAAAAAwACAAbZ2x5ZgAAAywAAAW8AAAG/Ivn/ztoZWFkAAABRAAAADYAAAA2E+AL5GhoZWEAAAF8AAAAIAAAACQIawJ9aG10eAAAAhQAAABMAAAATCG/Auxsb2NhAAADAAAAACwAAAAsDjIQIm1heHAAAAGcAAAAGgAAACAAfgBDbmFtZQAACOgAAAIUAAAEm0zGvtJwb3N0AAAK/AAAAGYAAAB/4wuGdnByZXAAAAL4AAAACAAAAAhwAgESAAEAAAABBR/xlpGAXw889QALA+gAAAAA2Ac3gwAAAADY+IxB//L/HAPPAwAAAAAIAAIAAAAAAAB42mNgZGBgWf7vFAMD84v/n/7vZD7PABRBAYIAwxQH7XjaY2BkYGAQZXBiYGEAAUYGGEiBUAAMEQDCAAB42mNgYepm2sPAysDA1MUUwcDA4A2hGeMYjBjNgKI8HMxMTCz8TCwLGJj2CzCAgRiI8PX382d0YGBMEmQ2+u/FcIJlOVA9CwMjSI6JlekwkFJgYAQAR1kL+QAAAAJYAHYAAAAAAU0AAAEEAAACUAAhAlYAFQJUACACKgAdAZUANgEUABUBYAAkA5wAFQINABsBqAA0AnAAKgJYACoD6ACF//YANP/yACN42mNgYGBmgGAZBkYGEHgG5DGC+SwMp4C0HIMAUISPQYEhiSGNIZMhl6GUoZJhgeIkfS6/N4GpQQuSBP//B+tMZEgByucwFGOT/7/4/6L/C/7P+z/z/7T/yffqLrJvVFu3Zm3xPJBtcgz4ADPFkIGRDWgMIcBAIWBhZWBj52Bg4GRg4OIGi/Dw8gFJfgYqA/JcCgA99Se8ALgB/4W4AAGNAAAAFAAUABQAFABSAIIAsgD6ASIBOAFYAYIBxgHwAhQCRAJaAogCygMYA3542k1Ua2xTZRj+LmtP23Vdz2lPz3pZb2dr1+u2nq2H0d3Z2OhI5mC4AZMBo0gM98E0oRn1AqgoIYDG4BAkakDkJ4iyiCZGAiISUH8YjIQfEhNUMCoJrme+bTfkx/nxveec53ne533eDxE0Nn0V/0V2I4oYhATWw1ZKrDiGx5Vfzp6NkXPZ7mH8ECGCPNNXiZWokANVIYRjFt7MUI83iuvrWnAzTWA5Xl/nC2G/SZJFr7oUq3mzBaf7F5S0Kt+F59i1aq0j0tbwJmXcwUtsz3HHhEtQaYr0RFUbL0sqB8yRClvcG2uwa7hKg4VLKdFjZmeEN5SwwM0Dd4DcRaXAXuAuxQYqekwSIxRoqRRrxjLevrK/RNCUhXt7lYevpQMf6StjW1sz/gCrIqqm+Z7krsjCmJU67/zexrvMykE+CniA7wb8oUf4EhW9vtm2qGQSvQY808+t9Ov/5ih0zlhfyytfbGodd/ht2rJiH7k7dTtP0NQTYpUo3qDzSnsSVpc5j18O+DzRIzeqyel34nwDOISZHGoCM6I3SvKMRJppKM8pxeIy3tbU4S4y6Oc231Sp3OFLgXMV7dV2xq8L9K9IUZvY5XAZi4wfms2U6K21PoHvnVfvchitXEorhH2O4K253RUTnIYt0Xv4ITanB6M4zPwm9GuBQ4GIEaloEmVJzvXM4JeqXaxWfYg7tCi9qIddqNZZg53yKEv2lLgkEWaDr6fERHfEnEL5/mA+uJzcLmTIJEsMNUl0t5AR7o+kR8jqTCa7nsjZS3nuCvg2DtwcpC3ftQzfgg1ODMRfBjt8eiP8ZLAE7HNDsRJPUKhYTmqy13osvJGm4H/gomsgh8Ksk2oGpM+kAHqAh0wmuitpSWO6SWer2NOX7vu8D2SouFCACSSVk3hgjmBllRp8Takhck6THTT9DZjVoMkPprfgmSE8igHzKNOJ//MX4X4rN6m0VF022lan02g4R11DNngDahqoPQe1YoZ31SRiKWN5fammqNgddMiRcNheAoXIbKGlKsoXo7w3daDjFDmNbOAiM+uJOONSbjoSIzEPI1owpyO0oL6twlAcSbI9zrTzKIePTdeaeZamOHe4zJtA06kTJ3J+BSF7vdCbKZ/swsbGZzyHZvCN9BrlzmN+vUVU2UsFq/CAcjJnVV5bcNpB2gCnPI9jILDquZ1rwflleQzwqx1ri8PJzh+4K3GnUcPsg32pSdfAJmYA+dPm+a61rBj0N3Z5ktio3Gu1uAqZ3IVW0R2Ar4GDwIiyp97jJ5MXLuzbn71IGvYT1fXrR545kvdp+p/pNB0nvyI7QiqR+A2UEZupLPmjOK/KIlg4On741U6xf3hj49Ha0e2bov3tVFd6oOx2uVvb9dlLeOTjxt1797XUvDBx+snTOv1Sk/2y0g7YRTCDNP2WIqSHhLqQCHQGnEuYmpHhhvFYBNHnV8UsUPJLpjjwYZNA8ZC+yv7Uxu5QiG/vMWOfNzzq9uH7XqFoyz3byRWUv1ClnNowsoqi+CYp248fhN0TC94YXrSkYyRod03dWkYOmpdOfUAetGWTiE7/CRomQYML+VG0kHOG5AjjpjjE0kAYMc7JubMsqZnK3GvqMQmNOHcR4u9t5Woai31t3xXxYVrcaZQXt+5cOWZZV7ZXp94Io6u1C5Q+rZzY+pP62YEdZEmpx6QcUb4ZnMJ2nXbs2uWtb+P57w6sSoYytQYnl62juhVEnT1e2HURtF0EbQnUBsrAD9AADglwhfgLHkWxX2TiMqPOeyQzftinXHjhbVwuJVIdiOY9fvBRstDzyh9Ys6B6W2h0HnvO85DbvMsa2xJhNPgiPzBkaN4W/NF+0PaOd/ug7Yz+DNfVp3/v5+Jx8yRdT3F5dLMorLXik6PrnhhYfKAxM/hyb0Oanycm3+86bHMPC6JyZfB8YJnN8sngi4xqqdq3nN0//vzOTNXq5YsR+g8984WfeNq1Us1qFEEQ/npnk0X8IQGJIjnUSRLYLLt7MMlFCHvNKRvMuTPTmZ1kdib0zAY3ePMFfAAvigi5+Ry+gA8iiOLFr3tbTFZWcnGgu76qrvqqpqoAPMQ3KMy+A3wMWOGBehRwAy31LOAIayoPuEmfdwEv4Z76HPAy7quvAbfwqvEz4BXcjd4EvIpG9J5sqnmH2gfP7LDCOn4E3CD/04AjtNXzgJtYV68DXsJj9SngZTxRXwJu4XtDBbyCtehlwKtoRm8xQIlzTGGRIcUINQRXPH100cMOtgLape0QBgVif9dBjxlTkCPnSckj2MCQNudzGeQmrS5PB22ifcYmxII9RuWUf3JXXjOUhvKCt/PEoDyf2iwd1XIl/W5vZ4vXrhyaIjZFTRmPijIv06lsDEemuOTZlEHZact+nXRkL8/FR1diTWXshUnIefNnhtCULv0Rtvk4ox7qopKjbbcNhhVOWK1mXTgw6STX9t8kMh91k1RuRfJXJS98Zyp2rKSbcDIdzqfPB2OrrCyk1+n2F3HOMzrC+aFmPrcOg0i9XvukbhhCbPmaUBv73zqjrcTJf1gPV7PL6PK4yGN6L6oq882IvaWm/0w/ZfOt9014x3yZta1yS/V7fbJKNBcjzaraWJNIbXVixtqeSXly6x3TRSJjPZVjc50qKyQ2ttaUpxObVUkW15xRtXD9rg8Hs3FxRr8ATJnl93jaY2BiAIP/zQxGDNiAKBAzMjAxMjG4MLgyuDN4MHgy+DD4MwQwhDGEM0QwxDAyM7IwsjKyMbKzl+ZlGhgYGHIlFhXllxdlpmeUgISM3AwcQbSJq6sziDY1cjQA0WZGhoYAgBwU3AAAAAEAAgAIAAr//wAPAAEAAAAMAAAAFgAAAAIAAQADABQAAQAEAAAAAgAAAAAAAAABAAAAANWkJwgAAAAA2Ac3gwAAAADY+IxB')
+ format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/src/style/index.less b/src/style/index.less
new file mode 100644
index 0000000..dfda36c
--- /dev/null
+++ b/src/style/index.less
@@ -0,0 +1,2 @@
+@import './font-family.less';
+@import './reset.less';
diff --git a/src/style/layout.less b/src/style/layout.less
new file mode 100644
index 0000000..3e8f9bd
--- /dev/null
+++ b/src/style/layout.less
@@ -0,0 +1,215 @@
+@import './font-family.less';
+
+// layout rewrite
+
+.t-layout__sider {
+ width: fit-content;
+}
+
+.t-button + .t-button {
+ margin-left: var(--td-comp-margin-s);
+}
+
+.t-transfer,
+.t-jumper,
+.t-pagination-mini {
+ .t-button + .t-button {
+ margin-left: 0;
+ }
+}
+
+.@{starter-prefix}-link {
+ color: var(--td-brand-color);
+ text-decoration: none;
+ margin-right: 24px;
+ cursor: pointer;
+ transition: color 0.2s cubic-bezier(0.38, 0, 0.24, 1);
+}
+
+.left-operation-container,
+.operation-container {
+ .t-button + .t-button {
+ margin-left: var(--td-comp-margin-s);
+ }
+}
+
+.t-layout.t-layout--with-sider {
+ > .t-layout {
+ flex: 1;
+ min-width: 760px;
+ }
+}
+
+.t-menu--dark .t-menu__operations .t-icon {
+ color: rgb(255 255 255 / 55%);
+
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.t-default-menu.t-menu--dark {
+ background: var(--td-gray-color-13);
+}
+
+.@{starter-prefix} {
+ // 布局元素调整
+ &-wrapper {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &-main-wrapper {
+ height: 500px;
+ overflow: scroll;
+ }
+
+ &-side-nav-layout {
+ &-relative {
+ height: 100%;
+ }
+ }
+
+ &-content-layout {
+ padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl);
+ }
+
+ &-layout {
+ height: calc(100vh - var(--td-comp-size-xxxl));
+ overflow-y: scroll;
+
+ &-tabs-nav {
+ max-width: 100%;
+ position: fixed;
+ overflow: visible;
+ z-index: 100;
+ }
+ &-tabs-nav + .@{starter-prefix}-content-layout {
+ padding-top: var(--td-comp-paddingTB-xxl);
+ }
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: 6px;
+ border: 2px solid transparent;
+ background-clip: content-box;
+ background-color: var(--td-scrollbar-color);
+ }
+ }
+
+ &-footer-layout {
+ padding: 0;
+ margin-bottom: var(--td-comp-margin-xxl);
+ }
+
+ // slideBar
+ &-sidebar-layout {
+ height: 100%;
+ }
+
+ &-sidebar-compact {
+ width: 64px;
+ }
+
+ &-sidebar-layout-side {
+ z-index: 100;
+ }
+
+ &-side-nav {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ z-index: 200;
+ transition: all 0.3s;
+ min-height: 100%;
+
+ &-mix {
+ top: var(--td-comp-size-xxxl);
+
+ &-fixed {
+ top: var(--td-comp-size-xxxl);
+ z-index: 0;
+ }
+ }
+
+ &-no-fixed {
+ position: relative;
+ z-index: 1;
+ }
+
+ &-no-logo {
+ z-index: 1;
+ }
+
+ &-logo-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ &-logo-t-logo {
+ height: var(--td-comp-size-s);
+ width: 100%;
+ }
+
+ &-logo-tdesign-logo {
+ padding: 0 var(--td-comp-paddingLR-xl);
+ height: var(--td-comp-size-s);
+ width: 100%;
+ color: var(--td-text-color-primary);
+ }
+
+ &-logo-normal {
+ color: var(--td-brand-color);
+ font: var(--td-font-body-large);
+ transition: all 0.3s;
+ }
+ }
+
+ &-side-nav-placeholder {
+ flex: 1 1 232px;
+ min-width: 232px;
+ transition: all 0.3s;
+
+ &-hidden {
+ flex: 1 1 72px;
+ min-width: 72px;
+ transition: all 0.3s;
+ }
+ }
+}
+
+.route-tabs-dropdown {
+ .t-icon {
+ margin-right: 8px;
+ }
+}
+
+.logo-container {
+ cursor: pointer;
+ display: inline-flex;
+ margin-left: 24px;
+}
+
+.version-container {
+ color: var(--td-text-color-primary);
+ opacity: 0.4;
+}
+
+.t-menu__popup {
+ z-index: 1000;
+}
+
+.container-base-margin-top {
+ margin-top: 16px;
+}
diff --git a/src/style/reset.less b/src/style/reset.less
new file mode 100644
index 0000000..96cb11f
--- /dev/null
+++ b/src/style/reset.less
@@ -0,0 +1,38 @@
+// 对部分样式进行重置
+body {
+ color: var(--td-text-color-secondary);
+ font: var(--td-font-body-medium);
+ font-family: -apple-system, BlinkMacSystemFont, var(--td-font-family);
+ -webkit-font-smoothing: antialiased;
+ padding: 0;
+ margin: 0;
+}
+
+pre {
+ font-family: var(--td-font-family);
+}
+
+ul,
+dl,
+li,
+dd,
+dt {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+figure,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p {
+ margin: 0;
+}
+
+* {
+ box-sizing: border-box;
+}
diff --git a/src/style/variables.less b/src/style/variables.less
new file mode 100644
index 0000000..d0889f3
--- /dev/null
+++ b/src/style/variables.less
@@ -0,0 +1,26 @@
+/** 公共前缀 */
+@starter-prefix: tdesign-starter;
+
+// 颜色、尺寸、阴影、圆角、字体 variables 请参考 https://tdesign.tencent.com/starter/docs/vue/design-token
+// 响应式断点
+@screen-sm: 768px;
+@screen-md: 992px;
+@screen-lg: 1200px;
+@screen-xl: 1400px;
+
+@screen-sm-min: @screen-sm;
+@screen-md-min: @screen-md;
+@screen-lg-min: @screen-lg;
+@screen-xl-min: @screen-xl;
+
+@screen-sm-max: calc(@screen-md-min - 1px);
+@screen-md-max: calc(@screen-lg-min - 1px);
+@screen-lg-max: calc(@screen-xl-min - 1px);
+
+// 动画
+@anim-time-fn-easing: cubic-bezier(0.38, 0, 0.24, 1);
+@anim-time-fn-ease-out: cubic-bezier(0, 0, 0.15, 1);
+@anim-time-fn-ease-in: cubic-bezier(0.82, 0, 1, 0.9);
+@anim-duration-base: 0.2s;
+@anim-duration-moderate: 0.24s;
+@anim-duration-slow: 0.28s;
diff --git a/src/types/axios.d.ts b/src/types/axios.d.ts
new file mode 100644
index 0000000..1c90329
--- /dev/null
+++ b/src/types/axios.d.ts
@@ -0,0 +1,98 @@
+import type { AxiosRequestConfig } from 'axios';
+
+/**
+ * Axios请求配置
+ */
+export interface RequestOptions {
+ /**
+ * 接口地址
+ *
+ * 例: http://www.baidu.com/api
+ */
+ apiUrl?: string;
+ /**
+ * 是否自动添加接口前缀
+ *
+ * 例: http://www.baidu.com/api
+ * urlPrefix: 'api'
+ */
+ isJoinPrefix?: boolean;
+ /**
+ * 接口前缀
+ */
+ urlPrefix?: string;
+ /**
+ * POST请求的时候添加参数到Url中
+ */
+ joinParamsToUrl?: boolean;
+ /**
+ * 格式化提交参数时间
+ */
+ formatDate?: boolean;
+ /**
+ * 是否需要对响应数据进行处理
+ */
+ isTransformResponse?: boolean;
+ /**
+ * 是否返回原生响应头
+ *
+ * 例: 需要获取响应头时使用该属性
+ */
+ isReturnNativeResponse?: boolean;
+ /**
+ * 是否忽略请求取消令牌
+ *
+ * 如果启用,则重复请求时不进行处理
+ *
+ * 如果禁用,则重复请求时会取消当前请求
+ */
+ ignoreCancelToken?: boolean;
+ /**
+ * 自动对请求添加时间戳参数
+ */
+ joinTime?: boolean;
+ /**
+ * 是否携带Token
+ */
+ withToken?: boolean;
+ /**
+ * 重试配置
+ */
+ retry?: {
+ /**
+ * 重试次数
+ */
+ count: number;
+ /**
+ * 隔多久重试
+ *
+ * 单位: 毫秒
+ */
+ delay: number;
+ };
+ /**
+ * 接口级节流
+ *
+ * 单位: 毫秒
+ */
+ throttle?: {
+ delay: number;
+ };
+ /**
+ * 接口级防抖
+ *
+ * 单位: 毫秒
+ */
+ debounce?: {
+ delay: number;
+ };
+}
+
+export interface Result {
+ code: number;
+ data: T;
+}
+
+export interface AxiosRequestConfigRetry extends AxiosRequestConfig {
+ retryCount?: number;
+}
diff --git a/src/types/env.d.ts b/src/types/env.d.ts
new file mode 100644
index 0000000..6ca1ddb
--- /dev/null
+++ b/src/types/env.d.ts
@@ -0,0 +1,5 @@
+export interface ImportMetaEnv {
+ readonly VITE_IS_REQUEST_PROXY: string;
+ readonly VITE_API_URL: string;
+ readonly VITE_API_URL_PREFIX: string;
+}
diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts
new file mode 100644
index 0000000..49efe88
--- /dev/null
+++ b/src/types/globals.d.ts
@@ -0,0 +1,18 @@
+// 通用声明
+
+// Vue
+declare module '*.vue' {
+ import { DefineComponent } from 'vue';
+
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
+
+declare type ClassName = { [className: string]: any } | ClassName[] | string;
+
+declare module '*.svg' {
+ const CONTENT: string;
+ export default CONTENT;
+}
+
+declare type Recordable = Record;
diff --git a/src/types/interface.d.ts b/src/types/interface.d.ts
new file mode 100644
index 0000000..ef782ff
--- /dev/null
+++ b/src/types/interface.d.ts
@@ -0,0 +1,78 @@
+import type { TabValue } from 'tdesign-vue-next';
+import { LocationQueryRaw, RouteRecordName } from 'vue-router';
+
+import STYLE_CONFIG from '@/config/style';
+
+export interface RouteMeta {
+ title?: string | Record;
+ icon?: string;
+ expanded?: boolean;
+ orderNo?: number;
+ hidden?: boolean;
+ hiddenBreadcrumb?: boolean;
+ single?: boolean;
+ keepAlive?: boolean;
+ frameSrc?: string;
+ frameBlank?: boolean;
+}
+
+export interface MenuRoute {
+ path: string;
+ title?: string | Record;
+ name?: string;
+ icon?:
+ | string
+ | {
+ render: () => void;
+ };
+ redirect?: string;
+ children: MenuRoute[];
+ meta: RouteMeta;
+}
+
+export type ModeType = 'dark' | 'light';
+
+export type SettingType = typeof STYLE_CONFIG;
+
+export type ClassName = { [className: string]: any } | ClassName[] | string;
+
+export type CommonObjType = {
+ [key: string]: string | number;
+};
+
+export interface UserInfo {
+ name: string;
+ roles: string[];
+}
+
+export interface NotificationItem {
+ id: string;
+ content: string;
+ type: string;
+ status: boolean;
+ collected: boolean;
+ date: string;
+ quality: string;
+}
+
+export interface TRouterInfo {
+ path: string;
+ query?: LocationQueryRaw;
+ routeIdx?: number;
+ title?: string;
+ name?: RouteRecordName;
+ isAlive?: boolean;
+ isHome?: boolean;
+ meta?: any;
+}
+
+export interface TTabRouterType {
+ isRefreshing: boolean;
+ tabRouterList: Array;
+}
+
+export interface TTabRemoveOptions {
+ value: TabValue;
+ index: number;
+ e: MouseEvent;
+}
diff --git a/src/utils/charts.ts b/src/utils/charts.ts
new file mode 100644
index 0000000..93afaa8
--- /dev/null
+++ b/src/utils/charts.ts
@@ -0,0 +1,40 @@
+import dayjs from 'dayjs';
+
+/**
+ * 获取表头数据
+ *
+ * @export
+ * @param {string[]} dateTime
+ * @param {number} divideNum
+ * @returns {string[]}
+ */
+export function getDateArray(dateTime: string[] = [], divideNum = 10): string[] {
+ const timeArray: string[] = [];
+ if (dateTime.length > 0) {
+ for (let i = 0; i < divideNum; i++) {
+ const dateAbsTime: number = (new Date(dateTime[1]).getTime() - new Date(dateTime[0]).getTime()) / divideNum;
+ const enhandTime: number = new Date(dateTime[0]).getTime() + dateAbsTime * i;
+ timeArray.push(dayjs(enhandTime).format('YYYY-MM-DD'));
+ }
+ }
+
+ return timeArray;
+}
+
+/**
+ * 获取随机数
+ *
+ * @param {number} [num=100]
+ * @returns
+ *
+ * @memberOf DashboardBase
+ */
+export function getRandomArray(num = 100): number {
+ let resultNum = Number((Math.random() * num).toFixed(0));
+
+ if (resultNum <= 1) {
+ resultNum = 1;
+ }
+
+ return resultNum;
+}
diff --git a/src/utils/color.ts b/src/utils/color.ts
new file mode 100644
index 0000000..eaae69d
--- /dev/null
+++ b/src/utils/color.ts
@@ -0,0 +1,119 @@
+import * as echarts from 'echarts/core';
+import trim from 'lodash/trim';
+import { Color } from 'tvision-color';
+
+import { TColorToken } from '@/config/color';
+
+/**
+ * 依据主题类型获取颜色
+ *
+ * @export
+ * @param {string} theme
+ * @returns {}
+ */
+export function getColorFromTheme(): Array {
+ const theme = trim(getComputedStyle(document.documentElement).getPropertyValue('--td-brand-color'));
+ const themeColorList = Color.getRandomPalette({
+ color: theme,
+ colorGamut: 'bright',
+ number: 8,
+ });
+
+ return themeColorList;
+}
+
+/** 图表颜色 */
+export function getChartListColor(): Array {
+ const res = getColorFromTheme();
+
+ return res;
+}
+
+/**
+ * 更改图表主题颜色
+ *
+ * @export
+ * @param {Array} chartsList
+ * @param {string} theme
+ */
+export function changeChartsTheme(chartsList: echarts.EChartsType[]): void {
+ if (chartsList && chartsList.length) {
+ const chartChangeColor = getChartListColor();
+
+ for (let index = 0; index < chartsList.length; index++) {
+ const elementChart = chartsList[index];
+
+ if (elementChart) {
+ const optionVal = elementChart.getOption();
+
+ // 更改主题颜色
+ optionVal.color = chartChangeColor;
+
+ elementChart.setOption(optionVal, true);
+ }
+ }
+ }
+}
+
+/**
+ * 根据当前主题色、模式等情景 计算最后生成的色阶
+ */
+export function generateColorMap(
+ theme: string,
+ colorPalette: Array,
+ mode: 'light' | 'dark',
+ brandColorIdx: number,
+) {
+ const isDarkMode = mode === 'dark';
+
+ if (isDarkMode) {
+ // eslint-disable-next-line no-use-before-define
+ colorPalette.reverse().map((color) => {
+ const [h, s, l] = Color.colorTransform(color, 'hex', 'hsl');
+ return Color.colorTransform([h, Number(s) - 4, l], 'hsl', 'hex');
+ });
+ brandColorIdx = 5;
+ colorPalette[0] = `${colorPalette[brandColorIdx]}20`;
+ }
+
+ const colorMap = {
+ '--td-brand-color': colorPalette[brandColorIdx], // 主题色
+ '--td-brand-color-1': colorPalette[0], // light
+ '--td-brand-color-2': colorPalette[1], // focus
+ '--td-brand-color-3': colorPalette[2], // disabled
+ '--td-brand-color-4': colorPalette[3],
+ '--td-brand-color-5': colorPalette[4],
+ '--td-brand-color-6': colorPalette[5],
+ '--td-brand-color-7': brandColorIdx > 0 ? colorPalette[brandColorIdx - 1] : theme, // hover
+ '--td-brand-color-8': colorPalette[brandColorIdx], // 主题色
+ '--td-brand-color-9': brandColorIdx > 8 ? theme : colorPalette[brandColorIdx + 1], // click
+ '--td-brand-color-10': colorPalette[9],
+ };
+ return colorMap;
+}
+
+/**
+ * 将生成的样式嵌入头部
+ */
+export function insertThemeStylesheet(theme: string, colorMap: TColorToken, mode: 'light' | 'dark') {
+ const isDarkMode = mode === 'dark';
+ const root = !isDarkMode ? `:root[theme-color='${theme}']` : `:root[theme-color='${theme}'][theme-mode='dark']`;
+
+ const styleSheet = document.createElement('style');
+ styleSheet.type = 'text/css';
+ styleSheet.innerText = `${root}{
+ --td-brand-color: ${colorMap['--td-brand-color']};
+ --td-brand-color-1: ${colorMap['--td-brand-color-1']};
+ --td-brand-color-2: ${colorMap['--td-brand-color-2']};
+ --td-brand-color-3: ${colorMap['--td-brand-color-3']};
+ --td-brand-color-4: ${colorMap['--td-brand-color-4']};
+ --td-brand-color-5: ${colorMap['--td-brand-color-5']};
+ --td-brand-color-6: ${colorMap['--td-brand-color-6']};
+ --td-brand-color-7: ${colorMap['--td-brand-color-7']};
+ --td-brand-color-8: ${colorMap['--td-brand-color-8']};
+ --td-brand-color-9: ${colorMap['--td-brand-color-9']};
+ --td-brand-color-10: ${colorMap['--td-brand-color-10']};
+ }`;
+
+ document.head.appendChild(styleSheet);
+}
diff --git a/src/utils/date.ts b/src/utils/date.ts
new file mode 100644
index 0000000..bbfa47a
--- /dev/null
+++ b/src/utils/date.ts
@@ -0,0 +1,12 @@
+// 获取常用时间
+import dayjs from 'dayjs';
+
+export const LAST_7_DAYS = [
+ dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
+ dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
+];
+
+export const LAST_30_DAYS = [
+ dayjs().subtract(30, 'day').format('YYYY-MM-DD'),
+ dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
+];
diff --git a/src/utils/request/Axios.ts b/src/utils/request/Axios.ts
new file mode 100644
index 0000000..fc52027
--- /dev/null
+++ b/src/utils/request/Axios.ts
@@ -0,0 +1,293 @@
+import axios, {
+ AxiosError,
+ AxiosInstance,
+ AxiosRequestConfig,
+ AxiosRequestHeaders,
+ AxiosResponse,
+ InternalAxiosRequestConfig,
+} from 'axios';
+import cloneDeep from 'lodash/cloneDeep';
+import debounce from 'lodash/debounce';
+import isFunction from 'lodash/isFunction';
+import throttle from 'lodash/throttle';
+import { stringify } from 'qs';
+
+import { ContentTypeEnum } from '@/constants';
+import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
+
+import { AxiosCanceler } from './AxiosCancel';
+import { CreateAxiosOptions } from './AxiosTransform';
+
+/**
+ * Axios 模块
+ */
+export class VAxios {
+ /**
+ * Axios实例句柄
+ * @private
+ */
+ private instance: AxiosInstance;
+
+ /**
+ * Axios配置
+ * @private
+ */
+ private readonly options: CreateAxiosOptions;
+
+ constructor(options: CreateAxiosOptions) {
+ this.options = options;
+ this.instance = axios.create(options);
+ this.setupInterceptors();
+ }
+
+ /**
+ * 创建Axios实例
+ * @param config
+ * @private
+ */
+ private createAxios(config: CreateAxiosOptions): void {
+ this.instance = axios.create(config);
+ }
+
+ /**
+ * 获取数据处理类
+ * @private
+ */
+ private getTransform() {
+ const { transform } = this.options;
+ return transform;
+ }
+
+ /**
+ * 获取Axios实例
+ */
+ getAxios(): AxiosInstance {
+ return this.instance;
+ }
+
+ /**
+ * 配置Axios
+ * @param config
+ */
+ configAxios(config: CreateAxiosOptions) {
+ if (!this.instance) return;
+ this.createAxios(config);
+ }
+
+ /**
+ * 设置公共头部信息
+ * @param headers
+ */
+ setHeader(headers: Record): void {
+ if (!this.instance) return;
+ Object.assign(this.instance.defaults.headers, headers);
+ }
+
+ /**
+ * 设置拦截器
+ * @private
+ */
+ private setupInterceptors() {
+ const transform = this.getTransform();
+ if (!transform) return;
+
+ const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
+ transform;
+ const axiosCanceler = new AxiosCanceler();
+
+ // 请求拦截器
+ this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
+ // 如果忽略取消令牌,则不会取消重复的请求
+ // @ts-ignore
+ const { ignoreCancelToken } = config.requestOptions;
+ const ignoreCancel = ignoreCancelToken ?? this.options.requestOptions?.ignoreCancelToken;
+ if (!ignoreCancel) axiosCanceler.addPending(config);
+
+ if (requestInterceptors && isFunction(requestInterceptors)) {
+ config = requestInterceptors(config, this.options) as InternalAxiosRequestConfig;
+ }
+
+ return config;
+ }, undefined);
+
+ // 请求错误处理
+ if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) {
+ this.instance.interceptors.request.use(undefined, requestInterceptorsCatch);
+ }
+
+ // 响应结果处理
+ this.instance.interceptors.response.use((res: AxiosResponse) => {
+ if (res) axiosCanceler.removePending(res.config);
+ if (responseInterceptors && isFunction(responseInterceptors)) {
+ res = responseInterceptors(res);
+ }
+ return res;
+ }, undefined);
+
+ // 响应错误处理
+ if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
+ this.instance.interceptors.response.use(undefined, (error) => responseInterceptorsCatch(error, this.instance));
+ }
+ }
+
+ /**
+ * 支持 FormData 请求格式
+ * @param config
+ */
+ supportFormData(config: AxiosRequestConfig) {
+ const headers = config.headers || (this.options.headers as AxiosRequestHeaders);
+ const contentType = headers?.['Content-Type'] || headers?.['content-type'];
+
+ if (
+ contentType !== ContentTypeEnum.FormURLEncoded ||
+ !Reflect.has(config, 'data') ||
+ config.method?.toUpperCase() === 'GET'
+ ) {
+ return config;
+ }
+
+ return {
+ ...config,
+ data: stringify(config.data, { arrayFormat: 'brackets' }),
+ };
+ }
+
+ /**
+ * 支持 params 序列化
+ * @param config
+ */
+ supportParamsStringify(config: AxiosRequestConfig) {
+ const headers = config.headers || this.options.headers;
+ const contentType = headers?.['Content-Type'] || headers?.['content-type'];
+
+ if (contentType === ContentTypeEnum.FormURLEncoded || !Reflect.has(config, 'params')) {
+ return config;
+ }
+
+ return {
+ ...config,
+ paramsSerializer: (params: any) => stringify(params, { arrayFormat: 'brackets' }),
+ };
+ }
+
+ get(config: AxiosRequestConfig, options?: RequestOptions): Promise {
+ return this.request({ ...config, method: 'GET' }, options);
+ }
+
+ post(config: AxiosRequestConfig, options?: RequestOptions): Promise {
+ return this.request({ ...config, method: 'POST' }, options);
+ }
+
+ put(config: AxiosRequestConfig, options?: RequestOptions): Promise {
+ return this.request({ ...config, method: 'PUT' }, options);
+ }
+
+ delete(config: AxiosRequestConfig, options?: RequestOptions): Promise {
+ return this.request({ ...config, method: 'DELETE' }, options);
+ }
+
+ patch(config: AxiosRequestConfig, options?: RequestOptions): Promise {
+ return this.request({ ...config, method: 'PATCH' }, options);
+ }
+
+ /**
+ * 上传文件封装
+ * @param key 文件所属的key
+ * @param file 文件
+ * @param config 请求配置
+ * @param options
+ */
+ upload(key: string, file: File, config: AxiosRequestConfig, options?: RequestOptions): Promise {
+ const params: FormData = config.params ?? new FormData();
+ params.append(key, file);
+
+ return this.request(
+ {
+ ...config,
+ method: 'POST',
+ headers: {
+ 'Content-Type': ContentTypeEnum.FormData,
+ },
+ params,
+ },
+ options,
+ );
+ }
+
+ /**
+ * 请求封装
+ * @param config
+ * @param options
+ */
+ request(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise {
+ const { requestOptions } = this.options;
+
+ if (requestOptions.throttle !== undefined && requestOptions.debounce !== undefined) {
+ throw new Error('throttle and debounce cannot be set at the same time');
+ }
+
+ if (requestOptions.throttle && requestOptions.throttle.delay !== 0) {
+ return new Promise((resolve) => {
+ throttle(() => resolve(this.synthesisRequest(config, options)), requestOptions.throttle.delay);
+ });
+ }
+
+ if (requestOptions.debounce && requestOptions.debounce.delay !== 0) {
+ return new Promise((resolve) => {
+ debounce(() => resolve(this.synthesisRequest(config, options)), requestOptions.debounce.delay);
+ });
+ }
+
+ return this.synthesisRequest(config, options);
+ }
+
+ /**
+ * 请求方法
+ * @private
+ */
+ private async synthesisRequest(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise {
+ let conf: CreateAxiosOptions = cloneDeep(config);
+ const transform = this.getTransform();
+
+ const { requestOptions } = this.options;
+
+ const opt: RequestOptions = { ...requestOptions, ...options };
+
+ const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
+ if (beforeRequestHook && isFunction(beforeRequestHook)) {
+ conf = beforeRequestHook(conf, opt);
+ }
+ conf.requestOptions = opt;
+
+ conf = this.supportFormData(conf);
+ // 支持params数组参数格式化,因axios默认的toFormData即为brackets方式,无需配置paramsSerializer为qs,有需要可解除注释,参数参考qs文档
+ // conf = this.supportParamsStringify(conf);
+
+ return new Promise((resolve, reject) => {
+ this.instance
+ .request>(!config.retryCount ? conf : config)
+ .then((res: AxiosResponse) => {
+ if (transformRequestHook && isFunction(transformRequestHook)) {
+ try {
+ const ret = transformRequestHook(res, opt);
+ resolve(ret);
+ } catch (err) {
+ reject(err || new Error('请求错误!'));
+ }
+ return;
+ }
+ resolve(res as unknown as Promise);
+ })
+ .catch((e: Error | AxiosError) => {
+ if (requestCatchHook && isFunction(requestCatchHook)) {
+ reject(requestCatchHook(e, opt));
+ return;
+ }
+ if (axios.isAxiosError(e)) {
+ // 在这里重写Axios的错误信息
+ }
+ reject(e);
+ });
+ });
+ }
+}
diff --git a/src/utils/request/AxiosCancel.ts b/src/utils/request/AxiosCancel.ts
new file mode 100644
index 0000000..010b0da
--- /dev/null
+++ b/src/utils/request/AxiosCancel.ts
@@ -0,0 +1,67 @@
+import type { AxiosRequestConfig, Canceler } from 'axios';
+import axios from 'axios';
+import isFunction from 'lodash/isFunction';
+
+// 存储请求与取消令牌的键值对列表
+let pendingMap = new Map();
+
+/**
+ * 获取请求Url
+ * @param config
+ */
+export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
+
+/**
+ * @description 请求管理器
+ */
+export class AxiosCanceler {
+ /**
+ * 添加请求到列表中
+ * @param config
+ */
+ addPending(config: AxiosRequestConfig) {
+ this.removePending(config);
+ const url = getPendingUrl(config);
+ config.cancelToken =
+ config.cancelToken ||
+ new axios.CancelToken((cancel) => {
+ if (!pendingMap.has(url)) {
+ // 如果当前没有相同请求就添加
+ pendingMap.set(url, cancel);
+ }
+ });
+ }
+
+ /**
+ * 移除现有的所有请求
+ */
+ removeAllPending() {
+ pendingMap.forEach((cancel) => {
+ if (cancel && isFunction(cancel)) cancel();
+ });
+ pendingMap.clear();
+ }
+
+ /**
+ * 移除指定请求
+ * @param config
+ */
+ removePending(config: AxiosRequestConfig) {
+ const url = getPendingUrl(config);
+
+ if (pendingMap.has(url)) {
+ // If there is a current request identifier in pending,
+ // the current request needs to be cancelled and removed
+ const cancel = pendingMap.get(url);
+ if (cancel) cancel(url);
+ pendingMap.delete(url);
+ }
+ }
+
+ /**
+ * 重置
+ */
+ reset() {
+ pendingMap = new Map();
+ }
+}
diff --git a/src/utils/request/AxiosTransform.ts b/src/utils/request/AxiosTransform.ts
new file mode 100644
index 0000000..d05c64a
--- /dev/null
+++ b/src/utils/request/AxiosTransform.ts
@@ -0,0 +1,64 @@
+import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
+import { AxiosError } from 'axios';
+
+import type { RequestOptions, Result } from '@/types/axios';
+
+/**
+ * @description 创建Axios实例配置
+ */
+export interface CreateAxiosOptions extends AxiosRequestConfig {
+ /**
+ * 请求验证方案
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
+ */
+ authenticationScheme?: string;
+ /**
+ * 请求数据处理
+ */
+ transform?: AxiosTransform;
+ /**
+ * 请求配置
+ */
+ requestOptions?: RequestOptions;
+}
+
+/**
+ * Axios请求数据处理 抽象类
+ */
+export abstract class AxiosTransform {
+ /**
+ * 请求前钩子
+ */
+ beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
+
+ /**
+ * 数据处理前钩子
+ */
+ transformRequestHook?: (res: AxiosResponse, options: RequestOptions) => T;
+
+ /**
+ * 请求失败钩子
+ */
+ requestCatchHook?: (e: Error | AxiosError, options: RequestOptions) => Promise;
+
+ /**
+ * 请求拦截器
+ */
+ requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
+
+ /**
+ * 响应拦截器
+ */
+ responseInterceptors?: (res: AxiosResponse) => AxiosResponse;
+
+ /**
+ * 请求拦截器错误处理
+ */
+ requestInterceptorsCatch?: (error: AxiosError) => void;
+
+ /**
+ * 响应拦截器错误处理
+ */
+ responseInterceptorsCatch?: (error: AxiosError, instance: AxiosInstance) => void;
+}
diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts
new file mode 100644
index 0000000..6dd0173
--- /dev/null
+++ b/src/utils/request/index.ts
@@ -0,0 +1,206 @@
+// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
+import type { AxiosInstance } from 'axios';
+import isString from 'lodash/isString';
+import merge from 'lodash/merge';
+
+import { ContentTypeEnum } from '@/constants';
+import { useUserStore } from '@/store';
+
+import { VAxios } from './Axios';
+import type { AxiosTransform, CreateAxiosOptions } from './AxiosTransform';
+import { formatRequestDate, joinTimestamp, setObjToUrlParams } from './utils';
+
+const env = import.meta.env.MODE || 'development';
+
+// 如果是mock模式 或 没启用直连代理 就不配置host 会走本地Mock拦截 或 Vite 代理
+const host = env === 'mock' || import.meta.env.VITE_IS_REQUEST_PROXY !== 'true' ? '' : import.meta.env.VITE_API_URL;
+
+// 数据处理,方便区分多种处理方式
+const transform: AxiosTransform = {
+ // 处理请求数据。如果数据不是预期格式,可直接抛出错误
+ transformRequestHook: (res, options) => {
+ const { isTransformResponse, isReturnNativeResponse } = options;
+
+ // 如果204无内容直接返回
+ const method = res.config.method?.toLowerCase();
+ if (res.status === 204 && ['put', 'patch', 'delete'].includes(method)) {
+ return res;
+ }
+
+ // 是否返回原生响应头 比如:需要获取响应头时使用该属性
+ if (isReturnNativeResponse) {
+ return res;
+ }
+ // 不进行任何处理,直接返回
+ // 用于页面代码可能需要直接获取code,data,message这些信息时开启
+ if (!isTransformResponse) {
+ return res.data;
+ }
+
+ // 错误的时候返回
+ const { data } = res;
+ if (!data) {
+ throw new Error('请求接口错误');
+ }
+
+ // 这里 code为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
+ const { code } = data;
+
+ // 这里逻辑可以根据项目进行修改
+ const hasSuccess = data && code === 0;
+ if (hasSuccess) {
+ return data.data;
+ }
+
+ throw new Error(`请求接口错误, 错误码: ${code}`);
+ },
+
+ // 请求前处理配置
+ beforeRequestHook: (config, options) => {
+ const { apiUrl, isJoinPrefix, urlPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;
+
+ // 添加接口前缀
+ if (isJoinPrefix && urlPrefix && isString(urlPrefix)) {
+ config.url = `${urlPrefix}${config.url}`;
+ }
+
+ // 将baseUrl拼接
+ if (apiUrl && isString(apiUrl)) {
+ config.url = `${apiUrl}${config.url}`;
+ }
+ const params = config.params || {};
+ const data = config.data || false;
+
+ if (formatDate && data && !isString(data)) {
+ formatRequestDate(data);
+ }
+ if (config.method?.toUpperCase() === 'GET') {
+ if (!isString(params)) {
+ // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
+ config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
+ } else {
+ // 兼容restful风格
+ config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`;
+ config.params = undefined;
+ }
+ } else if (!isString(params)) {
+ if (formatDate) {
+ formatRequestDate(params);
+ }
+ if (
+ Reflect.has(config, 'data') &&
+ config.data &&
+ (Object.keys(config.data).length > 0 || data instanceof FormData)
+ ) {
+ config.data = data;
+ config.params = params;
+ } else {
+ // 非GET请求如果没有提供data,则将params视为data
+ config.data = params;
+ config.params = undefined;
+ }
+ if (joinParamsToUrl) {
+ config.url = setObjToUrlParams(config.url as string, { ...config.params, ...config.data });
+ }
+ } else {
+ // 兼容restful风格
+ config.url += params;
+ config.params = undefined;
+ }
+ return config;
+ },
+
+ // 请求拦截器处理
+ requestInterceptors: (config, options) => {
+ // 请求之前处理config
+ const userStore = useUserStore();
+ const { token } = userStore;
+
+ if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
+ // jwt token
+ (config as Recordable).headers.Authorization = options.authenticationScheme
+ ? `${options.authenticationScheme} ${token}`
+ : token;
+ }
+ return config;
+ },
+
+ // 响应拦截器处理
+ responseInterceptors: (res) => {
+ return res;
+ },
+
+ // 响应错误处理
+ responseInterceptorsCatch: (error: any, instance: AxiosInstance) => {
+ const { config } = error;
+ if (!config || !config.requestOptions.retry) return Promise.reject(error);
+
+ config.retryCount = config.retryCount || 0;
+
+ if (config.retryCount >= config.requestOptions.retry.count) return Promise.reject(error);
+
+ config.retryCount += 1;
+
+ const backoff = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(config);
+ }, config.requestOptions.retry.delay || 1);
+ });
+ config.headers = { ...config.headers, 'Content-Type': ContentTypeEnum.Json };
+ return backoff.then((config) => instance.request(config));
+ },
+};
+
+function createAxios(opt?: Partial) {
+ return new VAxios(
+ merge(
+ {
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
+ // 例如: authenticationScheme: 'Bearer'
+ authenticationScheme: '',
+ // 超时
+ timeout: 10 * 1000,
+ // 携带Cookie
+ withCredentials: true,
+ // 头信息
+ headers: { 'Content-Type': ContentTypeEnum.Json },
+ // 数据处理方式
+ transform,
+ // 配置项,下面的选项都可以在独立的接口请求中覆盖
+ requestOptions: {
+ // 接口地址
+ apiUrl: host,
+ // 是否自动添加接口前缀
+ isJoinPrefix: true,
+ // 接口前缀
+ // 例如: https://www.baidu.com/api
+ // urlPrefix: '/api'
+ urlPrefix: import.meta.env.VITE_API_URL_PREFIX,
+ // 是否返回原生响应头 比如:需要获取响应头时使用该属性
+ isReturnNativeResponse: false,
+ // 需要对返回数据进行处理
+ isTransformResponse: true,
+ // post请求的时候添加参数到url
+ joinParamsToUrl: false,
+ // 格式化提交参数时间
+ formatDate: true,
+ // 是否加入时间戳
+ joinTime: true,
+ // 是否忽略请求取消令牌
+ // 如果启用,则重复请求时不进行处理
+ // 如果禁用,则重复请求时会取消当前请求
+ ignoreCancelToken: true,
+ // 是否携带token
+ withToken: true,
+ // 重试
+ retry: {
+ count: 3,
+ delay: 1000,
+ },
+ },
+ },
+ opt || {},
+ ),
+ );
+}
+export const request = createAxios();
diff --git a/src/utils/request/utils.ts b/src/utils/request/utils.ts
new file mode 100644
index 0000000..72afea7
--- /dev/null
+++ b/src/utils/request/utils.ts
@@ -0,0 +1,54 @@
+import isObject from 'lodash/isObject';
+import isString from 'lodash/isString';
+
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
+
+export function joinTimestamp(join: boolean, restful: T): T extends true ? string : object;
+
+export function joinTimestamp(join: boolean, restful = false): string | object {
+ if (!join) {
+ return restful ? '' : {};
+ }
+ const now = new Date().getTime();
+ if (restful) {
+ return `?_t=${now}`;
+ }
+ return { _t: now };
+}
+
+// 格式化提交参数时间
+export function formatRequestDate(params: Recordable) {
+ if (Object.prototype.toString.call(params) !== '[object Object]') {
+ return;
+ }
+
+ for (const key in params) {
+ // eslint-disable-next-line no-underscore-dangle
+ if (params[key] && params[key]._isAMomentObject) {
+ params[key] = params[key].format(DATE_TIME_FORMAT);
+ }
+ if (isString(key)) {
+ const value = params[key];
+ if (value) {
+ try {
+ params[key] = isString(value) ? value.trim() : value;
+ } catch (error: any) {
+ throw new Error(error);
+ }
+ }
+ }
+ if (isObject(params[key])) {
+ formatRequestDate(params[key]);
+ }
+ }
+}
+
+// 将对象转为Url参数
+export function setObjToUrlParams(baseUrl: string, obj: { [index: string]: any }): string {
+ let parameters = '';
+ for (const key in obj) {
+ parameters += `${key}=${encodeURIComponent(obj[key])}&`;
+ }
+ parameters = parameters.replace(/&$/, '');
+ return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
+}
diff --git a/src/utils/route/constant.ts b/src/utils/route/constant.ts
new file mode 100644
index 0000000..a3ec01c
--- /dev/null
+++ b/src/utils/route/constant.ts
@@ -0,0 +1,14 @@
+export const LAYOUT = () => import('@/layouts/index.vue');
+export const BLANK_LAYOUT = () => import('@/layouts/blank.vue');
+export const IFRAME = () => import('@/layouts/components/FrameBlank.vue');
+export const EXCEPTION_COMPONENT = () => import('@/pages/result/500/index.vue');
+export const PARENT_LAYOUT = () =>
+ new Promise((resolve) => {
+ resolve({ name: 'ParentLayout' });
+ });
+
+export const PAGE_NOT_FOUND_ROUTE = {
+ path: '/:w+',
+ name: '404Page',
+ redirect: '/result/404',
+};
diff --git a/src/utils/route/index.ts b/src/utils/route/index.ts
new file mode 100644
index 0000000..d00a030
--- /dev/null
+++ b/src/utils/route/index.ts
@@ -0,0 +1,110 @@
+import cloneDeep from 'lodash/cloneDeep';
+import { shallowRef } from 'vue';
+
+import { RouteItem } from '@/api/model/permissionModel';
+import { RouteMeta } from '@/types/interface';
+import {
+ BLANK_LAYOUT,
+ EXCEPTION_COMPONENT,
+ IFRAME,
+ LAYOUT,
+ PAGE_NOT_FOUND_ROUTE,
+ PARENT_LAYOUT,
+} from '@/utils/route/constant';
+
+// vite 3+ support dynamic import from node_modules
+const iconsPath = import.meta.glob('../../../node_modules/tdesign-icons-vue-next/esm/components/*.js');
+
+const LayoutMap = new Map Promise>();
+
+LayoutMap.set('LAYOUT', LAYOUT);
+LayoutMap.set('BLANK', BLANK_LAYOUT);
+LayoutMap.set('IFRAME', IFRAME);
+
+let dynamicViewsModules: Record Promise>;
+
+// 动态从包内引入单个Icon
+async function getMenuIcon(iconName: string): Promise {
+ const RenderIcon = iconsPath[`../../../node_modules/tdesign-icons-vue-next/esm/components/${iconName}.js`];
+
+ const Icon = await RenderIcon();
+ // @ts-ignore
+ return shallowRef(Icon.default);
+}
+
+// 动态引入路由组件
+function asyncImportRoute(routes: RouteItem[] | undefined) {
+ dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../pages/**/*.vue');
+ if (!routes) return;
+
+ routes.forEach(async (item) => {
+ const { component, name } = item;
+ const { children } = item;
+
+ if (component) {
+ const layoutFound = LayoutMap.get(component.toUpperCase());
+ if (layoutFound) {
+ item.component = layoutFound;
+ } else {
+ item.component = dynamicImport(dynamicViewsModules, component);
+ }
+ } else if (name) {
+ item.component = PARENT_LAYOUT();
+ }
+
+ if (item.meta.icon) item.meta.icon = await getMenuIcon(item.meta.icon);
+
+ // eslint-disable-next-line no-unused-expressions
+ children && asyncImportRoute(children);
+ });
+}
+
+function dynamicImport(dynamicViewsModules: Record Promise>, component: string) {
+ const keys = Object.keys(dynamicViewsModules);
+ const matchKeys = keys.filter((key) => {
+ const k = key.replace('../../pages', '');
+ const startFlag = component.startsWith('/');
+ const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
+ const startIndex = startFlag ? 0 : 1;
+ const lastIndex = endFlag ? k.length : k.lastIndexOf('.');
+ return k.substring(startIndex, lastIndex) === component;
+ });
+ if (matchKeys?.length === 1) {
+ const matchKey = matchKeys[0];
+ return dynamicViewsModules[matchKey];
+ }
+ if (matchKeys?.length > 1) {
+ throw new Error(
+ 'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure',
+ );
+ } else {
+ console.warn(`Can't find ${component} in pages folder`);
+ }
+ return EXCEPTION_COMPONENT;
+}
+
+// 将背景对象变成路由对象
+export function transformObjectToRoute(routeList: RouteItem[]): T[] {
+ routeList.forEach(async (route) => {
+ const component = route.component as string;
+
+ if (component) {
+ if (component.toUpperCase() === 'LAYOUT') {
+ route.component = LayoutMap.get(component.toUpperCase());
+ } else {
+ route.children = [cloneDeep(route)];
+ route.component = LAYOUT;
+ route.name = `${route.name}Parent`;
+ route.path = '';
+ route.meta = (route.meta || {}) as RouteMeta;
+ }
+ } else {
+ throw new Error('component is undefined');
+ }
+ // eslint-disable-next-line no-unused-expressions
+ route.children && asyncImportRoute(route.children);
+ if (route.meta.icon) route.meta.icon = await getMenuIcon(route.meta.icon);
+ });
+
+ return [PAGE_NOT_FOUND_ROUTE, ...routeList] as unknown as T[];
+}
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000..8bfdbdb
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,28 @@
+module.exports = {
+ defaultSeverity: 'error',
+ extends: ['stylelint-config-standard'],
+ rules: {
+ 'no-descending-specificity': null,
+ 'import-notation': 'string',
+ 'no-empty-source': null,
+ 'custom-property-pattern': null,
+ 'selector-class-pattern': null,
+ 'selector-pseudo-class-no-unknown': [
+ true,
+ {
+ ignorePseudoClasses: ['deep'],
+ },
+ ],
+ 'media-query-no-invalid': null, // 官方表示此规则应当仅对于原生CSS启用,对于预处理器(Less)不应启用
+ },
+ overrides: [
+ {
+ files: ['**/*.html', '**/*.vue'],
+ customSyntax: 'postcss-html',
+ },
+ {
+ files: ['**/*.less'],
+ customSyntax: 'postcss-less',
+ },
+ ],
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1703a2d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "allowSyntheticDefaultImports": true,
+ "lib": ["esnext", "dom"],
+ "types": ["vite/client"],
+ "noEmit": true,
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "noImplicitAny": true
+ },
+ "include": [
+ "**/*.ts",
+ "src/**/*.d.ts",
+ "src/types/**/*.d.ts",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue",
+ "node_modules/tdesign-vue-next/global.d.ts"
+ ],
+ "compileOnSave": false
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..30737de
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,52 @@
+import path from 'node:path';
+
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import { ConfigEnv, loadEnv, UserConfig } from 'vite';
+import { viteMockServe } from 'vite-plugin-mock';
+import svgLoader from 'vite-svg-loader';
+
+const CWD = process.cwd();
+
+// https://vitejs.dev/config/
+export default ({ mode }: ConfigEnv): UserConfig => {
+ const { VITE_BASE_URL, VITE_API_URL_PREFIX } = loadEnv(mode, CWD);
+ return {
+ base: VITE_BASE_URL,
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+
+ css: {
+ preprocessorOptions: {
+ less: {
+ modifyVars: {
+ hack: `true; @import (reference) "${path.resolve('src/style/variables.less')}";`,
+ },
+ math: 'strict',
+ javascriptEnabled: true,
+ },
+ },
+ },
+
+ plugins: [
+ vue(),
+ vueJsx(),
+ viteMockServe({
+ mockPath: 'mock',
+ enable: true,
+ }),
+ svgLoader(),
+ ],
+
+ server: {
+ port: 3002,
+ host: '0.0.0.0',
+ proxy: {
+ [VITE_API_URL_PREFIX]: 'http://127.0.0.1:3000/',
+ },
+ },
+ };
+};