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 @@ + +

+ +

+

+ + TDesign Logo + +

+ +

+ node compatility + + License + +

+ +简体中文 | [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) + +### 兼容性 + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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 @@ +

+ +

+

+ + TDesign Logo + +

+ +

+ node compatility + + License + +

+ +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 + +| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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 @@ + + + + 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 @@ + + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + + 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 @@ + + + + + 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 @@ + + + + + + + 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 @@ + + + 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 @@ + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + + + 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 @@ + + + + + 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 @@ + + + + + + + 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 @@ + + + + + + + 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 @@ + + + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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/', + }, + }, + }; +};