Repository: manyuemeiquqi/vue-tsx-admin
Branch: master
Commit: 096b266c700b
Files: 167
Total size: 299.6 KB
Directory structure:
gitextract_ge8ngqc5/
├── .commitlintrc.json
├── .eslintrc.cjs
├── .github/
│ └── workflows/
│ └── CICD.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .lintstagedrc.json
├── .prettierrc.json
├── .stylelintrc.json
├── .vscode/
│ └── extensions.json
├── LICENSE
├── README.md
├── README.zh-CN.md
├── config/
│ ├── plugin/
│ │ ├── arcoStyleImport.ts
│ │ ├── compress.ts
│ │ └── visualizer.ts
│ ├── vite.config.base.ts
│ ├── vite.config.dev.ts
│ └── vite.config.prod.ts
├── env.d.ts
├── index.html
├── package.json
├── postcss.config.js
├── src/
│ ├── App.tsx
│ ├── api/
│ │ ├── dashboard/
│ │ │ ├── index.ts
│ │ │ └── type.ts
│ │ ├── form/
│ │ │ ├── index.ts
│ │ │ └── type.ts
│ │ ├── interceptors.ts
│ │ ├── list/
│ │ │ ├── index.ts
│ │ │ └── type.ts
│ │ ├── profile/
│ │ │ ├── index.ts
│ │ │ └── type.ts
│ │ └── user/
│ │ ├── index.ts
│ │ └── type.ts
│ ├── assets/
│ │ └── style/
│ │ ├── generic.scss
│ │ ├── index.scss
│ │ └── resetBrowser.scss
│ ├── components/
│ │ ├── card-layout/
│ │ │ └── index.tsx
│ │ ├── chart-component/
│ │ │ └── index.tsx
│ │ └── layout-component/
│ │ ├── AppSetting.tsx
│ │ ├── AvatarAndOptions.tsx
│ │ ├── BreadcrumbComponent.tsx
│ │ ├── FooterComponent.tsx
│ │ ├── MenuComponent.tsx
│ │ ├── Navbar.tsx
│ │ ├── PageComponent.tsx
│ │ ├── TabBar.tsx
│ │ ├── TabItem.tsx
│ │ ├── index.tsx
│ │ └── style.module.scss
│ ├── hooks/
│ │ ├── appRoute.ts
│ │ ├── auth.ts
│ │ ├── chartOption.ts
│ │ ├── loading.ts
│ │ ├── locale.ts
│ │ └── permission.ts
│ ├── locale/
│ │ ├── en-US/
│ │ │ ├── dashboard.json
│ │ │ ├── exception.json
│ │ │ ├── form.json
│ │ │ ├── global.json
│ │ │ ├── list.json
│ │ │ ├── login.json
│ │ │ ├── profile.json
│ │ │ ├── result.json
│ │ │ ├── settings.json
│ │ │ └── user.json
│ │ ├── index.ts
│ │ └── zh-CN/
│ │ ├── dashboard.json
│ │ ├── exception.json
│ │ ├── form.json
│ │ ├── global.json
│ │ ├── list.json
│ │ ├── login.json
│ │ ├── profile.json
│ │ ├── result.json
│ │ ├── settings.json
│ │ └── user.json
│ ├── main.ts
│ ├── mock/
│ │ ├── index.ts
│ │ ├── modules/
│ │ │ ├── dashboardMock.ts
│ │ │ ├── formMock.ts
│ │ │ ├── listMock.ts
│ │ │ ├── profileMock.ts
│ │ │ └── userMock.ts
│ │ └── setupMock.ts
│ ├── router/
│ │ ├── guard/
│ │ │ ├── index.ts
│ │ │ ├── login.ts
│ │ │ └── permission.ts
│ │ ├── index.ts
│ │ └── routes/
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── dashboard.ts
│ │ ├── exception.ts
│ │ ├── form.ts
│ │ ├── list.ts
│ │ ├── profile.ts
│ │ ├── result.ts
│ │ └── user.ts
│ ├── store/
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── app/
│ │ │ └── index.ts
│ │ ├── tab/
│ │ │ └── index.ts
│ │ └── user/
│ │ └── index.ts
│ ├── types/
│ │ ├── constants.ts
│ │ └── global.ts
│ ├── utils/
│ │ ├── event.ts
│ │ ├── routerListener.ts
│ │ ├── sort.ts
│ │ └── token.ts
│ └── views/
│ ├── dashboard/
│ │ ├── monitor/
│ │ │ ├── ChatPanel.tsx
│ │ │ ├── LiveInformation.tsx
│ │ │ ├── LivePanel.tsx
│ │ │ ├── LiveStatus.tsx
│ │ │ ├── MessageItem.tsx
│ │ │ ├── QuickOperation.tsx
│ │ │ ├── index.tsx
│ │ │ └── style.module.scss
│ │ └── workplace/
│ │ ├── Announcement.tsx
│ │ ├── ContentChart.tsx
│ │ ├── ContentPercentage.tsx
│ │ ├── HelpDocs.tsx
│ │ ├── OverView.tsx
│ │ ├── PopularContents.tsx
│ │ ├── RightTopArea.tsx
│ │ ├── index.tsx
│ │ └── style.module.scss
│ ├── exception/
│ │ ├── 403/
│ │ │ └── index.tsx
│ │ ├── 404/
│ │ │ └── index.tsx
│ │ └── 500/
│ │ └── index.tsx
│ ├── form/
│ │ ├── group/
│ │ │ └── index.tsx
│ │ └── step/
│ │ └── index.tsx
│ ├── list/
│ │ ├── card-list/
│ │ │ ├── AddCard.tsx
│ │ │ ├── QualityInspection.tsx
│ │ │ ├── RulesPreset.tsx
│ │ │ ├── SkeletonCard.tsx
│ │ │ ├── TheService.tsx
│ │ │ ├── index.tsx
│ │ │ └── style.module.scss
│ │ └── search-table/
│ │ ├── TableSearchForm.tsx
│ │ ├── index.tsx
│ │ └── style.module.scss
│ ├── login/
│ │ ├── LoginBanner.tsx
│ │ ├── LoginForm.tsx
│ │ └── index.tsx
│ ├── not-found/
│ │ └── index.tsx
│ ├── profile/
│ │ ├── DataUpdateRecord.tsx
│ │ ├── ProfileItem.tsx
│ │ └── index.tsx
│ ├── redirect/
│ │ └── index.tsx
│ ├── result/
│ │ ├── error/
│ │ │ └── index.tsx
│ │ └── success/
│ │ └── index.tsx
│ └── user/
│ ├── info/
│ │ ├── ActivityItem.tsx
│ │ ├── InSiteNotifications.tsx
│ │ ├── LatestActivities.tsx
│ │ ├── MyProject.tsx
│ │ ├── MyTeam.tsx
│ │ ├── UserInfoHeader.tsx
│ │ └── index.tsx
│ └── setting/
│ ├── BasicInformation.tsx
│ ├── Certification.tsx
│ ├── SecuritySettings.tsx
│ ├── UserPanel.tsx
│ ├── index.tsx
│ └── style.module.scss
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.node.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .commitlintrc.json
================================================
{ "extends": ["@commitlint/config-conventional"] }
================================================
FILE: .eslintrc.cjs
================================================
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 0
}
}
================================================
FILE: .github/workflows/CICD.yml
================================================
name: CI
on:
push:
branches: [master]
schedule:
- cron: '0 2 * * 0-6'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
- uses: pnpm/action-setup@v2 # pnpm is optional but recommended, you can also use npm / yarn
with:
version: 8.5.0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.18.0
cache: pnpm
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install dependencies
run: pnpm install
- name: Build with vue-tsx-admin
run: |
pnpm run lint-style
pnpm run lint
pnpm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3.0.1
with:
path: dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
================================================
FILE: .lintstagedrc.json
================================================
{
"**/*.{ts,tsx,js,jsx,html,css,scss,md,json}": ["prettier --write"]
}
================================================
FILE: .prettierrc.json
================================================
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
================================================
FILE: .stylelintrc.json
================================================
{
"extends": ["stylelint-config-standard-scss", "stylelint-config-css-modules"],
"rules": {
"scss/at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["tailwind"]
}
]
}
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"esbenp.prettier-vscode",
"lokalise.i18n-ally",
"wmaurer.change-case",
"formulahendry.auto-rename-tag",
"mhutchie.git-graph",
"eamodio.gitlens"
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 manyuemeiquqi
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.
================================================
FILE: README.md
================================================
English | [中文](https://github.com/manyuemeiquqi/vue-tsx-admin/blob/master/README.zh-CN.md)
## What is the Vue TSX Admin?

### Overview
[Vue TSX Admin ](https://github.com/manyuemeiquqi/vue-tsx-admin)it is a free open source mid-end and back-end management system module template. The UI is referred to acro design pro + ant design pro. It uses the latest front-end technology stack and uses the Vue3 + TSX mode for development. It provides out-of-the-box mid-end and back-end solutions. It has built-in i18n internationalized solutions, configurable layout, theme color modification, permission verification, and refined typical business models, it can help you quickly build a middle-end and back-end project.
The main development solutions are as follows:
- CSS solution:modules css + tailwind
- Network request: axios
- Authentication scheme: token + jwt
- Fake data: mockjs
- Store manager: pinia
- UI component library: arco desigin vue
- Tool Library: lodash + vue-use
- Internationalization switching solution: vue-i18n
- Packaging solution and locale static server: vite
### Preview address
[access address ](https://manyuemeiquqi.github.io/vue-tsx-admin/)
> login username: admin
>
> password: admin
>
> login username: user
>
> password: user
[code address ](https://github.com/manyuemeiquqi/vue-tsx-admin)
### installation and Use
- project conditions
- Node. js 18 +
- pnpm 8.5.0
- use
```javascript
# clone
git clone https://github.com/manyuemeiquqi/vue-tsx-admin.git
# cd project
cd vue-tsx-admin
# install dependency
pnpm install
# start project
pnpm run dev
```
Browser access: [http://localhost: 5173/vue-tsx-admin/ ](http://localhost:5173/vue-tsx-admin/)
- Publish
```javascript
# build project
pnpm run build
```
- Others
```javascript
# install husky
pnpm run husky
# format code
pnpm run format
# lint and fix code
pnpm run lint
# lint and fix style
pnpm run lint-style
```
### Browser support
- Chrome >=87
- Firefox >=78
- Safari >=14
- Edge >=88
- IE is not supported in Vue3.
### About the performance of Vue JSX
For more information, see [https://github.com/krausest/js-framework-benchmark/pull/1546#issuecomment-1872904990](https://github.com/krausest/js-framework-benchmark/pull/1546#issuecomment-1872904990)
### Demo

### Author
[manyuemeiquqi](https://github.com/manyuemeiquqi/vue-tsx-admin/commits?author=manyuemeiquqi)
### License
[MIT License](https://github.com/manyuemeiquqi/vue-tsx-admin?tab=MIT-1-ov-file)
Finally, if this project helps you, I hope you can help the author [star ](https://github.com/manyuemeiquqi/vue-tsx-admin?tab=readme-ov-file) ⭐ Encourage
if you find the project [bug ](https://github.com/manyuemeiquqi/vue-tsx-admin/issues), welcome to mention [PR ](https://github.com/manyuemeiquqi/vue-tsx-admin/pulls), thank you
================================================
FILE: README.zh-CN.md
================================================
中文 | [English](https://github.com/manyuemeiquqi/vue-tsx-admin/tree/master)
## Vue TSX Admin

### 简介
[Vue TSX Admin](https://github.com/manyuemeiquqi/vue-tsx-admin) 是一个免费开源的中后台管理系统模块,UI 参考 acro design pro + ant design pro,它使用了最新的前端技术栈,完全采用 Vue3 + TSX 的模式进行开发,提供了开箱即用的中后台前端解决方案,内置了 i18n 国际化解决方案,可配置化布局,主题色修改,权限验证,提炼了典型的业务模型,可以帮助你快速搭建起一个中后台前端项目。
主要的开发方案为:
- CSS 方案 modules css + tailwind
- 网络请求 axios
- 鉴权方案 token + jwt
- 模拟数据方案 mockjs
- 全局数据状态管理 pinia
- ui 组件库 arco desigin vue
- 工具库 lodash vue-use
- 国际化切换方案 vue-i18n
- 打包方案+静态服务器 vite
### 预览地址
[访问地址](https://manyuemeiquqi.github.io/vue-tsx-admin/)
> 登录用户名: admin
>
> 密码:admin
>
> 登录用户名:user
>
> 密码: user
[代码地址](https://github.com/manyuemeiquqi/vue-tsx-admin)
### 安装使用
- 项目条件
- Node.js 18+
- pnpm 8.5.0
- 使用
```javascript
# 克隆项目
git clone https://github.com/manyuemeiquqi/vue-tsx-admin.git
# 进入项目目录
cd vue-tsx-admin
# 安装依赖
pnpm install
# 启动服务
pnpm run dev
```
浏览器访问: [http://localhost:5173/vue-tsx-admin/](http://localhost:5173/vue-tsx-admin/) 即可
- 发布
```javascript
pnpm run build
```
- 其他
```javascript
# husky 安装
pnpm run husky
# 格式化
pnpm run format
# 代码 lint + fix
pnpm run lint
pnpm run lint-style
```
### 浏览器支持
- Chrome >=87
- Firefox >=78
- Safari >=14
- Edge >=88
- Vue3 不支持 IE
### 关于 Vue JSX 的性能问题
可参考 [https://github.com/krausest/js-framework-benchmark/pull/1546#issuecomment-1872904990](https://github.com/krausest/js-framework-benchmark/pull/1546#issuecomment-1872904990)
如果你对为什么使用 Vue + TSX 进行中后台开发感兴趣,可参考 [🎉Vue TSX Admin, 中后台管理系统开发的新方向](https://juejin.cn/post/7318446251631804467)
### 演示

### 作者
[manyuemeiquqi](https://github.com/manyuemeiquqi/vue-tsx-admin/commits?author=manyuemeiquqi)
### License
[MIT License](https://github.com/manyuemeiquqi/vue-tsx-admin?tab=MIT-1-ov-file)
最后,如果本项目帮助到你,希望你可以帮作者点个 [star](https://github.com/manyuemeiquqi/vue-tsx-admin?tab=readme-ov-file) ⭐ 表示鼓励
如果你发现项目 [bug](https://github.com/manyuemeiquqi/vue-tsx-admin/issues) ,欢迎提 [PR](https://github.com/manyuemeiquqi/vue-tsx-admin/pulls) , 感谢 🤞
---
================================================
FILE: config/plugin/arcoStyleImport.ts
================================================
/**
* Theme import
* 样式按需引入
* https://github.com/arco-design/arco-plugins/blob/main/packages/plugin-vite-vue/README.md
* https://arco.design/vue/docs/start
*/
import { vitePluginForArco } from '@arco-plugins/vite-vue'
export default function configArcoStyleImportPlugin() {
const arcoResolverPlugin = vitePluginForArco({})
return arcoResolverPlugin
}
================================================
FILE: config/plugin/compress.ts
================================================
/**
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* gzip压缩
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite'
import compressPlugin from 'vite-plugin-compression'
export default function configCompressPlugin(
compress: 'gzip' | 'brotli',
deleteOriginFile = false
): Plugin | Plugin[] {
const plugins: Plugin[] = []
if (compress === 'gzip') {
plugins.push(
compressPlugin({
ext: '.gz',
deleteOriginFile
})
)
}
if (compress === 'brotli') {
plugins.push(
compressPlugin({
ext: '.br',
algorithm: 'brotliCompress',
deleteOriginFile
})
)
}
return plugins
}
================================================
FILE: config/plugin/visualizer.ts
================================================
/**
* Generation packaging analysis
* 生成打包分析
*/
import visualizer from 'rollup-plugin-visualizer'
export default function configVisualizerPlugin() {
return visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
}
================================================
FILE: config/vite.config.base.ts
================================================
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('../src', import.meta.url))
}
},
base: '/vue-tsx-admin/'
})
================================================
FILE: config/vite.config.dev.ts
================================================
import { mergeConfig } from 'vite'
import baseConfig from './vite.config.base'
export default mergeConfig(
{
mode: 'development',
server: {
open: true,
fs: {
strict: true
}
}
},
baseConfig
)
================================================
FILE: config/vite.config.prod.ts
================================================
import { mergeConfig } from 'vite'
import baseConfig from './vite.config.base'
import configCompressPlugin from './plugin/compress'
import configVisualizerPlugin from './plugin/visualizer'
export default mergeConfig(
{
mode: 'production',
plugins: [
configCompressPlugin('gzip')
// configVisualizerPlugin(),
],
build: {
rollupOptions: {
output: {
manualChunks: {
arco: ['@arco-design/web-vue'],
chart: ['echarts', 'vue-echarts'],
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n']
}
}
},
chunkSizeWarningLimit: 2000
},
define: {
'process.env': {
REPORT: true
}
}
},
baseConfig
)
================================================
FILE: env.d.ts
================================================
///
declare module '*.tsx' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '@arco-design/color' {
export { generate, getRgbStr }
}
================================================
FILE: index.html
================================================
%VITE_APP_TITLE%
================================================
FILE: package.json
================================================
{
"name": "vue-tsx-admin",
"version": "0.0.0",
"private": true,
"author": "manyuemeiquqi <940495614@qq.com> https://github.com/manyuemeiquqi",
"description": "A Vue3 + tsx admin template, provide some best practice about back manager project.",
"scripts": {
"dev": "vite --config ./config/vite.config.dev.ts",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build --config ./config/vite.config.prod.ts",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"lint-style": "stylelint \"src/**/*.{css,scss}\" --fix",
"format": "prettier --write src/",
"prepare": "husky install"
},
"dependencies": {
"@arco-design/color": "^0.4.0",
"@arco-design/web-vue": "^2.53.1",
"@ckpack/vue-color": "^1.5.0",
"@vueuse/core": "^10.6.1",
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"query-string": "^8.1.0",
"sortablejs": "^1.15.1",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.6.5",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@arco-plugins/vite-vue": "^1.4.5",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.2",
"@types/lodash": "^4.14.201",
"@types/mockjs": "^1.0.10",
"@types/node": "^18.18.5",
"@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.7",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"husky": "^8.0.3",
"less": "^4.2.0",
"lint-staged": "^15.2.0",
"npm-run-all2": "^6.1.1",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.69.5",
"stylelint": "^16.0.2",
"stylelint-config-css-modules": "^4.3.0",
"stylelint-config-standard-scss": "^12.0.0",
"tailwindcss": "^3.3.5",
"typescript": "~5.2.0",
"vite": "^4.4.11",
"vite-plugin-compression": "^0.5.1",
"vue-tsc": "^1.8.19"
},
"license": "MIT",
"repository": {
"url": "https://github.com/manyuemeiquqi/vue-tsx-admin"
},
"keywords": [
"Vue3",
"admin",
"best-practice",
"admin-template",
"management-system",
"tsx"
],
"bugs": {
"url": "https://github.com/manyuemeiquqi/vue-tsx-admin/issues"
}
}
================================================
FILE: postcss.config.js
================================================
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
================================================
FILE: src/App.tsx
================================================
import { ConfigProvider } from '@arco-design/web-vue'
import { computed, defineComponent, watch } from 'vue'
import { RouterView } from 'vue-router'
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn'
import enUS from '@arco-design/web-vue/es/locale/lang/en-us'
import { AppTheme, LocaleOptions } from './types/constants'
import useLocale from './hooks/locale'
import { useAppStore } from './store'
import { generate, getRgbStr } from '@arco-design/color'
export default defineComponent({
name: 'app',
setup() {
const appStore = useAppStore()
const arcoLocaleMap = {
[LocaleOptions.cn]: zhCN,
[LocaleOptions.en]: enUS
}
const { currentLocale } = useLocale()
const arcoLocale = computed(() => {
switch (currentLocale.value) {
case LocaleOptions.cn:
case LocaleOptions.en:
return arcoLocaleMap[currentLocale.value]
default:
return enUS
}
})
watch(
() => appStore.theme,
(theme) => {
if (theme === AppTheme.dark) {
document.body.setAttribute('arco-theme', AppTheme.dark)
} else {
document.body.removeAttribute('arco-theme')
}
},
{
immediate: true
}
)
watch(
() => appStore.themeColor,
(newColor) => {
const newList = generate(newColor, {
list: true,
dark: appStore.isDark
})
newList.forEach((l: any, index: number) => {
const rgbStr = getRgbStr(l)
document.body.style.setProperty(`--arcoblue-${index + 1}`, rgbStr)
})
},
{
immediate: true
}
)
watch(
() => appStore.colorWeak,
(colorWeak: boolean) => {
document.body.style.filter = colorWeak ? 'invert(80%)' : 'none'
},
{
immediate: true
}
)
return () => (
)
}
})
================================================
FILE: src/api/dashboard/index.ts
================================================
import axios from 'axios'
import type { TableData } from '@arco-design/web-vue/es/table/interface'
import type { ChatRecord, ContentDataRecord, PopularRecord } from './type'
export type { ContentDataRecord, PopularRecord }
export function queryContentData() {
return axios.get('/api/content-data')
}
export function queryPopularList(params: { type: string }) {
return axios.get('/api/popular/list', { params })
}
export function queryChatList() {
return axios.post('/api/chat/list')
}
================================================
FILE: src/api/dashboard/type.ts
================================================
export type PopularRecord = {
key: number
clickNumber: string
title: string
increases: number
}
export type ContentDataRecord = {
x: string
y: number
}
export type ChatRecord = {
id: number
username: string
content: string
time: string
isCollect: boolean
}
================================================
FILE: src/api/form/index.ts
================================================
import axios from 'axios'
import type { BaseInfoModel, ChannelInfoModel, GroupFormModel } from './type'
import type { OKResponse } from '@/types/global'
export type { BaseInfoModel, ChannelInfoModel, GroupFormModel }
export type UnitChannelModel = BaseInfoModel & ChannelInfoModel
export function submitChannelForm(data: UnitChannelModel) {
return axios.post('/api/channel-form/submit', { data })
}
export function submitGroupForm(data: GroupFormModel) {
return axios.post('/api/channel-form/group', { data })
}
================================================
FILE: src/api/form/type.ts
================================================
export type BaseInfoModel = {
activityName: string
channelType: string
promotionTime: string[]
promoteLink: string
}
export type ChannelInfoModel = {
advertisingSource: string
advertisingMedia: string
keyword: string[]
pushNotify: boolean
advertisingContent: string
}
export type GroupFormModel = {
video: {
mode: string
acquisition: {
resolution: string
frameRate: number
}
encoding: {
resolution: string
rate: {
min: number
max: number
default: number
}
frameRate: number
profile: string
}
}
audio: {
mode: string
explanation: string
acquisition: {
channels: number
}
encoding: {
channels: number
rate: number
profile: string
}
}
}
================================================
FILE: src/api/interceptors.ts
================================================
import useAuth from '@/hooks/auth'
import { ResCode } from '@/types/constants'
import { getToken } from '@/utils/token'
import { Message, Modal } from '@arco-design/web-vue'
import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
// InternalAxiosRequestConfig
export interface HttpResponse extends AxiosResponse {
status: number
msg: string
code: number
data: T
}
if (import.meta.env.VITE_API_BASE_URL) {
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL
}
axios.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
axios.interceptors.response.use(
(response: AxiosResponse) => {
const { data: responseData } = response
if (responseData.code !== ResCode.success) {
Message.error({
content: responseData.msg || 'Error',
duration: 5 * 1000
})
if (
[ResCode.illegalToken, ResCode.expiredToken, ResCode.otherLogin].includes(
responseData.code
) &&
response.config.url !== '/api/user/info'
) {
Modal.error({
title: 'Confirm logout',
content: 'You have been logged out, you can cancel to stay on this page, or log in again',
okText: 'Re-Login',
async onOk() {
const { logoutApp } = useAuth()
await logoutApp()
window.location.reload()
}
})
}
return Promise.reject(new Error(responseData.msg || 'Error'))
}
return responseData
},
(error) => {
Message.error({
content: error.msg || 'Request Error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
================================================
FILE: src/api/list/index.ts
================================================
import axios from 'axios'
import type { Pagination } from '@/types/global'
import type { ServiceRecord, PolicyQuery, PolicyListRes, PolicyRecord } from './type'
export type { ServiceRecord, PolicyQuery, PolicyListRes, PolicyRecord }
export function queryInspectionList() {
return axios.get('/api/list/quality-inspection')
}
export function queryTheServiceList() {
return axios.get('/api/list/the-service')
}
export function queryRulesPresetList() {
return axios.get('/api/list/rules-preset')
}
export function queryPolicyList(params: PolicyQuery & Pagination) {
return axios.get('/api/list/policy', {
params
})
}
================================================
FILE: src/api/list/type.ts
================================================
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface'
export type ServiceRecord = {
id: number
title: string
description: string
name?: string
actionType?: string
icon?: string
data?: DescData[]
enable?: boolean
expires?: boolean
}
export type PolicyRecord = {
id: string
number: number
name: string
contentType: 'img' | 'horizontalVideo' | 'verticalVideo'
filterType: 'artificial' | 'rules'
count: number
status: 'online' | 'offline'
createdTime: string
}
export type PolicyQuery = {
number: string
name: string
createdTime: string | number | Date[]
contentType: string
filterType: string
status: string
}
export type PolicyParams = {
current: number
pageSize: number
} & Partial
export type PolicyListRes = {
list: PolicyRecord[]
total: number
}
================================================
FILE: src/api/profile/index.ts
================================================
import axios from 'axios'
import type { ProfileBasicRes, operationLogRes } from './type'
export type { ProfileBasicRes, operationLogRes }
export function queryOperationLog() {
return axios.get('/api/operation/log')
}
export function queryProfileBasic() {
return axios.get('/api/profile/basic')
}
================================================
FILE: src/api/profile/type.ts
================================================
export type ProfileBasicRes = {
status: number
video: {
mode: string
acquisition: {
resolution: string
frameRate: number
}
encoding: {
resolution: string
rate: {
min: number
max: number
default: number
}
frameRate: number
profile: string
}
}
audio: {
mode: string
acquisition: {
channels: number
}
encoding: {
channels: number
rate: number
profile: string
}
}
}
export type operationLogRes = Array<{
key: string
contentNumber: string
updateContent: string
status: number
updateTime: string
}>
================================================
FILE: src/api/user/index.ts
================================================
import axios from 'axios'
import type {
LatestActivity,
LoginData,
LoginRes,
ProjectItem,
TeamItem,
UnitCertification,
UserInfo,
BasicInfoModel,
RoleRes
} from './type'
import type { OKResponse } from '@/types/global'
export type {
UserInfo,
LatestActivity,
LoginData,
LoginRes,
ProjectItem,
TeamItem,
UnitCertification,
BasicInfoModel
}
export function getUserInfo() {
return axios.post('/api/user/info')
}
export function login(data: LoginData) {
return axios.post('/api/user/login', data)
}
export function logout() {
return axios.post('/api/user/logout')
}
export function userUploadApi(data: FormData) {
return axios.post('/api/user/upload', data)
}
export function queryMyProjectList() {
return axios.post('/api/user/my-project/list')
}
export function queryMyTeamList() {
return axios.post('/api/user/my-team/list')
}
export function queryLatestActivity() {
return axios.post('/api/user/latest-activity')
}
export function saveUserInfo() {
return axios.post('/api/user/save-info')
}
export function queryCertification() {
return axios.post('/api/user/certification')
}
export function requestSwitchRole() {
return axios.post('/api/user/switch-user-role')
}
================================================
FILE: src/api/user/type.ts
================================================
export type EnterpriseCertificationModel = {
accountType: number
status: number
time: string
legalPerson: string
certificateType: string
authenticationNumber: string
enterpriseName: string
enterpriseCertificateType: string
organizationCode: string
}
export type Certification = {
certificationType: number
certificationContent: string
status: number
time: string
}
export type RoleType = '' | '*' | 'admin' | 'user'
export type UserInfo = {
name?: string
avatar?: string
job?: string
organization?: string
location?: string
email?: string
introduction?: string
personalWebsite?: string
jobName?: string
organizationName?: string
locationName?: string
phone?: string
registrationDate?: string
accountId?: string
certification?: number
role: RoleType
}
export type CertificationRecord = Certification[]
export type UnitCertification = {
enterpriseInfo: EnterpriseCertificationModel
record: CertificationRecord
}
export type LatestActivity = {
id: number
title: string
description: string
avatar: string
}
export type MyProjectRecord = {
id: number
name: string
description: string
peopleNumber: number
contributors: {
name: string
email: string
avatar: string
}[]
}
export type BasicInfoModel = {
email: string
nickname: string
countryRegion: string
area: string
address: string
profile: string
}
export type ProjectItem = {
id: number
name: string
description: string
peopleNumber: number
contributors: {
name: string
avatar: string
email: string
}[]
}
export type TeamItem = {
id: number
avatar: string
name: string
peopleNumber: number
}
export type LoginData = {
username: string
password: string
}
export type LoginRes = {
token: string
}
export type RoleRes = {
role: RoleType
}
================================================
FILE: src/assets/style/generic.scss
================================================
.content-wrapper {
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--border-radius-medium) !important;
background-color: var(--color-bg-1);
}
.general-card {
border-radius: var(--border-radius-medium);
border: none;
& > .arco-card-header {
height: auto;
padding: 20px;
border: none;
}
& > .arco-card-body {
padding: 0 20px 20px;
}
}
.echarts-tooltip-diy {
background: linear-gradient(
304.17deg,
rgb(253 254 255 / 60%) -6.04%,
rgb(244 247 252 / 60%) 85.2%
) !important;
border: none !important;
backdrop-filter: blur(10px) !important;
/* Note: backdrop-filter has minimal browser support */
border-radius: var(--border-radius-medium);
.content-panel {
display: flex;
justify-content: space-between;
padding: 0 9px;
background: rgb(255 255 255 / 80%);
width: 164px;
height: 32px;
line-height: 32px;
box-shadow: 6px 0 20px rgb(34 87 188 / 10%);
border-radius: var(--border-radius-medium);
margin-bottom: 4px;
}
.tooltip-title {
margin: 0 0 10px;
}
p {
margin: 0;
}
.tooltip-title,
.tooltip-value {
font-size: 13px;
line-height: 15px;
display: flex;
align-items: center;
text-align: right;
color: #1d2129;
font-weight: bold;
}
.tooltip-item-icon {
display: inline-block;
margin-right: 8px;
width: 10px;
height: 10px;
border-radius: var(--border-radius-circle);
}
}
================================================
FILE: src/assets/style/index.scss
================================================
@import './resetBrowser';
@import './generic';
@tailwind base;
@tailwind components;
@tailwind utilities;
================================================
FILE: src/assets/style/resetBrowser.scss
================================================
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-size: 14px;
background-color: var(--color-bg-1);
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
/* width */
::-webkit-scrollbar {
width: 10px;
height: 12px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--color-text-4);
border-radius: var(--border-radius-medium);
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-3);
}
================================================
FILE: src/components/card-layout/index.tsx
================================================
import { useAppStore } from '@/store'
import { layoutStyleConfig } from '@/types/constants'
import { computed, defineComponent, type VNode } from 'vue'
type Slots = {
default: () => VNode
}
export default defineComponent({
name: 'CardLayout',
setup(_, { slots }) {
const appStore = useAppStore()
const height = computed(() => {
let ret =
layoutStyleConfig.breadcrumbHeight +
layoutStyleConfig.footerHeight +
layoutStyleConfig.tabHeight
if (appStore.navbar) {
ret += layoutStyleConfig.navbarHeight
}
return ret
})
return () => (
{(slots as unknown as Slots).default()}
)
}
})
================================================
FILE: src/components/chart-component/index.tsx
================================================
import { defineComponent, nextTick, ref } from 'vue'
import VChart from 'vue-echarts'
export default defineComponent({
name: 'ChartComponent',
props: {
options: {
type: Object,
default() {
return {}
}
},
autoResize: {
type: Boolean,
default: true
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
}
},
setup(props) {
const renderChart = ref(false)
nextTick(() => {
renderChart.value = true
})
return () =>
renderChart.value && (
)
}
})
================================================
FILE: src/components/layout-component/AppSetting.tsx
================================================
import { useAppStore } from '@/store'
import {
Button,
Divider,
Drawer,
Grid,
InputNumber,
Message,
Space,
Switch,
Trigger,
Typography
} from '@arco-design/web-vue'
import { Sketch } from '@ckpack/vue-color'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { generate } from '@arco-design/color'
import { IconSettings } from '@arco-design/web-vue/es/icon'
export default defineComponent({
name: 'AppSetting',
setup() {
const { t } = useI18n()
const appStore = useAppStore()
const handleCancel = () => {
appStore.settingVisible = false
}
const handleOK = () => {
appStore.resetSetting()
Message.success('重置成功')
appStore.settingVisible = false
}
const handleChangeThemeColor = (val: any) => {
appStore.themeColor = val.hex
}
const colorList = computed(() => generate(appStore.themeColor, { list: true }))
return () => (
<>
{!appStore.navbar && (
)}
{{
title() {
return t('settings.title')
},
default() {
return (
<>
{t('settings.themeColor')}
{{
default() {
return (
<>
>
)
},
content() {
return (
<>
>
)
}
}}
{colorList.value.map((item: any, index: string) => (
))}
根据主题颜色生成的 10 个梯度色
{t('settings.content')}
{t('settings.navbar')}
{t('settings.menu')}
{t('settings.menuWidth')}
{t('settings.otherSettings')}
{t('settings.colorWeak')}
>
)
}
}}
>
)
}
})
================================================
FILE: src/components/layout-component/AvatarAndOptions.tsx
================================================
import useAuth from '@/hooks/auth'
import { useUserStore } from '@/store'
import { ViewNames } from '@/types/constants'
import { Avatar, Dropdown, Message, Space } from '@arco-design/web-vue'
import { IconExport, IconSettings, IconTag, IconUser } from '@arco-design/web-vue/es/icon'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
export default defineComponent({
name: 'AvatarAndOptions',
setup() {
const { t } = useI18n()
const userStore = useUserStore()
const { logoutApp } = useAuth()
const router = useRouter()
const handleLogout = async () => {
try {
await logoutApp()
Message.success('登出成功')
router.push({ name: ViewNames.login })
} finally {
/* empty */
}
}
const switchRole = () => {
userStore
.switchRoles()
.then((res) => {
if (res) Message.success(res)
})
.catch(() => {
Message.error('切换失败')
})
}
const optionList = computed(() => [
{
label: t('messageBox.switchRoles'),
onClick: switchRole,
icon:
},
{
label: t('messageBox.userCenter'),
onClick: () => {
router.push({ name: ViewNames.info })
},
icon:
},
{
label: t('messageBox.userSettings'),
onClick: () => {
router.push({ name: ViewNames.setting })
},
icon:
},
{
label: t('messageBox.logout'),
onClick: handleLogout,
icon:
}
])
return () => (
{{
default: () => (
),
content: () => {
return optionList.value.map((item) => (
{item.icon}
{item.label}
))
}
}}
)
}
})
================================================
FILE: src/components/layout-component/BreadcrumbComponent.tsx
================================================
import { layoutStyleConfig } from '@/types/constants'
import { Breadcrumb } from '@arco-design/web-vue'
import { IconApps } from '@arco-design/web-vue/es/icon'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'BreadcrumbComponent',
setup() {
const route = useRoute()
const { t } = useI18n()
return () => (
{route.matched.map((item) => (
{t(item.meta.locale + '')}
))}
)
}
})
================================================
FILE: src/components/layout-component/FooterComponent.tsx
================================================
import { ApplicationInfo, layoutStyleConfig } from '@/types/constants'
import { LayoutFooter } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'FooterComponent',
setup() {
return () => (
{ApplicationInfo.appTitle}
)
}
})
================================================
FILE: src/components/layout-component/MenuComponent.tsx
================================================
import useAppRoute from '@/hooks/appRoute'
import { useAppStore } from '@/store'
import { listenerRouteChange } from '@/utils/routerListener'
import { Menu } from '@arco-design/web-vue'
import { defineComponent, h, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter, type RouteRecordRaw } from 'vue-router'
export default defineComponent({
name: 'MenuComponent',
setup() {
const { t } = useI18n()
const router = useRouter()
const { appRouteData } = useAppRoute()
const appStore = useAppStore()
const openKeys = ref([])
const selectedKey = ref([])
const renderMenuContent = () => {
const traverse = (routeList: any[]) => {
const list = []
for (let i = 0; i < routeList.length; i++) {
const route = routeList[i]
if (route.children === undefined) {
list.push(
handleMenuItemClick(route)}>
{t(route.locale)}
)
} else {
if (route.children.length > 0) {
list.push(
h(route.icon),
title: () => t(route.locale || '')
}}
>
{traverse(route.children)}
)
}
}
}
return list
}
return traverse(appRouteData.value.tree)
}
const handleMenuItemClick = (item: RouteRecordRaw) => {
router.push({
name: item.name
})
}
listenerRouteChange((newRoute) => {
if (newRoute.name) {
const appRoute = appRouteData.value.map[newRoute.name]
if (appRoute) {
const namePath = appRoute.namePath
openKeys.value = Array.from(new Set([...namePath, ...openKeys.value]))
const stackTopName = namePath[namePath.length - 1]
selectedKey.value = [stackTopName]
}
}
}, true)
return () => (
)
}
})
================================================
FILE: src/components/layout-component/Navbar.tsx
================================================
import Logo from '@/assets/logo.svg'
import useLocale from '@/hooks/locale'
import { useAppStore } from '@/store'
import { ApplicationInfo, LocaleOptions, layoutStyleConfig } from '@/types/constants'
import { Button, Input, Select, Space, Tooltip, Typography } from '@arco-design/web-vue'
import {
IconFullscreen,
IconFullscreenExit,
IconLanguage,
IconMoonFill,
IconSettings,
IconSunFill
} from '@arco-design/web-vue/es/icon'
import { useFullscreen } from '@vueuse/core'
import { isString } from 'lodash'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import AvatarAndOptions from './AvatarAndOptions'
import styles from './style.module.scss'
export default defineComponent({
name: 'Navbar',
setup() {
const { t } = useI18n()
const { changeLocale } = useLocale()
const appStore = useAppStore()
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen()
const handleLocaleChange = (val: unknown) => {
if (isString(val)) {
changeLocale(val)
}
}
const setVisible = () => {
appStore.settingVisible = true
}
return () => (
)
}
})
================================================
FILE: src/components/layout-component/PageComponent.tsx
================================================
import { useTabStore } from '@/store'
import { get } from 'lodash'
import { KeepAlive, Transition, defineComponent, type VNode } from 'vue'
import { RouterView, type RouteLocationNormalizedLoaded } from 'vue-router'
export default defineComponent({
name: 'PageComponent',
setup() {
const tabStore = useTabStore()
return () => (
{({ Component, route }: { Component: VNode; route: RouteLocationNormalizedLoaded }) => (
{get(route, 'meta.ignoreCache') === true ? (
Component
) : (
{Component}
)}
)}
)
}
})
================================================
FILE: src/components/layout-component/TabBar.tsx
================================================
import useTabStore from '@/store/modules/tab/index'
import { layoutStyleConfig } from '@/types/constants'
import { listenerRouteChange, removeRouteListener } from '@/utils/routerListener'
import { Affix } from '@arco-design/web-vue'
import { computed, defineComponent, onUnmounted, ref, watch } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import TabItem from './TabItem'
import styles from './style.module.scss'
import { useAppStore } from '@/store'
export default defineComponent({
name: 'TabBar',
setup() {
const tabStore = useTabStore()
const tabList = computed(() => {
return tabStore.tabList
})
const affixRef = ref()
listenerRouteChange((route: RouteLocationNormalized) => {
if (!tabList.value.some((item) => item.name === route.name)) {
tabStore.updateTabList(route)
}
}, true)
onUnmounted(() => {
removeRouteListener()
})
const appStore = useAppStore()
const offsetTop = computed(() => {
return appStore.navbar ? layoutStyleConfig.navbarHeight : 0
})
watch(
() => appStore.navbar,
() => {
affixRef.value.updatePosition()
}
)
return () => (
{tabList.value.map((item, index) => (
))}
)
}
})
================================================
FILE: src/components/layout-component/TabItem.tsx
================================================
import { useTabStore } from '@/store'
import { defaultTab, type TabItem } from '@/store/modules/tab'
import { ViewNames } from '@/types/constants'
import { Doption, Dropdown } from '@arco-design/web-vue'
import {
IconClose,
IconFolderDelete,
IconRefresh,
IconSwap,
IconToLeft,
IconToRight
} from '@arco-design/web-vue/es/icon'
import { cloneDeep } from 'lodash'
import { computed, defineComponent, withModifiers, type PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import styles from './style.module.scss'
enum TabActionType {
reload = 'reload',
current = 'current',
left = 'left',
right = 'right',
others = 'others',
all = 'all'
}
export default defineComponent({
name: 'TabItem',
props: {
itemData: {
type: Object as PropType,
required: true
},
index: {
type: Number,
required: true
}
},
setup(props) {
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const tabStore = useTabStore()
const tabList = computed(() => {
return tabStore.tabList
})
const findCurrentRouteIndex = () => {
return tabList.value.findIndex((el) => el.name === route.name)
}
const handleTabClick = () => {
router.push({
path: props.itemData.fullPath
})
}
const handleTabClose = () => {
tabStore.deleteTab(props.itemData.name as ViewNames)
if (props.itemData.name === route.name) {
const prevTab = tabList.value[props.index - 1]
router.push({ path: prevTab.fullPath })
}
}
const handleSelect = async (value: unknown) => {
const actionType = value
switch (actionType) {
case TabActionType.all: {
tabStore.resetTabList()
break
}
case TabActionType.others: {
const filterList = tabList.value.filter((el, idx) => {
return [0, props.index].includes(idx)
})
tabStore.freshTabList(filterList)
break
}
case TabActionType.left: {
const currentRouteIdx = findCurrentRouteIndex()
const replaceList = cloneDeep(tabList.value).splice(props.index)
tabStore.freshTabList([defaultTab, ...replaceList])
if (currentRouteIdx < props.index) {
router.push({ path: props.itemData.fullPath })
}
break
}
case TabActionType.right: {
const currentRouteIdx = findCurrentRouteIndex()
const replaceList = cloneDeep(tabList.value).splice(0, props.index + 1)
tabStore.freshTabList(replaceList)
if (currentRouteIdx > props.index) {
router.push({ path: props.itemData.fullPath })
}
break
}
case TabActionType.reload: {
router.push({
name: ViewNames.redirect,
params: {
path: route.fullPath
}
})
break
}
case TabActionType.current: {
handleTabClose()
break
}
default:
break
}
}
const disabledRight = computed(() => {
return props.index === tabStore.tabList.length - 1
})
const disabledReload = computed(() => {
return props.index !== findCurrentRouteIndex()
})
const disabledLeft = computed(() => {
return [0, 1].includes(props.index)
})
const shouldClose = computed(() => props.index !== 0)
const tagChecked = computed(() => props.itemData.name === route.name)
return () => (
{{
// argo tag exist bug,so use div
default: () => (
{t(props.itemData.title)}
{shouldClose.value && (
)}
),
content: () => (
<>
重新加载
关闭当前标签页
关闭左侧标签页
关闭右侧标签页
关闭其他标签页
关闭全部标签页
>
)
}}
)
}
})
================================================
FILE: src/components/layout-component/index.tsx
================================================
import { useAppStore, useUserStore } from '@/store'
import { ViewNames, layoutStyleConfig } from '@/types/constants'
import { Layout } from '@arco-design/web-vue'
import { computed, defineComponent, watch } from 'vue'
import BreadcrumbComponent from './BreadcrumbComponent'
import FooterComponent from './FooterComponent'
import MenuComponent from './MenuComponent'
import Navbar from './Navbar'
import PageComponent from './PageComponent'
import TabBar from './TabBar'
import styles from './style.module.scss'
import AppSetting from './AppSetting'
import usePermission from '@/hooks/permission'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
name: 'LayoutComponent',
setup() {
const appStore = useAppStore()
const paddingStyle = computed(() => {
const paddingLeft = appStore.menu ? { paddingLeft: `${siderWidth.value}px` } : {}
const paddingTop = appStore.navbar
? { paddingTop: layoutStyleConfig.navbarHeight + 'px' }
: {}
return { ...paddingLeft, ...paddingTop }
})
const siderWidth = computed(() => {
return appStore.menuCollapse ? 48 : appStore.menuWidth
})
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const permission = usePermission()
watch(
() => userStore.role,
(roleValue) => {
if (roleValue && !permission.checkRoutePermission(route))
router.push({ name: ViewNames.notFound })
}
)
return () => {
return (
<>
{appStore.navbar && }
{appStore.menu && (
(appStore.menuCollapse = val)}
>
)}
>
)
}
}
})
================================================
FILE: src/components/layout-component/style.module.scss
================================================
.navbar {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
position: fixed;
top: 0;
left: 0;
z-index: 100;
background-color: var(--color-bg-2);
padding-left: 1.25rem /* 20px */;
box-sizing: border-box;
border-bottom: 1px solid var(--color-border);
}
.nav-btn {
border-color: rgb(var(--gray-2)) !important;
color: rgb(var(--gray-8)) !important;
font-size: 16px;
}
.sider {
height: 100%;
position: fixed;
left: 0;
z-index: 50;
box-sizing: border-box;
}
.main {
min-width: 1100px;
min-height: 100vh;
transition-property: padding-left;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
transition-duration: 0.2s;
background: var(--color-fill-2);
}
.tab-box {
height: 40px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-2);
padding-left: 16px;
padding-right: 16px;
::-webkit-scrollbar {
width: 12px;
height: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--color-text-4);
border-radius: var(--border-radius-medium);
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-3);
}
}
.tag-link {
color: var(--color-text-2);
text-decoration: none;
}
.link-activated {
color: rgb(var(--link-6));
.tag-link {
color: rgb(var(--link-6));
}
& + .arco-tag-close-btn {
color: rgb(var(--link-6));
}
}
================================================
FILE: src/hooks/appRoute.ts
================================================
import { appRoutes } from '@/router/routes'
import { ViewNames } from '@/types/constants'
import {
IconCloseCircle,
IconDashboard,
IconExclamationCircle,
IconFile,
IconList,
IconSettings,
IconUser
} from '@arco-design/web-vue/es/icon'
import { isString } from 'lodash'
import { computed } from 'vue'
import type { RouteRecordName, RouteRecordRaw } from 'vue-router'
import usePermission from './permission'
const routeIconMap: Record = {
[ViewNames.dashboard]: IconDashboard,
[ViewNames.profile]: IconFile,
[ViewNames.exception]: IconExclamationCircle,
[ViewNames.form]: IconSettings,
[ViewNames.list]: IconList,
[ViewNames.result]: IconCloseCircle,
[ViewNames.user]: IconUser
}
type Context = {
currentNode: MenuData | null
parent: MenuData | null
}
type MenuData = {
name: string
icon?: typeof IconDashboard
namePath: string[]
locale: string
localePath: string[]
children?: MenuData[]
}
/**
* @desc convention
* dashboard is fixed no permission view, if you want dynamic ,use following and complete other logic
*/
export const firstPermissionRoute = {
name: ViewNames.workplace,
title: 'menu.dashboard.workplace',
fullPath: '/dashboard/workplace'
}
// computed to memo
// const firstPermissionRoute = (() => {
// const getFirstChild = (node: RouteRecordRaw): null | RouteRecordRaw => {
// if (permission.checkRoutePermission(node)) {
// if (node.children === undefined) {
// return node
// } else {
// for (let i = 0; i < node.children.length; i++) {
// const findRes = getFirstChild(node.children[i])
// if (findRes) {
// return findRes
// }
// }
// }
// }
// return null
// }
// for (let i = 0; i < appRoutes.length; i++) {
// const findRes = getFirstChild(appRoutes[i])
// if (findRes) {
// return findRes
// }
// }
// return null
// })
export default function useAppRoute() {
const permission = usePermission()
const appRouteData = computed(() => {
const getMenuData = (route: RouteRecordRaw, context: Context) => {
const ret: MenuData = {
name: isString(route.name) ? route.name : '',
locale: typeof route.meta?.locale === 'string' ? route.meta.locale : '',
localePath: [],
namePath: []
}
ret.namePath.push(ret.name)
ret.localePath.push(ret.locale)
if (context.parent?.localePath) {
ret.localePath = context.parent.localePath.concat(ret.localePath)
}
if (context.parent?.namePath) {
ret.namePath = context.parent.namePath.concat(ret.namePath)
}
if (ret.name in routeIconMap) {
ret.icon = routeIconMap[ret.name]
}
return ret
}
const getSubMenu = (node: RouteRecordRaw, context: Context) => {
if (permission.checkRoutePermission(node)) {
const menuData = getMenuData(node, context)
context.currentNode = menuData
if (node.children === undefined) {
_map[menuData.name] = menuData
return menuData
} else {
const list: MenuData[] = []
for (let j = 0; j < node.children.length; j++) {
context.parent = menuData
const child = getSubMenu(node.children[j], context)
if (child) list.push(child)
}
if (list.length) {
menuData.children = list
_map[menuData.name] = menuData
return menuData
}
return null
}
} else {
return null
}
}
const _map: Record = {}
const nodeList = []
for (let i = 0; i < appRoutes.length; i++) {
const context: Context = {
currentNode: null,
parent: null
}
const menuNode = getSubMenu(appRoutes[i], context)
if (menuNode) {
nodeList.push(menuNode)
}
}
return { tree: nodeList, map: _map }
})
return {
appRouteData
}
}
================================================
FILE: src/hooks/auth.ts
================================================
import { login, logout, type LoginData } from '@/api/user'
import { useUserStore } from '@/store'
import { clearToken, setToken } from '@/utils/token'
import { removeRouteListener } from '@/utils/routerListener'
/**
*
* @desc system authentication
*/
export default function useAuth() {
const loginApp = async (data: LoginData) => {
try {
const res = await login(data)
setToken(res.data.token)
} catch (err) {
clearToken()
throw err
}
}
const logoutApp = async () => {
const userStore = useUserStore()
const afterLogout = () => {
userStore.resetUserInfo()
clearToken()
removeRouteListener()
}
try {
await logout()
} finally {
afterLogout()
}
}
return {
loginApp,
logoutApp
}
}
================================================
FILE: src/hooks/chartOption.ts
================================================
import { computed } from 'vue'
import { type EChartsOption } from 'echarts'
import { useAppStore } from '@/store'
type optionsFn = {
(isDark: boolean): EChartsOption
}
export default function useChartOption(sourceOption: optionsFn) {
const appStore = useAppStore()
const chartOption = computed(() => {
return sourceOption(appStore.isDark)
})
return {
chartOption
}
}
================================================
FILE: src/hooks/loading.ts
================================================
import { ref } from 'vue'
export default function useLoading(initialState: boolean = false) {
const loading = ref(initialState)
const setLoading = (state: boolean) => {
loading.value = state
}
const toggleLoading = () => (loading.value = !loading.value)
return {
loading,
setLoading,
toggleLoading
}
}
================================================
FILE: src/hooks/locale.ts
================================================
import { LocalStorageKey, LocaleOptions } from '@/types/constants'
import { Message } from '@arco-design/web-vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
/**
*
* @desc get project locale state hook
* >state flow:
* - i18n read storage to init
* - app read i18n state and produce reactive data currentLocale
* - update currentLocale with updating storage and i18n stance
*/
export default function useLocale() {
const i18n = useI18n()
const currentLocale = computed(() => i18n.locale.value)
const changeLocale = (locale: string) => {
if (i18n.locale.value === locale) return
const isLocaleOption = (val: string): val is LocaleOptions => {
return (Object.values(LocaleOptions) as string[]).includes(val)
}
if (isLocaleOption(locale)) {
i18n.locale.value = locale
localStorage.setItem(LocalStorageKey.localeKey, locale)
Message.success(i18n.t('navbar.action.locale'))
}
}
return {
currentLocale,
changeLocale
}
}
================================================
FILE: src/hooks/permission.ts
================================================
import { useUserStore } from '@/store'
import { cloneDeep, get } from 'lodash'
import type {
RouteLocationNormalizedLoaded,
RouteRecordNormalized,
RouteRecordRaw
} from 'vue-router'
export default function usePermission() {
const userStore = useUserStore()
return {
checkRoutePermission(
route: RouteRecordRaw | RouteRecordNormalized | RouteLocationNormalizedLoaded
) {
const requiresAuth = get(route, 'meta.requiresAuth')
const needRoles = (get(route, 'meta.roles') || []) as string[]
return (
!requiresAuth ||
needRoles.length === 0 ||
needRoles.includes('*') ||
needRoles.includes(userStore.role)
)
},
checkButtonPermission(needPermission: string[]): boolean {
const userStore = useUserStore()
return needPermission.includes(userStore.role)
}
}
}
================================================
FILE: src/locale/en-US/dashboard.json
================================================
{
"menu.dashboard.monitor": "Real-time Monitor",
"monitor.title.chatPanel": "Chat Window",
"monitor.title.quickOperation": "Quick Operation",
"monitor.title.studioInfo": "Studio Information",
"monitor.title.studioPreview": "Studio Preview",
"monitor.chat.options.all": "All",
"monitor.chat.placeholder.searchCategory": "Search Category",
"monitor.chat.update": "Update",
"monitor.list.title.order": "Order",
"monitor.list.title.cover": "Cover",
"monitor.list.title.name": "Name",
"monitor.list.title.duration": "Duration",
"monitor.list.title.id": "ID",
"monitor.list.tip.rotations": "Rotations ",
"monitor.list.tip.rest": ", The program list is not visible to viewers",
"monitor.list.tag.auditFailed": "Audit Failed",
"monitor.tab.title.liveMethod": "Live Method",
"monitor.tab.title.onlinePopulation": "Online Population",
"monitor.liveMethod.normal": "Normal Live",
"monitor.liveMethod.flowControl": "Flow Control Live",
"monitor.liveMethod.video": "Video Live",
"monitor.liveMethod.web": "Web Live",
"monitor.editCarousel": "Edit",
"monitor.startCarousel": "Start",
"monitor.quickOperation.changeClarity": "Change the Clarity",
"monitor.quickOperation.switchStream": "Switch Stream",
"monitor.quickOperation.removeClarity": "Remove the Clarity",
"monitor.quickOperation.pushFlowGasket": "Push Flow Gasket",
"monitor.studioInfo.label.studioTitle": "Studio Title",
"monitor.studioInfo.label.onlineNotification": "Online Notification",
"monitor.studioInfo.label.studioCategory": "Studio Category",
"monitor.studioInfo.placeholder.studioTitle": "'s Studio",
"monitor.studioInfo.btn.fresh": "Fresh",
"monitor.studioStatus.title.studioStatus": "Studio Status",
"monitor.studioStatus.title.pictureInfo": "Picture Information",
"monitor.studioStatus.smooth": "Smooth",
"monitor.studioStatus.frameRate": "Frame",
"monitor.studioStatus.bitRate": "Bit",
"monitor.studioStatus.mainstream": "Main",
"monitor.studioStatus.hotStandby": "Hot",
"monitor.studioStatus.coldStandby": "Cold",
"monitor.studioStatus.line": "Line",
"monitor.studioStatus.play": "Format",
"monitor.studioStatus.pictureQuality": "Quality",
"monitor.studioPreview.studio": "Studio",
"monitor.studioPreview.watching": "watching",
"menu.dashboard.workplace": "Workplace",
"workplace.welcome": "Welcome!",
"workplace.balance": "Balance (CNY)",
"workplace.order.pending": "Pending",
"workplace.order.pendingRenewal": "Renewal Order",
"workplace.onlineContent": "Online Content",
"workplace.putIn": "Put In",
"workplace.newDay": "Daily Additional Comments",
"workplace.newFromYesterday": "New From Yesterday",
"workplace.minute": "Min",
"workplace.docs": "Documents",
"workplace.docs.productOverview": "Product Overview",
"workplace.docs.userGuide": "User Guide",
"workplace.docs.workflow": "Workflow",
"workplace.docs.interfaceDocs": "Interface Docs",
"workplace.contentManagement": "Content Management",
"workplace.contentStatistical": "Content Statistical",
"workplace.advanced": "Advanced",
"workplace.onlinePromotion": "Online Promotion",
"workplace.contentPutIn": "Put In",
"workplace.announcement": "Announcement",
"workplace.recently.visited": "Recently Visited",
"workplace.record.nodata": "No data",
"workplace.quick.operation": "Quick Operation",
"workplace.quickOperation.setup": "Setup",
"workplace.allProject": "All",
"workplace.loadMore": "More",
"workplace.viewMore": "More",
"workplace.contentData": "Content Data",
"workplace.popularContent": "Popular Content",
"workplace.popularContent.text": "text",
"workplace.popularContent.image": "image",
"workplace.popularContent.video": "video",
"workplace.categoriesPercent": "Categories Percent",
"workplace.pecs": "pecs"
}
================================================
FILE: src/locale/en-US/exception.json
================================================
{
"menu.exception.500": "500",
"exception.result.500.description": "Internal server error",
"exception.result.500.back": "Back",
"menu.exception.404": "404",
"exception.result.404.description": "Whoops, this page is gone.",
"exception.result.404.retry": "Retry",
"exception.result.404.back": "Back",
"menu.exception.403": "403",
"exception.result.403.description": "Access to this resource on the server is denied.",
"exception.result.403.back": "Back"
}
================================================
FILE: src/locale/en-US/form.json
================================================
{
"menu.form.group": "Group Form",
"groupForm.title.video": "Video Parameters",
"groupForm.title.audio": "Audio Parameters",
"groupForm.title.description": "Enter Description",
"groupForm.form.label.video.mode": "Match Mode",
"groupForm.form.label.video.acquisition.resolution": "Acquisition Resolution",
"groupForm.form.label.video.acquisition.frameRate": "Acquisition Frame Rate",
"groupForm.form.label.video.encoding.resolution": "Encoding Resolution",
"groupForm.form.label.video.encoding.rate.min": "Encoding Min Rate",
"groupForm.form.label.video.encoding.rate.max": "Encoding Max Rate",
"groupForm.form.label.video.encoding.rate.default": "Encoding Default Rate",
"groupForm.form.label.video.encoding.frameRate": "Encoding Frame Rate",
"groupForm.form.label.video.encoding.profile": "Encoding Profile",
"groupForm.placeholder.video.mode": "Please Select",
"groupForm.placeholder.video.acquisition.resolution": "Please Select",
"groupForm.placeholder.video.acquisition.frameRate": "Enter Range [1, 30]",
"groupForm.placeholder.video.encoding.resolution": "Please Select",
"groupForm.placeholder.video.encoding.rate.min": "Enter Range [150, 1800]",
"groupForm.placeholder.video.encoding.rate.max": "Enter Range [150, 1800]",
"groupForm.placeholder.video.encoding.rate.default": "Enter Range [150, 1800]",
"groupForm.placeholder.video.encoding.frameRate": "Enter Range [1, 30]",
"groupForm.placeholder.video.encoding.profile": "Enter Range [150, 1800]",
"groupForm.form.label.audio.mode": "Match Mode",
"groupForm.form.label.audio.acquisition.channels": "Acquisition Channels",
"groupForm.form.label.audio.encoding.rate": "Encoding Rate",
"groupForm.form.label.audio.encoding.channels": "Encoding Channels",
"groupForm.placeholder.audio.encoding.channels": "Enter Range [150, 1800]",
"groupForm.form.label.audio.encoding.profile": "Encoding Profile",
"groupForm.placeholder.audio.mode": "Please Select",
"groupForm.placeholder.audio.acquisition.channels": "Please Select",
"groupForm.placeholder.audio.encoding.rate": "Enter Range [150, 1800]",
"groupForm.placeholder.audio.encoding.profile": "Enter Range [1, 30]",
"groupForm.form.label.parameterDescription": "Parameter Description",
"groupForm.placeholder.description": "Please fill in the parameter description with a maximum of 200 words",
"groupForm.submit": "Submit",
"groupForm.reset": "Reset",
"groupForm.submitSuccess": "Submit Success",
"menu.form.step": "Step Form",
"stepForm.step.title": "Create Channel Forms",
"stepForm.step.title.baseInfo": "Select Basic Information",
"stepForm.step.subTitle.baseInfo": "Channel creation activities",
"stepForm.step.title.channel": "Channel Information",
"stepForm.step.subTitle.channel": "Select upstream of domain",
"stepForm.step.title.finish": "Finish",
"stepForm.step.subTitle.finish": "Submit success",
"stepForm.success.title": "Success",
"stepForm.success.subTitle": "The form is submitted successfully!",
"stepForm.button.next": "Next",
"stepForm.button.prev": "Prev",
"stepForm.button.submit": "Submit",
"stepForm.button.again": "Again",
"stepForm.button.view": "Detail",
"stepForm.form.label.activityName": "Activity Name",
"stepForm.placeholder.activityName": "Enter a maximum of 20 Chinese characters, letters, or digits",
"stepForm.form.error.activityName.pattern": "Enter a maximum of 20 Chinese characters, letters, or digits",
"stepForm.form.error.activityName.required": "Please enter the activity name",
"stepForm.form.label.channelType": "Channel Type",
"stepForm.placeholder.channelType": "Select a channel type",
"stepForm.form.error.channelType.required": "Please select a channel type",
"stepForm.form.label.promotionTime": "Promotion Time",
"stepForm.form.error.promotionTime.required": "Please select the promotion time",
"stepForm.form.label.promoteLink": "Promote Link",
"stepForm.form.error.promoteLink.required": "Please enter the promotion link",
"stepForm.form.error.promoteLink.pattern": "For example, the download address of Android or iOS or the intermediate URL must start with http:// or https://",
"stepForm.form.tip.promoteLink": "For example, the download address of Android or iOS or the intermediate URL must start with http:// or https://",
"stepForm.placeholder.promoteLink": "Please enter the promotion page Link",
"stepForm.form.label.advertisingSource": "Advertising Source",
"stepForm.placeholder.advertisingSource": "Introduction source address: Sohu, Sina",
"stepForm.form.error.advertisingSource.required": "Please enter the advertising source",
"stepForm.form.label.advertisingMedia": "Advertising Media",
"stepForm.placeholder.advertisingMedia": "Marketing media: CPC, Banner, EDM",
"stepForm.form.error.advertisingMedia.required": "Please enter the advertising media",
"stepForm.form.label.keyword": "keyword",
"stepForm.placeholder.keyword": "Please select keyword",
"stepForm.form.error.keyword.required": "Please select keyword",
"stepForm.form.label.pushNotify": "Push Notify",
"stepForm.form.label.advertisingContent": "Advertising Content",
"stepForm.placeholder.advertisingContent": "Please enter the description of advertisement content, the maximum is 200 words",
"stepForm.form.error.advertisingContent.required": "Please enter the description of advertisement content",
"stepForm.form.error.advertisingContent.maxLength": "the maximum is 200 words",
"stepForm.form.description.title": "Channel Form Description",
"stepForm.form.description.text": "Advertiser channel promotion supports tracking of users who download apps by placing ads on third-party advertisers, such as toutiao channel, and tracking users who activate apps by downloading apps through channels.",
"menu.form": "Form",
"stepForm.title": "Create a channel form",
"stepForm.next": "Next",
"stepForm.prev": "Prev",
"stepForm.title.basicInfo": "Basic Information",
"stepForm.desc.basicInfo": "Create event channel",
"stepForm.title.channel": "Channel Information",
"stepForm.desc.channel": "Enter detailed channel content",
"stepForm.title.created": "Complete creation",
"stepForm.desc.created": "Created successfully",
"stepForm.basicInfo.name": "Event name",
"stepForm.basicInfo.name.required": "Please enter the event name",
"stepForm.basicInfo.name.placeholder": "Enter Chinese characters, letters or numbers, up to 20 characters",
"stepForm.basicInfo.channelType": "Channel Type",
"stepForm.basicInfo.channelType.required": "Please select the channel type",
"stepForm.basicInfo.time": "Promotion time",
"stepForm.basicInfo.time.required": "Please select the promotion time",
"stepForm.basicInfo.link": "Promotion URL",
"stepForm.basicInfo.link.placeholder": "Please enter the promotion page address",
"stepForm.basicInfo.link.tips": "Such as Android or iOS download address, intermediate redirect URL, the URL must start with http:// or https://",
"stepForm.channel.source": "Advertising source",
"stepForm.channel.source.required": "Please enter the advertising source",
"stepForm.channel.source.placeholder": "Referral address: sohu, sina",
"stepForm.channel.media": "Advertising medium",
"stepForm.channel.media.required": "Please enter the advertising medium",
"stepForm.channel.media.placeholder": "Marketing media: cpc, bannner, edm",
"stepForm.channel.keywords": "Key words",
"stepForm.channel.remind": "Push reminder",
"stepForm.channel.content": "Advertising content",
"stepForm.channel.content.required": "Please enter the advertising content",
"stepForm.channel.content.placeholder": "Please enter the description of the advertisement content, no more than 200 words",
"stepForm.created.success.title": "Created successfully",
"stepForm.created.success.desc": "Form created successfully",
"stepForm.created.success.view": "View form",
"stepForm.created.success.again": "Create again",
"stepForm.created.extra.title": "Channel form description",
"stepForm.created.extra.desc": "Advertiser channel promotion supports the tracking of users who place ads on third-party advertisers to download App users, such as launching App download advertisements on Toutiao channels, and tracking users who activate App by downloading on channels. ",
"stepForm.created.extra.detail": "Details",
"groupForm.title.explanation": "Enter Explanation",
"groupForm.form.label.explanation": "Explanation",
"groupForm.placeholder.explanation": "Please fill in the parameter description, no more than 200 characters"
}
================================================
FILE: src/locale/en-US/global.json
================================================
{
"menu.dashboard": "Dashboard",
"menu.server.dashboard": "Dashboard-Server",
"menu.server.workplace": "Workplace-Server",
"menu.server.monitor": "Monitor-Server",
"menu.list": "List",
"menu.result": "Result",
"menu.exception": "Exception",
"menu.form": "Form",
"menu.profile": "Profile",
"menu.visualization": "Data Visualization",
"menu.user": "User Center",
"menu.arcoWebsite": "Arco Design",
"menu.faq": "FAQ",
"navbar.docs": "Docs",
"navbar.action.locale": "Switch to English",
"messageBox.tab.title.message": "Message",
"messageBox.tab.title.notice": "Notice",
"messageBox.tab.title.todo": "Todo",
"messageBox.tab.button": "empty",
"messageBox.allRead": "All Read",
"messageBox.viewMore": "View More",
"messageBox.noContent": "No Content",
"messageBox.switchRoles": "Switch Roles",
"messageBox.userCenter": "User Center",
"messageBox.userSettings": "User Settings",
"messageBox.logout": "Logout",
"navbar.search.placeholder": "Please search"
}
================================================
FILE: src/locale/en-US/list.json
================================================
{
"menu.list.cardList": "Card List",
"cardList.tab.title.all": "All",
"cardList.tab.title.content": "Quality Inspection",
"cardList.tab.title.service": "The service",
"cardList.tab.title.preset": "Rules Preset",
"cardList.searchInput.placeholder": "Search",
"cardList.content.delete": "Delete",
"cardList.content.inspection": "Inspection",
"cardList.content.action": "Click Create Qc Content queue",
"cardList.service.open": "Open",
"cardList.service.cancel": "Cancel",
"cardList.service.renew": "Contract of service",
"cardList.service.tag": "Opened",
"cardList.service.expiresTag": "Expired",
"cardList.preset.tag": "Enable",
"menu.list.searchTable": "Search Table",
"searchTable.form.number": "Set Number",
"searchTable.form.number.placeholder": "Please enter Set Number",
"searchTable.form.name": "Set Name",
"searchTable.form.name.placeholder": "Please enter Set Name",
"searchTable.form.contentType": "Content Type",
"searchTable.form.contentType.img": "image-text",
"searchTable.form.contentType.horizontalVideo": "Horizontal short video",
"searchTable.form.contentType.verticalVideo": "Vertical short video",
"searchTable.form.filterType": "Filter Type",
"searchTable.form.filterType.artificial": "artificial",
"searchTable.form.filterType.rules": "Rules",
"searchTable.form.createdTime": "Create Date",
"searchTable.form.status": "Status",
"searchTable.form.status.online": "Online",
"searchTable.form.status.offline": "Offline",
"searchTable.form.search": "Search",
"searchTable.form.reset": "Reset",
"searchTable.form.selectDefault": "All",
"searchTable.operation.create": "Create",
"searchTable.operation.import": "Import",
"searchTable.operation.download": "Download",
"searchTable.columns.index": "#",
"searchTable.columns.number": "Set Number",
"searchTable.columns.name": "Set Name",
"searchTable.columns.contentType": "Content Type",
"searchTable.columns.filterType": "Filter Type",
"searchTable.columns.count": "Count",
"searchTable.columns.createdTime": "CreatedTime",
"searchTable.columns.status": "Status",
"searchTable.columns.operations": "Operations",
"searchTable.columns.operations.view": "View",
"searchTable.size.mini": "mini",
"searchTable.size.small": "small",
"searchTable.size.medium": "middle",
"searchTable.size.large": "large",
"searchTable.actions.refresh": "refresh",
"searchTable.actions.density": "density",
"searchTable.actions.columnSetting": "columnSetting",
"menu.list": "List",
"searchTable.columns.id": "Collection ID",
"searchTable.columns.contentNum": "Content quantity",
"searchTable.columns.operations.update": "Edit",
"searchTable.columns.operations.offline": "Offline",
"searchTable.columns.operations.online": "Online",
"searchTable.operations.add": "New",
"searchTable.operations.upload": "Bulk upload",
"searchForm.id.placeholder": "Please enter the collection ID",
"searchForm.name.placeholder": "Please enter the collection name",
"searchForm.all.placeholder": "all"
}
================================================
FILE: src/locale/en-US/login.json
================================================
{
"login.form.title": "Login to Vue TSX Admin",
"login.form.userName.errMsg": "Username cannot be empty",
"login.form.password.errMsg": "Password cannot be empty",
"login.form.login.errMsg": "Login error, refresh and try again",
"login.form.login.success": "welcome to use",
"login.form.userName.placeholder": "Username: admin",
"login.form.password.placeholder": "Password: admin",
"login.form.rememberPassword": "Remember password",
"login.form.forgetPassword": "Forgot password",
"login.form.login": "login",
"login.form.register": "register account",
"login.banner.slogan1": "Out-of-the-box high-quality template",
"login.banner.subSlogan1": "Rich page templates, covering most typical business scenarios",
"login.banner.slogan2": "Built-in solutions to common problems",
"login.banner.subSlogan2": "Internationalization, routing configuration, state management everything",
"login.banner.slogan3": "Access visualization enhancement tool AUX",
"login.banner.subSlogan3": "Realize flexible block development"
}
================================================
FILE: src/locale/en-US/profile.json
================================================
{
"menu.profile.basic": "Basic Profile",
"basicProfile.title.form": "Parameter Approval Process Table",
"basicProfile.steps.commit": "Commit",
"basicProfile.steps.approval": "Approval",
"basicProfile.steps.finish": "Finish",
"basicProfile.title.currentParams": "Current Parameters",
"basicProfile.title.originParams": "Original Parameters",
"basicProfile.title.video": "Video Parameters",
"basicProfile.title.audio": "Audio Parameters",
"basicProfile.title.preVideo": "Original video parameters",
"basicProfile.title.preAudio": "Original audio parameters",
"basicProfile.label.video.mode": "Match Mode",
"basicProfile.label.video.acquisition.resolution": "Acquisition Resolution",
"basicProfile.label.video.acquisition.frameRate": "Acquisition Frame Rate",
"basicProfile.label.video.encoding.resolution": "Encoding Resolution",
"basicProfile.label.video.encoding.rate.min": "Encoding Min Rate",
"basicProfile.label.video.encoding.rate.max": "Encoding Max Rate",
"basicProfile.label.video.encoding.rate.default": "Encoding Default Rate",
"basicProfile.label.video.encoding.frameRate": "Encoding Frame Rate",
"basicProfile.label.video.encoding.profile": "Encoding Profile",
"basicProfile.label.audio.mode": "Match Mode",
"basicProfile.label.audio.acquisition.channels": "Acquisition Channels",
"basicProfile.label.audio.encoding.channels": "Encoding Channels",
"basicProfile.label.audio.encoding.rate": "Encoding Rate",
"basicProfile.label.audio.encoding.profile": "Encoding Profile",
"basicProfile.unit.audio.channels": "channels",
"basicProfile.goBack": "GoBack",
"basicProfile.cancel": "Cancel Process",
"basicProfile.title.operationLog": "Operation Log",
"basicProfile.column.contentNumber": "Content Number",
"basicProfile.column.updateContent": "Update Content",
"basicProfile.column.status": "Status",
"basicProfile.column.updateTime": "Update Time",
"basicProfile.column.operation": "Operation",
"basicProfile.cell.pass": "Pass",
"basicProfile.cell.auditing": "Auditing",
"basicProfile.cell.view": "View"
}
================================================
FILE: src/locale/en-US/result.json
================================================
{
"menu.result.error": "Error",
"error.result.title": "Submit Error",
"error.result.subTitle": "Submit form error",
"error.result.goBack": "Go Back",
"error.result.retry": "return for correction",
"error.detailTitle": "Details of Error",
"error.detailLine.record": "The current domain name has not been registered, please check the registration process: ",
"error.detailLine.record.link": "Registration Process",
"error.detailLine.auth": "Your user group does not have the authority to perform this operation;",
"menu.result.success": "Success",
"success.result.title": "Submit Success",
"success.result.subTitle": "Submit form success!",
"success.result.printResult": "Print result",
"success.result.projectList": "Project List",
"success.result.progress": "Progress",
"success.submitApplication": "Submit Application",
"success.leaderReview": "Leader Review",
"success.purchaseCertificate": "Purchase Certificate",
"success.safetyTest": "Safety Test",
"success.launched": "Officially launched",
"success.waiting": "Waiting",
"success.processing": "Processing"
}
================================================
FILE: src/locale/en-US/settings.json
================================================
{
"settings.title": "Settings",
"settings.themeColor": "Theme Color",
"settings.content": "Content Setting",
"settings.search": "Search",
"settings.language": "Language",
"settings.navbar": "Navbar",
"settings.menuWidth": "Menu Width (px)",
"settings.navbar.theme.toLight": "Click to use light mode",
"settings.navbar.theme.toDark": "Click to use dark mode",
"settings.navbar.screen.toFull": "Click to switch to full screen mode",
"settings.navbar.screen.toExit": "Click to exit the full screen mode",
"settings.navbar.alerts": "alerts",
"settings.menu": "Menu",
"settings.topMenu": "Top Menu",
"settings.tabBar": "Tab Bar",
"settings.footer": "Footer",
"settings.otherSettings": "Other Settings",
"settings.colorWeak": "Color Weak",
"settings.alertContent": "After the configuration is only temporarily effective, if you want to really affect the project, click the \"Copy Settings\" button below and replace the configuration in settings.json.",
"settings.resetSettings": "Reset Settings",
"settings.copySettings.message": "Copy succeeded, please paste to file src/settings.json.",
"settings.close": "Close",
"settings.color.tooltip": "10 gradient colors generated according to the theme color",
"settings.menuFromServer": "Menu From Server",
"menu.user": "Personal Center",
"menu.user.setting": "User Setting",
"userSetting.menu.title.info": "Personal Information",
"userSetting.menu.title.account": "Account Setting",
"userSetting.menu.title.password": "Password",
"userSetting.menu.title.message": "Message Notification",
"userSetting.menu.title.result": "Result",
"userSetting.menu.title.data": "Export Data",
"userSetting.saveSuccess": "Save Success",
"userSetting.title.basicInfo": "Basic Information",
"userSetting.title.security": "Security Settings",
"userSetting.label.avatar": "Avatar",
"userSetting.label.name": "User Name",
"userSetting.label.accountId": "Account ID",
"userSetting.label.verified": "Whether Verified",
"userSetting.value.verified": "verified",
"userSetting.value.notVerified": "not verified",
"userSetting.label.phoneNumber": "Phone Number",
"userSetting.label.registrationTime": "Registration time",
"userSetting.btn.edit": "Edit",
"userSetting.save": "Save",
"userSetting.reset": "Reset",
"userSetting.info.email": "Email",
"userSetting.info.email.placeholder": "Please enter your email address, such as xxx@bytedance.com",
"userSetting.info.nickName": "Nick name",
"userSetting.info.nickName.placeholder": "Please enter your nickname",
"userSetting.info.area": "Country / Region",
"userSetting.info.area.placeholder": "Please select a country/region",
"userSetting.info.location": "Your location",
"userSetting.info.address": "Specific address",
"userSetting.info.address.placeholder": "Please enter your address",
"userSetting.info.profile": "Personal profile",
"userSetting.info.profile.placeholder": "Please enter your profile, no more than 200 words.",
"userSetting.security.password": "Login Password",
"userSetting.security.password.tips": "Has been set. The password has at least 6 characters, supports numbers, letters and special characters except spaces, and must contain both numbers and uppercase and lowercase letters. ",
"userSetting.security.question": "Secure question",
"userSetting.security.question.placeholder": "You have not set a secret security question, which can effectively protect the security of your account.",
"userSetting.security.phone": "Secure phone",
"userSetting.security.phone.tips": "Bound:",
"userSetting.security.email": "Secure email",
"userSetting.security.email.placeholder": "You have not set up an email address yet. The bound email address can be used to retrieve your password, receive notifications, etc.",
"userSetting.verified.enterprise": "Enterprise real-name certification",
"userSetting.verified.records": "Certification records",
"userSetting.verified.label.accountType": "Account Type",
"userSetting.verified.label.isVerified": "Authentication status",
"userSetting.verified.label.verifiedTime": "Authentication time",
"userSetting.verified.label.legalPersonName": "Legal Person name",
"userSetting.verified.label.certificateType": "Type of legal person certificate",
"userSetting.verified.label.certificationNumber": "Legal person certification number",
"userSetting.verified.label.enterpriseName": "Enterprise Name",
"userSetting.verified.label.enterpriseCertificateType": "Enterprise certificate type",
"userSetting.verified.label.organizationCode": "Organization Code",
"userSetting.verified.authType": "Authentication type",
"userSetting.verified.authContent": "Authentication content",
"userSetting.verified.authStatus": "Current status",
"userSetting.verified.createdTime": "Created time",
"userSetting.verified.operation": "Operation",
"userSetting.verified.operation.view": "View",
"userSetting.verified.operation.revoke": "Revoke",
"userSetting.verified.status.success": "passed",
"userSetting.verified.status.waiting": "under review"
}
================================================
FILE: src/locale/en-US/user.json
================================================
{
"menu.user.info": "User Info",
"userInfo.editUserInfo": "Edit Info",
"userInfo.tab.title.overview": "Overview",
"userInfo.tab.title.project": "Project",
"userInfo.tab.title.team": "My Team",
"userInfo.title.latestActivity": "Latest Activity",
"userInfo.title.latestNotification": "In-site Notification",
"userInfo.title.myProject": "My Project",
"userInfo.showMore": "Show More",
"userInfo.viewAll": "View All",
"userInfo.nodata": "No Data",
"userInfo.visits.unit": "times",
"userInfo.visits.lastMonth": "Last Month",
"menu.user.setting": "User Setting",
"userSetting.menu.title.info": "Personal Information",
"userSetting.menu.title.account": "Account Setting",
"userSetting.menu.title.password": "Password",
"userSetting.menu.title.message": "Message Notification",
"userSetting.menu.title.result": "Result",
"userSetting.menu.title.data": "Export Data",
"userSetting.saveSuccess": "Save Success",
"userSetting.title.basicInfo": "Basic Information",
"userSetting.title.socialInfo": "Social Information",
"userSetting.label.avatar": "Avatar",
"userSetting.label.name": "User Name",
"userSetting.label.location": "Office Location",
"userSetting.label.introduction": "Introduction",
"userSetting.label.personalWebsite": "Website",
"userSetting.save": "Save",
"userSetting.cancel": "Cancel",
"userSetting.reset": "Reset",
"userSetting.label.certification": "Certification",
"userSetting.label.phone": "Phone",
"userSetting.label.accountId": "Account Id",
"userSetting.label.registrationDate": "Registration Date",
"userSetting.tab.basicInformation": "Basic Information",
"userSetting.tab.securitySettings": "Security Settings",
"userSetting.tab.certification": "Certification",
"userSetting.basicInfo.form.label.email": "Email",
"userSetting.basicInfo.placeholder.email": "Please enter your email address, such as xxx{'@'}bytedance.com",
"userSetting.form.error.email.required": "Please enter email address",
"userSetting.basicInfo.form.label.nickname": "Nickname",
"userSetting.basicInfo.placeholder.nickname": "Please enter nickname",
"userSetting.form.error.nickname.required": "Please enter nickname",
"userSetting.basicInfo.form.label.countryRegion": "Country/region",
"userSetting.basicInfo.placeholder.countryRegion": "Please select country/region",
"userSetting.form.error.countryRegion.required": "Please select country/region",
"userSetting.basicInfo.form.label.area": "Area",
"userSetting.basicInfo.placeholder.area": "Please select area",
"userSetting.form.error.area.required": "Please Select a area",
"userSetting.basicInfo.form.label.address": "Address",
"userSetting.basicInfo.placeholder.address": "Please enter address",
"userSetting.basicInfo.form.label.profile": "Personal profile",
"userSetting.basicInfo.placeholder.profile": "Please enter your profile, no more than 200 words",
"userSetting.form.error.profile.maxLength": "No more than 200 words",
"userSetting.SecuritySettings.form.label.password": "Login Password",
"userSetting.SecuritySettings.placeholder.password": "Has been set. The password must contain at least six letters, digits, and special characters except Spaces. The password must contain both uppercase and lowercase letters.",
"userSetting.SecuritySettings.form.label.securityQuestion": "Security Question",
"userSetting.SecuritySettings.placeholder.securityQuestion": "You have not set the password protection question. The password protection question can effectively protect the account security.",
"userSetting.SecuritySettings.form.label.phone": "Phone",
"userSetting.SecuritySettings.form.label.email": "Email",
"userSetting.SecuritySettings.placeholder.email": "You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.",
"userSetting.SecuritySettings.button.settings": "Settings",
"userSetting.SecuritySettings.button.update": "Update",
"userSetting.certification.title.enterprise": "Enterprise Real Name Authentication",
"userSetting.certification.extra.enterprise": "Modifying an Authentication Body",
"userSetting.certification.label.accountType": "Account Type",
"userSetting.certification.label.status": "status",
"userSetting.certification.label.time": "time",
"userSetting.certification.label.legalPerson": "Legal Person Name",
"userSetting.certification.label.certificateType": "Types of legal person documents",
"userSetting.certification.label.authenticationNumber": "Legal person certification number",
"userSetting.certification.label.enterpriseName": "Enterprise Name",
"userSetting.certification.label.enterpriseCertificateType": "Types of corporate certificates",
"userSetting.certification.label.organizationCode": "Organization Code",
"userSetting.certification.title.record": "Certification Records",
"userSetting.certification.columns.certificationType": "Certification Type",
"userSetting.certification.cell.certificationType": "Enterprise certificate Certification",
"userSetting.certification.columns.certificationContent": "Certification Content",
"userSetting.certification.columns.status": "Status",
"userSetting.certification.cell.pass": "Pass",
"userSetting.certification.cell.auditing": "Auditing",
"userSetting.certification.columns.time": "Time",
"userSetting.certification.columns.operation": "Operation",
"userSetting.certification.button.check": "Check",
"userSetting.certification.button.withdraw": "Withdraw"
}
================================================
FILE: src/locale/index.ts
================================================
import { createI18n } from 'vue-i18n'
import { LocalStorageKey, LocaleOptions } from '@/types/constants'
import { get } from 'lodash'
const defaultLocale = localStorage.getItem(LocalStorageKey.localeKey) || LocaleOptions.en
const getMessageFromModules = (_moduleMap: Record) => {
const ret = {}
for (const key in _moduleMap) {
const exportContent = get(_moduleMap[key], 'default')
if (exportContent) {
Object.assign(ret, exportContent)
}
}
return ret
}
const cnMessages = getMessageFromModules(import.meta.glob('./zh-CN/*.json', { eager: true }))
const enMessages = getMessageFromModules(import.meta.glob('./en-US/*.json', { eager: true }))
const i18n = createI18n({
locale: defaultLocale,
fallbackLocale: LocaleOptions.cn,
legacy: false,
allowComposition: true,
messages: {
[LocaleOptions.en]: enMessages,
[LocaleOptions.cn]: cnMessages
}
})
export default i18n
================================================
FILE: src/locale/zh-CN/dashboard.json
================================================
{
"menu.dashboard.monitor": "实时监控",
"monitor.title.chatPanel": "聊天窗口",
"monitor.title.quickOperation": "快捷操作",
"monitor.title.studioInfo": "直播信息",
"monitor.title.studioPreview": "直播预览",
"monitor.chat.options.all": "全部",
"monitor.chat.placeholder.searchCategory": "搜索类目",
"monitor.chat.update": "更新",
"monitor.list.title.order": "序号",
"monitor.list.title.cover": "封面",
"monitor.list.title.name": "名称",
"monitor.list.title.duration": "视频时长",
"monitor.list.title.id": "视频Id",
"monitor.list.tip.rotations": "轮播次数",
"monitor.list.tip.rest": ",节目单观众不可见",
"monitor.list.tag.auditFailed": "审核未通过",
"monitor.tab.title.liveMethod": "直播方式",
"monitor.tab.title.onlinePopulation": "在线人数",
"monitor.liveMethod.normal": "普通直播",
"monitor.liveMethod.flowControl": "控流直播",
"monitor.liveMethod.video": "视频直播",
"monitor.liveMethod.web": "网页开播",
"monitor.editCarousel": "编辑轮播",
"monitor.startCarousel": "开始轮播",
"monitor.quickOperation.changeClarity": "切换清晰度",
"monitor.quickOperation.switchStream": "主备流切换",
"monitor.quickOperation.removeClarity": "摘除清晰度",
"monitor.quickOperation.pushFlowGasket": "推流垫片",
"monitor.studioInfo.label.studioTitle": "直播标题",
"monitor.studioInfo.label.onlineNotification": "上线通知",
"monitor.studioInfo.label.studioCategory": "直播类目",
"monitor.studioInfo.placeholder.studioTitle": "的直播间",
"monitor.studioInfo.btn.fresh": "更新",
"monitor.studioStatus.title.studioStatus": "直播状态",
"monitor.studioStatus.title.pictureInfo": "画面信息",
"monitor.studioStatus.smooth": "流畅",
"monitor.studioStatus.frameRate": "帧率",
"monitor.studioStatus.bitRate": "码率",
"monitor.studioStatus.mainstream": "主流",
"monitor.studioStatus.hotStandby": "热备",
"monitor.studioStatus.coldStandby": "冷备",
"monitor.studioStatus.line": "线路",
"monitor.studioStatus.play": "播放格式",
"monitor.studioStatus.pictureQuality": "画质",
"monitor.studioPreview.studio": "直播间",
"monitor.studioPreview.watching": "在看",
"menu.dashboard.workplace": "工作台",
"workplace.welcome": "欢迎回来!",
"workplace.balance": "余额(元)",
"workplace.order.pending": "待支付",
"workplace.order.pendingRenewal": "待续费订单",
"workplace.onlineContent": "线上总内容",
"workplace.putIn": "投放中内容",
"workplace.newDay": "日新增评论",
"workplace.newFromYesterday": "较昨日新增",
"workplace.minute": "分钟",
"workplace.docs": "帮助文档",
"workplace.docs.productOverview": "产品概要",
"workplace.docs.userGuide": "使用指南",
"workplace.docs.workflow": "接入流程",
"workplace.docs.interfaceDocs": "接口文档",
"workplace.contentManagement": "内容管理",
"workplace.contentStatistical": "内容分析",
"workplace.advanced": "高级管理",
"workplace.onlinePromotion": "线上推广",
"workplace.contentPutIn": "内容投放",
"workplace.announcement": "公告",
"workplace.recently.visited": "最近访问",
"workplace.record.nodata": "暂无数据",
"workplace.quick.operation": "快捷操作",
"workplace.quickOperation.setup": "管理",
"workplace.allProject": "所有项目",
"workplace.loadMore": "加载更多",
"workplace.viewMore": "查看更多",
"workplace.contentData": "内容数据",
"workplace.popularContent": "线上热门内容",
"workplace.popularContent.text": "文本",
"workplace.popularContent.image": "图片",
"workplace.popularContent.video": "视频",
"workplace.categoriesPercent": "内容类型占比",
"workplace.pecs": "个"
}
================================================
FILE: src/locale/zh-CN/exception.json
================================================
{
"menu.exception.403": "403",
"exception.result.403.description": "对不起,您没有访问该资源的权限",
"exception.result.403.back": "返回",
"menu.exception.404": "404",
"exception.result.404.description": "抱歉,页面不见了~",
"exception.result.404.retry": "重试",
"exception.result.404.back": "返回",
"menu.exception.500": "500",
"exception.result.500.description": "抱歉,服务器出了点问题~",
"exception.result.500.back": "返回"
}
================================================
FILE: src/locale/zh-CN/form.json
================================================
{
"menu.form.group": "分组表单",
"groupForm.title.video": "视频参数",
"groupForm.title.audio": "音频参数",
"groupForm.title.description": "填写说明",
"groupForm.form.label.video.mode": "匹配模式",
"groupForm.form.label.video.acquisition.resolution": "采集分辨率",
"groupForm.form.label.video.acquisition.frameRate": "采集帧率",
"groupForm.form.label.video.encoding.resolution": "编码分辨率",
"groupForm.form.label.video.encoding.rate.min": "编码码率最小值",
"groupForm.form.label.video.encoding.rate.max": "编码码率最大值",
"groupForm.form.label.video.encoding.rate.default": "编码码率默认值",
"groupForm.form.label.video.encoding.frameRate": "编码帧率",
"groupForm.form.label.video.encoding.profile": "编码profile",
"groupForm.placeholder.video.mode": "请选择",
"groupForm.placeholder.video.acquisition.resolution": "请选择",
"groupForm.placeholder.video.acquisition.frameRate": "输入范围[1, 30]",
"groupForm.placeholder.video.encoding.resolution": "请选择",
"groupForm.placeholder.video.encoding.rate.min": "输入范围[150, 1800]",
"groupForm.placeholder.video.encoding.rate.max": "输入范围[150, 1800]",
"groupForm.placeholder.video.encoding.rate.default": "输入范围[150, 1800]",
"groupForm.placeholder.video.encoding.frameRate": "输入范围[1, 30]",
"groupForm.placeholder.video.encoding.profile": "输入范围[150, 1800]",
"groupForm.form.label.audio.mode": "匹配模式",
"groupForm.form.label.audio.acquisition.channels": "采集声道数",
"groupForm.form.label.audio.encoding.rate": "编码码率",
"groupForm.form.label.audio.encoding.channels": "编码声道数",
"groupForm.placeholder.audio.encoding.channels": "输入范围[150, 1800]",
"groupForm.form.label.audio.encoding.profile": "编码profile",
"groupForm.placeholder.audio.mode": "请选择",
"groupForm.placeholder.audio.acquisition.channels": "请选择",
"groupForm.placeholder.audio.encoding.rate": "输入范围[150, 1800]",
"groupForm.placeholder.audio.encoding.profile": "输入范围[1, 30]",
"groupForm.form.label.parameterDescription": "参数说明",
"groupForm.placeholder.description": "请填写参数说明,最多不超多200字。",
"groupForm.submit": "提交",
"groupForm.reset": "重置",
"groupForm.submitSuccess": "提交成功",
"menu.form.step": "分步表单",
"stepForm.step.title": "创建渠道表单",
"stepForm.step.title.baseInfo": "选择基本信息",
"stepForm.step.subTitle.baseInfo": "创建渠道活动",
"stepForm.step.title.channel": "输入渠道信息",
"stepForm.step.subTitle.channel": "输入详细的渠道信息",
"stepForm.step.title.finish": "完成创建",
"stepForm.step.subTitle.finish": "创建成功",
"stepForm.success.title": "提交成功",
"stepForm.success.subTitle": "表单提交成功!",
"stepForm.button.next": "下一步",
"stepForm.button.prev": "上一步",
"stepForm.button.submit": "提交",
"stepForm.button.again": "再次创建",
"stepForm.button.view": "查看详情",
"stepForm.form.label.activityName": "活动名称",
"stepForm.placeholder.activityName": "输入汉字、字母或数字,最多20字符",
"stepForm.form.error.activityName.required": "请输入活动名称",
"stepForm.form.error.activityName.pattern": "输入汉字、字母或数字,最多20字符",
"stepForm.form.label.channelType": "渠道类型",
"stepForm.placeholder.channelType": "请选择渠道类型",
"stepForm.form.error.channelType.required": "请选择渠道类型",
"stepForm.form.label.promotionTime": "推广时间",
"stepForm.form.error.promotionTime.required": "请选择推广时间",
"stepForm.form.label.promoteLink": "推广地址",
"stepForm.form.error.promoteLink.required": "请输入推广地址",
"stepForm.form.error.promoteLink.pattern": "如 Android 或 iOS 的下载地址、中间跳转URL,网址必须以 http:// 或 https:// 开头",
"stepForm.form.tip.promoteLink": "如 Android 或 iOS 的下载地址、中间跳转URL,网址必须以 http:// 或 https:// 开头",
"stepForm.placeholder.promoteLink": "请输入推广页面地址",
"stepForm.form.label.advertisingSource": "广告来源",
"stepForm.placeholder.advertisingSource": "引荐来源地址:sohu、sina",
"stepForm.form.error.advertisingSource.required": "请输入广告来源",
"stepForm.form.label.advertisingMedia": "广告媒介",
"stepForm.placeholder.advertisingMedia": "营销媒介:cpc、banner、edm",
"stepForm.form.error.advertisingMedia.required": "请输入广告媒介",
"stepForm.form.label.keyword": "关键词",
"stepForm.placeholder.keyword": "请选择关键词",
"stepForm.form.error.keyword.required": "请选择关键词",
"stepForm.form.label.pushNotify": "推送提醒",
"stepForm.form.label.advertisingContent": "广告内容",
"stepForm.placeholder.advertisingContent": "请输入广告内容介绍,最多不超过200字。",
"stepForm.form.error.advertisingContent.required": "请输入广告内容",
"stepForm.form.error.advertisingContent.maxLength": "最多不超过200字",
"stepForm.form.description.title": "渠道表单说明",
"stepForm.form.description.text": "广告商渠道推广支持追踪在第三方广告商投放广告下载App用户的场景,例如在今日头条渠道投放下载App广告,追踪通过在渠道下载激活App的用户。",
"menu.form": "表单页",
"stepForm.title": "创建渠道表单",
"stepForm.next": "下一步",
"stepForm.prev": "上一步",
"stepForm.title.basicInfo": "基本信息",
"stepForm.desc.basicInfo": "创建活动渠道",
"stepForm.title.channel": "输入渠道信息",
"stepForm.desc.channel": "输入详细的渠道内容",
"stepForm.title.created": "完成创建",
"stepForm.desc.created": "创建成功",
"stepForm.basicInfo.name": "活动名称",
"stepForm.basicInfo.name.required": "请输入活动名称",
"stepForm.basicInfo.name.placeholder": "输入汉字、字母或数字,最多20字符",
"stepForm.basicInfo.channelType": "渠道类型",
"stepForm.basicInfo.channelType.required": "请选择渠道类型",
"stepForm.basicInfo.time": "推广时间",
"stepForm.basicInfo.time.required": "请选择推广时间",
"stepForm.basicInfo.link": "推广地址",
"stepForm.basicInfo.link.placeholder": "请输入推广页面地址",
"stepForm.basicInfo.link.tips": "如 Android 或 iOS 的下载地址、中间跳转URL,网址必须以 http:// 或 https:// 开头",
"stepForm.channel.source": "广告来源",
"stepForm.channel.source.required": "请输入广告来源",
"stepForm.channel.source.placeholder": "引荐来源地址:sohu、sina",
"stepForm.channel.media": "广告媒介",
"stepForm.channel.media.required": "请输入广告媒介",
"stepForm.channel.media.placeholder": "营销媒介:cpc、bannner、edm",
"stepForm.channel.keywords": "关键词",
"stepForm.channel.remind": "推送提醒",
"stepForm.channel.content": "广告内容",
"stepForm.channel.content.required": "请输入广告内容",
"stepForm.channel.content.placeholder": "请输入广告内容介绍,最多不超过200字",
"stepForm.created.success.title": "创建成功",
"stepForm.created.success.desc": "表单创建成功",
"stepForm.created.success.view": "查看表单",
"stepForm.created.success.again": "再次创建",
"stepForm.created.extra.title": "渠道表单说明",
"stepForm.created.extra.desc": "广告商渠道推广支持追踪在第三方广告商投放广告下载App用户的场景,例如在今日头条渠道投放下载App广告,追踪通过在渠道下载激活App的用户。",
"stepForm.created.extra.detail": "查看详情",
"groupForm.title.explanation": "填写说明",
"groupForm.form.label.explanation": "参数说明",
"groupForm.placeholder.explanation": "请填写参数说明,最多不超多200字"
}
================================================
FILE: src/locale/zh-CN/global.json
================================================
{
"menu.dashboard": "仪表盘",
"menu.server.dashboard": "仪表盘-服务端",
"menu.server.workplace": "工作台-服务端",
"menu.server.monitor": "实时监控-服务端",
"menu.list": "列表页",
"menu.result": "结果页",
"menu.exception": "异常页",
"menu.form": "表单页",
"menu.profile": "详情页",
"menu.visualization": "数据可视化",
"menu.user": "个人中心",
"menu.arcoWebsite": "Arco Design",
"menu.faq": "常见问题",
"navbar.docs": "文档中心",
"navbar.action.locale": "切换为中文",
"messageBox.tab.title.message": "消息",
"messageBox.tab.title.notice": "通知",
"messageBox.tab.title.todo": "待办",
"messageBox.tab.button": "清空",
"messageBox.allRead": "全部已读",
"messageBox.viewMore": "查看更多",
"messageBox.noContent": "暂无内容",
"messageBox.switchRoles": "切换角色",
"messageBox.userCenter": "用户中心",
"messageBox.userSettings": "用户设置",
"messageBox.logout": "登出登录",
"navbar.search.placeholder": "输入内容查询"
}
================================================
FILE: src/locale/zh-CN/list.json
================================================
{
"menu.list.cardList": "卡片列表",
"cardList.tab.title.all": "全部",
"cardList.tab.title.content": "内容质检",
"cardList.tab.title.service": "开通服务",
"cardList.tab.title.preset": "规则预置",
"cardList.searchInput.placeholder": "搜索",
"cardList.content.delete": "删除",
"cardList.content.inspection": "质检",
"cardList.content.action": "点击创建质检内容队列",
"cardList.service.open": "开通服务",
"cardList.service.cancel": "取消服务",
"cardList.service.renew": "续约服务",
"cardList.service.tag": "已开通",
"cardList.service.expiresTag": "已过期",
"cardList.preset.tag": "已启用",
"menu.list.searchTable": "查询表格",
"searchTable.form.number": "集合编号",
"searchTable.form.number.placeholder": "请输入集合编号",
"searchTable.form.name": "集合名称",
"searchTable.form.name.placeholder": "请输入集合名称",
"searchTable.form.contentType": "内容体裁",
"searchTable.form.contentType.img": "图文",
"searchTable.form.contentType.horizontalVideo": "横版短视频",
"searchTable.form.contentType.verticalVideo": "竖版小视频",
"searchTable.form.filterType": "筛选方式",
"searchTable.form.filterType.artificial": "人工筛选",
"searchTable.form.filterType.rules": "规则筛选",
"searchTable.form.createdTime": "创建时间",
"searchTable.form.status": "状态",
"searchTable.form.status.online": "已上线",
"searchTable.form.status.offline": "已下线",
"searchTable.form.search": "查询",
"searchTable.form.reset": "重置",
"searchTable.form.selectDefault": "全部",
"searchTable.operation.create": "新建",
"searchTable.operation.import": "批量导入",
"searchTable.operation.download": "下载",
"searchTable.columns.index": "#",
"searchTable.columns.number": "集合编号",
"searchTable.columns.name": "集合名称",
"searchTable.columns.contentType": "内容体裁",
"searchTable.columns.filterType": "筛选方式",
"searchTable.columns.count": "内容量",
"searchTable.columns.createdTime": "创建时间",
"searchTable.columns.status": "状态",
"searchTable.columns.operations": "操作",
"searchTable.columns.operations.view": "查看",
"searchTable.size.mini": "迷你",
"searchTable.size.small": "偏小",
"searchTable.size.medium": "中等",
"searchTable.size.large": "偏大",
"searchTable.actions.refresh": "刷新",
"searchTable.actions.density": "密度",
"searchTable.actions.columnSetting": "列设置",
"menu.list": "列表页",
"searchTable.columns.id": "集合编号",
"searchTable.columns.contentNum": "内容量",
"searchTable.columns.operations.update": "修改",
"searchTable.columns.operations.online": "上线",
"searchTable.columns.operations.offline": "下线",
"searchTable.operations.add": "新建",
"searchTable.operations.upload": "批量导入",
"searchForm.id.placeholder": "请输入集合编号",
"searchForm.name.placeholder": "请输入集合名称",
"searchForm.all.placeholder": "全部"
}
================================================
FILE: src/locale/zh-CN/login.json
================================================
{
"login.form.title": "登录 Vue TSX Admin",
"login.form.userName.errMsg": "用户名不能为空",
"login.form.password.errMsg": "密码不能为空",
"login.form.login.errMsg": "登录出错,轻刷新重试",
"login.form.login.success": "欢迎使用",
"login.form.userName.placeholder": "用户名:admin",
"login.form.password.placeholder": "密码:admin",
"login.form.rememberPassword": "记住密码",
"login.form.forgetPassword": "忘记密码",
"login.form.login": "登录",
"login.form.register": "注册账号",
"login.banner.slogan1": "开箱即用的高质量模板",
"login.banner.subSlogan1": "丰富的的页面模板,覆盖大多数典型业务场景",
"login.banner.slogan2": "内置了常见问题的解决方案",
"login.banner.subSlogan2": "国际化,路由配置,状态管理应有尽有",
"login.banner.slogan3": "使用 TSX 进行项目开发",
"login.banner.subSlogan3": "拓展更加灵活的业务需求"
}
================================================
FILE: src/locale/zh-CN/profile.json
================================================
{
"menu.profile.basic": "基础详情页",
"basicProfile.title.form": "参数审批流程表",
"basicProfile.steps.commit": "提交修改",
"basicProfile.steps.approval": "审批中",
"basicProfile.steps.finish": "修改完成",
"basicProfile.title.currentParams": "修改后参数",
"basicProfile.title.originParams": "原参数",
"basicProfile.title.video": "现视频参数",
"basicProfile.title.preVideo": "原视频参数",
"basicProfile.title.audio": "现音频参数",
"basicProfile.title.preAudio": "原音频参数",
"basicProfile.label.video.mode": "匹配模式",
"basicProfile.label.video.acquisition.resolution": "采集分辨率",
"basicProfile.label.video.acquisition.frameRate": "采集帧率",
"basicProfile.label.video.encoding.resolution": "编码分辨率",
"basicProfile.label.video.encoding.rate.min": "编码码率最小值",
"basicProfile.label.video.encoding.rate.max": "编码码率最大值",
"basicProfile.label.video.encoding.rate.default": "编码码率默认值",
"basicProfile.label.video.encoding.frameRate": "编码帧率",
"basicProfile.label.video.encoding.profile": "编码profile",
"basicProfile.label.audio.mode": "匹配模式",
"basicProfile.label.audio.acquisition.channels": "采集声道数",
"basicProfile.label.audio.encoding.channels": "编码声道数",
"basicProfile.label.audio.encoding.rate": "编码码率",
"basicProfile.label.audio.encoding.profile": "编码 profile",
"basicProfile.unit.audio.channels": "声道",
"basicProfile.goBack": "返回",
"basicProfile.cancel": "取消流程",
"basicProfile.title.operationLog": "参数调整记录",
"basicProfile.column.contentNumber": "内容编号",
"basicProfile.column.updateContent": "调整内容",
"basicProfile.column.status": "当前状态",
"basicProfile.column.updateTime": "修改时间",
"basicProfile.column.operation": "操作",
"basicProfile.cell.pass": "已通过",
"basicProfile.cell.auditing": "审核中",
"basicProfile.cell.view": "查看"
}
================================================
FILE: src/locale/zh-CN/result.json
================================================
{
"menu.result.error": "失败页",
"error.result.title": "提交失败",
"error.result.subTitle": "表单提交失败,请重试。",
"error.result.goBack": "回到首页",
"error.result.retry": "返回修改",
"error.detailTitle": "错误详情",
"error.detailLine.record": "当前域名未备案,备案流程请查看:",
"error.detailLine.record.link": "备案流程",
"error.detailLine.auth": "你的用户组不具有进行此操作的权限;",
"menu.result.success": "成功页",
"success.result.title": "提交成功",
"success.result.subTitle": "表单提交成功!",
"success.result.printResult": "打印结果",
"success.result.projectList": "返回项目列表",
"success.result.progress": "当前进度",
"success.submitApplication": "提交申请",
"success.leaderReview": "直属领导审核",
"success.purchaseCertificate": "购买证书",
"success.safetyTest": "安全测试",
"success.launched": "正式上线",
"success.waiting": "未开始",
"success.processing": "进行中"
}
================================================
FILE: src/locale/zh-CN/settings.json
================================================
{
"settings.title": "页面配置",
"settings.themeColor": "主题色",
"settings.content": "内容区域",
"settings.search": "搜索",
"settings.language": "语言",
"settings.navbar": "导航栏",
"settings.menuWidth": "菜单宽度 (px)",
"settings.navbar.theme.toLight": "点击切换为亮色模式",
"settings.navbar.theme.toDark": "点击切换为暗黑模式",
"settings.navbar.screen.toFull": "点击切换全屏模式",
"settings.navbar.screen.toExit": "点击退出全屏模式",
"settings.navbar.alerts": "消息通知",
"settings.menu": "菜单栏",
"settings.topMenu": "顶部菜单栏",
"settings.tabBar": "多页签",
"settings.footer": "底部",
"settings.otherSettings": "其他设置",
"settings.colorWeak": "色弱模式",
"settings.alertContent": "配置之后仅是临时生效,要想真正作用于项目,点击下方的 \"复制配置\" 按钮,将配置替换到 settings.json 中即可。",
"settings.resetSettings": "重置配置",
"settings.copySettings.message": "复制成功,请粘贴到 src/settings.json 文件中",
"settings.close": "关闭",
"settings.color.tooltip": "根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)",
"settings.menuFromServer": "菜单来源于后台",
"menu.user": "个人中心",
"menu.user.setting": "用户设置",
"userSetting.menu.title.info": "个人信息",
"userSetting.menu.title.account": "账号设置",
"userSetting.menu.title.password": "密码",
"userSetting.menu.title.message": "消息通知",
"userSetting.menu.title.result": "结果页",
"userSetting.menu.title.data": "导出数据",
"userSetting.saveSuccess": "保存成功",
"userSetting.title.basicInfo": "基本信息",
"userSetting.title.security": "安全设置",
"userSetting.label.avatar": "头像",
"userSetting.label.name": "用户名",
"userSetting.label.accountId": "账号ID",
"userSetting.label.verified": "实名认证",
"userSetting.value.verified": "已认证",
"userSetting.value.notVerified": "未认证",
"userSetting.label.phoneNumber": "手机号码",
"userSetting.label.registrationTime": "注册时间",
"userSetting.btn.edit": "修改",
"userSetting.save": "保存",
"userSetting.reset": "重置",
"userSetting.info.email": "邮箱",
"userSetting.info.email.placeholder": "请输入邮箱地址,如xxx@bytedance.com",
"userSetting.info.nickName": "昵称",
"userSetting.info.nickName.placeholder": "请输入您的昵称",
"userSetting.info.area": "国家/地区",
"userSetting.info.area.placeholder": "请选择国家/地区",
"userSetting.info.location": "所在区域",
"userSetting.info.address": "具体地址",
"userSetting.info.address.placeholder": "请输入您的地址",
"userSetting.info.profile": "个人简介",
"userSetting.info.profile.placeholder": "请输入您的个人简介,最多不超过200字。",
"userSetting.security.password": "登陆密码",
"userSetting.security.password.tips": "已设置。密码至少6位字符,支持数字、字母和除空格外的特殊字符,且必须同时包含数字和大小写字母。",
"userSetting.security.question": "密保问题",
"userSetting.security.question.placeholder": "您暂未设置密保问题,密保问题可以有效的保护账号的安全。",
"userSetting.security.phone": "安全手机",
"userSetting.security.phone.tips": "已绑定:",
"userSetting.security.email": "安全邮箱",
"userSetting.security.email.placeholder": "您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。",
"userSetting.verified.enterprise": "企业实名认证",
"userSetting.verified.label.accountType": "账号类型",
"userSetting.verified.label.isVerified": "认证状态",
"userSetting.verified.label.verifiedTime": "认证时间",
"userSetting.verified.label.legalPersonName": "法人姓名",
"userSetting.verified.label.certificateType": "法人证件类型",
"userSetting.verified.label.certificationNumber": "法人认证号码",
"userSetting.verified.label.enterpriseName": "企业名称",
"userSetting.verified.label.enterpriseCertificateType": "企业证件类型",
"userSetting.verified.label.organizationCode": "组织机构代码",
"userSetting.verified.records": "认证记录",
"userSetting.verified.authType": "认证类型",
"userSetting.verified.authContent": "认证内容",
"userSetting.verified.authStatus": "当前状态",
"userSetting.verified.createdTime": "创建时间",
"userSetting.verified.operation": "操作",
"userSetting.verified.operation.view": "查看",
"userSetting.verified.operation.revoke": "撤回",
"userSetting.verified.status.success": "已通过",
"userSetting.verified.status.waiting": "审核中"
}
================================================
FILE: src/locale/zh-CN/user.json
================================================
{
"menu.user.info": "用户信息",
"userInfo.editUserInfo": "编辑信息",
"userInfo.tab.title.overview": "总览",
"userInfo.tab.title.project": "项目",
"userInfo.tab.title.team": "我的团队",
"userInfo.title.latestActivity": "最新动态",
"userInfo.title.latestNotification": "站内通知",
"userInfo.title.myProject": "我的项目",
"userInfo.showMore": "查看更多",
"userInfo.viewAll": "查看全部",
"userInfo.nodata": "暂无数据",
"userInfo.visits.unit": "人次",
"userInfo.visits.lastMonth": "较上月",
"menu.user.setting": "用户设置",
"userSetting.menu.title.info": "个人信息",
"userSetting.menu.title.account": "账号设置",
"userSetting.menu.title.password": "密码",
"userSetting.menu.title.message": "消息通知",
"userSetting.menu.title.result": "结果页",
"userSetting.menu.title.data": "导出数据",
"userSetting.saveSuccess": "保存成功",
"userSetting.title.basicInfo": "基本信息",
"userSetting.title.socialInfo": "社交信息",
"userSetting.label.avatar": "头像",
"userSetting.label.name": "用户名",
"userSetting.label.location": "办公地点",
"userSetting.label.introduction": "个人简介",
"userSetting.label.personalWebsite": "个人网站",
"userSetting.save": "保存",
"userSetting.cancel": "取消",
"userSetting.reset": "重置",
"userSetting.label.certification": "实名认证",
"userSetting.label.phone": "手机号码",
"userSetting.label.accountId": "账号ID",
"userSetting.label.registrationDate": "注册时间",
"userSetting.tab.basicInformation": "基础信息",
"userSetting.tab.securitySettings": "安全设置",
"userSetting.tab.certification": "实名认证",
"userSetting.basicInfo.form.label.email": "邮箱",
"userSetting.basicInfo.placeholder.email": "请输入邮箱地址,如xxx{'@'}bytedance.com",
"userSetting.form.error.email.required": "请输入邮箱",
"userSetting.basicInfo.form.label.nickname": "昵称",
"userSetting.basicInfo.placeholder.nickname": "请输入您的昵称",
"userSetting.form.error.nickname.required": "请输入昵称",
"userSetting.basicInfo.form.label.countryRegion": "国家/地区",
"userSetting.basicInfo.placeholder.countryRegion": "请选择",
"userSetting.form.error.countryRegion.required": "请选择国家/地区",
"userSetting.basicInfo.form.label.area": "所在区域",
"userSetting.basicInfo.placeholder.area": "请选择",
"userSetting.form.error.area.required": "请选择所在区域",
"userSetting.basicInfo.form.label.address": "具体地址",
"userSetting.basicInfo.placeholder.address": "请输入您的地址",
"userSetting.basicInfo.form.label.profile": "个人简介",
"userSetting.basicInfo.placeholder.profile": "请输入您的个人简介,最多不超过200字。",
"userSetting.form.error.profile.maxLength": "最多不超过200字",
"userSetting.SecuritySettings.form.label.password": "登录密码",
"userSetting.SecuritySettings.placeholder.password": "已设置。密码至少6位字符,支持数字、字母和除空格外的特殊字符,且必须同时包含数字和大小写字母。",
"userSetting.SecuritySettings.form.label.securityQuestion": "密保问题",
"userSetting.SecuritySettings.placeholder.securityQuestion": "您暂未设置密保问题,密保问题可以有效的保护账号的安全。",
"userSetting.SecuritySettings.form.label.phone": "安全手机",
"userSetting.SecuritySettings.form.label.email": "安全邮箱",
"userSetting.SecuritySettings.placeholder.email": "您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。",
"userSetting.SecuritySettings.button.settings": "设置",
"userSetting.SecuritySettings.button.update": "修改",
"userSetting.certification.title.enterprise": "企业实名认证",
"userSetting.certification.extra.enterprise": "修改认证主体",
"userSetting.certification.label.accountType": "账号类型",
"userSetting.certification.label.status": "认证状态",
"userSetting.certification.label.time": "认证时间",
"userSetting.certification.label.legalPerson": "法人姓名",
"userSetting.certification.label.certificateType": "法人证件类型",
"userSetting.certification.label.authenticationNumber": "法人认证号码",
"userSetting.certification.label.enterpriseName": "企业名称",
"userSetting.certification.label.enterpriseCertificateType": "企业证件类型",
"userSetting.certification.label.organizationCode": "组织机构代码",
"userSetting.certification.title.record": "认证记录",
"userSetting.certification.columns.certificationType": "认证类型",
"userSetting.certification.cell.certificationType": "企业证件认证",
"userSetting.certification.columns.certificationContent": "认证内容",
"userSetting.certification.columns.status": "当前状态",
"userSetting.certification.cell.pass": "已通过",
"userSetting.certification.cell.auditing": "审核中",
"userSetting.certification.columns.time": "创建时间",
"userSetting.certification.columns.operation": "操作",
"userSetting.certification.button.check": "查看",
"userSetting.certification.button.withdraw": "撤回"
}
================================================
FILE: src/main.ts
================================================
import { createApp } from 'vue'
import ArcoVue from '@arco-design/web-vue'
// attention import css sort
// arco component css has normalize css so doesn't need in this project
import '@arco-design/web-vue/dist/arco.css'
import './assets/style/index.scss'
import App from './App'
import router from './router'
import '@/mock'
import i18n from './locale'
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import store from '@/store/index'
import '@/api/interceptors'
const app = createApp(App)
app.use(ArcoVueIcon)
app.use(ArcoVue)
app.use(i18n)
app.use(store)
app.use(router)
app.mount('#app')
================================================
FILE: src/mock/index.ts
================================================
import Mock from 'mockjs'
import.meta.glob('./modules/*.ts', {
eager: true
})
Mock.setup({
timeout: '100-300'
})
================================================
FILE: src/mock/modules/dashboardMock.ts
================================================
import Mock from 'mockjs'
import dayjs from 'dayjs'
import qs from 'query-string'
import setupMock, { successResponseWrap } from '@/mock/setupMock'
import type { GetParams } from '@/types/global'
const textList = [
{
key: 1,
clickNumber: '346.3w+',
title: '经济日报:财政政策要精准提升…',
increases: 35
},
{
key: 2,
clickNumber: '324.2w+',
title: '双12遇冷,消费者厌倦了电商平…',
increases: 22
},
{
key: 3,
clickNumber: '318.9w+',
title: '致敬坚守战“疫”一线的社区工作…',
increases: 9
},
{
key: 4,
clickNumber: '257.9w+',
title: '普高还是职高?家长们陷入选择…',
increases: 17
},
{
key: 5,
clickNumber: '124.2w+',
title: '人民快评:没想到“浓眉大眼”的…',
increases: 37
}
]
const imageList = [
{
key: 1,
clickNumber: '15.3w+',
title: '杨涛接替陆慷出任外交部美大司…',
increases: 15
},
{
key: 2,
clickNumber: '12.2w+',
title: '图集:龙卷风袭击美国多州房屋…',
increases: 26
},
{
key: 3,
clickNumber: '18.9w+',
title: '52岁大姐贴钱照顾自闭症儿童八…',
increases: 9
},
{
key: 4,
clickNumber: '7.9w+',
title: '杭州一家三口公园宿营取暖中毒',
increases: 0
},
{
key: 5,
clickNumber: '5.2w+',
title: '派出所副所长威胁市民?警方调…',
increases: 4
}
]
const videoList = [
{
key: 1,
clickNumber: '367.6w+',
title: '这是今日10点的南京',
increases: 5
},
{
key: 2,
clickNumber: '352.2w+',
title: '立陶宛不断挑衅致经济受损民众…',
increases: 17
},
{
key: 3,
clickNumber: '348.9w+',
title: '韩国艺人刘在石确诊新冠',
increases: 30
},
{
key: 4,
clickNumber: '346.3w+',
title: '关于北京冬奥会,文在寅表态',
increases: 12
},
{
key: 5,
clickNumber: '271.2w+',
title: '95后现役军人荣立一等功',
increases: 2
}
]
setupMock({
setup() {
Mock.mock(new RegExp('/api/content-data'), () => {
const presetData = [58, 81, 53, 90, 64, 88, 49, 79]
const getLineData = () => {
const count = 8
return new Array(count).fill(0).map((el, idx) => ({
x: dayjs()
.day(idx - 2)
.format('YYYY-MM-DD'),
y: presetData[idx]
}))
}
return successResponseWrap([...getLineData()])
})
Mock.mock(new RegExp('/api/popular/list'), (params: GetParams) => {
const { type = 'text' } = qs.parseUrl(params.url).query
if (type === 'image') {
return successResponseWrap([...videoList])
}
if (type === 'video') {
return successResponseWrap([...imageList])
}
return successResponseWrap([...textList])
})
Mock.mock(new RegExp('/api/chat/list'), () => {
const data = Mock.mock({
'data|4-6': [
{
'id|+1': 1,
username: '用户7352772',
content: '马上就开始了,好激动!',
time: '13:09:12',
'isCollect|2': true
}
]
})
return successResponseWrap(data.data)
})
}
})
================================================
FILE: src/mock/modules/formMock.ts
================================================
import Mock from 'mockjs'
import setupMock, { successResponseWrap } from '@/mock/setupMock'
setupMock({
setup() {
Mock.mock(new RegExp('/api/channel-form/submit'), () => {
return successResponseWrap('ok')
})
Mock.mock(new RegExp('/api/channel-form/group'), () => {
return successResponseWrap('ok')
})
}
})
================================================
FILE: src/mock/modules/listMock.ts
================================================
import Mock from 'mockjs'
import setupMock, { successResponseWrap } from '@/mock/setupMock'
import { type ServiceRecord } from '@/api/list'
import qs from 'query-string'
import { type GetParams } from '@/types/global'
const { Random } = Mock
const data = Mock.mock({
'list|55': [
{
'id|8': /[A-Z][a-z][-][0-9]/,
'number|2-3': /[0-9]/,
'name|4-8': /[A-Z]/,
'contentType|1': ['img', 'horizontalVideo', 'verticalVideo'],
'count|2-3': /[0-9]/,
'status|1': ['online', 'offline'],
'filterType|1': ['artificial', 'rules'],
createdTime: Random.datetime()
}
]
})
const qualityInspectionList: ServiceRecord[] = [
{
id: 1,
name: 'quality',
title: '视频类-历史导入',
description: '2021-10-12 00:00:00',
data: [
{
label: '待质检数',
value: '120'
},
{
label: '积压时长',
value: '60s'
},
{
label: '待抽检数',
value: '0'
}
]
},
{
id: 2,
name: 'quality',
title: '图文类-图片版权',
description: '2021-12-11 18:30:00',
data: [
{
label: '待质检数',
value: '120'
},
{
label: '积压时长',
value: '60s'
},
{
label: '待抽检数',
value: '0'
}
]
},
{
id: 3,
name: 'quality',
title: '图文类-高清图片',
description: '2021-10-15 08:10:00',
data: [
{
label: '待质检数',
value: '120'
},
{
label: '积压时长',
value: '60s'
},
{
label: '待抽检数',
value: '0'
}
]
}
]
const theServiceList: ServiceRecord[] = [
{
id: 1,
icon: 'code',
title: '漏斗分析',
description:
'用户行为分析之漏斗分析模型是企业实现精细化运营、进行用户行为分析的重要数据分析模型。',
enable: true,
actionType: 'button'
},
{
id: 2,
icon: 'edit',
title: '用户分布',
description:
'快速诊断用户人群,地域细分情况,了解数据分布的集中度,以及主要的数据分布的区间段是什么。',
enable: true,
actionType: 'button',
expires: true
},
{
id: 3,
icon: 'user',
title: '资源分发',
description:
'移动端动态化资源分发解决方案。提供稳定大流量服务支持、灵活定制的分发圈选规则,通过离线化预加载。',
enable: false,
actionType: 'button'
},
{
id: 4,
icon: 'user',
title: '用户画像分析',
description:
'用户画像就是将典型用户信息标签化,根据用户特征、业务场景和用户行为等信息,构建一个标签化的用户模型。',
enable: true,
actionType: 'button'
}
]
const rulesPresetList: ServiceRecord[] = [
{
id: 1,
title: '内容屏蔽规则',
description: '用户在执行特定的内容分发任务时,可使用内容屏蔽规则根据特定标签,过滤内容集合。',
enable: true,
actionType: 'switch'
},
{
id: 2,
title: '内容置顶规则',
description: '该规则支持用户在执行特定内容分发任务时,对固定的几条内容置顶。',
enable: true,
actionType: 'switch'
},
{
id: 3,
title: '内容加权规则',
description: '选定内容加权规则后可自定义从不同内容集合获取内容的概率。',
enable: false,
actionType: 'switch'
},
{
id: 4,
title: '内容分发规则',
description: '内容分发时,对某些内容需要固定在C端展示的位置。',
enable: true,
actionType: 'switch'
},
{
id: 5,
title: '违禁内容识别',
description: '精准识别赌博、刀枪、毒品、造假、贩假等违规物品和违规行为。',
enable: false,
actionType: 'switch'
},
{
id: 6,
title: '多语言文字符号识别',
description: '精准识别英语、维语、藏语、蒙古语、朝鲜语等多种语言以及emoji表情形态的语义识别。',
enable: false,
actionType: 'switch'
}
]
setupMock({
setup() {
// Quality Inspection
Mock.mock(new RegExp('/api/list/quality-inspection'), () => {
return successResponseWrap(
qualityInspectionList.map((_, index) => ({
...qualityInspectionList[index % qualityInspectionList.length],
id: Mock.Random.guid()
}))
)
})
// the service
Mock.mock(new RegExp('/api/list/the-service'), () => {
return successResponseWrap(
theServiceList.map((_, index) => ({
...theServiceList[index % theServiceList.length],
id: Mock.Random.guid()
}))
)
})
// rules preset
Mock.mock(new RegExp('/api/list/rules-preset'), () => {
return successResponseWrap(
rulesPresetList.map((_, index) => ({
...rulesPresetList[index % rulesPresetList.length],
id: Mock.Random.guid()
}))
)
})
Mock.mock(new RegExp('/api/list/policy'), (params: GetParams) => {
const { current = 1, pageSize = 10 } = qs.parseUrl(params.url).query
const p = current as number
const ps = pageSize as number
return successResponseWrap({
list: data.list.slice((p - 1) * ps, p * ps),
total: 55
})
})
}
})
================================================
FILE: src/mock/modules/profileMock.ts
================================================
import Mock from 'mockjs'
import setupMock, { successResponseWrap } from '@/mock/setupMock'
setupMock({
setup() {
Mock.mock(new RegExp('/api/profile/basic'), () => {
return successResponseWrap({
status: 2,
video: {
mode: '自定义',
acquisition: {
resolution: '720*1280',
frameRate: 15
},
encoding: {
resolution: '720*1280',
rate: {
min: 300,
max: 800,
default: 1500
},
frameRate: 15,
profile: 'high'
}
},
audio: {
mode: '自定义',
acquisition: {
channels: 8
},
encoding: {
channels: 8,
rate: 128,
profile: 'ACC-LC'
}
}
})
})
Mock.mock(new RegExp('/api/operation/log'), () => {
return successResponseWrap([
{
key: '1',
contentNumber: '视频类001003',
updateContent: '视频参数变更',
status: 0,
updateTime: '2021-02-28 10:30:50'
},
{
key: '2',
contentNumber: '视频类058212',
updateContent: '视频参数变更;音频参数变更',
status: 1,
updateTime: '2020-05-13 08:00:00'
}
])
})
}
})
================================================
FILE: src/mock/modules/userMock.ts
================================================
import Mock from 'mockjs'
import { isLogin } from '@/utils/token'
import setupMock, { successResponseWrap, failResponseWrap } from '@/mock/setupMock'
import type { GetParams } from '@/types/global'
import { ResCode } from '@/types/constants'
setupMock({
setup() {
// 用户信息
Mock.mock(new RegExp('/api/user/info'), () => {
if (isLogin()) {
const role = window.localStorage.getItem('data-base-role') || 'admin'
return successResponseWrap({
name: '蔓越莓曲奇',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/54520846%20(1).jpg',
email: '1486498123@email.com',
job: 'frontend',
jobName: '前端开发',
organization: 'Frontend',
organizationName: '前端',
location: 'hangzhou',
locationName: '杭州',
introduction: '人潇洒,性温存',
personalWebsite: 'https://www.arco.design',
phone: '150****0000',
registrationDate: '2013-05-10 12:10:00',
accountId: '15012312300',
certification: 1,
role
})
}
return failResponseWrap(null, '未登录', ResCode.illegalToken)
})
// 登录
Mock.mock(new RegExp('/api/user/login'), (params: GetParams) => {
const { username, password } = JSON.parse(params.body)
if (!username) {
return failResponseWrap(null, '用户名不能为空', ResCode.error)
}
if (!password) {
return failResponseWrap(null, '密码不能为空', ResCode.error)
}
if (username === 'admin' && password === 'admin') {
window.localStorage.setItem('data-base-role', 'admin')
return successResponseWrap({
token: '12345'
})
}
if (username === 'user' && password === 'user') {
window.localStorage.setItem('data-base-role', 'user')
return successResponseWrap({
token: '54321'
})
}
return failResponseWrap(null, '账号或者密码错误', ResCode.error)
})
// 登出
Mock.mock(new RegExp('/api/user/logout'), () => {
return successResponseWrap(null)
})
// 最新项目
Mock.mock(new RegExp('/api/user/my-project/list'), () => {
const contributors = [
{
name: '秦臻宇',
email: 'qingzhenyu@arco.design',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar3.webp'
},
{
name: '于涛',
email: 'yuebao@arco.design',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar3.webp'
},
{
name: '宁波',
email: 'ningbo@arco.design',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar2.webp'
},
{
name: '郑曦月',
email: 'zhengxiyue@arco.design',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar1.webp'
},
{
name: '宁波',
email: 'ningbo@arco.design',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar2.webp'
}
]
const units = [
{
name: '企业级产品设计系统',
description: 'Arco Design System'
},
{
name: '火山引擎智能应用',
description: 'The Volcano Engine'
},
{
name: 'OCR文本识别',
description: 'OCR text recognition'
},
{
name: '内容资源管理',
description: 'Content resource management '
},
{
name: '今日头条内容管理',
description: 'Toutiao content management'
},
{
name: '智能机器人',
description: 'Intelligent Robot Project'
}
]
return successResponseWrap(
new Array(6).fill(null).map((_item, index) => ({
id: index,
name: units[index].name,
description: units[index].description,
peopleNumber: Mock.Random.natural(10, 1000),
contributors
}))
)
})
// 最新动态
Mock.mock(new RegExp('/api/user/latest-activity'), () => {
return successResponseWrap(
new Array(7).fill(null).map((_item, index) => ({
id: index,
title: '发布了项目 Arco Design System',
description: '企业级产品设计系统',
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar3.webp'
}))
)
})
// 访问量
Mock.mock(new RegExp('/api/user/visits'), () => {
return successResponseWrap([
{
name: '主页访问量',
visits: 5670,
growth: 206.32
},
{
name: '项目访问量',
visits: 5670,
growth: 206.32
}
])
})
// 项目和团队列表
Mock.mock(new RegExp('/api/user/project-and-team/list'), () => {
return successResponseWrap([
{
id: 1,
content: '他创建的项目'
},
{
id: 2,
content: '他参与的项目'
},
{
id: 3,
content: '他创建的团队'
},
{
id: 4,
content: '他加入的团队'
}
])
})
// 团队列表
Mock.mock(new RegExp('/api/user/my-team/list'), () => {
return successResponseWrap([
{
id: 1,
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar3.webp',
name: '火山引擎智能应用团队',
peopleNumber: Mock.Random.natural(10, 100)
},
{
id: 2,
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar2.webp',
name: '企业级产品设计团队',
peopleNumber: Mock.Random.natural(5000, 6000)
},
{
id: 3,
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar2.webp',
name: '前端/UE小分队',
peopleNumber: Mock.Random.natural(10, 5000)
},
{
id: 4,
avatar: 'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/avatar1.webp',
name: '内容识别插件小分队',
peopleNumber: Mock.Random.natural(10, 100)
}
])
})
Mock.mock(new RegExp('/api/user/certification'), () => {
return successResponseWrap({
enterpriseInfo: {
accountType: '企业账号',
status: 0,
time: '2018-10-22 14:53:12',
legalPerson: '李**',
certificateType: '中国身份证',
authenticationNumber: '130************123',
enterpriseName: '低调有实力的企业',
enterpriseCertificateType: '企业营业执照',
organizationCode: '7*******9'
},
record: [
{
certificationType: 1,
certificationContent: '企业实名认证,法人姓名:李**',
status: 0,
time: '2021-02-28 10:30:50'
},
{
certificationType: 1,
certificationContent: '企业实名认证,法人姓名:李**',
status: 1,
time: '2020-05-13 08:00:00'
}
]
})
})
Mock.mock(new RegExp('/api/user/upload'), () => {
return successResponseWrap('ok')
})
Mock.mock(new RegExp('/api/user/save-info'), () => {
return successResponseWrap('ok')
})
Mock.mock(new RegExp('/api/user/switch-user-role'), () => {
const role = window.localStorage.getItem('data-base-role') || 'admin'
const roleValue = role === 'admin' ? 'user' : 'admin'
window.localStorage.setItem('data-base-role', roleValue)
return successResponseWrap({
role: roleValue
})
})
}
})
================================================
FILE: src/mock/setupMock.ts
================================================
import { ResCode } from '@/types/constants'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isDEV = import.meta.env.MODE === 'development'
export default ({ mock, setup }: { mock?: boolean; setup: () => void }) => {
// if (!mock && isDEV) setup() // correct
if (!mock) setup()
}
export const successResponseWrap = (data: unknown) => {
return {
data,
status: 'ok',
msg: '请求成功',
code: ResCode.success
}
}
export const failResponseWrap = (data: unknown, msg: string, code = ResCode.error) => {
return {
data,
status: 'fail',
msg,
code
}
}
================================================
FILE: src/router/guard/index.ts
================================================
import { setRouteEmitter } from '@/utils/routerListener'
import type { Router } from 'vue-router'
import setupUserLoginInfoGuard from './login'
import setupPermissionGuard from './permission'
/**
*
* @param desc emit router change
*/
function setupPageGuard(router: Router) {
router.beforeEach((to) => {
setRouteEmitter(to)
})
}
export default function configRouteGuard(router: Router) {
setupPageGuard(router)
setupUserLoginInfoGuard(router)
setupPermissionGuard(router)
}
================================================
FILE: src/router/guard/login.ts
================================================
import type { Router } from 'vue-router'
import NProgress from 'nprogress'
import { useUserStore } from '@/store'
import { isLogin } from '@/utils/token'
import { ViewNames } from '@/types/constants'
import useAuth from '@/hooks/auth'
/**
*
* @desc userInfo and token guard
* - no token to login view
* - has token check userInfo
* - - has userInfo go
* - - no userInfo update info go
*/
export default function setupUserLoginInfoGuard(router: Router) {
const { logoutApp } = useAuth()
router.beforeEach(async (to, _from, next) => {
NProgress.start()
const userStore = useUserStore()
if (isLogin()) {
if (userStore.role) {
next()
} else {
try {
await userStore.refreshUserInfo()
next()
} catch (error) {
await logoutApp()
next({
name: ViewNames.login
})
}
}
} else {
if (to.name === ViewNames.login) {
next()
return
}
next({
name: ViewNames.login
})
}
})
}
================================================
FILE: src/router/guard/permission.ts
================================================
import NProgress from 'nprogress' // progress bar
import type { RouteRecord, Router } from 'vue-router'
import usePermission from '@/hooks/permission'
import { ViewNames } from '@/types/constants'
import { firstPermissionRoute } from '@/hooks/appRoute'
export default function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const Permission = usePermission()
const permissionsAllow = Permission.checkRoutePermission(to as unknown as RouteRecord)
// eslint-disable-next-line no-lonely-if
if (permissionsAllow) next()
else {
const destination = firstPermissionRoute?.name || ViewNames.notFound
next({ name: destination })
}
NProgress.done()
})
}
================================================
FILE: src/router/index.ts
================================================
import { createRouter, createWebHashHistory } from 'vue-router'
import { ViewNames } from '@/types/constants'
import configRouteGuard from './guard'
import { appRoutes } from './routes'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false })
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: { name: ViewNames.login }
},
{
path: '/login',
name: ViewNames.login,
component: () => import('@/views/login/index'),
meta: {
requiresAuth: false
}
},
...appRoutes,
{
path: '/redirect',
meta: {
requiresAuth: true
},
children: [
{
path: '/redirect/:path',
name: ViewNames.redirect,
component: () => import('@/views/redirect/index'),
meta: {
requiresAuth: true
}
}
]
},
{
path: '/:pathMatch(.*)*',
name: ViewNames.notFound,
component: () => import('@/views/not-found/index')
}
]
})
configRouteGuard(router)
export default router
================================================
FILE: src/router/routes/index.ts
================================================
// https://vitejs.dev/guide/features.html#glob-import
import { get } from 'lodash'
import type { RouteRecordRaw } from 'vue-router'
const innerModules = import.meta.glob('./modules/*.ts', { eager: true })
const getRoutesFromModules = (_moduleMap: Record) => {
const ret: RouteRecordRaw[] = []
for (const key in _moduleMap) {
const exportContent = get(_moduleMap[key], 'default')
if (exportContent) {
ret.push(...[exportContent])
}
}
return ret
}
export const appRoutes: RouteRecordRaw[] = getRoutesFromModules(innerModules)
================================================
FILE: src/router/routes/modules/dashboard.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/dashboard',
name: ViewNames.dashboard,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true
},
children: [
{
path: 'workplace',
name: ViewNames.workplace,
component: () => import('@/views/dashboard/workplace/index'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['*']
}
},
{
path: 'monitor',
name: ViewNames.monitor,
component: () => import('@/views/dashboard/monitor/index'),
meta: {
locale: 'menu.dashboard.monitor',
requiresAuth: true,
roles: ['admin']
}
}
]
}
================================================
FILE: src/router/routes/modules/exception.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/exception',
name: ViewNames.exception,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.exception',
requiresAuth: true
},
children: [
{
path: '403',
name: ViewNames._403,
component: () => import('@/views/exception/403/index'),
meta: {
locale: 'menu.exception.403',
requiresAuth: true,
roles: ['admin']
}
},
{
path: '404',
name: ViewNames._404,
component: () => import('@/views/exception/404/index'),
meta: {
locale: 'menu.exception.404',
requiresAuth: true,
roles: ['*']
}
},
{
path: '500',
name: ViewNames._500,
component: () => import('@/views/exception/500/index'),
meta: {
locale: 'menu.exception.500',
requiresAuth: true,
roles: ['*']
}
}
]
}
================================================
FILE: src/router/routes/modules/form.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/form',
name: ViewNames.form,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.form',
icon: 'icon-settings',
requiresAuth: true
},
children: [
{
path: 'step',
name: ViewNames.step,
component: () => import('@/views/form/step/index'),
meta: {
locale: 'menu.form.step',
requiresAuth: true,
roles: ['admin']
}
},
{
path: 'group',
name: ViewNames.group,
component: () => import('@/views/form/group/index'),
meta: {
locale: 'menu.form.group',
requiresAuth: true,
roles: ['admin']
}
}
]
}
================================================
FILE: src/router/routes/modules/list.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/list',
name: ViewNames.list,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.list',
requiresAuth: true,
icon: 'icon-list'
},
children: [
{
path: 'search-table',
name: ViewNames.searchTable,
component: () => import('@/views/list/search-table/index'),
meta: {
locale: 'menu.list.searchTable',
requiresAuth: true,
roles: ['*']
}
},
{
path: 'card',
name: ViewNames.cardList,
component: () => import('@/views/list/card-list/index'),
meta: {
locale: 'menu.list.cardList',
requiresAuth: true,
roles: ['*']
}
}
]
}
================================================
FILE: src/router/routes/modules/profile.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/profile',
name: ViewNames.profile,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.profile',
requiresAuth: true
},
children: [
{
path: 'basic',
name: 'Basic',
component: () => import('@/views/profile/index'),
meta: {
locale: 'menu.profile.basic',
requiresAuth: true,
roles: ['admin']
}
}
]
}
================================================
FILE: src/router/routes/modules/result.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/result',
name: ViewNames.result,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.result',
icon: 'icon-check-circle',
requiresAuth: true
},
children: [
{
path: 'success',
name: ViewNames.success,
component: () => import('@/views/result/success'),
meta: {
locale: 'menu.result.success',
requiresAuth: true,
roles: ['admin']
}
},
{
path: 'error',
name: ViewNames.error,
component: () => import('@/views/result/error'),
meta: {
locale: 'menu.result.error',
requiresAuth: true,
roles: ['admin']
}
}
]
}
================================================
FILE: src/router/routes/modules/user.ts
================================================
import { ViewNames } from '@/types/constants'
export default {
path: '/user',
name: ViewNames.user,
component: () => import('@/components/layout-component/index'),
meta: {
locale: 'menu.user',
icon: 'icon-user',
requiresAuth: true
},
children: [
{
path: 'info',
name: ViewNames.info,
component: () => import('@/views/user/info/index'),
meta: {
locale: 'menu.user.info',
requiresAuth: true,
roles: ['*']
}
},
{
path: 'setting',
name: ViewNames.setting,
component: () => import('@/views/user/setting/index'),
meta: {
locale: 'menu.user.setting',
requiresAuth: true,
roles: ['*']
}
}
]
}
================================================
FILE: src/store/index.ts
================================================
import { createPinia } from 'pinia'
import useAppStore from './modules/app'
import useTabStore from './modules/tab'
import useUserStore from './modules/user'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export { useAppStore, useTabStore, useUserStore }
export default pinia
================================================
FILE: src/store/modules/app/index.ts
================================================
import { defineStore } from 'pinia'
import { AppTheme } from '@/types/constants'
export type AppState = {
theme: AppTheme
colorWeak: boolean
navbar: boolean
menu: boolean
menuCollapse: boolean
footer: boolean
themeColor: string
menuWidth: number
settingVisible: boolean
[key: string]: unknown
}
export default defineStore('appStore', {
state(): AppState {
return {
theme: AppTheme.light,
colorWeak: false,
navbar: true,
menu: true,
menuCollapse: false,
footer: true,
themeColor: '#165DFF',
menuWidth: 220,
settingVisible: false
}
},
persist: true,
getters: {
isDark(state: AppState) {
return state.theme === AppTheme.dark
}
},
actions: {
updateSettings(partial: Partial) {
// @ts-ignore-next-line
this.$patch(partial)
},
resetSetting() {
this.$reset()
},
toggleDarkLightMode() {
if (this.isDark) {
this.theme = AppTheme.light
} else {
this.theme = AppTheme.dark
}
}
}
})
================================================
FILE: src/store/modules/tab/index.ts
================================================
import type { RouteLocationNormalized } from 'vue-router'
import { defineStore } from 'pinia'
import { ViewNames } from '@/types/constants'
import { cloneDeep } from 'lodash'
import { firstPermissionRoute } from '@/hooks/appRoute'
const BAN_LIST = [ViewNames.redirect, ViewNames.notFound]
export type TabItem = {
title: string
name: string
fullPath: string
}
const formatRoute = (route: RouteLocationNormalized): TabItem => {
const { name, meta, fullPath } = route
return {
title: meta.locale + '' || '',
name: String(name),
fullPath
}
}
const firstRoute = cloneDeep(firstPermissionRoute)
export const defaultTab = {
title: firstRoute.title,
name: firstRoute.name,
fullPath: firstRoute.fullPath
}
// tab list doesn't process permission limit, if you want to use in project, add test case according to requirement and develop
export default defineStore('tabStore', {
state: (): {
tabList: TabItem[]
} => ({
tabList: [defaultTab]
}),
getters: {
getCacheList(state) {
return Array.from(new Set(state.tabList.map((item) => item.name)))
}
},
actions: {
updateTabList(route: RouteLocationNormalized) {
if (BAN_LIST.includes(route.name as ViewNames)) return
this.tabList.push(formatRoute(route))
},
deleteTab(name: ViewNames) {
const idx = this.tabList.findIndex((item) => item.name === name)
this.tabList.splice(idx, 1)
},
freshTabList(tabs: TabItem[]) {
this.tabList = tabs
},
resetTabList() {
this.$reset()
}
}
})
================================================
FILE: src/store/modules/user/index.ts
================================================
import { defineStore } from 'pinia'
import { getUserInfo, requestSwitchRole, type UserInfo } from '@/api/user'
export default defineStore('userStore', {
state: (): UserInfo => ({
name: undefined,
avatar: undefined,
job: undefined,
organization: undefined,
location: undefined,
email: undefined,
introduction: undefined,
personalWebsite: undefined,
jobName: undefined,
organizationName: undefined,
locationName: undefined,
phone: undefined,
registrationDate: undefined,
accountId: undefined,
certification: undefined,
role: ''
}),
actions: {
setUserInfo(payload: Partial) {
this.$patch(payload)
},
resetUserInfo() {
this.$reset()
},
async switchRoles() {
try {
const res = await requestSwitchRole()
const role = res.data.role
this.role = role
return role
} catch (e) {
/* empty */
}
},
async refreshUserInfo() {
const res = await getUserInfo()
this.setUserInfo(res.data)
}
}
})
================================================
FILE: src/types/constants.ts
================================================
export enum LocaleOptions {
cn = 'zh-CN',
en = 'en-US'
}
export enum LocalStorageKey {
localeKey = 'VUE_TSX_ADMIN_LOCALE',
loginFormKey = 'VUE_TSX_ADMIN_LOGIN_FORM_INFO',
tokenKey = 'VUE_TSX_ADMIN_TOKEN'
}
export enum AppTheme {
light = 'light',
dark = 'dark'
}
export enum ApplicationInfo {
appTitle = import.meta.env.VITE_APP_TITLE
}
// component name
// route name
// keepalive require component name to same
export enum ViewNames {
login = 'login',
redirect = 'redirect',
notFound = 'notFound',
// =============== DIVIDER ==================
dashboard = 'dashboard',
workplace = 'workplace',
monitor = 'monitor',
// =============== DIVIDER ==================
exception = 'exception',
_403 = '403',
_404 = '404',
_500 = '500',
// =============== DIVIDER ==================
form = 'form',
step = 'step',
group = 'group',
// =============== DIVIDER ==================
profile = 'profile',
// =============== DIVIDER ==================
list = 'list',
searchTable = 'searchTable',
cardList = 'cardList',
// =============== DIVIDER ==================
result = 'result',
success = 'success',
error = 'error',
// =============== DIVIDER ==================
user = 'user',
info = 'info',
setting = 'setting'
// =============== DIVIDER ==================
}
export const layoutStyleConfig = {
navbarHeight: 64,
breadcrumbHeight: 52,
tabHeight: 40,
footerHeight: 35
} as const
export enum ResCode {
success = 20000,
error = 50000,
illegalToken = 50008,
expiredToken = 50014,
// Other clients logged
otherLogin = 50012
}
================================================
FILE: src/types/global.ts
================================================
export type AnyObject = {
[key: string]: unknown
}
export type GetParams = {
body: string
type: string
url: string
}
export type OKResponse = 'ok'
export type Pagination = {
current: number
pageSize: number
total?: number
}
================================================
FILE: src/utils/event.ts
================================================
// wrapper window event listen
export function addEventListen(
target: Window | HTMLElement,
event: string,
handler: EventListenerOrEventListenerObject,
capture = false
) {
if (target.addEventListener && typeof target.addEventListener === 'function') {
target.addEventListener(event, handler, capture)
}
}
export function removeEventListen(
target: Window | HTMLElement,
event: string,
handler: EventListenerOrEventListenerObject,
capture = false
) {
if (target.removeEventListener && typeof target.removeEventListener === 'function') {
target.removeEventListener(event, handler, capture)
}
}
================================================
FILE: src/utils/routerListener.ts
================================================
/**
* Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management
* 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
*/
import mitt, { type Handler } from 'mitt'
import type { RouteLocationNormalized } from 'vue-router'
const emitter = mitt()
const key = Symbol('ROUTE_CHANGE')
let latestRoute: RouteLocationNormalized
export function setRouteEmitter(to: RouteLocationNormalized) {
emitter.emit(key, to)
latestRoute = to
}
export function listenerRouteChange(
handler: (route: RouteLocationNormalized) => void,
immediate = true
) {
emitter.on(key, handler as Handler)
if (immediate && latestRoute) {
handler(latestRoute)
}
}
export function removeRouteListener() {
emitter.off(key)
}
================================================
FILE: src/utils/sort.ts
================================================
import { cloneDeep } from 'lodash'
/**
* @desc exchange array sort
* @param array origin data source
* @param beforeIdx array source index
* @param newIdx array target index
* @param isDeep array is nested ?
* @returns exchanged array
*/
export const exchangeArray = >(
array: T,
beforeIdx: number,
newIdx: number,
isDeep = false
): T => {
const ret = isDeep ? cloneDeep(array) : array
if (beforeIdx > -1 && newIdx > -1) {
ret.splice(beforeIdx, 1, ret.splice(newIdx, 1, ret[beforeIdx]).pop())
}
return ret
}
================================================
FILE: src/utils/token.ts
================================================
import { LocalStorageKey } from '@/types/constants'
const tokenKey = LocalStorageKey.tokenKey
const isLogin = () => {
return !!localStorage.getItem(tokenKey)
}
const getToken = () => {
return localStorage.getItem(tokenKey)
}
const setToken = (token: string) => {
localStorage.setItem(tokenKey, token)
}
const clearToken = () => {
localStorage.removeItem(tokenKey)
}
export { isLogin, getToken, setToken, clearToken }
================================================
FILE: src/views/dashboard/monitor/ChatPanel.tsx
================================================
import useLoading from '@/hooks/loading'
import {
Button,
Card,
Input,
Link,
Option,
Select,
Skeleton,
SkeletonLine,
Space
} from '@arco-design/web-vue'
import { IconDownload, IconFaceSmileFill } from '@arco-design/web-vue/es/icon'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import MessageItem from './MessageItem'
import { queryChatList } from '@/api/dashboard'
import type { ChatRecord } from '@/api/dashboard/type'
export default defineComponent({
name: 'ChatPanel',
setup() {
const chatData = ref([])
const { loading, setLoading } = useLoading(true)
const fillList = new Array(8).fill(undefined)
const fetchData = async () => {
try {
const { data } = await queryChatList()
chatData.value = data
} catch (err) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
const { t } = useI18n()
return () => (
{loading.value
? fillList.map(() => (
))
: chatData.value.map((item) => )}
{{
suffix: () =>
}}
)
}
})
================================================
FILE: src/views/dashboard/monitor/LiveInformation.tsx
================================================
import { Button, Card, Form, Input } from '@arco-design/web-vue'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'LiveInformation',
setup() {
const { t } = useI18n()
const formData = ref({})
return () => (
)
}
})
================================================
FILE: src/views/dashboard/monitor/LivePanel.tsx
================================================
import useLoading from '@/hooks/loading'
import { useUserStore } from '@/store'
import {
Avatar,
Button,
Card,
Grid,
Link,
Radio,
RadioGroup,
Skeleton,
SkeletonShape,
Space,
Table,
Tabs,
Tag,
Typography
} from '@arco-design/web-vue'
import { IconMore } from '@arco-design/web-vue/es/icon'
import type { TableColumnData, TableData } from '@arco-design/web-vue/es/table/interface.d'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'LivePanel',
setup() {
const userStore = useUserStore()
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
type PreviewRecord = {
cover: string
name: string
duration: string
id: string
status: number
}
const tableData: PreviewRecord[] = [
{
cover:
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/c788fc704d32cf3b1136c7d45afc2669.png~tplv-uwbnlip3yd-webp.webp',
name: '视频直播',
duration: '00:05:19',
id: '54e23ade',
status: -1
}
]
const columns = computed(() => {
return [
{
title: t('monitor.list.title.order'),
render({ rowIndex }: { record: TableData; column: TableColumnData; rowIndex: number }) {
return {rowIndex + 1}
}
},
{
title: t('monitor.list.title.cover'),
render({ record }: { record: TableData; column: TableColumnData; rowIndex: number }) {
return (

{record.status === -1 && (
{t('monitor.list.tag.auditFailed')}
)}
)
}
},
{
title: t('monitor.list.title.name'),
dataIndex: 'name'
},
{
dataIndex: 'duration',
title: t('monitor.list.title.duration')
},
{
dataIndex: 'id',
title: t('monitor.list.title.id')
}
]
})
return () => (
{{
default: () => (
<>
{loading.value && (
)}
setLoading(false)}
onError={() => setLoading(false)}
class={[
'w-full',
'max-w-xl',
'block',
'my-0',
'mx-auto',
'mb-4',
loading.value && 'hidden'
]}
src="https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/c788fc704d32cf3b1136c7d45afc2669.png~tplv-uwbnlip3yd-webp.webp"
/>
{userStore.name}
{t('monitor.studioPreview.studio')}
36,000 {t('monitor.studioPreview.watching')}
>
),
extra: () =>
}}
{t('monitor.liveMethod.normal')}
{t('monitor.liveMethod.flowControl')}
{t('monitor.liveMethod.video')}
{t('monitor.liveMethod.web')}
{t('monitor.editCarousel')}
{t('monitor.list.tip.rotations')} {tableData.length}
{t('monitor.list.tip.rest')}
)
}
})
================================================
FILE: src/views/dashboard/monitor/LiveStatus.tsx
================================================
import { Card, Descriptions, Tag, Typography } from '@arco-design/web-vue'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'LiveStatus',
setup() {
const { t } = useI18n()
const dataStatus = computed(() => [
{
label: 'mainstream',
value: '6 Mbps'
},
{
label: t('monitor.studioStatus.frameRate'),
value: '60'
},
{
label: 'hotStandby',
value: '6 Mbps'
},
{
label: t('monitor.studioStatus.frameRate'),
value: '60'
},
{
label: 'coldStandby',
value: '6 Mbps'
},
{
label: t('monitor.studioStatus.frameRate'),
value: '60'
}
])
const dataPicture = computed(() => [
{
label: t('monitor.studioStatus.line'),
value: '热备'
},
{
label: 'CDN',
value: 'KS'
},
{
label: t('monitor.studioStatus.play'),
value: 'FLV'
},
{
label: t('monitor.studioStatus.pictureQuality'),
value: '原画'
}
])
return () => (
{{
default: () => {
return (
{{
label: ({ label }: { label: string }) =>
['mainstream', 'hotStandby', 'coldStandby'].includes(label) ? (
<>
{t(`monitor.studioStatus.${label}`)}
{t('monitor.studioStatus.bitRate')}:
>
) : (
{label}:
)
}}
{t('monitor.studioStatus.title.pictureInfo')}
{label}:
}
}}
labelStyle={{
paddingRight: '8px'
}}
layout="horizontal"
data={dataPicture.value}
column={2}
/>
)
},
extra: () => {
return {t('monitor.studioStatus.smooth')}
}
}}
)
}
})
================================================
FILE: src/views/dashboard/monitor/MessageItem.tsx
================================================
import { Grid, Space, Typography } from '@arco-design/web-vue'
import { IconCommand, IconStar } from '@arco-design/web-vue/es/icon'
import { defineComponent, type PropType } from 'vue'
import styles from './style.module.scss'
import type { ChatRecord } from '@/api/dashboard/type'
export default defineComponent({
name: 'MessageItem',
props: {
data: {
type: Object as PropType,
default() {
return {}
}
}
},
setup(props) {
return () => (
{props.data.username}
{props.data.content}
{props.data.time}
)
}
})
================================================
FILE: src/views/dashboard/monitor/QuickOperation.tsx
================================================
import { Button, Card, Space } from '@arco-design/web-vue'
import { IconTags, IconStop, IconSwap, IconArrowRight } from '@arco-design/web-vue/es/icon'
import { defineComponent, h } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'QuickOperation',
setup() {
const { t } = useI18n()
const quickOperationList = [
{ getTitle: () => t('monitor.quickOperation.changeClarity'), icon: IconTags },
{ getTitle: () => t('monitor.quickOperation.switchStream'), icon: IconSwap },
{ getTitle: () => t('monitor.quickOperation.removeClarity'), icon: IconStop },
{ getTitle: () => t('monitor.quickOperation.pushFlowGasket'), icon: IconArrowRight }
]
return () => (
{quickOperationList.map((item) => (
))}
)
}
})
================================================
FILE: src/views/dashboard/monitor/index.tsx
================================================
import { Grid, Space } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import ChatPanel from './ChatPanel'
import LiveInformation from './LiveInformation'
import LivePanel from './LivePanel'
import LiveStatus from './LiveStatus'
import QuickOperation from './QuickOperation'
import { ViewNames } from '@/types/constants'
export default defineComponent({
name: ViewNames.monitor,
setup() {
return () => (
)
}
})
================================================
FILE: src/views/dashboard/monitor/style.module.scss
================================================
.chat-item {
padding: 8px;
font-size: 12px;
line-height: 20px;
border-radius: var(--border-radius-small);
&-actions {
display: flex;
opacity: 0;
&-item {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 4px;
color: var(--color-text-3);
font-size: 14px;
border-radius: var(--border-radius-circle);
cursor: pointer;
&:hover {
background-color: rgb(var(--gray-3));
}
&:last-child {
margin-right: 0;
}
}
}
&:hover {
background-color: rgb(var(--gray-2));
.chat-item-actions {
opacity: 1;
}
}
}
================================================
FILE: src/views/dashboard/workplace/Announcement.tsx
================================================
import { Card, Link, Space, Tag, Typography } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'Announcement',
setup() {
const { t } = useI18n()
const list = [
{
type: 'orangered',
label: '活动',
content: '内容最新优惠活动'
},
{
type: 'cyan',
label: '消息',
content: '新增内容尚未通过审核,详情请点击查看。'
},
{
type: 'blue',
label: '通知',
content: '当前产品试用期即将结束,如需续费请点击查看。'
},
{
type: 'blue',
label: '通知',
content: '1月新系统升级计划通知'
},
{
type: 'cyan',
label: '消息',
content: '新增内容已经通过审核,详情请点击查看。'
}
]
return () => (
{{
extra: () => {t('workplace.viewMore')},
default: () => (
{list.map((item) => (
{item.label}
{item.content}
))}
)
}}
)
}
})
================================================
FILE: src/views/dashboard/workplace/ContentChart.tsx
================================================
import { queryContentData, type ContentDataRecord } from '@/api/dashboard'
import ChartComponent from '@/components/chart-component'
import useChartOption from '@/hooks/chartOption'
import useLoading from '@/hooks/loading'
import type { AnyObject } from '@/types/global'
import { Card, Link, Spin } from '@arco-design/web-vue'
import { graphic } from 'echarts'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'ContentChart',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const xAxis = ref([])
const chartsData = ref([])
function graphicFactory(side: AnyObject) {
return {
type: 'text',
bottom: '8',
...side,
style: {
text: '',
textAlign: 'center',
fill: '#4E5969',
fontSize: 12
}
}
}
const graphicElements = ref([graphicFactory({ left: '2.6%' }), graphicFactory({ right: 0 })])
const { chartOption } = useChartOption(() => {
return {
grid: {
left: '5.6%',
right: 0,
top: '10',
bottom: '30'
},
xAxis: {
type: 'category',
offset: 2,
data: xAxis.value,
boundaryGap: false,
axisLabel: {
color: '#4E5969',
formatter(value: number, idx: number) {
if (idx === 0) return ''
if (idx === xAxis.value.length - 1) return ''
return `${value}`
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
interval: (idx: number) => {
if (idx === 0) return false
if (idx === xAxis.value.length - 1) return false
return true
},
lineStyle: {
color: '#E5E8EF'
}
},
axisPointer: {
show: true,
lineStyle: {
color: '#23ADFF',
width: 2
}
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
axisLabel: {
formatter(value: any, idx: number) {
if (idx === 0) return value
return `${value}k`
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#E5E8EF'
}
}
},
tooltip: {
trigger: 'axis',
formatter(params) {
const [firstElement] = params as any[]
return `
${firstElement.axisValueLabel}
总内容量${(
Number(firstElement.value) * 10000
).toLocaleString()}
`
},
className: 'echarts-tooltip-diy'
},
graphic: {
elements: graphicElements.value
},
series: [
{
data: chartsData.value,
type: 'line',
smooth: true,
symbolSize: 12,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2
}
},
lineStyle: {
width: 3,
color: new graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: 'rgba(30, 231, 255, 1)'
},
{
offset: 0.5,
color: 'rgba(36, 154, 255, 1)'
},
{
offset: 1,
color: 'rgba(111, 66, 251, 1)'
}
])
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(17, 126, 255, 0.16)'
},
{
offset: 1,
color: 'rgba(17, 128, 255, 0)'
}
])
}
}
]
}
})
const fetchData = async () => {
try {
const { data: chartData } = await queryContentData()
chartData.forEach((el: ContentDataRecord, idx: number) => {
xAxis.value.push(el.x)
chartsData.value.push(el.y)
if (idx === 0) {
graphicElements.value[0].style.text = el.x
}
if (idx === chartData.length - 1) {
graphicElements.value[1].style.text = el.x
}
})
} catch (err) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{t('workplace.viewMore')}
}}
>
)
}
})
================================================
FILE: src/views/dashboard/workplace/ContentPercentage.tsx
================================================
import ChartComponent from '@/components/chart-component'
import useChartOption from '@/hooks/chartOption'
import useLoading from '@/hooks/loading'
import { Card, Link, Spin } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'ContentPercentage',
setup() {
const { loading, setLoading } = useLoading(true)
setTimeout(() => {
setLoading(false)
}, 800)
const { chartOption } = useChartOption((isDark) => {
return {
legend: {
left: 'center',
data: ['纯文本', '图文类', '视频类'],
bottom: 0,
icon: 'circle',
itemWidth: 8,
textStyle: {
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969'
},
itemStyle: {
borderWidth: 0
}
},
tooltip: {
show: true,
trigger: 'item'
},
graphic: {
elements: [
{
type: 'text',
left: 'center',
top: '40%',
style: {
text: '内容量',
textAlign: 'center',
fill: isDark ? '#ffffffb3' : '#4E5969',
fontSize: 14
}
},
{
type: 'text',
left: 'center',
top: '50%',
style: {
text: '928,531',
textAlign: 'center',
fill: isDark ? '#ffffffb3' : '#1D2129',
fontSize: 16,
fontWeight: 500
}
}
]
},
series: [
{
type: 'pie',
radius: ['50%', '70%'],
center: ['50%', '50%'],
label: {
formatter: '{d}%',
fontSize: 14,
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969'
},
itemStyle: {
borderColor: isDark ? '#232324' : '#fff',
borderWidth: 1
},
data: [
{
value: [148564],
name: '纯文本',
itemStyle: {
color: isDark ? '#3D72F6' : '#249EFF'
}
},
{
value: [334271],
name: '图文类',
itemStyle: {
color: isDark ? '#A079DC' : '#313CA9'
}
},
{
value: [445694],
name: '视频类',
itemStyle: {
color: isDark ? '#6CAAF5' : '#21CCFF'
}
}
]
}
]
}
})
const { t } = useI18n()
return () => (
{t('workplace.viewMore')}
}}
class="general-card"
title={t('workplace.contentData')}
>
)
}
})
================================================
FILE: src/views/dashboard/workplace/HelpDocs.tsx
================================================
import { Card, Grid, Link } from '@arco-design/web-vue'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'HelpDocs',
setup() {
const linkList = computed(() => [
{
href: 'https://arco.design/react/docs/start',
desc: t('workplace.docs.productOverview')
},
{
href: 'https://arco.design/vue/docs/start',
desc: t('workplace.docs.userGuide')
},
{
href: 'https://arco.design/themes',
desc: t('workplace.docs.workflow')
},
{
href: 'https://arco.design/material/',
desc: t('workplace.docs.interfaceDocs')
}
])
const { t } = useI18n()
return () => (
{t('workplace.viewMore')}
}}
>
{linkList.value.map(({ desc }) => (
{desc}
))}
)
}
})
================================================
FILE: src/views/dashboard/workplace/OverView.tsx
================================================
import { useUserStore } from '@/store'
import { Avatar, Card, Divider, Grid, Space, Statistic, Typography } from '@arco-design/web-vue'
import { IconCaretUp } from '@arco-design/web-vue/es/icon'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentChart from './ContentChart'
export default defineComponent({
name: 'OverView',
setup() {
const userStore = useUserStore()
const { t } = useI18n()
const dataList = [
{
imgSrc:
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/c8b36e26d2b9bb5dbf9b74dd6d7345af.svg~tplv-49unhts6dw-image.image',
value: 2.8,
precision: 1,
valueFrom: 0,
getTitle: () => t('workplace.newFromYesterday'),
getSuffix: () => (
<>
%
>
)
},
{
imgSrc:
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/fdc66b07224cdf18843c6076c2587eb5.svg~tplv-49unhts6dw-image.image',
value: 373.5,
precision: 1,
valueFrom: 0,
getTitle: () => t('workplace.onlineContent'),
getSuffix: () => (
<>
W+ {t('workplace.pecs')}
>
)
},
{
imgSrc:
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/fdc66b07224cdf18843c6076c2587eb5.svg~tplv-49unhts6dw-image.image',
value: 368,
precision: 1,
valueFrom: 0,
getTitle: () => t('workplace.putIn'),
getSuffix: () => (
<>
W+ {t('workplace.pecs')}
>
)
},
{
imgSrc:
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77d74c9a245adeae1ec7fb5d4539738d.svg~tplv-49unhts6dw-image.image',
value: 8874,
precision: 1,
valueFrom: 0,
getTitle: () => t('workplace.newDay'),
getSuffix: () => (
<>
W+ {t('workplace.pecs')}
>
)
}
]
return () => (
{t('workplace.welcome')}
{userStore.name}
{dataList.map((item) => (
{item.getTitle()}
item.getSuffix()
}}
>
))}
)
}
})
================================================
FILE: src/views/dashboard/workplace/PopularContents.tsx
================================================
import { queryPopularList } from '@/api/dashboard'
import useLoading from '@/hooks/loading'
import { Card, Link, RadioGroup, Space, Spin, Table } from '@arco-design/web-vue'
import { IconCaretDown, IconCaretUp } from '@arco-design/web-vue/es/icon'
import type { TableData } from '@arco-design/web-vue/es/table/interface'
import { defineComponent, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'PopularContents',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading()
const tableData = ref()
const sourceType = ref('text')
const fetchData = async (contentType: string) => {
try {
setLoading(true)
const { data } = await queryPopularList({ type: contentType })
tableData.value = data
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false)
}
}
watch(sourceType, () => {
fetchData(sourceType.value)
})
fetchData(sourceType.value)
const columns = [
{
title: '排名',
dataIndex: 'key',
width: 65
},
{
title: '内容标题',
dataIndex: 'title',
render: ({ record }: { record: TableData }) => {record.title}
},
{
title: '点击量',
dataIndex: 'clickNumber'
},
{
title: '日涨幅',
dataIndex: 'increases',
render: ({ record }: { record: TableData }) => {
return (
{record.increases}%
{record.increases > 20 ? (
) : (
record.increases !== 0 &&
)}
)
}
}
]
return () => (
{{
extra: () => {t('workplace.viewMore')},
default: () => (
)
}}
)
}
})
================================================
FILE: src/views/dashboard/workplace/RightTopArea.tsx
================================================
import { Card, Divider, Grid, Link, Space, Typography } from '@arco-design/web-vue'
import {
IconFile,
IconFire,
IconMobile,
IconSettings,
IconStorage
} from '@arco-design/web-vue/es/icon'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import styles from './style.module.scss'
export default defineComponent({
name: 'RightTopArea',
setup() {
const { t } = useI18n()
const links = computed(() => [
{ text: t('workplace.contentManagement'), icon: },
{ text: t('workplace.contentStatistical'), icon: },
{ text: t('workplace.advanced'), icon: },
{ text: t('workplace.onlinePromotion'), icon: },
{ text: t('workplace.contentPutIn'), icon: }
])
const quickLinks = computed(() => [
{
text: t('workplace.contentManagement'),
icon:
},
{
text: t('workplace.contentStatistical'),
icon:
},
{
text: t('workplace.advanced'),
icon:
}
])
return () => (
{t('workplace.quickOperation.setup')}
}
}}
title={t('workplace.quick.operation')}
>
{links.value.map((item) => (
{item.icon}
{item.text}
))}
{quickLinks.value.map((item) => (
{item.icon}
{item.text}
))}
)
}
})
================================================
FILE: src/views/dashboard/workplace/index.tsx
================================================
import ContentPercentage from '@/views/dashboard/workplace/ContentPercentage'
import OverView from '@/views/dashboard/workplace/OverView'
import PopularContents from '@/views/dashboard/workplace/PopularContents'
import { Carousel, Grid, Space } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import Announcement from './Announcement'
import HelpDocs from './HelpDocs'
import RightTopArea from './RightTopArea'
import { ViewNames } from '@/types/constants'
export default defineComponent({
name: ViewNames.workplace,
setup() {
const imageSrc = [
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/f7e8fc1e09c42e30682526252365be1c.jpg~tplv-uwbnlip3yd-webp.webp',
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/94e8dd2d6dc4efb2c8cfd82c0ff02a2c.jpg~tplv-uwbnlip3yd-webp.webp',
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/ec447228c59ae1ebe185bab6cd776ca4.jpg~tplv-uwbnlip3yd-webp.webp',
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/1d1580d2a5a1e27415ff594c756eabd8.jpg~tplv-uwbnlip3yd-webp.webp'
]
return () => (
{imageSrc.map((src, index) => (
))}
)
}
})
================================================
FILE: src/views/dashboard/workplace/style.module.scss
================================================
.icon {
display: inline-block;
width: 32px;
height: 32px;
margin-bottom: 4px;
color: rgb(var(--dark-gray-1));
line-height: 32px;
font-size: 16px;
text-align: center;
background-color: rgb(var(--gray-1));
border-radius: var(--border-radius-medium);
}
.text {
font-size: 12px;
overflow-wrap: normal;
color: rgb(var(--gray-8));
}
.wrapper {
text-align: center;
cursor: pointer;
&:hover {
.icon {
color: rgb(var(--arcoblue-6));
background-color: rgb(var(--blue-1));
}
.text {
color: rgb(var(--arcoblue-6));
}
}
}
.area {
:global(.arco-card-body) {
padding-left: 0;
padding-right: 0;
}
}
================================================
FILE: src/views/exception/403/index.tsx
================================================
import CardLayout from '@/components/card-layout'
import { ViewNames } from '@/types/constants'
import { Button, Result, Space } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames._403,
setup() {
const { t } = useI18n()
return () => (
{{
extra: () => (
)
}}
)
}
})
================================================
FILE: src/views/exception/404/index.tsx
================================================
import CardLayout from '@/components/card-layout'
import { ViewNames } from '@/types/constants'
import { Button, Result, Space } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames._404,
setup() {
const { t } = useI18n()
return () => (
{{
extra: () => (
)
}}
)
}
})
================================================
FILE: src/views/exception/500/index.tsx
================================================
import { defineComponent } from 'vue'
import { Button, Result } from '@arco-design/web-vue'
import { useI18n } from 'vue-i18n'
import CardLayout from '@/components/card-layout'
import { ViewNames } from '@/types/constants'
export default defineComponent({
name: ViewNames._500,
setup() {
const { t } = useI18n()
return () => (
{{
extra: () =>
}}
)
}
})
================================================
FILE: src/views/form/group/index.tsx
================================================
import { submitGroupForm, type GroupFormModel } from '@/api/form'
import useLoading from '@/hooks/loading'
import { ViewNames } from '@/types/constants'
import {
Button,
Card,
Form,
Grid,
Input,
InputNumber,
Message,
Select,
Space,
Textarea,
type FormInstance
} from '@arco-design/web-vue'
import { isEmpty } from 'lodash'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames.group,
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading()
const formRef = ref()
const formData = ref({
video: {
mode: undefined,
acquisition: {
resolution: undefined,
frameRate: undefined
},
encoding: {
resolution: undefined,
rate: {
min: undefined,
max: undefined,
default: undefined
},
frameRate: undefined,
profile: undefined
}
},
audio: {
mode: undefined,
acquisition: {
channels: undefined
},
explanation: undefined,
encoding: {
channels: undefined,
rate: undefined,
profile: undefined
}
}
})
const handleSubmit = async () => {
formRef.value?.validate().then(async (errors) => {
if (isEmpty(errors)) {
try {
setLoading(true)
await submitGroupForm(formData.value as unknown as GroupFormModel)
Message.success('提交成功')
} catch (e) {
/* empty */
} finally {
setLoading(false)
}
}
})
}
const handleReset = () => {
formRef.value?.resetFields()
}
return () => (
)
}
})
================================================
FILE: src/views/form/step/index.tsx
================================================
import { submitChannelForm, type UnitChannelModel } from '@/api/form'
import useLoading from '@/hooks/loading'
import { ViewNames } from '@/types/constants'
import {
Button,
Card,
DatePicker,
Form,
Input,
InputTag,
Message,
Result,
Select,
Space,
Steps,
Switch,
Textarea,
Typography,
type FormInstance
} from '@arco-design/web-vue'
import { isEmpty } from 'lodash'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames.step,
setup() {
const { loading, setLoading } = useLoading(false)
const { t } = useI18n()
const current = ref(1)
const getDefaultFormData = () => ({
activityName: '',
channelType: '',
promotionTime: [],
promoteLink: 'https://github.com/',
advertisingSource: '',
advertisingMedia: '',
keyword: [],
pushNotify: true,
advertisingContent: ''
})
const formData = ref(getDefaultFormData())
const formRef = ref()
// form actions
const handlePrev = () => {
current.value--
}
const handleNext = async () => {
try {
setLoading(true)
const errors = await formRef.value?.validate()
if (isEmpty(errors)) {
if (current.value === 2) {
await submitChannelForm(formData.value)
Message.success('提交成功')
}
current.value++
}
} catch (e) {
/* empty */
} finally {
setLoading(false)
}
}
const viewForm = () => {
current.value = 1
}
const recreateForm = () => {
formData.value = getDefaultFormData()
current.value = 1
}
return () => (
)
}
})
================================================
FILE: src/views/list/card-list/AddCard.tsx
================================================
import { Card, Result } from '@arco-design/web-vue'
import { IconPlus } from '@arco-design/web-vue/es/icon'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import styles from './style.module.scss'
export default defineComponent({
name: 'AddCard',
setup() {
const { t } = useI18n()
return () => (
{{
icon: () =>
}}
)
}
})
================================================
FILE: src/views/list/card-list/QualityInspection.tsx
================================================
import { queryInspectionList, type ServiceRecord } from '@/api/list'
import useLoading from '@/hooks/loading'
import { Button, Card, Descriptions, Grid, Skeleton, Space, Typography } from '@arco-design/web-vue'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import AddCard from './AddCard'
import SkeletonCard from './SkeletonCard'
import styles from './style.module.scss'
import { itemSpan } from '.'
export default defineComponent({
name: 'QualityInspection',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const cardList = ref([])
const fillList = new Array(7).fill(undefined)
const fetchData = async () => {
try {
const res = await queryInspectionList()
cardList.value = res.data
} catch (error) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{t('cardList.tab.title.content')}
{loading.value
? fillList.map(() => )
: cardList.value.map((item) => (
{{
default: () => (
{{
title: () => {item.title},
description: () => (
<>
{item.description}
(
)
}}
>
>
)
}}
),
actions: () => (
)
}}
))}
)
}
})
================================================
FILE: src/views/list/card-list/RulesPreset.tsx
================================================
import { queryRulesPresetList, type ServiceRecord } from '@/api/list'
import useLoading from '@/hooks/loading'
import { Card, Grid, Space, Switch, Tag, Typography } from '@arco-design/web-vue'
import { IconCheckCircleFill } from '@arco-design/web-vue/es/icon'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import SkeletonCard from './SkeletonCard'
import styles from './style.module.scss'
import { itemSpan } from '.'
export default defineComponent({
name: 'RulesPreset',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const fillList = new Array(7).fill(undefined)
const cardList = ref([])
const fetchData = async () => {
try {
const res = await queryRulesPresetList()
cardList.value = res.data
} catch (error) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{t('cardList.tab.title.preset')}
{loading.value
? fillList.map(() => )
: cardList.value.map((item) => (
{{
default: () => (
{{
title: () => (
{item.title}
{item.enable && (
}}
>
{t('cardList.preset.tag')}
)}
),
description: () => (
{item.description}
)
}}
),
actions: () =>
}}
))}
)
}
})
================================================
FILE: src/views/list/card-list/SkeletonCard.tsx
================================================
import { Card, Grid, Skeleton } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import styles from './style.module.scss'
import { itemSpan } from '.'
export default defineComponent({
name: 'SkeletonCard',
props: {
loading: {
type: Boolean,
required: true
}
},
setup(props) {
return () => (
)
}
})
================================================
FILE: src/views/list/card-list/TheService.tsx
================================================
import { queryTheServiceList, type ServiceRecord } from '@/api/list'
import useLoading from '@/hooks/loading'
import { Avatar, Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-vue'
import { IconCheckCircleFill, IconFilter } from '@arco-design/web-vue/es/icon'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import SkeletonCard from './SkeletonCard'
import styles from './style.module.scss'
import { itemSpan } from '.'
export default defineComponent({
name: 'TheService',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const cardList = ref([])
const fillList = new Array(7).fill(undefined)
const fetchData = async () => {
try {
const res = await queryTheServiceList()
cardList.value = res.data
} catch (error) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{t('cardList.tab.title.service')}
{loading.value
? fillList.map(() => )
: cardList.value.map((item) => (
{{
default: () => (
{item.icon && (
)}
{{
title: () => (
{item.title}
{item.enable &&
(item.expires ? (
}}
>
{t('cardList.service.expiresTag')}
) : (
}}
>
{t('cardList.service.tag')}
))}
),
description: () => (
{item.description}
)
}}
),
actions: () => (
{item.expires ? (
) : item.enable ? (
) : (
)}
)
}}
))}
)
}
})
================================================
FILE: src/views/list/card-list/index.tsx
================================================
import { Card, Input, Tabs, Typography } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import QualityInspection from './QualityInspection'
import RulesPreset from './RulesPreset'
import TheService from './TheService'
import { ViewNames } from '@/types/constants'
export const itemSpan = {
xs: 12,
sm: 12,
md: 12,
lg: 8,
xl: 8,
xxl: 6
} as const
export default defineComponent({
name: ViewNames.cardList,
setup() {
const { t } = useI18n()
const tabList = [
{
getTitle: () => t('cardList.tab.title.all'),
value: 1,
pane: (
<>
>
)
},
{
getTitle: () => t('cardList.tab.title.content'),
value: 2,
pane: (
<>
>
)
},
{
getTitle: () => t('cardList.tab.title.service'),
value: 3,
pane: (
<>
>
)
},
{
getTitle: () => t('cardList.tab.title.preset'),
value: 4,
pane: (
<>
>
)
}
]
return () => (
{t('menu.list.cardList')}
}}
>
(
)
}}
>
{tabList.map((tab) => (
{tab.pane}
))}
)
}
})
================================================
FILE: src/views/list/card-list/style.module.scss
================================================
.card {
:global(.arco-card-meta-title) {
margin-bottom: 20px;
}
:global(.arco-card-meta-content) {
height: 88px;
}
}
.skeleton-card {
height: 170px;
}
.add-card {
height: 172px;
}
================================================
FILE: src/views/list/search-table/TableSearchForm.tsx
================================================
import type { PolicyQuery } from '@/api/list'
import useLocale from '@/hooks/locale'
import { LocaleOptions } from '@/types/constants'
import {
Button,
Form,
Grid,
Input,
RangePicker,
Select,
type FormInstance,
type SelectOptionData
} from '@arco-design/web-vue'
import { IconRefresh, IconSearch } from '@arco-design/web-vue/es/icon'
import { computed, defineComponent, ref, type PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import styles from './style.module.scss'
export default defineComponent({
name: 'TableSearchForm',
emits: ['onSearch'],
props: {
searchQuery: {
type: Object as PropType,
required: true
},
searchLoading: {
type: Boolean,
required: true
}
},
setup(props, { emit }) {
const { t } = useI18n()
const { currentLocale } = useLocale()
const formRef = ref()
const contentTypeOptions = computed(() => [
{
label: t('searchTable.form.contentType.img'),
value: 'img'
},
{
label: t('searchTable.form.contentType.horizontalVideo'),
value: 'horizontalVideo'
},
{
label: t('searchTable.form.contentType.verticalVideo'),
value: 'verticalVideo'
}
])
const filterTypeOptions = computed(() => [
{
label: t('searchTable.form.filterType.artificial'),
value: 'artificial'
},
{
label: t('searchTable.form.filterType.rules'),
value: 'rules'
}
])
const statusOptions = computed(() => [
{
label: t('searchTable.form.status.online'),
value: 'online'
},
{
label: t('searchTable.form.status.offline'),
value: 'offline'
}
])
const colSpan = computed(() => {
if (currentLocale.value === LocaleOptions.en) return 12
return 8
})
return () => (
)
}
})
================================================
FILE: src/views/list/search-table/index.tsx
================================================
import { queryPolicyList, type PolicyQuery, type PolicyRecord } from '@/api/list'
import useLoading from '@/hooks/loading'
import { type Pagination } from '@/types/global'
import { exchangeArray } from '@/utils/sort'
import {
Avatar,
Badge,
Button,
Card,
Checkbox,
Divider,
Dropdown,
Link,
Popover,
Space,
Table,
Tooltip,
Upload,
type PaginationProps,
type TableColumnData
} from '@arco-design/web-vue'
import {
IconDownload,
IconDragArrow,
IconLineHeight,
IconPlus,
IconSettings
} from '@arco-design/web-vue/es/icon'
import Sortable from 'sortablejs'
import { computed, defineComponent, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import TableSearchForm from './TableSearchForm'
import { ViewNames } from '@/types/constants'
import usePermission from '@/hooks/permission'
export default defineComponent({
name: ViewNames.searchTable,
setup() {
const { t } = useI18n()
const { checkButtonPermission } = usePermission()
const initPagination: Pagination = {
current: 1,
pageSize: 20
}
const paginationConfig = ref({
...initPagination,
showTotal: true
})
const resetPagination = () => {
paginationConfig.value = {
...paginationConfig.value,
...initPagination
}
}
const handleCurrentChange = (page: number) => {
paginationConfig.value.current = page
fetchData()
}
const handlePageSizeChange = (pageSize: number) => {
paginationConfig.value.pageSize = pageSize
fetchData()
}
const handleQuerySearch = () => {
resetPagination()
fetchData()
}
// =============== DIVIDER ==================
// table size change
type TableSize = 'medium' | 'mini' | 'small' | 'large'
const tableSize = ref('medium')
const densityList = computed(() => [
{
name: t('searchTable.size.mini'),
value: 'mini'
},
{
name: t('searchTable.size.small'),
value: 'small'
},
{
name: t('searchTable.size.medium'),
value: 'medium'
},
{
name: t('searchTable.size.large'),
value: 'large'
}
])
const handleSelectDensity = (val: unknown) => {
tableSize.value = val as TableSize
}
// =============== DIVIDER ==================
// fetch data logic
const renderData = ref([])
const searchQuery = ref({
number: '',
name: '',
contentType: '',
filterType: '',
createdTime: [],
status: ''
})
const { loading, setLoading } = useLoading()
const fetchData = async () => {
setLoading(true)
try {
const query = searchQuery.value
const params = {
...query,
current: paginationConfig.value.current,
pageSize: paginationConfig.value.pageSize
}
const { data } = await queryPolicyList(params)
renderData.value = data.list
paginationConfig.value.total = data.total
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false)
}
}
fetchData()
// =============== DIVIDER ==================
// table columns render logic
const colList = ref([
{
getTitle: () => t('searchTable.columns.number'),
dataIndex: 'number',
checked: true
},
{
getTitle: () => t('searchTable.columns.name'),
dataIndex: 'name',
checked: true
},
{
getTitle: () => t('searchTable.columns.contentType'),
dataIndex: 'contentType',
render: ({ record }: { record: PolicyRecord }) => {
const map: Record = {
img: '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
horizontalVideo:
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77721e365eb2ab786c889682cbc721c1.svg~tplv-49unhts6dw-image.image',
verticalVideo:
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/ea8b09190046da0ea7e070d83c5d1731.svg~tplv-49unhts6dw-image.image'
}
return (
<>
{t(`searchTable.form.contentType.${record.contentType}`)}
>
)
},
checked: true
},
{
getTitle: () => t('searchTable.columns.filterType'),
dataIndex: 'filterType',
render: ({ record }: { record: PolicyRecord }) => (
<>{t(`searchTable.form.filterType.${record.filterType}`)}>
),
checked: true
},
{
getTitle: () => t('searchTable.columns.count'),
dataIndex: 'count',
checked: true
},
{
getTitle: () => t('searchTable.columns.createdTime'),
dataIndex: 'createdTime',
checked: true
},
{
getTitle: () => t('searchTable.columns.status'),
dataIndex: 'status',
render: ({ record }: { record: PolicyRecord }) => {
return (
{t(`searchTable.form.status.${record.status}`)}
)
},
checked: true
},
{
getTitle: () => t('searchTable.columns.operations'),
dataIndex: 'operations',
render: () =>
checkButtonPermission(['admin']) && (
{t('searchTable.columns.operations.view')}
),
checked: true
}
])
const popupVisibleChange = (val: boolean) => {
if (val) {
nextTick(() => {
const el = document.getElementById('tableSetting') as HTMLElement
new Sortable(el, {
onEnd(e: any) {
const { oldIndex, newIndex } = e
exchangeArray(colList.value, oldIndex, newIndex)
}
})
})
}
}
const tableColumns = computed(() => {
return colList.value
.filter((col) => col.checked)
.map((item) => {
const ret: TableColumnData = {
title: item.getTitle(),
dataIndex: item.dataIndex
}
if (item.render) ret.render = item.render as unknown as TableColumnData['render']
return ret
})
})
return () => (
{{
'upload-button': () =>
}}
{{
default: () => (
),
content: () =>
densityList.value.map((item) => (
{item.name}
))
}}
{{
content: () => (
{colList.value.map((item) => (
))}
),
default: () =>
}}
)
}
})
================================================
FILE: src/views/list/search-table/style.module.scss
================================================
.form {
:global(.arco-form-item-label-col-left) {
> label {
white-space: nowrap;
}
}
}
.button-area {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 20px;
margin-bottom: 20px;
border-left: 1px solid var(--color-border-2);
box-sizing: border-box;
margin-left: 20px;
}
================================================
FILE: src/views/login/LoginBanner.tsx
================================================
import { Carousel } from '@arco-design/web-vue'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'loginBanner',
setup() {
const { t } = useI18n()
const dataList = computed(() => [
{
slogan: t('login.banner.slogan3'),
subSlogan: t('login.banner.subSlogan3'),
image:
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/%E8%BD%AE%E6%92%AD%E5%9B%BE.png'
},
{
slogan: t('login.banner.slogan1'),
subSlogan: t('login.banner.subSlogan1'),
image:
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/%E8%BD%AE%E6%92%AD%E5%9B%BE.png'
},
{
slogan: t('login.banner.slogan2'),
subSlogan: t('login.banner.subSlogan2'),
image:
'https://cdn.jsdelivr.net/gh/manyuemeiquqi/my-image-bed/dist/%E8%BD%AE%E6%92%AD%E5%9B%BE.png'
}
])
return () => (
{dataList.value.map((item) => {
return (
{item.slogan}
{item.subSlogan}
)
})}
)
}
})
================================================
FILE: src/views/login/LoginForm.tsx
================================================
import { type LoginData } from '@/api/user'
import useAuth from '@/hooks/auth'
import useLoading from '@/hooks/loading'
import { ApplicationInfo, LocalStorageKey, ViewNames } from '@/types/constants'
import {
Button,
Checkbox,
Form,
Input,
Link,
Message,
Space,
Typography,
type FieldRule,
type ValidatedError
} from '@arco-design/web-vue'
import { IconLock, IconUser } from '@arco-design/web-vue/es/icon'
import { useStorage } from '@vueuse/core'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Logo from '@/assets/logo.svg'
import { firstPermissionRoute } from '@/hooks/appRoute'
export default defineComponent({
name: 'loginForm',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading()
const router = useRouter()
const { loginApp } = useAuth()
const storageLoginInfo = useStorage(LocalStorageKey.loginFormKey, {
rememberPassword: true,
username: 'admin',
password: 'admin'
})
const loginFormData = ref({
username: storageLoginInfo.value.username,
password: storageLoginInfo.value.password
})
const errorMessage = ref()
const formRules: Record = {
username: [{ required: true, message: t('login.form.userName.errMsg') }],
password: [{ required: true, message: t('login.form.password.errMsg') }]
}
const handleSubmit = async ({
errors,
values
}: {
errors: Record | undefined
values: Record
}) => {
if (loading.value) return
if (!errors) {
setLoading(true)
try {
await loginApp(values as LoginData)
router.push({
name: firstPermissionRoute.name
})
Message.success(t('login.form.login.success'))
const { rememberPassword } = storageLoginInfo.value
const { username, password } = values
storageLoginInfo.value.username = rememberPassword ? username : ''
storageLoginInfo.value.password = rememberPassword ? password : ''
} catch (err) {
errorMessage.value = (err as Error).message
} finally {
setLoading(false)
}
}
}
return () => (
)
}
})
================================================
FILE: src/views/login/index.tsx
================================================
import { ViewNames } from '@/types/constants'
import LoginBanner from '@/views/login/LoginBanner'
import LoginForm from '@/views/login/LoginForm'
import { defineComponent } from 'vue'
export default defineComponent({
name: ViewNames.login,
setup() {
return () => (
)
}
})
================================================
FILE: src/views/not-found/index.tsx
================================================
import CardLayout from '@/components/card-layout'
import { firstPermissionRoute } from '@/hooks/appRoute'
import { ViewNames } from '@/types/constants'
import { Button, Result, Space } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
export default defineComponent({
name: ViewNames.notFound,
setup() {
const { t } = useI18n()
const router = useRouter()
const handleBack = () => {
router.push({
name: firstPermissionRoute.name
})
}
return () => (
{{
extra: () => (
)
}}
)
}
})
================================================
FILE: src/views/profile/DataUpdateRecord.tsx
================================================
import { queryOperationLog, type operationLogRes } from '@/api/profile'
import useLoading from '@/hooks/loading'
import { Badge, Button, Card, Spin, Table } from '@arco-design/web-vue'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'DataUpdateRecord',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const tableData = ref([])
const fetchData = async () => {
try {
const { data } = await queryOperationLog()
tableData.value = data
} catch (err) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{
if (record.status != 0) {
return
}
return
}
},
{
dataIndex: 'updateTime',
title: t('basicProfile.column.updateTime')
},
{
title: t('basicProfile.column.operation'),
render() {
return
}
}
]}
>
)
}
})
================================================
FILE: src/views/profile/ProfileItem.tsx
================================================
import type { ProfileBasicRes } from '@/api/profile'
import { Card, Descriptions, Skeleton, type DescData } from '@arco-design/web-vue'
import { computed, defineComponent, type PropType } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'ProfileItem',
props: {
profileData: {
type: Object as PropType,
default: () => ({
status: 0,
video: {
mode: '',
acquisition: {
resolution: '',
frameRate: 0
},
encoding: {
resolution: '',
rate: {
min: 0,
max: 0,
default: 0
},
frameRate: 0,
profile: ''
}
},
audio: {
mode: '',
acquisition: {
channels: 0
},
encoding: {
channels: 0,
rate: 0,
profile: ''
}
}
})
},
loading: {
type: Boolean
}
},
setup(props) {
const { t } = useI18n()
type BlockData = {
title: string
descData: DescData[]
}
const blockDataList = computed(() => {
const ret: BlockData[] = []
const { video, audio } = props.profileData
// assign render data
ret.push({
title: t(`basicProfile.title.preVideo`),
descData: [
{
label: t('basicProfile.label.video.mode'),
value: video?.mode || '-'
},
{
label: t('basicProfile.label.video.acquisition.resolution'),
value: video?.acquisition.resolution || '-'
},
{
label: t('basicProfile.label.video.acquisition.frameRate'),
value: `${video?.acquisition.frameRate || '-'} fps`
},
{
label: t('basicProfile.label.video.encoding.resolution'),
value: video?.encoding.resolution || '-'
},
{
label: t('basicProfile.label.video.encoding.rate.min'),
value: `${video?.encoding.rate.min || '-'} bps`
},
{
label: t('basicProfile.label.video.encoding.rate.max'),
value: `${video?.encoding.rate.max || '-'} bps`
},
{
label: t('basicProfile.label.video.encoding.rate.default'),
value: `${video?.encoding.rate.default || '-'} bps`
},
{
label: t('basicProfile.label.video.encoding.frameRate'),
value: `${video?.encoding.frameRate || '-'} fpx`
},
{
label: t('basicProfile.label.video.encoding.profile'),
value: video?.encoding.profile || '-'
}
]
})
ret.push({
title: t(`basicProfile.title.video`),
descData: [
{
label: t('basicProfile.label.audio.mode'),
value: audio?.mode || '-'
},
{
label: t('basicProfile.label.audio.acquisition.channels'),
value: `${audio?.acquisition.channels || '-'} ${t('basicProfile.unit.audio.channels')}`
},
{
label: t('basicProfile.label.audio.encoding.channels'),
value: `${audio?.encoding.channels || '-'} ${t('basicProfile.unit.audio.channels')}`
},
{
label: t('basicProfile.label.audio.encoding.rate'),
value: `${audio?.encoding.rate || '-'} kbps`
},
{
label: t('basicProfile.label.audio.encoding.profile'),
value: audio?.encoding.profile || '-'
}
]
})
return ret
})
return () => (
{blockDataList.value.map((item) => (
{{
value: ({ value }: any) =>
props.loading ? (
) : (
{value}
)
}}
))}
)
}
})
================================================
FILE: src/views/profile/index.tsx
================================================
import { queryProfileBasic, type ProfileBasicRes } from '@/api/profile'
import useLoading from '@/hooks/loading'
import { ViewNames } from '@/types/constants'
import DataUpdateRecord from '@/views/profile/DataUpdateRecord'
import ProfileItem from '@/views/profile/ProfileItem'
import { Button, Card, Space, Steps } from '@arco-design/web-vue'
import { computed, defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames.profile,
setup() {
const { t } = useI18n()
const step = ref(1)
const stepList = computed(() => [
{ status: 1, label: t('basicProfile.steps.commit') },
{ status: 2, label: t('basicProfile.steps.approval') },
{ status: 3, label: t('basicProfile.steps.finish') }
])
const { loading, setLoading } = useLoading(true)
const currentData = ref()
const preData = ref()
const fetchData = async () => {
try {
const [profileData1, profileData2] = await Promise.allSettled([
queryProfileBasic(),
queryProfileBasic()
])
if (profileData1.status === 'fulfilled') {
currentData.value = profileData1.value.data
}
if (profileData2.status === 'fulfilled') {
preData.value = profileData2.value.data
}
step.value = 3
} catch (error) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{{
extra: () => (
),
default: () => (
{stepList.value.map((step) => (
{step.label}
))}
)
}}
)
}
})
================================================
FILE: src/views/redirect/index.tsx
================================================
import { ViewNames } from '@/types/constants'
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
name: ViewNames.redirect,
setup() {
const router = useRouter()
const route = useRoute()
const fullPath = route.params.path as string
router.replace({ path: fullPath })
return () =>
}
})
================================================
FILE: src/views/result/error/index.tsx
================================================
import CardLayout from '@/components/card-layout'
import { ViewNames } from '@/types/constants'
import { Button, Link, Result, Space, Typography } from '@arco-design/web-vue'
import { IconLink } from '@arco-design/web-vue/es/icon'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames.error,
setup() {
const { t } = useI18n()
return () => (
{{
extra: () => (
),
default: () => (
{t('error.detailTitle')}
-
{t('error.detailLine.record')}
{t('error.detailLine.record.link')}
- {t('error.detailLine.auth')}
)
}}
)
}
})
================================================
FILE: src/views/result/success/index.tsx
================================================
import CardLayout from '@/components/card-layout'
import { ViewNames } from '@/types/constants'
import { Button, Result, Space, Steps, Typography } from '@arco-design/web-vue'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: ViewNames.success,
setup() {
const { t } = useI18n()
const stepList = computed(() => {
return [
{
title: t('success.submitApplication'),
description: '2020/10/10 14:00:39'
},
{
title: t('success.leaderReview'),
description: t('success.processing')
},
{
title: t('success.purchaseCertificate'),
description: t('success.waiting')
},
{
title: t('success.safetyTest'),
description: t('success.waiting')
},
{
title: t('success.launched'),
description: t('success.waiting')
}
]
})
return () => (
{{
extra: () => (
),
default: () => (
{t('success.result.progress')}
{stepList.value.map((item) => {
return (
)
})}
)
}}
)
}
})
================================================
FILE: src/views/user/info/ActivityItem.tsx
================================================
import { Avatar, List } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ActivityItem',
props: {
title: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
description: {
type: String,
required: true
}
},
setup(props) {
return () => (
(
)
}}
/>
)
}
})
================================================
FILE: src/views/user/info/InSiteNotifications.tsx
================================================
import useLoading from '@/hooks/loading'
import { Card, Result, Skeleton } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'InSiteNotifications',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
// just mock
setTimeout(() => {
setLoading(false)
}, 500)
return () => (
{loading.value ? (
) : (
)}
)
}
})
================================================
FILE: src/views/user/info/LatestActivities.tsx
================================================
import { queryLatestActivity, type LatestActivity } from '@/api/user'
import useLoading from '@/hooks/loading'
import ActivityItem from '@/views/user/info/ActivityItem'
import { Card, Grid, Link, List, Skeleton } from '@arco-design/web-vue'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'LatestActivities',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const fillList = new Array(7).fill(undefined)
const activityList = ref([])
const fetchData = async () => {
try {
const res = await queryLatestActivity()
activityList.value = res.data
} catch (error) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
return () => (
{{
extra: () => {t('userInfo.viewAll')},
default: () => (
{loading.value
? fillList.map(() => (
))
: activityList.value.map((item, index) => (
))}
)
}}
)
}
})
================================================
FILE: src/views/user/info/MyProject.tsx
================================================
import { queryMyProjectList, type ProjectItem } from '@/api/user'
import useLoading from '@/hooks/loading'
import {
Avatar,
AvatarGroup,
Card,
Grid,
Link,
Skeleton,
Space,
Typography
} from '@arco-design/web-vue'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'MyProject',
setup() {
const { t } = useI18n()
const dataSource = ref([])
const fillList: unknown[] = new Array(6).fill(undefined)
const { loading, setLoading } = useLoading(true)
const fetchData = () => {
queryMyProjectList()
.then((res) => {
dataSource.value = res.data
})
.catch(() => {})
.finally(() => {
setLoading(false)
})
}
fetchData()
return () => (
{{
default: () => {
return (
{loading.value
? fillList.map(() => (
))
: dataSource.value.map((item) => (
{item.name}
{item.description}
{item.contributors.map((contributor: any) => (
))}
等{item.peopleNumber}人
))}
)
},
extra: () => {t('userInfo.showMore')}
}}
)
}
})
================================================
FILE: src/views/user/info/MyTeam.tsx
================================================
import { queryMyTeamList, type TeamItem } from '@/api/user'
import useLoading from '@/hooks/loading'
import { Avatar, Card, Grid, List, Skeleton } from '@arco-design/web-vue'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'MyTeam',
setup() {
const { t } = useI18n()
const fillList: unknown[] = new Array(4).fill(undefined)
const dataSource = ref([])
const { loading, setLoading } = useLoading(true)
const fetchData = () => {
queryMyTeamList()
.then((res) => {
dataSource.value = res.data
})
.catch(() => {})
.finally(() => {
setLoading(false)
})
}
fetchData()
return () => (
{loading.value
? fillList.map(() => (
))
: dataSource.value.map((item) => (
(
)
}}
title={item.name}
description={`共${item.peopleNumber}人`}
>
))}
)
}
})
================================================
FILE: src/views/user/info/UserInfoHeader.tsx
================================================
import { useUserStore } from '@/store'
import { Avatar, Space, Typography } from '@arco-design/web-vue'
import { IconCamera, IconHome, IconLocation, IconUser } from '@arco-design/web-vue/es/icon'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'UserInfoHeader',
setup() {
const userStore = useUserStore()
return () => (
{{
default: () => (
),
'trigger-icon': () =>
}}
{userStore.name}
{userStore.jobName}
{userStore.organizationName}
{userStore.locationName}
)
}
})
================================================
FILE: src/views/user/info/index.tsx
================================================
import { ViewNames } from '@/types/constants'
import InSiteNotifications from '@/views/user/info/InSiteNotifications'
import LatestActivities from '@/views/user/info/LatestActivities'
import MyProject from '@/views/user/info/MyProject'
import MyTeam from '@/views/user/info/MyTeam'
import UserInfoHeader from '@/views/user/info/UserInfoHeader'
import { Grid, Space } from '@arco-design/web-vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: ViewNames.info,
setup() {
return () => (
)
}
})
================================================
FILE: src/views/user/setting/BasicInformation.tsx
================================================
import type { BasicInfoModel } from '@/api/user'
import {
Button,
Cascader,
Form,
Input,
Message,
Select,
Space,
Textarea,
type FormInstance
} from '@arco-design/web-vue'
import { isEmpty } from 'lodash'
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'BasicInformation',
setup() {
const { t } = useI18n()
const formRef = ref()
const formData = ref({
email: '',
nickname: '',
countryRegion: '',
area: '',
address: '',
profile: ''
})
// actions
const handleReset = () => {
formRef.value?.resetFields()
}
const handleSave = async () => {
try {
const validRet = await formRef.value?.validate()
if (isEmpty(validRet)) Message.success('保存成功')
} catch (e) {
/* empty */
}
}
return () => (
{
}
{
}
{
}
{
}
)
}
})
================================================
FILE: src/views/user/setting/Certification.tsx
================================================
import { queryCertification, type UnitCertification } from '@/api/user'
import useLoading from '@/hooks/loading'
import {
Badge,
Card,
Descriptions,
Link,
Space,
Table,
Tag,
type TableData
} from '@arco-design/web-vue'
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface'
import { computed, defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'Certification',
setup() {
const { t } = useI18n()
const { loading, setLoading } = useLoading(true)
const responseData = ref()
const fetchData = async () => {
try {
const { data } = await queryCertification()
responseData.value = data
} catch (err) {
/* empty */
} finally {
setLoading(false)
}
}
fetchData()
const columns = computed(() => [
{
title: t('userSetting.certification.columns.certificationType'),
render() {
return t('userSetting.certification.cell.certificationType')
}
},
{
title: t('userSetting.certification.columns.certificationContent'),
dataIndex: 'certificationContent'
},
{
title: t('userSetting.certification.columns.status'),
render({ record }: { record: TableData }) {
return record.status === 0 ? (
) : (
)
}
},
{
title: t('userSetting.certification.columns.time'),
dataIndex: 'time'
},
{
title: t('userSetting.certification.columns.operation'),
render: ({ record }: { record: TableData }) => {
if (record.status !== 0) {
return {t('userSetting.certification.button.check')}
}
return (
{t('userSetting.certification.button.check')}
{t('userSetting.certification.button.withdraw')}
)
}
}
])
const descData = computed(() => {
const enterpriseInfo = responseData.value?.enterpriseInfo
if (!enterpriseInfo) return []
const {
accountType,
status,
time,
legalPerson,
certificateType,
authenticationNumber,
enterpriseName,
enterpriseCertificateType,
organizationCode
} = enterpriseInfo
return [
{
label: t('userSetting.certification.label.accountType'),
value: accountType
},
{
label: t('userSetting.certification.label.status'),
value: status
},
{
label: t('userSetting.certification.label.time'),
value: time
},
{
label: t('userSetting.certification.label.legalPerson'),
value: legalPerson
},
{
label: t('userSetting.certification.label.certificateType'),
value: certificateType
},
{
label: t('userSetting.certification.label.authenticationNumber'),
value: authenticationNumber
},
{
label: t('userSetting.certification.label.enterpriseName'),
value: enterpriseName
},
{
label: t('userSetting.certification.label.enterpriseCertificateType'),
value: enterpriseCertificateType
},
{
label: t('userSetting.certification.label.organizationCode'),
value: organizationCode
}
] as DescData[]
})
return () => (
{{
default: () => (
{{
label: ({ label }: { label: string }) => label + ' :',
value: ({ value, data }: { data: DescData; value: unknown }) => {
if (data.label === t('userSetting.certification.label.status'))
return (
已认证
)
else return value
}
}}
),
extra: () => {t('userSetting.certification.extra.enterprise')}
}}
)
}
})
================================================
FILE: src/views/user/setting/SecuritySettings.tsx
================================================
import { useUserStore } from '@/store'
import { Link, List, Typography } from '@arco-design/web-vue'
import { computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import styles from './style.module.scss'
export default defineComponent({
name: 'SecuritySettings',
setup() {
const { t } = useI18n()
const userStore = useUserStore()
const settingList = computed(() => [
{
title: t('userSetting.security.password'),
value: t('userSetting.security.password.tips')
},
{
title: t('userSetting.security.question'),
value: '',
placeholder: t('userSetting.security.question.placeholder')
},
{
title: t('userSetting.security.phone'),
value: userStore.phone ? `${t('userSetting.security.phone.tips')} ${userStore.phone}` : ''
},
{
title: t('userSetting.security.email'),
value: '',
placeholder: t('userSetting.security.email.placeholder')
}
])
return () => (
{settingList.value.map((item) => (
{{
avatar: () => {item.title},
description: () => (
<>
{item.value ? item.value : item.placeholder}
{item.value ? (
{t('userSetting.SecuritySettings.button.update')}
) : (
{t('userSetting.SecuritySettings.button.settings')}
)}
>
)
}}
))}
)
}
})
================================================
FILE: src/views/user/setting/UserPanel.tsx
================================================
import { userUploadApi } from '@/api/user'
import { useUserStore } from '@/store'
import {
Avatar,
Card,
Descriptions,
Message,
Space,
Tag,
Upload,
type FileItem,
type RequestOption
} from '@arco-design/web-vue'
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface'
import { IconCamera } from '@arco-design/web-vue/es/icon'
import { computed, defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'UserPanel',
setup() {
const { t } = useI18n()
const userStore = useUserStore()
const userInfoData = computed(() => [
{
label: t('userSetting.label.name'),
value: userStore.name
},
{
label: t('userSetting.label.certification'),
value: userStore.certification
},
{
label: t('userSetting.label.accountId'),
value: userStore.accountId
},
{
label: t('userSetting.label.phone'),
value: userStore.phone
},
{
label: t('userSetting.label.registrationDate'),
value: userStore.registrationDate
}
])
const fileList = ref()
const handleUploadChange = (fileItemList: FileItem[], fileItem: FileItem) => {
fileList.value = [fileItem]
}
const avatarSrc = computed(() => {
if (fileList.value) {
return fileList.value[0].url
}
return userStore.avatar
})
const customRequest = (options: RequestOption) => {
const controller = new AbortController()
;(async function requestWrap() {
const { onError, onSuccess, fileItem, name = 'file' } = options
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await userUploadApi(formData)
onSuccess(res)
Message.success('上传成功')
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
}
}
}
return () => (
(
{{
'trigger-icon': () => ,
default: () =>
}}
)
}}
showUploadButton={true}
listType="picture-card"
showFileList={false}
onChange={handleUploadChange}
customRequest={customRequest}
/>
{{
label: ({ label }: { label: string }) => {
return label
},
value: ({ value, data }: { value: string; data: DescData }) => {
if (data.label === 'userSetting.label.certification')
return (
已认证
)
else return {value}
}
}}
)
}
})
================================================
FILE: src/views/user/setting/index.tsx
================================================
import { Card, Grid, Tabs } from '@arco-design/web-vue'
import { defineComponent, h } from 'vue'
import { useI18n } from 'vue-i18n'
import BasicInformation from './BasicInformation'
import Certification from './Certification'
import SecuritySettings from './SecuritySettings'
import UserPanel from './UserPanel'
import { ViewNames } from '@/types/constants'
export default defineComponent({
name: ViewNames.setting,
setup() {
const { t } = useI18n()
const componentList = [
{
key: '1',
component: BasicInformation,
getTitle: () => t('userSetting.tab.basicInformation')
},
{
key: '2',
component: SecuritySettings,
getTitle: () => t('userSetting.tab.securitySettings')
},
{
key: '3',
component: Certification,
getTitle: () => t('userSetting.tab.certification')
}
]
return () => (
{componentList.map((item) => {
return (
{h(item.component)}
)
})}
)
}
})
================================================
FILE: src/views/user/setting/style.module.scss
================================================
.list-item {
:global(.arco-list-item-meta-content) {
flex: 1;
border-bottom: 1px solid var(--color-neutral-3);
:global(.arco-list-item-meta-description) {
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
}
}
}
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
// eslint-disable-next-line no-undef
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {}
},
plugins: [],
corePlugins: {
preflight: false
}
}
================================================
FILE: tsconfig.app.json
================================================
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue","src/**/*.json"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
================================================
FILE: tsconfig.json
================================================
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
================================================
FILE: tsconfig.node.json
================================================
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"config/**/*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}