Compare commits

...

29 Commits
master ... mini

Author SHA1 Message Date
kailong321200875 9caa008551 feat: 同步代码 2024-02-22 17:35:05 +08:00
kailong321200875 7c18060e18 feat: 同步代码 2024-02-07 11:07:41 +08:00
kailong321200875 4891f49b7d feat: 同步代码 2024-01-31 14:23:10 +08:00
kailong321200875 07ca222ee0 mod: 解决冲突 2024-01-29 08:52:17 +08:00
kailong321200875 bfda9f6485 feat: 同步代码 2024-01-29 08:50:16 +08:00
kailong321200875 67a202fef3 feat: 同步代码 2024-01-21 15:09:34 +08:00
kailong321200875 48c5c9af8a feat: 同步代码 2024-01-18 16:13:14 +08:00
kailong321200875 1af33d8a7e feat: 同步master 2024-01-16 11:20:49 +08:00
kailong321200875 9eb1356c4e feat: 同步master 2024-01-09 14:25:23 +08:00
kailong321200875 6a83d08309 feat: 同步master代码 2024-01-03 10:39:42 +08:00
kailong321200875 607b73a7b3 feat: 更新mini分支 2023-12-19 09:46:22 +08:00
kailong321200875 a1e89b7c8a chore: node最低要求18 2023-11-22 14:47:35 +08:00
kailong321200875 d93d1ee6a0 chore: 更新依赖 2023-11-22 14:44:18 +08:00
kailong321200875 96c5a968ef chore: 更新依赖 2023-11-22 14:43:31 +08:00
kailong321200875 c954f2b7e0 fix: 修复BUG 2023-10-14 11:29:40 +08:00
Archer 1df9644a11
Merge pull request #353 from wen-shiqiang/mini
fix: 修复BUG
2023-10-11 09:32:59 +08:00
文士强 47f4b5a8ae fix: 修复BUG 2023-10-10 19:39:00 +08:00
kailong321200875 fbe68ba683 fix: 修复BUG 2023-10-07 15:07:37 +08:00
kailong321200875 9652712677 fix: 修复Table组件插槽传参错误 2023-08-12 07:57:08 +08:00
Archer 7959b6c62a
Merge pull request #312 from maoxuner/mini
fix: #311 移除stylelint-config-prettier依赖
2023-08-10 08:50:28 +08:00
陆伯言 25e416e3bb
fix: #311 移除stylelint-config-prettier依赖 2023-08-09 20:59:12 +08:00
kailong321200875 5d1356c4a9 fix: 修复Collapse点击区域问题 2023-08-09 17:23:57 +08:00
kailong321200875 b0daac8471 mod: 删除无用文件 2023-08-09 17:17:45 +08:00
Archer d46119e174
Merge pull request #310 from maoxuner/mini
fix: 404页面缺失
2023-08-09 17:07:19 +08:00
陆伯言 7efed9ed12
fix: 404页面缺失 2023-08-09 16:56:08 +08:00
kailong321200875 5f2048d52d fix: 修复Form组件BUG 2023-08-05 20:38:07 +08:00
kailong321200875 97c293bc6e Merge branch 'v2' into mini 2023-08-05 17:50:04 +08:00
kailong321200875 c3a2cc0a5f Merge branch 'v2' into mini 2023-08-05 14:15:55 +08:00
kailong321200875 255843d4c0 feat: 简易版本 2023-08-05 11:09:40 +08:00
165 changed files with 1830 additions and 11754 deletions

View File

@ -1,11 +1,20 @@
# 环境 # 环境
NODE_ENV=development VITE_NODE_ENV=development
# 接口前缀 # 接口前缀
VITE_API_BASE_PATH=base VITE_API_BASE_PATH=
# 打包路径 # 打包路径
VITE_BASE_PATH=/ VITE_BASE_PATH=/
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=ElementAdmin
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=true
# 是否开启mock
VITE_USE_MOCK=true
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true

View File

@ -1,8 +1,8 @@
# 环境 # 环境
NODE_ENV=production VITE_NODE_ENV=production
# 接口前缀 # 接口前缀
VITE_API_BASE_PATH=dev VITE_API_BASE_PATH=
# 打包路径 # 打包路径
VITE_BASE_PATH=/dist-dev/ VITE_BASE_PATH=/dist-dev/
@ -21,3 +21,18 @@ VITE_OUT_DIR=dist-dev
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=ElementAdmin
# 是否包分析
VITE_USE_BUNDLE_ANALYZER=false
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock
VITE_USE_MOCK=true
# 是否切割css
VITE_USE_CSS_SPLIT=true
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true

View File

@ -1,8 +1,8 @@
# 环境 # 环境
NODE_ENV=production VITE_NODE_ENV=production
# 接口前缀 # 接口前缀
VITE_API_BASE_PATH=pro VITE_API_BASE_PATH=
# 打包路径 # 打包路径
VITE_BASE_PATH=/vue-element-plus-admin/ VITE_BASE_PATH=/vue-element-plus-admin/
@ -21,3 +21,18 @@ VITE_OUT_DIR=dist-pro
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=ElementAdmin
# 是否包分析
VITE_USE_BUNDLE_ANALYZER=false
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock
VITE_USE_MOCK=true
# 是否切割css
VITE_USE_CSS_SPLIT=true
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true

View File

@ -1,8 +1,8 @@
# 环境 # 环境
NODE_ENV=production VITE_NODE_ENV=production
# 接口前缀 # 接口前缀
VITE_API_BASE_PATH=pro VITE_API_BASE_PATH=
# 打包路径 # 打包路径
VITE_BASE_PATH=/ VITE_BASE_PATH=/
@ -21,3 +21,18 @@ VITE_OUT_DIR=dist-pro
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=ElementAdmin
# 是否包分析
VITE_USE_BUNDLE_ANALYZER=true
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock
VITE_USE_MOCK=true
# 是否切割css
VITE_USE_CSS_SPLIT=true
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true

View File

@ -1,8 +1,8 @@
# 环境 # 环境
NODE_ENV=production VITE_NODE_ENV=production
# 接口前缀 # 接口前缀
VITE_API_BASE_PATH=test VITE_API_BASE_PATH=
# 打包路径 # 打包路径
VITE_BASE_PATH=/dist-test/ VITE_BASE_PATH=/dist-test/
@ -21,3 +21,18 @@ VITE_OUT_DIR=dist-test
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=ElementAdmin
# 是否包分析
VITE_USE_BUNDLE_ANALYZER=false
# 是否全量引入element-plus样式
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock
VITE_USE_MOCK=true
# 是否切割css
VITE_USE_CSS_SPLIT=false
# 是否使用在线图标
VITE_USE_ONLINE_ICON=true

View File

@ -24,6 +24,7 @@ module.exports = defineConfig({
'plugin:prettier/recommended' 'plugin:prettier/recommended'
], ],
rules: { rules: {
'vue/no-setup-props-destructure': 'off',
'vue/script-setup-uses-vars': 'error', 'vue/script-setup-uses-vars': 'error',
'vue/no-reserved-component-names': 'off', 'vue/no-reserved-component-names': 'off',
'@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/ban-ts-ignore': 'off',

View File

@ -1,130 +0,0 @@
name: Automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
status: {}
jobs:
# 合并发布版本的 pr 到 master
auto-merge:
runs-on: ubuntu-latest
steps:
- name: Automerge
uses: 'pascalgn/automerge-action@v0.14.3'
env:
GITHUB_TOKEN: '${{ secrets.TOKEN }}'
MERGE_LABELS: ''
MERGE_FILTER_AUTHOR: 'kailong321200875'
push-to-gh-pages:
needs: [auto-merge]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: use Node.js 16
uses: actions/setup-node@v2.1.2
with:
node-version: '16.x'
- name: Set SSH Environment
env:
DOCS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh/
echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com > ~/.ssh/known_hosts
chmod 700 ~/.ssh && chmod 600 ~/.ssh/*
git config --local user.email "321200875@qq.com"
git config --local user.name "kailong321200875"
# 发布到 github
- name: Build Github
run: |
pnpm install --no-frozen-lockfile
pnpm run build:pro
- name: Deploy Github
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{secrets.ACTIONS_DEPLOY_KEY}}
publish_branch: gh-pages
publish_dir: ./dist-pro
force_orphan: true
cname: element-plus-admin.cn
push-to-gh-pages-gitee:
needs: [auto-merge]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: use Node.js 16
uses: actions/setup-node@v2.1.2
with:
node-version: '16.x'
- name: Set SSH Environment
env:
DOCS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh/
echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com > ~/.ssh/known_hosts
chmod 700 ~/.ssh && chmod 600 ~/.ssh/*
git config --local user.email "321200875@qq.com"
git config --local user.name "kailong321200875"
- name: Build Gitee
run: |
pnpm install --no-frozen-lockfile
pnpm run build:gitee
# 发布到 gitee
- name: Deploy Gitee
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{secrets.ACTIONS_DEPLOY_KEY}}
publish_branch: gh-pages-gitee
publish_dir: ./dist-pro
force_orphan: true
- name: Sync Github Repos To Gitee # 名字随便起
uses: Yikun/hub-mirror-action@v1.1 # 使用Yikun/hub-mirror-action
with:
src: github/kailong321200875 # 源端账户名(github)
dst: gitee/kailong110120130 # 目的端账户名(gitee)
dst_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # SSH密钥对中的私钥
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌
account_type: user # 账户类型
clone_style: 'https' # 使用https方式进行clone也可以使用ssh
debug: true # 启用后会显示所有执行命令
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库
static_list: 'vue-element-plus-admin' # 静态同步列表,在此填写需要同步的仓库名称,可填写多个
timeout: '600s' # git超时设置超时后会自动重试git操作

View File

@ -1,25 +0,0 @@
# on:
# push:
# branches:
# - master
# jobs:
# contrib-readme-en-job:
# runs-on: ubuntu-latest
# name: A job to automate contrib in readme
# steps:
# - name: Contribute List
# uses: akhilmhdh/contributors-readme-action@v2.3.4
# env:
# GITHUB_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }}
# contrib-readme-job:
# runs-on: ubuntu-latest
# name: A job to automate contrib in readme.zh-CN
# steps:
# - name: Contribute List
# uses: akhilmhdh/contributors-readme-action@v2.3.4
# with:
# readme_path: README.zh-CN.md
# env:
# GITHUB_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }}

View File

@ -1,18 +0,0 @@
on:
push:
branches:
- release
name: Release
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v3
id: release
with:
token: ${{ secrets.TOKEN }}
release-type: node
package-name: standard-version
changelog-types: '[{"type": "types", "section":"Types", "hidden": false},{"type": "revert", "section":"Reverts", "hidden": false},{"type": "feat", "section": "Features", "hidden": false},{"type": "fix", "section": "Bug Fixes", "hidden": false},{"type": "improvement", "section": "Feature Improvements", "hidden": false},{"type": "docs", "section":"Docs", "hidden": false},{"type": "style", "section":"Styling", "hidden": false},{"type": "refactor", "section":"Code Refactoring", "hidden": false},{"type": "perf", "section":"Performance Improvements", "hidden": false},{"type": "test", "section":"Tests", "hidden": false},{"type": "build", "section":"Build System", "hidden": false},{"type": "ci", "section":"CI", "hidden":false}]'

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ dist-ssr
/dist* /dist*
*-lock.* *-lock.*
pnpm-debug pnpm-debug
stats.html

View File

@ -2,7 +2,7 @@
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"prettier.enable": false, "prettier.enable": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint" "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"

View File

@ -1,6 +1,6 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.glob('./**/*.ts', { const modules = import.meta.glob('./**/*.mock.ts', {
import: 'default', import: 'default',
eager: true eager: true
}) })

View File

@ -1,97 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
const { code } = config
const timeout = 1000
export default [
// 分析页统计接口
{
url: '/analysis/total',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: {
users: 102400,
messages: 81212,
moneys: 9280,
shoppings: 13600
}
}
}
}
},
// 用户来源
{
url: '/analysis/userAccessSource',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{ value: 1000, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
}
}
}
},
// 每周用户活跃量
{
url: '/analysis/weeklyUserActivity',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
}
}
}
},
// 每月销售额
{
url: '/analysis/monthlySales',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{ estimate: 100, actual: 120, name: 'analysis.january' },
{ estimate: 120, actual: 82, name: 'analysis.february' },
{ estimate: 161, actual: 91, name: 'analysis.march' },
{ estimate: 134, actual: 154, name: 'analysis.april' },
{ estimate: 105, actual: 162, name: 'analysis.may' },
{ estimate: 160, actual: 140, name: 'analysis.june' },
{ estimate: 165, actual: 145, name: 'analysis.july' },
{ estimate: 114, actual: 250, name: 'analysis.august' },
{ estimate: 163, actual: 134, name: 'analysis.september' },
{ estimate: 185, actual: 56, name: 'analysis.october' },
{ estimate: 118, actual: 99, name: 'analysis.november' },
{ estimate: 123, actual: 123, name: 'analysis.december' }
]
}
}
}
}
] as MockMethod[]

View File

@ -1,219 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import { toAnyString } from '@/utils'
import Mock from 'mockjs'
const { code } = config
const departmentList: any = []
const citys = ['厦门总公司', '北京分公司', '上海分公司', '福州分公司', '深圳分公司', '杭州分公司']
for (let i = 0; i < 5; i++) {
departmentList.push({
// 部门名称
departmentName: citys[i],
id: toAnyString(),
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
// 备注
remark: '@cword(10, 15)',
children: [
{
// 部门名称
departmentName: '研发部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '产品部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '运营部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '市场部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '销售部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '客服部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
}
]
})
}
export default [
// 列表接口
{
url: '/department/list',
method: 'get',
response: () => {
return {
data: {
code: code,
data: {
list: departmentList
}
}
}
}
},
{
url: '/department/table/list',
method: 'get',
response: () => {
return {
data: {
code: code,
data: {
list: departmentList,
total: 5
}
}
}
}
},
{
url: '/department/users',
method: 'get',
timeout: 1000,
response: ({ query }) => {
const { pageSize } = query
// 根据pageSize来创建数据
const mockList: any = []
for (let i = 0; i < pageSize; i++) {
mockList.push(
Mock.mock({
// 用户名
username: '@cname',
// 账号
account: '@first',
// 邮箱
email: '@EMAIL',
// 创建时间
createTime: '@datetime',
// 角色
role: '@first',
// 用户id
id: toAnyString()
})
)
}
return {
data: {
code: code,
data: {
total: 100,
list: mockList
}
}
}
}
},
// 保存接口
{
url: '/department/user/save',
method: 'post',
timeout: 1000,
response: () => {
return {
data: {
code: code,
data: 'success'
}
}
}
},
// 删除接口
{
url: '/department/user/delete',
method: 'post',
response: ({ body }) => {
const ids = body.ids
if (!ids) {
return {
code: '500',
message: '请选择需要删除的数据'
}
} else {
return {
data: {
code: code,
data: 'success'
}
}
}
}
},
// 保存接口
{
url: '/department/save',
method: 'post',
timeout: 1000,
response: () => {
return {
data: {
code: code,
data: 'success'
}
}
}
},
// 删除接口
{
url: '/department/delete',
method: 'post',
response: ({ body }) => {
const ids = body.ids
if (!ids) {
return {
code: '500',
message: '请选择需要删除的数据'
}
} else {
return {
data: {
code: code,
data: 'success'
}
}
}
}
}
] as MockMethod[]

View File

@ -1,67 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
const { code } = config
const timeout = 1000
const dictObj: Recordable = {
importance: [
{
value: 0,
label: 'tableDemo.commonly'
},
{
value: 1,
label: 'tableDemo.good'
},
{
value: 2,
label: 'tableDemo.important'
}
]
}
export default [
// 字典接口
{
url: '/dict/list',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: dictObj
}
}
}
},
// 获取某个字典
{
url: '/dict/one',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{
label: 'test1',
value: 0
},
{
label: 'test2',
value: 1
},
{
label: 'test3',
value: 2
}
]
}
}
}
}
] as MockMethod[]

View File

@ -1,246 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import Mock from 'mockjs'
import { toAnyString } from '@/utils'
const { code } = config
const timeout = 1000
export default [
// 列表接口
{
url: '/menu/list',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: {
list: [
{
path: '/dashboard',
component: '#',
redirect: '/dashboard/analysis',
name: 'Dashboard',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '首页',
icon: 'ant-design:dashboard-filled',
alwaysShow: true
},
children: [
{
path: 'analysis',
component: 'views/Dashboard/Analysis',
name: 'Analysis',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '分析页',
noCache: true
}
},
{
path: 'workplace',
component: 'views/Dashboard/Workplace',
name: 'Workplace',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '工作台',
noCache: true
}
}
]
},
{
path: '/external-link',
component: '#',
meta: {
title: '文档',
icon: 'clarity:document-solid'
},
name: 'ExternalLink',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
children: [
{
path: 'https://element-plus-admin-doc.cn/',
name: 'DocumentLink',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '文档'
}
}
]
},
{
path: '/level',
component: '#',
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '菜单',
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: '##',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1'
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: '##',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1-1',
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: 'views/Level/Menu111',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '菜单1-1-1',
permission: ['edit', 'add']
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: 'views/Level/Menu12',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '菜单1-2',
permission: ['edit', 'add']
}
}
]
},
{
path: 'menu2',
name: 'Menu2Demo',
component: 'views/Level/Menu2',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '菜单2',
permission: ['edit', 'add']
}
}
]
},
{
path: '/example',
component: '#',
redirect: '/example/example-dialog',
name: 'Example',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '综合示例',
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-dialog',
component: 'views/Example/Dialog/ExampleDialog',
name: 'ExampleDialog',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '综合示例-弹窗',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-page',
component: 'views/Example/Page/ExamplePage',
name: 'ExamplePage',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '综合示例-页面',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-add',
component: 'views/Example/Page/ExampleAdd',
name: 'ExampleAdd',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '综合示例-新增',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-edit',
component: 'views/Example/Page/ExampleEdit',
name: 'ExampleEdit',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '综合示例-编辑',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-detail',
component: 'views/Example/Page/ExampleDetail',
name: 'ExampleDetail',
status: Mock.Random.integer(0, 1),
id: toAnyString(),
meta: {
title: '综合示例-详情',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
}
]
}
]
}
}
}
}
}
] as MockMethod[]

91
mock/role/index.mock.ts Normal file
View File

@ -0,0 +1,91 @@
import { MockMethod } from 'vite-plugin-mock'
import { SUCCESS_CODE } from '@/constants'
const timeout = 1000
const adminList = [
{
path: '/level',
component: '#',
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
meta: {
title: 'router.level',
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: '##',
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: 'router.menu1'
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: '##',
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: 'router.menu11',
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: 'views/Level/Menu111',
meta: {
title: 'router.menu111'
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: 'views/Level/Menu12',
meta: {
title: 'router.menu12'
}
}
]
},
{
path: 'menu2',
name: 'Menu2Demo',
component: 'views/Level/Menu2',
meta: {
title: 'router.menu2'
}
}
]
}
]
const testList: string[] = [
'/level',
'/level/menu1',
'/level/menu1/menu1-1',
'/level/menu1/menu1-1/menu1-1-1',
'/level/menu1/menu1-2',
'/level/menu2'
]
export default [
// 列表接口
{
url: '/mock/role/list',
method: 'get',
timeout,
response: ({ query }) => {
const { roleName } = query
return {
code: SUCCESS_CODE,
data: roleName === 'admin' ? adminList : testList
}
}
}
] as MockMethod[]

View File

@ -1,546 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
const { code } = config
const timeout = 1000
const adminList = [
{
path: '/dashboard',
component: '#',
redirect: '/dashboard/analysis',
name: 'Dashboard',
meta: {
title: 'router.dashboard',
icon: 'ant-design:dashboard-filled',
alwaysShow: true
},
children: [
{
path: 'analysis',
component: 'views/Dashboard/Analysis',
name: 'Analysis',
meta: {
title: 'router.analysis',
noCache: true
}
},
{
path: 'workplace',
component: 'views/Dashboard/Workplace',
name: 'Workplace',
meta: {
title: 'router.workplace',
noCache: true
}
}
]
},
{
path: '/external-link',
component: '#',
meta: {},
name: 'ExternalLink',
children: [
{
path: 'https://element-plus-admin-doc.cn/',
name: 'DocumentLink',
meta: {
title: 'router.document',
icon: 'clarity:document-solid'
}
}
]
},
{
path: '/guide',
component: '#',
name: 'Guide',
meta: {},
children: [
{
path: 'index',
component: 'views/Guide/Guide',
name: 'GuideDemo',
meta: {
title: 'router.guide',
icon: 'cib:telegram-plane'
}
}
]
},
{
path: '/components',
component: '#',
redirect: '/components/form/default-form',
name: 'ComponentsDemo',
meta: {
title: 'router.component',
icon: 'bx:bxs-component',
alwaysShow: true
},
children: [
{
path: 'form',
component: '##',
name: 'Form',
meta: {
title: 'router.form',
alwaysShow: true
},
children: [
{
path: 'default-form',
component: 'views/Components/Form/DefaultForm',
name: 'DefaultForm',
meta: {
title: 'router.defaultForm'
}
},
{
path: 'use-form',
component: 'views/Components/Form/UseFormDemo',
name: 'UseForm',
meta: {
title: 'UseForm'
}
}
]
},
{
path: 'table',
component: '##',
redirect: '/components/table/default-table',
name: 'TableDemo',
meta: {
title: 'router.table',
alwaysShow: true
},
children: [
{
path: 'default-table',
component: 'views/Components/Table/DefaultTable',
name: 'DefaultTable',
meta: {
title: 'router.defaultTable'
}
},
{
path: 'use-table',
component: 'views/Components/Table/UseTableDemo',
name: 'UseTable',
meta: {
title: 'UseTable'
}
},
{
path: 'tree-table',
component: 'views/Components/Table/TreeTable',
name: 'TreeTable',
meta: {
title: 'router.TreeTable'
}
},
{
path: 'table-image-preview',
component: 'views/Components/Table/TableImagePreview',
name: 'TableImagePreview',
meta: {
title: 'router.PicturePreview'
}
},
{
path: 'ref-table',
component: 'views/Components/Table/RefTable',
name: 'RefTable',
meta: {
title: 'RefTable'
}
}
]
},
{
path: 'editor-demo',
component: '##',
redirect: '/components/editor-demo/editor',
name: 'EditorDemo',
meta: {
title: 'router.editor',
alwaysShow: true
},
children: [
{
path: 'editor',
component: 'views/Components/Editor/Editor',
name: 'Editor',
meta: {
title: 'router.richText'
}
}
]
},
{
path: 'search',
component: 'views/Components/Search',
name: 'Search',
meta: {
title: 'router.search'
}
},
{
path: 'descriptions',
component: 'views/Components/Descriptions',
name: 'Descriptions',
meta: {
title: 'router.descriptions'
}
},
{
path: 'image-viewer',
component: 'views/Components/ImageViewer',
name: 'ImageViewer',
meta: {
title: 'router.imageViewer'
}
},
{
path: 'dialog',
component: 'views/Components/Dialog',
name: 'Dialog',
meta: {
title: 'router.dialog'
}
},
{
path: 'icon',
component: 'views/Components/Icon',
name: 'Icon',
meta: {
title: 'router.icon'
}
},
{
path: 'echart',
component: 'views/Components/Echart',
name: 'Echart',
meta: {
title: 'router.echart'
}
},
{
path: 'count-to',
component: 'views/Components/CountTo',
name: 'CountTo',
meta: {
title: 'router.countTo'
}
},
{
path: 'qrcode',
component: 'views/Components/Qrcode',
name: 'Qrcode',
meta: {
title: 'router.qrcode'
}
},
{
path: 'highlight',
component: 'views/Components/Highlight',
name: 'Highlight',
meta: {
title: 'router.highlight'
}
},
{
path: 'infotip',
component: 'views/Components/Infotip',
name: 'Infotip',
meta: {
title: 'router.infotip'
}
},
{
path: 'input-password',
component: 'views/Components/InputPassword',
name: 'InputPassword',
meta: {
title: 'router.inputPassword'
}
},
{
path: 'sticky',
component: 'views/Components/Sticky',
name: 'Sticky',
meta: {
title: 'router.sticky'
}
}
]
},
{
path: '/hooks',
component: '#',
redirect: '/hooks/useWatermark',
name: 'Hooks',
meta: {
title: 'hooks',
icon: 'ic:outline-webhook',
alwaysShow: true
},
children: [
{
path: 'useWatermark',
component: 'views/hooks/useWatermark',
name: 'UseWatermark',
meta: {
title: 'useWatermark'
}
},
{
path: 'useCrudSchemas',
component: 'views/hooks/useCrudSchemas',
name: 'UseCrudSchemas',
meta: {
title: 'useCrudSchemas'
}
}
]
},
{
path: '/level',
component: '#',
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
meta: {
title: 'router.level',
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: '##',
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: 'router.menu1'
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: '##',
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: 'router.menu11',
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: 'views/Level/Menu111',
meta: {
title: 'router.menu111'
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: 'views/Level/Menu12',
meta: {
title: 'router.menu12'
}
}
]
},
{
path: 'menu2',
name: 'Menu2Demo',
component: 'views/Level/Menu2',
meta: {
title: 'router.menu2'
}
}
]
},
{
path: '/example',
component: '#',
redirect: '/example/example-dialog',
name: 'Example',
meta: {
title: 'router.example',
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-dialog',
component: 'views/Example/Dialog/ExampleDialog',
name: 'ExampleDialog',
meta: {
title: 'router.exampleDialog'
}
},
{
path: 'example-page',
component: 'views/Example/Page/ExamplePage',
name: 'ExamplePage',
meta: {
title: 'router.examplePage'
}
},
{
path: 'example-add',
component: 'views/Example/Page/ExampleAdd',
name: 'ExampleAdd',
meta: {
title: 'router.exampleAdd',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page'
}
},
{
path: 'example-edit',
component: 'views/Example/Page/ExampleEdit',
name: 'ExampleEdit',
meta: {
title: 'router.exampleEdit',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page'
}
},
{
path: 'example-detail',
component: 'views/Example/Page/ExampleDetail',
name: 'ExampleDetail',
meta: {
title: 'router.exampleDetail',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page'
}
}
]
},
{
path: '/error',
component: '#',
redirect: '/error/404',
name: 'Error',
meta: {
title: 'router.errorPage',
icon: 'ci:error',
alwaysShow: true
},
children: [
{
path: '404-demo',
component: 'views/Error/404',
name: '404Demo',
meta: {
title: '404'
}
},
{
path: '403-demo',
component: 'views/Error/403',
name: '403Demo',
meta: {
title: '403'
}
},
{
path: '500-demo',
component: 'views/Error/500',
name: '500Demo',
meta: {
title: '500'
}
}
]
}
]
const testList: string[] = [
'/dashboard',
'/dashboard/analysis',
'/dashboard/workplace',
'external-link',
'https://element-plus-admin-doc.cn/',
'/guide',
'/guide/index',
'/components',
'/components/form',
'/components/form/default-form',
'/components/form/use-form',
'/components/form/ref-form',
'/components/table',
'/components/table/default-table',
'/components/table/use-table',
'/components/table/tree-table',
'/components/table/table-image-preview',
'/components/table/ref-table',
'/components/editor-demo',
'/components/editor-demo/editor',
'/components/search',
'/components/descriptions',
'/components/image-viewer',
'/components/dialog',
'/components/icon',
'/components/echart',
'/components/count-to',
'/components/qrcode',
'/components/highlight',
'/components/infotip',
'/Components/InputPassword',
'/Components/Sticky',
'/hooks',
'/hooks/useWatermark',
'/hooks/useCrudSchemas',
'/level',
'/level/menu1',
'/level/menu1/menu1-1',
'/level/menu1/menu1-1/menu1-1-1',
'/level/menu1/menu1-2',
'/level/menu2',
'/example',
'/example/example-dialog',
'/example/example-page',
'/example/example-add',
'/example/example-edit',
'/example/example-detail',
'/error',
'/error/404-demo',
'/error/403-demo',
'/error/500-demo'
]
export default [
// 列表接口
{
url: '/role/list',
method: 'get',
timeout,
response: ({ query }) => {
const { roleName } = query
return {
data: {
code: code,
data: roleName === 'admin' ? adminList : testList
}
}
}
}
] as MockMethod[]

View File

@ -1,268 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import { toAnyString } from '@/utils'
import Mock from 'mockjs'
const { code } = config
const timeout = 1000
const count = 100
const baseContent =
'<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
interface ListProps {
id: string
author: string
title: string
content: string
importance: number
display_time: string
pageviews: number
image_uri: string
}
interface TreeListProps {
id: string
author: string
title: string
content: string
importance: number
display_time: string
pageviews: number
children: TreeListProps[]
}
let List: ListProps[] = []
for (let i = 0; i < count; i++) {
List.push(
Mock.mock({
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)',
image_uri: Mock.Random.image('@integer(300, 5000)x@integer(300, 5000)')
})
)
}
const treeList: TreeListProps[] = []
for (let i = 0; i < count; i++) {
treeList.push(
Mock.mock({
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)',
children: [
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)',
children: [
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}
]
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}
]
// image_uri
})
)
}
export default [
// 树形列表接口
{
url: '/example/treeList',
method: 'get',
timeout,
response: ({ query }) => {
const { title, pageIndex, pageSize } = query
const mockList = treeList.filter((item) => {
if (title && item.title.indexOf(title) < 0) return false
return true
})
const pageList = mockList.filter(
(_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
)
return {
data: {
code: code,
data: {
total: mockList.length,
list: pageList
}
}
}
}
},
// 列表接口
{
url: '/example/list',
method: 'get',
timeout,
response: ({ query }) => {
const { title, pageIndex, pageSize } = query
const mockList = List.filter((item) => {
if (title && item.title.indexOf(title) < 0) return false
return true
})
const pageList = mockList.filter(
(_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
)
return {
data: {
code: code,
data: {
total: mockList.length,
list: pageList
}
}
}
}
},
// 保存接口
{
url: '/example/save',
method: 'post',
timeout,
response: ({ body }) => {
if (!body.id) {
List = [
Object.assign(body, {
id: toAnyString()
})
].concat(List)
return {
data: {
code: code,
data: 'success'
}
}
} else {
List.map((item) => {
if (item.id === body.id) {
for (const key in item) {
item[key] = body[key]
}
}
})
return {
data: {
code: code,
data: 'success'
}
}
}
}
},
// 详情接口
{
url: '/example/detail',
method: 'get',
response: ({ query }) => {
const { id } = query
for (const example of List) {
if (example.id === id) {
return {
data: {
code: code,
data: example
}
}
}
}
}
},
// 删除接口
{
url: '/example/delete',
method: 'post',
response: ({ body }) => {
const ids = body.ids
if (!ids) {
return {
code: '500',
message: '请选择需要删除的数据'
}
} else {
let i = List.length
while (i--) {
if (ids.indexOf(List[i].id) !== -1) {
List.splice(i, 1)
}
}
return {
data: {
code: code,
data: 'success'
}
}
}
}
}
] as MockMethod[]

View File

@ -1,7 +1,4 @@
import config from '@/config/axios/config' import { SUCCESS_CODE } from '@/constants'
import { MockMethod } from 'vite-plugin-mock'
const { code } = config
const timeout = 1000 const timeout = 1000
@ -31,7 +28,7 @@ const List: {
export default [ export default [
// 列表接口 // 列表接口
{ {
url: '/user/list', url: '/mock/user/list',
method: 'get', method: 'get',
response: ({ query }) => { response: ({ query }) => {
const { username, pageIndex, pageSize } = query const { username, pageIndex, pageSize } = query
@ -45,19 +42,17 @@ export default [
) )
return { return {
code: SUCCESS_CODE,
data: { data: {
code: code, total: mockList.length,
data: { list: pageList
total: mockList.length,
list: pageList
}
} }
} }
} }
}, },
// 登录接口 // 登录接口
{ {
url: '/user/login', url: '/mock/user/login',
method: 'post', method: 'post',
timeout, timeout,
response: ({ body }) => { response: ({ body }) => {
@ -67,10 +62,8 @@ export default [
if (user.username === data.username && user.password === data.password) { if (user.username === data.username && user.password === data.password) {
hasUser = true hasUser = true
return { return {
data: { code: SUCCESS_CODE,
code: code, data: user
data: user
}
} }
} }
} }
@ -84,16 +77,14 @@ export default [
}, },
// 退出接口 // 退出接口
{ {
url: '/user/loginOut', url: '/mock/user/loginOut',
method: 'get', method: 'get',
timeout, timeout,
response: () => { response: () => {
return { return {
data: { code: SUCCESS_CODE,
code: code, data: null
data: null
}
} }
} }
} }
] as MockMethod[] ]

View File

@ -1,182 +0,0 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
const { code } = config
const timeout = 1000
export default [
// 获取统计
{
url: '/workplace/total',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: {
project: 40,
access: 2340,
todo: 10
}
}
}
}
},
// 获取项目
{
url: '/workplace/project',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{
name: 'Github',
icon: 'akar-icons:github-fill',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Vue',
icon: 'logos:vue',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Angular',
icon: 'logos:angular-icon',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'React',
icon: 'logos:react',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Webpack',
icon: 'logos:webpack',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
}
]
}
}
}
},
// 获取动态
{
url: '/workplace/dynamic',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
}
]
}
}
}
},
// 获取团队信息
{
url: '/workplace/team',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{
name: 'Github',
icon: 'akar-icons:github-fill'
},
{
name: 'Vue',
icon: 'logos:vue'
},
{
name: 'Angular',
icon: 'logos:angular-icon'
},
{
name: 'React',
icon: 'logos:react'
},
{
name: 'Webpack',
icon: 'logos:webpack'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite'
}
]
}
}
}
},
// 获取指数
{
url: '/workplace/radar',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: [
{ name: 'workplace.quote', max: 65, personal: 42, team: 50 },
{ name: 'workplace.contribution', max: 160, personal: 30, team: 140 },
{ name: 'workplace.hot', max: 300, personal: 20, team: 28 },
{ name: 'workplace.yield', max: 130, personal: 35, team: 35 },
{ name: 'workplace.follow', max: 100, personal: 80, team: 90 }
]
}
}
}
}
] as MockMethod[]

View File

@ -1,23 +1,23 @@
{ {
"name": "vue-element-plus-admin", "name": "vue-element-plus-admin",
"version": "1.9.9", "version": "2.5.6",
"description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。", "description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
"author": "Archer <502431556@qq.com>", "author": "Archer <502431556@qq.com>",
"private": false, "private": false,
"scripts": { "scripts": {
"i": "pnpm install", "i": "pnpm install",
"dev": "vite --mode base", "dev": "pnpm vite --mode base",
"ts:check": "vue-tsc --noEmit --skipLibCheck", "ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
"build:pro": "vite build --mode pro", "build:pro": "pnpm vite build --mode pro",
"build:gitee": "vite build --mode gitee", "build:gitee": "pnpm vite build --mode gitee",
"build:dev": "vite build --mode dev", "build:dev": "pnpm vite build --mode dev",
"build:test": "npm run ts:check && vite build --mode test", "build:test": "pnpm vite build --mode test",
"serve:pro": "vite preview --mode pro", "serve:pro": "pnpm vite preview --mode pro",
"serve:dev": "vite preview --mode dev", "serve:dev": "pnpm vite preview --mode dev",
"serve:test": "vite preview --mode test", "serve:test": "pnpm vite preview --mode test",
"npm:check": "npx npm-check-updates", "npm:check": "pnpx npm-check-updates -u",
"clean": "npx rimraf node_modules", "clean": "pnpx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache", "clean:cache": "pnpx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"", "lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"",
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", "lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
@ -26,93 +26,100 @@
"p": "plop" "p": "plop"
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "^8.4.0",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.7.2",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.3", "@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.4.0", "axios": "^1.6.7",
"dayjs": "^1.11.9", "cropperjs": "^1.6.1",
"dayjs": "^1.11.10",
"driver.js": "^1.3.1",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.3.8", "element-plus": "2.5.5",
"intro.js": "^7.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.4", "pinia": "^2.1.7",
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.2", "qs": "^6.11.2",
"sortablejs": "^1.15.0", "url": "^0.11.3",
"url": "^0.11.1", "vue": "3.4.15",
"vue": "3.3.4", "vue-draggable-plus": "^0.3.5",
"vue-i18n": "9.2.2", "vue-i18n": "9.9.1",
"vue-router": "^4.2.4", "vue-json-pretty": "^2.3.0",
"vue-types": "^5.1.0" "vue-router": "^4.2.5",
"vue-types": "^5.1.1",
"xgplayer": "^3.0.12"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.6.7", "@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^17.6.7", "@commitlint/config-conventional": "^18.6.0",
"@iconify/json": "^2.2.92", "@iconify/json": "^2.2.180",
"@intlify/unplugin-vue-i18n": "^0.12.2", "@intlify/unplugin-vue-i18n": "^2.0.0",
"@purge-icons/generated": "^0.9.0", "@types/fs-extra": "^11.0.4",
"@types/intro.js": "^5.1.1", "@types/inquirer": "^9.0.7",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.4.2", "@types/node": "^20.11.16",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.1", "@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.11",
"@types/sortablejs": "^1.15.1", "@types/sortablejs": "^1.15.7",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^6.21.0",
"@unocss/transformer-variant-group": "^0.53.5", "@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-legacy": "^4.1.0", "@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.0.1", "@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue-macros/volar": "^0.12.2", "autoprefixer": "^10.4.17",
"autoprefixer": "^10.4.14", "chalk": "^5.3.0",
"consola": "^3.2.3", "consola": "^3.2.3",
"eslint": "^8.45.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^1.21.0", "eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.15.1", "eslint-plugin-vue": "^9.21.1",
"husky": "^8.0.3", "esno": "^4.0.0",
"less": "^4.1.3", "fs-extra": "^11.2.0",
"lint-staged": "^13.2.3", "husky": "^9.0.10",
"plop": "^3.1.2", "inquirer": "^9.2.14",
"postcss": "^8.4.26", "less": "^4.2.0",
"postcss-html": "^1.5.0", "lint-staged": "^15.2.2",
"plop": "^4.0.1",
"postcss": "^8.4.34",
"postcss-html": "^1.6.0",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"prettier": "^3.0.0", "prettier": "^3.2.5",
"rimraf": "^5.0.1", "rimraf": "^5.0.5",
"rollup": "^3.26.3", "rollup": "^4.9.6",
"stylelint": "^15.10.1", "rollup-plugin-visualizer": "^5.12.0",
"stylelint": "^16.2.1",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.5", "stylelint-config-recommended": "^14.0.0",
"stylelint-config-recommended": "^13.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-config-standard": "^34.0.0", "stylelint-order": "^6.0.4",
"stylelint-order": "^6.0.3", "terser": "^5.27.0",
"terser": "^5.19.1", "typescript": "5.3.3",
"typescript": "5.1.6", "unocss": "^0.58.5",
"unocss": "^0.53.5", "vite": "5.0.12",
"unplugin-vue-define-options": "^1.3.11", "vite-plugin-ejs": "^1.7.0",
"vite": "4.4.4",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "2.9.6", "vite-plugin-mock": "2.9.6",
"vite-plugin-progress": "^0.0.7", "vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2", "vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-style-import": "2.0.0", "vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.8.5" "vue-tsc": "^1.8.27"
}, },
"packageManager": "pnpm@8.1.0",
"engines": { "engines": {
"node": ">= 14.18.0" "node": ">=18.0.0",
"pnpm": ">=8.1.0"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {

View File

@ -2,9 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { ConfigGlobal } from '@/components/ConfigGlobal' import { ConfigGlobal } from '@/components/ConfigGlobal'
import { isDark } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { useStorage } from '@/hooks/web/useStorage'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -16,19 +14,7 @@ const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode) const greyMode = computed(() => appStore.getGreyMode)
const { getStorage } = useStorage() appStore.initTheme()
//
const setDefaultTheme = () => {
if (getStorage('isDark') !== null) {
appStore.setIsDark(getStorage('isDark'))
return
}
const isDarkTheme = isDark()
appStore.setIsDark(isDarkTheme)
}
setDefaultTheme()
</script> </script>
<template> <template>

View File

@ -1,11 +0,0 @@
import request from '@/config/axios'
// 获取所有字典
export const getDictApi = () => {
return request.get({ url: '/dict/list' })
}
// 模拟获取某个字典
export const getDictOneApi = async () => {
return request.get({ url: '/dict/one' })
}

View File

@ -1,23 +0,0 @@
import request from '@/config/axios'
import type {
AnalysisTotalTypes,
UserAccessSource,
WeeklyUserActivity,
MonthlySales
} from './types'
export const getCountApi = (): Promise<IResponse<AnalysisTotalTypes[]>> => {
return request.get({ url: '/analysis/total' })
}
export const getUserAccessSourceApi = (): Promise<IResponse<UserAccessSource[]>> => {
return request.get({ url: '/analysis/userAccessSource' })
}
export const getWeeklyUserActivityApi = (): Promise<IResponse<WeeklyUserActivity[]>> => {
return request.get({ url: '/analysis/weeklyUserActivity' })
}
export const getMonthlySalesApi = (): Promise<IResponse<MonthlySales[]>> => {
return request.get({ url: '/analysis/monthlySales' })
}

View File

@ -1,22 +0,0 @@
export type AnalysisTotalTypes = {
users: number
messages: number
moneys: number
shoppings: number
}
export type UserAccessSource = {
value: number
name: string
}
export type WeeklyUserActivity = {
value: number
name: string
}
export type MonthlySales = {
name: string
estimate: number
actual: number
}

View File

@ -1,22 +0,0 @@
import request from '@/config/axios'
import type { WorkplaceTotal, Project, Dynamic, Team, RadarData } from './types'
export const getCountApi = (): Promise<IResponse<WorkplaceTotal>> => {
return request.get({ url: '/workplace/total' })
}
export const getProjectApi = (): Promise<IResponse<Project>> => {
return request.get({ url: '/workplace/project' })
}
export const getDynamicApi = (): Promise<IResponse<Dynamic[]>> => {
return request.get({ url: '/workplace/dynamic' })
}
export const getTeamApi = (): Promise<IResponse<Team[]>> => {
return request.get({ url: '/workplace/team' })
}
export const getRadarApi = (): Promise<IResponse<RadarData[]>> => {
return request.get({ url: '/workplace/radar' })
}

View File

@ -1,30 +0,0 @@
export type WorkplaceTotal = {
project: number
access: number
todo: number
}
export type Project = {
name: string
icon: string
message: string
personal: string
time: Date | number | string
}
export type Dynamic = {
keys: string[]
time: Date | number | string
}
export type Team = {
name: string
icon: string
}
export type RadarData = {
personal: number
team: number
max: number
name: string
}

View File

@ -1,30 +0,0 @@
import request from '@/config/axios'
import { DepartmentListResponse, DepartmentUserParams, DepartmentUserResponse } from './types'
export const getDepartmentApi = () => {
return request.get<DepartmentListResponse>({ url: '/department/list' })
}
export const getUserByIdApi = (params: DepartmentUserParams) => {
return request.get<DepartmentUserResponse>({ url: '/department/users', params })
}
export const deleteUserByIdApi = (ids: string[] | number[]) => {
return request.post({ url: '/department/user/delete', data: { ids } })
}
export const saveUserApi = (data: any) => {
return request.post({ url: '/department/user/save', data })
}
export const saveDepartmentApi = (data: any) => {
return request.post({ url: '/department/save', data })
}
export const deleteDepartmentApi = (ids: string[] | number[]) => {
return request.post({ url: '/department/delete', data: { ids } })
}
export const getDepartmentTableApi = (params: any) => {
return request.get({ url: '/department/table/list', params })
}

View File

@ -1,32 +0,0 @@
export interface DepartmentItem {
id: string
departmentName: string
children?: DepartmentItem[]
}
export interface DepartmentListResponse {
list: DepartmentItem[]
}
export interface DepartmentUserParams {
pageSize: number
pageIndex: number
id: string
username?: string
account?: string
}
export interface DepartmentUserItem {
id: string
username: string
account: string
email: string
createTime: string
role: string
department: DepartmentItem
}
export interface DepartmentUserResponse {
list: DepartmentUserItem[]
total: number
}

View File

@ -1,4 +1,4 @@
import request from '@/config/axios' import request from '@/axios'
import type { UserType } from './types' import type { UserType } from './types'
interface RoleParams { interface RoleParams {
@ -6,29 +6,19 @@ interface RoleParams {
} }
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => { export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
return request.post({ url: '/user/login', data }) return request.post({ url: '/mock/user/login', data })
} }
export const loginOutApi = (): Promise<IResponse> => { export const loginOutApi = (): Promise<IResponse> => {
return request.get({ url: '/user/loginOut' }) return request.get({ url: '/mock/user/loginOut' })
}
export const getUserListApi = ({ params }: AxiosConfig) => {
return request.get<{
code: string
data: {
list: UserType[]
total: number
}
}>({ url: '/user/list', params })
} }
export const getAdminRoleApi = ( export const getAdminRoleApi = (
params: RoleParams params: RoleParams
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => { ): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
return request.get({ url: '/role/list', params }) return request.get({ url: '/mock/role/list', params })
} }
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => { export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/role/list', params }) return request.get({ url: '/mock/role/list2', params })
} }

View File

@ -1,5 +0,0 @@
import request from '@/config/axios'
export const getMenuListApi = () => {
return request.get({ url: '/menu/list' })
}

View File

@ -1,22 +0,0 @@
import request from '@/config/axios'
import type { TableData } from './types'
export const getTableListApi = (params: any) => {
return request.get({ url: '/example/list', params })
}
export const getTreeTableListApi = (params: any) => {
return request.get({ url: '/example/treeList', params })
}
export const saveTableApi = (data: Partial<TableData>): Promise<IResponse> => {
return request.post({ url: '/example/save', data })
}
export const getTableDetApi = (id: string): Promise<IResponse<TableData>> => {
return request.get({ url: '/example/detail', params: { id } })
}
export const delTableListApi = (ids: string[] | number[]): Promise<IResponse> => {
return request.post({ url: '/example/delete', data: { ids } })
}

View File

@ -1,9 +0,0 @@
export type TableData = {
id: string
author: string
title: string
content: string
importance: number
display_time: string
pageviews: number
}

52
src/axios/config.ts Normal file
View File

@ -0,0 +1,52 @@
import { AxiosResponse, InternalAxiosRequestConfig } from './types'
import { ElMessage } from 'element-plus'
import qs from 'qs'
import { SUCCESS_CODE, TRANSFORM_REQUEST_DATA } from '@/constants'
import { useUserStoreWithOut } from '@/store/modules/user'
import { objToFormData } from '@/utils'
const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
if (
config.method === 'post' &&
config.headers['Content-Type'] === 'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(config.data)
} else if (
TRANSFORM_REQUEST_DATA &&
config.method === 'post' &&
config.headers['Content-Type'] === 'multipart/form-data'
) {
config.data = objToFormData(config.data)
}
if (config.method === 'get' && config.params) {
let url = config.url as string
url += '?'
const keys = Object.keys(config.params)
for (const key of keys) {
if (config.params[key] !== void 0 && config.params[key] !== null) {
url += `${key}=${encodeURIComponent(config.params[key])}&`
}
}
url = url.substring(0, url.length - 1)
config.params = {}
config.url = url
}
return config
}
const defaultResponseInterceptors = (response: AxiosResponse) => {
if (response?.config?.responseType === 'blob') {
// 如果是文件流,直接过
return response
} else if (response.data.code === SUCCESS_CODE) {
return response.data
} else {
ElMessage.error(response?.data?.message)
if (response?.data?.code === 401) {
const userStore = useUserStoreWithOut()
userStore.logout()
}
}
}
export { defaultResponseInterceptors, defaultRequestInterceptors }

View File

@ -1,11 +1,10 @@
import service from './service' import service from './service'
import { CONTENT_TYPE } from '@/constants'
import config from './config' import { useUserStoreWithOut } from '@/store/modules/user'
const { defaultHeaders } = config
const request = (option: AxiosConfig) => { const request = (option: AxiosConfig) => {
const { url, method, params, data, headersType, responseType } = option const { url, method, params, data, headers, responseType } = option
const userStore = useUserStoreWithOut()
return service.request({ return service.request({
url: url, url: url,
method, method,
@ -13,7 +12,9 @@ const request = (option: AxiosConfig) => {
data, data,
responseType: responseType, responseType: responseType,
headers: { headers: {
'Content-Type': headersType || defaultHeaders 'Content-Type': CONTENT_TYPE,
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '',
...headers
} }
}) })
} }

View File

@ -1,17 +1,16 @@
import axios from 'axios' import axios, { AxiosError } from 'axios'
import config, { defaultRequestInterceptors, defaultResponseInterceptors } from './config' import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types' import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types'
import { ElMessage } from 'element-plus'
import { REQUEST_TIMEOUT } from '@/constants'
const { interceptors, baseUrl } = config export const PATH_URL = import.meta.env.VITE_API_BASE_PATH
export const PATH_URL = baseUrl[import.meta.env.VITE_API_BASE_PATH]
const { requestInterceptors, responseInterceptors } = interceptors
const abortControllerMap: Map<string, AbortController> = new Map() const abortControllerMap: Map<string, AbortController> = new Map()
const axiosInstance: AxiosInstance = axios.create({ const axiosInstance: AxiosInstance = axios.create({
...config, timeout: REQUEST_TIMEOUT,
baseURL: PATH_URL baseURL: PATH_URL
}) })
@ -27,13 +26,18 @@ axiosInstance.interceptors.response.use(
(res: AxiosResponse) => { (res: AxiosResponse) => {
const url = res.config.url || '' const url = res.config.url || ''
abortControllerMap.delete(url) abortControllerMap.delete(url)
return res.data // 这里不能做任何处理,否则后面的 interceptors 拿不到完整的上下文了
return res
}, },
(err: any) => err (error: AxiosError) => {
console.log('err ' + error) // for debug
ElMessage.error(error.message)
return Promise.reject(error)
}
) )
axiosInstance.interceptors.request.use(requestInterceptors || defaultRequestInterceptors) axiosInstance.interceptors.request.use(defaultRequestInterceptors)
axiosInstance.interceptors.response.use(responseInterceptors || defaultResponseInterceptors) axiosInstance.interceptors.response.use(defaultResponseInterceptors)
const service = { const service = {
request: (config: RequestConfig) => { request: (config: RequestConfig) => {

View File

@ -15,18 +15,6 @@ interface RequestInterceptors<T> {
responseInterceptors?: (config: T) => T responseInterceptors?: (config: T) => T
responseInterceptorsCatch?: (err: any) => any responseInterceptorsCatch?: (err: any) => any
} }
interface AxiosConfig<T = AxiosResponse> {
baseUrl: {
base: string
dev: string
pro: string
test: string
}
code: number
defaultHeaders: AxiosHeaders
timeout: number
interceptors: RequestInterceptors<T>
}
interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig { interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: RequestInterceptors<T> interceptors?: RequestInterceptors<T>
@ -36,7 +24,6 @@ export {
AxiosResponse, AxiosResponse,
RequestInterceptors, RequestInterceptors,
RequestConfig, RequestConfig,
AxiosConfig,
AxiosInstance, AxiosInstance,
InternalAxiosRequestConfig, InternalAxiosRequestConfig,
AxiosRequestHeaders, AxiosRequestHeaders,

View File

@ -0,0 +1,3 @@
import BaseButton from './src/Button.vue'
export { BaseButton }

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import { useDesign } from '@/hooks/web/useDesign'
import { ElButton, ComponentSize, ButtonType } from 'element-plus'
import { PropType, Component, computed, unref } from 'vue'
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
const getTheme = computed(() => appStore.getTheme)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('button')
const props = defineProps({
size: {
type: String as PropType<ComponentSize>,
default: undefined
},
type: {
type: String as PropType<ButtonType>,
default: 'default'
},
disabled: {
type: Boolean,
default: false
},
plain: {
type: Boolean,
default: false
},
text: {
type: Boolean,
default: false
},
bg: {
type: Boolean,
default: false
},
link: {
type: Boolean,
default: false
},
round: {
type: Boolean,
default: false
},
circle: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
loadingIcon: {
type: [String, Object] as PropType<String | Component>,
default: undefined
},
icon: {
type: [String, Object] as PropType<String | Component>,
default: undefined
},
autofocus: {
type: Boolean,
default: false
},
nativeType: {
type: String as PropType<'button' | 'submit' | 'reset'>,
default: 'button'
},
autoInsertSpace: {
type: Boolean,
default: false
},
color: {
type: String,
default: ''
},
darker: {
type: Boolean,
default: false
},
tag: {
type: [String, Object] as PropType<String | Component>,
default: 'button'
}
})
const emits = defineEmits(['click'])
const color = computed(() => {
const { type } = props
if (type === 'primary') {
return unref(getTheme).elColorPrimary
}
return ''
})
const style = computed(() => {
const { type } = props
if (type === 'primary') {
return '--el-button-text-color: #fff; --el-button-hover-text-color: #fff'
}
return ''
})
</script>
<template>
<ElButton
:class="`${prefixCls} color-#fff`"
v-bind="{ ...props }"
:color="color"
:style="style"
@click="() => emits('click')"
>
<slot></slot>
<slot name="icon"></slot>
<slot name="loading"></slot>
</ElButton>
</template>

View File

@ -23,13 +23,12 @@ const toggleCollapse = () => {
</script> </script>
<template> <template>
<div :class="prefixCls"> <div :class="prefixCls" @click="toggleCollapse">
<Icon <Icon
:size="18" :size="18"
:icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'" :icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'"
:color="color" :color="color"
class="cursor-pointer" class="cursor-pointer"
@click="toggleCollapse"
/> />
</div> </div>
</template> </template>

View File

@ -1,3 +0,0 @@
import CountTo from './src/CountTo.vue'
export { CountTo }

View File

@ -1,180 +0,0 @@
<script setup lang="ts">
import { reactive, computed, watch, onMounted, unref, toRef, PropType } from 'vue'
import { isNumber } from '@/utils/is'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('count-to')
const props = defineProps({
startVal: propTypes.number.def(0),
endVal: propTypes.number.def(2021),
duration: propTypes.number.def(3000),
autoplay: propTypes.bool.def(true),
decimals: propTypes.number.validate((value: number) => value >= 0).def(0),
decimal: propTypes.string.def('.'),
separator: propTypes.string.def(','),
prefix: propTypes.string.def(''),
suffix: propTypes.string.def(''),
useEasing: propTypes.bool.def(true),
easingFn: {
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
default(t: number, b: number, c: number, d: number) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
}
}
})
const emit = defineEmits(['mounted', 'callback'])
const formatNumber = (num: number | string) => {
const { decimals, decimal, separator, suffix, prefix } = props
num = Number(num).toFixed(decimals)
num += ''
const x = num.split('.')
let x1 = x[0]
const x2 = x.length > 1 ? decimal + x[1] : ''
const rgx = /(\d+)(\d{3})/
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2')
}
}
return prefix + x1 + x2 + suffix
}
const state = reactive<{
localStartVal: number
printVal: number | null
displayValue: string
paused: boolean
localDuration: number | null
startTime: number | null
timestamp: number | null
rAF: any
remaining: number | null
}>({
localStartVal: props.startVal,
displayValue: formatNumber(props.startVal),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null
})
const displayValue = toRef(state, 'displayValue')
onMounted(() => {
if (props.autoplay) {
start()
}
emit('mounted')
})
const getCountDown = computed(() => {
return props.startVal > props.endVal
})
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start()
}
})
const start = () => {
const { startVal, duration } = props
state.localStartVal = startVal
state.startTime = null
state.localDuration = duration
state.paused = false
state.rAF = requestAnimationFrame(count)
}
const pauseResume = () => {
if (state.paused) {
resume()
state.paused = false
} else {
pause()
state.paused = true
}
}
const pause = () => {
cancelAnimationFrame(state.rAF)
}
const resume = () => {
state.startTime = null
state.localDuration = +(state.remaining as number)
state.localStartVal = +(state.printVal as number)
requestAnimationFrame(count)
}
const reset = () => {
state.startTime = null
cancelAnimationFrame(state.rAF)
state.displayValue = formatNumber(props.startVal)
}
const count = (timestamp: number) => {
const { useEasing, easingFn, endVal } = props
if (!state.startTime) state.startTime = timestamp
state.timestamp = timestamp
const progress = timestamp - state.startTime
state.remaining = (state.localDuration as number) - progress
if (useEasing) {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
} else {
state.printVal = easingFn(
progress,
state.localStartVal,
endVal - state.localStartVal,
state.localDuration as number
)
}
} else {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
(state.localStartVal - endVal) * (progress / (state.localDuration as number))
} else {
state.printVal =
state.localStartVal +
(endVal - state.localStartVal) * (progress / (state.localDuration as number))
}
}
if (unref(getCountDown)) {
state.printVal = state.printVal < endVal ? endVal : state.printVal
} else {
state.printVal = state.printVal > endVal ? endVal : state.printVal
}
state.displayValue = formatNumber(state.printVal!)
if (progress < (state.localDuration as number)) {
state.rAF = requestAnimationFrame(count)
} else {
emit('callback')
}
}
defineExpose({
pauseResume,
reset,
start,
pause
})
</script>
<template>
<span :class="prefixCls">
{{ displayValue }}
</span>
</template>

View File

@ -1,3 +0,0 @@
import Echart from './src/Echart.vue'
export { Echart }

View File

@ -1,113 +0,0 @@
<script setup lang="ts">
import type { EChartsOption } from 'echarts'
import echarts from '@/plugins/echarts'
import { debounce } from 'lodash-es'
import 'echarts-wordcloud'
import { propTypes } from '@/utils/propTypes'
import { computed, PropType, ref, unref, watch, onMounted, onBeforeUnmount, onActivated } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { isString } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('echart')
const appStore = useAppStore()
const props = defineProps({
options: {
type: Object as PropType<EChartsOption>,
required: true
},
width: propTypes.oneOfType([Number, String]).def(''),
height: propTypes.oneOfType([Number, String]).def('500px')
})
const isDark = computed(() => appStore.getIsDark)
const theme = computed(() => {
const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
return echartTheme
})
const options = computed(() => {
return Object.assign(props.options, {
darkMode: unref(theme)
})
})
const elRef = ref<ElRef>()
let echartRef: Nullable<echarts.ECharts> = null
const contentEl = ref<Element>()
const styles = computed(() => {
const width = isString(props.width) ? props.width : `${props.width}px`
const height = isString(props.height) ? props.height : `${props.height}px`
return {
width,
height
}
})
const initChart = () => {
if (unref(elRef) && props.options) {
echartRef = echarts.init(unref(elRef) as HTMLElement)
echartRef?.setOption(unref(options))
}
}
watch(
() => options.value,
(options) => {
if (echartRef) {
echartRef?.setOption(options)
}
},
{
deep: true
}
)
const resizeHandler = debounce(() => {
if (echartRef) {
echartRef.resize()
}
}, 100)
const contentResizeHandler = async (e: TransitionEvent) => {
if (e.propertyName === 'width') {
resizeHandler()
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', resizeHandler)
contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0]
unref(contentEl) &&
(unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeHandler)
unref(contentEl) &&
(unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
})
onActivated(() => {
if (echartRef) {
echartRef.resize()
}
})
</script>
<template>
<div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div>
</template>

View File

@ -15,7 +15,7 @@ const title = computed(() => appStore.getTitle)
<template> <template>
<div <div
:class="prefixCls" :class="prefixCls"
class="text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-content-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]" class="shrink-0 text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-content-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]"
> >
Copyright ©2021-present {{ title }} Copyright ©2021-present {{ title }}
</div> </div>

View File

@ -93,9 +93,6 @@ export default defineComponent({
// element form // element form
const elFormRef = ref<ComponentRef<typeof ElForm>>() const elFormRef = ref<ComponentRef<typeof ElForm>>()
// useFormprops
const outsideProps = ref<FormProps>({})
const mergeProps = ref<FormProps>({}) const mergeProps = ref<FormProps>({})
const getProps = computed(() => { const getProps = computed(() => {
@ -124,8 +121,6 @@ export default defineComponent({
const setProps = (props: FormProps = {}) => { const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props) mergeProps.value = Object.assign(unref(mergeProps), props)
// @ts-ignore
outsideProps.value = props
} }
const delSchema = (field: string) => { const delSchema = (field: string) => {
@ -235,7 +230,7 @@ export default defineComponent({
const { schema = [], isCol } = unref(getProps) const { schema = [], isCol } = unref(getProps)
return schema return schema
.filter((v) => !v.remove) .filter((v) => !v.remove && !v.hidden)
.map((item) => { .map((item) => {
// Divider // Divider
const isDivider = item.component === 'Divider' const isDivider = item.component === 'Divider'
@ -334,7 +329,11 @@ export default defineComponent({
ref={(el: any) => setComponentRefMap(el, item.field)} ref={(el: any) => setComponentRefMap(el, item.field)}
{...(autoSetPlaceholder && setTextPlaceholder(item))} {...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)} {...setComponentProps(item)}
style={item.componentProps?.style || {}} style={
item.componentProps?.style || {
width: '100%'
}
}
> >
{{ ...slotsMap }} {{ ...slotsMap }}
</Com> </Com>
@ -406,4 +405,16 @@ export default defineComponent({
margin-right: 0 !important; margin-right: 0 !important;
margin-left: 0 !important; margin-left: 0 !important;
} }
.@{elNamespace}-form--inline {
:deep(.el-form-item__content) {
& > :first-child {
min-width: 229.5px;
}
}
.@{elNamespace}-input-number {
// 229.5pxel-input-number,
min-width: 229.5px;
}
}
</style> </style>

View File

@ -146,7 +146,7 @@ export const initModel = (schema: FormSchema[], formModel: Recordable) => {
schema.map((v) => { schema.map((v) => {
if (v.remove) { if (v.remove) {
delete model[v.field] delete model[v.field]
} else if (v.component && v.component !== 'Divider') { } else if (v.component !== 'Divider') {
// const hasField = Reflect.has(model, v.field) // const hasField = Reflect.has(model, v.field)
const hasField = get(model, v.field) const hasField = get(model, v.field)
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值 // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值

View File

@ -1,3 +0,0 @@
import Highlight from './src/Highlight.vue'
export { Highlight }

View File

@ -1,65 +0,0 @@
<script lang="tsx">
import { defineComponent, PropType, computed, h, unref } from 'vue'
import { propTypes } from '@/utils/propTypes'
export default defineComponent({
name: 'Highlight',
props: {
tag: propTypes.string.def('span'),
keys: {
type: Array as PropType<string[]>,
default: () => []
},
color: propTypes.string.def('var(--el-color-primary)')
},
emits: ['click'],
setup(props, { emit, slots }) {
const keyNodes = computed(() => {
return props.keys.map((key) => {
return h(
'span',
{
onClick: () => {
emit('click', key)
},
style: {
color: props.color,
cursor: 'pointer'
}
},
key
)
})
})
const parseText = (text: string) => {
props.keys.forEach((key, index) => {
const regexp = new RegExp(key, 'g')
text = text.replace(regexp, `{{${index}}}`)
})
return text.split(/{{|}}/)
}
const renderText = () => {
if (!slots?.default) return null
const node = slots?.default()[0].children
if (!node) {
return slots?.default()[0]
}
const textArray = parseText(node as string)
const regexp = /^[0-9]*$/
const nodes = textArray.map((t) => {
if (regexp.test(t)) {
return unref(keyNodes)[t] || t
}
return t
})
return h(props.tag, nodes)
}
return () => renderText()
}
})
</script>

View File

@ -25,6 +25,11 @@ const symbolId = computed(() => {
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
}) })
// 使线
const isUseOnline = computed(() => {
return import.meta.env.VITE_USE_ONLINE_ICON === 'true'
})
const getIconifyStyle = computed(() => { const getIconifyStyle = computed(() => {
const { color, size } = props const { color, size } = props
return { return {
@ -40,7 +45,10 @@ const getIconifyStyle = computed(() => {
<use :xlink:href="symbolId" /> <use :xlink:href="symbolId" />
</svg> </svg>
<Icon v-else :icon="icon" :style="getIconifyStyle" /> <template v-else>
<Icon v-if="isUseOnline" :icon="icon" :style="getIconifyStyle" />
<div v-else :class="`${icon} iconify`" :style="getIconifyStyle"></div>
</template>
</ElIcon> </ElIcon>
</template> </template>
@ -49,11 +57,18 @@ const getIconifyStyle = computed(() => {
.@{prefix-cls}, .@{prefix-cls},
.iconify { .iconify {
&:hover { :deep(svg) {
:deep(svg) { &:hover {
// stylelint-disable-next-line // stylelint-disable-next-line
color: v-bind(hoverColor) !important; color: v-bind(hoverColor) !important;
} }
} }
} }
.iconify {
&:hover {
// stylelint-disable-next-line
color: v-bind(hoverColor) !important;
}
}
</style> </style>

View File

@ -1,33 +0,0 @@
import ImageViewer from './src/ImageViewer.vue'
import { isClient } from '@/utils/is'
import { createVNode, render, VNode } from 'vue'
import { ImageViewerProps } from './src/types'
let instance: Nullable<VNode> = null
export function createImageViewer(options: ImageViewerProps) {
if (!isClient) return
const {
urlList,
initialIndex = 0,
infinite = true,
hideOnClickModal = false,
teleported = false,
zIndex = 2000,
show = true
} = options
const propsData: Partial<ImageViewerProps> = {}
const container = document.createElement('div')
propsData.urlList = urlList
propsData.initialIndex = initialIndex
propsData.infinite = infinite
propsData.hideOnClickModal = hideOnClickModal
propsData.teleported = teleported
propsData.zIndex = zIndex
propsData.show = show
document.body.appendChild(container)
instance = createVNode(ImageViewer, propsData)
render(instance, container)
}

View File

@ -1,34 +0,0 @@
<script setup lang="ts">
import { ElImageViewer } from 'element-plus'
import { computed, ref, PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
urlList: {
type: Array as PropType<string[]>,
default: (): string[] => []
},
zIndex: propTypes.number.def(200),
initialIndex: propTypes.number.def(0),
infinite: propTypes.bool.def(true),
hideOnClickModal: propTypes.bool.def(false),
teleported: propTypes.bool.def(false),
show: propTypes.bool.def(false)
})
const getBindValue = computed(() => {
const propsData: Recordable = { ...props }
delete propsData.show
return propsData
})
const show = ref(props.show)
const close = () => {
show.value = false
}
</script>
<template>
<ElImageViewer v-if="show" v-bind="getBindValue" @close="close" />
</template>

View File

@ -1,9 +0,0 @@
export interface ImageViewerProps {
urlList?: string[]
zIndex?: number
initialIndex?: number
infinite?: boolean
hideOnClickModal?: boolean
teleported?: boolean
show?: boolean
}

View File

@ -1,5 +0,0 @@
import Infotip from './src/Infotip.vue'
export type { InfoTipSchema } from './src/types'
export { Infotip }

View File

@ -1,53 +0,0 @@
<script setup lang="ts">
import { PropType } from 'vue'
import { Highlight } from '@/components/Highlight'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { InfoTipSchema } from './types'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('infotip')
defineProps({
title: propTypes.string.def(''),
schema: {
type: Array as PropType<Array<string | InfoTipSchema>>,
required: true,
default: () => []
},
showIndex: propTypes.bool.def(true),
highlightColor: propTypes.string.def('var(--el-color-primary)')
})
const emit = defineEmits(['click'])
const keyClick = (key: string) => {
emit('click', key)
}
</script>
<template>
<div
:class="[
prefixCls,
'p-20px mb-20px border-1px border-solid border-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)]'
]"
>
<div v-if="title" :class="[`${prefixCls}__header`, 'flex items-center']">
<Icon icon="bi:exclamation-circle-fill" :size="22" color="var(--el-color-primary)" />
<span :class="[`${prefixCls}__title`, 'pl-5px text-16px font-bold']">{{ title }}</span>
</div>
<div :class="`${prefixCls}__content`">
<p v-for="(item, $index) in schema" :key="$index" class="text-14px mt-15px">
<Highlight
:keys="typeof item === 'string' ? [] : item.keys"
:color="highlightColor"
@click="keyClick"
>
{{ showIndex ? `${$index + 1}` : '' }}{{ typeof item === 'string' ? item : item.label }}
</Highlight>
</p>
</div>
</div>
</template>

View File

@ -1,4 +0,0 @@
export interface InfoTipSchema {
label: string
keys?: string[]
}

View File

@ -89,11 +89,16 @@ export default defineComponent({
backgroundColor="var(--left-menu-bg-color)" backgroundColor="var(--left-menu-bg-color)"
textColor="var(--left-menu-text-color)" textColor="var(--left-menu-text-color)"
activeTextColor="var(--left-menu-text-active-color)" activeTextColor="var(--left-menu-text-active-color)"
popperClass={
unref(menuMode) === 'vertical'
? `${prefixCls}-popper--vertical`
: `${prefixCls}-popper--horizontal`
}
onSelect={menuSelect} onSelect={menuSelect}
> >
{{ {{
default: () => { default: () => {
const { renderMenuItem } = useRenderMenuItem(unref(menuMode)) const { renderMenuItem } = useRenderMenuItem()
return renderMenuItem(unref(routers)) return renderMenuItem(unref(routers))
} }
}} }}
@ -123,30 +128,10 @@ export default defineComponent({
<style lang="less" scoped> <style lang="less" scoped>
@prefix-cls: ~'@{namespace}-menu'; @prefix-cls: ~'@{namespace}-menu';
// .is-active--after {
// position: absolute;
// top: 0;
// right: 0;
// width: 4px;
// height: 100%;
// background-color: var(--el-color-primary);
// content: '';
// }
.@{prefix-cls} { .@{prefix-cls} {
position: relative; position: relative;
transition: width var(--transition-time-02); transition: width var(--transition-time-02);
// &:after {
// position: absolute;
// top: 0;
// right: 0;
// height: 100%;
// width: 1px;
// background-color: var(--el-border-color);
// content: '';
// }
:deep(.@{elNamespace}-menu) { :deep(.@{elNamespace}-menu) {
width: 100% !important; width: 100% !important;
border-right: none; border-right: none;
@ -168,7 +153,6 @@ export default defineComponent({
} }
// //
.@{elNamespace}-sub-menu.is-active,
.@{elNamespace}-menu-item.is-active { .@{elNamespace}-menu-item.is-active {
color: var(--left-menu-text-active-color) !important; color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-active-color) !important; background-color: var(--left-menu-bg-active-color) !important;
@ -180,10 +164,6 @@ export default defineComponent({
.@{elNamespace}-menu-item.is-active { .@{elNamespace}-menu-item.is-active {
position: relative; position: relative;
// &:after {
// .is-active--after;
// }
} }
// //
@ -203,16 +183,11 @@ export default defineComponent({
& > .is-active > .@{elNamespace}-sub-menu__title { & > .is-active > .@{elNamespace}-sub-menu__title {
position: relative; position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important; background-color: var(--left-menu-collapse-bg-active-color) !important;
// &:after {
// .is-active--after;
// }
} }
} }
// //
:deep(.horizontal-collapse-transition) { :deep(.horizontal-collapse-transition) {
// transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
.@{prefix-cls}__title { .@{prefix-cls}__title {
display: none; display: none;
} }
@ -235,7 +210,7 @@ export default defineComponent({
.@{elNamespace}-menu-item.is-active { .@{elNamespace}-menu-item.is-active {
position: relative; position: relative;
&:after { &::after {
display: none !important; display: none !important;
} }
} }
@ -254,16 +229,6 @@ export default defineComponent({
<style lang="less"> <style lang="less">
@prefix-cls: ~'@{namespace}-menu-popper'; @prefix-cls: ~'@{namespace}-menu-popper';
// .is-active--after {
// position: absolute;
// top: 0;
// right: 0;
// width: 4px;
// height: 100%;
// background-color: var(--el-color-primary);
// content: '';
// }
.@{prefix-cls}--vertical, .@{prefix-cls}--vertical,
.@{prefix-cls}--horizontal { .@{prefix-cls}--horizontal {
// //
@ -290,10 +255,6 @@ export default defineComponent({
&:hover { &:hover {
background-color: var(--left-menu-bg-active-color) !important; background-color: var(--left-menu-bg-active-color) !important;
} }
// &:after {
// .is-active--after;
// }
} }
} }
</style> </style>

View File

@ -1,59 +1,50 @@
import { ElSubMenu, ElMenuItem } from 'element-plus' import { ElSubMenu, ElMenuItem } from 'element-plus'
import type { RouteMeta } from 'vue-router'
import { hasOneShowingChild } from '../helper' import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is' import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle' import { useRenderMenuTitle } from './useRenderMenuTitle'
import { useDesign } from '@/hooks/web/useDesign'
import { pathResolve } from '@/utils/routerHelper' import { pathResolve } from '@/utils/routerHelper'
export const useRenderMenuItem = ( const { renderMenuTitle } = useRenderMenuTitle()
export const useRenderMenuItem = () =>
// allRouters: AppRouteRecordRaw[] = [], // allRouters: AppRouteRecordRaw[] = [],
menuMode: 'vertical' | 'horizontal' {
) => { const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { return routers
return routers.map((v) => { .filter((v) => !v.meta?.hidden)
const meta = (v.meta ?? {}) as RouteMeta .map((v) => {
if (!meta.hidden) { const meta = v.meta ?? {}
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/') const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
const { renderMenuTitle } = useRenderMenuTitle() if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
return (
<ElSubMenu index={fullPath}>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
})
}
if ( return {
oneShowingChild && renderMenuItem
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && }
!meta?.alwaysShow
) {
return (
<ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
const { getPrefixCls } = useDesign()
const preFixCls = getPrefixCls('menu-popper')
return (
<ElSubMenu
index={fullPath}
popperClass={
menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
}
>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
}
})
} }
return {
renderMenuItem
}
}

View File

@ -10,10 +10,14 @@ export const useRenderMenuTitle = () => {
return icon ? ( return icon ? (
<> <>
<Icon icon={meta.icon}></Icon> <Icon icon={meta.icon}></Icon>
<span class="v-menu__title">{t(title as string)}</span> <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
</> </>
) : ( ) : (
<span class="v-menu__title">{t(title as string)}</span> <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
) )
} }

View File

@ -1,4 +1,3 @@
import type { RouteMeta } from 'vue-router'
import { ref, unref } from 'vue' import { ref, unref } from 'vue'
import { findPath } from '@/utils/tree' import { findPath } from '@/utils/tree'
@ -21,7 +20,7 @@ export const hasOneShowingChild = (
const onlyOneChild = ref<OnlyOneChildType>() const onlyOneChild = ref<OnlyOneChildType>()
const showingChildren = children.filter((v) => { const showingChildren = children.filter((v) => {
const meta = (v.meta ?? {}) as RouteMeta const meta = v.meta ?? {}
if (meta.hidden) { if (meta.hidden) {
return false return false
} else { } else {

View File

@ -1,5 +0,0 @@
import Qrcode from './src/Qrcode.vue'
export type { QrcodeLogo } from './src/types'
export { Qrcode }

View File

@ -1,252 +0,0 @@
<script setup lang="ts">
import { PropType, nextTick, ref, watch, computed, unref } from 'vue'
import QRCode from 'qrcode'
import { QRCodeRenderersOptions } from 'qrcode'
import { cloneDeep } from 'lodash-es'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { isString } from '@/utils/is'
import { QrcodeLogo } from '@/components/Qrcode'
const props = defineProps({
// img canvas,imglogo
tag: propTypes.string.validate((v: string) => ['canvas', 'img'].includes(v)).def('canvas'),
//
text: {
type: [String, Array] as PropType<string | Recordable[]>,
default: null
},
// qrcode.js
options: {
type: Object as PropType<QRCodeRenderersOptions>,
default: () => ({})
},
//
width: propTypes.number.def(200),
// logo
logo: {
type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
default: ''
},
//
disabled: propTypes.bool.def(false),
//
disabledText: propTypes.string.def('')
})
const emit = defineEmits(['done', 'click', 'disabled-click'])
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('qrcode')
const { toCanvas, toDataURL } = QRCode
const loading = ref(true)
const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null)
const renderText = computed(() => String(props.text))
const wrapStyle = computed(() => {
return {
width: props.width + 'px',
height: props.width + 'px'
}
})
const initQrcode = async () => {
await nextTick()
const options = cloneDeep(props.options || {})
if (props.tag === 'canvas') {
//
options.errorCorrectionLevel =
options.errorCorrectionLevel || getErrorCorrectionLevel(unref(renderText))
const _width: number = await getOriginWidth(unref(renderText), options)
options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
const canvasRef = (await toCanvas(
unref(wrapRef) as HTMLCanvasElement,
unref(renderText),
options
)) as unknown as HTMLCanvasElement
if (props.logo) {
const url = await createLogoCode(canvasRef)
emit('done', url)
loading.value = false
} else {
emit('done', canvasRef.toDataURL())
loading.value = false
}
} else {
const url = await toDataURL(renderText.value, {
errorCorrectionLevel: 'H',
width: props.width,
...options
})
;(unref(wrapRef) as HTMLImageElement).src = url
emit('done', url)
loading.value = false
}
}
watch(
() => renderText.value,
(val) => {
if (!val) return
initQrcode()
},
{
deep: true,
immediate: true
}
)
const createLogoCode = (canvasRef: HTMLCanvasElement) => {
const canvasWidth = canvasRef.width
const logoOptions: QrcodeLogo = Object.assign(
{
logoSize: 0.15,
bgColor: '#ffffff',
borderSize: 0.05,
crossOrigin: 'anonymous',
borderRadius: 8,
logoRadius: 0
},
isString(props.logo) ? {} : props.logo
)
const {
logoSize = 0.15,
bgColor = '#ffffff',
borderSize = 0.05,
crossOrigin = 'anonymous',
borderRadius = 8,
logoRadius = 0
} = logoOptions
const logoSrc = isString(props.logo) ? props.logo : props.logo.src
const logoWidth = canvasWidth * logoSize
const logoXY = (canvasWidth * (1 - logoSize)) / 2
const logoBgWidth = canvasWidth * (logoSize + borderSize)
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
const ctx = canvasRef.getContext('2d')
if (!ctx) return
// logo
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
ctx.fillStyle = bgColor
ctx.fill()
// logo
const image = new Image()
if (crossOrigin || logoRadius) {
image.setAttribute('crossOrigin', crossOrigin)
}
;(image as any).src = logoSrc
// 使image
const drawLogoWithImage = (image: HTMLImageElement) => {
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
}
// 使canvas
const drawLogoWithCanvas = (image: HTMLImageElement) => {
const canvasImage = document.createElement('canvas')
canvasImage.width = logoXY + logoWidth
canvasImage.height = logoXY + logoWidth
const imageCanvas = canvasImage.getContext('2d')
if (!imageCanvas || !ctx) return
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
if (!ctx) return
const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
if (fillStyle) {
ctx.fillStyle = fillStyle
ctx.fill()
}
}
// logo canvas
return new Promise((resolve: any) => {
image.onload = () => {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
resolve(canvasRef.toDataURL())
}
})
}
// QrCode便QrCode
const getOriginWidth = async (content: string, options: QRCodeRenderersOptions) => {
const _canvas = document.createElement('canvas')
await toCanvas(_canvas, content, options)
return _canvas.width
}
// QrCode
const getErrorCorrectionLevel = (content: string) => {
if (content.length > 36) {
return 'M'
} else if (content.length > 16) {
return 'Q'
} else {
return 'H'
}
}
// copy
const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
return (x: number, y: number, w: number, h: number, r: number) => {
const minSize = Math.min(w, h)
if (r > minSize / 2) {
r = minSize / 2
}
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.arcTo(x + w, y, x + w, y + h, r)
ctx.arcTo(x + w, y + h, x, y + h, r)
ctx.arcTo(x, y + h, x, y, r)
ctx.arcTo(x, y, x + w, y, r)
ctx.closePath()
return ctx
}
}
const clickCode = () => {
emit('click')
}
const disabledClick = () => {
emit('disabled-click')
}
</script>
<template>
<div v-loading="loading" :class="[prefixCls, 'relative inline-block']" :style="wrapStyle">
<component :is="tag" ref="wrapRef" @click="clickCode" />
<div
v-if="disabled"
:class="`${prefixCls}--disabled`"
class="absolute top-0 left-0 flex w-full h-full items-center justify-center"
@click="disabledClick"
>
<div class="absolute top-[50%] left-[50%] font-bold">
<Icon icon="ep:refresh-right" :size="30" color="var(--el-color-primary)" />
<div>{{ disabledText }}</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-qrcode';
.@{prefix-cls} {
&--disabled {
background: rgba(255, 255, 255, 0.95);
& > div {
transform: translate(-50%, -50%);
}
}
}
</style>

View File

@ -1,9 +0,0 @@
export interface QrcodeLogo {
src?: string
logoSize?: number
bgColor?: string
borderSize?: number
crossOrigin?: string
borderRadius?: number
logoRadius?: number
}

View File

@ -88,6 +88,9 @@ const newSchema = computed(() => {
/> />
</div> </div>
) )
},
label: () => {
return <span>&nbsp;</span>
} }
} }
} }
@ -117,11 +120,14 @@ const setProps = (props: SearchProps = {}) => {
outsideProps.value = props outsideProps.value = props
} }
const schemaRef = ref<FormSchema[]>([])
// formModel // formModel
watch( watch(
() => unref(newSchema), () => unref(newSchema),
async (schema = []) => { async (schema = []) => {
formModel.value = initModel(schema, unref(formModel)) formModel.value = initModel(schema, unref(formModel))
schemaRef.value = schema
}, },
{ {
immediate: true, immediate: true,
@ -241,7 +247,7 @@ const onFormValidate = (prop: FormItemProp, isValid: boolean, message: string) =
hide-required-asterisk hide-required-asterisk
:inline="getProps.inline" :inline="getProps.inline"
:is-col="getProps.isCol" :is-col="getProps.isCol"
:schema="newSchema" :schema="schemaRef"
@register="formRegister" @register="formRegister"
@validate="onFormValidate" @validate="onFormValidate"
/> />

View File

@ -1,12 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElDrawer, ElDivider, ElButton, ElMessage } from 'element-plus' import { ElDrawer, ElDivider, ElMessage } from 'element-plus'
import { ref, unref, computed, watch } from 'vue' import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { ThemeSwitch } from '@/components/ThemeSwitch' import { ThemeSwitch } from '@/components/ThemeSwitch'
import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
import { useCssVar } from '@vueuse/core' import { useCssVar } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { trim, setCssVar } from '@/utils' import { trim, setCssVar, getCssVar } from '@/utils'
import ColorRadioPicker from './components/ColorRadioPicker.vue' import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue' import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue' import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
@ -14,7 +13,7 @@ import { useStorage } from '@/hooks/web/useStorage'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
const { removeStorage } = useStorage() const { clear: storageClear } = useStorage('localStorage')
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -24,8 +23,6 @@ const appStore = useAppStore()
const { t } = useI18n() const { t } = useI18n()
const layout = computed(() => appStore.getLayout)
const drawer = ref(false) const drawer = ref(false)
// //
@ -42,70 +39,28 @@ const setSystemTheme = (color: string) => {
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '') const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
const setHeaderTheme = (color: string) => { const setHeaderTheme = (color: string) => {
const isDarkColor = colorIsDark(color) appStore.setHeaderTheme(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
appStore.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
topHeaderHoverColor: textHoverColor,
topToolBorderColor
})
if (unref(layout) === 'top') {
setMenuTheme(color)
}
} }
// //
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '') const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
const setMenuTheme = (color: string) => { const setMenuTheme = (color: string) => {
const primaryColor = useCssVar('--el-color-primary', document.documentElement) appStore.setMenuTheme(color)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
//
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
//
leftMenuBgColor: color,
//
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
//
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
//
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
//
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
//
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo
logoBorderColor: isDarkColor ? color : '#eee'
}
appStore.setTheme(theme)
appStore.setCssVarTheme()
} }
// layout // layout
watch( // watch(
() => layout.value, // () => layout.value,
(n) => { // (n) => {
if (n === 'top' && !appStore.getIsDark) { // if (n === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff' // headerTheme.value = '#fff'
setHeaderTheme('#fff') // setHeaderTheme('#fff')
} else { // } else {
setMenuTheme(unref(menuTheme)) // setMenuTheme(unref(menuTheme))
} // }
} // }
) // )
// //
const copyConfig = async () => { const copyConfig = async () => {
@ -174,7 +129,8 @@ const copyConfig = async () => {
// //
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}' topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
} }
` `,
legacy: true
}) })
if (!isSupported) { if (!isSupported) {
ElMessage.error(t('setting.copyFailed')) ElMessage.error(t('setting.copyFailed'))
@ -188,11 +144,15 @@ const copyConfig = async () => {
// //
const clear = () => { const clear = () => {
removeStorage('layout') storageClear()
removeStorage('theme')
removeStorage('isDark')
window.location.reload() window.location.reload()
} }
const themeChange = () => {
const color = getCssVar('--el-bg-color')
setMenuTheme(color)
setHeaderTheme(color)
}
</script> </script>
<template> <template>
@ -212,7 +172,7 @@ const clear = () => {
<div class="text-center"> <div class="text-center">
<!-- 主题 --> <!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider> <ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch /> <ThemeSwitch @change="themeChange" />
<!-- 布局 --> <!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider> <ElDivider>{{ t('setting.layout') }}</ElDivider>
@ -253,23 +213,21 @@ const clear = () => {
/> />
<!-- 菜单主题 --> <!-- 菜单主题 -->
<template v-if="layout !== 'top'"> <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider> <ColorRadioPicker
<ColorRadioPicker v-model="menuTheme"
v-model="menuTheme" :schema="[
:schema="[ '#fff',
'#fff', '#001529',
'#001529', '#212121',
'#212121', '#273352',
'#273352', '#191b24',
'#191b24', '#383f45',
'#383f45', '#001628',
'#001628', '#344058'
'#344058' ]"
]" @change="setMenuTheme"
@change="setMenuTheme" />
/>
</template>
</div> </div>
<!-- 界面显示 --> <!-- 界面显示 -->
@ -278,12 +236,14 @@ const clear = () => {
<ElDivider /> <ElDivider />
<div> <div>
<ElButton type="primary" class="w-full" @click="copyConfig">{{ t('setting.copy') }}</ElButton> <BaseButton type="primary" class="w-full" @click="copyConfig">{{
t('setting.copy')
}}</BaseButton>
</div> </div>
<div class="mt-5px"> <div class="mt-5px">
<ElButton type="danger" class="w-full" @click="clear"> <BaseButton type="danger" class="w-full" @click="clear">
{{ t('setting.clearAndReset') }} {{ t('setting.clearAndReset') }}
</ElButton> </BaseButton>
</div> </div>
</ElDrawer> </ElDrawer>
</template> </template>

View File

@ -108,13 +108,21 @@ const greyModeChange = (show: boolean) => {
} }
// //
const dynamicRouter = ref(appStore.getDynamicRouter) const dynamicRouter = ref(!!appStore.getDynamicRouter)
const dynamicRouterChange = (show: boolean) => { const dynamicRouterChange = (show: boolean) => {
ElMessage.info(t('setting.reExperienced')) ElMessage.info(t('setting.reExperienced'))
appStore.setDynamicRouter(show) appStore.setDynamicRouter(show)
} }
//
const serverDynamicRouter = ref(appStore.getServerDynamicRouter)
const serverDynamicRouterChange = (show: boolean) => {
ElMessage.info(t('setting.reExperienced'))
appStore.setServerDynamicRouter(show)
}
// //
const fixedMenu = ref(appStore.getFixedMenu) const fixedMenu = ref(appStore.getFixedMenu)
@ -206,6 +214,11 @@ watch(
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" /> <ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
</div> </div>
<div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.serverDynamicRouter') }}</span>
<ElSwitch v-model="serverDynamicRouter" @change="serverDynamicRouterChange" />
</div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span> <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" /> <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />

View File

@ -67,7 +67,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb; border: 2px solid #e5e7eb;
border-radius: 4px; border-radius: 4px;
&:before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -79,14 +79,14 @@ const layout = computed(() => appStore.getLayout)
content: ''; content: '';
} }
&:after { &::after {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 25%; height: 25%;
background-color: #fff; background-color: #fff;
border-radius: 4px 4px 0 4px; border-radius: 4px 4px 0;
content: ''; content: '';
} }
} }
@ -95,7 +95,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb; border: 2px solid #e5e7eb;
border-radius: 4px; border-radius: 4px;
&:before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -107,7 +107,7 @@ const layout = computed(() => appStore.getLayout)
content: ''; content: '';
} }
&:after { &::after {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -123,7 +123,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb; border: 2px solid #e5e7eb;
border-radius: 4px; border-radius: 4px;
&:before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -140,7 +140,7 @@ const layout = computed(() => appStore.getLayout)
border: 2px solid #e5e7eb; border: 2px solid #e5e7eb;
border-radius: 4px; border-radius: 4px;
&:before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -152,7 +152,7 @@ const layout = computed(() => appStore.getLayout)
content: ''; content: '';
} }
&:after { &::after {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;

View File

@ -85,6 +85,9 @@ export default defineComponent({
} else { } else {
showTitle.value = !collapse showTitle.value = !collapse
} }
},
{
immediate: true
} }
) )
@ -197,11 +200,12 @@ export default defineComponent({
</div> </div>
<Menu <Menu
class={[ class={[
'!absolute top-0', '!absolute top-0 z-3000',
{ {
'!left-[var(--tab-menu-min-width)]': unref(collapse), '!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse), '!left-[var(--tab-menu-max-width)]': !unref(collapse),
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu), '!w-[var(--left-menu-max-width)] border-r-1 border-r-solid border-[var(--el-border-color)]':
unref(showMenu) || unref(fixedMenu),
'!w-0': !unref(showMenu) && !unref(fixedMenu) '!w-0': !unref(showMenu) && !unref(fixedMenu)
} }
]} ]}

View File

@ -5,7 +5,9 @@ import {
ElPagination, ElPagination,
ComponentSize, ComponentSize,
ElTooltipProps, ElTooltipProps,
ElImage ElImage,
ElEmpty,
ElCard
} from 'element-plus' } from 'element-plus'
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue' import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
@ -15,8 +17,9 @@ import { set, get } from 'lodash-es'
import { CSSProperties } from 'vue' import { CSSProperties } from 'vue'
import { getSlot } from '@/utils/tsxHelper' import { getSlot } from '@/utils/tsxHelper'
import TableActions from './components/TableActions.vue' import TableActions from './components/TableActions.vue'
// import Sortable from 'sortablejs' import { createVideoViewer } from '@/components/VideoPlayer'
// import { Icon } from '@/components/Icon' import { Icon } from '@/components/Icon'
import { BaseButton } from '@/components/Button'
export default defineComponent({ export default defineComponent({
name: 'Table', name: 'Table',
@ -32,8 +35,6 @@ export default defineComponent({
type: Array as PropType<TableColumn[]>, type: Array as PropType<TableColumn[]>,
default: () => [] default: () => []
}, },
//
// expand: propTypes.bool.def(false),
// //
pagination: { pagination: {
type: Object as PropType<Pagination>, type: Object as PropType<Pagination>,
@ -57,12 +58,16 @@ export default defineComponent({
type: Array as PropType<Recordable[]>, type: Array as PropType<Recordable[]>,
default: () => [] default: () => []
}, },
// //
preview: { imagePreview: {
type: Array as PropType<string[]>,
default: () => []
},
//
videoPreview: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
default: () => [] default: () => []
}, },
// sortable: propTypes.bool.def(false),
height: propTypes.oneOfType([Number, String]), height: propTypes.oneOfType([Number, String]),
maxHeight: propTypes.oneOfType([Number, String]), maxHeight: propTypes.oneOfType([Number, String]),
stripe: propTypes.bool.def(false), stripe: propTypes.bool.def(false),
@ -186,9 +191,27 @@ export default defineComponent({
default: 'fixed' default: 'fixed'
}, },
scrollbarAlwaysOn: propTypes.bool.def(false), scrollbarAlwaysOn: propTypes.bool.def(false),
flexible: propTypes.bool.def(false) flexible: propTypes.bool.def(false),
//
customContent: propTypes.bool.def(false),
cardBodyStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({})
},
cardBodyClass: {
type: String as PropType<string>,
default: ''
},
cardWrapStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({})
},
cardWrapClass: {
type: String as PropType<string>,
default: ''
}
}, },
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh', 'sortable-change'], emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh'],
setup(props, { attrs, emit, slots, expose }) { setup(props, { attrs, emit, slots, expose }) {
const elTableRef = ref<ComponentRef<typeof ElTable>>() const elTableRef = ref<ComponentRef<typeof ElTable>>()
@ -213,33 +236,6 @@ export default defineComponent({
return propsObj return propsObj
}) })
// const sortableEl = ref()
//
// const initDropTable = () => {
// const el = unref(elTableRef)?.$el.querySelector('.el-table__body tbody')
// if (!el) return
// if (unref(sortableEl)) unref(sortableEl).destroy()
// sortableEl.value = Sortable.create(el, {
// handle: '.table-move',
// animation: 180,
// onEnd(e: any) {
// emit('sortable-change', e)
// }
// })
// }
// watch(
// () => getProps.value.sortable,
// async (v) => {
// await nextTick()
// v && initDropTable()
// },
// {
// immediate: true
// }
// )
const setProps = (props: TableProps = {}) => { const setProps = (props: TableProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props) mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = { ...props } as any outsideProps.value = { ...props } as any
@ -260,7 +256,7 @@ export default defineComponent({
const addColumn = (column: TableColumn, index?: number) => { const addColumn = (column: TableColumn, index?: number) => {
const { columns } = unref(getProps) const { columns } = unref(getProps)
if (index) { if (index !== void 0) {
columns.splice(index, 0, column) columns.splice(index, 0, column)
} else { } else {
columns.push(column) columns.push(column)
@ -283,6 +279,10 @@ export default defineComponent({
setProps({ size }) setProps({ size })
} }
const confirmSetColumn = (columns: TableColumn[]) => {
setProps({ columns })
}
expose({ expose({
setProps, setProps,
setColumn, setColumn,
@ -339,11 +339,13 @@ export default defineComponent({
const bindValue: Recordable = { ...attrs, ...unref(getProps) } const bindValue: Recordable = { ...attrs, ...unref(getProps) }
delete bindValue.columns delete bindValue.columns
delete bindValue.data delete bindValue.data
delete bindValue.align
return bindValue return bindValue
}) })
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => { const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps) const { align, headerAlign, showOverflowTooltip, imagePreview, videoPreview } =
unref(getProps)
return columnsChildren.map((v) => { return columnsChildren.map((v) => {
if (v.hidden) return null if (v.hidden) return null
const props = { ...v } as any const props = { ...v } as any
@ -354,24 +356,24 @@ export default defineComponent({
const slots = { const slots = {
default: (...args: any[]) => { default: (...args: any[]) => {
const data = args[0] const data = args[0]
let isImageUrl = false let isPreview = false
if (preview.length) { isPreview =
isImageUrl = preview.some((item) => (item as string) === v.field) imagePreview.some((item) => (item as string) === v.field) ||
} videoPreview.some((item) => (item as string) === v.field)
return children && children.length return children && children.length
? renderTreeTableColumn(children) ? renderTreeTableColumn(children)
: props?.slots?.default : props?.slots?.default
? props.slots.default(args) ? props.slots.default(...args)
: v?.formatter : v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index) ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl : isPreview
? renderPreview(get(data.row, v.field)) ? renderPreview(get(data.row, v.field), v.field)
: get(data.row, v.field) : get(data.row, v.field)
} }
} }
if (props?.slots?.header) { if (props?.slots?.header) {
slots['header'] = (...args: any[]) => props.slots.header(args) slots['header'] = (...args: any[]) => props.slots.header(...args)
} }
return ( return (
@ -388,17 +390,32 @@ export default defineComponent({
}) })
} }
const renderPreview = (url: string) => { const renderPreview = (url: string, field: string) => {
const { imagePreview, videoPreview } = unref(getProps)
return ( return (
<div class="flex items-center"> <div class="flex items-center">
<ElImage {imagePreview.includes(field) ? (
src={url} <ElImage
fit="cover" src={url}
class="w-[100%] h-100px" fit="cover"
lazy class="w-[100%]"
preview-src-list={[url]} lazy
preview-teleported preview-src-list={[url]}
/> preview-teleported
/>
) : videoPreview.includes(field) ? (
<BaseButton
type="primary"
icon={<Icon icon="ep:video-play" />}
onClick={() => {
createVideoViewer({
url
})
}}
>
预览
</BaseButton>
) : null}
</div> </div>
) )
} }
@ -413,7 +430,8 @@ export default defineComponent({
headerAlign, headerAlign,
showOverflowTooltip, showOverflowTooltip,
reserveSelection, reserveSelection,
preview imagePreview,
videoPreview
} = unref(getProps) } = unref(getProps)
return (columnsChildren || columns).map((v) => { return (columnsChildren || columns).map((v) => {
@ -428,6 +446,7 @@ export default defineComponent({
align={v.align || align} align={v.align || align}
headerAlign={v.headerAlign || headerAlign} headerAlign={v.headerAlign || headerAlign}
label={v.label} label={v.label}
fixed={v.fixed}
width="65px" width="65px"
></ElTableColumn> ></ElTableColumn>
) )
@ -438,6 +457,7 @@ export default defineComponent({
reserveSelection={reserveSelection} reserveSelection={reserveSelection}
align={align} align={align}
headerAlign={headerAlign} headerAlign={headerAlign}
selectable={v.selectable}
width="50" width="50"
></ElTableColumn> ></ElTableColumn>
) )
@ -451,24 +471,24 @@ export default defineComponent({
default: (...args: any[]) => { default: (...args: any[]) => {
const data = args[0] const data = args[0]
let isImageUrl = false let isPreview = false
if (preview.length) { isPreview =
isImageUrl = preview.some((item) => (item as string) === v.field) imagePreview.some((item) => (item as string) === v.field) ||
} videoPreview.some((item) => (item as string) === v.field)
return children && children.length return children && children.length
? renderTreeTableColumn(children) ? renderTreeTableColumn(children)
: props?.slots?.default : props?.slots?.default
? props.slots.default(args) ? props.slots.default(...args)
: v?.formatter : v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index) ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl : isPreview
? renderPreview(get(data.row, v.field)) ? renderPreview(get(data.row, v.field), v.field)
: get(data.row, v.field) : get(data.row, v.field)
} }
} }
if (props?.slots?.header) { if (props?.slots?.header) {
slots['header'] = (...args: any[]) => props.slots.header(args) slots['header'] = (...args: any[]) => props.slots.header(...args)
} }
return ( return (
@ -495,35 +515,63 @@ export default defineComponent({
tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args) tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
} }
// const { sortable } = unref(getProps)
// const sortableEl = sortable ? (
// <ElTableColumn
// className="table-move cursor-move"
// type="sortable"
// prop="sortable"
// width="60px"
// align="center"
// >
// <Icon icon="ant-design:drag-outlined" />
// </ElTableColumn>
// ) : null
return ( return (
<div v-loading={unref(getProps).loading}> <div v-loading={unref(getProps).loading}>
{unref(getProps).showAction ? ( {unref(getProps).customContent ? (
<TableActions <div class="flex flex-wrap">
columns={unref(getProps).columns} {unref(getProps)?.data?.length ? (
onChangSize={changSize} unref(getProps)?.data.map((item) => {
onRefresh={refresh} const cardSlots = {
/> default: () => {
) : null} return getSlot(slots, 'content', item)
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}> }
{{ }
default: () => renderTableColumn(), if (getSlot(slots, 'content-header')) {
...tableSlots cardSlots['header'] = () => {
}} return getSlot(slots, 'content-header', item)
</ElTable> }
}
if (getSlot(slots, 'content-footer')) {
cardSlots['footer'] = () => {
return getSlot(slots, 'content-footer', item)
}
}
return (
<ElCard
shadow="hover"
class={unref(getProps).cardWrapClass}
style={unref(getProps).cardWrapStyle}
bodyClass={unref(getProps).cardBodyClass}
bodyStyle={unref(getProps).cardBodyStyle}
>
{cardSlots}
</ElCard>
)
})
) : (
<div class="flex flex-1 justify-center">
<ElEmpty description="暂无数据" />
</div>
)}
</div>
) : (
<>
{unref(getProps).showAction && !unref(getProps).customContent ? (
<TableActions
columns={unref(getProps).columns}
onChangSize={changSize}
onRefresh={refresh}
onConfirm={confirmSetColumn}
/>
) : null}
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
{{
default: () => renderTableColumn(),
...tableSlots
}}
</ElTable>
</>
)}
{unref(getProps).pagination ? ( {unref(getProps).pagination ? (
<ElPagination <ElPagination
v-model:pageSize={pageSizeRef.value} v-model:pageSize={pageSizeRef.value}

View File

@ -0,0 +1,166 @@
<script setup lang="ts">
import {
ElDrawer,
ElCheckbox,
ElCheckboxGroup,
ElText,
ElRadioButton,
ElRadioGroup
} from 'element-plus'
import { TableColumn } from '../types'
import { PropType, ref, watch, unref } from 'vue'
import { cloneDeep } from 'lodash-es'
import { DEFAULT_FILTER_COLUMN } from '@/constants'
import { VueDraggable } from 'vue-draggable-plus'
const modelValue = defineModel<boolean>()
const props = defineProps({
columns: {
type: Array as PropType<TableColumn[]>,
default: () => []
}
})
const emit = defineEmits(['confirm'])
const oldColumns = ref<TableColumn[]>()
const settingColumns = ref<TableColumn[]>()
//
const hiddenColumns = ref<TableColumn[]>([])
const defaultCheckColumns = ref<string[]>([])
const checkColumns = ref<string[]>([])
const checkAll = ref(false)
const isIndeterminate = ref(true)
const handleCheckAllChange = (val: boolean) => {
checkColumns.value = val ? unref(defaultCheckColumns) : []
isIndeterminate.value = false
}
const handleCheckedColumnsChange = (value: string[]) => {
const checkedCount = value.length
checkAll.value = checkedCount === unref(defaultCheckColumns)?.length
isIndeterminate.value = checkedCount > 0 && checkedCount < unref(defaultCheckColumns)?.length
}
const confirm = () => {
const newColumns = cloneDeep(unref(settingColumns))?.map((item) => {
const fixed = unref(settingColumns)?.find((col) => col.field === item.field)?.fixed
item.hidden = !!!unref(checkColumns)?.includes(item.field)
item.fixed = fixed ? fixed : undefined
return item
})
emit('confirm', [...unref(hiddenColumns), ...(newColumns || [])])
modelValue.value = false
}
const restore = () => {
initColumns([...unref(hiddenColumns), ...(unref(oldColumns) || [])], true)
}
const initColumns = (columns: TableColumn[], isReStore = false) => {
const newColumns = columns?.filter((item) => {
if (!isReStore) {
item.fixed = item.fixed !== void 0 ? item.fixed : undefined
}
return (item.type && !DEFAULT_FILTER_COLUMN.includes(item.type)) || !item.type
})
if (!unref(oldColumns)?.length) {
oldColumns.value = cloneDeep(newColumns)
}
settingColumns.value = cloneDeep(newColumns)
hiddenColumns.value = cloneDeep(
columns?.filter((item) => item.type && DEFAULT_FILTER_COLUMN.includes(item.type))
)
defaultCheckColumns.value = unref(settingColumns)?.map((item) => item.field) || []
checkColumns.value =
unref(settingColumns)
?.filter((item) => !item.hidden)
?.map((item) => item.field) || []
if (unref(checkColumns)?.length === unref(defaultCheckColumns)?.length) {
checkAll.value = true
isIndeterminate.value = false
}
}
watch(
() => props.columns,
(columns) => {
initColumns(columns)
},
{
immediate: true,
deep: true
}
)
</script>
<template>
<ElDrawer v-model="modelValue" title="列设置" size="350px">
<div>
<div class="flex items-center justify-between">
<div class="flex items-center justify-between">
<ElCheckbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
/>
<ElText class="ml-8px!">{{ checkColumns.length }} / {{ settingColumns?.length }}</ElText>
</div>
<ElText>固定 / 排序</ElText>
</div>
<div v-if="settingColumns?.length">
<VueDraggable
v-model="settingColumns"
target=".el-checkbox-group"
handle=".handle"
:animation="150"
>
<ElCheckboxGroup
ref="draggableWrap"
v-model="checkColumns"
@change="handleCheckedColumnsChange"
>
<div
v-for="item in settingColumns"
:key="item.field"
class="flex items-center justify-between mt-12px"
>
<ElCheckbox :label="item.field">
{{ item.label }}
</ElCheckbox>
<div class="flex items-center">
<ElRadioGroup size="small" v-model="item.fixed">
<ElRadioButton label="left">
<Icon icon="ep:arrow-left" />
</ElRadioButton>
<ElRadioButton :label="undefined">
<Icon icon="ep:close" />
</ElRadioButton>
<ElRadioButton label="right">
<Icon icon="ep:arrow-right" />
</ElRadioButton>
</ElRadioGroup>
<div class="ml-12px cursor-move handle"><Icon icon="ep:rank" /></div>
</div>
</div>
</ElCheckboxGroup>
</VueDraggable>
</div>
</div>
<template #footer>
<div>
<BaseButton @click="restore">还原</BaseButton>
<BaseButton type="primary" @click="confirm">确定</BaseButton>
</div>
</template>
</ElDrawer>
</template>

View File

@ -1,36 +1,30 @@
<script lang="tsx"> <script lang="tsx">
import { defineComponent, unref, computed, PropType, watch } from 'vue' import { defineComponent, unref, computed, PropType, ref } from 'vue'
import { import { ElDropdown, ElDropdownMenu, ElDropdownItem, ComponentSize } from 'element-plus'
ElTooltip,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ComponentSize
// ElPopover,
// ElTree
} from 'element-plus'
import { Icon } from '@/components/Icon' import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { TableColumn } from '../types' import { TableColumn } from '../types'
import { cloneDeep } from 'lodash-es' import ColumnSetting from './ColumnSetting.vue'
// import { eachTree } from '@/utils/tree'
const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const { t } = useI18n()
export default defineComponent({ export default defineComponent({
name: 'TableActions', name: 'TableActions',
components: {
ColumnSetting
},
props: { props: {
columns: { columns: {
type: Array as PropType<TableColumn[]>, type: Array as PropType<TableColumn[]>,
default: () => [] default: () => []
} }
}, },
emits: ['refresh', 'changSize'], emits: ['refresh', 'changSize', 'confirm'],
setup(props, { emit }) { setup(props, { emit }) {
const appStore = useAppStore()
const { t } = useI18n()
const sizeMap = computed(() => appStore.sizeMap)
const showSetting = ref(false)
const refresh = () => { const refresh = () => {
emit('refresh') emit('refresh')
} }
@ -39,111 +33,71 @@ export default defineComponent({
emit('changSize', size) emit('changSize', size)
} }
const columns = computed(() => { const confirm = (columns: TableColumn[]) => {
return cloneDeep(props.columns).filter((v) => { emit('confirm', columns)
// typeselectionexpand }
if (v.type !== 'selection' && v.type !== 'expand') {
return v
}
})
})
watch( const showColumnSetting = () => {
() => columns.value, showSetting.value = true
(newColumns) => { }
console.log('columns change', newColumns)
},
{
deep: true
}
)
return () => ( return () => (
<> <>
<div class="text-right h-28px flex items-center justify-end"> <div class="text-right h-28px flex items-center justify-end">
<ElTooltip content={t('common.refresh')} placement="top"> <div title="刷新" class="w-30px h-20px flex items-center justify-end" onClick={refresh}>
<span onClick={refresh}> <Icon
<Icon icon="ant-design:sync-outlined"
icon="ant-design:sync-outlined" class="cursor-pointer"
class="cursor-pointer" hover-color="var(--el-color-primary)"
hover-color="var(--el-color-primary)" />
/> </div>
</span>
</ElTooltip>
<ElTooltip content={t('common.size')} placement="top"> <ElDropdown trigger="click" onCommand={changSize}>
<ElDropdown trigger="click" onCommand={changSize}>
{{
default: () => {
return (
<span>
<Icon
icon="ant-design:column-height-outlined"
class="cursor-pointer mr-8px ml-8px"
hover-color="var(--el-color-primary)"
/>
</span>
)
},
dropdown: () => {
return (
<ElDropdownMenu>
{{
default: () => {
return unref(sizeMap).map((v) => {
return (
<ElDropdownItem key={v} command={v}>
{t(`size.${v}`)}
</ElDropdownItem>
)
})
}
}}
</ElDropdownMenu>
)
}
}}
</ElDropdown>
</ElTooltip>
{/* <ElTooltip content={t('common.columnSetting')} placement="top"> */}
{/* <ElPopover trigger="click" placement="left">
{{ {{
default: () => { default: () => {
return ( return (
<div> <div title="尺寸" class="w-30px h-20px flex items-center justify-end">
<ElTree <Icon
data={unref(columns)} icon="ant-design:column-height-outlined"
show-checkbox class="cursor-pointer"
default-checked-keys={unref(defaultCheckeds)} hover-color="var(--el-color-primary)"
draggable
node-key="field"
allow-drop={(_draggingNode: any, _dropNode: any, type: string) => {
if (type === 'inner') {
return false
} else {
return true
}
}}
onNode-drag-end={onNodeDragEnd}
onCheck-change={onCheckChange}
/> />
</div> </div>
) )
}, },
reference: () => { dropdown: () => {
return ( return (
<Icon <ElDropdownMenu>
icon="ant-design:setting-outlined" {{
class="cursor-pointer" default: () => {
hoverColor="var(--el-color-primary)" return unref(sizeMap).map((v) => {
/> return (
<ElDropdownItem key={v} command={v}>
{t(`size.${v}`)}
</ElDropdownItem>
)
})
}
}}
</ElDropdownMenu>
) )
} }
}} }}
</ElPopover> */} </ElDropdown>
{/* </ElTooltip> */}
<div
title="列设置"
class="w-30px h-20px flex items-center justify-end"
onClick={showColumnSetting}
>
<Icon
icon="ant-design:setting-outlined"
class="cursor-pointer"
hover-color="var(--el-color-primary)"
/>
</div>
</div> </div>
<ColumnSetting v-model={showSetting.value} columns={props.columns} onConfirm={confirm} />
</> </>
) )
} }

View File

@ -12,6 +12,8 @@ import { useDesign } from '@/hooks/web/useDesign'
import { useTemplateRefsList } from '@vueuse/core' import { useTemplateRefsList } from '@vueuse/core'
import { ElScrollbar } from 'element-plus' import { ElScrollbar } from 'element-plus'
import { useScrollTo } from '@/hooks/event/useScrollTo' import { useScrollTo } from '@/hooks/event/useScrollTo'
import { useTagsView } from '@/hooks/web/useTagsView'
import { cloneDeep } from 'lodash-es'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@ -19,7 +21,9 @@ const prefixCls = getPrefixCls('tags-view')
const { t } = useI18n() const { t } = useI18n()
const { currentRoute, push, replace } = useRouter() const { currentRoute, push } = useRouter()
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
@ -31,6 +35,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews)
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([]) const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
const setSelectTag = tagsViewStore.setSelectedTag
const appStore = useAppStore() const appStore = useAppStore()
const tagsViewIcon = computed(() => appStore.getTagsViewIcon) const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
@ -43,66 +51,30 @@ const initTags = () => {
for (const tag of unref(affixTagArr)) { for (const tag of unref(affixTagArr)) {
// Must have tag name // Must have tag name
if (tag.name) { if (tag.name) {
tagsViewStore.addVisitedView(tag) tagsViewStore.addVisitedView(cloneDeep(tag))
} }
} }
} }
const selectedTag = ref<RouteLocationNormalizedLoaded>()
// tag // tag
const addTags = () => { const addTags = () => {
const { name } = unref(currentRoute) const { name } = unref(currentRoute)
if (name) { if (name) {
selectedTag.value = unref(currentRoute) setSelectTag(unref(currentRoute))
tagsViewStore.addView(unref(currentRoute)) tagsViewStore.addView(unref(currentRoute))
} }
return false
} }
// tag // tag
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => { const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
if (view?.meta?.affix) return closeCurrent(view, () => {
tagsViewStore.delView(view) if (isActive(view)) {
if (isActive(view)) { toLastView()
toLastView() }
}
}
//
const closeAllTags = () => {
tagsViewStore.delAllViews()
toLastView()
}
//
const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
//
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
if (!view) return
tagsViewStore.delCachedView()
const { path, query } = view
await nextTick()
replace({
path: '/redirect' + path,
query: query
}) })
} }
// //
const closeLeftTags = () => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
//
const closeRightTags = () => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
//
const toLastView = () => { const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0] const latestView = visitedViews.slice(-1)[0]
@ -121,6 +93,33 @@ const toLastView = () => {
} }
} }
//
const closeAllTags = () => {
closeAll(() => {
toLastView()
})
}
//
const closeOthersTags = () => {
closeOther()
}
//
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
refreshPage(view)
}
//
const closeLeftTags = () => {
closeLeft()
}
//
const closeRightTags = () => {
closeRight()
}
// tag // tag
const moveToCurrentTag = async () => { const moveToCurrentTag = async () => {
await nextTick() await nextTick()
@ -211,13 +210,14 @@ const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
// //
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>() const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
// //
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => { const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
if (visible) { if (visible) {
for (const v of unref(itemRefs)) { for (const v of unref(itemRefs)) {
const elDropdownMenuRef = v.elDropdownMenuRef const elDropdownMenuRef = v.elDropdownMenuRef
if (tagItem.fullPath !== v.tagItem.fullPath) { if (tagItem.fullPath !== v.tagItem.fullPath) {
elDropdownMenuRef?.handleClose() elDropdownMenuRef?.handleClose()
setSelectTag(tagItem)
} }
} }
} }

View File

@ -7,6 +7,8 @@ import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
const emit = defineEmits(['change'])
const prefixCls = getPrefixCls('theme-switch') const prefixCls = getPrefixCls('theme-switch')
const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' }) const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
@ -23,6 +25,7 @@ const blackColor = 'var(--el-color-black)'
const themeChange = (val: boolean) => { const themeChange = (val: boolean) => {
appStore.setIsDark(val) appStore.setIsDark(val)
emit('change', val)
} }
</script> </script>

View File

@ -1,49 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus' import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useStorage } from '@/hooks/web/useStorage'
import { resetRouter } from '@/router'
import { useRouter } from 'vue-router'
import { loginOutApi } from '@/api/login'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView'
import LockDialog from './components/LockDialog.vue' import LockDialog from './components/LockDialog.vue'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import LockPage from './components/LockPage.vue' import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/store/modules/lock' import { useLockStore } from '@/store/modules/lock'
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const lockStore = useLockStore() const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false) const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const tagsViewStore = useTagsViewStore()
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('user-info') const prefixCls = getPrefixCls('user-info')
const { t } = useI18n() const { t } = useI18n()
const { clear } = useStorage()
const { replace } = useRouter()
const loginOut = () => { const loginOut = () => {
ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), { userStore.logoutConfirm()
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
const res = await loginOutApi().catch(() => {})
if (res) {
clear()
tagsViewStore.delAllViews()
resetRouter() //
replace('/login')
}
})
.catch(() => {})
} }
const dialogVisible = ref<boolean>(false) const dialogVisible = ref<boolean>(false)
@ -66,7 +44,9 @@ const toDocument = () => {
alt="" alt=""
class="w-[calc(var(--logo-height)-25px)] rounded-[50%]" class="w-[calc(var(--logo-height)-25px)] rounded-[50%]"
/> />
<span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">Archer</span> <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">{{
userStore.getUserInfo?.username
}}</span>
</div> </div>
<template #dropdown> <template #dropdown>
<ElDropdownMenu> <ElDropdownMenu>

View File

@ -0,0 +1,27 @@
import { VNode, createVNode, render } from 'vue'
import VideoPlayer from './src/VideoPlayer.vue'
import { isClient } from '@/utils/is'
import { VideoPlayerViewer } from '@/components/VideoPlayerViewer'
import { toAnyString } from '@/utils'
export { VideoPlayer }
let instance: Nullable<VNode> = null
export function createVideoViewer(options: { url: string; poster?: string; show?: boolean }) {
if (!isClient) return
const { url, poster } = options
const propsData: Partial<{ url: string; poster?: string; show?: boolean; id?: string }> = {}
const container = document.createElement('div')
const id = toAnyString()
container.id = id
propsData.url = url
propsData.poster = poster
propsData.show = true
propsData.id = id
document.body.appendChild(container)
instance = createVNode(VideoPlayerViewer, propsData)
render(instance, container)
}

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import Player from 'xgplayer'
import { ref, unref, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
import 'xgplayer/dist/index.min.css'
const props = defineProps({
url: {
type: String,
default: '',
required: true
},
poster: {
type: String,
default: ''
}
})
const playerRef = ref<Player>()
const videoEl = ref<HTMLDivElement>()
const intiPlayer = () => {
if (!unref(videoEl)) return
new Player({
autoplay: false,
...props,
el: unref(videoEl)
})
}
onMounted(() => {
intiPlayer()
})
watch(
() => props,
async (newProps) => {
await nextTick()
if (newProps) {
unref(playerRef)?.setConfig(newProps)
}
},
{
deep: true
}
)
onBeforeUnmount(() => {
unref(playerRef)?.destroy()
})
defineExpose({
playerExpose: () => unref(playerRef)
})
</script>
<template>
<div ref="videoEl"></div>
</template>

View File

@ -0,0 +1,3 @@
import VideoPlayerViewer from './src/VideoPlayerViewer.vue'
export { VideoPlayerViewer }

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { VideoPlayer } from '@/components/VideoPlayer'
import { ElOverlay } from 'element-plus'
import { ref, nextTick } from 'vue'
import { Icon } from '@/components/Icon'
const props = defineProps({
show: {
type: Boolean,
default: false
},
url: {
type: String,
default: '',
required: true
},
poster: {
type: String,
default: ''
},
id: {
type: String,
default: ''
}
})
const visible = ref(props.show)
const close = async () => {
visible.value = false
await nextTick()
const wrap = document.getElementById(props.id)
if (!wrap) return
document.body.removeChild(wrap)
}
</script>
<template>
<ElOverlay v-show="visible" @click="close">
<div class="w-full h-full flex justify-center items-center relative" @click="close">
<div
class="w-44px h-44px color-[#fff] bg-[var(--el-text-color-regular)] rounded-full border-[#fff] flex justify-center items-center cursor-pointer absolute top-40px right-40px"
@click="close"
>
<Icon icon="ep:close" :size="24" />
</div>
<VideoPlayer :url="url" :poster="poster" />
</div>
</ElOverlay>
</template>

View File

@ -1,6 +1,8 @@
import type { App } from 'vue' import type { App } from 'vue'
import { Icon } from './Icon' import { Icon } from './Icon'
import { BaseButton } from './Button'
export const setupGlobCom = (app: App<Element>): void => { export const setupGlobCom = (app: App<Element>): void => {
app.component('Icon', Icon) app.component('Icon', Icon)
app.component('BaseButton', BaseButton)
} }

View File

@ -1,101 +0,0 @@
import {
AxiosConfig,
AxiosResponse,
AxiosRequestHeaders,
AxiosError,
InternalAxiosRequestConfig
} from './types'
import { ElMessage } from 'element-plus'
import qs from 'qs'
const config: AxiosConfig = {
/**
* api请求基础路径
*/
baseUrl: {
// 开发环境接口前缀
base: '',
// 打包开发环境接口前缀
dev: '',
// 打包生产环境接口前缀
pro: '',
// 打包测试环境接口前缀
test: ''
},
/**
*
*/
code: 0,
/**
*
*/
timeout: 60000,
/**
*
* application/x-www-form-urlencoded multipart/form-data
*/
defaultHeaders: 'application/json',
interceptors: {
//请求拦截
// requestInterceptors: (config) => {
// return config
// },
// 响应拦截器
// responseInterceptors: (result: AxiosResponse) => {
// return result
// }
}
}
const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
if (
config.method === 'post' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] === 'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(config.data)
}
if (config.method === 'get' && config.params) {
let url = config.url as string
url += '?'
const keys = Object.keys(config.params)
for (const key of keys) {
if (config.params[key] !== void 0 && config.params[key] !== null) {
url += `${key}=${encodeURIComponent(config.params[key])}&`
}
}
url = url.substring(0, url.length - 1)
config.params = {}
config.url = url
}
return config
}
;(error: AxiosError) => {
console.log(error)
Promise.reject(error)
}
const defaultResponseInterceptors = (response: AxiosResponse<any>) => {
if (response?.config?.responseType === 'blob') {
// 如果是文件流,直接过
return response
} else if (response.data.code === config.code) {
return response.data
} else {
ElMessage.error(response.data.message)
}
}
;(error: AxiosError) => {
console.log('err' + error) // for debug
ElMessage.error(error.message)
return Promise.reject(error)
}
export { defaultResponseInterceptors, defaultRequestInterceptors }
export default config

34
src/constants/index.ts Normal file
View File

@ -0,0 +1,34 @@
/**
*
*/
export const SUCCESS_CODE = 0
/**
* contentType
*/
export const CONTENT_TYPE: AxiosContentType = 'application/json'
/**
*
*/
export const REQUEST_TIMEOUT = 60000
/**
*
*/
export const NO_REDIRECT_WHITE_LIST = ['/login']
/**
*
*/
export const NO_RESET_WHITE_LIST = ['Redirect', 'Login', 'NoFind', 'Root']
/**
*
*/
export const DEFAULT_FILTER_COLUMN = ['expand', 'selection']
/**
* headers->content-type自动转换数据格式
*/
export const TRANSFORM_REQUEST_DATA = true

View File

@ -1,10 +0,0 @@
import type { App } from 'vue'
import { setupPermissionDirective } from './permission/hasPermi'
/**
* v-xxx
* @methods hasPermi 用法: v-hasPermi
*/
export const setupPermission = (app: App<Element>) => {
setupPermissionDirective(app)
}

View File

@ -1,47 +0,0 @@
import type { App, Directive, DirectiveBinding } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useStorage } from '@/hooks/web/useStorage'
import { intersection } from 'lodash-es'
import { isArray } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app'
const { t } = useI18n()
const { getStorage } = useStorage()
const appStore = useAppStoreWithOut()
// 全部权限
const all_permission = ['*.*.*']
const hasPermission = (value: string | string[]): boolean => {
const permissions = getStorage(appStore.getUserInfo).permissions as string[]
if (!value) {
throw new Error(t('permission.hasPermission'))
}
if (!isArray(value)) {
return permissions?.includes(value as string)
}
if (all_permission[0] === permissions[0]) {
return true
}
return (intersection(value, permissions) as string[]).length > 0
}
function hasPermi(el: Element, binding: DirectiveBinding) {
const value = binding.value
const flag = hasPermission(value)
if (!flag) {
el.parentNode?.removeChild(el)
}
}
const mounted = (el: Element, binding: DirectiveBinding<any>) => {
hasPermi(el, binding)
}
const permiDirective: Directive = {
mounted
}
export const setupPermissionDirective = (app: App<Element>) => {
app.directive('hasPermi', permiDirective)
}
export default permiDirective

View File

@ -78,20 +78,14 @@ const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const schemaItem = crudSchema[i] const schemaItem = crudSchema[i]
// 判断是否隐藏 const searchSchemaItem = {
if (!schemaItem?.search?.hidden) { component: schemaItem?.search?.component || 'Input',
const searchSchemaItem = { ...schemaItem.search,
component: schemaItem?.search?.component || 'Input', field: schemaItem.field,
...schemaItem.search, label: schemaItem.label
field: schemaItem.field,
label: schemaItem.label
}
// 删除不必要的字段
delete searchSchemaItem.hidden
searchSchema.push(searchSchemaItem)
} }
searchSchema.push(searchSchemaItem)
} }
return searchSchema return searchSchema
@ -127,19 +121,14 @@ const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const formItem = crudSchema[i] const formItem = crudSchema[i]
// 判断是否隐藏 // 判断是否隐藏
if (!formItem?.form?.hidden) { const formSchemaItem = {
const formSchemaItem = { component: formItem?.form?.component || 'Input',
component: formItem?.form?.component || 'Input', ...formItem.form,
...formItem.form, field: formItem.field,
field: formItem.field, label: formItem.label
label: formItem.label
}
// 删除不必要的字段
delete formSchemaItem.hidden
formSchema.push(formSchemaItem)
} }
formSchema.push(formSchemaItem)
} }
return formSchema return formSchema

View File

@ -1,47 +0,0 @@
import introJs from 'intro.js'
import { IntroJs, Step, Options } from 'intro.js'
import 'intro.js/introjs.css'
import { useI18n } from '@/hooks/web/useI18n'
import { useDesign } from '@/hooks/web/useDesign'
export const useIntro = (setps?: Step[], options?: Options) => {
const { t } = useI18n()
const { variables } = useDesign()
const defaultSetps: Step[] = setps || [
{
element: `#${variables.namespace}-menu`,
title: t('common.menu'),
intro: t('common.menuDes'),
position: 'right'
},
{
element: `#${variables.namespace}-tool-header`,
title: t('common.tool'),
intro: t('common.toolDes'),
position: 'left'
},
{
element: `#${variables.namespace}-tags-view`,
title: t('common.tagsView'),
intro: t('common.tagsViewDes'),
position: 'bottom'
}
]
const defaultOptions: Options = options || {
prevLabel: t('common.prevLabel'),
nextLabel: t('common.nextLabel'),
skipLabel: t('common.skipLabel'),
doneLabel: t('common.doneLabel')
}
const introRef: IntroJs = introJs()
introRef.addSteps(defaultSetps).setOptions(defaultOptions)
return {
introRef
}
}

View File

@ -1,13 +1,13 @@
import { useAppStoreWithOut } from '@/store/modules/app' import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
export const usePageLoading = () => { export const usePageLoading = () => {
const loadStart = () => { const loadStart = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(true) appStore.setPageLoading(true)
} }
const loadDone = () => { const loadDone = () => {
const appStore = useAppStoreWithOut()
appStore.setPageLoading(false) appStore.setPageLoading(false)
} }

View File

@ -0,0 +1,63 @@
import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
import { computed, nextTick, unref } from 'vue'
export const useTagsView = () => {
const tagsViewStore = useTagsViewStoreWithOut()
const { replace, currentRoute } = useRouter()
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
const closeAll = (callback?: Fn) => {
tagsViewStore.delAllViews()
callback?.()
}
const closeLeft = (callback?: Fn) => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
callback?.()
}
const closeRight = (callback?: Fn) => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
callback?.()
}
const closeOther = (callback?: Fn) => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
callback?.()
}
const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
if (view?.meta?.affix) return
tagsViewStore.delView(view || unref(currentRoute))
callback?.()
}
const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
tagsViewStore.delCachedView()
const { path, query } = view || unref(currentRoute)
await nextTick()
replace({
path: '/redirect' + path,
query: query
})
callback?.()
}
const setTitle = (title: string, path?: string) => {
tagsViewStore.setTitle(title, path)
}
return {
closeAll,
closeLeft,
closeRight,
closeOther,
closeCurrent,
refreshPage,
setTitle
}
}

View File

@ -3,10 +3,10 @@ import { isString } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app' import { useAppStoreWithOut } from '@/store/modules/app'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
const appStore = useAppStoreWithOut()
export const useTitle = (newTitle?: string) => { export const useTitle = (newTitle?: string) => {
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStoreWithOut()
const title = ref( const title = ref(
newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle
) )

View File

@ -71,8 +71,14 @@ export default defineComponent({
.@{prefix-cls} { .@{prefix-cls} {
background-color: var(--app-content-bg-color); background-color: var(--app-content-bg-color);
:deep(.@{elNamespace}-scrollbar__view) { .@{prefix-cls}-content-scrollbar {
height: 100% !important; & > :deep(.el-scrollbar__wrap) {
& > .@{elNamespace}-scrollbar__view {
display: flex;
height: 100% !important;
flex-direction: column;
}
}
} }
} }
</style> </style>

View File

@ -6,10 +6,6 @@ import { computed } from 'vue'
const appStore = useAppStore() const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const fixedHeader = computed(() => appStore.getFixedHeader)
const footer = computed(() => appStore.getFooter) const footer = computed(() => appStore.getFooter)
const tagsViewStore = useTagsViewStore() const tagsViewStore = useTagsViewStore()
@ -17,39 +13,12 @@ const tagsViewStore = useTagsViewStore()
const getCaches = computed((): string[] => { const getCaches = computed((): string[] => {
return tagsViewStore.getCachedViews return tagsViewStore.getCachedViews
}) })
const tagsView = computed(() => appStore.getTagsView)
</script> </script>
<template> <template>
<section <section
:class="[ :class="[
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]', 'flex-1 p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]'
{
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
(fixedHeader &&
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
footer) ||
(!tagsView && layout === 'top' && footer),
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
tagsView && layout === 'top' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
!fixedHeader && layout === 'classic' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
!fixedHeader && layout === 'topLeft' && footer,
// '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height)-var(--top-tool-height))]':
// !fixedHeader && layout === 'top' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
fixedHeader && layout === 'cutMenu' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
!fixedHeader && layout === 'cutMenu' && footer
}
]" ]"
> >
<router-view> <router-view>

View File

@ -41,8 +41,7 @@ export default defineComponent({
id={`${variables.namespace}-tool-header`} id={`${variables.namespace}-tool-header`}
class={[ class={[
prefixCls, prefixCls,
'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between', 'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
'dark:bg-[var(--el-bg-color)]'
]} ]}
> >
{layout.value !== 'top' ? ( {layout.value !== 'top' ? (

View File

@ -91,7 +91,8 @@ export default {
tagsViewIcon: 'Tags view icon', tagsViewIcon: 'Tags view icon',
dynamicRouter: 'Dynamic router', dynamicRouter: 'Dynamic router',
reExperienced: 'Please exit the login experience again', reExperienced: 'Please exit the login experience again',
fixedMenu: 'Fixed menu' fixedMenu: 'Fixed menu',
serverDynamicRouter: 'Server dynamic router'
}, },
size: { size: {
default: 'Default', default: 'Default',

View File

@ -91,7 +91,8 @@ export default {
tagsViewIcon: '标签页图标', tagsViewIcon: '标签页图标',
dynamicRouter: '动态路由', dynamicRouter: '动态路由',
reExperienced: '请重新退出登录体验', reExperienced: '请重新退出登录体验',
fixedMenu: '固定菜单' fixedMenu: '固定菜单',
serverDynamicRouter: '服务端动态路由'
}, },
size: { size: {
default: '默认', default: '默认',

View File

@ -1,4 +1,6 @@
// 引入windi css import 'vue/jsx'
// 引入unocss
import '@/plugins/unocss' import '@/plugins/unocss'
// 导入全局的svg图标 // 导入全局的svg图标
@ -25,9 +27,6 @@ import '@/plugins/animate.css'
// 路由 // 路由
import { setupRouter } from './router' import { setupRouter } from './router'
// 权限
import { setupPermission } from './directives'
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
@ -48,8 +47,6 @@ const setupAll = async () => {
setupRouter(app) setupRouter(app)
setupPermission(app)
app.mount('#app') app.mount('#app')
} }

View File

@ -1,59 +1,42 @@
import router from './router' import router from './router'
import { useAppStoreWithOut } from '@/store/modules/app' import { useAppStoreWithOut } from '@/store/modules/app'
import { useStorage } from '@/hooks/web/useStorage'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { useTitle } from '@/hooks/web/useTitle' import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress' import { useNProgress } from '@/hooks/web/useNProgress'
import { usePermissionStoreWithOut } from '@/store/modules/permission' import { usePermissionStoreWithOut } from '@/store/modules/permission'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { usePageLoading } from '@/hooks/web/usePageLoading' import { usePageLoading } from '@/hooks/web/usePageLoading'
import { getDictApi } from '@/api/common' import { NO_REDIRECT_WHITE_LIST } from '@/constants'
import { useUserStoreWithOut } from '@/store/modules/user'
const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut()
const dictStore = useDictStoreWithOut()
const { getStorage } = useStorage()
const { start, done } = useNProgress() const { start, done } = useNProgress()
const { loadStart, loadDone } = usePageLoading() const { loadStart, loadDone } = usePageLoading()
const whiteList = ['/login'] // 不重定向白名单
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
start() start()
loadStart() loadStart()
if (getStorage(appStore.getUserInfo)) { const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut()
const userStore = useUserStoreWithOut()
if (userStore.getUserInfo) {
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }) next({ path: '/' })
} else { } else {
if (!dictStore.getIsSetDict) {
// 获取所有字典
const res = await getDictApi()
if (res) {
dictStore.setDictObj(res.data)
dictStore.setIsSetDict(true)
}
}
if (permissionStore.getIsAddRouters) { if (permissionStore.getIsAddRouters) {
next() next()
return return
} }
// 开发者可根据实际情况进行修改 // 开发者可根据实际情况进行修改
const roleRouters = getStorage('roleRouters') || [] const roleRouters = userStore.getRoleRouters || []
const userInfo = getStorage(appStore.getUserInfo)
// 是否使用动态路由 // 是否使用动态路由
if (appStore.getDynamicRouter) { if (appStore.getDynamicRouter) {
userInfo.role === 'admin' appStore.serverDynamicRouter
? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[]) ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[])
: await permissionStore.generateRoutes('test', roleRouters as string[]) : await permissionStore.generateRoutes('frontEnd', roleRouters as string[])
} else { } else {
await permissionStore.generateRoutes('none') await permissionStore.generateRoutes('static')
} }
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {
@ -66,7 +49,7 @@ router.beforeEach(async (to, from, next) => {
next(nextData) next(nextData)
} }
} else { } else {
if (whiteList.indexOf(to.path) !== -1) { if (NO_REDIRECT_WHITE_LIST.indexOf(to.path) !== -1) {
next() next()
} else { } else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页

View File

@ -12,7 +12,13 @@ export const setupElementPlus = (app: App<Element>) => {
app.use(plugin) app.use(plugin)
}) })
// 为了开发环境启动更快,一次性引入所有样式
if (import.meta.env.VITE_USE_ALL_ELEMENT_PLUS_STYLE === 'true') {
import('element-plus/dist/index.css')
return
}
components.forEach((component) => { components.forEach((component) => {
app.component(component.name, component) app.component(component.name!, component)
}) })
} }

View File

@ -1,3 +1 @@
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register'
import '@purge-icons/generated'

View File

@ -10,7 +10,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: Layout, component: Layout,
redirect: '/dashboard/analysis', redirect: '/level',
name: 'Root', name: 'Root',
meta: { meta: {
hidden: true hidden: true
@ -56,292 +56,6 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
] ]
export const asyncRouterMap: AppRouteRecordRaw[] = [ export const asyncRouterMap: AppRouteRecordRaw[] = [
{
path: '/dashboard',
component: Layout,
redirect: '/dashboard/analysis',
name: 'Dashboard',
meta: {
title: t('router.dashboard'),
icon: 'ant-design:dashboard-filled',
alwaysShow: true
},
children: [
{
path: 'analysis',
component: () => import('@/views/Dashboard/Analysis.vue'),
name: 'Analysis',
meta: {
title: t('router.analysis'),
noCache: true,
affix: true
}
},
{
path: 'workplace',
component: () => import('@/views/Dashboard/Workplace.vue'),
name: 'Workplace',
meta: {
title: t('router.workplace'),
noCache: true
}
}
]
},
{
path: '/external-link',
component: Layout,
meta: {},
name: 'ExternalLink',
children: [
{
path: 'https://element-plus-admin-doc.cn/',
name: 'DocumentLink',
meta: {
title: t('router.document'),
icon: 'clarity:document-solid'
}
}
]
},
{
path: '/guide',
component: Layout,
name: 'Guide',
meta: {},
children: [
{
path: 'index',
component: () => import('@/views/Guide/Guide.vue'),
name: 'GuideDemo',
meta: {
title: t('router.guide'),
icon: 'cib:telegram-plane'
}
}
]
},
{
path: '/components',
component: Layout,
name: 'ComponentsDemo',
meta: {
title: t('router.component'),
icon: 'bx:bxs-component',
alwaysShow: true
},
children: [
{
path: 'form',
component: getParentLayout(),
redirect: '/components/form/default-form',
name: 'Form',
meta: {
title: t('router.form'),
alwaysShow: true
},
children: [
{
path: 'default-form',
component: () => import('@/views/Components/Form/DefaultForm.vue'),
name: 'DefaultForm',
meta: {
title: t('router.defaultForm')
}
},
{
path: 'use-form',
component: () => import('@/views/Components/Form/UseFormDemo.vue'),
name: 'UseForm',
meta: {
title: 'UseForm'
}
}
]
},
{
path: 'table',
component: getParentLayout(),
redirect: '/components/table/default-table',
name: 'TableDemo',
meta: {
title: t('router.table'),
alwaysShow: true
},
children: [
{
path: 'default-table',
component: () => import('@/views/Components/Table/DefaultTable.vue'),
name: 'DefaultTable',
meta: {
title: t('router.defaultTable')
}
},
{
path: 'use-table',
component: () => import('@/views/Components/Table/UseTableDemo.vue'),
name: 'UseTable',
meta: {
title: 'UseTable'
}
},
{
path: 'tree-table',
component: () => import('@/views/Components/Table/TreeTable.vue'),
name: 'TreeTable',
meta: {
title: t('router.treeTable')
}
},
{
path: 'table-image-preview',
component: () => import('@/views/Components/Table/TableImagePreview.vue'),
name: 'TableImagePreview',
meta: {
title: t('router.PicturePreview')
}
}
]
},
{
path: 'editor-demo',
component: getParentLayout(),
redirect: '/components/editor-demo/editor',
name: 'EditorDemo',
meta: {
title: t('router.editor'),
alwaysShow: true
},
children: [
{
path: 'editor',
component: () => import('@/views/Components/Editor/Editor.vue'),
name: 'Editor',
meta: {
title: t('router.richText')
}
}
]
},
{
path: 'search',
component: () => import('@/views/Components/Search.vue'),
name: 'Search',
meta: {
title: t('router.search')
}
},
{
path: 'descriptions',
component: () => import('@/views/Components/Descriptions.vue'),
name: 'Descriptions',
meta: {
title: t('router.descriptions')
}
},
{
path: 'image-viewer',
component: () => import('@/views/Components/ImageViewer.vue'),
name: 'ImageViewer',
meta: {
title: t('router.imageViewer')
}
},
{
path: 'dialog',
component: () => import('@/views/Components/Dialog.vue'),
name: 'Dialog',
meta: {
title: t('router.dialog')
}
},
{
path: 'icon',
component: () => import('@/views/Components/Icon.vue'),
name: 'Icon',
meta: {
title: t('router.icon')
}
},
{
path: 'echart',
component: () => import('@/views/Components/Echart.vue'),
name: 'Echart',
meta: {
title: t('router.echart')
}
},
{
path: 'count-to',
component: () => import('@/views/Components/CountTo.vue'),
name: 'CountTo',
meta: {
title: t('router.countTo')
}
},
{
path: 'qrcode',
component: () => import('@/views/Components/Qrcode.vue'),
name: 'Qrcode',
meta: {
title: t('router.qrcode')
}
},
{
path: 'highlight',
component: () => import('@/views/Components/Highlight.vue'),
name: 'Highlight',
meta: {
title: t('router.highlight')
}
},
{
path: 'infotip',
component: () => import('@/views/Components/Infotip.vue'),
name: 'Infotip',
meta: {
title: t('router.infotip')
}
},
{
path: 'input-password',
component: () => import('@/views/Components/InputPassword.vue'),
name: 'InputPassword',
meta: {
title: t('router.inputPassword')
}
}
]
},
{
path: '/hooks',
component: Layout,
redirect: '/hooks/useWatermark',
name: 'Hooks',
meta: {
title: 'hooks',
icon: 'ic:outline-webhook',
alwaysShow: true
},
children: [
{
path: 'useWatermark',
component: () => import('@/views/hooks/useWatermark.vue'),
name: 'UseWatermark',
meta: {
title: 'useWatermark'
}
}
// {
// path: 'useCrudSchemas',
// component: () => import('@/views/hooks/useCrudSchemas.vue'),
// name: 'UseCrudSchemas',
// meta: {
// title: 'useCrudSchemas'
// }
// }
]
},
{ {
path: '/level', path: '/level',
component: Layout, component: Layout,
@ -400,156 +114,6 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
} }
} }
] ]
},
{
path: '/example',
component: Layout,
redirect: '/example/example-dialog',
name: 'Example',
meta: {
title: t('router.example'),
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-dialog',
component: () => import('@/views/Example/Dialog/ExampleDialog.vue'),
name: 'ExampleDialog',
meta: {
title: t('router.exampleDialog')
}
},
{
path: 'example-page',
component: () => import('@/views/Example/Page/ExamplePage.vue'),
name: 'ExamplePage',
meta: {
title: t('router.examplePage')
}
},
{
path: 'example-add',
component: () => import('@/views/Example/Page/ExampleAdd.vue'),
name: 'ExampleAdd',
meta: {
title: t('router.exampleAdd'),
noTagsView: true,
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/example/example-page'
}
},
{
path: 'example-edit',
component: () => import('@/views/Example/Page/ExampleEdit.vue'),
name: 'ExampleEdit',
meta: {
title: t('router.exampleEdit'),
noTagsView: true,
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/example/example-page'
}
},
{
path: 'example-detail',
component: () => import('@/views/Example/Page/ExampleDetail.vue'),
name: 'ExampleDetail',
meta: {
title: t('router.exampleDetail'),
noTagsView: true,
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/example/example-page'
}
}
]
},
{
path: '/error',
component: Layout,
redirect: '/error/404',
name: 'Error',
meta: {
title: t('router.errorPage'),
icon: 'ci:error',
alwaysShow: true
},
children: [
{
path: '404-demo',
component: () => import('@/views/Error/404.vue'),
name: '404Demo',
meta: {
title: '404'
}
},
{
path: '403-demo',
component: () => import('@/views/Error/403.vue'),
name: '403Demo',
meta: {
title: '403'
}
},
{
path: '500-demo',
component: () => import('@/views/Error/500.vue'),
name: '500Demo',
meta: {
title: '500'
}
}
]
},
{
path: '/authorization',
component: Layout,
redirect: '/authorization/user',
name: 'Authorization',
meta: {
title: t('router.authorization'),
icon: 'eos-icons:role-binding',
alwaysShow: true
},
children: [
{
path: 'department',
component: () => import('@/views/Authorization/Department/Department.vue'),
name: 'Department',
meta: {
title: t('router.department')
}
},
{
path: 'user',
component: () => import('@/views/Authorization/User/User.vue'),
name: 'User',
meta: {
title: t('router.user')
}
},
{
path: 'menu',
component: () => import('@/views/Authorization/Menu/Menu.vue'),
name: 'Menu',
meta: {
title: t('router.menuManagement')
}
},
{
path: 'role',
component: () => import('@/views/Authorization/Role/Role.vue'),
name: 'Role',
meta: {
title: t('router.role')
}
}
]
} }
] ]

View File

@ -1,10 +1,10 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia() const store = createPinia()
store.use(piniaPersist) store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => { export const setupStore = (app: App<Element>) => {
app.use(store) app.use(store)

Some files were not shown because too many files have changed in this diff Show More