Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d722f5cbc0 | ||
|
|
c77ba236e7 | ||
|
|
c6d57ed0db | ||
|
|
e4c5330a18 | ||
|
|
772e35b35b | ||
|
|
05496ae4c4 | ||
|
|
c6a4604d1f | ||
|
|
a4bb81dc0a | ||
|
|
79e16909f0 | ||
|
|
23a7285e29 | ||
|
|
2a76877bdb | ||
|
|
0a54a7affb | ||
|
|
fc84d925d6 | ||
|
|
a76046e2cc | ||
|
|
9a2fb78f42 | ||
|
|
6d93092f10 | ||
|
|
cbdc1231ce | ||
|
|
90297a14ed | ||
|
|
9a8a2e3064 | ||
|
|
b7ca434172 | ||
|
|
c21e778036 | ||
|
|
65090d8dcf | ||
|
|
a6035e5f5f | ||
|
|
b03db2d89e | ||
|
|
822c4e9f90 |
6
.npmrc
Normal file
6
.npmrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 设置 npm 官方镜像源为淘宝镜像
|
||||||
|
registry=https://registry.npmmirror.com/
|
||||||
|
|
||||||
|
# 常见二进制依赖包的镜像地址
|
||||||
|
sass_binary_site=https://npmmirror.com/mirrors/node-sass/
|
||||||
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
163
Jenkinsfile
vendored
163
Jenkinsfile
vendored
@@ -28,7 +28,7 @@ pipeline {
|
|||||||
extensions : [
|
extensions : [
|
||||||
[$class: 'RelativeTargetDirectory', relativeTargetDir: 'docker'],
|
[$class: 'RelativeTargetDirectory', relativeTargetDir: 'docker'],
|
||||||
],
|
],
|
||||||
userRemoteConfigs: [[credentialsId: 'jenkins', url: 'https://gitlab.fengchaoit.com/basic/buildscripts.git']]
|
userRemoteConfigs: [[credentialsId: '82ba88dd-b6dd-46f0-8448-3cbd60bf15c2', url: 'https://gitea.zzmjart.com/mj/devopsscripts.git']]
|
||||||
])
|
])
|
||||||
sh "echo '拷贝Dockerfile文件'"
|
sh "echo '拷贝Dockerfile文件'"
|
||||||
sh "cp ./docker/front/Dockerfile ."
|
sh "cp ./docker/front/Dockerfile ."
|
||||||
@@ -49,9 +49,9 @@ pipeline {
|
|||||||
stage('推送镜像到镜像仓库') {
|
stage('推送镜像到镜像仓库') {
|
||||||
steps {
|
steps {
|
||||||
sh "echo '改镜像标签'"
|
sh "echo '改镜像标签'"
|
||||||
sh "docker tag $projectName:$targetVersion harbor.fengchaoit.com/$groupName/$projectName:$targetVersion"
|
sh "docker tag $projectName:$targetVersion 172.31.127.251:8083/$groupName/$projectName:$targetVersion"
|
||||||
sh "echo '镜像入库'"
|
sh "echo '镜像入库'"
|
||||||
sh "docker push harbor.fengchaoit.com/$groupName/$projectName:$targetVersion"
|
sh "docker push 172.31.127.251:8083/$groupName/$projectName:$targetVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,57 +68,57 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
stage('保存到发布版本目录'){
|
// stage('保存到发布版本目录'){
|
||||||
when {
|
// when {
|
||||||
expression { env.branch == "main" }
|
// expression { env.branch == "main" }
|
||||||
}
|
// }
|
||||||
steps {
|
// steps {
|
||||||
sh "mkdir -p /projects/$groupName/$targetVersion/$projectName"
|
// sh "mkdir -p /projects/$groupName/$targetVersion/$projectName"
|
||||||
sh "rm -f /projects/$groupName/$targetVersion/$projectName/*"
|
// sh "rm -f /projects/$groupName/$targetVersion/$projectName/*"
|
||||||
sh "cp project/* /projects/$groupName/$targetVersion/$projectName/"
|
// sh "cp project/* /projects/$groupName/$targetVersion/$projectName/"
|
||||||
script {
|
// script {
|
||||||
if(Boolean.parseBoolean(proddeploy)) {
|
// if(Boolean.parseBoolean(proddeploy)) {
|
||||||
sshPublisher(
|
// sshPublisher(
|
||||||
continueOnError: false,
|
// continueOnError: false,
|
||||||
failOnError: true,
|
// failOnError: true,
|
||||||
publishers: [
|
// publishers: [
|
||||||
sshPublisherDesc(
|
// sshPublisherDesc(
|
||||||
configName: env.branchConfig,
|
// configName: env.branchConfig,
|
||||||
transfers: [
|
// transfers: [
|
||||||
sshTransfer(
|
// sshTransfer(
|
||||||
cleanRemote: false,
|
// cleanRemote: false,
|
||||||
excludes: '',
|
// excludes: '',
|
||||||
execCommand: [
|
// execCommand: [
|
||||||
"echo 定位到项目位置",
|
// "echo 定位到项目位置",
|
||||||
"cd /usr/local/fengchaoit/$projectName",
|
// "cd /usr/local/fengchaoit/$projectName",
|
||||||
"echo 授予启动脚本权限",
|
// "echo 授予启动脚本权限",
|
||||||
"chmod +x ./apprun.sh",
|
// "chmod +x ./apprun.sh",
|
||||||
"echo 停止正在运行服务",
|
// "echo 停止正在运行服务",
|
||||||
"./apprun.sh remove",
|
// "./apprun.sh remove",
|
||||||
"echo 启动新构建服务",
|
// "echo 启动新构建服务",
|
||||||
"./apprun.sh restart"
|
// "./apprun.sh restart"
|
||||||
].join('\n'),
|
// ].join('\n'),
|
||||||
execTimeout: 120000,
|
// execTimeout: 120000,
|
||||||
flatten: false,
|
// flatten: false,
|
||||||
makeEmptyDirs: false,
|
// makeEmptyDirs: false,
|
||||||
noDefaultExcludes: false,
|
// noDefaultExcludes: false,
|
||||||
patternSeparator: '[, ]+',
|
// patternSeparator: '[, ]+',
|
||||||
remoteDirectory: "/usr/local/fengchaoit/$projectName",
|
// remoteDirectory: "/usr/local/fengchaoit/$projectName",
|
||||||
remoteDirectorySDF: false,
|
// remoteDirectorySDF: false,
|
||||||
removePrefix: 'project',
|
// removePrefix: 'project',
|
||||||
sourceFiles: 'project/*'
|
// sourceFiles: 'project/*'
|
||||||
)
|
// )
|
||||||
],
|
// ],
|
||||||
usePromotionTimestamp: false,
|
// usePromotionTimestamp: false,
|
||||||
useWorkspaceInPromotion: false,
|
// useWorkspaceInPromotion: false,
|
||||||
verbose: true
|
// verbose: true
|
||||||
)
|
// )
|
||||||
]
|
// ]
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
stage('部署项目到服务器'){
|
stage('部署项目到服务器'){
|
||||||
@@ -138,7 +138,7 @@ pipeline {
|
|||||||
excludes: '',
|
excludes: '',
|
||||||
execCommand: [
|
execCommand: [
|
||||||
"echo 定位到项目位置",
|
"echo 定位到项目位置",
|
||||||
"cd /usr/local/fengchaoit/WorkSpace/$projectName",
|
"cd /opt/mingjiang/$projectName",
|
||||||
"echo 授予启动脚本权限",
|
"echo 授予启动脚本权限",
|
||||||
"chmod +x ./apprun.sh",
|
"chmod +x ./apprun.sh",
|
||||||
"echo 停止正在运行服务",
|
"echo 停止正在运行服务",
|
||||||
@@ -151,7 +151,7 @@ pipeline {
|
|||||||
makeEmptyDirs: false,
|
makeEmptyDirs: false,
|
||||||
noDefaultExcludes: false,
|
noDefaultExcludes: false,
|
||||||
patternSeparator: '[, ]+',
|
patternSeparator: '[, ]+',
|
||||||
remoteDirectory: "/usr/local/fengchaoit/WorkSpace/$projectName",
|
remoteDirectory: "/opt/mingjiang/$projectName",
|
||||||
remoteDirectorySDF: false,
|
remoteDirectorySDF: false,
|
||||||
removePrefix: 'project',
|
removePrefix: 'project',
|
||||||
sourceFiles: 'project/*'
|
sourceFiles: 'project/*'
|
||||||
@@ -174,28 +174,37 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
success {
|
// success {
|
||||||
script {
|
// script {
|
||||||
if(env.branch == "main"){
|
// if(env.branch == "main"){
|
||||||
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${targetVersion}版本打包成功,请及时获取备份\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目打包成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
// sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${targetVersion}版本打包成功,请及时获取备份\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目打包成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
} else if(env.branch == "dev" || env.branch == "test" || env.branch =~ /(^f_.*_dev$)|(^fix_.*_test$)/) {
|
// } else if(env.branch == "dev" || env.branch == "test" || env.branch =~ /(^f_.*_dev$)|(^fix_.*_test$)/) {
|
||||||
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${targetVersion}版本打包成功且已部署到${env.branchConfig}环境,请及时查看验证\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目部署成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
// sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${targetVersion}版本打包成功且已部署到${env.branchConfig}环境,请及时查看验证\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目部署成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
} else {
|
// } else {
|
||||||
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建成功,非有效分支无法进行远程部署,请及时检查\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
// sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建成功,非有效分支无法进行远程部署,请及时检查\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
failure {
|
// failure {
|
||||||
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建失败,请及时查看失败原因并修复\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"项目构建失败通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建失败\"},\"template\":\"red\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"close_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
// sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建失败,请及时查看失败原因并修复\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"项目构建失败通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建失败\"},\"template\":\"red\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"close_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
}
|
// }
|
||||||
aborted {
|
// aborted {
|
||||||
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建未成功,请及时查看失败原因并修复\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"项目构建未成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建未成功\"},\"template\":\"orange\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"warning_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
// sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建未成功,请及时查看失败原因并修复\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"项目构建未成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建未成功\"},\"template\":\"orange\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"warning_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
}
|
// }
|
||||||
cleanup {
|
cleanup {
|
||||||
sh "echo '清理无用空镜像'"
|
sh '''
|
||||||
sh "docker image prune -f"
|
echo "开始清理已停止容器......"
|
||||||
sh "var=\$(docker ps -a -q --filter \"status=exited\");if [ -n \"\$var\" ];then docker rm \$var; fi"
|
# 1. 清理已停止的容器
|
||||||
cleanWs()
|
# -r 参数防止 xargs 在输入为空时报错
|
||||||
|
docker ps -a -q --filter "status=exited" | xargs -r docker rm
|
||||||
|
echo "清理已停止容器完成......"
|
||||||
|
|
||||||
|
echo "清理无用空镜像......"
|
||||||
|
# 2. 清理无用的空镜像 (dangling images)
|
||||||
|
docker image prune -f
|
||||||
|
echo "无用空镜像清理完成"
|
||||||
|
'''
|
||||||
|
deleteDir()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
components.d.ts
vendored
116
components.d.ts
vendored
@@ -5,21 +5,29 @@
|
|||||||
// ------
|
// ------
|
||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import { GlobalComponents } from 'vue'
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
|
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
||||||
|
CardItem: typeof import('./src/components/cardManager/cardItem.vue')['default']
|
||||||
|
CardManager: typeof import('./src/components/CardManager/index.vue')['default']
|
||||||
|
CardModule: typeof import('./src/components/cardModule/index.vue')['default']
|
||||||
|
CollapseHeader: typeof import('./src/components/collapseHeader/index.vue')['default']
|
||||||
Comment: typeof import('./src/components/comment/index.vue')['default']
|
Comment: typeof import('./src/components/comment/index.vue')['default']
|
||||||
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||||
|
DynamicSvgIcon: typeof import('./src/components/dynamicSvgIcon/index.vue')['default']
|
||||||
ElAside: typeof import('element-plus/es')['ElAside']
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
|
||||||
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
||||||
@@ -32,6 +40,7 @@ declare module 'vue' {
|
|||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
@@ -40,12 +49,14 @@ declare module 'vue' {
|
|||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMention: typeof import('element-plus/es')['ElMention']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
@@ -53,8 +64,10 @@ declare module 'vue' {
|
|||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTableV2: typeof import('element-plus/es')['ElTableV2']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
@@ -65,18 +78,119 @@ declare module 'vue' {
|
|||||||
EmojiPicker: typeof import('./src/components/comment/emojiPicker.vue')['default']
|
EmojiPicker: typeof import('./src/components/comment/emojiPicker.vue')['default']
|
||||||
GlobaIcon: typeof import('./src/components/globaIcon/index.vue')['default']
|
GlobaIcon: typeof import('./src/components/globaIcon/index.vue')['default']
|
||||||
GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default']
|
GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default']
|
||||||
|
MemberSelector: typeof import('./src/components/memberSelector/index.vue')['default']
|
||||||
NameAvatar: typeof import('./src/components/nameAvatar/index.vue')['default']
|
NameAvatar: typeof import('./src/components/nameAvatar/index.vue')['default']
|
||||||
|
NodeFlow: typeof import('./src/components/nodeFlow/index.vue')['default']
|
||||||
OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
|
OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
|
||||||
PageForm: typeof import('./src/components/pageForm/index.vue')['default']
|
PageForm: typeof import('./src/components/pageForm/index.vue')['default']
|
||||||
|
PopoverMenu: typeof import('./src/components/popoverMenu/index.vue')['default']
|
||||||
ProTable: typeof import('./src/components/proTable/index.vue')['default']
|
ProTable: typeof import('./src/components/proTable/index.vue')['default']
|
||||||
|
ProTablev2: typeof import('./src/components/proTable/proTablev2.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SopNode: typeof import('./src/components/nodeFlow/sopNode.vue')['default']
|
||||||
StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
|
StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
|
||||||
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
||||||
StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
||||||
|
UserSelector: typeof import('./src/components/userSelector/index.vue')['default']
|
||||||
|
Viewswitcher: typeof import('./src/components/viewswitcher/index.vue')['default']
|
||||||
|
ViewSwitcher: typeof import('./src/components/viewSwitcher/index.vue')['default']
|
||||||
|
VirtualCardList: typeof import('./src/components/cardManager/VirtualCardList.vue')['default']
|
||||||
Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
||||||
|
Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
|
vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For TSX support
|
||||||
|
declare global {
|
||||||
|
const AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
||||||
|
const CardItem: typeof import('./src/components/cardManager/cardItem.vue')['default']
|
||||||
|
const CardManager: typeof import('./src/components/CardManager/index.vue')['default']
|
||||||
|
const CardModule: typeof import('./src/components/cardModule/index.vue')['default']
|
||||||
|
const CollapseHeader: typeof import('./src/components/collapseHeader/index.vue')['default']
|
||||||
|
const Comment: typeof import('./src/components/comment/index.vue')['default']
|
||||||
|
const CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||||
|
const DynamicSvgIcon: typeof import('./src/components/dynamicSvgIcon/index.vue')['default']
|
||||||
|
const ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
const ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
|
const ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
|
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
const ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
const ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
|
||||||
|
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
|
const ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
const ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
const ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
||||||
|
const ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
const ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||||
|
const ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||||
|
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
const ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
const ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
const ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
|
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
const ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
|
const ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
const ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
|
const ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
const ElMention: typeof import('element-plus/es')['ElMention']
|
||||||
|
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
const ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
const ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
|
const ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
|
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
const ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
|
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
const ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
|
const ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||||
|
const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
|
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
|
const ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
const ElTableV2: typeof import('element-plus/es')['ElTableV2']
|
||||||
|
const ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
const ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
|
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
const ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||||
|
const ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||||
|
const ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
const ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
|
const EmojiPicker: typeof import('./src/components/comment/emojiPicker.vue')['default']
|
||||||
|
const GlobaIcon: typeof import('./src/components/globaIcon/index.vue')['default']
|
||||||
|
const GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default']
|
||||||
|
const MemberSelector: typeof import('./src/components/memberSelector/index.vue')['default']
|
||||||
|
const NameAvatar: typeof import('./src/components/nameAvatar/index.vue')['default']
|
||||||
|
const NodeFlow: typeof import('./src/components/nodeFlow/index.vue')['default']
|
||||||
|
const OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
|
||||||
|
const PageForm: typeof import('./src/components/pageForm/index.vue')['default']
|
||||||
|
const PopoverMenu: typeof import('./src/components/popoverMenu/index.vue')['default']
|
||||||
|
const ProTable: typeof import('./src/components/proTable/index.vue')['default']
|
||||||
|
const ProTablev2: typeof import('./src/components/proTable/proTablev2.vue')['default']
|
||||||
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
const SopNode: typeof import('./src/components/nodeFlow/sopNode.vue')['default']
|
||||||
|
const StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
|
||||||
|
const StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
||||||
|
const StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
||||||
|
const UserSelector: typeof import('./src/components/userSelector/index.vue')['default']
|
||||||
|
const Viewswitcher: typeof import('./src/components/viewswitcher/index.vue')['default']
|
||||||
|
const ViewSwitcher: typeof import('./src/components/viewSwitcher/index.vue')['default']
|
||||||
|
const VirtualCardList: typeof import('./src/components/cardManager/VirtualCardList.vue')['default']
|
||||||
|
const Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
||||||
|
const Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>智视界</title>
|
<title>智服链</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"test": "vitest",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@micro-zoe/micro-app": "^1.0.0-rc.28",
|
"@micro-zoe/micro-app": "^1.0.0-rc.28",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
||||||
|
"@vue-flow/core": "^1.48.1",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
"element-plus": "^2.13.0",
|
"element-plus": "^2.13.0",
|
||||||
|
"happy-dom": "^20.1.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"vitest": "^4.0.17",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-i18n": "^11.2.7",
|
"vue-i18n": "^11.2.7",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
@@ -23,6 +29,7 @@
|
|||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
|
"lodash-es": "^4.17.22",
|
||||||
"sass": "^1.97.1",
|
"sass": "^1.97.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"unicode-emoji-json": "^0.8.0",
|
"unicode-emoji-json": "^0.8.0",
|
||||||
|
|||||||
1207
pnpm-lock.yaml
generated
1207
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,11 @@ export const getRouteMenus = () => {
|
|||||||
return request.get('/auth/v1/backend/menu');
|
return request.get('/auth/v1/backend/menu');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
export const getUserInfo = () => {
|
||||||
|
return request.get('/auth/v1/my/info');
|
||||||
|
};
|
||||||
|
|
||||||
// 登录接口
|
// 登录接口
|
||||||
export const login = (data: { username: string; password: string }) => {
|
export const login = (data: { username: string; password: string }) => {
|
||||||
return request.post('/auth/oauth2/token', data,{
|
return request.post('/auth/oauth2/token', data,{
|
||||||
@@ -13,3 +18,22 @@ export const login = (data: { username: string; password: string }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取字典映射
|
||||||
|
export const getDictMap = (ids:string) => {
|
||||||
|
return request.get(`/auth/v1/dict/type/${ids}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取二级菜单数据
|
||||||
|
export const getDictMapLevel = (key:string,parentId:string) => {
|
||||||
|
return request.get(`/auth/v1/dict/type/${key}/${parentId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**员工公用接口*/
|
||||||
|
|
||||||
|
|
||||||
|
// 根据员工关键字查询员工
|
||||||
|
export const getEmployeeList = (params: any) => {
|
||||||
|
return request.get('/auth/v1/employee', params);
|
||||||
|
};
|
||||||
41
src/api/modules/Comment/index.ts
Normal file
41
src/api/modules/Comment/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import request from '@/request';
|
||||||
|
|
||||||
|
|
||||||
|
type commentProps = {
|
||||||
|
moduleId:string;
|
||||||
|
nodeId:string;
|
||||||
|
instanceId:string;
|
||||||
|
commentId:string;
|
||||||
|
pageNo:number;
|
||||||
|
pageSize:number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface addCommentProps {
|
||||||
|
moduleId:string;
|
||||||
|
nodeId:string;
|
||||||
|
instanceId:string;
|
||||||
|
content:string;
|
||||||
|
mentions:Record<string,any>[]
|
||||||
|
[key:string]:any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取评论
|
||||||
|
export const getComment = (data: commentProps) => {
|
||||||
|
return request.post('/communicate/v1/comment/list', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加评论-回复评论
|
||||||
|
export const addReplyComment = (data: addCommentProps) => {
|
||||||
|
return request.post('/communicate/v1/comment/add', data)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除评论
|
||||||
|
export const deleteComment = (id: string) => {
|
||||||
|
return request.delete(`/communicate/v1/comment/del/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据员工关键字查询员工信息
|
||||||
|
export const getEmployeeByKeyword = (params: Record<string, any>) => {
|
||||||
|
return request.get(`/auth/v1/employee`,params);
|
||||||
|
}
|
||||||
69
src/api/stage/organization/index.ts
Normal file
69
src/api/stage/organization/index.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import request from '@/request';
|
||||||
|
|
||||||
|
type paramsProps = {
|
||||||
|
name:string;
|
||||||
|
domain:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type wxWorkProps = {
|
||||||
|
corpId:string;
|
||||||
|
agentId:number;
|
||||||
|
secret:string;
|
||||||
|
token:string;
|
||||||
|
encodingAesKey:string;
|
||||||
|
}
|
||||||
|
interface addDataProps{
|
||||||
|
name:string;
|
||||||
|
wxWork:wxWorkProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface defaultProps{
|
||||||
|
keyword?:string;
|
||||||
|
pageNo:number;
|
||||||
|
pageSize:number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 查询企业
|
||||||
|
export const getEnterprise = (params: paramsProps) =>{
|
||||||
|
return request.get('/auth/v1/backend/enterprise', params);
|
||||||
|
}
|
||||||
|
// 添加企业
|
||||||
|
export const addEnterprise = (data: addDataProps) => {
|
||||||
|
return request.post('/auth/v1/backend/enterprise', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用企业
|
||||||
|
export const enableEnterprise = (id:string) => {
|
||||||
|
return request.post(`/auth/v1/backend/enterprise/${id}/enable`);
|
||||||
|
}
|
||||||
|
// 禁用企业
|
||||||
|
export const disableEnterprise = (id:string) => {
|
||||||
|
return request.post(`/auth/v1/backend/enterprise/${id}/disable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载企业机构
|
||||||
|
export const getEnterpriseOrg = (parentId:number) => {
|
||||||
|
return request.get(`/auth/v1/backend/enterprise/institution/${parentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载员工详情
|
||||||
|
export const getEnterpriseUser = (employeeId:number) => {
|
||||||
|
return request.get(`/auth/v1/backend/enterprise/employee/${employeeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业详情
|
||||||
|
export const getEnterpriseDetail = () => {
|
||||||
|
return request.get(`/auth/v1/backend/enterprise/detail`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载部门详情
|
||||||
|
export const getEnterpriseOrgDetail = (departmentId:string) => {
|
||||||
|
return request.get(`/auth/v1/backend/enterprise/department/${departmentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取企业信息职位
|
||||||
|
export const getEnterprisePosition = (params:defaultProps) => {
|
||||||
|
return request.get(`/auth/v1/backend/enterprise/position`,params);
|
||||||
|
}
|
||||||
64
src/api/stage/permission/index.ts
Normal file
64
src/api/stage/permission/index.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import request from '@/request';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 查询角色列表
|
||||||
|
export const getRoleList = (params: any) => {
|
||||||
|
return request.get('/auth/v1/backend/role', params);
|
||||||
|
}
|
||||||
|
// 获取角色详情
|
||||||
|
export const getRoleDetail = (id: string) => {
|
||||||
|
return request.get(`/auth/v1/backend/role/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新角色
|
||||||
|
export const updateRole = (data: any) => {
|
||||||
|
return request.put('/auth/v1/backend/role', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增角色
|
||||||
|
export const addRole = (data: any) => {
|
||||||
|
return request.post('/auth/v1/backend/role', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除角色
|
||||||
|
export const deleteRole = (id: string) => {
|
||||||
|
return request.delete(`/auth/v1/backend/role/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用角色
|
||||||
|
export const enableRole = (id: string) => {
|
||||||
|
return request.put(`/auth/v1/backend/role/${id}/enable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用角色
|
||||||
|
export const disableRole = (id: string) => {
|
||||||
|
return request.put(`/auth/v1/backend/role/${id}/disable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制权限
|
||||||
|
export const copyRolePermission = (roleId: string) => {
|
||||||
|
return request.post(`/auth/v1/backend/role/${roleId}/copy`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存角色
|
||||||
|
export const batchSaveRole = (roleId: string,data: number[]) => {
|
||||||
|
return request.post(`auth/v1/backend/role/${roleId}/members`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**------------------------角色权限相关---------------------------**/
|
||||||
|
|
||||||
|
// 保存角色权限
|
||||||
|
export const saveRolePermission = (data: any) => {
|
||||||
|
return request.post(`/auth/v1/backend/role/permissions/save`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询角色权限 (获取角色权限详情)
|
||||||
|
export const getRolePermission = (roleId: string) => {
|
||||||
|
return request.get(`/auth/v1/backend/role/${roleId}/permissions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色成员列表
|
||||||
|
export const getRoleMemberList = (roleId: string) => {
|
||||||
|
return request.get(`/auth/v1/backend/role/${roleId}/members`);
|
||||||
|
}
|
||||||
2
src/auto-imports.d.ts
vendored
2
src/auto-imports.d.ts
vendored
@@ -7,8 +7,10 @@
|
|||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue').EffectScope
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
|
const ElButton: typeof import('element-plus/es').ElButton
|
||||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||||
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
|
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
|
||||||
|
const ElTag: typeof import('element-plus/es').ElTag
|
||||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||||
const computed: typeof import('vue').computed
|
const computed: typeof import('vue').computed
|
||||||
const createApp: typeof import('vue').createApp
|
const createApp: typeof import('vue').createApp
|
||||||
|
|||||||
39
src/components/autoTooltip/index.vue
Normal file
39
src/components/autoTooltip/index.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<el-tooltip
|
||||||
|
:content="content"
|
||||||
|
:disabled="!isOverflow"
|
||||||
|
placement="top"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ellipsis-content"
|
||||||
|
@mouseenter="checkOverflow"
|
||||||
|
>
|
||||||
|
<slot>{{ content }}</slot>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
defineOptions({ name: 'AutoEllipsis' })
|
||||||
|
defineProps({
|
||||||
|
content: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOverflow = ref(false);
|
||||||
|
|
||||||
|
const checkOverflow = (event) => {
|
||||||
|
const el = event.currentTarget;
|
||||||
|
isOverflow.value = el.scrollWidth > el.clientWidth;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ellipsis-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="mj-card-container"
|
|
||||||
:style="containerStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mj-card-standard"
|
|
||||||
v-for="(card,carIndex) in list"
|
|
||||||
:key="carIndex"
|
|
||||||
@click="cardItemClick(card, carIndex)"
|
|
||||||
>
|
|
||||||
<slot name="cardTip">
|
|
||||||
<div class="mj-card-standard-tip" :style="standardTopStyle"></div>
|
|
||||||
</slot>
|
|
||||||
<div class="mj-card-standard-content">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'CardItem' });
|
|
||||||
const { list = [], maxColumns = 4, standardTopStyle } = defineProps<{
|
|
||||||
list: any[]
|
|
||||||
maxColumns?: number // 每行最大展示数量,默认 4
|
|
||||||
standardTopStyle?: string | Record<string, any> // 外部传入的顶部样式
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 向外抛出的事件
|
|
||||||
const emits = defineEmits<{
|
|
||||||
/**
|
|
||||||
* 卡片点击事件
|
|
||||||
* @param e 事件名
|
|
||||||
* @param payload 点击的卡片数据和索引
|
|
||||||
*/
|
|
||||||
(e: 'card-click', payload: { item: any; index: number }): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 根据屏幕宽度和最大列数计算实际列数
|
|
||||||
const screenWidth = ref(window.innerWidth)
|
|
||||||
|
|
||||||
const containerStyle = computed(() => {
|
|
||||||
let cols = maxColumns
|
|
||||||
|
|
||||||
// 响应式调整
|
|
||||||
if (screenWidth.value <= 640) {
|
|
||||||
cols = 1
|
|
||||||
} else if (screenWidth.value <= 900) {
|
|
||||||
cols = Math.min(maxColumns, 2)
|
|
||||||
} else if (screenWidth.value <= 1200) {
|
|
||||||
cols = Math.min(maxColumns, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 卡片点击事件,向外抛出当前卡片数据和索引
|
|
||||||
const cardItemClick = (item: any, index: number) => {
|
|
||||||
emits('card-click', { item, index })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
const handleResize = () => {
|
|
||||||
screenWidth.value = window.innerWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mj-card-container {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.mj-card-standard {
|
|
||||||
--radius: 16px;
|
|
||||||
--padding-standard: 20px;
|
|
||||||
--border-color: #e5e7eb;
|
|
||||||
--background-color: #fff;
|
|
||||||
--tw-shadow: 0 8px 12px #0000001a, 0 0px 12px #0000001a;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
min-height: 140px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
transition: box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--tw-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mj-card-standard-tip {
|
|
||||||
--height: 8px;
|
|
||||||
height: var(--height);
|
|
||||||
border-top-left-radius: inherit;
|
|
||||||
border-top-right-radius: inherit;
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mj-card-standard-content {
|
|
||||||
padding: var(--padding-standard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 平板:调整间距
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.mj-card-container {
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动端:调整间距
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mj-card-container {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
75
src/components/cardManager/cardItem.vue
Normal file
75
src/components/cardManager/cardItem.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mj-grid-container">
|
||||||
|
<div class="mj-card-item" @click="$emit('card-click', item)">
|
||||||
|
<slot name="card-prefix" :item="item">
|
||||||
|
</slot>
|
||||||
|
<div class="mj-card-standard-content">
|
||||||
|
<slot name="content" :item="item"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.actions" class="mj-card-actions">
|
||||||
|
<slot name="actions" :item="item"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "CardItem" });
|
||||||
|
|
||||||
|
// 接收单条数据 item 而不是 list
|
||||||
|
interface Props {
|
||||||
|
item: any;
|
||||||
|
standardTopStyle?: string | Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: "card-click", item: any): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mj-grid-container {
|
||||||
|
.mj-card-item {
|
||||||
|
--radius: 12px;
|
||||||
|
--primary-color: #409eff;
|
||||||
|
--border-color: #e4e7ed;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
// 丝滑动画过渡
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 12px 24px rgba(64, 158, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-card-standard-tip {
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-card-standard-content {
|
||||||
|
padding: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-card-actions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封面图内部图片的平滑过渡
|
||||||
|
:deep(img) {
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
165
src/components/cardManager/index.vue
Normal file
165
src/components/cardManager/index.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card-manager-viewport" ref="viewportRef" @scroll.passive="handleScroll">
|
||||||
|
<div :style="{ height: `${paddingTop}px` }"></div>
|
||||||
|
|
||||||
|
<div class="mj-card-container">
|
||||||
|
<template v-for="(item, index) in visibleList" :key="item.id || index">
|
||||||
|
<slot :item="item" :openMenu="openMenu"></slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :style="{ height: `${paddingBottom}px` }"></div>
|
||||||
|
|
||||||
|
<div ref="loadMoreRef" class="load-more-trigger">
|
||||||
|
<el-icon v-if="loading" class="is-loading" :size="26"><Loading /></el-icon>
|
||||||
|
<span v-else-if="finished && list.length > 0">没有更多数据了</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionMenu
|
||||||
|
:visible="menuVisible"
|
||||||
|
:style="menuStyle"
|
||||||
|
:data="activeData"
|
||||||
|
:bodyStyle="bodyStyle"
|
||||||
|
@close="closeMenu"
|
||||||
|
@action="(type, data) => $emit('on-action', type, data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends { id: string | number }">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { Loading } from '@element-plus/icons-vue';
|
||||||
|
import { useUniversalPopover } from "@/hooks/useActionMenu";
|
||||||
|
import ActionMenu from "@/components/popoverMenu/index.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fetchApi: (params: any) => Promise<any>; // 外部传入请求方法
|
||||||
|
pageSize?: number; // 分页大小
|
||||||
|
itemHeight: number; // 必填:单行高度(含gap)
|
||||||
|
columnCount: number; // 必填:当前列数
|
||||||
|
extraParams?: Record<string, any>; // 外部搜索参数
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 菜单额外的样式
|
||||||
|
const bodyStyle = {
|
||||||
|
backgroundColor:'#fff',
|
||||||
|
boxShadow: '0 0px 10px rgba(0, 0, 0, 0.05)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border:'1px solid #EEF1F6'
|
||||||
|
|
||||||
|
}
|
||||||
|
const emit = defineEmits(['on-action']);
|
||||||
|
|
||||||
|
// --- 状态管理 ---
|
||||||
|
const list = ref<T[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const finished = ref(false);
|
||||||
|
const viewportRef = ref<HTMLElement | null>(null);
|
||||||
|
const scrollTop = ref(0);
|
||||||
|
let pageNo = 1;
|
||||||
|
|
||||||
|
// --- 弹出菜单 Hook ---
|
||||||
|
const { visible: menuVisible, activeData, menuStyle, openMenu, closeMenu } = useUniversalPopover<T>();
|
||||||
|
|
||||||
|
// --- 1. 请求数据逻辑 ---
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (loading.value || finished.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
pageNo,
|
||||||
|
pageSize: props.pageSize || 20,
|
||||||
|
...props.extraParams
|
||||||
|
};
|
||||||
|
const res = await props.fetchApi(params);
|
||||||
|
const records = res.records || [];
|
||||||
|
|
||||||
|
if (records.length === 0) {
|
||||||
|
finished.value = true;
|
||||||
|
} else {
|
||||||
|
list.value.push(...records);
|
||||||
|
if (records.length < (props.pageSize || 20)) finished.value = true;
|
||||||
|
pageNo++;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 2. 虚拟滚动计算 (裁剪可见区域) ---
|
||||||
|
const visibleList = computed(() => {
|
||||||
|
const rowHeight = props.itemHeight;
|
||||||
|
if (rowHeight <= 0) return list.value;
|
||||||
|
const startRow = Math.floor(scrollTop.value / rowHeight);
|
||||||
|
const viewportHeight = viewportRef.value?.clientHeight || 800;
|
||||||
|
const visibleRows = Math.ceil(viewportHeight / rowHeight);
|
||||||
|
|
||||||
|
// 缓冲区设为 2 行,保证滑动顺畅
|
||||||
|
const start = Math.max(0, (startRow - 2) * props.columnCount);
|
||||||
|
const end = (startRow + visibleRows + 2) * props.columnCount;
|
||||||
|
|
||||||
|
return list.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
const paddingTop = computed(() => {
|
||||||
|
const rowHeight = props.itemHeight;
|
||||||
|
const startRow = Math.floor(scrollTop.value / rowHeight);
|
||||||
|
return Math.max(0, startRow - 2) * rowHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
const paddingBottom = computed(() => {
|
||||||
|
const rowHeight = props.itemHeight;
|
||||||
|
const totalRows = Math.ceil(list.value.length / props.columnCount);
|
||||||
|
const startRow = Math.floor(scrollTop.value / rowHeight);
|
||||||
|
const viewportHeight = viewportRef.value?.clientHeight || 800;
|
||||||
|
const visibleRows = Math.ceil(viewportHeight / rowHeight);
|
||||||
|
const renderedRows = startRow + visibleRows + 2;
|
||||||
|
|
||||||
|
return Math.max(0, (totalRows - renderedRows) * rowHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (viewportRef.value) scrollTop.value = viewportRef.value.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 3. 无缝加载监听 (IntersectionObserver) ---
|
||||||
|
const loadMoreRef = ref(null);
|
||||||
|
let observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData(); // 初始化加载
|
||||||
|
observer = new IntersectionObserver(([entry]) => {
|
||||||
|
if (entry.isIntersecting) fetchData();
|
||||||
|
}, { root: viewportRef.value, threshold: 0.1 });
|
||||||
|
if (loadMoreRef.value) observer.observe(loadMoreRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 4. 监听外部参数变化 (搜索重置) ---
|
||||||
|
watch(() => props.extraParams, () => {
|
||||||
|
list.value = [];
|
||||||
|
pageNo = 1;
|
||||||
|
finished.value = false;
|
||||||
|
scrollTop.value = 0;
|
||||||
|
viewportRef.value?.scrollTo(0, 0);
|
||||||
|
fetchData();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
onUnmounted(() => observer?.disconnect());
|
||||||
|
|
||||||
|
// 暴露给外部用于增删改的接口
|
||||||
|
defineExpose({ list });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-manager-viewport {
|
||||||
|
height: 100%; // 外部容器需给高度
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.load-more-trigger {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
src/components/collapseHeader/index.vue
Normal file
60
src/components/collapseHeader/index.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mj-collapse-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="title">{{ title }}</div>
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<slot name="headerRight"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 全局公用collapse表头组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$primary-blue: #0052d9;
|
||||||
|
$text-main: #1d2129;
|
||||||
|
.mj-collapse-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-main;
|
||||||
|
|
||||||
|
// 使用伪类实现蓝色装饰条
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
background-color: $primary-blue;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,77 +1,112 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mj-filter-group">
|
<div class="mj-filter-group">
|
||||||
<div :class="className ? className : 'mj-icon-container'">
|
<div :class="className ? className : 'mj-icon-container'">
|
||||||
<el-popover
|
<div class="mj-icon-warp" @click.stop="openMenu($event, null)">
|
||||||
ref="filterPopover"
|
|
||||||
trigger="click"
|
|
||||||
popper-class="filter-popper"
|
|
||||||
placement="bottom-end"
|
|
||||||
:teleported="true"
|
|
||||||
width="auto"
|
|
||||||
@hide="$emit('on-hide')"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<div class="mj-icon-warp">
|
|
||||||
<div class="mj-icon-item" title="筛选">
|
<div class="mj-icon-item" title="筛选">
|
||||||
<el-icon><Filter /></el-icon>
|
<el-icon><Filter /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<slot name="filterLabel"></slot>
|
<slot name="filterLabel"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<div class="filter-container" @click.stop>
|
<div
|
||||||
<slot></slot>
|
class="mj-icon-item"
|
||||||
</div>
|
title="下载"
|
||||||
</el-popover>
|
@click="onDownload"
|
||||||
<div class="mj-icon-item" title="下载" @click="$emit('download')" v-if="download">
|
v-if="download"
|
||||||
|
>
|
||||||
<el-icon><Download /></el-icon>
|
<el-icon><Download /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ActionMenu
|
||||||
|
:visible="visible"
|
||||||
|
:style="menuStyle"
|
||||||
|
:data="null"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="filter-container" @click.stop>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ActionMenu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { Filter, Download } from "@element-plus/icons-vue";
|
import { ref, watch } from 'vue';
|
||||||
const filterPopover = ref(null);
|
import { Filter, Download } from '@element-plus/icons-vue';
|
||||||
|
import ActionMenu from "@/components/popoverMenu/index.vue";
|
||||||
|
import { useUniversalPopover } from "@/hooks/useActionMenu";
|
||||||
|
|
||||||
// 定义事件:重置、应用、下载
|
const emit = defineEmits(["download", "on-hide"]);
|
||||||
defineEmits(["download",'on-hide']);
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
defineProps({
|
download: {
|
||||||
download:{
|
type: Boolean,
|
||||||
type:Boolean,
|
default: true,
|
||||||
default:true
|
|
||||||
},
|
},
|
||||||
className:{
|
className: {
|
||||||
type:String,
|
type: String,
|
||||||
default:''
|
default: "",
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
close() {
|
|
||||||
filterPopover.value?.hide()
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 使用 Hook,配置为右对齐
|
||||||
|
const { visible, menuStyle, openMenu, closeMenu } = useUniversalPopover({
|
||||||
|
placement: 'bottom-end',
|
||||||
|
offsetY: 10,
|
||||||
|
offsetX: 37
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听隐藏事件,模拟原 el-popover 的 @hide
|
||||||
|
watch(visible, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
emit('on-hide');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露关闭方法
|
||||||
|
defineExpose({
|
||||||
|
close: closeMenu,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDownload = () => {
|
||||||
|
// @ts-ignore (确保你项目中已按需引入 ElMessage)
|
||||||
|
ElMessage.warning("功能开发中...");
|
||||||
|
emit("download");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
// 保留你原本的所有样式逻辑
|
||||||
.mj-icon-container {
|
.mj-icon-container {
|
||||||
|
--radius: 4px;
|
||||||
|
--bg-color: #fff;
|
||||||
|
--border-color: transparent;
|
||||||
|
--shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
--hover-color: #f5f7fa;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: var(--radius);
|
||||||
background-color: #fff;
|
background-color: var(--bg-color);
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow);
|
||||||
.mj-icon-warp{
|
border: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.mj-icon-warp {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 3px;
|
padding-right: 3px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color:#45556C;
|
color: #45556c;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-icon-item {
|
.mj-icon-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -84,21 +119,22 @@ defineExpose({
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
background-color: #f5f7fa;
|
&:active {
|
||||||
color: #409eff;
|
background-color: var(--hover-color);
|
||||||
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-icon-level-container{
|
.mj-icon-level-container {
|
||||||
@extend .mj-icon-container;
|
@extend .mj-icon-container;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 1px solid #E2E8F0;
|
border: 1px solid #e2e8f0;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
|
|
||||||
.mj-icon-item{
|
.mj-icon-item {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|||||||
71
src/components/dynamicSvgIcon/index.vue
Normal file
71
src/components/dynamicSvgIcon/index.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<i class="svg-icon" :style="iconStyle" v-html="processedContent" v-if="content"></i>
|
||||||
|
<i v-else class="svg-icon" :style="iconStyle">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
>
|
||||||
|
<path v-for="(d, index) in paths" :key="index" :d="d" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 预设图标名称
|
||||||
|
name: { type: String, default: '' },
|
||||||
|
// 外部传递的完整 SVG 字符串
|
||||||
|
content: { type: String, default: '' },
|
||||||
|
size: { type: [Number, String], default: 16 },
|
||||||
|
color: { type: String, default: '' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预设路径数据
|
||||||
|
const iconData: Record<string, string[]> = {
|
||||||
|
building: [
|
||||||
|
"M832 128H192a32 32 0 0 0-32 32v704a32 32 0 0 0 32 32h640a32 32 0 0 0 32-32V160a32 32 0 0 0-32-32zm-64 704H256V192h512v640z",
|
||||||
|
"M320 256h384v64H320zm0 160h384v64H320zm0 160h384v64H320zM448 704h128v128H448z"
|
||||||
|
],
|
||||||
|
flag: [
|
||||||
|
"M288 128a32 32 0 0 0-32 32v704a32 32 0 0 0 64 0V544h448l-64-160 64-160H320V160a32 32 0 0 0-32-32zM320 288h352l-32 80 32 80H320V288z"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理外部传入的 SVG 字符串,确保它能继承颜色
|
||||||
|
const processedContent = computed(() => {
|
||||||
|
if (!props.content) return '';
|
||||||
|
// 简单处理:确保外部 SVG 内部使用 currentColor
|
||||||
|
return props.content.replace(/fill="[^"]*"/g, 'fill="currentColor"');
|
||||||
|
});
|
||||||
|
|
||||||
|
const paths = computed(() => iconData[props.name] || []);
|
||||||
|
|
||||||
|
const iconStyle = computed(() => ({
|
||||||
|
fontSize: typeof props.size === 'number' ? `${props.size}px` : props.size,
|
||||||
|
color: props.color || 'inherit',
|
||||||
|
width: '1em',
|
||||||
|
height: '1em',
|
||||||
|
display: 'inline-flex'
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.svg-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: color 0.2s, fill 0.2s; // 添加平滑过渡
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
fill: currentColor; // 默认使用父级 color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
344
src/components/memberSelector/index.vue
Normal file
344
src/components/memberSelector/index.vue
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
size="440px"
|
||||||
|
class="standard-ui-back-drawer"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
destroy-on-close
|
||||||
|
:with-header="false"
|
||||||
|
modal-class="standard-overlay-dialog-flat"
|
||||||
|
>
|
||||||
|
<div class="customer-drawer-header">
|
||||||
|
<div class="title-row">
|
||||||
|
<span class="decorator"></span>
|
||||||
|
<span class="title">成员管理</span>
|
||||||
|
<el-icon class="close-icon" @click="drawerVisible = false"
|
||||||
|
><Close
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
<p class="sub-title">角色:<span>超级管理员</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-body-container">
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-select
|
||||||
|
v-model="memberSearchQuery"
|
||||||
|
placeholder="按姓名搜索成员"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="remoteMethod"
|
||||||
|
:loading="remoteLoading"
|
||||||
|
collapse-tags
|
||||||
|
:teleported="false"
|
||||||
|
:fit-input-width="true"
|
||||||
|
value-key="id"
|
||||||
|
v-select-more="handleLoadMore"
|
||||||
|
@change="changeMember"
|
||||||
|
popper-class="standard-select-dropdown-height"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in selectOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item"
|
||||||
|
:label="item.name"
|
||||||
|
>
|
||||||
|
<div class="select-item">
|
||||||
|
<name-avatar
|
||||||
|
:size="26"
|
||||||
|
:name="item.name"
|
||||||
|
:src="item.avatar"
|
||||||
|
:bgColor="'var(--mj-avatar-bg)'"
|
||||||
|
:avatarTextColor="'var(--mj-text-color)'"
|
||||||
|
/>
|
||||||
|
<div class="select-item-name">
|
||||||
|
<AutoTooltip :content="item.name"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
<el-option
|
||||||
|
v-if="loadMore"
|
||||||
|
disabled
|
||||||
|
class="mj-select-dropdown-loading"
|
||||||
|
value="loading"
|
||||||
|
>
|
||||||
|
<div class="status-container">
|
||||||
|
<el-icon v-if="loadMore" class="is-loading" :size="20"
|
||||||
|
><Loading
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar
|
||||||
|
height="calc(100vh - 220px)"
|
||||||
|
@end-reached="loadMoreList"
|
||||||
|
ref="listRef"
|
||||||
|
>
|
||||||
|
<div class="member-list">
|
||||||
|
<transition-group name="list-fade">
|
||||||
|
<div v-for="item in displayMemberList" :key="item.id" class="member-item">
|
||||||
|
<name-avatar
|
||||||
|
:size="30"
|
||||||
|
:name="item.name"
|
||||||
|
:src="item.avatar"
|
||||||
|
:bgColor="'var(--mj-avatar-bg)'"
|
||||||
|
:avatarTextColor="'var(--mj-text-color)'"
|
||||||
|
/>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="name">{{ item.name }}</div>
|
||||||
|
<div class="dept">
|
||||||
|
{{
|
||||||
|
(item.departments || []).map((dept) => dept.name).join(",")
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-action">
|
||||||
|
<el-button link type="info" @click.stop="handleRemove(item.id)"
|
||||||
|
>移除</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="custom-flat-drawer-footer">
|
||||||
|
<div class="stats-info">
|
||||||
|
<span class="label">统计信息</span>
|
||||||
|
<span class="count">共 {{ memberList.length }} 名成员</span>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button link @click="cancelMember">取消</el-button>
|
||||||
|
<el-button type="primary" class="btn-confirm" @click="saveMember"
|
||||||
|
>确认保存变更</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
import NameAvatar from "@/components/NameAvatar/index.vue";
|
||||||
|
import { getEmployeeList } from "@/api";
|
||||||
|
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
|
||||||
|
import { useLocalManager } from "@/hooks/useLocalManager";
|
||||||
|
import AutoTooltip from "@/components/autoTooltip/index.vue";
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
remoteLoading,
|
||||||
|
loadMore,
|
||||||
|
noMore,
|
||||||
|
remoteMethod,
|
||||||
|
handleLoadMore,
|
||||||
|
} = useSelectLoadMore({
|
||||||
|
fetchApi: (p) => getEmployeeList({ ...p, kind: 1 }),
|
||||||
|
});
|
||||||
|
defineOptions({ name: "MemberSelector" });
|
||||||
|
|
||||||
|
// emit数据
|
||||||
|
const emit = defineEmits(["save-member"]);
|
||||||
|
const listRef = ref(null);
|
||||||
|
const drawerVisible = defineModel("visible", { type: Boolean });
|
||||||
|
// 搜索部门的数据
|
||||||
|
const memberSearchQuery = ref([]);
|
||||||
|
|
||||||
|
const memberList = defineModel<any[]>("dataList", { default: () => [] });
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayData: displayMemberList,
|
||||||
|
loadMore: handleLoadMoreList,
|
||||||
|
noMore: noMoreLocal
|
||||||
|
} = useLocalManager(() => memberList.value, 20);
|
||||||
|
|
||||||
|
// 动态返回options数据
|
||||||
|
const selectOptions = computed(() => {
|
||||||
|
const remoteData = options.value || [];
|
||||||
|
const selectedData = memberList.value || [];
|
||||||
|
|
||||||
|
// 创建一个 Map 用于去重,以 id 为 key
|
||||||
|
const optionMap = new Map();
|
||||||
|
|
||||||
|
// 1. 先把远程搜索结果放进去
|
||||||
|
remoteData.forEach(item => optionMap.set(item.id, item));
|
||||||
|
|
||||||
|
// 2. 再把已选择的数据放进去
|
||||||
|
selectedData.forEach(item => {
|
||||||
|
if (!optionMap.has(item.id)) {
|
||||||
|
optionMap.set(item.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(optionMap.values());
|
||||||
|
});
|
||||||
|
// 监听数据变化
|
||||||
|
watch(
|
||||||
|
() => memberList.value,
|
||||||
|
(newVal) => {
|
||||||
|
memberSearchQuery.value = [...newVal];
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加成员 (添加成员)
|
||||||
|
const changeMember = (value) => {
|
||||||
|
memberList.value = value;
|
||||||
|
nextTick(() => {
|
||||||
|
if (listRef.value) {
|
||||||
|
listRef.value.scrollTo({
|
||||||
|
top: listRef.value?.wrapRef.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除成员方法
|
||||||
|
const handleRemove = async (id) => {
|
||||||
|
memberList.value = memberList.value.filter((item) => item.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 前端加载更多 触底操作
|
||||||
|
const loadMoreList = () => {
|
||||||
|
handleLoadMoreList();
|
||||||
|
};
|
||||||
|
// 保存成员方法
|
||||||
|
const saveMember = async () => {
|
||||||
|
try {
|
||||||
|
await emit("save-member");
|
||||||
|
cancelMember();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("add member error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消成员方法
|
||||||
|
const cancelMember = () => {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
memberSearchQuery.value = [];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.standard-ui-back-drawer {
|
||||||
|
.drawer-body-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
:deep(.el-input) {
|
||||||
|
--el-input-border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
padding: 0 14px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
.list-fade-enter-active,
|
||||||
|
.list-fade-leave-active {
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px); // 从上方滑入
|
||||||
|
background-color: #ecf5ff; // 初始色可以亮一点
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px); // 向侧面滑出
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-item {
|
||||||
|
--mj-avatar-bg: #f2f3f5;
|
||||||
|
--mj-text-color: #abb0b8;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--mj-avatar-bg: #d1e9ff;
|
||||||
|
--mj-text-color: #409eff;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-color: #f5f5f5;
|
||||||
|
.member-action {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
.name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dept {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action {
|
||||||
|
opacity: 0; // 默认隐藏,高亮或hover显示
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-item {
|
||||||
|
--mj-avatar-bg: #f2f3f5;
|
||||||
|
--mj-text-color: #abb0b8;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
&:hover {
|
||||||
|
--mj-avatar-bg: #d1e9ff;
|
||||||
|
--mj-text-color: #409eff;
|
||||||
|
}
|
||||||
|
.select-item-name {
|
||||||
|
width: calc(100% - 34px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
.custom-flat-drawer-footer {
|
||||||
|
.stats-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.4;
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
<el-avatar
|
<el-avatar
|
||||||
:size="size"
|
:size="size"
|
||||||
:src="src"
|
:src="src"
|
||||||
:style="{ backgroundColor: !src ? bgColor : '' }"
|
:style="{
|
||||||
|
backgroundColor: !src ? bgColor : '',
|
||||||
|
'--avatar-text-size': fontSize,
|
||||||
|
'--avatar-text-color': avatarTextColor,
|
||||||
|
}"
|
||||||
|
v-bind="$attrs"
|
||||||
class="mj-name-avatar"
|
class="mj-name-avatar"
|
||||||
>
|
>
|
||||||
<span v-if="!src" class="avatar-text">{{ displayText }}</span>
|
<span v-if="!src" class="avatar-text">{{ displayText }}</span>
|
||||||
@@ -10,31 +15,61 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
defineOptions({name: 'NameAvatar'})
|
defineOptions({ name: "NameAvatar" });
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: { type: String, default: '' },
|
name: { type: String, default: "" },
|
||||||
src: { type: String, default: '' },
|
src: { type: String, default: "" },
|
||||||
size: { type: Number, default: 40 }
|
size: { type: Number, default: 40 },
|
||||||
|
bgColor: { type: String, default: "" },
|
||||||
|
avatarTextColor: { type: String, default: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayText = computed(() => {
|
const displayText = computed(() => {
|
||||||
return props.name ? props.name.charAt(0) : '';
|
return props.name ? props.name.charAt(0) : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const fontSize = computed(() => {
|
||||||
|
return `${Math.max(props.size * 0.4, 12)}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const bgColor = computed(() => {
|
const bgColor = computed(() => {
|
||||||
if (!props.name) return '#409EFF';
|
if (!props.name) return "#409EFF";
|
||||||
|
if (props.bgColor) return props.bgColor;
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < props.name.length; i++) {
|
for (let i = 0; i < props.name.length; i++) {
|
||||||
hash = props.name.charCodeAt(i) + ((hash << 5) - hash);
|
hash = props.name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
}
|
||||||
const colors = [
|
const colors = [
|
||||||
'#337ecc', '#409eff', '#53a8ff', '#79bbff', '#95d475',
|
"#337ecc",
|
||||||
'#eebe77', '#f89898', '#b37feb', '#ff85c0',
|
"#409eff",
|
||||||
'#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2',
|
"#53a8ff",
|
||||||
'#eb2f96', '#a0d911', '#fa8c16', '#e74c3c', '#9b59b6',
|
"#79bbff",
|
||||||
'#1abc9c', '#34495e', '#f39c12', '#e67e22', '#3498db',
|
"#95d475",
|
||||||
'#9b59b6', '#2ecc71', '#f1c40f', '#d35400', '#7f8c8d'
|
"#eebe77",
|
||||||
|
"#f89898",
|
||||||
|
"#b37feb",
|
||||||
|
"#ff85c0",
|
||||||
|
"#52c41a",
|
||||||
|
"#faad14",
|
||||||
|
"#f5222d",
|
||||||
|
"#722ed1",
|
||||||
|
"#13c2c2",
|
||||||
|
"#eb2f96",
|
||||||
|
"#a0d911",
|
||||||
|
"#fa8c16",
|
||||||
|
"#e74c3c",
|
||||||
|
"#9b59b6",
|
||||||
|
"#1abc9c",
|
||||||
|
"#34495e",
|
||||||
|
"#f39c12",
|
||||||
|
"#e67e22",
|
||||||
|
"#3498db",
|
||||||
|
"#9b59b6",
|
||||||
|
"#2ecc71",
|
||||||
|
"#f1c40f",
|
||||||
|
"#d35400",
|
||||||
|
"#7f8c8d",
|
||||||
];
|
];
|
||||||
return colors[Math.abs(hash) % colors.length];
|
return colors[Math.abs(hash) % colors.length];
|
||||||
});
|
});
|
||||||
@@ -42,14 +77,14 @@ const bgColor = computed(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mj-name-avatar {
|
.mj-name-avatar {
|
||||||
--el-avatar-bg-color:transparent;
|
--el-avatar-bg-color: transparent;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
.avatar-text {
|
.avatar-text {
|
||||||
color: var(--el-avatar-text-color);
|
color: var(--avatar-text-color);
|
||||||
font-size: 16px;
|
font-size: var(--avatar-text-size);
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
252
src/components/nodeFlow/index.vue
Normal file
252
src/components/nodeFlow/index.vue
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { VueFlow, useVueFlow, getRectOfNodes } from "@vue-flow/core";
|
||||||
|
import sopNode from "./sopNode.vue";
|
||||||
|
const { updateNode, setViewport, nodes: flowNodes } = useVueFlow();
|
||||||
|
const nodes = ref([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
type: "my-custom", // 对应插槽名 #node-my-custom
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
label: "完成项目文档",
|
||||||
|
data: { title: "开发任务", isFinished: true, value: "", isActive: false }, // 传入自定义数据
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
type: "my-custom",
|
||||||
|
position: { x: 160 + 64, y: 0 },
|
||||||
|
label: "代码上线评审",
|
||||||
|
selected: true,
|
||||||
|
data: {
|
||||||
|
title: "代码上线评审",
|
||||||
|
isFinished: false,
|
||||||
|
value: "",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
type: "my-custom",
|
||||||
|
position: { x: 320 + 128, y: 0 },
|
||||||
|
label: "代码发布",
|
||||||
|
data: { title: "代码发布", value: "", isFinished: false, isActive: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3-top",
|
||||||
|
type: "my-custom",
|
||||||
|
label: "任务完成",
|
||||||
|
position: { x: 448 + 192, y: -100 },
|
||||||
|
data: { title: "任务完成" },
|
||||||
|
}, // 向上偏
|
||||||
|
{
|
||||||
|
id: "3-bottom",
|
||||||
|
type: "my-custom",
|
||||||
|
label: "任务未完成",
|
||||||
|
position: { x: 448 + 192, y: 100 },
|
||||||
|
data: { title: "任务未完成" },
|
||||||
|
}, // 向下偏
|
||||||
|
]);
|
||||||
|
|
||||||
|
const edges = ref([
|
||||||
|
{ id: "e1-2", source: "1", target: "2", },
|
||||||
|
{ id: "e2-3", source: "2", target: "3", },
|
||||||
|
{ id: "e3-3t", source: "3", target: "3-top", },
|
||||||
|
{ id: "e3-3b", source: "3", target: "3-bottom", },
|
||||||
|
]);
|
||||||
|
const PADDING = 20;
|
||||||
|
const canvasRect = computed(() => {
|
||||||
|
// 过滤掉还没有尺寸信息的节点
|
||||||
|
const validNodes = flowNodes.value.filter((n) => n.dimensions.width > 0);
|
||||||
|
|
||||||
|
if (validNodes.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
|
||||||
|
|
||||||
|
return getRectOfNodes(validNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
const translateExtent = computed(() => {
|
||||||
|
const rect = canvasRect.value;
|
||||||
|
if (rect.width === 0)
|
||||||
|
return [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
];
|
||||||
|
const result = [
|
||||||
|
[rect.x - PADDING, rect.y - PADDING],
|
||||||
|
[rect.x + rect.width + PADDING, rect.y + rect.height + PADDING],
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvasStyle = computed(() => {
|
||||||
|
const bounds = canvasRect.value;
|
||||||
|
return {
|
||||||
|
// 宽度 = 节点矩形宽度 + 左右边距
|
||||||
|
width: `${bounds.width + PADDING * 2}px`,
|
||||||
|
// 高度 = 节点矩形高度 + 上下边距
|
||||||
|
height: `${bounds.height + PADDING * 2}px`,
|
||||||
|
position: "relative",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新增节点
|
||||||
|
const addNode = () => {
|
||||||
|
const HORIZONTAL_GAP = 40; // 缩短间距,让主干紧贴汇合点
|
||||||
|
const MAX_WIDTH = 160;
|
||||||
|
const VERTICAL_GAP = 64;
|
||||||
|
|
||||||
|
// 1. 获取所有待链接分支中,最右侧的边界
|
||||||
|
const pendingBranchNodes = nodes.value.filter(node => {
|
||||||
|
const isBranch = node.id.includes('top') || node.id.includes('bottom');
|
||||||
|
const isAlreadyLinked = edges.value.some(edge => edge.source === node.id);
|
||||||
|
return isBranch && !isAlreadyLinked;
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextX = 0;
|
||||||
|
|
||||||
|
if (pendingBranchNodes.length > 0) {
|
||||||
|
// 如果有分支待汇合,新主干 X = 分支最右侧位置 + 小间距
|
||||||
|
// 注意:这里最好加上节点自身的宽度(假设是 160)
|
||||||
|
const rightmostX = Math.max(...pendingBranchNodes.map(n => n.position.x));
|
||||||
|
nextX = rightmostX + MAX_WIDTH + HORIZONTAL_GAP;
|
||||||
|
} else {
|
||||||
|
// 如果没有分支,按正常主干逻辑追加
|
||||||
|
const mainNodes = nodes.value.filter(n => !n.id.includes('-'));
|
||||||
|
const lastMain = mainNodes[mainNodes.length - 1];
|
||||||
|
nextX = (lastMain?.position.x || 0) + MAX_WIDTH + VERTICAL_GAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = (nodes.value.reduce((max, n) => Math.max(max, parseInt(n.id) || 0), 0) + 1).toString();
|
||||||
|
// 2. 创建新节点,Y 轴保持 0(主干中轴)
|
||||||
|
const newNode = {
|
||||||
|
id: newId,
|
||||||
|
type: "my-custom",
|
||||||
|
position: { x: nextX, y: 0 },
|
||||||
|
label: `任务 ${newId}`,
|
||||||
|
data: { title: `任务 ${newId}`, isFinished: false, isActive: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 连线逻辑不变,但确保 type 是 smoothstep
|
||||||
|
const newEdges = [];
|
||||||
|
|
||||||
|
// 链接分支到汇合点
|
||||||
|
pendingBranchNodes.forEach(branch => {
|
||||||
|
newEdges.push({
|
||||||
|
id: `e${branch.id}-${newId}`,
|
||||||
|
source: branch.id,
|
||||||
|
target: newId,
|
||||||
|
type: "smoothstep",
|
||||||
|
// 调整 pathOptions 让折线更陡峭/紧凑,视觉上更像在“链接点处”
|
||||||
|
pathOptions: { borderRadius: 10, offset: 10 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 主干相连
|
||||||
|
const mainNodes = nodes.value.filter(n => !n.id.includes('-'));
|
||||||
|
if (mainNodes.length > 0) {
|
||||||
|
const prevMain = mainNodes[mainNodes.length - 1];
|
||||||
|
newEdges.push({
|
||||||
|
id: `e${prevMain.id}-${newId}`,
|
||||||
|
source: prevMain.id,
|
||||||
|
target: newId,
|
||||||
|
type: "smoothstep"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.value.push(newNode);
|
||||||
|
edges.value.push(...newEdges);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeClick = (clickedNodeId) => {
|
||||||
|
// 1. 先判断点击的是否是已禁用的节点,如果是则直接跳过
|
||||||
|
const targetNode = nodes.value.find((n) => n.id === clickedNodeId);
|
||||||
|
if (targetNode?.data?.isFinished) return;
|
||||||
|
|
||||||
|
// 2. 一次性映射出所有节点的新状态
|
||||||
|
nodes.value = nodes.value.map((node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
// 只有点击的那个变 true,其他所有节点强制变 false
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
isActive: node.id === clickedNodeId,
|
||||||
|
},
|
||||||
|
// 顺便同步官方的选中状态,确保视觉和逻辑统一
|
||||||
|
selected: node.id === clickedNodeId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 rect 的变化(当节点增减或尺寸测量完成时触发)
|
||||||
|
watch(
|
||||||
|
() => canvasRect.value,
|
||||||
|
async (rect) => {
|
||||||
|
console.log("rect", -rect.x + PADDING, rect.width);
|
||||||
|
if (rect.width > 0) {
|
||||||
|
await nextTick();
|
||||||
|
setViewport({
|
||||||
|
x: -rect.x + PADDING,
|
||||||
|
y: -rect.y + PADDING,
|
||||||
|
zoom: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化setViewport
|
||||||
|
const onInit = (instance) => {
|
||||||
|
const validNodes = flowNodes.value.filter((n) => n.dimensions.width > 0);
|
||||||
|
if (validNodes.length > 0) {
|
||||||
|
const rect = canvasRect.value;
|
||||||
|
setViewport({
|
||||||
|
x: -rect.x + PADDING,
|
||||||
|
y: -rect.y + PADDING,
|
||||||
|
zoom: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-button type="primary" @click="addNode">添加节点</el-button>
|
||||||
|
<div class="scroll-container">
|
||||||
|
<el-scrollbar>
|
||||||
|
<div :style="canvasStyle">
|
||||||
|
<VueFlow
|
||||||
|
:nodes="nodes"
|
||||||
|
:edges="edges"
|
||||||
|
:fit-view-on-init="false"
|
||||||
|
:zoom-on-scroll="false"
|
||||||
|
:nodes-draggable="false"
|
||||||
|
:zoom-on-double-click="false"
|
||||||
|
:pan-on-drag="false"
|
||||||
|
:nodes-connectable="false"
|
||||||
|
:selection-key="null"
|
||||||
|
:pan-activation-action="null"
|
||||||
|
:zoom-on-pinch="false"
|
||||||
|
:min-zoom="1"
|
||||||
|
:max-zoom="1"
|
||||||
|
:translate-extent="translateExtent"
|
||||||
|
@pane-ready="onInit"
|
||||||
|
>
|
||||||
|
<template #node-my-custom="nodeProps">
|
||||||
|
<sopNode v-bind="nodeProps" @activate-node="handleNodeClick" />
|
||||||
|
</template>
|
||||||
|
</VueFlow>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 官方基础样式还是要引,不然无法拖拽和缩放 */
|
||||||
|
@import "@vue-flow/core/dist/style.css";
|
||||||
|
@import "@vue-flow/core/dist/theme-default.css";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.vue-flow__edge-path) {
|
||||||
|
stroke: #e2e8f0;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
src/components/nodeFlow/sopNode.vue
Normal file
154
src/components/nodeFlow/sopNode.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from "@vue-flow/core";
|
||||||
|
// 接收数据
|
||||||
|
const props = defineProps(["id","data", "label"]);
|
||||||
|
const emit = defineEmits(['activateNode']);
|
||||||
|
|
||||||
|
const updateItem = () => {
|
||||||
|
// 已经完成就不能在点击了
|
||||||
|
if(props.data.isFinished){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('activateNode', props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="custom-step-node" :class="{ 'is-finished': data.isFinished }" @click="updateItem">
|
||||||
|
<div class="node-capsule" :class="{ 'is-active': data.isActive }">
|
||||||
|
<Handle type="target" :position="Position.Left" class="hidden-handle" />
|
||||||
|
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span class="node-label">{{ data.title }}</span>
|
||||||
|
|
||||||
|
<Handle type="source" :position="Position.Right" class="hidden-handle" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-input-area" @click.stop>
|
||||||
|
<div class="input-wrapper-container">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<el-input type="text" v-model="data.value" @click.stop></el-input>
|
||||||
|
</div>
|
||||||
|
<span class="percent-unit">%</span>
|
||||||
|
</div>
|
||||||
|
<div class="node-tooltips">产能赋权</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.custom-step-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px; /* 胶囊与输入框的间距 */
|
||||||
|
width: 120px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 胶囊样式 */
|
||||||
|
.node-capsule {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 20px; /* 圆角胶囊 */
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激活状态(蓝框) */
|
||||||
|
.node-capsule.is-active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 1px #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态小圆点 */
|
||||||
|
.status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #d1d5db; /* 默认灰色 */
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.is-active .status-dot {
|
||||||
|
background-color: #2563eb; /* 激活蓝色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.is-active .node-label {
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框区域 */
|
||||||
|
.node-input-area {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
.input-wrapper-container{
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-tooltips{
|
||||||
|
color: #90a1b9;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
&:hover .node-tooltips{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
width: 60px;
|
||||||
|
:deep(.el-input__inner){
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.percent-input {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1f2937;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percent-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-finished {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏连接点逻辑,但保持在胶囊中心高度 */
|
||||||
|
.hidden-handle {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tabs-outer-container" ref="containerRef" :style="{ height: height + 'px' }">
|
<div class="tabs-outer-container" ref="containerRef" :style="{ height: height + 'px' }">
|
||||||
|
<div class="ghost-wrapper" ref="ghostRef">
|
||||||
|
<div v-for="item in items" :key="'ghost' + item[itemMap.id]" class="tab-item">
|
||||||
|
<span class="tab-text">{{ item[itemMap.label] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tabs-wrapper">
|
<div class="tabs-wrapper">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in visibleItems"
|
v-for="(item, index) in visibleItems"
|
||||||
:key="item.id"
|
:key="item[itemMap.id]"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: modelValue === item.id }"
|
:class="{ 'active': modelValue === item[itemMap.id] }"
|
||||||
@click="$emit('update:modelValue', item.id)"
|
@click="$emit('update:modelValue', item[itemMap.id])"
|
||||||
>
|
>
|
||||||
<span class="tab-text">{{ item.label }}</span>
|
<span class="tab-text">{{ item[itemMap.label] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
@@ -17,7 +23,7 @@
|
|||||||
@command="handleCommand"
|
@command="handleCommand"
|
||||||
class="more-dropdown"
|
class="more-dropdown"
|
||||||
>
|
>
|
||||||
<div class="tab-item more-trigger">
|
<div class="tab-item more-trigger" :class="{ 'is-more-active': isHiddenActive }">
|
||||||
<span>更多</span>
|
<span>更多</span>
|
||||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,11 +31,12 @@
|
|||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
v-for="item in hiddenItems"
|
v-for="item in hiddenItems"
|
||||||
:key="item.id"
|
:key="item[itemMap.id]"
|
||||||
:command="item.id"
|
:command="item[itemMap.id]"
|
||||||
|
|
||||||
>
|
>
|
||||||
<span :class="{ 'is-active-item-overflow-tabs': modelValue === item.id }">{{ item.label }}</span>
|
<span :class="{ 'is-active-item-overflow-tabs': modelValue === item[itemMap.id] }">
|
||||||
|
{{ item[itemMap.label] }}
|
||||||
|
</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,50 +53,88 @@ import { ArrowDown } from "@element-plus/icons-vue";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: [String, Number],
|
modelValue: [String, Number],
|
||||||
items: {
|
items: { type: Array, default: () => [] },
|
||||||
type: Array,
|
itemMap: { type: Object, default: () => ({ id: 'id', label: 'label' }) },
|
||||||
default: () => [],
|
height: { type: Number, default: 32 },
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 32,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
const containerRef = ref(null);
|
const containerRef = ref(null);
|
||||||
|
const ghostRef = ref(null);
|
||||||
const splitIndex = ref(props.items.length);
|
const splitIndex = ref(props.items.length);
|
||||||
|
const itemWidths = ref([]); // 缓存所有项的宽度
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
const activeBarStyle = ref({
|
const activeBarStyle = ref({ width: "0px", left: "0px", opacity: 0 });
|
||||||
width: "0px",
|
|
||||||
left: "0px",
|
|
||||||
opacity: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleItems = computed(() => props.items.slice(0, splitIndex.value));
|
const visibleItems = computed(() => props.items.slice(0, splitIndex.value));
|
||||||
const hiddenItems = computed(() => props.items.slice(splitIndex.value));
|
const hiddenItems = computed(() => props.items.slice(splitIndex.value));
|
||||||
const isHiddenActive = computed(() => {
|
const isHiddenActive = computed(() => {
|
||||||
return hiddenItems.value.some((item) => item.id === props.modelValue);
|
return hiddenItems.value.some((item) => item[props.itemMap.id] === props.modelValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取所有 Tab 的初始宽度
|
||||||
|
const measureWidths = () => {
|
||||||
|
if (!ghostRef.value) return;
|
||||||
|
const nodes = ghostRef.value.querySelectorAll(".tab-item");
|
||||||
|
if (nodes.length === 0) return;
|
||||||
|
|
||||||
|
const widths = Array.from(nodes).map(node => node.getBoundingClientRect().width);
|
||||||
|
|
||||||
|
// 只有拿到有效宽度才更新,防止在某些极端情况下宽度全为 0 导致计算错误
|
||||||
|
if (widths.some(w => w > 0)) {
|
||||||
|
itemWidths.value = widths;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateLayout = () => {
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
|
// 如果没有宽度数据,先测一遍
|
||||||
|
if (itemWidths.value.length === 0) {
|
||||||
|
measureWidths();
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerWidth = containerRef.value.offsetWidth;
|
||||||
|
// 如果容器本身没宽度(比如在隐藏的弹窗里),直接返回
|
||||||
|
if (containerWidth <= 0) return;
|
||||||
|
|
||||||
|
const moreBtnWidth = 80;
|
||||||
|
let currentWidth = 0;
|
||||||
|
let newSplitIndex = props.items.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < itemWidths.value.length; i++) {
|
||||||
|
const w = itemWidths.value[i];
|
||||||
|
const fullWidth = w;
|
||||||
|
|
||||||
|
if (currentWidth + fullWidth > containerWidth) {
|
||||||
|
newSplitIndex = i;
|
||||||
|
// 预留更多按钮位置
|
||||||
|
while (newSplitIndex > 0 && currentWidth + moreBtnWidth > containerWidth) {
|
||||||
|
newSplitIndex--;
|
||||||
|
currentWidth -= itemWidths.value[newSplitIndex];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentWidth += fullWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
splitIndex.value = newSplitIndex;
|
||||||
|
};
|
||||||
|
|
||||||
const updateActiveBar = async () => {
|
const updateActiveBar = async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!containerRef.value) return;
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
// 1. 检查激活项是否在可见区域
|
const activeIndex = visibleItems.value.findIndex(item => item[props.itemMap.id] === props.modelValue);
|
||||||
const activeIndex = visibleItems.value.findIndex((item) => item.id === props.modelValue);
|
|
||||||
|
|
||||||
if (activeIndex >= 0) {
|
if (activeIndex >= 0) {
|
||||||
// 激活项在可见区域:计算位置并显示下划线
|
const tabItems = containerRef.value.querySelectorAll(".tabs-wrapper > .tab-item:not(.more-trigger)");
|
||||||
const tabItems = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
|
|
||||||
const activeElement = tabItems[activeIndex];
|
const activeElement = tabItems[activeIndex];
|
||||||
|
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
const rect = activeElement.getBoundingClientRect();
|
const rect = activeElement.getBoundingClientRect();
|
||||||
const containerRect = containerRef.value.getBoundingClientRect();
|
const containerRect = containerRef.value.getBoundingClientRect();
|
||||||
|
|
||||||
activeBarStyle.value = {
|
activeBarStyle.value = {
|
||||||
width: `${rect.width * 0.6}px`,
|
width: `${rect.width * 0.6}px`,
|
||||||
left: `${rect.left - containerRect.left + rect.width * 0.2}px`,
|
left: `${rect.left - containerRect.left + rect.width * 0.2}px`,
|
||||||
@@ -98,66 +143,42 @@ const updateActiveBar = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
activeBarStyle.value.opacity = 0;
|
||||||
// 2. 如果在隐藏区域或未找到,将下划线宽度设为 0
|
|
||||||
activeBarStyle.value = {
|
|
||||||
...activeBarStyle.value,
|
|
||||||
width: "0px",
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateLayout = () => {
|
const handleResize = () => {
|
||||||
if (!containerRef.value) return;
|
|
||||||
const containerWidth = Math.floor(containerRef.value.getBoundingClientRect().width);
|
|
||||||
const itemsNodes = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
|
|
||||||
const moreBtnWidth = 90;
|
|
||||||
|
|
||||||
let currentWidth = 0;
|
|
||||||
let newSplitIndex = props.items.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < itemsNodes.length; i++) {
|
|
||||||
currentWidth += Math.ceil(itemsNodes[i].getBoundingClientRect().width) + 20;
|
|
||||||
if (currentWidth + moreBtnWidth > containerWidth) {
|
|
||||||
newSplitIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (splitIndex.value !== newSplitIndex) {
|
|
||||||
splitIndex.value = newSplitIndex;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedCalc = () => {
|
|
||||||
if (timer) clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
calculateLayout();
|
calculateLayout();
|
||||||
|
nextTick(() => {
|
||||||
updateActiveBar();
|
updateActiveBar();
|
||||||
}, 100);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let resizeObserver = null;
|
let resizeObserver = null;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
measureWidths();
|
||||||
calculateLayout();
|
calculateLayout();
|
||||||
updateActiveBar();
|
updateActiveBar();
|
||||||
resizeObserver = new ResizeObserver(() => debouncedCalc());
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
handleResize();
|
||||||
|
});
|
||||||
|
|
||||||
if (containerRef.value) resizeObserver.observe(containerRef.value);
|
if (containerRef.value) resizeObserver.observe(containerRef.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (resizeObserver) resizeObserver.disconnect();
|
resizeObserver?.disconnect();
|
||||||
if (timer) clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCommand = (id) => emit("update:modelValue", id);
|
const handleCommand = (id) => emit("update:modelValue", id);
|
||||||
|
|
||||||
watch(() => props.modelValue, () => updateActiveBar());
|
watch(() => props.modelValue, () => updateActiveBar());
|
||||||
watch(() => props.items, async () => {
|
watch(() => props.items, async () => {
|
||||||
splitIndex.value = props.items.length;
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
measureWidths();
|
||||||
calculateLayout();
|
calculateLayout();
|
||||||
updateActiveBar();
|
updateActiveBar();
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
@@ -165,8 +186,21 @@ watch(() => props.items, async () => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.tabs-outer-container {
|
.tabs-outer-container {
|
||||||
|
--more-left-line:#f0f2f5;
|
||||||
|
--item-color:#9DA1B9;
|
||||||
|
--item-size:12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
position: relative;
|
||||||
|
|
||||||
|
// 关键:测量层不可见且不占位,但必须渲染以获取宽度
|
||||||
|
.ghost-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: -9999px;
|
||||||
|
left: -9999px;
|
||||||
|
visibility: hidden;
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs-wrapper {
|
.tabs-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -174,6 +208,7 @@ watch(() => props.items, async () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
@@ -182,39 +217,37 @@ watch(() => props.items, async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #606266;
|
color: var(--item-color);
|
||||||
font-size: 14px;
|
font-size: var(--item-size);
|
||||||
position: relative;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #409eff;
|
color: var(--el-color-primary);
|
||||||
font-weight: 600;
|
}
|
||||||
|
|
||||||
|
&.is-more-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-trigger {
|
.more-trigger {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
border-left: 1px solid #f0f2f5;
|
border-left: 1px solid var( --more-left-line);
|
||||||
|
|
||||||
// 选中更多中的数据时,仅文字和图标变蓝
|
|
||||||
&.is-more-active {
|
|
||||||
color: #409eff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-bar {
|
.active-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background-color: #409eff;
|
background-color: var(--el-color-primary);
|
||||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
transition: all 0.12s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
bottom: -1px;
|
||||||
opacity 0.2s;
|
|
||||||
bottom: 0;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-active-item-overflow-tabs {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
111
src/components/popoverMenu/index.vue
Normal file
111
src/components/popoverMenu/index.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition name="el-zoom-in-top">
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
ref="menuRef"
|
||||||
|
v-click-outside="close"
|
||||||
|
class="mj-universal-menu"
|
||||||
|
:style="[style, customContainerStyle]"
|
||||||
|
>
|
||||||
|
<slot :data="data" :close="close">
|
||||||
|
<div class="default-menu-wrapper">
|
||||||
|
<div class="menu-item" @click="handleAction('edit')">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
<span>编辑</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item danger" @click="handleAction('delete')">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
<span>删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, type CSSProperties } from "vue";
|
||||||
|
import { ClickOutside as vClickOutside } from "element-plus";
|
||||||
|
import { Edit, Delete } from "@element-plus/icons-vue";
|
||||||
|
import type { MenuPosition } from "./types";
|
||||||
|
|
||||||
|
// 扩展 Props 以适应不同场景
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
style: MenuPosition;
|
||||||
|
data: T | null;
|
||||||
|
// 新增:允许外部传入额外的样式(如宽度、padding)
|
||||||
|
bodyStyle?: CSSProperties;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
action: [type: string, data: T];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 合并样式
|
||||||
|
const customContainerStyle = computed(() => {
|
||||||
|
return props.bodyStyle || {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => emit("close");
|
||||||
|
|
||||||
|
const handleAction = (type: string) => {
|
||||||
|
if (props.data) {
|
||||||
|
emit("action", type, props.data);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mj-universal-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3000;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 95vw;
|
||||||
|
overflow: hidden; // 保证内部 hover 背景不溢出圆角
|
||||||
|
|
||||||
|
// 当没有插槽内容时使用的默认包装器
|
||||||
|
.default-menu-wrapper {
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单项基础样式
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s ease; // 增加平滑过渡
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 危险操作样式(如删除)
|
||||||
|
&.danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fef0f0;
|
||||||
|
color: #f56c6c; // 保持红色
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果菜单里有图标,可以统一控制图标大小
|
||||||
|
.el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pro-table-container">
|
<div ref="tableContainerRef" class="pro-table-container">
|
||||||
<el-table
|
<el-table
|
||||||
:data="data"
|
:data="data"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-loading="innerLoading"
|
v-loading="innerLoading"
|
||||||
class="hover-action-table"
|
class="hover-action-table"
|
||||||
header-row-class-name="header-row-name"
|
header-row-class-name="header-row-name"
|
||||||
|
:height="tableHeight"
|
||||||
>
|
>
|
||||||
<template v-for="(col, index) in columns" :key="col.prop">
|
<template v-for="(col, index) in columns" :key="index">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="!col.slot && col.prop !== 'actions'"
|
v-if="!col.slot && col.prop !== 'actions'"
|
||||||
v-bind="col"
|
v-bind="col"
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
v-bind="getButtonProps(btn)"
|
v-bind="getButtonProps(btn)"
|
||||||
|
:disabled="getButtonDisabled(btn, scope.row)"
|
||||||
@click="handleButtonClick(btn, scope.row)"
|
@click="handleButtonClick(btn, scope.row)"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
@@ -57,15 +59,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 如果按钮超过maxButtons,显示下拉菜单 -->
|
<!-- 如果按钮超过maxButtons,显示下拉菜单 (如果是移入移除展示按钮 不建议开启下拉菜单)-->
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
v-if="
|
v-if="showDropdown && col.actions.length > (col.maxButtons || MAX_BUTTON_LENGTH) && hasDropdownPermission(col.actions.slice(col.maxButtons || MAX_BUTTON_LENGTH))"
|
||||||
col.actions.length >
|
|
||||||
(col.maxButtons || MAX_BUTTON_LENGTH) &&
|
|
||||||
hasDropdownPermission(
|
|
||||||
col.actions.slice(col.maxButtons || MAX_BUTTON_LENGTH)
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="dropdown-menu-table"
|
class="dropdown-menu-table"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
>
|
>
|
||||||
@@ -86,6 +82,7 @@
|
|||||||
<el-dropdown-item>
|
<el-dropdown-item>
|
||||||
<el-button
|
<el-button
|
||||||
v-bind="getButtonProps(btn)"
|
v-bind="getButtonProps(btn)"
|
||||||
|
:disabled="getButtonDisabled(btn, scope.row)"
|
||||||
@click="handleButtonClick(btn, scope.row)"
|
@click="handleButtonClick(btn, scope.row)"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
@@ -108,9 +105,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="pro-table-footer">
|
<div class="pro-table-footer" v-if="pagination">
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
<div class="mj-footer-content" v-if="pagination">
|
<div class="mj-footer-content">
|
||||||
<div class="footer-left">
|
<div class="footer-left">
|
||||||
<span class="total-text">共 {{ total || data.length }} 个条目</span>
|
<span class="total-text">共 {{ total || data.length }} 个条目</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +130,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { ArrowDown } from "@element-plus/icons-vue";
|
import { debounce } from 'lodash-es';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
columns: { type: Array, required: true },
|
columns: { type: Array, required: true },
|
||||||
data: { type: Array, required: true },
|
data: { type: Array, required: true },
|
||||||
@@ -144,8 +141,15 @@ const props = defineProps({
|
|||||||
immediate: { type: Boolean, default: true },
|
immediate: { type: Boolean, default: true },
|
||||||
// 是否在激活时刷新数据
|
// 是否在激活时刷新数据
|
||||||
refreshOnActivated: { type: Boolean, default: true },
|
refreshOnActivated: { type: Boolean, default: true },
|
||||||
|
// 是否展示下拉菜单 (如果按钮是鼠标移入移出 不建议开启下拉菜单 会导致弹层的问题)
|
||||||
|
showDropdown: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DEFAULT_PAGINATION = {
|
||||||
|
layout: "prev, pager, next",
|
||||||
|
background: false,
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
"current-change",
|
"current-change",
|
||||||
"size-change",
|
"size-change",
|
||||||
@@ -159,6 +163,39 @@ const innerLoading = ref(false);
|
|||||||
// 默认按钮长度
|
// 默认按钮长度
|
||||||
const MAX_BUTTON_LENGTH = 3;
|
const MAX_BUTTON_LENGTH = 3;
|
||||||
|
|
||||||
|
// 高度模块的计算
|
||||||
|
const tableContainerRef = ref(null);
|
||||||
|
const containerHeight = ref(0);
|
||||||
|
const minHeight = 400; // 最小高度值
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = debounce(() => {
|
||||||
|
updateContainerHeight();
|
||||||
|
},100);
|
||||||
|
|
||||||
|
// 动态计算表格容器高度
|
||||||
|
const updateContainerHeight = async () => {
|
||||||
|
await nextTick();
|
||||||
|
if (tableContainerRef.value) {
|
||||||
|
// 获取容器相对于视口的位置
|
||||||
|
const element = tableContainerRef.value;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const containerTop = rect.top;
|
||||||
|
|
||||||
|
// 计算从容器位置到浏览器底部的可用高度
|
||||||
|
const availableHeight = window.innerHeight - containerTop -20;
|
||||||
|
containerHeight.value = Math.max(availableHeight, minHeight);
|
||||||
|
} else {
|
||||||
|
// 如果元素尚未渲染,使用默认偏移值
|
||||||
|
containerHeight.value = Math.max(window.innerHeight - 100, minHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeight = computed(() => {
|
||||||
|
// 为页码预留空间
|
||||||
|
const paginationHeight = props.pagination ? 38 : 0;
|
||||||
|
return (containerHeight.value - paginationHeight) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
// 标记是否是首次挂载
|
// 标记是否是首次挂载
|
||||||
let isFirstMount = true;
|
let isFirstMount = true;
|
||||||
|
|
||||||
@@ -183,11 +220,12 @@ const params = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 请求方法
|
// 请求方法
|
||||||
const refresh = async () => {
|
const refresh = async (isReset:boolean=false) => {
|
||||||
if (!props.requestApi) return;
|
if (!props.requestApi) return;
|
||||||
innerLoading.value = true;
|
innerLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await props.requestApi(params.value);
|
const requestParams = isReset ? {...params.value,pageNo:1} : params.value;
|
||||||
|
const res = await props.requestApi(requestParams);
|
||||||
emit("update:data", res?.records || []);
|
emit("update:data", res?.records || []);
|
||||||
emit("update:total", res?.total || 0);
|
emit("update:total", res?.total || 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -208,7 +246,17 @@ const handleSizeChange = (val) => {
|
|||||||
if (props.requestApi) refresh();
|
if (props.requestApi) refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
// 按钮支持 disabledFn函数
|
||||||
|
const getButtonDisabled = (button, row) => {
|
||||||
|
if (button.disabledFn && typeof button.disabledFn === 'function') {
|
||||||
|
return button.disabledFn(row);
|
||||||
|
}
|
||||||
|
return button.disabled || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await updateContainerHeight();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
if (props.immediate) {
|
if (props.immediate) {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
@@ -218,21 +266,36 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 兼容设置了keep-alive
|
// 兼容设置了keep-alive
|
||||||
onActivated(() => {
|
onActivated(async () => {
|
||||||
if (!isFirstMount && props.refreshOnActivated) {
|
if (!isFirstMount && props.refreshOnActivated) {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
await updateContainerHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件销毁的生命周期
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 暴露 refresh 方法给外部使用
|
// 暴露 refresh 方法给外部使用
|
||||||
defineExpose({ refresh });
|
defineExpose({ refresh,reset:()=>refresh(true) });
|
||||||
|
|
||||||
// 分页配置
|
// 分页配置
|
||||||
const paginationConfig = computed(() => ({
|
const paginationConfig = computed(() =>{
|
||||||
layout: "prev, pager, next",
|
if (typeof props.pagination === 'boolean') {
|
||||||
background: false,
|
return DEFAULT_PAGINATION;
|
||||||
...props.pagination,
|
}
|
||||||
}));
|
|
||||||
|
if (typeof props.pagination === 'object' && props.pagination !== null) {
|
||||||
|
return {
|
||||||
|
...DEFAULT_PAGINATION,
|
||||||
|
...props.pagination
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_PAGINATION;
|
||||||
|
});
|
||||||
|
|
||||||
// action按钮组的数据
|
// action按钮组的数据
|
||||||
const handleButtonClick = (button, row) => {
|
const handleButtonClick = (button, row) => {
|
||||||
@@ -250,6 +313,9 @@ const shouldHideButton = (button, row) => {
|
|||||||
const getVisibleButtonCount = (col) => {
|
const getVisibleButtonCount = (col) => {
|
||||||
const { actions, maxButtons } = col;
|
const { actions, maxButtons } = col;
|
||||||
const totalButtons = maxButtons || MAX_BUTTON_LENGTH;
|
const totalButtons = maxButtons || MAX_BUTTON_LENGTH;
|
||||||
|
if(!props.showDropdown){
|
||||||
|
return actions.length;
|
||||||
|
}
|
||||||
return totalButtons === actions.length ? totalButtons : Math.min(totalButtons, actions.length)-1;
|
return totalButtons === actions.length ? totalButtons : Math.min(totalButtons, actions.length)-1;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +325,7 @@ const hasDropdownPermission = (dropdownActions) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
const getButtonProps = (button) => {
|
const getButtonProps = (button) => {
|
||||||
const { label, onClick, show, permission, ...buttonProps } = button;
|
const { label, onClick, show, permission,disabledFn, ...buttonProps } = button;
|
||||||
return buttonProps;
|
return buttonProps;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -267,7 +333,7 @@ const getButtonProps = (button) => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.pro-table-container {
|
.pro-table-container {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
@@ -277,11 +343,15 @@ const getButtonProps = (button) => {
|
|||||||
background-color: #fbfcfd;
|
background-color: #fbfcfd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(.el-table__inner-wrapper:before){
|
||||||
|
--el-table-border-color:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* 底部容器样式:对应图片中的布局 */
|
/* 底部容器样式:对应图片中的布局 */
|
||||||
.pro-table-footer {
|
.pro-table-footer {
|
||||||
padding: 3px 24px;
|
padding: 3px 24px;
|
||||||
background-color: #fcfdfe;
|
background-color: #fcfdfe;
|
||||||
|
border-top: 1px solid #E2E8F0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-footer-content {
|
.mj-footer-content {
|
||||||
@@ -302,7 +372,7 @@ const getButtonProps = (button) => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 0.25s ease-in, transform 0.25s ease-in;
|
transition: opacity 0.25s ease-in, transform 0.25s ease-in;
|
||||||
}
|
}
|
||||||
:deep(.el-table__row:hover) .action-group {
|
:deep(.el-table__row:hover) .action-group,:deep(.row-dropdown-active) .action-group{
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|||||||
535
src/components/proTable/proTablev2.vue
Normal file
535
src/components/proTable/proTablev2.vue
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="tableContainerRef" class="pro-table-v2-container">
|
||||||
|
<el-table-v2
|
||||||
|
ref="tableRef"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:columns="adaptedColumns"
|
||||||
|
:data="innerData"
|
||||||
|
:width="tableSize.width"
|
||||||
|
:height="tableSize.height"
|
||||||
|
:fixed="true"
|
||||||
|
@rows-rendered="handleRowsRendered"
|
||||||
|
>
|
||||||
|
<template #overlay v-if="loading && innerData.length === 0">
|
||||||
|
<div class="v2-initial-loading">
|
||||||
|
<div class="loading-content">
|
||||||
|
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="v2-infinite-footer">
|
||||||
|
<div v-if="loading && innerData.length > 0" class="loading-more">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="noMore" class="no-more">
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="text">已经到底了~</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空数据 -->
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="暂无数据" :image-size="100" />
|
||||||
|
</template>
|
||||||
|
</el-table-v2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
|
import {
|
||||||
|
ElTag,
|
||||||
|
ElButton,
|
||||||
|
ElText,
|
||||||
|
ElTooltip,
|
||||||
|
ElCheckbox,
|
||||||
|
useLocale,
|
||||||
|
} from "element-plus";
|
||||||
|
import { debounce } from "lodash-es";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { DictManage } from "@/dict";
|
||||||
|
import { usePermission } from "@/utils/permission";
|
||||||
|
const { checkPermission } = usePermission();
|
||||||
|
|
||||||
|
// 组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
columns: { type: Array, required: true },
|
||||||
|
requestApi: { type: Function, required: true },
|
||||||
|
initParam: { type: Object, default: () => ({}) },
|
||||||
|
pageSize: { type: Number, default: 20 },
|
||||||
|
selection: { type: Boolean, default: false }, // 是否开启多选
|
||||||
|
selectionConfig:{
|
||||||
|
type: Object,
|
||||||
|
default:()=>({
|
||||||
|
width: 50,
|
||||||
|
fixed: false,
|
||||||
|
align: "center",
|
||||||
|
key: "selection"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 内部数据状态
|
||||||
|
const tableRef = ref(null);
|
||||||
|
const tableContainerRef = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const innerData = ref<any[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const pageNo = ref(1);
|
||||||
|
const tableSize = ref({ width: 0, height: 400 });
|
||||||
|
const selectedKeys = ref<Set<string | number>>(new Set());
|
||||||
|
const isAllSelectedMode = ref(false); //全选模式
|
||||||
|
const noMore = computed(() => {
|
||||||
|
if (total.value <= 0) return false;
|
||||||
|
const isReachTotal = innerData.value.length >= total.value;
|
||||||
|
return isReachTotal;
|
||||||
|
});
|
||||||
|
|
||||||
|
const SelectionCell = (props: {
|
||||||
|
value: boolean;
|
||||||
|
intermediate?: boolean;
|
||||||
|
onChange: (val: any) => void;
|
||||||
|
}) => (
|
||||||
|
<ElCheckbox
|
||||||
|
modelValue={props.value}
|
||||||
|
indeterminate={props.intermediate}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 1. 核心渲染工厂 (内置常用业务组件) ---
|
||||||
|
// 定义一个内部小组件处理溢出逻辑
|
||||||
|
const OverflowTooltip = defineComponent({
|
||||||
|
props: {
|
||||||
|
val: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const isOverflow = ref(false);
|
||||||
|
const textRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const checkOverflow = () => {
|
||||||
|
if (textRef.value) {
|
||||||
|
// 计算溢出
|
||||||
|
isOverflow.value =
|
||||||
|
textRef.value.scrollWidth > textRef.value.clientWidth;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<ElTooltip
|
||||||
|
content={props.val}
|
||||||
|
placement="top"
|
||||||
|
effect="dark"
|
||||||
|
disabled={!isOverflow.value}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
default: () => (
|
||||||
|
<div
|
||||||
|
ref={textRef}
|
||||||
|
class="mj-ellipsis-cell"
|
||||||
|
onMouseenter={checkOverflow}
|
||||||
|
>
|
||||||
|
{props.val}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
</ElTooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const RenderFactory = {
|
||||||
|
// 状态点/标签
|
||||||
|
status: (scope: any, col: any) => {
|
||||||
|
const val = scope.rowData[col.prop];
|
||||||
|
let rawOptions = col.options;
|
||||||
|
// 如果是 ref 或 computed,先取 .value
|
||||||
|
if (rawOptions && typeof rawOptions === "object" && "value" in rawOptions) {
|
||||||
|
rawOptions = rawOptions.value;
|
||||||
|
}
|
||||||
|
// 如果是函数则执行
|
||||||
|
if (typeof rawOptions === "function") {
|
||||||
|
rawOptions = rawOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制转为数组,防止 find 报错
|
||||||
|
const currentOptions = Array.isArray(rawOptions) ? rawOptions : [];
|
||||||
|
const target = currentOptions.find((opt: any) => opt.value == val);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="mj-status-dot"
|
||||||
|
style={{
|
||||||
|
"--data-status-color": DictManage.statusDictColor[target?.value],
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (col.onClick) {
|
||||||
|
col.onClick({ cellValue: val, rowData: scope.rowData });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{target?.label || "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// 日期格式化
|
||||||
|
date: (scope: any, col: any) => {
|
||||||
|
const val = scope.rowData[col.prop];
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{val ? dayjs(val).format(col.format || "YYYY-MM-DD HH:mm") : "-"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// 文本自动省略
|
||||||
|
ellipsis: (scope: any, col: any) => {
|
||||||
|
const val = scope.rowData[col.prop] ?? "-";
|
||||||
|
return <OverflowTooltip val={val} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 局部更新某一行数据
|
||||||
|
* @param id 唯一标识
|
||||||
|
* @param rowData 新的数据对象(可以是部分属性)
|
||||||
|
*/
|
||||||
|
const updateRow = (id: string | number, rowData: object) => {
|
||||||
|
const index = innerData.value.findIndex((item) => item.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
// 使用合并方式,保留原有的其他字段
|
||||||
|
innerData.value[index] = { ...innerData.value[index], ...rowData };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前已加载的数据
|
||||||
|
const getSelectedRows = () => {
|
||||||
|
return innerData.value.filter(row => selectedKeys.value.has(row.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除某一行数据
|
||||||
|
* @param id 唯一标识
|
||||||
|
*/
|
||||||
|
const removeRow = (id: string | number) => {
|
||||||
|
const index = innerData.value.findIndex((item) => item.id == id);
|
||||||
|
if (index !== -1) {
|
||||||
|
innerData.value.splice(index, 1);
|
||||||
|
total.value = Math.max(0, total.value - 1);
|
||||||
|
|
||||||
|
// 💡 关键:删除后如果当前显示的数据太少(比如不足一屏),自动去接下一页
|
||||||
|
if (innerData.value.length < 15 && !noMore.value) {
|
||||||
|
fetchTableData(false); // 这里的 false 表示“追加”数据来补位
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果删除后这一页空了,且还有数据,也去拉取
|
||||||
|
if (innerData.value.length === 0 && total.value > 0) {
|
||||||
|
fetchTableData(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入一条新数据(通常用于新增成功后插到最前面)
|
||||||
|
* @param rowData 完整的数据对象
|
||||||
|
*/
|
||||||
|
const addRow = (rowData: object) => {
|
||||||
|
innerData.value.unshift(rowData);
|
||||||
|
total.value++;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 适配 Columns 配置 ---
|
||||||
|
const adaptedColumns = computed(() => {
|
||||||
|
const containerWidth = tableSize.value.width;
|
||||||
|
if (containerWidth <= 0) return [];
|
||||||
|
|
||||||
|
// 从配置中提取宽度,如果没有传入则默认为 50
|
||||||
|
const hasSelection = props.selection;
|
||||||
|
const selectionWidth = hasSelection ? (props.selectionConfig?.width || 50) : 0;
|
||||||
|
|
||||||
|
// 1. 找出用户配置中没有设置宽度的列
|
||||||
|
const autoColumns = props.columns.filter((col: any) => !col.width);
|
||||||
|
|
||||||
|
// 2. 计算所有显式设置了宽度的用户列总和
|
||||||
|
const userFixedWidthTotal = props.columns
|
||||||
|
.filter((col: any) => col.width)
|
||||||
|
.reduce((prev, curr: any) => prev + Number(curr.width), 0);
|
||||||
|
|
||||||
|
// 3. 计算剩余可用宽度 (如果开启了 selection,需要额外扣除 selection 的宽度)
|
||||||
|
const occupiedWidth = userFixedWidthTotal + selectionWidth;
|
||||||
|
const remainingWidth = Math.max(containerWidth - occupiedWidth, 0);
|
||||||
|
|
||||||
|
// 4. 计算自适应列宽度
|
||||||
|
const perAutoWidth = autoColumns.length > 0
|
||||||
|
? Math.floor(remainingWidth / autoColumns.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 5. 映射用户列
|
||||||
|
const userColumns = props.columns.map((col: any) => {
|
||||||
|
const isAuto = !col.width;
|
||||||
|
const finalWidth = isAuto ? Math.max(perAutoWidth, 100) : Number(col.width);
|
||||||
|
return {
|
||||||
|
key: col.prop,
|
||||||
|
dataKey: col.prop,
|
||||||
|
title: col.label,
|
||||||
|
width: finalWidth,
|
||||||
|
flexGrow: isAuto ? 1 : 0,
|
||||||
|
flexShrink: 1,
|
||||||
|
fixed: col.fixed,
|
||||||
|
align: col.align || "left",
|
||||||
|
cellRenderer: (scope: any) => {
|
||||||
|
// ... 此处保留你原有的 cellRenderer 逻辑
|
||||||
|
if (typeof col.render === "function") return col.render(scope);
|
||||||
|
if (col.valueType && RenderFactory[col.valueType]) {
|
||||||
|
return RenderFactory[col.valueType](scope, col);
|
||||||
|
}
|
||||||
|
return <span class="v2-cell-text">{scope.rowData[col.prop] ?? "-"}</span>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 如果开启了多选,在数组头部注入多选列
|
||||||
|
if (hasSelection) {
|
||||||
|
const selectionColumn = {
|
||||||
|
key: props.selectionConfig?.key || "selection",
|
||||||
|
width: selectionWidth,
|
||||||
|
fixed: props.selectionConfig?.fixed ?? false,
|
||||||
|
align: props.selectionConfig?.align || "center",
|
||||||
|
// 表头:全选
|
||||||
|
headerCellRenderer: () => {
|
||||||
|
const isAllSelected = innerData.value.length > 0 && selectedKeys.value.size >= innerData.value.length;
|
||||||
|
const isIndeterminate = selectedKeys.value.size > 0 && !isAllSelected;
|
||||||
|
const onAllChange = (val: any) => {
|
||||||
|
isAllSelectedMode.value = val; // 记录全选模式
|
||||||
|
if (val) {
|
||||||
|
innerData.value.forEach(row => selectedKeys.value.add(row.id));
|
||||||
|
} else {
|
||||||
|
selectedKeys.value.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <SelectionCell value={isAllSelected} intermediate={isIndeterminate} onChange={onAllChange} />;
|
||||||
|
},
|
||||||
|
// 单元格:单选
|
||||||
|
cellRenderer: (scope: any) => {
|
||||||
|
const rowId = scope.rowData.id;
|
||||||
|
const checked = selectedKeys.value.has(rowId);
|
||||||
|
const onRowChange = (val: any) => {
|
||||||
|
if (val){
|
||||||
|
selectedKeys.value.add(rowId);
|
||||||
|
if (selectedKeys.value.size === innerData.value.length) isAllSelectedMode.value = true;
|
||||||
|
}else{
|
||||||
|
selectedKeys.value.delete(rowId);
|
||||||
|
isAllSelectedMode.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <SelectionCell value={checked} onChange={onRowChange} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return [selectionColumn, ...userColumns];
|
||||||
|
}
|
||||||
|
|
||||||
|
return userColumns;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 请求逻辑 ---
|
||||||
|
const fetchTableData = async (isReset = false) => {
|
||||||
|
if (loading.value) return;
|
||||||
|
if (!isReset && noMore.value) return;
|
||||||
|
|
||||||
|
if (isReset) {
|
||||||
|
pageNo.value = 1;
|
||||||
|
innerData.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
pageNo: pageNo.value,
|
||||||
|
pageSize: props.pageSize,
|
||||||
|
...props.initParam,
|
||||||
|
};
|
||||||
|
const res = await props.requestApi(params);
|
||||||
|
const records = res?.records || [];
|
||||||
|
|
||||||
|
// 联动模式 全选滚动加载数据默认添加到Set中
|
||||||
|
if (isAllSelectedMode.value) {
|
||||||
|
records.forEach((row: any) => {
|
||||||
|
if (row.id !== undefined) selectedKeys.value.add(row.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
innerData.value = isReset ? records : [...innerData.value, ...records];
|
||||||
|
total.value = res?.total || 0;
|
||||||
|
pageNo.value++;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 交互逻辑 ---
|
||||||
|
const updateSize = () => {
|
||||||
|
if (tableContainerRef.value) {
|
||||||
|
// 1. offsetWidth 包含 border 和 padding,是最准确的外盒宽度
|
||||||
|
const containerWidth = tableContainerRef.value.offsetWidth;
|
||||||
|
|
||||||
|
// 不要减 2,让表格填满
|
||||||
|
tableSize.value.width = containerWidth;
|
||||||
|
|
||||||
|
const rect = tableContainerRef.value.getBoundingClientRect();
|
||||||
|
const remainingHeight = window.innerHeight - rect.top - 24;
|
||||||
|
tableSize.value.height = Math.max(remainingHeight, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowsRendered = ({ endRowIndex }: any) => {
|
||||||
|
// 阈值调大一点(10),保证滚动流畅,用户无感知加载
|
||||||
|
if (endRowIndex >= innerData.value.length - 10) {
|
||||||
|
fetchTableData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = debounce(updateSize, 200);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateSize();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
fetchTableData(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => window.removeEventListener("resize", handleResize));
|
||||||
|
|
||||||
|
// 暴露 API
|
||||||
|
defineExpose({
|
||||||
|
refresh: () => fetchTableData(true),
|
||||||
|
updateRow,
|
||||||
|
removeRow,
|
||||||
|
addRow,
|
||||||
|
updateSize,
|
||||||
|
getSelection: () => getSelectedRows(),
|
||||||
|
clearSelection: () => {
|
||||||
|
isAllSelectedMode.value = false;
|
||||||
|
selectedKeys.value.clear();
|
||||||
|
},
|
||||||
|
getCurrentParams: () => ({
|
||||||
|
pageNo: pageNo.value - 1, // 因为 fetch 完后 pageNo 会自增,所以要减 1 才是当前页
|
||||||
|
pageSize: props.pageSize,
|
||||||
|
...props.initParam,
|
||||||
|
}),
|
||||||
|
innerData,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pro-table-v2-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* 移除外边框,让样式更融入页面 */
|
||||||
|
.el-table-v2 {
|
||||||
|
box-shadow: 0 0 6px #eef1f6;
|
||||||
|
}
|
||||||
|
// 1. 初始全屏 Loading
|
||||||
|
.v2-initial-loading {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
p {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 底部流式加载样式
|
||||||
|
.v2-infinite-footer {
|
||||||
|
padding: 20px 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
.is-loading {
|
||||||
|
animation: rotating 2s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
.line {
|
||||||
|
width: 30px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #dcdfe6;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 适配 el-table-v2 核心样式
|
||||||
|
:deep(.el-table-v2__main) {
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table-v2__header-wrapper) {
|
||||||
|
border-bottom: 1px solid #f2f3f5;
|
||||||
|
.el-table-v2__header-cell {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table-v2__row) {
|
||||||
|
border-bottom: 1px solid #f9fafb;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.operation-column-cell) {
|
||||||
|
// 默认隐藏内容,但保留位置
|
||||||
|
.v2-operation-btns {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
.el-button + .el-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 当鼠标移入行时,显示该行内的操作按钮
|
||||||
|
:deep(.el-table-v2__row:hover) {
|
||||||
|
background-color: #f5f7fa; // 顺便加个行高亮
|
||||||
|
|
||||||
|
.operation-column-cell .v2-operation-btns {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stage-breadcrumbs">
|
<div class="stage-breadcrumbs" :class="styleClass">
|
||||||
|
<slot name="title">
|
||||||
<div class="mj-panel-title">{{ title }}</div>
|
<div class="mj-panel-title">{{ title }}</div>
|
||||||
|
</slot>
|
||||||
<div class="stage-breadcrumbs-content">
|
<div class="stage-breadcrumbs-content">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,13 +14,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "StageBreadcrumbs" });
|
defineOptions({ name: "StageBreadcrumbs" });
|
||||||
|
|
||||||
const { title } = defineProps<{
|
const { title,styleClass,showTitle=true } = defineProps<{
|
||||||
title: string;
|
title?: string;
|
||||||
|
styleClass?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.stage-breadcrumbs{
|
.stage-breadcrumbs{
|
||||||
padding: $mj-padding-standard 0;
|
padding: 0 0 $mj-padding-standard 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-bottom: 1px solid #E2E8F099;
|
border-bottom: 1px solid #E2E8F099;
|
||||||
@@ -48,4 +51,8 @@ const { title } = defineProps<{
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stage-breadcrumbs-list{
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
<div class="mj-standard-menu">
|
<div class="mj-standard-menu">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeIndex"
|
:default-active="activeIndex"
|
||||||
:active-text-color="mode === 'horizontal' ? '#409EFF' : undefined"
|
:class="['mj-menu',menuClass]"
|
||||||
class="mj-menu"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:collapse="isCollapse"
|
:collapse="isCollapse"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
@@ -20,7 +19,7 @@
|
|||||||
<template #title>{{ row.meta.title }}</template>
|
<template #title>{{ row.meta.title }}</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
<el-menu-item v-else :index="item.path">
|
<el-menu-item v-else :index="getFirstChildPath(item)" :disabled="item.meta.disabled">
|
||||||
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
|
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
|
||||||
<template #title>{{ item.meta.title }}</template>
|
<template #title>{{ item.meta.title }}</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@@ -29,15 +28,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Location } from '@element-plus/icons-vue'
|
|
||||||
defineOptions({ name: "standardMenu" })
|
defineOptions({ name: "standardMenu" })
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const {mode="vertical",menuList,isCollapse,activeMenu} = defineProps<{
|
const {mode="vertical",menuList,isCollapse,activeMenu,menuClass} = defineProps<{
|
||||||
mode?: 'vertical' | 'horizontal'
|
mode?: 'vertical' | 'horizontal'
|
||||||
menuList: any[]
|
menuList: any[]
|
||||||
isCollapse?:boolean
|
isCollapse?:boolean
|
||||||
activeMenu?: string
|
activeMenu?: string
|
||||||
|
menuClass?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -69,6 +68,18 @@ const resolvePath = (parentPath: string, childPath: string) => {
|
|||||||
// 4. 返回拼接后的路径
|
// 4. 返回拼接后的路径
|
||||||
return `${parent}/${child}`;
|
return `${parent}/${child}`;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 获取菜单项的跳转路径
|
||||||
|
* 如果是顶级菜单且有子项,点击应跳转到第一个子项
|
||||||
|
*/
|
||||||
|
const getFirstChildPath = (item: any) => {
|
||||||
|
// 如果是顶级菜单且有子菜单
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
return resolvePath(item.path, item.children[0].path);
|
||||||
|
}
|
||||||
|
// 如果没有子菜单,直接返回 item.path,但要确保它是以 / 开头
|
||||||
|
return item.path.startsWith('/') ? item.path : `/${item.path}`;
|
||||||
|
};
|
||||||
|
|
||||||
// 处理菜单选中事件
|
// 处理菜单选中事件
|
||||||
const handleMenuSelect = (index: string) => {
|
const handleMenuSelect = (index: string) => {
|
||||||
@@ -101,7 +112,7 @@ const handleMenuSelect = (index: string) => {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 1000px; // 设置一个足够大的值
|
max-width: 1000px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-width 0.25s, opacity 0.25s, width 0.25s;
|
transition: max-width 0.25s, opacity 0.25s, width 0.25s;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -139,5 +150,43 @@ const handleMenuSelect = (index: string) => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 菜单样式类
|
||||||
|
.mj-top_menu{
|
||||||
|
--el-menu-hover-bg-color:transparent;
|
||||||
|
--el-menu-active-color:var(--el-color-primary);
|
||||||
|
--el-menu-text-color:#62748E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-aside_menu{
|
||||||
|
--el-menu-hover-bg-color:#F8FAFC;
|
||||||
|
--el-menu-item-height:46px;
|
||||||
|
--el-menu-item-padding:20px;
|
||||||
|
--el-menu-item-border-color:#DBEAFE;
|
||||||
|
--el-menu-item-active-color:#EFF6FF;
|
||||||
|
padding: 0 12px;
|
||||||
|
.el-menu-item{
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.el-menu-item.is-active{
|
||||||
|
background-color: var(--el-menu-item-active-color);
|
||||||
|
border: 1px solid var(--el-menu-item-border-color);
|
||||||
|
border-radius: var(--el-menu-item-padding);
|
||||||
|
}
|
||||||
|
.el-menu-item:hover{
|
||||||
|
border-radius: var(--el-menu-item-padding);
|
||||||
|
transition: transform .12s ease;
|
||||||
|
.el-icon{
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收缩菜单样式
|
||||||
|
.el-menu--collapse{
|
||||||
|
--el-menu-hover-bg-color:transparent;
|
||||||
|
--el-menu-item-border-color:transparent;
|
||||||
|
--el-menu-item-active-color:transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
17
src/components/userSelector/index.vue
Normal file
17
src/components/userSelector/index.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-selector-modules">
|
||||||
|
<name-avatar :name="name" :src="avatar" :size="size" style="margin-right: 12px" />
|
||||||
|
<span>{{ userAvatarName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import nameAvatar from "../nameAvatar/index.vue";
|
||||||
|
defineOptions({ name: "UserSelector" });
|
||||||
|
const props = defineProps({
|
||||||
|
userAvatarName: { type: String, default: "" },
|
||||||
|
avatar: { type: String, default: "" },
|
||||||
|
size: { type: Number, default: 24 },
|
||||||
|
name: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
110
src/components/viewSwitcher/index.vue
Normal file
110
src/components/viewSwitcher/index.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="view-switcher">
|
||||||
|
<div class="active-indicator" :style="indicatorStyle"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="switcher-item"
|
||||||
|
:class="{ active: modelValue === 'grid' }"
|
||||||
|
@click="toggleView('grid')"
|
||||||
|
>
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="switcher-item"
|
||||||
|
:class="{ active: modelValue === 'list' }"
|
||||||
|
@click="toggleView('list')"
|
||||||
|
>
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { Grid, List } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: "grid" | "list";
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
// 统一样式变量
|
||||||
|
const PADDING = 3; // 容器内边距
|
||||||
|
const ITEM_WIDTH = 32; // 每个图标按钮的宽度
|
||||||
|
|
||||||
|
const indicatorStyle = computed(() => {
|
||||||
|
const offset = props.modelValue === "grid" ? 0 : ITEM_WIDTH;
|
||||||
|
return {
|
||||||
|
width: `${ITEM_WIDTH}px`,
|
||||||
|
transform: `translateX(${offset}px)`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleView = (view: "grid" | "list") => {
|
||||||
|
emit("update:modelValue", view);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.view-switcher {
|
||||||
|
/* 核心高度控制 */
|
||||||
|
$total-height: 38px;
|
||||||
|
$padding: 3px;
|
||||||
|
$inner-height: $total-height - ($padding * 2); // 32px
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
height: $total-height;
|
||||||
|
padding: $padding;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.switcher-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px; // 保持正方形
|
||||||
|
height: $inner-height;
|
||||||
|
color: #606266;
|
||||||
|
transition: color 0.12s ease;
|
||||||
|
.el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
/* 核心:设置 SVG 的过渡效果 */
|
||||||
|
svg {
|
||||||
|
transition: stroke-width 0.12s ease, transform 0.12s ease;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 0; // 默认不加粗
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
|
||||||
|
.el-icon svg {
|
||||||
|
stroke-width: 1.5;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-indicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: $padding;
|
||||||
|
left: $padding;
|
||||||
|
height: $inner-height;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
// 后台 - 字典管理模块-字典内容信息
|
// 后台 - 字典管理模块-字典内容信息
|
||||||
|
|
||||||
|
|
||||||
// 字典状态映射
|
|
||||||
const statusDict = {
|
|
||||||
1: '正常',
|
|
||||||
0: '禁用'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字典状态颜色
|
// 字典状态颜色
|
||||||
const statusDictColor = {
|
const statusDictColor = {
|
||||||
1:'#66E5BE',
|
1:'#66E5BE',
|
||||||
0:'#90A1B9'
|
0:'#90A1B9'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置字典转换为目标格式
|
|
||||||
const statusOptions = Object.keys(statusDict).map((key) => {
|
|
||||||
return {
|
|
||||||
label: statusDict[key],
|
|
||||||
value: Number(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
statusDict,
|
|
||||||
statusDictColor,
|
statusDictColor,
|
||||||
statusOptions
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
import DictManage from './dictManage';
|
const modules = import.meta.glob('./*.{vue,ts,js}', { eager: true });
|
||||||
|
const components: Record<string, any> = {};
|
||||||
const Dict = { DictManage }
|
Object.entries(modules).forEach(([path, module]: [string, any]) => {
|
||||||
export { DictManage }
|
// 过滤掉 index 文件本身
|
||||||
export default Dict;
|
if (path.includes('index')) return;
|
||||||
|
const name = path.replace(/^\.\/(.*)\.\w+$/, '$1');
|
||||||
|
components[name] = module.default || module;
|
||||||
|
});
|
||||||
|
// 2. 统一导出
|
||||||
|
export const {
|
||||||
|
DictManage,
|
||||||
|
} = components;
|
||||||
|
export default components;
|
||||||
97
src/hooks/useActionMenu.ts
Normal file
97
src/hooks/useActionMenu.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { ref, reactive, nextTick } from 'vue';
|
||||||
|
|
||||||
|
export type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
||||||
|
|
||||||
|
export interface PopoverOptions {
|
||||||
|
placement?: Placement;
|
||||||
|
offsetX?: number;
|
||||||
|
offsetY?: number;
|
||||||
|
// 这里的 width 变成“最小预估宽度”,防止首次渲染跳动
|
||||||
|
minWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuPosition {
|
||||||
|
top: string;
|
||||||
|
left: string;
|
||||||
|
position: 'fixed';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUniversalPopover<T = any>(options: PopoverOptions = {}) {
|
||||||
|
const {
|
||||||
|
placement = 'bottom-end',
|
||||||
|
offsetX = 0,
|
||||||
|
offsetY = 8,
|
||||||
|
minWidth = 120
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const activeData = ref<T | null>(null);
|
||||||
|
|
||||||
|
const menuStyle = reactive<MenuPosition>({
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
position: 'fixed'
|
||||||
|
});
|
||||||
|
|
||||||
|
const openMenu = (e: MouseEvent, data: T) => {
|
||||||
|
// 切换逻辑:点击同一个则关闭
|
||||||
|
if (visible.value && activeData.value === data) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeData.value = data;
|
||||||
|
visible.value = true;
|
||||||
|
|
||||||
|
// 获取触发源位置
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// 1. 获取刚刚渲染出来的弹窗 DOM 实例
|
||||||
|
// 注意:这里需要确保你的 ActionMenu 组件根类名一致
|
||||||
|
const menuEl = document.querySelector('.mj-universal-menu') as HTMLElement;
|
||||||
|
if (!menuEl) return;
|
||||||
|
|
||||||
|
const realWidth = menuEl.offsetWidth || minWidth;
|
||||||
|
const realHeight = menuEl.offsetHeight || 0;
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
|
||||||
|
let top = 0;
|
||||||
|
let left = 0;
|
||||||
|
|
||||||
|
// 2. 根据方位计算坐标
|
||||||
|
if (placement.includes('bottom')) {
|
||||||
|
top = rect.bottom + offsetY;
|
||||||
|
} else if (placement.includes('top')) {
|
||||||
|
top = rect.top - realHeight - offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placement.includes('end')) {
|
||||||
|
// 右对齐:触发源右侧 - 弹窗宽度
|
||||||
|
left = rect.right - realWidth + offsetX;
|
||||||
|
} else {
|
||||||
|
// 左对齐
|
||||||
|
left = rect.left + offsetX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 边界检测:防止超出屏幕
|
||||||
|
// 左边界
|
||||||
|
if (left < 10) left = 10;
|
||||||
|
// 右边界
|
||||||
|
if (left + realWidth > screenWidth - 10) {
|
||||||
|
left = screenWidth - realWidth - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuStyle.top = `${top}px`;
|
||||||
|
menuStyle.left = `${left}px`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
visible.value = false;
|
||||||
|
activeData.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { visible, activeData, menuStyle, openMenu, closeMenu };
|
||||||
|
}
|
||||||
79
src/hooks/useDictData.ts
Normal file
79
src/hooks/useDictData.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { getDictMap,getDictMapLevel } from '@/api';
|
||||||
|
|
||||||
|
// 全局静态缓存,跨组件共享
|
||||||
|
const dictCache = reactive({});
|
||||||
|
const pendingPromises = new Map();
|
||||||
|
|
||||||
|
export function useDict(codes) {
|
||||||
|
const dicts = ref({});
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const fetchDicts = async () => {
|
||||||
|
if (!codes) return;
|
||||||
|
|
||||||
|
const codeArray = codes.split(',').map(s => s.trim());
|
||||||
|
const result = {};
|
||||||
|
const needFetch = [];
|
||||||
|
|
||||||
|
// 1. 区分哪些在缓存,哪些需要查
|
||||||
|
codeArray.forEach(code => {
|
||||||
|
if (dictCache[code]) {
|
||||||
|
result[code] = dictCache[code];
|
||||||
|
} else {
|
||||||
|
needFetch.push(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needFetch.length === 0) {
|
||||||
|
dicts.value = result;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 处理并发合并
|
||||||
|
const fetchKey = needFetch.sort().join(',');
|
||||||
|
if (!pendingPromises.has(fetchKey)) {
|
||||||
|
const p = getDictMap(codes).then(res => {
|
||||||
|
const data = res || [];
|
||||||
|
Object.assign(dictCache, data);
|
||||||
|
pendingPromises.delete(fetchKey);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
pendingPromises.set(fetchKey, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const remoteData = await pendingPromises.get(fetchKey);
|
||||||
|
dicts.value = { ...result, ...remoteData };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLevel = async (code, parentId) => {
|
||||||
|
if (!code || parentId === undefined) return [];
|
||||||
|
|
||||||
|
// 构造层级缓存 Key:例如 "GENDER_1"
|
||||||
|
const cacheKey = `${code}_${parentId}`;
|
||||||
|
|
||||||
|
if (dictCache[cacheKey]) {
|
||||||
|
return dictCache[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 假设 getDictMapLevel 接受 code 和 parentId
|
||||||
|
const res = await getDictMapLevel(code, parentId);
|
||||||
|
const data = res || [];
|
||||||
|
// 写入全局缓存
|
||||||
|
dictCache[cacheKey] = data;
|
||||||
|
return data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(fetchDicts);
|
||||||
|
return { dicts, loading,refresh: fetchDicts,fetchLevel };
|
||||||
|
}
|
||||||
45
src/hooks/useLocalManager.ts
Normal file
45
src/hooks/useLocalManager.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ref, shallowRef, computed, watch } from 'vue';
|
||||||
|
|
||||||
|
export function useLocalManager<T>(initialDataGetter: () => T[], pageSize = 20) {
|
||||||
|
// 1. 全量数据池
|
||||||
|
const fullData = shallowRef<T[]>([]);
|
||||||
|
|
||||||
|
// 2. 内部展示条数
|
||||||
|
const displayCount = ref(pageSize);
|
||||||
|
|
||||||
|
// 3. 监听初始数据的变化(处理后端异步返回)
|
||||||
|
watch(initialDataGetter, (newVal) => {
|
||||||
|
if (Array.isArray(newVal)) {
|
||||||
|
// 全量去重,确保内部池子纯净
|
||||||
|
const map = new Map();
|
||||||
|
newVal.forEach(item => { if((item as any).id) map.set((item as any).id, item) });
|
||||||
|
const uniqueData = Array.from(map.values()) as T[];
|
||||||
|
|
||||||
|
fullData.value = uniqueData;
|
||||||
|
// 如果数据被删减到小于当前水位,重置水位
|
||||||
|
if (uniqueData.length < displayCount.value) {
|
||||||
|
displayCount.value = Math.max(pageSize, uniqueData.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true,deep:true });
|
||||||
|
|
||||||
|
// 4. 切片数据(UI 渲染对象)
|
||||||
|
const displayData = computed(() => {
|
||||||
|
return fullData.value.slice(0, displayCount.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const noMore = computed(() => displayCount.value >= fullData.value.length);
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const loadMore = () => {
|
||||||
|
if (noMore.value) return;
|
||||||
|
displayCount.value += pageSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayData,
|
||||||
|
displayCount,
|
||||||
|
loadMore,
|
||||||
|
noMore,
|
||||||
|
};
|
||||||
|
}
|
||||||
52
src/hooks/useRelativeTime.ts
Normal file
52
src/hooks/useRelativeTime.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// useRelativeTime.ts
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import 'dayjs/locale/zh-cn' // 引入中文语言包
|
||||||
|
|
||||||
|
// 初始化插件和语言
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.locale('zh-cn')
|
||||||
|
|
||||||
|
export function useRelativeTime() {
|
||||||
|
/**
|
||||||
|
* 核心转换函数
|
||||||
|
* @param value 时间戳、Date对象或ISO字符串
|
||||||
|
*/
|
||||||
|
const formatTime = (value: string | number | Date): string => {
|
||||||
|
if (!value) return ''
|
||||||
|
|
||||||
|
const target = dayjs(value)
|
||||||
|
const now = dayjs()
|
||||||
|
const diffSeconds = now.diff(target, 'second')
|
||||||
|
const diffDays = now.diff(target, 'day')
|
||||||
|
|
||||||
|
// 1. 极短时间内(60秒内)显示“刚刚”
|
||||||
|
if (diffSeconds < 60) {
|
||||||
|
return '刚刚'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 一天以内(显示 10分钟前, 1小时前等)
|
||||||
|
if (diffDays < 1) {
|
||||||
|
return target.fromNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 超过一天但在一年以内(显示 10-27)
|
||||||
|
if (diffDays < 365) {
|
||||||
|
return target.format('MM-DD HH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 超过一年(显示 2023-10-27)
|
||||||
|
return target.format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果你需要判断“是否超过可删除时间”(比如5分钟内可删除)
|
||||||
|
const canAction = (value: string | number | Date, limitMinutes: number = 5) => {
|
||||||
|
return dayjs().diff(dayjs(value), 'minute') < limitMinutes
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dayjs, // 同时也暴露原生的 dayjs 方便外部灵活使用
|
||||||
|
formatTime,
|
||||||
|
canAction
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/hooks/useSelectLoadMore.ts
Normal file
77
src/hooks/useSelectLoadMore.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { reactive, toRefs } from "vue";
|
||||||
|
import { debounce } from "lodash-es";
|
||||||
|
|
||||||
|
export function useSelectLoadMore(config: any) {
|
||||||
|
const { fetchApi, pageSize = 10, delay = 300 } = config;
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
options: [] as any[],
|
||||||
|
remoteLoading: false,
|
||||||
|
loadMore: false,
|
||||||
|
noMore: false,
|
||||||
|
pageNo: 1,
|
||||||
|
query: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const executeFetch = async (isRefresh: boolean) => {
|
||||||
|
// 拦截逻辑
|
||||||
|
if (!isRefresh && (state.loadMore || state.noMore)) return;
|
||||||
|
if (isRefresh) {
|
||||||
|
state.pageNo = 1;
|
||||||
|
state.noMore = false;
|
||||||
|
state.remoteLoading = true;
|
||||||
|
} else {
|
||||||
|
state.loadMore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchApi({
|
||||||
|
pageNo: state.pageNo,
|
||||||
|
pageSize: pageSize,
|
||||||
|
keyword: state.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newList = res || [];
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
state.options = newList;
|
||||||
|
} else {
|
||||||
|
// 滚动加载:追加数据,不置空,不触发 select 的 loading
|
||||||
|
state.options.push(...newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 探测 noMore
|
||||||
|
if (newList.length < pageSize) {
|
||||||
|
state.noMore = true;
|
||||||
|
} else {
|
||||||
|
state.pageNo++;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 请求结束,关闭所有状态
|
||||||
|
state.remoteLoading = false;
|
||||||
|
state.loadMore = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteMethod = debounce(async (query: string) => {
|
||||||
|
state.query = query;
|
||||||
|
await executeFetch(true);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
const handleLoadMore = async () => {
|
||||||
|
await executeFetch(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVisibleChange = async (visible: boolean) => {
|
||||||
|
if (visible && state.options.length === 0) {
|
||||||
|
await executeFetch(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
remoteMethod,
|
||||||
|
handleLoadMore,
|
||||||
|
onVisibleChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
68
src/hooks/useTableAction.ts
Normal file
68
src/hooks/useTableAction.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tableRef ProTableV2 的组件实例引用
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ActionOptions {
|
||||||
|
showMsg?: boolean; // 是否显示操作成功提示
|
||||||
|
}
|
||||||
|
export const useTableAction = (tableRef: any) => {
|
||||||
|
/**
|
||||||
|
* 执行操作并静默更新单行数据
|
||||||
|
* @param apiPromise 调用的操作接口(如状态切换、编辑提交)
|
||||||
|
* @param id 当前行 ID
|
||||||
|
* @param fetchDataApi 可选:列表请求函数,若传入则在成功后自动拉取最新行数据
|
||||||
|
*/
|
||||||
|
const handleAction = async (
|
||||||
|
api: Promise<any> | (() => Promise<any>),
|
||||||
|
id?: string | number,
|
||||||
|
fetchDataApi?: Function,
|
||||||
|
options = { showMsg: true }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const res = typeof api === 'function' ? await api() : await api;
|
||||||
|
if (options.showMsg) ElMessage.success("操作成功");
|
||||||
|
|
||||||
|
// 局部更新逻辑... (同上)
|
||||||
|
if (id && fetchDataApi) {
|
||||||
|
const currentParams = tableRef.value?.getCurrentParams();
|
||||||
|
const listRes = await fetchDataApi(currentParams);
|
||||||
|
const records = listRes?.records || listRes?.data?.records || [];
|
||||||
|
const latestRowData = records.find((item: any) => item.id == id);
|
||||||
|
if (latestRowData) tableRef.value.updateRow(id, latestRowData);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error; // 抛出错误让外部 catch
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装删除逻辑
|
||||||
|
* @param id 行 ID
|
||||||
|
* @param deleteApi 删除接口函数
|
||||||
|
*/
|
||||||
|
const handleDelete = async (
|
||||||
|
deleteApi: (id: any) => Promise<any>,
|
||||||
|
...args: any[]
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const res = await deleteApi(...args);
|
||||||
|
const id = args[0];
|
||||||
|
if(id){
|
||||||
|
tableRef.value?.removeRow(id);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获取消行为或接口错误
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleAction,
|
||||||
|
handleDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
// useTokenRefresh.ts
|
|
||||||
import { ref } from "vue";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
interface TokenData {
|
|
||||||
accessToken: string; // token内容
|
|
||||||
refreshToken: string; // 刷新的token内容
|
|
||||||
expiresAt?: number; // 过期时间戳
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useTokenRefresh(baseUrl: string) {
|
|
||||||
let refreshPromise: Promise<TokenData> | null = null;
|
|
||||||
let lastRefreshError: any = null;
|
|
||||||
// 当前Token数据(使用localStorage持久化) 可根据实际情况改成localStorage存储
|
|
||||||
const tokenData = ref<TokenData>({
|
|
||||||
accessToken: localStorage.getItem("accessToken") || "",
|
|
||||||
refreshToken: localStorage.getItem("refreshToken") || "",
|
|
||||||
expiresAt: Number(localStorage.getItem("expiresAt")) || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 核心刷新方法
|
|
||||||
const refreshToken = async (): Promise<TokenData> => {
|
|
||||||
if (refreshPromise) return refreshPromise;
|
|
||||||
|
|
||||||
// 如果上次刷新失败,短时间内不重复刷新,直接抛错
|
|
||||||
if (lastRefreshError) {
|
|
||||||
return Promise.reject(lastRefreshError);
|
|
||||||
}
|
|
||||||
const plainClient = axios.create({
|
|
||||||
baseURL: baseUrl,
|
|
||||||
timeout: 5 * 1000,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json;charset=utf-8",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const refreshToken = getRefreshToken();
|
|
||||||
if (!refreshToken) throw new Error("refreshToken is null");
|
|
||||||
const res = await plainClient.post("/auth/refresh", {
|
|
||||||
refreshToken,
|
|
||||||
});
|
|
||||||
const newTokenData: TokenData = {
|
|
||||||
accessToken: res.data.accessToken,
|
|
||||||
refreshToken: res.data.refreshToken,
|
|
||||||
expiresAt: Date.now() + res.data.expiresIn * 1000,
|
|
||||||
};
|
|
||||||
setTokens(newTokenData);
|
|
||||||
lastRefreshError = null; // 成功清除错误
|
|
||||||
return newTokenData;
|
|
||||||
} catch (error) {
|
|
||||||
// 刷新失败处理
|
|
||||||
lastRefreshError = error;
|
|
||||||
clearTokens();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
refreshPromise = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return refreshPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存token
|
|
||||||
const setTokens = (data: TokenData) => {
|
|
||||||
tokenData.value = data;
|
|
||||||
localStorage.setItem("accessToken", data.accessToken);
|
|
||||||
localStorage.setItem("refreshToken", data.refreshToken);
|
|
||||||
localStorage.setItem("expiresAt", data.expiresAt.toString());
|
|
||||||
};
|
|
||||||
// 清理Token
|
|
||||||
const clearTokens = () => {
|
|
||||||
localStorage.removeItem("accessToken");
|
|
||||||
localStorage.removeItem("refreshToken");
|
|
||||||
localStorage.removeItem("expiresAt");
|
|
||||||
tokenData.value = { accessToken: "", refreshToken: "", expiresAt: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取当前Token
|
|
||||||
const getAccessToken = () => tokenData.value.accessToken;
|
|
||||||
// 获取refreshToken
|
|
||||||
const getRefreshToken = () => tokenData.value.refreshToken;
|
|
||||||
return {
|
|
||||||
refreshToken,
|
|
||||||
getRefreshToken,
|
|
||||||
setTokens,
|
|
||||||
getAccessToken,
|
|
||||||
clearTokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -8,16 +8,13 @@ import zhCn from "element-plus/es/locale/lang/zh-cn";
|
|||||||
import en from "element-plus/es/locale/lang/en";
|
import en from "element-plus/es/locale/lang/en";
|
||||||
import Directives from '@/utils/directives';
|
import Directives from '@/utils/directives';
|
||||||
import '@/styles/common.scss';
|
import '@/styles/common.scss';
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
// 全局导入element ui样式类
|
// 全局导入element ui样式类
|
||||||
import 'element-plus/es/components/message/style/css'
|
import 'element-plus/es/components/message/style/css'
|
||||||
import 'element-plus/es/components/notification/style/css'
|
import 'element-plus/es/components/notification/style/css'
|
||||||
import 'element-plus/es/components/message-box/style/css'
|
import 'element-plus/es/components/message-box/style/css'
|
||||||
import 'element-plus/es/components/loading/style/css'
|
import 'element-plus/es/components/loading/style/css'
|
||||||
|
|
||||||
const pinia = createPinia();
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
// 导入全局的i18n文件
|
// 导入全局的i18n文件
|
||||||
const loadLocalMessages = async () => {
|
const loadLocalMessages = async () => {
|
||||||
const messages: Record<string, any> = {};
|
const messages: Record<string, any> = {};
|
||||||
@@ -55,7 +52,9 @@ const getLocalLang = () => {
|
|||||||
const initApp = async () => {
|
const initApp = async () => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
// 加载语言消息
|
// 加载语言消息
|
||||||
const messages = await loadLocalMessages();
|
const messages = await loadLocalMessages();
|
||||||
const elementLocale = getLocalLang() === "zh" ? zhCn : en;
|
const elementLocale = getLocalLang() === "zh" ? zhCn : en;
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 登录相关 Mock 数据
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock 登录响应
|
|
||||||
*/
|
|
||||||
export const getMockLoginResponse = (username: string, password: string) => {
|
|
||||||
// 模拟登录验证
|
|
||||||
if (username && password) {
|
|
||||||
return {
|
|
||||||
code: 0,
|
|
||||||
data: {
|
|
||||||
accessToken: 'mock_access_token_' + Date.now(),
|
|
||||||
refreshToken: 'mock_refresh_token_' + Date.now(),
|
|
||||||
expiresIn: 7200, // 2小时
|
|
||||||
userInfo: {
|
|
||||||
id: 1,
|
|
||||||
username: username,
|
|
||||||
name: username === 'admin' ? '管理员' : '普通用户',
|
|
||||||
role: username === 'admin' ? 'admin' : 'user',
|
|
||||||
avatar: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
msg: '登录成功',
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
code: 400,
|
|
||||||
data: null,
|
|
||||||
msg: '用户名或密码不能为空',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock 用户信息响应
|
|
||||||
*/
|
|
||||||
export const getMockUserInfoResponse = () => {
|
|
||||||
return {
|
|
||||||
code: 0,
|
|
||||||
data: {
|
|
||||||
id: 1,
|
|
||||||
username: 'admin',
|
|
||||||
name: '管理员',
|
|
||||||
role: 'admin',
|
|
||||||
avatar: '',
|
|
||||||
},
|
|
||||||
msg: '获取用户信息成功',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mock 数据管理
|
|
||||||
* 用于开发阶段模拟后端接口返回的数据
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getMockMenuResponse } from './menu'
|
|
||||||
import { getMockLoginResponse, getMockUserInfoResponse } from './auth'
|
|
||||||
|
|
||||||
// 是否启用 Mock(可以通过环境变量控制)
|
|
||||||
export const ENABLE_MOCK = import.meta.env.VITE_USE_MOCK === 'true' || import.meta.env.DEV
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock API 响应映射
|
|
||||||
* key: API 路径
|
|
||||||
* value: 返回的 Mock 数据函数
|
|
||||||
*/
|
|
||||||
const mockApiMap: Record<string, (params?: any, data?: any) => any> = {
|
|
||||||
'/api/menus': () => getMockMenuResponse(),
|
|
||||||
'/api/auth/login': (_params?: any, data?: any) => {
|
|
||||||
return getMockLoginResponse(data?.username || '', data?.password || '')
|
|
||||||
},
|
|
||||||
'/api/user/info': () => getMockUserInfoResponse(),
|
|
||||||
// 可以在这里添加更多的 mock 接口
|
|
||||||
// '/api/user/list': getMockUserList,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Mock 数据
|
|
||||||
* @param url API 路径
|
|
||||||
* @param params 请求参数(GET)
|
|
||||||
* @param data 请求体(POST/PUT)
|
|
||||||
* @returns Mock 数据或 null
|
|
||||||
*/
|
|
||||||
export const getMockData = (url: string, params?: any, data?: any): any => {
|
|
||||||
if (!ENABLE_MOCK || !url) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 精确匹配
|
|
||||||
if (mockApiMap[url]) {
|
|
||||||
return mockApiMap[url](params, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模糊匹配(支持带查询参数的 URL)
|
|
||||||
const urlWithoutQuery = url.split('?')[0]
|
|
||||||
if (urlWithoutQuery && mockApiMap[urlWithoutQuery]) {
|
|
||||||
return mockApiMap[urlWithoutQuery](params, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否应该使用 Mock 数据
|
|
||||||
* @param url API 路径
|
|
||||||
* @returns 是否应该使用 Mock
|
|
||||||
*/
|
|
||||||
export const shouldUseMock = (url: string): boolean => {
|
|
||||||
if (!ENABLE_MOCK || !url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 精确匹配
|
|
||||||
if (mockApiMap[url]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模糊匹配(支持带查询参数的 URL)
|
|
||||||
const urlWithoutQuery = url.split('?')[0]
|
|
||||||
return !!(urlWithoutQuery && mockApiMap[urlWithoutQuery])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加 Mock 接口
|
|
||||||
* @param url API 路径
|
|
||||||
* @param mockFn Mock 数据函数
|
|
||||||
*/
|
|
||||||
export const addMockApi = (url: string, mockFn: (params?: any, data?: any) => any) => {
|
|
||||||
mockApiMap[url] = mockFn
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除 Mock 接口
|
|
||||||
* @param url API 路径
|
|
||||||
*/
|
|
||||||
export const removeMockApi = (url: string) => {
|
|
||||||
delete mockApiMap[url]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getMockData,
|
|
||||||
shouldUseMock,
|
|
||||||
addMockApi,
|
|
||||||
removeMockApi,
|
|
||||||
ENABLE_MOCK,
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -296,6 +296,35 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const mockBackendMenuData = [
|
||||||
|
{
|
||||||
|
"name": "字典管理",
|
||||||
|
"code": "dict",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "组织管理",
|
||||||
|
"code": "origanization",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "人员管理",
|
||||||
|
"code": "personnel",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "权限管理",
|
||||||
|
"code": "permission",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流程管理",
|
||||||
|
"code": "flow",
|
||||||
|
"icon": "OfficeBuilding",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Mock 菜单数据的响应格式
|
* 获取 Mock 菜单数据的响应格式
|
||||||
* 模拟后端接口返回的数据结构
|
* 模拟后端接口返回的数据结构
|
||||||
|
|||||||
266
src/modules/Comment/index.scss
Normal file
266
src/modules/Comment/index.scss
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
$color-blue: #409eff;
|
||||||
|
$color-blue-bg: #f5f8ff;
|
||||||
|
$color-text-main: #303133;
|
||||||
|
$color-text-sub: #99a2aa;
|
||||||
|
$color-border: #e4e7ed;
|
||||||
|
$color-white: #fff;
|
||||||
|
|
||||||
|
.comment-app {
|
||||||
|
font-family: -apple-system, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// 评论的样式
|
||||||
|
.main-publisher{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
.input-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid $color-border;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
position: relative;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
background-color: $color-white;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: $color-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.left-icons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
.el-button {
|
||||||
|
color: $color-text-sub;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.el-button + .el-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: $color-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 评论列表样式
|
||||||
|
.comment-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 0 0 calc(100% - 120px);
|
||||||
|
|
||||||
|
.comment-group {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
.parent-node {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
background-color: $color-blue;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-main {
|
||||||
|
flex: 1;
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
.nickname {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color-text-main;
|
||||||
|
}
|
||||||
|
.createTime {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $color-text-sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: $color-text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.el-button {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $color-text-sub;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
&:hover {
|
||||||
|
color: $color-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.comment-loading-status{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 子评论卡片样式
|
||||||
|
.sub-container {
|
||||||
|
background-color: $color-blue-bg;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid #d9e5ff;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.sub-node {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-main {
|
||||||
|
flex: 1;
|
||||||
|
.sub-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.sub-user-info {
|
||||||
|
font-size: 14px;
|
||||||
|
.nickname {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.reply-text {
|
||||||
|
margin: 0 6px;
|
||||||
|
color: $color-text-sub;
|
||||||
|
}
|
||||||
|
.target-name {
|
||||||
|
color: $color-blue;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.createTime {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $color-text-sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content-body {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-text-main;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展开收起样式
|
||||||
|
.sub-list-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 46px; // 与头像对齐的偏移量
|
||||||
|
|
||||||
|
.expand-line {
|
||||||
|
width: 20px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #dcdfe6;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $color-text-sub;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-left: 4px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.mention-highlight) {
|
||||||
|
background-color: rgba(64, 158, 255, 0.1);
|
||||||
|
color: #409eff;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(64, 158, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.observer-anchor{
|
||||||
|
height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论组件骨架屏
|
||||||
|
.skeleton-comment-parent-node {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-node-main {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.skeleton-user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-actions {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
104
src/modules/Comment/useUserSearch.ts
Normal file
104
src/modules/Comment/useUserSearch.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { ref, shallowRef } from "vue";
|
||||||
|
import { debounce } from "lodash-es";
|
||||||
|
|
||||||
|
interface SearchOptions {
|
||||||
|
debounceMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserSearch(
|
||||||
|
apiFn: (keyword: string, signal: AbortSignal) => Promise<any[]>,
|
||||||
|
options: SearchOptions = {}
|
||||||
|
) {
|
||||||
|
const { debounceMs = 300 } = options;
|
||||||
|
|
||||||
|
const filteredUsers = ref<any[]>([]);
|
||||||
|
const selectedUsersCache = new Map<string, any[]>();
|
||||||
|
const searching = ref(false);
|
||||||
|
|
||||||
|
// 使用 shallowRef 存储 Controller,因为它不需要深度响应
|
||||||
|
const abortController = shallowRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
// 存储用户选中的缓存
|
||||||
|
const recordSelection = (user: any) => {
|
||||||
|
if (user && user.name) {
|
||||||
|
// 依然按名存数组,支持同名
|
||||||
|
const list = selectedUsersCache.get(user.name) || [];
|
||||||
|
if (!list.find((u) => u.id === user.id)) {
|
||||||
|
list.push(user);
|
||||||
|
selectedUsersCache.set(user.name, [...list]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户已选中的用户组
|
||||||
|
const getSelectedUsers = () => Array.from(selectedUsersCache.values()).flat();
|
||||||
|
|
||||||
|
// 清除用户选中的缓存
|
||||||
|
const clearSelection = () => {
|
||||||
|
selectedUsersCache.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从缓存池中查找对应的数据
|
||||||
|
const getCacheByName = (name) => {
|
||||||
|
const list = selectedUsersCache.get(name);
|
||||||
|
return list ? [...list] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真正的请求逻辑
|
||||||
|
*/
|
||||||
|
const performSearch = async (keyword: string) => {
|
||||||
|
// 1. 中断之前的请求
|
||||||
|
if (abortController.value) {
|
||||||
|
abortController.value.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建新的控制实例
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. 执行外部传入的 API
|
||||||
|
const res = await apiFn(keyword, abortController.value.signal);
|
||||||
|
filteredUsers.value = res;
|
||||||
|
} catch (err: any) {
|
||||||
|
// 仅处理非取消类的错误
|
||||||
|
if (err.name !== "AbortError" && err.message !== "canceled") {
|
||||||
|
console.error("Search API Error:", err);
|
||||||
|
filteredUsers.value = [];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 4. 如果当前请求没有被中断,则关闭 loading
|
||||||
|
if (!abortController.value?.signal.aborted) {
|
||||||
|
searching.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. 创建防抖函数
|
||||||
|
const debouncedSearch = debounce(performSearch, debounceMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暴露给外部调用的入口
|
||||||
|
*/
|
||||||
|
const search = (keyword: string) => {
|
||||||
|
// 如果输入框里没这个 @名字 了,从 selectedUsersCache 删掉它
|
||||||
|
for (const name of selectedUsersCache.keys()) {
|
||||||
|
if (!mainInput.value.includes(`@${name}`)) {
|
||||||
|
selectedUsersCache.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searching.value = true;
|
||||||
|
debouncedSearch(keyword);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filteredUsers,
|
||||||
|
searching,
|
||||||
|
selectedUsersCache,
|
||||||
|
recordSelection,
|
||||||
|
clearSelection,
|
||||||
|
getSelectedUsers,
|
||||||
|
getCacheByName,
|
||||||
|
search,
|
||||||
|
};
|
||||||
|
}
|
||||||
30
src/modules/Comment/utils.ts
Normal file
30
src/modules/Comment/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* @param {string} text 原始文本
|
||||||
|
* @param {string[]} atUsers 这条评论真正圈到的人员列表
|
||||||
|
*/
|
||||||
|
type atUserProps = {
|
||||||
|
id:string|number;
|
||||||
|
name?:string;
|
||||||
|
}
|
||||||
|
export const parseMention = (text: string, atUsers: atUserProps[]) => {
|
||||||
|
if (!text) return "";
|
||||||
|
if (!atUsers || atUsers.length === 0) {
|
||||||
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
// 只循环这条评论里【真正圈到】的人
|
||||||
|
const sortedMentions = [...atUsers].sort((a, b) => b.start - a.start);
|
||||||
|
let result = text;
|
||||||
|
sortedMentions.forEach((m) => {
|
||||||
|
const prefix = result.slice(0, m.start);
|
||||||
|
const suffix = result.slice(m.end);
|
||||||
|
const rawMentionText = result.slice(m.start, m.end);
|
||||||
|
const cleanName = rawMentionText.trim().replace(/^@+/, "");
|
||||||
|
const displayName = `@${cleanName}`;
|
||||||
|
const safeDisplayName = displayName.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
const highlight = `<span class="mention-highlight" data-user-id="${m.userId}">${safeDisplayName}</span>`;
|
||||||
|
|
||||||
|
result = prefix + highlight + suffix;
|
||||||
|
});
|
||||||
|
return result.replace(/\n/g, '<br>');
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
{{ topTitle }}
|
{{ topTitle }}
|
||||||
</div>
|
</div>
|
||||||
<standardMenu
|
<standardMenu
|
||||||
class="mj-aside_menu"
|
menu-class="mj-aside_menu"
|
||||||
:isCollapse="isCollapse"
|
:isCollapse="isCollapse"
|
||||||
:menuList="sideMenuList"
|
:menuList="sideMenuList"
|
||||||
:active-menu="selectedActiveMenu"
|
:active-menu="selectedActiveMenu"
|
||||||
@@ -21,24 +21,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 展开收缩左侧菜单按钮 -->
|
<!-- 展开收缩左侧菜单按钮 -->
|
||||||
<div class="mj-collapse" @click="showCollapse">
|
<div class="mj-collapse" @click="showCollapse">
|
||||||
<el-icon><component :is="isCollapse ? DArrowRight : DArrowLeft" /></el-icon>
|
<el-icon><component :is="isCollapse ? 'DArrowRight' : 'DArrowLeft'" /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header class="mj-header-content">
|
<el-header class="mj-header-content">
|
||||||
<!-- 左侧的菜单展示 -->
|
<!-- 顶部左侧的菜单展示 -->
|
||||||
<standardMenu
|
<standardMenu
|
||||||
|
menu-class="mj-top_menu"
|
||||||
:menuList="topLevelMenuList"
|
:menuList="topLevelMenuList"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:active-menu="selectedTopMenu"
|
:active-menu="selectedTopMenu"
|
||||||
@menu-select="handleTopMenuSelect"
|
@menu-select="handleTopMenuSelect"
|
||||||
/>
|
/>
|
||||||
<!-- 右侧用户的内容 -->
|
<!-- 顶部右侧用户的内容 -->
|
||||||
<rightMenuGroup @on-stage-manage="onStageManage" />
|
<rightMenuGroup @on-stage-manage="(path)=>handleTopMenuSelect(path)" />
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main class="mj-main-backend-content">
|
||||||
<!-- <card-item :list="[1,2,3,4,5,6]"/> -->
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
@@ -48,7 +48,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import standardMenu from "@/components/standardMenu/index.vue";
|
import standardMenu from "@/components/standardMenu/index.vue";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import { DArrowLeft, DArrowRight } from "@element-plus/icons-vue";
|
|
||||||
import rightMenuGroup from './rightMenuGroup.vue';
|
import rightMenuGroup from './rightMenuGroup.vue';
|
||||||
import companyLogo from '@/assets/images/logo.png';
|
import companyLogo from '@/assets/images/logo.png';
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -57,6 +56,7 @@ defineOptions({
|
|||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// 响应式断点(小屏阈值,小于此值视为小屏)
|
// 响应式断点(小屏阈值,小于此值视为小屏)
|
||||||
const BREAKPOINT = 1024;
|
const BREAKPOINT = 1024;
|
||||||
@@ -97,7 +97,15 @@ const menuList = computed(() => {
|
|||||||
const topLevelMenuList = computed(() => {
|
const topLevelMenuList = computed(() => {
|
||||||
return menuList.value.map((item) => {
|
return menuList.value.map((item) => {
|
||||||
const { children, ...rest } = item;
|
const { children, ...rest } = item;
|
||||||
return rest;
|
const hasChildren = children && Array.isArray(children) && children.length > 0;
|
||||||
|
const enhancedMeta = {
|
||||||
|
...rest.meta,
|
||||||
|
disabled: !hasChildren
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
meta: enhancedMeta
|
||||||
|
};
|
||||||
}).filter(itv=>itv.name !== 'stage');
|
}).filter(itv=>itv.name !== 'stage');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,10 +114,7 @@ const backTitle = computed(()=>{
|
|||||||
return menuList.value.find(itv=>itv.name === 'stage')?.meta?.title || '-';
|
return menuList.value.find(itv=>itv.name === 'stage')?.meta?.title || '-';
|
||||||
})
|
})
|
||||||
|
|
||||||
// 后台管理点击获取列表
|
|
||||||
const onStageManage = () =>{
|
|
||||||
selectedTopMenu.value = '/stage';
|
|
||||||
}
|
|
||||||
|
|
||||||
const topTitle = computed(() => {
|
const topTitle = computed(() => {
|
||||||
return (
|
return (
|
||||||
@@ -167,6 +172,18 @@ const sideMenuList = computed(() => {
|
|||||||
// 处理顶部菜单选中事件
|
// 处理顶部菜单选中事件
|
||||||
const handleTopMenuSelect = (menuPath: string) => {
|
const handleTopMenuSelect = (menuPath: string) => {
|
||||||
selectedTopMenu.value = menuPath;
|
selectedTopMenu.value = menuPath;
|
||||||
|
const currentModule = menuList.value.find(item => item.path === menuPath);
|
||||||
|
|
||||||
|
if (currentModule && currentModule.children && currentModule.children.length > 0) {
|
||||||
|
const firstChild = currentModule.children[0];
|
||||||
|
const targetPath = firstChild.path.startsWith("/")
|
||||||
|
? firstChild.path
|
||||||
|
: `${currentModule.path}/${firstChild.path}`;
|
||||||
|
router.push(targetPath);
|
||||||
|
selectedActiveMenu.value = targetPath;
|
||||||
|
} else {
|
||||||
|
router.push(menuPath);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 左侧菜单选中事件
|
// 左侧菜单选中事件
|
||||||
@@ -174,23 +191,37 @@ const handleSideMenuSelect = (menuPath: string) => {
|
|||||||
selectedActiveMenu.value = menuPath;
|
selectedActiveMenu.value = menuPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 高亮当前激活的菜单
|
||||||
|
const activeMenuByUrl = () => {
|
||||||
|
// 1. 获取当前路由路径,例如 "/stage/dict" 或 "/business/customer"
|
||||||
|
const currentPath = route.path;
|
||||||
|
|
||||||
|
// 2. 尝试从 topLevelMenuList 中直接找匹配项
|
||||||
|
let matchedMenu = topLevelMenuList.value.find(menu =>
|
||||||
|
currentPath.startsWith(menu.path)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 如果没找到,且路径以 /stage 开头 那就是后台管理模块
|
||||||
|
if (!matchedMenu && currentPath.startsWith('/stage')) {
|
||||||
|
selectedTopMenu.value = '/stage';
|
||||||
|
}else{
|
||||||
|
// 4. 赋值选中的菜单
|
||||||
|
if (matchedMenu) {
|
||||||
|
selectedTopMenu.value = matchedMenu.path;
|
||||||
|
} else if (topLevelMenuList.value.length > 0) {
|
||||||
|
selectedTopMenu.value = topLevelMenuList.value[0].path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => route.path, () => {
|
||||||
|
activeMenuByUrl();
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
// 初始化:默认选中第一个菜单
|
// 初始化:默认选中第一个菜单
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
const currentRoutePath = router.currentRoute.value.path;
|
activeMenuByUrl();
|
||||||
const matchedTopMenu = topLevelMenuList.value.find(menu =>
|
|
||||||
currentRoutePath.startsWith(`${menu.path}/`) || currentRoutePath === menu.path
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchedTopMenu && matchedTopMenu.path) {
|
|
||||||
selectedTopMenu.value = matchedTopMenu.path;
|
|
||||||
} else if (topLevelMenuList.value.length > 0) {
|
|
||||||
// 否则默认选中第一个菜单
|
|
||||||
const firstMenu = topLevelMenuList.value[0];
|
|
||||||
if (firstMenu && firstMenu.path) {
|
|
||||||
selectedTopMenu.value = firstMenu.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
// 监听窗口大小变化
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
@@ -219,6 +250,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
:deep(.el-main) {
|
:deep(.el-main) {
|
||||||
--el-main-padding: 16px;
|
--el-main-padding: 16px;
|
||||||
|
padding: var(--el-main-padding) calc(var(--el-main-padding) * 2);
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +281,7 @@ onUnmounted(() => {
|
|||||||
.mj-aside-title {
|
.mj-aside-title {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #888;
|
color: #888;
|
||||||
padding: 10px var(--el-menu-base-level-padding);
|
padding: 10px calc(var(--el-menu-base-level-padding) + 16px);
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<el-dropdown placement="bottom" trigger="click" @command="handleCommand">
|
<el-dropdown placement="bottom" trigger="click" @command="handleCommand">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="text-meta">
|
<div class="text-meta">
|
||||||
<span class="userinfo-username">{{ userInfo.username }}</span>
|
<span class="userinfo-username">{{ userInfo.nickname }}</span>
|
||||||
<span class="userinfo-role">SUPER ADMIN</span>
|
<span class="userinfo-role">SUPER ADMIN</span>
|
||||||
</div>
|
</div>
|
||||||
<name-avatar :name="userInfo.nickname" :src="userInfo.avatar" :size="30" />
|
<name-avatar :name="userInfo.nickname" :src="userInfo.avatar" :size="30" />
|
||||||
@@ -48,7 +48,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Monitor, Bell } from "@element-plus/icons-vue";
|
|
||||||
import TokenManager from "@/utils/storage";
|
import TokenManager from "@/utils/storage";
|
||||||
import NameAvatar from "@/components/nameAvatar/index.vue";
|
import NameAvatar from "@/components/nameAvatar/index.vue";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
@@ -81,7 +80,7 @@ const handleCommand = (command: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onStageManage = () => {
|
const onStageManage = () => {
|
||||||
emits("on-stage-manage");
|
emits("on-stage-manage",'/stage');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取当前的用户的数据信息
|
// 获取当前的用户的数据信息
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<span>© 2025 智视界保留所有权利</span>
|
<span>© 2025 智服链保留所有权利</span>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<el-link underline="never">隐私政策</el-link>
|
<el-link underline="never">隐私政策</el-link>
|
||||||
<el-link underline="never">服务条款</el-link>
|
<el-link underline="never">服务条款</el-link>
|
||||||
@@ -87,10 +87,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||||
import { Message, Lock, Right } from "@element-plus/icons-vue";
|
|
||||||
import { login } from "@/api";
|
import { login } from "@/api";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import TokenManager from '@/utils/storage';
|
import TokenManager from '@/utils/storage';
|
||||||
|
import { Lock,Message,Right } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
defineOptions({ name: "Login" });
|
defineOptions({ name: "Login" });
|
||||||
|
|
||||||
@@ -110,12 +110,13 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const tokenManager = TokenManager.getInstance();
|
const tokenManager = TokenManager.getInstance();
|
||||||
|
const KEEP_KEY = "keep_login_remember";
|
||||||
const loginFormRef = ref<FormInstance>();
|
const loginFormRef = ref<FormInstance>();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
username: "user",
|
username: "",
|
||||||
password: "password",
|
password: "",
|
||||||
remember:false
|
remember:false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,9 +143,10 @@ const handleLogin = async () => {
|
|||||||
grant_type: 'password'
|
grant_type: 'password'
|
||||||
});
|
});
|
||||||
if (response) {
|
if (response) {
|
||||||
const { access_token, refresh_token, expires_in,username,userId,avatar } = response;
|
const { access_token, refresh_token, expires_in,username,userId,avatar,nickname } = response;
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
username,
|
username,
|
||||||
|
nickname,
|
||||||
userId,
|
userId,
|
||||||
avatar
|
avatar
|
||||||
}
|
}
|
||||||
@@ -158,22 +160,46 @@ const handleLogin = async () => {
|
|||||||
userStore.setToken(refresh_token);
|
userStore.setToken(refresh_token);
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
// TODO:记住密码功能
|
// 记住密码功能
|
||||||
|
if (loginForm.remember) {
|
||||||
|
const saveInfo = {
|
||||||
|
username: loginForm.username,
|
||||||
|
password: loginForm.password // TODO:密码要加密才行
|
||||||
|
};
|
||||||
|
tokenManager.setToken(KEEP_KEY, JSON.stringify(saveInfo));
|
||||||
|
} else {
|
||||||
|
// 如果用户取消勾选,则移除
|
||||||
|
tokenManager.removeToken(KEEP_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.success("登录成功");
|
ElMessage.success("登录成功");
|
||||||
|
|
||||||
// 跳转到首页或之前访问的页面
|
// 跳转到首页或之前访问的页面
|
||||||
const redirect = (route.query.redirect as string) || "/";
|
const redirect = (route.query.redirect as string);
|
||||||
router.push(redirect);
|
router.push({path:'/',query:redirect});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
ElMessage.error(error.msg || "登录失败,请稍后重试");
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedInfo = tokenManager.getToken(KEEP_KEY);
|
||||||
|
if (savedInfo) {
|
||||||
|
try {
|
||||||
|
const { username, password } = JSON.parse(savedInfo);
|
||||||
|
loginForm.username = username;
|
||||||
|
loginForm.password = password;
|
||||||
|
loginForm.remember = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("解析记住的密码失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
13
src/pages/businessManage/businessOpport/index.vue
Normal file
13
src/pages/businessManage/businessOpport/index.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
商机管理
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {reactive,ref,onMounted} from "vue"
|
||||||
|
|
||||||
|
defineOptions({})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
72
src/pages/businessManage/customer/filterContainer.vue
Normal file
72
src/pages/businessManage/customer/filterContainer.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="custom-filter-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">条件筛选</span>
|
||||||
|
<el-button link type="primary" class="panel-reset_btn" @click="$emit('reset')">重置所有</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-footer">
|
||||||
|
<el-button class="cancel-btn" link @click="$emit('cancel')">取消</el-button>
|
||||||
|
<el-button type="primary" class="confirm-btn" @click="$emit('confirm')">确认筛选</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineEmits(["reset", "cancel", "confirm"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.custom-filter-panel {
|
||||||
|
--bg-color: #fbfcfd;
|
||||||
|
--line-color:#F1F5F9;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(0,0,0,.1);
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding: 11px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--line-color);
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.panel-reset_btn{
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding: 5px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border-top: 1px solid var(--line-color);
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
color: #62748e;
|
||||||
|
&:hover{
|
||||||
|
color: #314158;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
603
src/pages/businessManage/customer/index.vue
Normal file
603
src/pages/businessManage/customer/index.vue
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-manage-container">
|
||||||
|
<!-- 顶部搜索 -->
|
||||||
|
<stageBreadcrumbs :show-title="false">
|
||||||
|
<template #title>
|
||||||
|
<div class="customer-flex customer-header-container">
|
||||||
|
<div class="customer-title">客户列表</div>
|
||||||
|
<div class="customer-num">8</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<el-button icon="Plus" type="primary">新增客户</el-button>
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<div class="customer-flex customer-actions">
|
||||||
|
<div class="search-auto-expand-input">
|
||||||
|
<el-input
|
||||||
|
placeholder="搜索客户..."
|
||||||
|
class="auto-expand-input"
|
||||||
|
:prefix-icon="'Search'"
|
||||||
|
v-model="searchVal"
|
||||||
|
@keyup.enter="() => {}"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
<!-- 查询条件 -->
|
||||||
|
<CommonFilter @on-hide="onMenuClose">
|
||||||
|
<FilterContainer>
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-form label-position="top" class="custom-form">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户分类">
|
||||||
|
<el-radio-group v-model="form.type" class="tag-group">
|
||||||
|
<el-radio-button label="潜在客户" />
|
||||||
|
<el-radio-button label="合作客户" />
|
||||||
|
<el-radio-button label="战略客户" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="客户标签">
|
||||||
|
<el-radio-group v-model="form.tag" class="tag-group">
|
||||||
|
<el-radio-button label="KA" />
|
||||||
|
<el-radio-button label="SMB" />
|
||||||
|
<el-radio-button label="NA" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="客户所在省/市">
|
||||||
|
<el-select
|
||||||
|
v-model="form.region"
|
||||||
|
placeholder="请选择地区"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<el-option label="上海" value="shanghai" />
|
||||||
|
<el-option label="北京" value="beijing" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="增值税税率">
|
||||||
|
<el-radio-group
|
||||||
|
v-model="form.taxRate"
|
||||||
|
class="tag-group"
|
||||||
|
>
|
||||||
|
<el-radio-button label="6%" />
|
||||||
|
<el-radio-button label="3%" />
|
||||||
|
<el-radio-button label="1%" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户来源">
|
||||||
|
<el-radio-group v-model="form.source" class="tag-group">
|
||||||
|
<el-radio-button label="公司渠道" />
|
||||||
|
<el-radio-button label="商务自拓" />
|
||||||
|
<el-radio-button label="转介绍" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="游戏负责人">
|
||||||
|
<el-select
|
||||||
|
v-model="form.manager"
|
||||||
|
placeholder="请选择负责人"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<el-option label="张三" value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="税种">
|
||||||
|
<el-select
|
||||||
|
v-model="form.taxType"
|
||||||
|
placeholder="请选择税种"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<el-option label="增值税" value="vat" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</FilterContainer>
|
||||||
|
</CommonFilter>
|
||||||
|
<ViewSwitcher v-model="currentView" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</stageBreadcrumbs>
|
||||||
|
|
||||||
|
<template v-if="currentView === 'grid'">
|
||||||
|
<CardManager
|
||||||
|
:fetch-api="apiGetCustomerList"
|
||||||
|
:pageSize="20"
|
||||||
|
:itemHeight="350"
|
||||||
|
:columnCount="cols"
|
||||||
|
:extraParams="searchForm"
|
||||||
|
@on-action="handleAction"
|
||||||
|
ref="managerRef"
|
||||||
|
>
|
||||||
|
<template #default="{ item, openMenu }">
|
||||||
|
<!-- 底部卡片列表 -->
|
||||||
|
<cardItem :item="item">
|
||||||
|
<template #content>
|
||||||
|
<!-- 顶部公司名称 -->
|
||||||
|
<div class="customer-flex customer-company">
|
||||||
|
<div class="customer-icon">
|
||||||
|
<name-avatar
|
||||||
|
:size="45"
|
||||||
|
:name="item.title"
|
||||||
|
shape="square"
|
||||||
|
></name-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="customer-info">
|
||||||
|
<div class="mj-ellipsis-one-line company-name">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
<div class="company-num">{{ item.num }}</div>
|
||||||
|
<!-- 更多按钮 -->
|
||||||
|
<div
|
||||||
|
class="customer-info-more"
|
||||||
|
@click.stop="openMenu($event, item)"
|
||||||
|
>
|
||||||
|
<el-icon><MoreFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 类型 -->
|
||||||
|
<div class="customer-flex customer-category">
|
||||||
|
<div
|
||||||
|
class="customer-category-item"
|
||||||
|
:key="cateIndex"
|
||||||
|
v-for="(cate, cateIndex) in item.category"
|
||||||
|
>
|
||||||
|
{{ cate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 相关业务指标 -->
|
||||||
|
<div class="customer-flex customer-indicators">
|
||||||
|
<div
|
||||||
|
class="customer-indicators-item"
|
||||||
|
v-for="(row, rowIndex) in item.indicators"
|
||||||
|
:key="rowIndex"
|
||||||
|
>
|
||||||
|
<div class="indicators-item-title">{{ row.title }}</div>
|
||||||
|
<div class="indicators-item-num">{{ row.num }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 底部地址 -->
|
||||||
|
<div class="customer-flex customer-bottom-address">
|
||||||
|
<div class="customer-bottom-address_icon">
|
||||||
|
<el-icon><Location /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="customer-bottom-address_text">
|
||||||
|
<AutoTooltip :content="item.address" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</cardItem>
|
||||||
|
</template>
|
||||||
|
</CardManager>
|
||||||
|
</template>
|
||||||
|
<!-- 底部Table列表 -->
|
||||||
|
<template v-else>
|
||||||
|
<CommonTable
|
||||||
|
ref="tableRef"
|
||||||
|
:columns="columns"
|
||||||
|
selection
|
||||||
|
:request-api="apiGetCustomerList"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- 查看更多虚拟触发 -->
|
||||||
|
<ActionMenu
|
||||||
|
:visible="visible"
|
||||||
|
:style="menuStyle"
|
||||||
|
:data="[]"
|
||||||
|
:bodyStyle="bodyStyle"
|
||||||
|
@close="closeMenu"
|
||||||
|
@action="handleMenuClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from "vue";
|
||||||
|
import { useWindowSize } from "@vueuse/core";
|
||||||
|
import collapseHeader from "@/components/collapseHeader/index.vue";
|
||||||
|
import cardItem from "@/components/cardManager/cardItem.vue";
|
||||||
|
import cardManager from "@/components/cardManager/index.vue";
|
||||||
|
import nameAvatar from "@/components/nameAvatar/index.vue";
|
||||||
|
import AutoTooltip from "@/components/autoTooltip/index.vue";
|
||||||
|
import ActionMenu from "@/components/popoverMenu/index.vue";
|
||||||
|
import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
|
||||||
|
import CommonFilter from "@/components/commonFilter/index.vue";
|
||||||
|
import ViewSwitcher from "@/components/viewSwitcher/index.vue";
|
||||||
|
import { useTableAction } from "@/hooks/useTableAction";
|
||||||
|
import CommonTable from "@/components/proTable/proTablev2.vue";
|
||||||
|
import FilterContainer from "./filterContainer.vue";
|
||||||
|
defineOptions({ name: "Customer" });
|
||||||
|
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
const cols = computed(() => {
|
||||||
|
if (width.value > 1400) return 5;
|
||||||
|
if (width.value > 1100) return 4;
|
||||||
|
if (width.value > 768) return 2;
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
type: [],
|
||||||
|
source: [],
|
||||||
|
tags: [],
|
||||||
|
manager: "",
|
||||||
|
taxRate: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
console.log("提交筛选:", form.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchVal = ref("");
|
||||||
|
const currentView = ref("grid");
|
||||||
|
|
||||||
|
const tableRef = ref(null);
|
||||||
|
const { handleAction, handleDelete } = useTableAction(tableRef);
|
||||||
|
const bodyStyle = {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: "8px",
|
||||||
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
|
||||||
|
border: "1px solid #e4e7ed",
|
||||||
|
};
|
||||||
|
const cardList = ref([]);
|
||||||
|
|
||||||
|
const apiGetCustomerList = async (params) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
return {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "腾讯科技(深圳)有限公司",
|
||||||
|
num: "KH-202403001",
|
||||||
|
category: ["重要客户", "全电专票", "6%"],
|
||||||
|
indicators: [
|
||||||
|
{
|
||||||
|
title: "工作室",
|
||||||
|
num: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "项目",
|
||||||
|
num: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "合同",
|
||||||
|
num: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
address: "杭州市滨江区网商路599号",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "腾讯科技(深圳)有限公司",
|
||||||
|
num: "KH-202403001",
|
||||||
|
category: ["重要客户", "全电专票", "6%"],
|
||||||
|
indicators: [
|
||||||
|
{
|
||||||
|
title: "工作室",
|
||||||
|
num: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "项目",
|
||||||
|
num: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "合同",
|
||||||
|
num: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
address: "杭州市滨江区网商路599号",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
prop: "id",
|
||||||
|
label: "线索名称/编号",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "name",
|
||||||
|
label: "客户名称",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "p1",
|
||||||
|
label: "工作室名称",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "p2",
|
||||||
|
label: "游戏名称",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "p3",
|
||||||
|
label: "线索跟进人",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "p4",
|
||||||
|
label: "线索状态",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "p5",
|
||||||
|
label: "创建时间",
|
||||||
|
align: "center",
|
||||||
|
valueType: "date",
|
||||||
|
format: "YYYY-MM-DD HH:mm",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "actions",
|
||||||
|
label: "操作",
|
||||||
|
align: "right",
|
||||||
|
width: "300",
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const handleMenuClick = (type: string, data: Customer) => {
|
||||||
|
console.log(`执行操作: ${type}`, data.name);
|
||||||
|
};
|
||||||
|
// 当前关闭事件
|
||||||
|
const onMenuClose = () => {
|
||||||
|
console.log("执行当前的关闭事件");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.customer-manage-container {
|
||||||
|
.customer-flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.customer-actions {
|
||||||
|
gap: 10px;
|
||||||
|
:deep(.search-auto-expand-input .auto-expand-input) {
|
||||||
|
--el-input-height: 38px;
|
||||||
|
--el-input-border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.stage-breadcrumbs) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.customer-header-container {
|
||||||
|
gap: 8px;
|
||||||
|
.customer-title {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #0f172b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.customer-num {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #62748e;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.mj-icon-container) {
|
||||||
|
--radius: 10px;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--shadow: inset 0 0 1px rgba(0, 0, 0, 0.06);
|
||||||
|
--hover-color: #eff6ff;
|
||||||
|
}
|
||||||
|
.customer-company {
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
:deep(.mj-name-avatar) {
|
||||||
|
--el-avatar-border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 28px;
|
||||||
|
.company-name {
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.company-num {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
display: inline-block;
|
||||||
|
color: #90a1b9;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.customer-info-more {
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-category {
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
.customer-category-item {
|
||||||
|
padding: 2px 8px;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
color: #9810fa;
|
||||||
|
background-color: #faf5ff;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
color: #6c788e;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #155dfc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-indicators {
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
.customer-indicators-item {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 50px;
|
||||||
|
background-color: #fbfcfd;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
.indicators-item-title {
|
||||||
|
font-size: 8px;
|
||||||
|
color: #9da7c4;
|
||||||
|
}
|
||||||
|
.indicators-item-num {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-bottom-address {
|
||||||
|
border-top: 1px solid #f8fafc;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
gap: 10px;
|
||||||
|
color: #94a0b3;
|
||||||
|
.customer-bottom-address_icon {
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
.el-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.customer-bottom-address_text {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用类名嵌套提升权重,无需 !important */
|
||||||
|
.custom-filter-panel {
|
||||||
|
// 1. 表单 Label 样式:浅灰色,加深间距
|
||||||
|
.el-form-item__label {
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Select 选择器:还原浅灰背景和圆角
|
||||||
|
.el-select {
|
||||||
|
.el-select__wrapper {
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none; // 移除默认投影
|
||||||
|
height: 42px;
|
||||||
|
|
||||||
|
&.is-focused {
|
||||||
|
box-shadow: 0 0 0 1px #165dff inset; // 聚焦时显示边框
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
// 1. 标题样式优化
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
color: #909399; // 浅灰色文字
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 按钮组样式重写 (变成图中的独立方块效果)
|
||||||
|
.tag-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px; // 间距
|
||||||
|
|
||||||
|
:deep(.el-radio-button) {
|
||||||
|
// 隐藏原生边框和阴影
|
||||||
|
.el-radio-button__inner {
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
padding: 8px 20px;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中状态
|
||||||
|
&.is-active .el-radio-button__inner {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
border-color: #b3d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 悬停效果
|
||||||
|
&:hover:not(.is-active) .el-radio-button__inner {
|
||||||
|
color: #409eff;
|
||||||
|
border-color: #c6e2ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 输入框/选择框圆角及背景
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
border-radius: 12px; // 更大的圆角
|
||||||
|
background-color: #f5f7fa; // 极浅灰背景
|
||||||
|
box-shadow: none; // 去除阴影边框
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&.is-focus {
|
||||||
|
border-color: #409eff;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/pages/businessManage/gameStudios/index.vue
Normal file
13
src/pages/businessManage/gameStudios/index.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
游戏和工作室
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {reactive,ref,onMounted} from "vue"
|
||||||
|
|
||||||
|
defineOptions({})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
<div class="mj-drawer-top-container">
|
<div class="mj-drawer-top-container">
|
||||||
<div class="top-toolbar">
|
<div class="top-toolbar">
|
||||||
<div class="left-actions">
|
<div class="left-actions">
|
||||||
<div class="search-dict-input">
|
<div class="search-auto-expand-input">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索字段名称..."
|
placeholder="搜索字段名称..."
|
||||||
class="custom-search-input auto-expand-input"
|
class="custom-search-input auto-expand-input"
|
||||||
:prefix-icon="Search"
|
:prefix-icon="Search"
|
||||||
@keyup.enter="onConfirmSuccess"
|
@keyup.enter="onSearchData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 状态筛选的内容 -->
|
<!-- 状态筛选的内容 -->
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
v-for="(item, index) in DictManage.statusOptions"
|
v-for="(item, index) in dicts.permission_list_enable_disable"
|
||||||
:key="index"
|
:key="index"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
class="apply-btn"
|
class="apply-btn"
|
||||||
@click="onConfirmSuccess"
|
@click="onSearchData"
|
||||||
>应用筛选</el-button
|
>应用筛选</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,43 +86,9 @@
|
|||||||
<!-- Table列表 -->
|
<!-- Table列表 -->
|
||||||
<CommonTable
|
<CommonTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
v-model:data="list"
|
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
pagination
|
|
||||||
height="calc(100vh - 186px)"
|
|
||||||
:immediate="false"
|
|
||||||
:request-api="fetchData"
|
:request-api="fetchData"
|
||||||
>
|
>
|
||||||
<!-- 名称点击 -->
|
|
||||||
<template #labelName="{ row }">
|
|
||||||
<el-button link type="primary" @click="onLevelNext(row)" v-if="!hasChild">{{
|
|
||||||
row.label
|
|
||||||
}}</el-button>
|
|
||||||
|
|
||||||
<span v-else>{{ row.label }}</span>
|
|
||||||
</template>
|
|
||||||
<!-- 状态插槽 -->
|
|
||||||
<template #status="{ row }">
|
|
||||||
<div
|
|
||||||
class="mj-status-dot"
|
|
||||||
:style="{
|
|
||||||
'--data-status-color': DictManage.statusDictColor[row.status],
|
|
||||||
}"
|
|
||||||
@click="handleDictStatus(row)"
|
|
||||||
>
|
|
||||||
{{ DictManage.statusDict[row.status] }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- <template #actions="{ row }">
|
|
||||||
<el-button link type="primary" v-if="!hasChild" @click="handleAddNext(row)"
|
|
||||||
>添加二级字段</el-button>
|
|
||||||
<el-button link type="primary" @click="handleEdit(row)"
|
|
||||||
>编辑</el-button
|
|
||||||
>
|
|
||||||
<el-button link type="danger" @click="handleDelete(row)"
|
|
||||||
>删除</el-button
|
|
||||||
>
|
|
||||||
</template> -->
|
|
||||||
</CommonTable>
|
</CommonTable>
|
||||||
<!-- 新增字段 -->
|
<!-- 新增字段 -->
|
||||||
<dictFieldLevelManage
|
<dictFieldLevelManage
|
||||||
@@ -143,11 +109,16 @@
|
|||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CommonTable from "@/components/proTable/index.vue";
|
import { h } from "vue";
|
||||||
|
import CommonTable from "@/components/proTable/proTablev2.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Search, Filter, Plus } from "@element-plus/icons-vue";
|
|
||||||
import dictFieldLevelManage from "./dictFieldLevelManage.vue";
|
import dictFieldLevelManage from "./dictFieldLevelManage.vue";
|
||||||
import { DictManage } from "@/dict";
|
import { DictManage } from "@/dict";
|
||||||
|
import { useTableAction } from "@/hooks/useTableAction";
|
||||||
|
import { useDict } from "@/hooks/useDictData";
|
||||||
|
// import { formatIndex } from "@/utils/utils";
|
||||||
|
const { dicts, refresh } = useDict("permission_list_enable_disable");
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDictTypeValue,
|
getDictTypeValue,
|
||||||
deleteDictTypeValue,
|
deleteDictTypeValue,
|
||||||
@@ -166,36 +137,49 @@ const filterForm = reactive({
|
|||||||
});
|
});
|
||||||
const size = ref<string>(""); //抽屉大小
|
const size = ref<string>(""); //抽屉大小
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
const { handleAction, handleDelete: runDelete } = useTableAction(tableRef);
|
||||||
const visible = ref<boolean>(false);
|
const visible = ref<boolean>(false);
|
||||||
const parentId = ref<string>("");
|
const parentId = ref<string>("");
|
||||||
const total = ref(0);
|
|
||||||
const list = ref([]);
|
|
||||||
const hasChild = ref<boolean>(false); //是否是子级弹窗
|
const hasChild = ref<boolean>(false); //是否是子级弹窗
|
||||||
const childId = ref<string|number>(''); // 子集的id
|
const childId = ref<string | number>(""); // 子集的id
|
||||||
const childModals = ref([]); //子弹窗的列表
|
const childModals = ref([]); //子弹窗的列表
|
||||||
const childModalRefs = ref({}); // 子弹窗的引用
|
const childModalRefs = ref({}); // 子弹窗的引用
|
||||||
const onCloseCallback = ref<Function | null>(null); // 关闭回调函数
|
const onCloseCallback = ref<Function | null>(null); // 关闭回调函数
|
||||||
const columns = computed(()=>[
|
const columns = computed(() => [
|
||||||
{
|
{
|
||||||
prop: "id",
|
prop: "id",
|
||||||
label: "字典编码",
|
align:'center',
|
||||||
|
label: "字典编码"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "label",
|
prop: "label",
|
||||||
label: "字典名称",
|
label: "字典名称",
|
||||||
align: "center",
|
align: "center",
|
||||||
slot: "labelName",
|
valueType: "ellipsis",
|
||||||
|
render: ({ rowData }: any) => {
|
||||||
|
if (hasChild.value) {
|
||||||
|
return h("span", { class: "v2-cell-text" }, rowData.label);
|
||||||
|
}
|
||||||
|
return h(ElButton, { type: "primary", link:true,onClick:()=>{
|
||||||
|
onLevelNext(rowData);
|
||||||
|
} }, () => rowData.label);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "value",
|
prop: "value",
|
||||||
label: "字典值",
|
label: "字典值",
|
||||||
|
showOverflowTooltip: true,
|
||||||
align: "center",
|
align: "center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "status",
|
prop: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
align: "center",
|
align: "center",
|
||||||
slot: "status",
|
valueType: "status",
|
||||||
|
options: computed(() => dicts.value.permission_list_enable_disable),
|
||||||
|
onClick: ({ cellValue, rowData }) => {
|
||||||
|
handleDictStatus(rowData);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "sort",
|
prop: "sort",
|
||||||
@@ -206,47 +190,42 @@ const columns = computed(()=>[
|
|||||||
prop: "updateTime",
|
prop: "updateTime",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
align: "center",
|
align: "center",
|
||||||
showOverflowTooltip: true,
|
valueType: "date",
|
||||||
width:200,
|
format: "YYYY-MM-DD HH:mm",
|
||||||
formatter: (val) => {
|
|
||||||
return val.updateTime
|
|
||||||
? dayjs(val.updateTime).format("YYYY-MM-DD HH:mm")
|
|
||||||
: "-";
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "actions",
|
prop: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
align: "right",
|
align: "right",
|
||||||
width: "300",
|
width: 300,
|
||||||
actions:[
|
actions: [
|
||||||
{
|
{
|
||||||
label: "添加二级字段",
|
label: "添加二级字段",
|
||||||
type: "primary",
|
type: "primary",
|
||||||
link:true,
|
link: true,
|
||||||
permission: ["edit"],
|
permission: ["edit"],
|
||||||
show:()=>{
|
show: () => {
|
||||||
return !hasChild.value
|
return !hasChild.value;
|
||||||
},
|
},
|
||||||
onClick: (row) => handleAddNext(row),
|
onClick: (row) => handleAddNext(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "编辑",
|
label: "编辑",
|
||||||
type: "primary",
|
type: "primary",
|
||||||
link:true,
|
link: true,
|
||||||
permission: ["edit"],
|
permission: ["edit"],
|
||||||
onClick: (row) => handleEdit(row),
|
onClick: (row) => handleEdit(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "删除",
|
label: "删除",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
link:true,
|
link: true,
|
||||||
permission: ["delete"],
|
permission: ["delete"],
|
||||||
onClick: (row) => handleDelete(row),
|
onClick: (row) => handleDelete(row),
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
|
|
||||||
// 设置子弹窗引用
|
// 设置子弹窗引用
|
||||||
const setChildModalRef = (el, key) => {
|
const setChildModalRef = (el, key) => {
|
||||||
@@ -256,7 +235,9 @@ const setChildModalRef = (el, key) => {
|
|||||||
};
|
};
|
||||||
// 点击获取二级菜单数据
|
// 点击获取二级菜单数据
|
||||||
const onLevelNext = (row) => {
|
const onLevelNext = (row) => {
|
||||||
const childKey = `child-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const childKey = `child-${Date.now()}-${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 9)}`;
|
||||||
childModals.value.push({
|
childModals.value.push({
|
||||||
key: childKey,
|
key: childKey,
|
||||||
data: row,
|
data: row,
|
||||||
@@ -268,9 +249,9 @@ const onLevelNext = (row) => {
|
|||||||
if (childRef) {
|
if (childRef) {
|
||||||
childRef.open({
|
childRef.open({
|
||||||
...row,
|
...row,
|
||||||
parentId:parentId.value,
|
parentId: parentId.value,
|
||||||
hasChild: true,
|
hasChild: true,
|
||||||
onClose: () => removeChildModal(childKey)
|
onClose: () => removeChildModal(childKey),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -278,7 +259,7 @@ const onLevelNext = (row) => {
|
|||||||
|
|
||||||
// 移除当前组件
|
// 移除当前组件
|
||||||
const removeChildModal = (key: string) => {
|
const removeChildModal = (key: string) => {
|
||||||
const index = childModals.value.findIndex(child => child.key === key);
|
const index = childModals.value.findIndex((child) => child.key === key);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
childModals.value.splice(index, 1);
|
childModals.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -293,9 +274,11 @@ const fetchData = async (params) => {
|
|||||||
...params,
|
...params,
|
||||||
keyword: searchQuery.value,
|
keyword: searchQuery.value,
|
||||||
...filterForm,
|
...filterForm,
|
||||||
}
|
};
|
||||||
|
|
||||||
const response = hasChild.value ? await getNextDictMenu(parentId.value,childId.value,queryParams) : await getDictTypeValue(parentId.value, queryParams);
|
const response = hasChild.value
|
||||||
|
? await getNextDictMenu(parentId.value, childId.value, queryParams)
|
||||||
|
: await getDictTypeValue(parentId.value, queryParams);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("getTableData Error", error);
|
console.log("getTableData Error", error);
|
||||||
@@ -319,20 +302,27 @@ const handleDictStatus = async (row) => {
|
|||||||
const addFields = () => {
|
const addFields = () => {
|
||||||
dictTitle.value = "新增字段";
|
dictTitle.value = "新增字段";
|
||||||
addVisible.value = true;
|
addVisible.value = true;
|
||||||
Object.assign(selectItem,{
|
Object.assign(selectItem, {
|
||||||
id:null,
|
id: null,
|
||||||
parentId:null,
|
parentId: null,
|
||||||
label: "",
|
label: "",
|
||||||
value: "",
|
value: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status:1,
|
status: 1,
|
||||||
remark:''
|
remark: "",
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确定刷新数据
|
// 确定刷新数据
|
||||||
const onConfirmSuccess = () => {
|
const onConfirmSuccess = () => {
|
||||||
tableRef.value && tableRef.value.refresh();
|
if (selectItem.id) {
|
||||||
|
handleAction(Promise.resolve(true), selectItem.id, getTableData, {
|
||||||
|
showMsg: false,
|
||||||
|
silentUpdate: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tableRef.value?.refresh();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭popover 重置数据信息
|
// 关闭popover 重置数据信息
|
||||||
@@ -340,17 +330,22 @@ const onCloseFilter = () => {
|
|||||||
filterForm.status = "";
|
filterForm.status = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 筛选
|
||||||
|
const onSearchData = () =>{
|
||||||
|
tableRef.value?.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
// 筛选重置
|
// 筛选重置
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
onCloseFilter();
|
onCloseFilter();
|
||||||
onConfirmSuccess();
|
onSearchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加二级字段
|
// 添加二级字段
|
||||||
const handleAddNext = async (item) => {
|
const handleAddNext = async (item) => {
|
||||||
addVisible.value = true;
|
addVisible.value = true;
|
||||||
dictTitle.value = "添加二级字段";
|
dictTitle.value = "添加二级字段";
|
||||||
Object.assign(selectItem,{},{parentId:item.id});
|
Object.assign(selectItem, {}, { parentId: item.id });
|
||||||
};
|
};
|
||||||
// 编辑当前字段
|
// 编辑当前字段
|
||||||
const handleEdit = (item) => {
|
const handleEdit = (item) => {
|
||||||
@@ -366,6 +361,8 @@ const handleDelete = async (item) => {
|
|||||||
try {
|
try {
|
||||||
await deleteDictTypeValue(parentId.value, item.id);
|
await deleteDictTypeValue(parentId.value, item.id);
|
||||||
tableRef.value && tableRef.value.refresh();
|
tableRef.value && tableRef.value.refresh();
|
||||||
|
|
||||||
|
await runDelete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("fetch error", error);
|
console.log("fetch error", error);
|
||||||
}
|
}
|
||||||
@@ -382,7 +379,7 @@ defineExpose({
|
|||||||
hasChild.value = item.hasChild ?? false;
|
hasChild.value = item.hasChild ?? false;
|
||||||
// 处理子集的弹窗
|
// 处理子集的弹窗
|
||||||
if (hasChild.value) {
|
if (hasChild.value) {
|
||||||
size.value = "60%";
|
size.value = "65%";
|
||||||
childId.value = item.id;
|
childId.value = item.id;
|
||||||
if (item.onClose) {
|
if (item.onClose) {
|
||||||
onCloseCallback.value = item.onClose;
|
onCloseCallback.value = item.onClose;
|
||||||
@@ -390,7 +387,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (tableRef.value) {
|
if (tableRef.value) {
|
||||||
await tableRef.value.refresh();
|
await tableRef.value.updateSize();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
destroy-on-close
|
destroy-on-close
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
|
modal-class="standard-overlay-dialog-flat"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="ruleFormRef"
|
ref="ruleFormRef"
|
||||||
@@ -17,10 +18,17 @@
|
|||||||
label-width="120px"
|
label-width="120px"
|
||||||
>
|
>
|
||||||
<el-form-item label="字段名称:" prop="label">
|
<el-form-item label="字段名称:" prop="label">
|
||||||
<el-input placeholder="请输入字典名称" v-model="form.label"></el-input>
|
<el-input
|
||||||
|
placeholder="请输入字典名称"
|
||||||
|
v-model="form.label"
|
||||||
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="字典值:" prop="value">
|
<el-form-item label="字典值:" prop="value">
|
||||||
<el-input placeholder="请输入字典值" v-model="form.value" :disabled="form.id ? true : false"></el-input>
|
<el-input
|
||||||
|
placeholder="请输入字典值"
|
||||||
|
v-model="form.value"
|
||||||
|
:disabled="form.id ? true : false"
|
||||||
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="sort">
|
<el-form-item prop="sort">
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -33,12 +41,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 换成排序的输入框 -->
|
<!-- 换成排序的输入框 -->
|
||||||
<el-input-number placeholder="请输入排序" v-model="form.sort" :min="0" controls-position="right"></el-input-number>
|
<el-input-number
|
||||||
|
placeholder="请输入排序"
|
||||||
|
v-model="form.sort"
|
||||||
|
:min="0"
|
||||||
|
controls-position="right"
|
||||||
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态:" prop="status">
|
<el-form-item label="状态:" prop="status">
|
||||||
<el-radio-group v-model="form.status">
|
<el-radio-group v-model="form.status">
|
||||||
<el-radio :value="1">启用</el-radio>
|
<el-radio
|
||||||
<el-radio :value="0">停用</el-radio>
|
:value="item.value"
|
||||||
|
v-for="(item, index) in dicts.permission_list_enable_disable"
|
||||||
|
:key="item.value"
|
||||||
|
>{{ item.label }}</el-radio
|
||||||
|
>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注:" prop="remark">
|
<el-form-item label="备注:" prop="remark">
|
||||||
@@ -54,7 +71,12 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="onCancel">取消</el-button>
|
<el-button @click="onCancel">取消</el-button>
|
||||||
<el-button @click="onConfirm(ruleFormRef)" type="primary" :loading="loading">确认</el-button>
|
<el-button
|
||||||
|
@click="onConfirm(ruleFormRef)"
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
>确认</el-button
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,8 +84,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, onMounted } from "vue";
|
import { reactive, ref, onMounted } from "vue";
|
||||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||||
import { QuestionFilled } from "@element-plus/icons-vue";
|
|
||||||
import { saveDictTypeValue, updateDictTypeValue } from "@/api/stage/dict";
|
import { saveDictTypeValue, updateDictTypeValue } from "@/api/stage/dict";
|
||||||
|
import { useDict } from "@/hooks/useDictData";
|
||||||
|
const { dicts, refresh } = useDict("permission_list_enable_disable");
|
||||||
defineOptions({ name: "DictFieldLevelManage" });
|
defineOptions({ name: "DictFieldLevelManage" });
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const {
|
const {
|
||||||
@@ -74,12 +97,10 @@ const {
|
|||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
dialogVisible: boolean;
|
dialogVisible: boolean;
|
||||||
row?: any;
|
row?: any;
|
||||||
parentId?: string|number;
|
parentId?: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:dialogVisible", value: boolean): void;
|
(e: "update:dialogVisible", value: boolean): void;
|
||||||
(e: "confirm-success"): void;
|
(e: "confirm-success"): void;
|
||||||
@@ -90,22 +111,26 @@ const form = reactive({
|
|||||||
label: "",
|
label: "",
|
||||||
value: "",
|
value: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status:1,
|
status: '1',
|
||||||
remark:''
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听组件中传递的数据-然后进行复制操作
|
// 监听组件中传递的数据-然后进行复制操作
|
||||||
watch(()=>row,(newRow)=>{
|
watch(
|
||||||
|
() => row,
|
||||||
|
(newRow) => {
|
||||||
if (newRow && Object.keys(newRow).length > 0) {
|
if (newRow && Object.keys(newRow).length > 0) {
|
||||||
Object.assign(form, newRow);
|
Object.assign(form, newRow,{status:String(newRow.status)});
|
||||||
}
|
}
|
||||||
},{deep:true})
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
label: [{ required: true, message: "请输入字段名称", trigger: "blur" }],
|
label: [{ required: true, message: "请输入字段名称", trigger: "blur" }],
|
||||||
value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
|
value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
|
||||||
sort: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
sort: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
||||||
remark:[{ required: false, message: "请输入备注", trigger: "blur" }]
|
remark: [{ required: false, message: "请输入备注", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确定
|
// 确定
|
||||||
@@ -114,16 +139,18 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
|
|||||||
await formEl.validate(async (valid, fields) => {
|
await formEl.validate(async (valid, fields) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
console.log("获取外部的数据信息:",form,parentId)
|
console.log("获取外部的数据信息:", form, parentId);
|
||||||
// 如果是一级新增字段就是parentId为0 如果是添加二级字段就是parentId为父级的id
|
// 如果是一级新增字段就是parentId为0 如果是添加二级字段就是parentId为父级的id
|
||||||
try {
|
try {
|
||||||
const response = row.id ? await updateDictTypeValue(parentId,row.id,form) : await saveDictTypeValue(parentId,form);
|
const response = row.id
|
||||||
ElMessage.success(row.id ? '修改成功' : '新增成功');
|
? await updateDictTypeValue(parentId, row.id, form)
|
||||||
|
: await saveDictTypeValue(parentId, form);
|
||||||
|
ElMessage.success(row.id ? "修改成功" : "新增成功");
|
||||||
onCancel();
|
onCancel();
|
||||||
emit('confirm-success');
|
emit("confirm-success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error',error);
|
console.log("error", error);
|
||||||
} finally{
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
destroy-on-close
|
destroy-on-close
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
|
modal-class="standard-overlay-dialog-flat"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="ruleFormRef"
|
ref="ruleFormRef"
|
||||||
@@ -33,8 +34,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态:" prop="status">
|
<el-form-item label="状态:" prop="status">
|
||||||
<el-radio-group v-model="form.status">
|
<el-radio-group v-model="form.status">
|
||||||
<el-radio :value="1">启用</el-radio>
|
<el-radio
|
||||||
<el-radio :value="0">停用</el-radio>
|
:value="item.value"
|
||||||
|
v-for="(item, index) in dicts.permission_list_enable_disable"
|
||||||
|
:key="item.value"
|
||||||
|
>{{ item.label }}</el-radio
|
||||||
|
>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注:">
|
<el-form-item label="备注:">
|
||||||
@@ -58,8 +63,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, onMounted } from "vue";
|
import { reactive, ref, onMounted } from "vue";
|
||||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||||
import { QuestionFilled } from "@element-plus/icons-vue";
|
|
||||||
import { addDictValue, updateDictValue } from "@/api/stage/dict";
|
import { addDictValue, updateDictValue } from "@/api/stage/dict";
|
||||||
|
import { useDict } from "@/hooks/useDictData";
|
||||||
|
const { dicts, refresh } = useDict("permission_list_enable_disable");
|
||||||
defineOptions({ name: "DictManage" });
|
defineOptions({ name: "DictManage" });
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const {
|
const {
|
||||||
@@ -80,14 +86,14 @@ const ruleFormRef = ref<FormInstance>();
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
key: "",
|
key: "",
|
||||||
status: 1,
|
status: '1',
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听组件中传递的数据-然后进行复制操作
|
// 监听组件中传递的数据-然后进行复制操作
|
||||||
watch(()=>row,(newRow)=>{
|
watch(()=>row,(newRow)=>{
|
||||||
if (newRow && Object.keys(newRow).length > 0) {
|
if (newRow && Object.keys(newRow).length > 0) {
|
||||||
Object.assign(form, newRow);
|
Object.assign(form, newRow,{status:String(newRow.status)});
|
||||||
}
|
}
|
||||||
},{deep:true})
|
},{deep:true})
|
||||||
|
|
||||||
@@ -109,7 +115,7 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
|
|||||||
onCancel();
|
onCancel();
|
||||||
emit('confirm-success');
|
emit('confirm-success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error',error);
|
console.log('error',row.id);
|
||||||
} finally{
|
} finally{
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="mj-dict">
|
<div class="mj-dict">
|
||||||
<stageBreadcrumbs title="组织管理">
|
<stageBreadcrumbs title="组织管理">
|
||||||
<template #content>
|
<template #content>
|
||||||
<el-button :icon="Plus" type="primary" @click="addDict"
|
<el-button :icon="'Plus'" type="primary" @click="addDict"
|
||||||
>新增字典</el-button
|
>新增字典</el-button
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
<el-option
|
<el-option
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
v-for="(item, index) in DictManage.statusOptions"
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in dicts.permission_list_enable_disable"
|
||||||
:key="index"
|
:key="index"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -47,11 +49,11 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</CommonFilter>
|
</CommonFilter>
|
||||||
<div class="search-dict-input">
|
<div class="search-auto-expand-input">
|
||||||
<el-input
|
<el-input
|
||||||
placeholder="搜索字典..."
|
placeholder="搜索字典..."
|
||||||
class="auto-expand-input"
|
class="auto-expand-input"
|
||||||
:prefix-icon="Search"
|
:prefix-icon="'Search'"
|
||||||
v-model="searchVal"
|
v-model="searchVal"
|
||||||
@keyup.enter="fetchTableData"
|
@keyup.enter="fetchTableData"
|
||||||
></el-input>
|
></el-input>
|
||||||
@@ -64,48 +66,12 @@
|
|||||||
<CommonTable
|
<CommonTable
|
||||||
ref="dictTableRef"
|
ref="dictTableRef"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
v-model:data="dataValue"
|
|
||||||
v-model:total="total"
|
|
||||||
pagination
|
|
||||||
:request-api="getTableData"
|
:request-api="getTableData"
|
||||||
>
|
>
|
||||||
<!-- 编号内容显示 -->
|
|
||||||
<template #number="{ row, index }">
|
|
||||||
<span>#{{ formatIndex(index) }}</span>
|
|
||||||
</template>
|
|
||||||
<template #name="{ row }">
|
|
||||||
<span style="font-weight: 600">{{ row.name }}</span>
|
|
||||||
</template>
|
|
||||||
<!-- 编码标识 -->
|
|
||||||
<template #code="{ row }">
|
|
||||||
<el-tag size="small" type="info">{{ row.key }}</el-tag>
|
|
||||||
</template>
|
|
||||||
<!-- 状态插槽 -->
|
|
||||||
<template #status="{ row }">
|
|
||||||
<div
|
|
||||||
class="mj-status-dot"
|
|
||||||
:style="{
|
|
||||||
'--data-status-color': DictManage.statusDictColor[row.status],
|
|
||||||
}"
|
|
||||||
@click="handleDictStatus(row)"
|
|
||||||
>
|
|
||||||
{{ DictManage.statusDict[row.status] }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- <template #actions="{ row }">
|
|
||||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
|
||||||
<el-button link type="primary" @click="handlefieldsConfig(row)"
|
|
||||||
>字段配置</el-button
|
|
||||||
>
|
|
||||||
<el-button link type="danger" @click="handleDelete(row)"
|
|
||||||
>删除</el-button
|
|
||||||
>
|
|
||||||
</template> -->
|
|
||||||
</CommonTable>
|
</CommonTable>
|
||||||
|
|
||||||
<!-- 新增-编辑字典弹窗 -->
|
<!-- 新增-编辑字典弹窗 -->
|
||||||
<dict-manage
|
<dict-manage-modules
|
||||||
v-model:dictVisible="dictVisible"
|
v-model:dictVisible="dictVisible"
|
||||||
:row="selectItem"
|
:row="selectItem"
|
||||||
@confirm-success="onConfirmSuccess"
|
@confirm-success="onConfirmSuccess"
|
||||||
@@ -116,10 +82,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Plus, Search } from "@element-plus/icons-vue";
|
import { h } from "vue";
|
||||||
import CommonTable from "@/components/proTable/index.vue";
|
import CommonTable from "@/components/proTable/proTablev2.vue";
|
||||||
import dictFieldConfig from "./dictFieldConfig.vue";
|
import dictFieldConfig from "./dictFieldConfig.vue";
|
||||||
import dictManage from "./dictManage.vue";
|
import dictManageModules from "./dictManage.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
getDictValues,
|
getDictValues,
|
||||||
@@ -130,13 +96,17 @@ import {
|
|||||||
import { DictManage } from "@/dict";
|
import { DictManage } from "@/dict";
|
||||||
import { formatIndex } from "@/utils/utils";
|
import { formatIndex } from "@/utils/utils";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useTableAction } from "@/hooks/useTableAction";
|
||||||
|
|
||||||
|
import { useDict } from "@/hooks/useDictData";
|
||||||
|
const { dicts, refresh } = useDict("permission_list_enable_disable");
|
||||||
|
|
||||||
defineOptions({ name: "Dictionary" });
|
defineOptions({ name: "Dictionary" });
|
||||||
const fieldsConfigRef = ref(null);
|
const fieldsConfigRef = ref(null);
|
||||||
const dictTableRef = ref(null);
|
const dictTableRef = ref(null);
|
||||||
|
const { handleAction, handleDelete: runDelete } = useTableAction(dictTableRef);
|
||||||
const dictVisible = ref<boolean>(false);
|
const dictVisible = ref<boolean>(false);
|
||||||
const searchVal = ref<string>("");
|
const searchVal = ref<string>("");
|
||||||
const total = ref<number>(0);
|
|
||||||
|
|
||||||
const filterForm = reactive({
|
const filterForm = reactive({
|
||||||
status: "",
|
status: "",
|
||||||
@@ -146,41 +116,63 @@ const selectItem = reactive({});
|
|||||||
|
|
||||||
// 列表columns数据
|
// 列表columns数据
|
||||||
const columns = [
|
const columns = [
|
||||||
{ prop: "id", label: "编号", width: "80", align: "center", slot: "number" },
|
{
|
||||||
{ prop: "name", label: "字典名称", align: "center", slot: "name" },
|
prop: "id",
|
||||||
|
label: "编号",
|
||||||
|
width: "80",
|
||||||
|
align: "center",
|
||||||
|
render: ({ rowData, rowIndex }) => {
|
||||||
|
return h("span", `#${formatIndex(rowIndex)}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "name",
|
||||||
|
label: "字典名称",
|
||||||
|
align: "center",
|
||||||
|
render: ({ rowData }: any) => {
|
||||||
|
return h("span", { style: { fontWeight: "600" } }, rowData.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "key",
|
prop: "key",
|
||||||
label: "编码标识",
|
label: "编码标识",
|
||||||
align: "center",
|
align: "center",
|
||||||
slot: "code",
|
width: 300,
|
||||||
|
render: ({ rowData }: any) => {
|
||||||
|
return h(ElTag, { size: "small", type: "info" }, () => rowData.key);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "status",
|
prop: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
align: "center",
|
align: "center",
|
||||||
slot: "status",
|
width: 150,
|
||||||
|
valueType: "status",
|
||||||
|
options: computed(() => dicts.value.permission_list_enable_disable),
|
||||||
|
onClick: ({ cellValue, rowData }) => {
|
||||||
|
handleDictStatus(rowData);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "remark",
|
prop: "remark",
|
||||||
label: "备注说明",
|
label: "备注说明",
|
||||||
align: "center",
|
align: "center",
|
||||||
showOverflowTooltip: true,
|
valueType: "ellipsis",
|
||||||
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "createTime",
|
prop: "createTime",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
align: "center",
|
align: "center",
|
||||||
showOverflowTooltip: true,
|
valueType: "date",
|
||||||
formatter: (val) => {
|
format: "YYYY-MM-DD HH:mm",
|
||||||
return val.createTime
|
width: 150,
|
||||||
? dayjs(val.createTime).format("YYYY-MM-DD HH:mm")
|
|
||||||
: "-";
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "updateByName",
|
prop: "updateByName",
|
||||||
label: "最后修改人",
|
label: "最后修改人",
|
||||||
align: "center",
|
align: "center",
|
||||||
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "actions",
|
prop: "actions",
|
||||||
@@ -209,13 +201,10 @@ const columns = [
|
|||||||
permission: ["delete"],
|
permission: ["delete"],
|
||||||
onClick: (row) => handleDelete(row),
|
onClick: (row) => handleDelete(row),
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 返回的data数据信息
|
|
||||||
const dataValue = ref([]);
|
|
||||||
|
|
||||||
// popover关闭事件
|
// popover关闭事件
|
||||||
const onPopoverHide = () => {
|
const onPopoverHide = () => {
|
||||||
filterForm.status = "";
|
filterForm.status = "";
|
||||||
@@ -230,8 +219,8 @@ const getTableData = async (params) => {
|
|||||||
try {
|
try {
|
||||||
const response = await getDictValues({
|
const response = await getDictValues({
|
||||||
...params,
|
...params,
|
||||||
keyword: searchVal.value,
|
...(searchVal.value && { keyword: searchVal.value }),
|
||||||
...filterForm,
|
...(filterForm.status && { status: filterForm.status }),
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -240,10 +229,16 @@ const getTableData = async (params) => {
|
|||||||
};
|
};
|
||||||
// 搜索查询条件信息
|
// 搜索查询条件信息
|
||||||
const fetchTableData = () => {
|
const fetchTableData = () => {
|
||||||
dictTableRef.value && dictTableRef.value.refresh();
|
onConfirmSuccess();
|
||||||
};
|
};
|
||||||
const clearSelectItem = () => {
|
const clearSelectItem = () => {
|
||||||
Object.assign(selectItem,{id:null,name:'',key:'',status:1,remark:''})
|
Object.assign(selectItem, {
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
key: "",
|
||||||
|
status: 1,
|
||||||
|
remark: "",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
// 新增字典信息
|
// 新增字典信息
|
||||||
const addDict = () => {
|
const addDict = () => {
|
||||||
@@ -259,19 +254,29 @@ const handleEdit = (item) => {
|
|||||||
// 启用-禁用事件
|
// 启用-禁用事件
|
||||||
const handleDictStatus = async (row) => {
|
const handleDictStatus = async (row) => {
|
||||||
try {
|
try {
|
||||||
row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
|
const apiCall = row.status === 1 ? disableDict(row.id) : enableDict(row.id);
|
||||||
ElMessage.success("操作成功");
|
// row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
|
||||||
onConfirmSuccess();
|
await handleAction(apiCall, row.id, getTableData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("error", error);
|
console.log("error", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新Table数据信息
|
// 刷新Table数据信息
|
||||||
const onConfirmSuccess = () => {
|
const onConfirmSuccess = async () => {
|
||||||
dictTableRef.value && dictTableRef.value.refresh();
|
if (selectItem.id) {
|
||||||
|
handleAction(
|
||||||
|
Promise.resolve(true),
|
||||||
|
selectItem.id,
|
||||||
|
getTableData,
|
||||||
|
{ showMsg: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dictTableRef.value?.refresh();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// TODO:字段配置
|
// 字段配置
|
||||||
const handlefieldsConfig = (ite) => {
|
const handlefieldsConfig = (ite) => {
|
||||||
fieldsConfigRef.value.open({ ...ite, parentId: ite.id });
|
fieldsConfigRef.value.open({ ...ite, parentId: ite.id });
|
||||||
};
|
};
|
||||||
@@ -281,8 +286,8 @@ const handleDelete = async (item) => {
|
|||||||
ElMessageBox.confirm("确定要删除吗?", "提示", { type: "warning" })
|
ElMessageBox.confirm("确定要删除吗?", "提示", { type: "warning" })
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
try {
|
try {
|
||||||
await deleteDictValue(item.id);
|
// await deleteDictValue(item.id);
|
||||||
dictTableRef.value && dictTableRef.value.refresh();
|
await runDelete(deleteDictValue,item.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("fetch error", error);
|
console.log("fetch error", error);
|
||||||
}
|
}
|
||||||
@@ -302,19 +307,5 @@ const handleDelete = async (item) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-status-dot {
|
|
||||||
cursor: pointer;
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: inline-block;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--data-status-color);
|
|
||||||
margin-right: 8px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
38
src/pages/stage/flow/detail.vue
Normal file
38
src/pages/stage/flow/detail.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
size="100%"
|
||||||
|
class="standard-ui-back-drawer"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
destroy-on-close
|
||||||
|
:with-header="false"
|
||||||
|
modal-class="standard-overlay-dialog-flat"
|
||||||
|
@opened="isOpened = true"
|
||||||
|
@closed="isOpened = false"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="flow-context-wrapper">
|
||||||
|
<nodeFlow v-if="drawerVisible" />
|
||||||
|
</div>
|
||||||
|
<!-- -->
|
||||||
|
<template #footer>
|
||||||
|
<div class="custom-flat-drawer-footer">
|
||||||
|
<div class="stats-info"></div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button link @click="drawerVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" class="btn-confirm">确认保存变更</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import nodeFlow from "@/components/nodeFlow/index.vue";
|
||||||
|
defineOptions({ name: "FlowDetail" });
|
||||||
|
const drawerVisible = defineModel("drawerVisible", { default: true });
|
||||||
|
const isOpened = ref(false);
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
96
src/pages/stage/flow/flowCard.vue
Normal file
96
src/pages/stage/flow/flowCard.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flow-card">
|
||||||
|
<div class="card-cover">
|
||||||
|
<img src="https://via.placeholder.com/300x180" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="title-row">
|
||||||
|
<h3 class="mj-ellipsis-one-line title">商机通用跟进流程</h3>
|
||||||
|
<el-icon class="more-icon"><MoreFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="meta-info">
|
||||||
|
<span>创建人:程彬</span>
|
||||||
|
<span class="divider">|</span>
|
||||||
|
<span>2024-12-20</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { MoreFilled } from "@element-plus/icons-vue";
|
||||||
|
defineOptions({ name: "FlowCard" });
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flow-card {
|
||||||
|
--card-active-border-color: #409eff;
|
||||||
|
--card-radius:5px;
|
||||||
|
width: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dce2e7;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--card-active-border-color);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-cover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 5px 12px;
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-icon {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #90a1b9;
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: #dce2e7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="mj-flow-container">
|
||||||
|
<stageBreadcrumbs title="SOP流程管理" styleClass="stage-breadcrumbs-list">
|
||||||
</div>
|
<template #content>
|
||||||
|
<OverflowTabs
|
||||||
|
:itemMap="{ id: 'id', label: 'name' }"
|
||||||
|
v-model="activeTab"
|
||||||
|
:items="tabList"
|
||||||
|
:height="60"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<div class="search-auto-expand-input">
|
||||||
|
<el-input
|
||||||
|
placeholder="搜索流程名称..."
|
||||||
|
class="auto-expand-input"
|
||||||
|
:prefix-icon="'Search'"
|
||||||
|
v-model="searchVal"
|
||||||
|
@keyup.enter="fetchTableData"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</stageBreadcrumbs>
|
||||||
|
<div class="mj-flow-content">
|
||||||
|
<subTabs v-model:tabs="tabsIndex" :menuList="menuList" />
|
||||||
|
<div class="mj-card-container mj-flow-card-container">
|
||||||
|
<div v-for="t in 5" :key="t" class="mj-flow-card-grid">
|
||||||
|
<flowCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 详情 -->
|
||||||
|
<!-- <flow-detail /> -->
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {reactive,ref,onMounted} from "vue"
|
import subTabs from "./subTabs.vue";
|
||||||
|
import flowCard from "./flowCard.vue";
|
||||||
|
import FlowDetail from './detail.vue';
|
||||||
|
|
||||||
defineOptions({})
|
defineOptions({ name: "Flow" });
|
||||||
|
const activeTab = ref<string>(1);
|
||||||
|
const searchVal = ref<string>("");
|
||||||
|
|
||||||
|
const tabsIndex = ref(1);
|
||||||
|
const menuList = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "线索管理",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "客户管理",
|
||||||
|
icon: "Setting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "游戏与工作室",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "商机管理",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const tabList = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "商机",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "合同",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "项目",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "招聘",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const fetchTableData = () => {
|
||||||
|
console.log("fetchTableData");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
|||||||
55
src/pages/stage/flow/subTabs.vue
Normal file
55
src/pages/stage/flow/subTabs.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mj-subnav-container">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in menuList"
|
||||||
|
:key="index"
|
||||||
|
:class="['nav-item', { 'is-active': tabs === index }]"
|
||||||
|
@click="tabs = index"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "subTabs" });
|
||||||
|
const props = defineProps({
|
||||||
|
menuList: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = defineModel("tabs");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mj-subnav-container {
|
||||||
|
$primary-color: var(--el-color-primary);
|
||||||
|
$bg-active: #EFF6FF;
|
||||||
|
$text-idle: #90A1B9;
|
||||||
|
$text-active: var(--el-color-primary);
|
||||||
|
$border-active:#DEECFE;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 12px 0;
|
||||||
|
.nav-item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-idle;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.12s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: $text-active;
|
||||||
|
background-color: $bg-active;
|
||||||
|
border: 1px solid $border-active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,8 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { Timer } from "@element-plus/icons-vue";
|
|
||||||
|
|
||||||
const logData = [
|
const logData = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,16 +9,15 @@
|
|||||||
<div class="info-text">
|
<div class="info-text">
|
||||||
<div class="title-row">
|
<div class="title-row">
|
||||||
<span class="main-title">集团1</span>
|
<span class="main-title">集团1</span>
|
||||||
<el-tag size="small" effect="plain" class="title-tag"
|
<!-- TODO:这块不要展示tag -->
|
||||||
>集团</el-tag
|
<!-- <el-tag size="small" effect="plain" class="title-tag">集团</el-tag> -->
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-id">ID: 3</div>
|
<div class="sub-id">ID: 3</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-actions">
|
<div class="right-actions">
|
||||||
<el-button plain :icon="EditPen">编辑配置</el-button>
|
<el-button plain :icon="'EditPen'">编辑配置</el-button>
|
||||||
<el-button type="danger" plain :icon="CircleClose">禁用</el-button>
|
<el-button :type="turnOn ? 'success':'danger'" plain :icon="turnOn ? 'CircleCheck' : 'CircleClose'" @click="onButtonCheck">{{turnOn ? '启用' : '禁用'}}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -39,8 +38,8 @@
|
|||||||
>
|
>
|
||||||
<div class="info-label">{{ base.name }}</div>
|
<div class="info-label">{{ base.name }}</div>
|
||||||
<div class="info-value">
|
<div class="info-value">
|
||||||
<el-button link type="primary" v-if="base.slotName === 'link'">{{base.value}} ></el-button>
|
<el-button link type="primary" v-if="base.slotName === 'link'" @click="emit('item-click',{evt:'open'})">已开启 ></el-button>
|
||||||
<span v-else>{{ base.value }}</span>
|
<span v-else>{{ info[base.value] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,21 +77,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { OfficeBuilding, EditPen, CircleClose } from "@element-plus/icons-vue";
|
const props = defineProps({
|
||||||
|
info:{
|
||||||
|
Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['item-click'])
|
||||||
|
const turnOn = ref(true);
|
||||||
const systemList = [
|
const systemList = [
|
||||||
{
|
{
|
||||||
name: "生效时间",
|
name: "生效时间",
|
||||||
value: "2024-01-01",
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "过期时间",
|
name: "过期时间",
|
||||||
value: "永久生效",
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "最后更新",
|
name: "最后更新",
|
||||||
value: "2025-12-29 16:06",
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "更新人",
|
name: "更新人",
|
||||||
@@ -104,22 +109,36 @@ const systemList = [
|
|||||||
const baseList = [
|
const baseList = [
|
||||||
{
|
{
|
||||||
name: "部门/公司名称",
|
name: "部门/公司名称",
|
||||||
value: "广州分公司",
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "上级单位",
|
name: "上级单位",
|
||||||
value: "集团1",
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "同步企微",
|
name: "同步企微",
|
||||||
value: "已开启",
|
value: "",
|
||||||
slotName: "link",
|
slotName: "link",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "部门负责人",
|
name: "部门负责人",
|
||||||
value: "赵康, 李思奇, 董峥",
|
value: "",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// 禁用-启用功能
|
||||||
|
const onButtonCheck = () =>{
|
||||||
|
turnOn.value = !turnOn.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启微信
|
||||||
|
|
||||||
|
const onOpenWeixin = () =>{
|
||||||
|
console.log('onOpenWeixin')
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
232
src/pages/stage/origanization/addOrgan.vue
Normal file
232
src/pages/stage/origanization/addOrgan.vue
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<div class="enterprise-dialog-wrapper">
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="创建企业配置"
|
||||||
|
width="500px"
|
||||||
|
:show-close="true"
|
||||||
|
class="standard-ui-dialog"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<div class="header-icon">
|
||||||
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="header-text">
|
||||||
|
<h3>创建企业配置</h3>
|
||||||
|
<span>对接企微组织架构</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" layout="vertical" label-position="top">
|
||||||
|
<!-- 编辑的时候展示 同步按钮 -->
|
||||||
|
|
||||||
|
<template v-if="isSyncConfig">
|
||||||
|
<el-form-item>
|
||||||
|
<div class="sync-card">
|
||||||
|
<div class="sync-card__content">
|
||||||
|
<h3 class="sync-card__title">开启企微同步</h3>
|
||||||
|
<p class="sync-card__desc">开启后将定期拉取企微通讯录数据</p>
|
||||||
|
</div>
|
||||||
|
<div class="sync-card__action">
|
||||||
|
<el-switch v-model="form.syncEnabled" size="large" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="企业名称">
|
||||||
|
<el-input v-model="form.name" placeholder="如:名匠视界" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12" v-if="!isSyncConfig">
|
||||||
|
<el-form-item label="域名">
|
||||||
|
<el-input v-model="form.domain" placeholder="example.com">
|
||||||
|
<template #prefix
|
||||||
|
><el-icon><component :is="'Global'" /></el-icon
|
||||||
|
></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="微信 CORPID">
|
||||||
|
<el-input v-model="form.corpId" placeholder="ww123...">
|
||||||
|
<template #prefix
|
||||||
|
><el-icon><CircleCheck /></el-icon
|
||||||
|
></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="应用 ID">
|
||||||
|
<el-input v-model="form.appId" placeholder="1000001">
|
||||||
|
<template #prefix
|
||||||
|
><el-icon><Postcard /></el-icon
|
||||||
|
></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="isSyncConfig ? 24 : 12">
|
||||||
|
<el-form-item label="应用密钥">
|
||||||
|
<el-input
|
||||||
|
v-model="form.appSecret"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
show-password
|
||||||
|
>
|
||||||
|
<template #prefix
|
||||||
|
><el-icon><Key /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="校验 TOKEN">
|
||||||
|
<el-input v-model="form.token" placeholder="输入校验 Token" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="加解密密钥">
|
||||||
|
<el-input v-model="form.aesKey" placeholder="输入 EncodingAESKey" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="org-ganization-footer">
|
||||||
|
<el-button @click="dialogVisible = false" round size="large">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="btn-confirm"
|
||||||
|
round
|
||||||
|
size="large"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>确认创建</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({ name: "AddOrgan" });
|
||||||
|
|
||||||
|
const dialogVisible = defineModel("visible");
|
||||||
|
const {isSyncConfig=false} = defineProps<{
|
||||||
|
isSyncConfig: boolean;
|
||||||
|
}>();
|
||||||
|
const form = reactive({
|
||||||
|
name: "",
|
||||||
|
domain: "",
|
||||||
|
corpId: "",
|
||||||
|
appId: "",
|
||||||
|
appSecret: "",
|
||||||
|
token: "",
|
||||||
|
aesKey: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.enterprise-dialog-wrapper {
|
||||||
|
// 深度覆盖 Element Plus 样式
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5f6d7e;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
background-color: #f7f9fb;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&.is-focus {
|
||||||
|
border-color: #2b6df8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶部标题布局
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
background-color: #edf2ff;
|
||||||
|
color: #2b6df8;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text {
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-ganization-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.el-button {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18px 20px;
|
||||||
|
background-color: #f8faff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #edf2f9;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px; // 标题和描述的间距
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129; // 深色标题
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #86909c; // 灰色描述
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
margin-left: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
<div class="mj-organization">
|
<div class="mj-organization">
|
||||||
<!-- 顶部的tabs菜单 -->
|
<!-- 顶部的tabs菜单 -->
|
||||||
<div class="organization-tabs">
|
<div class="organization-tabs">
|
||||||
<stageBreadcrumbs title="组织管理">
|
<stageBreadcrumbs title="组织管理" style-class="stage-breadcrumbs-list">
|
||||||
<template #content>
|
<template #content>
|
||||||
<OverflowTabs v-model="activeTab" :items="tabList" :height="60"/>
|
<OverflowTabs :itemMap="{id:'id',label:'name'}" v-model="activeTab" :items="tabList" :height="60" />
|
||||||
</template>
|
</template>
|
||||||
<template #action>
|
<template #action>
|
||||||
<el-button type="primary" :icon="Plus" plain>新增集团</el-button>
|
<el-button type="primary" :icon="'Plus'" plain @click="onAddGroup"
|
||||||
|
>新增集团</el-button
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</stageBreadcrumbs>
|
</stageBreadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,24 +20,49 @@
|
|||||||
<div class="mj-panel-title org-tree-head">组织架构</div>
|
<div class="mj-panel-title org-tree-head">组织架构</div>
|
||||||
<div class="org-tree-search">
|
<div class="org-tree-search">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="search"
|
v-model="filterText"
|
||||||
:prefix-icon="Search"
|
:prefix-icon="'Search'"
|
||||||
placeholder="搜索部门或公司"
|
placeholder="搜索部门或公司"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="org-tree-list">
|
<div class="org-tree-list">
|
||||||
<el-tree
|
<el-tree
|
||||||
:data="data"
|
v-loading="treeLoading"
|
||||||
|
ref="treeRef"
|
||||||
|
:data="treeData"
|
||||||
|
lazy
|
||||||
|
:load="loadNode"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
:props="defaultProps"
|
:props="defaultProps"
|
||||||
@node-click="handleNodeClick"
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<div class="org-tree-item">
|
||||||
|
<div class="org-tree-item-left">
|
||||||
|
<DynamicSvgIcon
|
||||||
|
:name="getIconComponent(node)"
|
||||||
|
:size="15"
|
||||||
|
:hover-color="'#67c23a'"
|
||||||
|
:color="getIconColor(node)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AutoTooltip :content="node.label" class="mj-ellipsis-one-line tree-node-label" />
|
||||||
|
</div>
|
||||||
|
<div class="org-tree-item-right">
|
||||||
|
<el-icon :size="15" @click.stop="onOrgTreeDelete(node,data)">
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="org-bottom-add">
|
<div class="org-bottom-add">
|
||||||
<el-input v-model="addValue" placeholder="快速添加分公司...">
|
<el-input v-model="addValue" placeholder="快速添加分公司...">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-button text type="primary" :icon="Plus"> </el-button>
|
<el-button text type="primary" :icon="'Plus'"> </el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,120 +70,202 @@
|
|||||||
<div class="mj-organization-card organization-info">
|
<div class="mj-organization-card organization-info">
|
||||||
<el-tabs v-model="activeName" class="organization-info-tabs">
|
<el-tabs v-model="activeName" class="organization-info-tabs">
|
||||||
<el-tab-pane label="基础信息" name="baseInfo">
|
<el-tab-pane label="基础信息" name="baseInfo">
|
||||||
<OrganizationDetail />
|
<OrganizationDetail :info="detailInfo" @item-click="handleItemClick"/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="动态日志" name="auditLogs">
|
<el-tab-pane label="动态日志" name="auditLogs">
|
||||||
<AuditLogs />
|
<AuditLogs :info="detailInfo"/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加集团-->
|
||||||
|
<addOrgan v-model:visible="showAddOrgan" :isSyncConfig="isSyncConfig"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Plus, Search } from "@element-plus/icons-vue";
|
|
||||||
import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
|
import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
|
||||||
import OverflowTabs from "@/components/overflowTabs/index.vue";
|
import OverflowTabs from "@/components/overflowTabs/index.vue";
|
||||||
import AuditLogs from "./AuditLogs.vue";
|
import AuditLogs from "./auditLogs.vue";
|
||||||
import OrganizationDetail from "./OrganizationDetail.vue";
|
import OrganizationDetail from "./organizationDetail.vue";
|
||||||
import { OfficeBuilding, EditPen, CircleClose } from "@element-plus/icons-vue";
|
import DynamicSvgIcon from "@/components/dynamicSvgIcon/index.vue";
|
||||||
defineOptions({ name: "Organization" });
|
import addOrgan from "./addOrgan.vue";
|
||||||
|
import AutoTooltip from "@/components/autoTooltip/index.vue";
|
||||||
const addValue = ref("");
|
import { debounce } from 'lodash-es'
|
||||||
const search = ref("");
|
import {
|
||||||
const activeName = ref("baseInfo");
|
getEnterprise,
|
||||||
|
addEnterprise,
|
||||||
// 集团Tabs切换
|
enableEnterprise,
|
||||||
const activeTab = ref(1);
|
disableEnterprise,
|
||||||
const tabList = ref([
|
getEnterpriseOrg,
|
||||||
{ id: 1, label: '集团1' },
|
getEnterpriseUser,
|
||||||
{ id: 2, label: '集团2' },
|
getEnterpriseDetail,
|
||||||
{ id: 3, label: '集团3' },
|
getEnterpriseOrgDetail
|
||||||
{ id: 4, label: '集团4' },
|
} from "@/api/stage/organization";
|
||||||
{ id: 5, label: '集团5' },
|
|
||||||
{ id: 6, label: '集团6' },
|
|
||||||
]);
|
|
||||||
interface Tree {
|
interface Tree {
|
||||||
label: string;
|
label: string;
|
||||||
children?: Tree[];
|
children?: Tree[];
|
||||||
}
|
}
|
||||||
|
defineOptions({ name: "Organization" });
|
||||||
|
|
||||||
const handleNodeClick = (data: Tree) => {
|
const addValue = ref("");
|
||||||
console.log(data);
|
const activeName = ref("baseInfo");
|
||||||
};
|
const isSyncConfig = ref(false);
|
||||||
|
const showAddOrgan = ref(false);
|
||||||
const data: Tree[] = [
|
const treeData = ref([]);
|
||||||
{
|
// 集团Tabs切换
|
||||||
label: "Level one 1",
|
const activeTab = ref('');
|
||||||
children: [
|
const tabList = ref([]);
|
||||||
{
|
const filterText = ref('');
|
||||||
label: "Level two 1-1",
|
const treeRef = ref(null);
|
||||||
children: [
|
const treeLoading = ref(false);
|
||||||
{
|
const detailInfo = reactive<Record<string, any>>({});
|
||||||
label: "Level three 1-1-1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Level one 2",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Level two 2-1",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Level three 2-1-1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Level two 2-2",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Level three 2-2-1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Level one 3",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Level two 3-1",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Level three 3-1-1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Level two 3-2",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Level three 3-2-1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
children: "children",
|
children: "children",
|
||||||
label: "label",
|
label: "name",
|
||||||
|
isLeaf:'leaf'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// children相关事件
|
||||||
|
const handleItemClick = (item)=>{
|
||||||
|
const { evt } = item;
|
||||||
|
if(evt === 'open'){
|
||||||
|
showAddOrgan.value = true;
|
||||||
|
isSyncConfig.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载子机构
|
||||||
|
const loadNode = async (node, resolve, reject) =>{
|
||||||
|
try {
|
||||||
|
const response = await getEnterpriseOrg(node.id);
|
||||||
|
resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('search error:',error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(filterText, (val) => {
|
||||||
|
treeRef.value!.filter(val)
|
||||||
|
})
|
||||||
|
// 过滤节点
|
||||||
|
const filterNode = (value: string, data:Record<string, any>) => {
|
||||||
|
if (!value) return true
|
||||||
|
return data.name.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加集团
|
||||||
|
const onAddGroup = () => {
|
||||||
|
showAddOrgan.value = true;
|
||||||
|
};
|
||||||
|
// 获取图标组件
|
||||||
|
const getIconComponent = (node: any) => {
|
||||||
|
if (node.level === 1) {
|
||||||
|
return "building"; // 一级节点使用
|
||||||
|
} else if (node.level === 2) {
|
||||||
|
return `flag`; // 二级节点使用其他图标
|
||||||
|
} else {
|
||||||
|
return "flag"; // 更深层级的默认图标
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取图标颜色(可选)
|
||||||
|
const getIconColor = (node: any) => {
|
||||||
|
return "#9EADC2";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 获取企业列表
|
||||||
|
const getEnterpriseList = async () => {
|
||||||
|
try {
|
||||||
|
const queryParams = { name: "", domain: "" };
|
||||||
|
const res = await getEnterprise(queryParams);
|
||||||
|
activeTab.value = res[0]?.id || "";
|
||||||
|
getOrgTree(activeTab.value);
|
||||||
|
tabList.value = res;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('getEnterpriseList error:',error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取组织架构数
|
||||||
|
const getOrgTree = async (id:number|string) => {
|
||||||
|
treeLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getEnterpriseOrg(id);
|
||||||
|
treeData.value = response;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('get org error:',error);
|
||||||
|
treeData.value = [];
|
||||||
|
} finally {
|
||||||
|
treeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取机构-企业详情数据
|
||||||
|
const apiMap = {
|
||||||
|
1: getEnterpriseOrg, //机构详情
|
||||||
|
2: getEnterpriseUser //员工详情
|
||||||
|
}
|
||||||
|
const orgDetail = async (data:Record<string,any>) =>{
|
||||||
|
const { kind,id } = data
|
||||||
|
try {
|
||||||
|
const response = await apiMap[kind](id);
|
||||||
|
Object.assign(detailInfo, response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('orgDetail error:',error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取企业详情
|
||||||
|
const getEnterpriseOrgDetail = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getEnterpriseDetail();
|
||||||
|
console.log('获取当前的企业详情数据信息:',response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('getEnterpriseDetail error:',error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除部门-人员-组织
|
||||||
|
const onOrgTreeDelete = async (node,data) => {
|
||||||
|
ElMessageBox.confirm("确定要删除吗?", "提示", { type: "warning" })
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
// await disableEnterprise(data.id);
|
||||||
|
treeRef.value && treeRef.value.remove(node);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('fetch error',error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 请求机构
|
||||||
|
const debouncedGetOrgTree = debounce((id) => {
|
||||||
|
if(!id) return;
|
||||||
|
getOrgTree(id);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// 监听切换
|
||||||
|
watch(activeTab, (val) => {
|
||||||
|
debouncedGetOrgTree(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击树节点获取数据
|
||||||
|
const handleNodeClick = (data: Tree) => {
|
||||||
|
orgDetail(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
Promise.all([getEnterpriseOrgDetail(),getEnterpriseList()]);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "sass:math";
|
@use "sass:math";
|
||||||
.mj-organization {
|
.mj-organization {
|
||||||
.organization-tabs{
|
height: 100%;
|
||||||
:deep(.stage-breadcrumbs){
|
|
||||||
// border-bottom: none;
|
|
||||||
padding:0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mj-organization-card {
|
.mj-organization-card {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid #e2e8f099;
|
border: 1px solid #e2e8f099;
|
||||||
@@ -165,6 +274,7 @@ const defaultProps = {
|
|||||||
box-shadow: 0 0 6px #e9e8e8;
|
box-shadow: 0 0 6px #e9e8e8;
|
||||||
}
|
}
|
||||||
.organization-content {
|
.organization-content {
|
||||||
|
height: calc(100% - 60px);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
.org-tree {
|
.org-tree {
|
||||||
@@ -184,6 +294,36 @@ const defaultProps = {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: math.div($mj-padding-standard, 2) $mj-padding-standard;
|
padding: math.div($mj-padding-standard, 2) $mj-padding-standard;
|
||||||
|
.org-tree-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
.org-tree-item-right {
|
||||||
|
color: #a5adb8;
|
||||||
|
&:hover {
|
||||||
|
color: #2065fc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon) {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.org-tree-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
:deep(.el-icon) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
.tree-node-label{
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.org-bottom-add {
|
.org-bottom-add {
|
||||||
border-top: 1px solid #f1f5f9;
|
border-top: 1px solid #f1f5f9;
|
||||||
|
|||||||
306
src/pages/stage/permission/addRoles.vue
Normal file
306
src/pages/stage/permission/addRoles.vue
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="computedTitle"
|
||||||
|
width="500px"
|
||||||
|
class="standard-ui-flat-dialog"
|
||||||
|
modal-class="standard-overlay-dialog-flat"
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
top="6vh"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" label-position="top" :rules="rules">
|
||||||
|
<div class="row-flex">
|
||||||
|
<el-form-item label="角色名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入角色名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色编码" prop="code">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
placeholder="ROLE_CODE"
|
||||||
|
:disabled="detailId ? true : false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="角色类型" prop="type">
|
||||||
|
<div class="full-width-radio">
|
||||||
|
<BaseSegmented
|
||||||
|
v-model="form.type"
|
||||||
|
:options="dicts.permission_role_type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="isOrgRelated">
|
||||||
|
<div class="feature-card light-green">
|
||||||
|
<div class="info">
|
||||||
|
<div class="title">组织架构</div>
|
||||||
|
<div class="desc">是否将该角色与系统组织架构体系进行深度关联</div>
|
||||||
|
</div>
|
||||||
|
<el-switch
|
||||||
|
v-model="form.isOrgRelated"
|
||||||
|
style="
|
||||||
|
--el-switch-on-color: #13ce66;
|
||||||
|
--el-switch-off-color: #cad5e2;
|
||||||
|
"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="关联岗位" prop="positionIds">
|
||||||
|
<el-select
|
||||||
|
v-model="form.positionIds"
|
||||||
|
placeholder="请选择关联岗位"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="remoteMethod"
|
||||||
|
:loading="remoteLoading"
|
||||||
|
collapse-tags
|
||||||
|
:fit-input-width="true"
|
||||||
|
:teleported="false"
|
||||||
|
v-select-more="handleLoadMore"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
v-for="(item, index) in options"
|
||||||
|
:key="item.id"
|
||||||
|
/>
|
||||||
|
<el-option v-if="loadMore" disabled value="more" class="mj-select-dropdown-loading">
|
||||||
|
<div class="status-container">
|
||||||
|
<el-icon v-if="loadMore" class="is-loading" :size="20"
|
||||||
|
><Loading
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="status">
|
||||||
|
<div class="feature-card light-blue">
|
||||||
|
<div class="info">
|
||||||
|
<div class="title">启用状态</div>
|
||||||
|
<div class="desc">控制该角色及其下属权限是否立即生效</div>
|
||||||
|
</div>
|
||||||
|
<el-switch
|
||||||
|
v-model="form.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注说明" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
resize="none"
|
||||||
|
placeholder="请输入备注说明..."
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="cancelRoles" text>取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit(formRef)"
|
||||||
|
>确认创建</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseSegmented from "./baseSegmented.vue";
|
||||||
|
import { Loading } from "@element-plus/icons-vue";
|
||||||
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
|
import {
|
||||||
|
addRole,
|
||||||
|
updateRole,
|
||||||
|
getRoleDetail,
|
||||||
|
} from "@/api/stage/permission/index.ts";
|
||||||
|
import { getEnterprisePosition } from "@/api/stage/organization";
|
||||||
|
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
|
||||||
|
import { useDict } from '@/hooks/useDictData';
|
||||||
|
const { dicts,refresh } = useDict('permission_role_type');
|
||||||
|
defineOptions({ name: "addRoles" });
|
||||||
|
const dialogVisible = defineModel("visible", { type: Boolean, default: false });
|
||||||
|
const props = defineProps({
|
||||||
|
detailId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
remoteLoading,
|
||||||
|
loadMore,
|
||||||
|
noMore,
|
||||||
|
remoteMethod,
|
||||||
|
handleLoadMore,
|
||||||
|
} = useSelectLoadMore({
|
||||||
|
fetchApi: (p) => getEnterprisePosition({ ...p }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedTitle = computed(() => {
|
||||||
|
return props.detailId ? "编辑系统角色" : "新增系统角色";
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["on-success"]);
|
||||||
|
const positionList = ref([]); //岗位列表数据
|
||||||
|
const formRef = ref<FormInstance>(null);
|
||||||
|
const form = reactive<RuleForm>({
|
||||||
|
name: "",
|
||||||
|
code: "",
|
||||||
|
type: "DEFAULT",
|
||||||
|
isOrgRelated: 0, // 是否和组织架构深度关联
|
||||||
|
positionIds: "",
|
||||||
|
status: 1,
|
||||||
|
remark: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = reactive<FormInstance<RuleForm>>({
|
||||||
|
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||||
|
code: [{ required: true, message: "请输入角色编码", trigger: "blur" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pageInfo = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancelRoles = () => {
|
||||||
|
formRef.value && formRef.value.resetFields();
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取角色详情
|
||||||
|
const getRoleShowDetail = async (val) => {
|
||||||
|
try {
|
||||||
|
const res = await getRoleDetail(props.detailId);
|
||||||
|
Object.assign(form, res, { isOrgRelated: res.isOrgRelated ? 1 : 0 });
|
||||||
|
} catch (error) {
|
||||||
|
console.log("getRoleDetail error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听详情
|
||||||
|
watch(
|
||||||
|
() => dialogVisible.value,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.detailId) {
|
||||||
|
getRoleShowDetail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 新增编辑角色(仅系统类型可新增)
|
||||||
|
const handleSubmit = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
const res = props.detailId
|
||||||
|
? await updateRole(form)
|
||||||
|
: await addRole(form);
|
||||||
|
ElMessage.success("操作成功");
|
||||||
|
cancelRoles();
|
||||||
|
emit("on-success");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("error submit!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化刷新角色类型值
|
||||||
|
onMounted(() => {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.standard-ui-flat-dialog {
|
||||||
|
.el-dialog__header {
|
||||||
|
.el-dialog__title {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: var(--blue-block-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单标签样式
|
||||||
|
.el-form-item__label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #86909c;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 两列布局
|
||||||
|
.row-flex {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
.el-form-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.full-width-radio {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绿色和蓝色的特色功能卡片
|
||||||
|
.feature-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.light-green {
|
||||||
|
background-color: #f6fffa; // 浅绿色背景
|
||||||
|
border: 1px solid #e8f3ee;
|
||||||
|
}
|
||||||
|
&.light-blue {
|
||||||
|
background-color: #f0f7ff; // 浅蓝色背景
|
||||||
|
border: 1px solid #e8f0f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
91
src/pages/stage/permission/baseSegmentMenu.vue
Normal file
91
src/pages/stage/permission/baseSegmentMenu.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="permission-scroll-area">
|
||||||
|
<div v-for="group in menuList" :key="group.id" class="permission-group">
|
||||||
|
<div class="group-header">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="group.selected"
|
||||||
|
:indeterminate="group.isIndeterminate"
|
||||||
|
@change="(val) => handleGroupCheckAll(group, val)"
|
||||||
|
>
|
||||||
|
<span class="group-title">{{ group.name }}</span>
|
||||||
|
<span class="group-count" v-if="group.children">({{ getCheckedCount(group) }}/{{ group.children.length }})</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-body">
|
||||||
|
<div v-for="row in group.children" :key="row.id" class="permission-row">
|
||||||
|
<div class="row-label">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="row.selected"
|
||||||
|
:indeterminate="row.isIndeterminate"
|
||||||
|
@change="() => handleRowChange(group, row)"
|
||||||
|
>
|
||||||
|
{{ row.name }}
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="row-actions">
|
||||||
|
<el-checkbox-group
|
||||||
|
v-model="row.actions"
|
||||||
|
@change="() => handleActionChange(group, row)"
|
||||||
|
>
|
||||||
|
<el-checkbox :value="check.id" v-for="(check,checkIndex) in row.operations" :class="check.code.search('delete') > -1 ? 'is-danger' : ''">{{ check.name }}</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({name: "baseSegmentedMenu"});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
menuList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取已选中的子项数量
|
||||||
|
const getCheckedCount = (group) =>{
|
||||||
|
return (group.children || []).filter((item) => item.selected).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理一级全选
|
||||||
|
const handleGroupCheckAll = (group, val) => {
|
||||||
|
group.children.forEach((row) => {
|
||||||
|
row.selected = val;
|
||||||
|
row.actions = val ? row.operations.map((item) => item.id) : [];
|
||||||
|
});
|
||||||
|
group.isIndeterminate = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理二级勾选
|
||||||
|
const handleRowChange = (group, row) => {
|
||||||
|
row.actions = row.selected ? row.operations.map((item) => item.id) : [];
|
||||||
|
updateGroupStatus(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理三级按钮勾选
|
||||||
|
const handleActionChange = (group, row) => {
|
||||||
|
if (row.actions.length > 0) row.selected = true;
|
||||||
|
const allActions = row.operations && row.operations.length;
|
||||||
|
const checkedActionsCount = row.actions ? row.actions.length : 0;
|
||||||
|
row.isIndeterminate = checkedActionsCount > 0 && checkedActionsCount < allActions;
|
||||||
|
row.selected = checkedActionsCount > 0 && checkedActionsCount === allActions;
|
||||||
|
updateGroupStatus(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新父级的半选/全选状态
|
||||||
|
const updateGroupStatus = (group) => {
|
||||||
|
const children = group.children || [];
|
||||||
|
const total = children.length;
|
||||||
|
const fullyCheckedCount = children.filter(c => c.selected && !c.isIndeterminate).length;
|
||||||
|
const anyCheckedCount = children.filter(c => c.selected || c.isIndeterminate).length;
|
||||||
|
group.selected = total > 0 && fullyCheckedCount === total;
|
||||||
|
group.isIndeterminate = !group.selected && anyCheckedCount > 0;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use './baseSegmentedPermission.scss' as *;
|
||||||
|
</style>
|
||||||
106
src/pages/stage/permission/baseSegmented.vue
Normal file
106
src/pages/stage/permission/baseSegmented.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div class="base-segmented-flat">
|
||||||
|
<div v-for="item in options" :key="item.value" class="segmented-item">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:id="uid + item.value"
|
||||||
|
:name="uid"
|
||||||
|
:value="item.value"
|
||||||
|
:checked="modelValue === item.value"
|
||||||
|
@change="handleChange(item.value)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="uid + item.value"
|
||||||
|
:class="{ 'is-active': modelValue === item.value }"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useFormItem } from "element-plus";
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: [String, Number],
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.options, (val) => {
|
||||||
|
// 1. 获取父组件的值
|
||||||
|
console.log("父组件的值111:", val);
|
||||||
|
},{deep:true});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
// 生成唯一ID,防止页面存在多个组件时 name 冲突
|
||||||
|
const uid = `seg-${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
const { formItem } = useFormItem();
|
||||||
|
|
||||||
|
const handleChange = (val) => {
|
||||||
|
// 2. 更新父组件的值
|
||||||
|
emit("update:modelValue", val);
|
||||||
|
emit("change", val);
|
||||||
|
|
||||||
|
// 通知 el-form-item 进行校验
|
||||||
|
if (formItem) {
|
||||||
|
formItem.validate("change").catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "sass:color";
|
||||||
|
$bg-color: #f2f3f5;
|
||||||
|
$active-bg: #ffffff;
|
||||||
|
$primary-color: #1661ff;
|
||||||
|
$text-secondary: #86909c;
|
||||||
|
|
||||||
|
.base-segmented-flat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
background-color: $bg-color;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.segmented-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text-secondary;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: color.adjust($text-secondary, $lightness: 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中状态样式
|
||||||
|
&.is-active {
|
||||||
|
background-color: $active-bg;
|
||||||
|
color: $primary-color;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
src/pages/stage/permission/baseSegmentedPermission.scss
Normal file
106
src/pages/stage/permission/baseSegmentedPermission.scss
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
.permission-container {
|
||||||
|
--primary-blue: #1661ff;
|
||||||
|
--danger-red: #f53f3f;
|
||||||
|
--text-main: #1d2129;
|
||||||
|
--text-grey: #86909c;
|
||||||
|
--border-color: #f2f3f5;
|
||||||
|
--common-padding:24px;
|
||||||
|
|
||||||
|
.custom-permission-tabs {
|
||||||
|
--el-tabs-header-height:50px;
|
||||||
|
--el-border-color-light:transparent;
|
||||||
|
:deep(.el-tabs__header){
|
||||||
|
padding: 0 var(--common-padding);
|
||||||
|
background-color: #FBFCFD;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
:deep(.el-tabs__content){
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
:deep(.el-tabs__item) {
|
||||||
|
font-size: 14px;
|
||||||
|
&.is-active {
|
||||||
|
color: var(--primary-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.permission-scroll-area{
|
||||||
|
padding: 0 var(--common-padding);
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: calc(100vh - 194px); //设置固定的高度
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-group {
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-count {
|
||||||
|
color: var(--text-grey);
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图二中:半选状态的蓝色方块样式 */
|
||||||
|
:deep(.el-checkbox__input.is-indeterminate .el-checkbox__inner) {
|
||||||
|
background-color: var(--primary-blue);
|
||||||
|
border-color: var(--primary-blue);
|
||||||
|
&::before {
|
||||||
|
height: 3px;
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-body {
|
||||||
|
padding-left: 28px;
|
||||||
|
|
||||||
|
.permission-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
width: 180px;
|
||||||
|
:deep(.el-checkbox__label) {
|
||||||
|
color: var(--text-grey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
:deep(.el-checkbox) {
|
||||||
|
margin-right: 32px;
|
||||||
|
|
||||||
|
/* 默认选中文字也保持蓝色 */
|
||||||
|
&.is-checked .el-checkbox__label {
|
||||||
|
color: var(--primary-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊处理:删除 按钮 */
|
||||||
|
&.is-danger.is-checked .el-checkbox__label {
|
||||||
|
color: var(--danger-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用时的文字颜色微调 */
|
||||||
|
:deep(.el-checkbox.is-disabled .el-checkbox__label) {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
363
src/pages/stage/permission/fieldPermissionManager.vue
Normal file
363
src/pages/stage/permission/fieldPermissionManager.vue
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<template>
|
||||||
|
<div class="field-permission-manager">
|
||||||
|
<div class="module-grid">
|
||||||
|
<div
|
||||||
|
v-for="mod in modules"
|
||||||
|
:key="mod.id"
|
||||||
|
class="module-card"
|
||||||
|
:class="{ 'is-active': activeModules === mod.id }"
|
||||||
|
@click="handleModuleChange(mod.id)"
|
||||||
|
>
|
||||||
|
{{ mod.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group-list">
|
||||||
|
<template v-if="fieldGroupsChildren.length">
|
||||||
|
<div
|
||||||
|
v-for="(group, index) in fieldGroupsChildren"
|
||||||
|
:key="group.id"
|
||||||
|
class="group-container"
|
||||||
|
:class="{ 'is-collapsed': collapsedIds.has(group.id) }"
|
||||||
|
>
|
||||||
|
<div class="group-header" :class="{ 'is-collapsed': collapsedIds.has(group.id) }">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
<span class="group-title">{{ group.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="quick-set-label">快速设置:</span>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<span
|
||||||
|
v-for="opt in dicts.permission_setting_status"
|
||||||
|
:key="opt.value"
|
||||||
|
class="quick-btn"
|
||||||
|
:class="{ 'is-disabled': isProcessing }"
|
||||||
|
@click.stop="batchSetPermissionChunked(group, opt.value)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-icon class="collapse-icon" @click.stop="toggleGroup(group)"
|
||||||
|
><ArrowDown
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field-rows-collapse">
|
||||||
|
<div class="field-rows-inner">
|
||||||
|
<div
|
||||||
|
v-for="field in group.fields"
|
||||||
|
:key="field.id"
|
||||||
|
class="field-item"
|
||||||
|
>
|
||||||
|
<div class="field-info">
|
||||||
|
<i class="tree-line-icon"></i>
|
||||||
|
<span class="field-label">{{ field.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-ctrl">
|
||||||
|
<el-select v-model="field.permission">
|
||||||
|
<el-option
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
v-for="(
|
||||||
|
dict, dicIndex
|
||||||
|
) in dicts.permission_setting_status"
|
||||||
|
:key="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-empty description="暂无数据~" v-else/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { List, Document, ArrowDown } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dicts:{
|
||||||
|
type:Object,
|
||||||
|
default:()=>({})
|
||||||
|
},
|
||||||
|
fieldGroupsList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:fieldGroupsList"]);
|
||||||
|
const isProcessing = ref(false);
|
||||||
|
const processingGroupId = ref(null);
|
||||||
|
const currentActiveId = ref(null);
|
||||||
|
const collapsedIds = ref(new Set()); //可折叠的集合
|
||||||
|
// 渲染顶部的数据
|
||||||
|
const modules = computed(() => {
|
||||||
|
return props.fieldGroupsList.map((group) => {
|
||||||
|
return {
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动态渲染activeModules的值
|
||||||
|
const activeModules = computed(() => currentActiveId.value);
|
||||||
|
|
||||||
|
// 动态匹配对应的下级数据
|
||||||
|
const fieldGroupsChildren = computed({
|
||||||
|
get: () => {
|
||||||
|
if (!currentActiveId.value) return [];
|
||||||
|
return (
|
||||||
|
props.fieldGroupsList.find((group) => group.id === currentActiveId.value)
|
||||||
|
?.fieldGroups || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听值的变化
|
||||||
|
watch(
|
||||||
|
() => props.fieldGroupsList,
|
||||||
|
(newList) => {
|
||||||
|
if (newList.length > 0) {
|
||||||
|
const exists = newList.find((item) => item.id === currentActiveId.value);
|
||||||
|
if (!exists) {
|
||||||
|
currentActiveId.value = newList[0].id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentActiveId.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const batchSetPermissionChunked = (group, type) => {
|
||||||
|
if (isProcessing.value) return;
|
||||||
|
|
||||||
|
const fields = group.fields;
|
||||||
|
const total = fields.length;
|
||||||
|
const chunkSize = 40;
|
||||||
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
isProcessing.value = true;
|
||||||
|
processingGroupId.value = group.groupId;
|
||||||
|
// 同步更新permission的数据
|
||||||
|
group.permission = type;
|
||||||
|
|
||||||
|
const runChunk = () => {
|
||||||
|
const nextLimit = Math.min(currentIndex + chunkSize, total);
|
||||||
|
for (let i = currentIndex; i < nextLimit; i++) {
|
||||||
|
fields[i].permission = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = nextLimit;
|
||||||
|
|
||||||
|
if (currentIndex < total) {
|
||||||
|
// 关键:将控制权交还给浏览器渲染线程,下一帧继续
|
||||||
|
requestAnimationFrame(runChunk);
|
||||||
|
} else {
|
||||||
|
isProcessing.value = false;
|
||||||
|
processingGroupId.value = null;
|
||||||
|
emit("update:fieldGroupsList", [...props.fieldGroupsList]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
runChunk();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换折叠面板
|
||||||
|
const toggleGroup = (group) => {
|
||||||
|
if (collapsedIds.value.has(group.id)) {
|
||||||
|
collapsedIds.value.delete(group.id);
|
||||||
|
} else {
|
||||||
|
collapsedIds.value.add(group.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换顶部权限的模块
|
||||||
|
const handleModuleChange = (id) => {
|
||||||
|
currentActiveId.value = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.field-permission-manager {
|
||||||
|
--primary-blue: #1661ff;
|
||||||
|
--bg-light: #f2f3f5;
|
||||||
|
--border-color: #e5e6eb;
|
||||||
|
--transition-speed: 0.3s;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 190px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
.module-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
width: calc((100% - 48px) / 5);
|
||||||
|
min-width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4e5969;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary-blue);
|
||||||
|
color: var(--primary-blue);
|
||||||
|
}
|
||||||
|
&.is-active {
|
||||||
|
background: var(--primary-blue);
|
||||||
|
border-color: var(--primary-blue);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.field-group-list {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 字段卡片样式
|
||||||
|
.group-container {
|
||||||
|
border: 1px solid var(--bg-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 48px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid var(--bg-light);
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
.el-icon {
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.quick-set-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-right: 16px;
|
||||||
|
.quick-btn {
|
||||||
|
padding: 2px 10px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--primary-blue);
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: #eff4ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collapse-icon {
|
||||||
|
color: #c9cdd4;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.field-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px 10px 32px;
|
||||||
|
border-bottom: 1px solid #fafafa;
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.tree-line-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-ctrl {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rows-collapse {
|
||||||
|
max-height: 6000px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height var(--transition-speed)
|
||||||
|
cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity var(--transition-speed);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折叠后的状态
|
||||||
|
&.is-collapsed {
|
||||||
|
.group-header {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rows-collapse {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: max-height var(--transition-speed) cubic-bezier(0, 1, 0, 1),
|
||||||
|
opacity var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
transition: transform var(--transition-speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,366 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mj-permission-management">
|
<div class="mj-permission-management">
|
||||||
|
<stageBreadcrumbs title="权限管理" style-class="stage-breadcrumbs-list">
|
||||||
|
<template #content>
|
||||||
|
<OverflowTabs v-model="activeTab" :items="tabList" :height="60" />
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<div class="mj-permission-actions">
|
||||||
|
<div class="search-auto-expand-input">
|
||||||
|
<el-input
|
||||||
|
:placeholder="checkRolesText.placeholder"
|
||||||
|
class="auto-expand-input"
|
||||||
|
:prefix-icon="'Search'"
|
||||||
|
v-model="searchVal"
|
||||||
|
@keyup.enter="onRefresh"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
<div class="mj-dict-actions-right">
|
||||||
|
<el-button
|
||||||
|
:icon="'Plus'"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="addBtnClick"
|
||||||
|
>{{ checkRolesText.btnText }}</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</stageBreadcrumbs>
|
||||||
|
<!-- 表格 -->
|
||||||
|
<CommonTable
|
||||||
|
ref="tableRef"
|
||||||
|
:columns="tableColumns"
|
||||||
|
:request-api="getTableData"
|
||||||
|
>
|
||||||
|
</CommonTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- 成员管理 -->
|
||||||
|
<member-selector
|
||||||
|
v-model:visible="showMember"
|
||||||
|
v-model:dataList="memberList"
|
||||||
|
@save-member="handleSaveMember"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 新增角色 -->
|
||||||
|
<add-roles
|
||||||
|
v-model:visible="showRoles"
|
||||||
|
@on-success="onRefresh"
|
||||||
|
:detail-id="selectMember?.id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 权限抽屉 -->
|
||||||
|
<permission-drawer
|
||||||
|
v-model:visible="showPermission"
|
||||||
|
:checkAuth="selectMember"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import CommonTable from "@/components/proTable/proTablev2.vue";
|
||||||
|
import memberSelector from "@/components/memberSelector/index.vue";
|
||||||
|
import addRoles from "./addRoles.vue";
|
||||||
|
import permissionDrawer from "./permissionDrawer.vue";
|
||||||
|
import { DictManage } from "@/dict";
|
||||||
|
import { useDict } from "@/hooks/useDictData";
|
||||||
|
import { formatIndex } from "@/utils/utils";
|
||||||
|
import {
|
||||||
|
getRoleList,
|
||||||
|
getRoleDetail,
|
||||||
|
updateRole,
|
||||||
|
addRole,
|
||||||
|
deleteRole,
|
||||||
|
enableRole,
|
||||||
|
disableRole,
|
||||||
|
getRoleMemberList,
|
||||||
|
copyRolePermission,
|
||||||
|
batchSaveRole,
|
||||||
|
} from "@/api/stage/permission/index.ts";
|
||||||
|
import { useLocalManager } from "@/hooks/useLocalManager";
|
||||||
|
import { useTableAction } from "@/hooks/useTableAction";
|
||||||
|
defineOptions({ name: "PermissionManagement" });
|
||||||
|
const { dicts, refresh } = useDict("permission_list_enable_disable");
|
||||||
|
const activeTab = ref(1);
|
||||||
|
const tableRef = ref(null);
|
||||||
|
const { handleAction, handleDelete } = useTableAction(tableRef);
|
||||||
|
const searchVal = ref("");
|
||||||
|
const memberList = ref([]); // 获取成员角色列表
|
||||||
|
const total = ref(0);
|
||||||
|
const showMember = ref(false);
|
||||||
|
const showRoles = ref(false);
|
||||||
|
const showPermission = ref(false);
|
||||||
|
const selectMember = ref({}); //当前选择的成员数量数据
|
||||||
|
const tabList = [
|
||||||
|
{
|
||||||
|
label: "角色与权限",
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
defineOptions({ name: "PermissionManagement"})
|
const { fullData, displayData, loadMore, noMore, remove, add } =
|
||||||
|
useLocalManager(() => memberList.value);
|
||||||
|
|
||||||
|
const roleColumns = [
|
||||||
|
{
|
||||||
|
prop: "id",
|
||||||
|
label: "编号",
|
||||||
|
width: "80",
|
||||||
|
align: "center",
|
||||||
|
render: ({ rowData, rowIndex }) => {
|
||||||
|
return h("span", `#${formatIndex(rowIndex)}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "name",
|
||||||
|
label: "角色名称",
|
||||||
|
align: "center",
|
||||||
|
valueType: "ellipsis",
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "memberCount",
|
||||||
|
label: "成员数量",
|
||||||
|
align: "center",
|
||||||
|
render: ({ rowData }: any) => {
|
||||||
|
return h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
type: "primary",
|
||||||
|
link: true,
|
||||||
|
icon: "UserFilled",
|
||||||
|
onClick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
addMember(rowData);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() => rowData.memberCount
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "permissionCount",
|
||||||
|
label: "权限数量",
|
||||||
|
align: "center",
|
||||||
|
render:({rowData})=>{
|
||||||
|
return h('span',rowData.permissionCount ? `${rowData.permissionCount}项` : "-")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "status",
|
||||||
|
label: "状态",
|
||||||
|
align: "center",
|
||||||
|
valueType: "status",
|
||||||
|
options: computed(() => dicts.value.permission_list_enable_disable),
|
||||||
|
onClick: ({ cellValue, rowData }) => {
|
||||||
|
handleDictStatus(rowData);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "type",
|
||||||
|
label: "角色类型",
|
||||||
|
align: "center",
|
||||||
|
render: ({ rowData }: any) => {
|
||||||
|
return h(ElTag, { size: "small" }, () => rowData.type);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "positionIds",
|
||||||
|
label: "关联岗位",
|
||||||
|
align: "center",
|
||||||
|
valueType: "ellipsis",
|
||||||
|
render: ({ rowData, rowIndex }) => {
|
||||||
|
return h("span", rowData.positionIds ? rowData.positionIds.join(",") : "-");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updateTime",
|
||||||
|
label: "更新时间",
|
||||||
|
align: "center",
|
||||||
|
valueType: "date",
|
||||||
|
format: "YYYY-MM-DD HH:mm",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "updater",
|
||||||
|
label: "更新人",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "actions",
|
||||||
|
label: "操作",
|
||||||
|
align: "right",
|
||||||
|
width: 200,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: "权限",
|
||||||
|
type: "primary",
|
||||||
|
link: true,
|
||||||
|
permission: ["*"],
|
||||||
|
onClick: (row) => {
|
||||||
|
showPermission.value = true;
|
||||||
|
selectMember.value = { ...row };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "复制",
|
||||||
|
type: "default",
|
||||||
|
link: true,
|
||||||
|
permission: ["*"],
|
||||||
|
onClick: async (row) => {
|
||||||
|
try {
|
||||||
|
await copyRolePermission(row.id);
|
||||||
|
ElMessage.success("复制成功");
|
||||||
|
tableRef.value.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("copy error", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "编辑",
|
||||||
|
type: "default",
|
||||||
|
link: true,
|
||||||
|
permission: ["*"],
|
||||||
|
onClick: (row) => {
|
||||||
|
showRoles.value = true;
|
||||||
|
selectMember.value = { ...row };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "删除",
|
||||||
|
type: "danger",
|
||||||
|
link: true,
|
||||||
|
permission: ["*"],
|
||||||
|
disabled: (row) => {
|
||||||
|
return row.type !== "SYSTEM";
|
||||||
|
},
|
||||||
|
onClick: async (row) => {
|
||||||
|
ElMessageBox.confirm("确定要删除吗?", "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await handleDelete(deleteRole,row.id)
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log("cancel");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableColumns = computed(() => {
|
||||||
|
const columns = {
|
||||||
|
1: roleColumns,
|
||||||
|
}[activeTab.value];
|
||||||
|
return columns;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前成员数量
|
||||||
|
const addMember = (row) => {
|
||||||
|
showMember.value = true;
|
||||||
|
selectMember.value = { ...row };
|
||||||
|
getMemberList({ id: row.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存成员数量
|
||||||
|
const handleSaveMember = async (row) => {
|
||||||
|
const { id } = selectMember.value;
|
||||||
|
const userIds = memberList.value.map((item) => item.id);
|
||||||
|
try {
|
||||||
|
await handleAction(()=>batchSaveRole(id,{userIds}),id, getTableData);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("batch save error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
const onRefresh = () => {
|
||||||
|
tableRef.value && tableRef.value.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听Tabs变化(目前暂时先用不到)
|
||||||
|
// watch(activeTab, () => {
|
||||||
|
// dataList.value = [];
|
||||||
|
// tableRef.value && tableRef.value.refresh();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 启用-停用
|
||||||
|
const handleDictStatus = async (row) => {
|
||||||
|
try {
|
||||||
|
// row.status === 1 ? await disableRole(row.id) : await enableRole(row.id);
|
||||||
|
const apiCall = row.status === 1 ? disableRole(row.id) : enableRole(row.id);
|
||||||
|
await handleAction(apiCall, row.id, getTableData);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 请求接口获取
|
||||||
|
const getTableData = async (params) => {
|
||||||
|
// 正式请求
|
||||||
|
const queryParams = {
|
||||||
|
...(searchVal.value && { name: searchVal.value }),
|
||||||
|
...params
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await getRoleList(queryParams);
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 后面会修改成全部获取获取成员角色列表
|
||||||
|
const getMemberList = async (params) => {
|
||||||
|
try {
|
||||||
|
const response = await getRoleMemberList(params.id);
|
||||||
|
memberList.value = response;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取
|
||||||
|
const checkRolesText = computed(() => {
|
||||||
|
const btnText = {
|
||||||
|
1: "新增角色",
|
||||||
|
}[activeTab.value];
|
||||||
|
|
||||||
|
const placeholder = {
|
||||||
|
1: "搜索角色名称...",
|
||||||
|
}[activeTab.value];
|
||||||
|
|
||||||
|
return {
|
||||||
|
btnText,
|
||||||
|
placeholder,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新增角色
|
||||||
|
const addBtnClick = () => {
|
||||||
|
if (activeTab.value === 1) {
|
||||||
|
showRoles.value = true;
|
||||||
|
selectMember.value = { id: "" };
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.mj-permission-management {
|
||||||
|
:deep(.stage-breadcrumbs) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.mj-permission-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
219
src/pages/stage/permission/permissionDrawer.vue
Normal file
219
src/pages/stage/permission/permissionDrawer.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
size="40%"
|
||||||
|
title="测试评论"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
destroy-on-close
|
||||||
|
:with-header="false"
|
||||||
|
modal-class="standard-overlay-dialog-flat"
|
||||||
|
class="standard-ui-back-drawer"
|
||||||
|
>
|
||||||
|
<div class="permission-drawer-container">
|
||||||
|
<!-- header -->
|
||||||
|
<div class="customer-drawer-header">
|
||||||
|
<div class="title-row">
|
||||||
|
<span class="title">权限配置</span>
|
||||||
|
<el-icon class="close-icon" @click="drawerVisible = false"
|
||||||
|
><Close
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="sub-title">
|
||||||
|
正在为:<span class="sub-roles">[{{ personAuth }}] </span>分配系统访问权限
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 内容 -->
|
||||||
|
|
||||||
|
<div class="permission-drawer-content">
|
||||||
|
<div class="permission-container">
|
||||||
|
<el-tabs v-model="activeTab" class="custom-permission-tabs">
|
||||||
|
<el-tab-pane label="菜单权限" name="menu">
|
||||||
|
<base-segment-menu :menuList="menuList"></base-segment-menu>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="字段权限" name="field">
|
||||||
|
<fieldPermissionManager
|
||||||
|
:dicts="dicts"
|
||||||
|
v-model:field-groups-list="fieldsList"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 底部footer -->
|
||||||
|
<template #footer>
|
||||||
|
<div class="custom-flat-drawer-footer">
|
||||||
|
<div class="stats-info"></div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button link @click="drawerVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" class="btn-confirm" @click="handleSavePermission" :loading="loading">确认应用</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Close } from "@element-plus/icons-vue";
|
||||||
|
import baseSegmentMenu from "./baseSegmentMenu.vue";
|
||||||
|
import fieldPermissionManager from "./fieldPermissionManager.vue";
|
||||||
|
import { saveRolePermission,getRolePermission } from '@/api/stage/permission';
|
||||||
|
import { useDict } from '@/hooks/useDictData';
|
||||||
|
const { dicts,refresh } = useDict('permission_setting_status');
|
||||||
|
|
||||||
|
interface permissionProps {
|
||||||
|
permissions:Record<string,any>[];
|
||||||
|
}
|
||||||
|
defineOptions({ name: "PermissionDrawer" });
|
||||||
|
const props = defineProps({
|
||||||
|
checkAuth:{
|
||||||
|
type: Object,
|
||||||
|
default: ()=>({}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const drawerVisible = defineModel("visible", { type: Boolean, default: false });
|
||||||
|
const activeTab = ref<string>("menu");
|
||||||
|
const currentModule = ref<number>(4); // 当前高亮的模块
|
||||||
|
const menuList = ref([]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
// 动态展示当前人员名称
|
||||||
|
const personAuth = computed(() => {
|
||||||
|
return props.checkAuth.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动态的数据
|
||||||
|
const fieldsList = computed({
|
||||||
|
get:()=>{
|
||||||
|
const activeFields = [];
|
||||||
|
menuList.value.forEach(group => {
|
||||||
|
(group.children || []).forEach(row => {
|
||||||
|
if (row.selected) {
|
||||||
|
activeFields.push(row); // 将包含 fields 的整个对象传过去
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return activeFields;
|
||||||
|
},
|
||||||
|
set:()=>{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取角色详情数据
|
||||||
|
const roleDetail = async (roleId) =>{
|
||||||
|
try {
|
||||||
|
const response = await getRolePermission(roleId);
|
||||||
|
menuList.value = response.map(group => {
|
||||||
|
const checkedGroup = (group.children || []).filter(c => c.selected);
|
||||||
|
const allCheckedGroup = (group.children || []).every(c => c.selected); //兼容后端全选后返回为false的问题
|
||||||
|
const checkedCount = checkedGroup.length;
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
selected:allCheckedGroup,
|
||||||
|
isIndeterminate: checkedCount > 0 && checkedCount < group.children.length,
|
||||||
|
children:(group.children||[]).map(child=>{
|
||||||
|
const actions = child.selected ? child.operations.filter(action => action.selected).map(action => action.id) : [];
|
||||||
|
return {
|
||||||
|
...child,
|
||||||
|
isIndeterminate: actions.length > 0 && actions.length < child.operations.length,
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error',error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 非阻塞提取权限载荷
|
||||||
|
* @param {Array} menuList 原始响应式数据
|
||||||
|
*/
|
||||||
|
const extractPermissionPayload = async (menuList) => {
|
||||||
|
// 使用深拷贝,避免清洗过程中由于响应式追踪导致的 UI 卡顿
|
||||||
|
const sourceData = JSON.parse(JSON.stringify(menuList));
|
||||||
|
const result = [];
|
||||||
|
// 辅助函数:处理单个功能节点(Leaf Node)
|
||||||
|
const formatLeafNode = (node) => {
|
||||||
|
const operationIds = (node.actions || []);
|
||||||
|
const fieldPermissions = (node.fieldGroups || []).map(group => ({
|
||||||
|
groupId: group.id,
|
||||||
|
permission: group.permission,
|
||||||
|
fields: (group.fields || []).map(field => ({
|
||||||
|
fieldId: field.id,
|
||||||
|
permission: field.permission
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
leafNodeId: node.id,
|
||||||
|
operationIds: operationIds,
|
||||||
|
fieldPermissions: fieldPermissions
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let i = 0;
|
||||||
|
const chunk = () => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
// 保持每一片执行时间在 16ms 以内(一帧的时间),确保不卡顿
|
||||||
|
while (i < sourceData.length && (performance.now() - startTime) < 16) {
|
||||||
|
const group = sourceData[i];
|
||||||
|
|
||||||
|
// 情况 A: 有二级菜单 (如商机管理)
|
||||||
|
if (group.children && group.children.length > 0) {
|
||||||
|
group.children.forEach(row => {
|
||||||
|
// 只要有选中的操作或二级被选中,就视为有效节点
|
||||||
|
if (row.selected || (row.actions && row.actions.length > 0)) {
|
||||||
|
result.push(formatLeafNode(row));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 情况 B: 一级就是功能节点 (如团队管理,无children有operations)
|
||||||
|
else {
|
||||||
|
if (group.selected || group.actions && group.actions.length > 0) {
|
||||||
|
result.push(formatLeafNode(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < sourceData.length) {
|
||||||
|
requestAnimationFrame(chunk);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chunk();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
watch(()=>menuList.value,(val)=>{
|
||||||
|
// console.log('获取数据动态变化',val);
|
||||||
|
},{deep:true})
|
||||||
|
// 保存权限
|
||||||
|
const handleSavePermission = async () =>{
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const finalPayload = await extractPermissionPayload(menuList.value);
|
||||||
|
const res = await saveRolePermission({roleId:props.checkAuth.id,permissions:finalPayload});
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
drawerVisible.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('save error',error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听传入的id值的变化
|
||||||
|
watch(drawerVisible,(newVisible)=>{
|
||||||
|
if(newVisible){
|
||||||
|
const newRoleId = props.checkAuth.id;
|
||||||
|
roleDetail(newRoleId);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "./baseSegmentedPermission.scss" as *;
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="personnel">
|
||||||
<Comment />
|
人员管理
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Comment from "@/modules/Comment/index.vue";
|
|
||||||
import { reactive, ref, onMounted } from "vue";
|
|
||||||
|
|
||||||
defineOptions({ name: "Personnel" });
|
defineOptions({ name: "Personnel" });
|
||||||
|
const drawer = ref(true);
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
|||||||
import { ElNotification } from "element-plus";
|
import { ElNotification } from "element-plus";
|
||||||
import { VITE_APP_BASE_API } from "../../config.js";
|
import { VITE_APP_BASE_API } from "../../config.js";
|
||||||
import TokenManager from "@/utils/storage";
|
import TokenManager from "@/utils/storage";
|
||||||
import { getMockData, shouldUseMock } from "@/mock"; //mock数据信息
|
|
||||||
const tokenManager = TokenManager.getInstance();
|
const tokenManager = TokenManager.getInstance();
|
||||||
const baseUrl = import.meta.env.MODE === "development" ? "/api" : VITE_APP_BASE_API;
|
const baseUrl =
|
||||||
|
import.meta.env.MODE === "development" ? "/api" : VITE_APP_BASE_API;
|
||||||
// 1. 锁和队列定义在类外部,确保全局唯一
|
// 1. 锁和队列定义在类外部,确保全局唯一
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
let requestsQueue: Array<(token: string) => void> = [];
|
let requestsQueue: Array<(token: string) => void> = [];
|
||||||
|
|
||||||
// 登录接口 传递参数不一样
|
// 登录接口 传递参数不一样
|
||||||
const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token";
|
const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token";
|
||||||
const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:secret");
|
const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:H7mHCOfjhV2VqlR31OHi6ruVMeOQvluz");
|
||||||
|
|
||||||
class HttpRequest {
|
class HttpRequest {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
@@ -127,13 +127,23 @@ class HttpRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 其它业务错误
|
// 其它业务错误
|
||||||
ElNotification.error({ title: "提示", message: res.msg || "服务异常" });
|
ElNotification.error({
|
||||||
|
title: "提示",
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
message: `${res.msg}<br/>错误码:${res.subCode}` || "服务异常",
|
||||||
|
});
|
||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 网络层错误处理
|
// 网络层错误处理
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
this.clearTokens();
|
this.clearTokens();
|
||||||
|
} else {
|
||||||
|
if(this.isAuthEndpoint(error.config.url)){
|
||||||
|
ElNotification.error({ title: "提示", message: error.response.data.error || '未知错误' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElNotification.error({ title: "提示", message: "服务异常" });
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/router/generateFinalMenu.ts
Normal file
33
src/router/generateFinalMenu.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import { componentMap } from "./routeMap";
|
||||||
|
/**
|
||||||
|
* 组装函数:将后端原始嵌套树重组为前端要求的骨架结构
|
||||||
|
* @param backendTree 后端返回的原始数据 (包含 backend, business 等顶层节点的树)
|
||||||
|
*/
|
||||||
|
export const transformBackendTree = (backendTree: any[]): any[] => {
|
||||||
|
if (!backendTree || !Array.isArray(backendTree)) return [];
|
||||||
|
|
||||||
|
// 排序操作
|
||||||
|
const sortedTree = [...backendTree].sort((a, b) => {
|
||||||
|
const sortA = a.sort ?? 999; // 兜底值,防止没有 sort 字段
|
||||||
|
const sortB = b.sort ?? 999;
|
||||||
|
return sortA - sortB;
|
||||||
|
});
|
||||||
|
return sortedTree.map(item => {
|
||||||
|
// 2. 构造当前节点
|
||||||
|
const newNode: any = {
|
||||||
|
...item,
|
||||||
|
code: componentMap[item.code],
|
||||||
|
originalCode: item.code, // 保留一份原始 code 备用
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 递归处理 children
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
newNode.children = transformBackendTree(item.children);
|
||||||
|
} else {
|
||||||
|
newNode.children = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -4,12 +4,13 @@ import {
|
|||||||
type RouteRecordRaw,
|
type RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import { getRouteMenus } from "@/api";
|
import { getUserInfo } from "@/api";
|
||||||
import TokenManager from "@/utils/storage";
|
import TokenManager from "@/utils/storage";
|
||||||
|
|
||||||
import Login from "@/pages/Login/index.vue";
|
import Login from "@/pages/Login/index.vue";
|
||||||
import HomeView from "@/pages/Layout/index.vue";
|
import HomeView from "@/pages/Layout/index.vue";
|
||||||
|
import { transformBackendTree } from './generateFinalMenu';
|
||||||
|
import { mockBackendMenuData } from "@/mock/menu";
|
||||||
const tokenManager = TokenManager.getInstance();
|
const tokenManager = TokenManager.getInstance();
|
||||||
// 基础路由(不需要权限验证)
|
// 基础路由(不需要权限验证)
|
||||||
const constantRoutes: RouteRecordRaw[] = [
|
const constantRoutes: RouteRecordRaw[] = [
|
||||||
@@ -30,7 +31,6 @@ const asyncRoutes: RouteRecordRaw[] = [
|
|||||||
path: "/",
|
path: "/",
|
||||||
name: "Layout",
|
name: "Layout",
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
// redirect: '/home',
|
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
@@ -85,7 +85,6 @@ const transformRoutes = (
|
|||||||
routes: any[],
|
routes: any[],
|
||||||
parentCode: string = ""
|
parentCode: string = ""
|
||||||
): RouteRecordRaw[] => {
|
): RouteRecordRaw[] => {
|
||||||
console.log("transformRoutes", routes);
|
|
||||||
return routes.flatMap((route) => {
|
return routes.flatMap((route) => {
|
||||||
const fullCode = parentCode ? `${parentCode}/${route.code}` : route.code;
|
const fullCode = parentCode ? `${parentCode}/${route.code}` : route.code;
|
||||||
|
|
||||||
@@ -129,6 +128,29 @@ const transformRoutes = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 递归查找路由配置中的第一个有效路径(叶子节点)
|
||||||
|
*/
|
||||||
|
const getFirstValidPath = (routes: RouteRecordRaw[], parentPath: string = "") => {
|
||||||
|
for (const route of routes) {
|
||||||
|
// 拼接当前层级的完整路径
|
||||||
|
let currentPath = route.path.startsWith("/")
|
||||||
|
? route.path
|
||||||
|
: `${parentPath}/${route.path}`.replace(/\/+/g, "/");
|
||||||
|
|
||||||
|
// 如果有子路由,继续递归,并将当前路径传下去
|
||||||
|
if (route.children && route.children.length > 0) {
|
||||||
|
const childPath = getFirstValidPath(route.children, currentPath);
|
||||||
|
if (childPath) return childPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是叶子节点(有组件),返回拼接好的完整路径
|
||||||
|
if (route.component) {
|
||||||
|
return currentPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/";
|
||||||
|
};
|
||||||
|
|
||||||
// 添加动态路由
|
// 添加动态路由
|
||||||
const addDynamicRoutes = async () => {
|
const addDynamicRoutes = async () => {
|
||||||
@@ -140,31 +162,16 @@ const addDynamicRoutes = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
const { modules:backendRawData } = await getUserInfo();
|
||||||
let allRoutes:any[] = [];
|
const allRoutes = transformBackendTree(backendRawData);
|
||||||
if (userStore.isBackendUser) {
|
console.log('获取最终渲染的菜单数据格式:',allRoutes);
|
||||||
const backendResponse = await getRouteMenus();
|
|
||||||
allRoutes = [
|
|
||||||
{
|
|
||||||
code: "stage",
|
|
||||||
name: "管理中心",
|
|
||||||
icon: "",
|
|
||||||
meta:{
|
|
||||||
title:'管理中心'
|
|
||||||
},
|
|
||||||
children: backendResponse,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// TODO:获取用户端的数据信息
|
|
||||||
// response = await getUserMenus();
|
|
||||||
allRoutes = [];
|
|
||||||
}
|
|
||||||
if (allRoutes) {
|
if (allRoutes) {
|
||||||
// 转换路由数据
|
// 转换路由数据
|
||||||
const dynamicRoutes = transformRoutes(
|
const dynamicRoutes = transformRoutes(
|
||||||
Array.isArray(allRoutes) ? allRoutes : [allRoutes]
|
Array.isArray(allRoutes) ? allRoutes : [allRoutes]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('最终渲染的菜单数据:',dynamicRoutes)
|
||||||
// 将动态路由添加到 Layout 的 children 中
|
// 将动态路由添加到 Layout 的 children 中
|
||||||
const layoutRoute = router
|
const layoutRoute = router
|
||||||
.getRoutes()
|
.getRoutes()
|
||||||
@@ -222,7 +229,7 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
// 未登录,重定向到登录页
|
// 未登录,重定向到登录页
|
||||||
next({
|
next({
|
||||||
path: "/login",
|
path: "/login",
|
||||||
query: { redirect: to.fullPath }, // 保存当前路径,登录后可以跳转回来
|
query: { redirect: to.fullPath },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -232,16 +239,29 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
try {
|
try {
|
||||||
// 加载动态路由
|
// 加载动态路由
|
||||||
await addDynamicRoutes();
|
await addDynamicRoutes();
|
||||||
|
const redirect = to.query.redirect as string;
|
||||||
|
|
||||||
|
if (to.path === "/" || to.path === "/login") {
|
||||||
|
const redirect = to.query.redirect as string;
|
||||||
|
const firstPath = getFirstValidPath(userStore.routes);
|
||||||
|
const targetPath = (redirect && redirect !== '/') ? redirect : firstPath;
|
||||||
|
next({ path: targetPath, replace: true });
|
||||||
|
} else {
|
||||||
// 路由加载完成后,重新导航到目标路由
|
// 路由加载完成后,重新导航到目标路由
|
||||||
next({ ...to, replace: true });
|
next({ ...to, replace: true });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Route loading error:", error);
|
console.error("Route loading error:", error);
|
||||||
next("/login");
|
next("/login");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 路由已加载,直接放行
|
if (to.path === "/") {
|
||||||
|
const firstPath = getFirstValidPath(userStore.routes);
|
||||||
|
next({ path: firstPath, replace: true });
|
||||||
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
41
src/router/routeMap.ts
Normal file
41
src/router/routeMap.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
const stageRoute = {
|
||||||
|
backend: "stage",
|
||||||
|
flow: "flow",
|
||||||
|
origanization: "origanization",
|
||||||
|
personnel: "personnel",
|
||||||
|
permission: "permission",
|
||||||
|
dict: "dict",
|
||||||
|
}
|
||||||
|
|
||||||
|
const businessRoute = {
|
||||||
|
business: "businessManage",
|
||||||
|
"business.customer": "customer",
|
||||||
|
"business.game_studio":"gameStudios",
|
||||||
|
"business.opportunity":"businessOpport",
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractRoute = {
|
||||||
|
contract: "contractManage",
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamRoute = {
|
||||||
|
team: "teamManage",
|
||||||
|
}
|
||||||
|
|
||||||
|
const financeRoute = {
|
||||||
|
finance: "financeManage",
|
||||||
|
}
|
||||||
|
|
||||||
|
const recruitmentRoute = {
|
||||||
|
recruitment: "recruitmentManage",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const componentMap: Record<string, string> = {
|
||||||
|
...stageRoute,
|
||||||
|
...businessRoute,
|
||||||
|
...contractRoute,
|
||||||
|
...teamRoute,
|
||||||
|
...financeRoute,
|
||||||
|
...recruitmentRoute
|
||||||
|
};
|
||||||
@@ -18,17 +18,18 @@ body {
|
|||||||
--mj-border-color:#{$mj-border-color};
|
--mj-border-color:#{$mj-border-color};
|
||||||
--mj-padding-standard:#{$mj-padding-standard};
|
--mj-padding-standard:#{$mj-padding-standard};
|
||||||
--mj-popper-radius: 8px;
|
--mj-popper-radius: 8px;
|
||||||
--el-color-primary:#2b65f6;
|
--el-color-primary: #2b65f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// popover 筛选框的全局样式
|
||||||
.filter-popper.el-popover.el-popper {
|
.filter-popper.el-popover.el-popper {
|
||||||
--el-popover-padding: 0;
|
--el-popover-padding: 0;
|
||||||
border-radius: var(--mj-popper-radius);
|
border-radius: var(--mj-popper-radius);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局重新element相关样式
|
// 全局重写element相关样式
|
||||||
.mj-input-form {
|
.mj-input-form {
|
||||||
.el-input {
|
.el-input {
|
||||||
--el-border-radius-base: 10px;
|
--el-border-radius-base: 10px;
|
||||||
@@ -38,7 +39,7 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
// 搜索框动画
|
// 搜索框动画
|
||||||
.search-dict-input {
|
.search-auto-expand-input {
|
||||||
--default-width: 160px;
|
--default-width: 160px;
|
||||||
--max-width: 224px;
|
--max-width: 224px;
|
||||||
width: var(--default-width);
|
width: var(--default-width);
|
||||||
@@ -58,6 +59,7 @@ body {
|
|||||||
// 字典状态全局样式
|
// 字典状态全局样式
|
||||||
.mj-status-dot {
|
.mj-status-dot {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -71,7 +73,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 筛选框全局样式内容
|
// 筛选框内容的全局样式内容
|
||||||
.mj-filter-content {
|
.mj-filter-content {
|
||||||
min-width: 380px;
|
min-width: 380px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -101,7 +103,8 @@ body {
|
|||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
label{
|
|
||||||
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
@@ -111,7 +114,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-select {
|
.custom-select {
|
||||||
--el-border-color:transparent;
|
--el-border-color: transparent;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -127,9 +130,79 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #407eff;
|
background-color: #407eff;
|
||||||
border-color: #407eff;
|
border-color: #407eff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 全局单行省略样式
|
||||||
|
.mj-ellipsis-one-line {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 卡片的全局样式
|
||||||
|
|
||||||
|
.mj-card-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
// 默认:一行 5 列
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
|
||||||
|
// 1400px 以下:觉得 5 个太挤,切到一行 4 列
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1100px 以下 (iPad横屏区间):一行 3 列
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 768px 以下 (iPad竖屏):一行 2 列
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 480px 以下 (手机端):一行 1 列
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// select下拉搜索loading全局公用样式
|
||||||
|
.mj-select-dropdown-loading {
|
||||||
|
cursor: default;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.status-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-ellipsis-cell {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: break-all;
|
||||||
|
display: block;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
|
|
||||||
|
|
||||||
// 当前样式表修改element 全局的样式
|
// 当前样式表修改element 全局的样式
|
||||||
|
|
||||||
// 标砖抽屉样式
|
// 标砖抽屉样式
|
||||||
.standard-ui-drawer{
|
.standard-ui-drawer {
|
||||||
.el-drawer__header{
|
.el-drawer__header {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-bottom: var(--el-drawer-padding-primary);
|
padding-bottom: var(--el-drawer-padding-primary);
|
||||||
&::after{
|
|
||||||
content:'';
|
&::after {
|
||||||
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--el-drawer-padding-primary);
|
left: var(--el-drawer-padding-primary);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -20,51 +19,185 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.standard-ui-back-drawer{
|
.standard-ui-back-drawer {
|
||||||
@extend .standard-ui-drawer;
|
@extend .standard-ui-drawer;
|
||||||
.el-drawer__header{
|
|
||||||
|
.el-drawer__header {
|
||||||
background-color: #FBFCFD;
|
background-color: #FBFCFD;
|
||||||
&::after{
|
|
||||||
|
&::after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-drawer__body{
|
|
||||||
|
.customer-drawer-header {
|
||||||
|
padding: 20px 24px 15px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #409eff;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
margin: 2px 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
.sub-roles{
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.el-drawer__footer{
|
|
||||||
|
.el-drawer__footer {
|
||||||
background-color: #FBFCFD;
|
background-color: #FBFCFD;
|
||||||
border-top: 1px solid #E5E7EB;
|
border-top: 1px solid #E5E7EB;
|
||||||
|
|
||||||
|
.custom-flat-drawer-footer {
|
||||||
|
--black-bg-hover-color:#2b364a;
|
||||||
|
--black-bg-color:#1d2635;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.btn-confirm {
|
||||||
|
background-color: var(--black-bg-color);
|
||||||
|
border-color: var(--black-bg-color);
|
||||||
|
padding: 10px 24px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--black-bg-hover-color);
|
||||||
|
border-color: var(--black-bg-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 标注弹窗样式
|
// 标注弹窗样式
|
||||||
.standard-ui-dialog{
|
.standard-ui-dialog {
|
||||||
&.el-dialog{
|
&.el-dialog {
|
||||||
--el-dialog-inset-padding-primary:16px;
|
--el-dialog-inset-padding-primary: 16px;
|
||||||
--el-dialog-padding-primary:0;
|
--el-dialog-padding-primary: 0;
|
||||||
--el-dialog-border-radius:16px;
|
--el-dialog-border-radius: 16px;
|
||||||
--el-dialog-bg-header-footer:#FBFCFD;
|
--el-dialog-bg-header-footer: #FBFCFD;
|
||||||
--el-dialog-border-header-footer-color:#E5E7EB;
|
--el-dialog-border-header-footer-color: #E5E7EB;
|
||||||
padding: var(--el-dialog-padding-primary);
|
padding: var(--el-dialog-padding-primary);
|
||||||
}
|
}
|
||||||
.el-dialog__header{
|
|
||||||
|
.el-dialog__header {
|
||||||
border-bottom: 1px solid var(--el-dialog-border-header-footer-color);
|
border-bottom: 1px solid var(--el-dialog-border-header-footer-color);
|
||||||
background-color:var(--el-dialog-bg-header-footer);
|
background-color: var(--el-dialog-bg-header-footer);
|
||||||
padding: var(--el-dialog-inset-padding-primary);
|
padding: var(--el-dialog-inset-padding-primary);
|
||||||
border-radius: var(--el-dialog-border-radius) var(--el-dialog-border-radius) 0 0;
|
border-radius: var(--el-dialog-border-radius) var(--el-dialog-border-radius) 0 0;
|
||||||
}
|
}
|
||||||
.el-dialog__headerbtn{
|
|
||||||
|
.el-dialog__headerbtn {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.el-dialog__body{
|
|
||||||
|
.el-dialog__body {
|
||||||
padding: var(--el-dialog-inset-padding-primary);
|
padding: var(--el-dialog-inset-padding-primary);
|
||||||
}
|
}
|
||||||
.el-dialog__footer{
|
|
||||||
|
.el-dialog__footer {
|
||||||
padding: var(--el-dialog-inset-padding-primary);
|
padding: var(--el-dialog-inset-padding-primary);
|
||||||
background-color: var(--el-dialog-bg-header-footer);
|
background-color: var(--el-dialog-bg-header-footer);
|
||||||
border-top: 1px solid var(--el-dialog-border-header-footer-color);
|
border-top: 1px solid var(--el-dialog-border-header-footer-color);
|
||||||
border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius);
|
border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 扁平化dialog样式
|
||||||
|
.standard-ui-flat-dialog {
|
||||||
|
&.el-dialog {
|
||||||
|
--el-dialog-inset-padding-primary: 16px;
|
||||||
|
--el-dialog-padding-primary: 0;
|
||||||
|
--el-dialog-border-radius: 4px;
|
||||||
|
--el-dialog-bg-header-footer: #fcfdfe;
|
||||||
|
--el-dialog-border-header-footer-color: #f0f2f5;
|
||||||
|
--blue-block-color: #1661ff;
|
||||||
|
padding: var(--el-dialog-padding-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: var(--el-dialog-inset-padding-primary);
|
||||||
|
border-bottom: 1px solid var(--el-dialog-border-header-footer-color);
|
||||||
|
|
||||||
|
.el-dialog__title{
|
||||||
|
position: relative;
|
||||||
|
padding-left: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
&::before{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: var(--blue-block-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
text-align: right;
|
||||||
|
background-color: var(--el-dialog-bg-header-footer);
|
||||||
|
border-top: 1px solid var(--el-dialog-border-header-footer-color);
|
||||||
|
border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius);
|
||||||
|
padding: var(--el-dialog-inset-padding-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 扁平化遮罩层样式 (drawer-dialog 的遮罩层样式)
|
||||||
|
.standard-overlay-dialog-flat {
|
||||||
|
--el-overlay-color-lighter: rgba(0, 0, 0, .12);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-select-dropdown-height{
|
||||||
|
.el-select-dropdown__wrap{
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
|
import { throttle } from "lodash-es";
|
||||||
function permissionDirective(el: HTMLElement,binding:any) {
|
function permissionDirective(el: HTMLElement, binding: any) {
|
||||||
const appStore = useUserStore();
|
const appStore = useUserStore();
|
||||||
const userPermissions = appStore.role;
|
const userPermissions = appStore.role;
|
||||||
let requiredPermissions = binding.value;
|
let requiredPermissions = binding.value;
|
||||||
|
// 使用特殊值 '*' 表示跳过权限检查
|
||||||
|
const isSkipCheck =
|
||||||
|
requiredPermissions === "*" ||
|
||||||
|
(Array.isArray(requiredPermissions) && requiredPermissions.includes("*"));
|
||||||
|
if (isSkipCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof requiredPermissions === "string") {
|
if (typeof requiredPermissions === "string") {
|
||||||
requiredPermissions = [requiredPermissions];
|
requiredPermissions = [requiredPermissions];
|
||||||
}
|
}
|
||||||
@@ -25,8 +32,48 @@ const permission = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectMore = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
const callback = binding.value;
|
||||||
|
const onScroll = throttle(
|
||||||
|
function () {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = this;
|
||||||
|
const isBottom = scrollHeight - scrollTop <= clientHeight + 10;
|
||||||
|
|
||||||
|
if (isBottom) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
{ trailing: true }
|
||||||
|
);
|
||||||
|
// 关键:轮询寻找滚动容器,因为 mounted 时下拉框可能还没渲染
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
// 兼容 teleport 情况,如果 el 找不到,尝试寻找 popper
|
||||||
|
const wrapper = el.querySelector(".el-scrollbar__wrap");
|
||||||
|
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.addEventListener("scroll", onScroll);
|
||||||
|
el._scrollWrapper = wrapper;
|
||||||
|
el._onScroll = onScroll;
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
setTimeout(() => clearInterval(interval), 10000);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount(el) {
|
||||||
|
if (el._scrollWrapper && el._onScroll) {
|
||||||
|
el._scrollWrapper.removeEventListener("scroll", el._onScroll);
|
||||||
|
el._onScroll.cancel?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const directives = {
|
const directives = {
|
||||||
permission,
|
permission,
|
||||||
|
selectMore,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (app: any) => {
|
export default (app: any) => {
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ export const usePermission = () => {
|
|||||||
const userPermissions = appStore.role;
|
const userPermissions = appStore.role;
|
||||||
const permissionArray = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions];
|
const permissionArray = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions];
|
||||||
|
|
||||||
|
// 为*跳过权限校验
|
||||||
|
const isSkipCheck = requiredPermissions === "*" || (permissionArray && permissionArray.includes("*"));
|
||||||
|
if (isSkipCheck) {return true;}
|
||||||
|
|
||||||
const hasPermission = permissionArray.some((permission) =>
|
const hasPermission = permissionArray.some((permission) =>
|
||||||
userPermissions.includes(permission)
|
userPermissions.includes(permission)
|
||||||
);
|
);
|
||||||
|
|
||||||
return hasPermission;
|
return hasPermission;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { checkPermission };
|
return { checkPermission };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ class TokenManager {
|
|||||||
private static instance: TokenManager | null = null;
|
private static instance: TokenManager | null = null;
|
||||||
private storage: Storage;
|
private storage: Storage;
|
||||||
|
|
||||||
private constructor(storageType: 'localStorage' | 'sessionStorage' = 'localStorage') {
|
private constructor(
|
||||||
this.storage = storageType === 'localStorage' ? localStorage : sessionStorage;
|
storageType: "localStorage" | "sessionStorage" = "localStorage"
|
||||||
|
) {
|
||||||
|
this.storage =
|
||||||
|
storageType === "localStorage" ? localStorage : sessionStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance(storageType: 'localStorage' | 'sessionStorage' = 'localStorage'): TokenManager {
|
public static getInstance(
|
||||||
|
storageType: "localStorage" | "sessionStorage" = "localStorage"
|
||||||
|
): TokenManager {
|
||||||
if (!TokenManager.instance) {
|
if (!TokenManager.instance) {
|
||||||
TokenManager.instance = new TokenManager(storageType);
|
TokenManager.instance = new TokenManager(storageType);
|
||||||
}
|
}
|
||||||
@@ -25,9 +30,13 @@ class TokenManager {
|
|||||||
this.storage.removeItem(key);
|
this.storage.removeItem(key);
|
||||||
}
|
}
|
||||||
clearStorage(): void {
|
clearStorage(): void {
|
||||||
this.storage.clear();
|
const PROTECT_PREFIX = "keep_";
|
||||||
|
Object.keys(this.storage).forEach((key) => {
|
||||||
|
if (!key.startsWith(PROTECT_PREFIX)) {
|
||||||
|
this.storage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default TokenManager;
|
export default TokenManager;
|
||||||
6
src/utils/svgIcon.ts
Normal file
6
src/utils/svgIcon.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const svgIcons: Record<string, string> = {
|
||||||
|
'custom-building': `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M853.333333 426.666667H725.333333v298.666666H298.666667V426.666667H170.666667v384c0 23.466667 19.2 42.666667 42.666666 42.666667h597.333334c23.466667 0 42.666667-19.2 42.666666-42.666667v-384z m-512-128c0-117.333333 96-213.333333 213.333333-213.333334s213.333333 96 213.333334 213.333334H341.333333z"/></svg>`,
|
||||||
|
'custom-company': `<svg class="icon-svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M288 128a32 32 0 0 0-32 32v704a32 32 0 0 0 64 0V544h448l-64-160 64-160H320V160a32 32 0 0 0-32-32zM320 288h352l-32 80 32 80H320V288z"/></svg>`,
|
||||||
|
'custom-department': `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M853.333333 426.666667H725.333333v298.666666H298.666667V426.666667H170.666667v384c0 23.466667 19.2 42.666667 42.666666 42.666667h597.333334c23.466667 0 42.666667-19.2 42.666666-42.666667v-384z"/></svg>`,
|
||||||
|
// 你可以添加更多 SVG 图标
|
||||||
|
}
|
||||||
@@ -8,6 +8,6 @@ export function getImageUrl(url: string) {
|
|||||||
|
|
||||||
|
|
||||||
// 设置index前缀 如:001 010 100种 默认兼容3位数
|
// 设置index前缀 如:001 010 100种 默认兼容3位数
|
||||||
export const formatIndex = (index,padNum=3) => {
|
export const formatIndex = (index,padNum?:number=3) => {
|
||||||
return String(index + 1).padStart(padNum, '0');
|
return String(index + 1).padStart(padNum, '0');
|
||||||
};
|
};
|
||||||
@@ -4,6 +4,7 @@ import AutoImport from "unplugin-auto-import/vite";
|
|||||||
import Components from "unplugin-vue-components/vite";
|
import Components from "unplugin-vue-components/vite";
|
||||||
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import { VITE_APP_BASE_API, VITE_PUBLIC_PATH } from './config.js';
|
import { VITE_APP_BASE_API, VITE_PUBLIC_PATH } from './config.js';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
@@ -23,6 +24,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
resolvers: [ElementPlusResolver()],
|
resolvers: [ElementPlusResolver()],
|
||||||
}),
|
}),
|
||||||
vue(),
|
vue(),
|
||||||
|
vueJsx()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
17
vitest.config.ts
Normal file
17
vitest.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
test: {
|
||||||
|
// 关键:模拟浏览器环境,否则无法测试 DOM 和滚动
|
||||||
|
environment: 'happy-dom',
|
||||||
|
// 支持全局 API 如 describe, it, expect
|
||||||
|
globals: true,
|
||||||
|
// 路径别名(确保能找到你的 @/components)
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user