add:新增商机管理-客户管理页面
This commit is contained in:
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
components.d.ts
vendored
18
components.d.ts
vendored
@@ -13,7 +13,9 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
||||||
CardItem: typeof import('./src/components/cardItem/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']
|
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']
|
||||||
@@ -24,6 +26,7 @@ declare module 'vue' {
|
|||||||
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']
|
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']
|
||||||
@@ -80,6 +83,7 @@ declare module 'vue' {
|
|||||||
NodeFlow: typeof import('./src/components/nodeFlow/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']
|
ProTablev2: typeof import('./src/components/proTable/proTablev2.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
@@ -89,6 +93,9 @@ declare module 'vue' {
|
|||||||
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']
|
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']
|
Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
||||||
}
|
}
|
||||||
@@ -101,7 +108,9 @@ declare module 'vue' {
|
|||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
const AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
const AutoTooltip: typeof import('./src/components/autoTooltip/index.vue')['default']
|
||||||
const CardItem: typeof import('./src/components/cardItem/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 CollapseHeader: typeof import('./src/components/collapseHeader/index.vue')['default']
|
||||||
const Comment: typeof import('./src/components/comment/index.vue')['default']
|
const Comment: typeof import('./src/components/comment/index.vue')['default']
|
||||||
const CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
const CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||||
@@ -112,6 +121,7 @@ declare global {
|
|||||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
const ElCard: typeof import('element-plus/es')['ElCard']
|
const ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
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 ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
const ElCol: typeof import('element-plus/es')['ElCol']
|
const ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
const ElContainer: typeof import('element-plus/es')['ElContainer']
|
const ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
@@ -168,6 +178,7 @@ declare global {
|
|||||||
const NodeFlow: typeof import('./src/components/nodeFlow/index.vue')['default']
|
const NodeFlow: typeof import('./src/components/nodeFlow/index.vue')['default']
|
||||||
const OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
|
const OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
|
||||||
const PageForm: typeof import('./src/components/pageForm/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 ProTable: typeof import('./src/components/proTable/index.vue')['default']
|
||||||
const ProTablev2: typeof import('./src/components/proTable/proTablev2.vue')['default']
|
const ProTablev2: typeof import('./src/components/proTable/proTablev2.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
@@ -177,6 +188,9 @@ declare global {
|
|||||||
const StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
const StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
||||||
const StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
const StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
||||||
const UserSelector: typeof import('./src/components/userSelector/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 Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
||||||
const Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
const Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"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": {
|
||||||
@@ -13,8 +14,11 @@
|
|||||||
"@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",
|
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
||||||
"@vue-flow/core": "^1.48.1",
|
"@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"
|
||||||
|
|||||||
661
pnpm-lock.yaml
generated
661
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mj-card-container mj-grid-container">
|
<div class="mj-grid-container">
|
||||||
<div
|
<div class="mj-card-item" @click="$emit('card-click', item)">
|
||||||
class="mj-card-item"
|
<slot name="card-prefix" :item="item">
|
||||||
v-for="(card, index) in list"
|
|
||||||
:key="index"
|
|
||||||
@click="cardItemClick(card, index)"
|
|
||||||
>
|
|
||||||
<slot name="cardCover" :item="card">
|
|
||||||
<div class="mj-card-standard-tip" :style="standardTopStyle"></div>
|
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<div class="mj-card-standard-content">
|
<div class="mj-card-standard-content">
|
||||||
<slot name="content" :item="card" :index="index"></slot>
|
<slot name="content" :item="item"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="$slots.actions" class="mj-card-actions">
|
<div v-if="$slots.actions" class="mj-card-actions">
|
||||||
<slot name="actions" :item="card"></slot>
|
<slot name="actions" :item="item"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,28 +17,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "CardItem" });
|
defineOptions({ name: "CardItem" });
|
||||||
|
|
||||||
|
// 接收单条数据 item 而不是 list
|
||||||
interface Props {
|
interface Props {
|
||||||
list: any[];
|
item: any;
|
||||||
standardTopStyle?: string | Record<string, any>;
|
standardTopStyle?: string | Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = defineProps<Props>();
|
||||||
list: () => [],
|
|
||||||
standardTopStyle: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: "card-click", payload: { item: any; index: number }): void;
|
(e: "card-click", item: any): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const cardItemClick = (item: any, index: number) => {
|
|
||||||
emits("card-click", { item, index });
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mj-grid-container {
|
.mj-grid-container {
|
||||||
// 卡片主体样式
|
|
||||||
.mj-card-item {
|
.mj-card-item {
|
||||||
--radius: 12px;
|
--radius: 12px;
|
||||||
--primary-color: #409eff;
|
--primary-color: #409eff;
|
||||||
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>
|
||||||
@@ -1,82 +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="onDownload" 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">
|
||||||
const filterPopover = ref(null);
|
import { ref, watch } from 'vue';
|
||||||
|
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 = () => {
|
const onDownload = () => {
|
||||||
|
// @ts-ignore (确保你项目中已按需引入 ElMessage)
|
||||||
ElMessage.warning("功能开发中...");
|
ElMessage.warning("功能开发中...");
|
||||||
emit("download");
|
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;
|
||||||
@@ -89,21 +119,22 @@ const onDownload = () => {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,9 @@
|
|||||||
:bgColor="'var(--mj-avatar-bg)'"
|
:bgColor="'var(--mj-avatar-bg)'"
|
||||||
:avatarTextColor="'var(--mj-text-color)'"
|
:avatarTextColor="'var(--mj-text-color)'"
|
||||||
/>
|
/>
|
||||||
<el-tooltip :content="item.name">
|
<div class="select-item-name">
|
||||||
<div class="select-item-name mj-ellipsis-one-line">
|
<AutoTooltip :content="item.name"/>
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -129,7 +127,7 @@ import NameAvatar from "@/components/NameAvatar/index.vue";
|
|||||||
import { getEmployeeList } from "@/api";
|
import { getEmployeeList } from "@/api";
|
||||||
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
|
import { useSelectLoadMore } from "@/hooks/useSelectLoadMore";
|
||||||
import { useLocalManager } from "@/hooks/useLocalManager";
|
import { useLocalManager } from "@/hooks/useLocalManager";
|
||||||
|
import AutoTooltip from "@/components/autoTooltip/index.vue";
|
||||||
const {
|
const {
|
||||||
options,
|
options,
|
||||||
remoteLoading,
|
remoteLoading,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
'--avatar-text-size': fontSize,
|
'--avatar-text-size': fontSize,
|
||||||
'--avatar-text-color': avatarTextColor,
|
'--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>
|
||||||
|
|||||||
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>
|
||||||
@@ -41,7 +41,14 @@
|
|||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
import { ElTag, ElButton, ElText, ElTooltip } from "element-plus";
|
import {
|
||||||
|
ElTag,
|
||||||
|
ElButton,
|
||||||
|
ElText,
|
||||||
|
ElTooltip,
|
||||||
|
ElCheckbox,
|
||||||
|
useLocale,
|
||||||
|
} from "element-plus";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DictManage } from "@/dict";
|
import { DictManage } from "@/dict";
|
||||||
@@ -54,6 +61,16 @@ const props = defineProps({
|
|||||||
requestApi: { type: Function, required: true },
|
requestApi: { type: Function, required: true },
|
||||||
initParam: { type: Object, default: () => ({}) },
|
initParam: { type: Object, default: () => ({}) },
|
||||||
pageSize: { type: Number, default: 20 },
|
pageSize: { type: Number, default: 20 },
|
||||||
|
selection: { type: Boolean, default: false }, // 是否开启多选
|
||||||
|
selectionConfig:{
|
||||||
|
type: Object,
|
||||||
|
default:()=>({
|
||||||
|
width: 50,
|
||||||
|
fixed: false,
|
||||||
|
align: "center",
|
||||||
|
key: "selection"
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 内部数据状态
|
// 内部数据状态
|
||||||
@@ -64,17 +81,31 @@ const innerData = ref<any[]>([]);
|
|||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const pageNo = ref(1);
|
const pageNo = ref(1);
|
||||||
const tableSize = ref({ width: 0, height: 400 });
|
const tableSize = ref({ width: 0, height: 400 });
|
||||||
|
const selectedKeys = ref<Set<string | number>>(new Set());
|
||||||
|
const isAllSelectedMode = ref(false); //全选模式
|
||||||
const noMore = computed(() => {
|
const noMore = computed(() => {
|
||||||
if (total.value <= 0) return false;
|
if (total.value <= 0) return false;
|
||||||
const isReachTotal = innerData.value.length >= total.value;
|
const isReachTotal = innerData.value.length >= total.value;
|
||||||
return isReachTotal;
|
return isReachTotal;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SelectionCell = (props: {
|
||||||
|
value: boolean;
|
||||||
|
intermediate?: boolean;
|
||||||
|
onChange: (val: any) => void;
|
||||||
|
}) => (
|
||||||
|
<ElCheckbox
|
||||||
|
modelValue={props.value}
|
||||||
|
indeterminate={props.intermediate}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// --- 1. 核心渲染工厂 (内置常用业务组件) ---
|
// --- 1. 核心渲染工厂 (内置常用业务组件) ---
|
||||||
// 定义一个内部小组件处理溢出逻辑
|
// 定义一个内部小组件处理溢出逻辑
|
||||||
const OverflowTooltip = defineComponent({
|
const OverflowTooltip = defineComponent({
|
||||||
props: {
|
props: {
|
||||||
val: { type: String, default: "" }
|
val: { type: String, default: "" },
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const isOverflow = ref(false);
|
const isOverflow = ref(false);
|
||||||
@@ -83,7 +114,8 @@ const OverflowTooltip = defineComponent({
|
|||||||
const checkOverflow = () => {
|
const checkOverflow = () => {
|
||||||
if (textRef.value) {
|
if (textRef.value) {
|
||||||
// 计算溢出
|
// 计算溢出
|
||||||
isOverflow.value = textRef.value.scrollWidth > textRef.value.clientWidth;
|
isOverflow.value =
|
||||||
|
textRef.value.scrollWidth > textRef.value.clientWidth;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,11 +135,11 @@ const OverflowTooltip = defineComponent({
|
|||||||
>
|
>
|
||||||
{props.val}
|
{props.val}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const RenderFactory = {
|
const RenderFactory = {
|
||||||
// 状态点/标签
|
// 状态点/标签
|
||||||
@@ -156,7 +188,7 @@ const RenderFactory = {
|
|||||||
ellipsis: (scope: any, col: any) => {
|
ellipsis: (scope: any, col: any) => {
|
||||||
const val = scope.rowData[col.prop] ?? "-";
|
const val = scope.rowData[col.prop] ?? "-";
|
||||||
return <OverflowTooltip val={val} />;
|
return <OverflowTooltip val={val} />;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,6 +204,11 @@ const updateRow = (id: string | number, rowData: object) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取当前已加载的数据
|
||||||
|
const getSelectedRows = () => {
|
||||||
|
return innerData.value.filter(row => selectedKeys.value.has(row.id));
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除某一行数据
|
* 删除某一行数据
|
||||||
* @param id 唯一标识
|
* @param id 唯一标识
|
||||||
@@ -205,24 +242,32 @@ const addRow = (rowData: object) => {
|
|||||||
|
|
||||||
// --- 适配 Columns 配置 ---
|
// --- 适配 Columns 配置 ---
|
||||||
const adaptedColumns = computed(() => {
|
const adaptedColumns = computed(() => {
|
||||||
// 1. 获取容器当前实际宽度
|
|
||||||
const containerWidth = tableSize.value.width;
|
const containerWidth = tableSize.value.width;
|
||||||
if (containerWidth <= 0) return [];
|
if (containerWidth <= 0) return [];
|
||||||
|
|
||||||
// 2. 找出没有设置宽度的列
|
// 从配置中提取宽度,如果没有传入则默认为 50
|
||||||
|
const hasSelection = props.selection;
|
||||||
|
const selectionWidth = hasSelection ? (props.selectionConfig?.width || 50) : 0;
|
||||||
|
|
||||||
|
// 1. 找出用户配置中没有设置宽度的列
|
||||||
const autoColumns = props.columns.filter((col: any) => !col.width);
|
const autoColumns = props.columns.filter((col: any) => !col.width);
|
||||||
// 3. 计算已经固定了宽度的列总和
|
|
||||||
const fixedWidthTotal = props.columns
|
// 2. 计算所有显式设置了宽度的用户列总和
|
||||||
|
const userFixedWidthTotal = props.columns
|
||||||
.filter((col: any) => col.width)
|
.filter((col: any) => col.width)
|
||||||
.reduce((prev, curr: any) => prev + Number(curr.width), 0);
|
.reduce((prev, curr: any) => prev + Number(curr.width), 0);
|
||||||
// 4. 计算剩余可用宽度
|
|
||||||
const remainingWidth = Math.max(containerWidth - fixedWidthTotal, 0);
|
// 3. 计算剩余可用宽度 (如果开启了 selection,需要额外扣除 selection 的宽度)
|
||||||
// 5. 计算每个自适应列应该分配到的平均宽度
|
const occupiedWidth = userFixedWidthTotal + selectionWidth;
|
||||||
const perAutoWidth =
|
const remainingWidth = Math.max(containerWidth - occupiedWidth, 0);
|
||||||
autoColumns.length > 0
|
|
||||||
|
// 4. 计算自适应列宽度
|
||||||
|
const perAutoWidth = autoColumns.length > 0
|
||||||
? Math.floor(remainingWidth / autoColumns.length)
|
? Math.floor(remainingWidth / autoColumns.length)
|
||||||
: 0;
|
: 0;
|
||||||
return props.columns.map((col: any) => {
|
|
||||||
|
// 5. 映射用户列
|
||||||
|
const userColumns = props.columns.map((col: any) => {
|
||||||
const isAuto = !col.width;
|
const isAuto = !col.width;
|
||||||
const finalWidth = isAuto ? Math.max(perAutoWidth, 100) : Number(col.width);
|
const finalWidth = isAuto ? Math.max(perAutoWidth, 100) : Number(col.width);
|
||||||
return {
|
return {
|
||||||
@@ -235,62 +280,57 @@ const adaptedColumns = computed(() => {
|
|||||||
fixed: col.fixed,
|
fixed: col.fixed,
|
||||||
align: col.align || "left",
|
align: col.align || "left",
|
||||||
cellRenderer: (scope: any) => {
|
cellRenderer: (scope: any) => {
|
||||||
const isOp = col.prop === "actions" || col.valueType === "actions";
|
// ... 此处保留你原有的 cellRenderer 逻辑
|
||||||
const cellClass = isOp ? "operation-column-cell" : "";
|
|
||||||
// 2. 如果是操作列,包裹一层带 class 的 div
|
|
||||||
if (isOp && col.actions) {
|
|
||||||
return (
|
|
||||||
<div class={cellClass}>
|
|
||||||
<div class="v2-operation-btns">
|
|
||||||
{col.actions.map((btn) => {
|
|
||||||
// --- 权限校验开始 ---
|
|
||||||
if (btn.permission) {
|
|
||||||
const hasAuth = checkPermission(btn.permission);
|
|
||||||
if (!hasAuth) return null; // 没权限,直接不渲染
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 现有的 show 逻辑判断(业务逻辑显隐)
|
|
||||||
const isShow =
|
|
||||||
typeof btn.show === "function"
|
|
||||||
? btn.show(scope.rowData)
|
|
||||||
: true;
|
|
||||||
const isDisabled =
|
|
||||||
typeof btn.disabled === "function"
|
|
||||||
? btn.disabled(scope.rowData)
|
|
||||||
: false;
|
|
||||||
if (!isShow) return null;
|
|
||||||
const {
|
|
||||||
onClick,
|
|
||||||
label,
|
|
||||||
permission,
|
|
||||||
show,
|
|
||||||
disabled,
|
|
||||||
...otherProps
|
|
||||||
} = btn;
|
|
||||||
return (
|
|
||||||
<ElButton
|
|
||||||
{...otherProps}
|
|
||||||
disabled={isDisabled}
|
|
||||||
onClick={() => btn.onClick(scope.rowData)}
|
|
||||||
>
|
|
||||||
{btn.label}
|
|
||||||
</ElButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof col.render === "function") return col.render(scope);
|
if (typeof col.render === "function") return col.render(scope);
|
||||||
if (col.valueType && RenderFactory[col.valueType]) {
|
if (col.valueType && RenderFactory[col.valueType]) {
|
||||||
return RenderFactory[col.valueType](scope, col);
|
return RenderFactory[col.valueType](scope, col);
|
||||||
}
|
}
|
||||||
return (
|
return <span class="v2-cell-text">{scope.rowData[col.prop] ?? "-"}</span>;
|
||||||
<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;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 请求逻辑 ---
|
// --- 请求逻辑 ---
|
||||||
@@ -313,6 +353,13 @@ const fetchTableData = async (isReset = false) => {
|
|||||||
const res = await props.requestApi(params);
|
const res = await props.requestApi(params);
|
||||||
const records = res?.records || [];
|
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];
|
innerData.value = isReset ? records : [...innerData.value, ...records];
|
||||||
total.value = res?.total || 0;
|
total.value = res?.total || 0;
|
||||||
pageNo.value++;
|
pageNo.value++;
|
||||||
@@ -360,6 +407,11 @@ defineExpose({
|
|||||||
removeRow,
|
removeRow,
|
||||||
addRow,
|
addRow,
|
||||||
updateSize,
|
updateSize,
|
||||||
|
getSelection: () => getSelectedRows(),
|
||||||
|
clearSelection: () => {
|
||||||
|
isAllSelectedMode.value = false;
|
||||||
|
selectedKeys.value.clear();
|
||||||
|
},
|
||||||
getCurrentParams: () => ({
|
getCurrentParams: () => ({
|
||||||
pageNo: pageNo.value - 1, // 因为 fetch 完后 pageNo 会自增,所以要减 1 才是当前页
|
pageNo: pageNo.value - 1, // 因为 fetch 完后 pageNo 会自增,所以要减 1 才是当前页
|
||||||
pageSize: props.pageSize,
|
pageSize: props.pageSize,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stage-breadcrumbs" :class="styleClass">
|
<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,8 +14,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: "StageBreadcrumbs" });
|
defineOptions({ name: "StageBreadcrumbs" });
|
||||||
|
|
||||||
const { title,styleClass } = defineProps<{
|
const { title,styleClass,showTitle=true } = defineProps<{
|
||||||
title: string;
|
title?: string;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
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>
|
||||||
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 };
|
||||||
|
}
|
||||||
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>
|
||||||
@@ -1,13 +1,603 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="customer-manage-container">
|
||||||
客户管理
|
<!-- 顶部搜索 -->
|
||||||
</div>
|
<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>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {reactive,ref,onMounted} from "vue"
|
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" });
|
||||||
|
|
||||||
defineOptions({})
|
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>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<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>
|
</style>
|
||||||
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