Squashed commit of the following:
commit0e84bedb67Author: 浅念 <email@sample.com> Date: Fri Aug 15 16:31:55 2025 +0800 v1.0.2 更新内容 commitf5deb7b8f7Author: 浅念 <email@sample.com> Date: Wed Aug 13 19:27:49 2025 +0800 添加菜单 权限 等页面 commite797cb24deAuthor: 浅念 <email@sample.com> Date: Mon Aug 11 19:24:24 2025 +0800 添加工单附件 及样式 commit619311dfbeAuthor: chenlan <suya123456> Date: Fri Aug 8 22:13:25 2025 +0800 添加crm 归属客户 commitb402e522bfAuthor: 浅念 <email@sample.com> Date: Thu Aug 7 10:51:17 2025 +0800 询单权限校验 commitf950f665a9Author: 浅念 <email@sample.com> Date: Thu Aug 7 10:10:45 2025 +0800 添加 询单图片预览 commit8deecc8197Author: 浅念 <email@sample.com> Date: Wed Aug 6 18:36:42 2025 +0800 修改用户页面 询单 commit3e24d1905fAuthor: 浅念 <email@sample.com> Date: Tue Aug 5 18:25:32 2025 +0800 添加用户页面 客户页面接口 等 commit7542d80e38Author: 浅念 <email@sample.com> Date: Mon Aug 4 18:05:07 2025 +0800 修改 仪表盘页面样式 commite5548f95e4Author: 浅念 <email@sample.com> Date: Mon Aug 4 14:50:55 2025 +0800 修改询单字段 仪表盘页面 及添加管理端客户页面 commitd30ea5b7bfAuthor: 浅念 <email@sample.com> Date: Mon Aug 4 14:48:58 2025 +0800 添加echarts 插件 commit0e4160c5caAuthor: 浅念 <email@sample.com> Date: Fri Aug 1 18:27:22 2025 +0800 仪表盘接口 commitbd23288acdAuthor: 浅念 <email@sample.com> Date: Fri Aug 1 18:26:46 2025 +0800 修改 仪表盘 commit6bfb940881Author: 浅念 <email@sample.com> Date: Fri Aug 1 10:01:35 2025 +0800 客户列表 添加公海详情展示 commitb50f650565Author: 浅念 <email@sample.com> Date: Thu Jul 31 17:26:36 2025 +0800 修改 客户标签样式 个人信息添加邮箱 修复关闭全部页面404 commitea1dbb7af4Author: 浅念 <email@sample.com> Date: Wed Jul 30 18:50:31 2025 +0800 修改公海和询单页面 commit1bfbb95bd9Author: 浅念 <email@sample.com> Date: Wed Jul 30 17:23:09 2025 +0800 修改样式和添加用户数据显示 commita9f5f76b35Author: 浅念 <email@sample.com> Date: Wed Jul 30 15:06:03 2025 +0800 设置默认页面 commit3ea9d3d5dfAuthor: 浅念 <email@sample.com> Date: Wed Jul 30 11:46:31 2025 +0800 修改飞书登录 公司id 系统id 获取不到问题 客户及公海添加用户信息显示 commit0cef82279fAuthor: 浅念 <email@sample.com> Date: Tue Jul 29 16:42:36 2025 +0800 修改工单 客户等页面 commit695ebbcc71Author: 浅念 <email@sample.com> Date: Fri Jul 25 17:50:04 2025 +0800 修改默认值 commitffc194a1c1Author: 浅念 <email@sample.com> Date: Fri Jul 25 17:05:33 2025 +0800 飞书授权添加系统码 及登录进入页面路由 commit48b95e252fAuthor: 浅念 <email@sample.com> Date: Fri Jul 25 15:48:49 2025 +0800 回收站添加占位 commit382011d592Author: 浅念 <email@sample.com> Date: Fri Jul 25 15:11:50 2025 +0800 修改登录页样式公海数据 个人信息默认图 commit0f790269dbAuthor: 浅念 <email@sample.com> Date: Fri Jul 25 09:16:55 2025 +0800 修改字段名称 commitf18cfdb69eAuthor: 浅念 <email@sample.com> Date: Thu Jul 24 17:28:51 2025 +0800 调整代码格式 commit858147fbbdAuthor: 浅念 <email@sample.com> Date: Thu Jul 24 10:17:22 2025 +0800 修改token刷新 及修改询单详情展示 commitc333b907e6Author: 浅念 <email@sample.com> Date: Wed Jul 23 18:22:56 2025 +0800 对接询单接口 commit7979a31e36Author: 浅念 <email@sample.com> Date: Wed Jul 23 14:40:02 2025 +0800 修改代码格式 commit4a39387865Author: 浅念 <email@sample.com> Date: Wed Jul 23 14:07:13 2025 +0800 设置客户列表权限 commitfc4de02582Author: 浅念 <email@sample.com> Date: Tue Jul 22 17:08:13 2025 +0800 feat(router): 调整菜单 commit2ffe0c9753Author: 浅念 <email@sample.com> Date: Tue Jul 22 17:04:16 2025 +0800 feat(crmApi.ts): 调整结构 commitc3fb2c48d3Author: 浅念 <email@sample.com> Date: Tue Jul 22 17:00:25 2025 +0800 feat(router): 路由调整 commitc5b0a8265aAuthor: 浅念 <email@sample.com> Date: Tue Jul 22 16:54:06 2025 +0800 chore: 添加询单页面及调整菜单 commitca06c9bc46Author: 浅念 <email@sample.com> Date: Tue Jul 22 16:35:43 2025 +0800 询单页面及菜单 commit3b6de4c7faMerge:d3bb4ec85e8b9bAuthor: 浅念 <email@sample.com> Date: Tue Jul 22 09:22:20 2025 +0800 Merge branch 'master' into feature/crm_pc commitd3bb4ec6e5Merge:c7d8908d7ab6f1Author: 浅念 <email@sample.com> Date: Mon Jul 21 18:22:55 2025 +0800 Merge branch 'master' into feature/crm_pc # Conflicts: # .gitignore # src/api/crmApi.ts # src/api/usersApi.ts # src/store/modules/user.ts # src/utils/http/index.ts # src/views/auth/login/index.vue # src/views/crmpc/customer/Show.vue # src/views/crmpc/customer/index.vue commitc7d890809cAuthor: 浅念 <email@sample.com> Date: Mon Jul 21 18:12:16 2025 +0800 添加系统码 commit31cd18fadeAuthor: 浅念 <email@sample.com> Date: Mon Jul 21 18:02:48 2025 +0800 Merge branch 'master' into feature/crm_pc # Conflicts: # .gitignore commit59c9c97c08Author: 浅念 <email@sample.com> Date: Mon Jul 21 17:56:52 2025 +0800 Merge branch 'master' into feature/crm_pc # Conflicts: # .gitignore commitd4b577c42eAuthor: 浅念 <email@sample.com> Date: Mon Jul 21 13:03:56 2025 +0800 feat(crm): 添加相关图片 commite4967760b7Merge:0c6766685102fcAuthor: 浅念 <email@sample.com> Date: Mon Jul 21 11:25:49 2025 +0800 Finish crm_pc commit85102fcf32Author: 浅念 <email@sample.com> Date: Mon Jul 21 11:24:06 2025 +0800 crm 接口对接 commit0c67666793Author: 浅念 <email@sample.com> Date: Wed Jul 16 17:18:14 2025 +0800 添加上传头部信息 commita41c75f703Author: 浅念 <email@sample.com> Date: Tue Jul 15 09:40:53 2025 +0800 修改页面 commit22ae0e86c8Author: 浅念 <email@sample.com> Date: Mon Jul 14 17:59:02 2025 +0800 修改页面 commit812e3c4b0cAuthor: 浅念 <email@sample.com> Date: Mon Jul 14 10:09:10 2025 +0800 修改菜单问题 commit2ad479b006Author: 浅念 <email@sample.com> Date: Fri Jul 11 19:45:57 2025 +0800 修改样式 commit5802e157bfAuthor: 浅念 <email@sample.com> Date: Fri Jul 11 19:43:34 2025 +0800 修改工单 commit713d43f7b4Author: 浅念 <email@sample.com> Date: Fri Jul 11 18:37:16 2025 +0800 添加登录 commit719911918fAuthor: 浅念 <email@sample.com> Date: Fri Jul 4 17:16:38 2025 +0800 项目创建 commitce47b39fa5Author: 浅念 <email@sample.com> Date: Wed Jul 2 09:37:51 2025 +0800 删除
@@ -3,6 +3,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
*.env
|
||||||
.cursorrules
|
.cursorrules
|
||||||
src/types/components.d.ts
|
src/types/components.d.ts
|
||||||
src/types/auto-imports.d.ts
|
src/types/auto-imports.d.ts
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pnpm dlx commitlint --edit $1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pnpm run lint:lint-staged
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@types/echarts": "^4.9.22",
|
||||||
"@vue/reactivity": "^3.4.35",
|
"@vue/reactivity": "^3.4.35",
|
||||||
"@vueuse/core": "^11.0.0",
|
"@vueuse/core": "^11.0.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"echarts": "^5.4.0",
|
"echarts": "^5.4.0",
|
||||||
|
"echarts-gl": "^2.0.9",
|
||||||
"element-plus": "^2.8.0",
|
"element-plus": "^2.8.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
@@ -2982,6 +2984,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/echarts": {
|
||||||
|
"version": "4.9.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.22.tgz",
|
||||||
|
"integrity": "sha512-7Fo6XdWpoi8jxkwP7BARUOM7riq8bMhmsCtSG8gzUcJmFhLo387tihoBYS/y5j7jl3PENT5RxeWZdN9RiwO7HQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/zrender": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -3138,6 +3149,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/zrender": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/zrender/-/zrender-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.34.1",
|
"version": "8.34.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
||||||
@@ -5103,6 +5120,11 @@
|
|||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/claygl": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
|
||||||
|
},
|
||||||
"node_modules/cli-cursor": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||||
@@ -6399,6 +6421,18 @@
|
|||||||
"zrender": "5.6.1"
|
"zrender": "5.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts-gl": {
|
||||||
|
"version": "2.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz",
|
||||||
|
"integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==",
|
||||||
|
"dependencies": {
|
||||||
|
"claygl": "^1.2.1",
|
||||||
|
"zrender": "^5.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"echarts": "^5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.170",
|
"version": "1.5.170",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
"*.{js,ts,vue}": [
|
||||||
|
"eslint --fix --quiet",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
"*.{js,ts,mjs,mts,tsx}": [
|
"*.{js,ts,mjs,mts,tsx}": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
@@ -48,6 +52,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@types/echarts": "^4.9.22",
|
||||||
"@vue/reactivity": "^3.4.35",
|
"@vue/reactivity": "^3.4.35",
|
||||||
"@vueuse/core": "^11.0.0",
|
"@vueuse/core": "^11.0.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
@@ -55,6 +60,7 @@
|
|||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"echarts": "^5.4.0",
|
"echarts": "^5.4.0",
|
||||||
|
"echarts-gl": "^2.0.9",
|
||||||
"element-plus": "^2.8.0",
|
"element-plus": "^2.8.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
|
|||||||
@@ -20,8 +20,11 @@
|
|||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const companyid = params.get('corp_id')
|
const companyid = params.get('corp_id')
|
||||||
const companyabbr = params.get('corp_abbr')
|
const companyabbr = params.get('corp_abbr')
|
||||||
|
// const systemid = params.get('system_id')
|
||||||
|
// const system_code = params.get('system_code')
|
||||||
if (companyid) {
|
if (companyid) {
|
||||||
userStore.setCompany(companyid, companyabbr || '')
|
userStore.setCompany(companyid, companyabbr || '')
|
||||||
|
// userStore.setSystems(systemid || '', system_code || '')
|
||||||
// 你也可以加 localStorage/sessionStorage 备份
|
// 你也可以加 localStorage/sessionStorage 备份
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,11 +32,9 @@
|
|||||||
zh: zh,
|
zh: zh,
|
||||||
en: en
|
en: en
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
setThemeTransitionClass(true)
|
setThemeTransitionClass(true)
|
||||||
setCompanyFromUrl()
|
setCompanyFromUrl()
|
||||||
useUserStore().restoreTokenTimer()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 10, 118, 0.1);
|
box-shadow: 0 2px 12px 0 rgba(0, 10, 118, 0.1);
|
||||||
background: var(--art-main-bg-color);
|
background: var(--art-main-bg-color);
|
||||||
padding: 10px;
|
padding: 10px 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.border {
|
.border {
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
export class manageService {
|
||||||
|
//仪表盘 总
|
||||||
|
static statisticswhole() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/statistics/whole'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//各渠道统计
|
||||||
|
static channelcount() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/statistics/channel/count'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//各国家统计
|
||||||
|
static countrycount() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/statistics/country/count'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//跟单人统计
|
||||||
|
static trackcount() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/statistics/track/count'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//每日录单统计
|
||||||
|
static daycount() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/statistics/day/count'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//每日录单统计
|
||||||
|
static inquirycount(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/statistics/inquiry/count',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//询单列表
|
||||||
|
static getinquiryList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/inquiry/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 获取详情
|
||||||
|
static inquirydetail(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/inquiry/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//添加询单
|
||||||
|
static inquiryAdd(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/inquiry/store',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//修改询单
|
||||||
|
static inquiryedit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/inquiry/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//删除询单
|
||||||
|
static inquirydelete(params: any) {
|
||||||
|
return request.get<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/inquiry/destroy',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//分配询单
|
||||||
|
static inquiryallocation(params: any) {
|
||||||
|
return request.post<Api.Work.AddResponse>({
|
||||||
|
url: '/admin/inquiry/allocation',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//重新分配询单
|
||||||
|
static inquiryreallocation(params: any) {
|
||||||
|
return request.post<Api.Work.AddResponse>({
|
||||||
|
url: '/admin/inquiry/reallocation',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//发布询单
|
||||||
|
static inquiryrelease(params: any) {
|
||||||
|
return request.post<Api.Work.AddResponse>({
|
||||||
|
url: '/admin/inquiry/release',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//获取用户下拉框
|
||||||
|
static presetusers() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/common/preset/users'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//crm 客户管理
|
||||||
|
//客户列表
|
||||||
|
static getcrmCustomerList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListDatad>({
|
||||||
|
url: '/admin/crm/customer/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//客户导出
|
||||||
|
static crmCustomerExport(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/crm/customer/export',
|
||||||
|
params,
|
||||||
|
responseType: 'blob' // 设置响应类型为 blob
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//客户审核
|
||||||
|
static crmCustomerAudit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/crm/customer/audit',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//客户转移
|
||||||
|
static crmCustomerTransfer(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/crm/customer/transfer',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//用户列表
|
||||||
|
static getUserList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/user/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//用户编辑
|
||||||
|
static userEdit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/user/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//用户设置角色
|
||||||
|
static userSetRole(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/user/authorize',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//用户菜单权限设置
|
||||||
|
static userMenuAuth(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/user/set/resources',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//清除缓存
|
||||||
|
static clearCache() {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/common/preset/cache/clear'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公海客户分配
|
||||||
|
static crmCustomerAllocation(params: any) {
|
||||||
|
return request.get<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/crm/customer/cede',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公海客户跟进记录
|
||||||
|
static crmCustomerFollow(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/crm/customer/follow',
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公海客户删除
|
||||||
|
static crmCustomerDelete(params: any) {
|
||||||
|
return request.get<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/crm/customer/delete',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,19 @@ export class CrmService {
|
|||||||
}
|
}
|
||||||
//删除客户
|
//删除客户
|
||||||
static getcustomerdelete(params: any) {
|
static getcustomerdelete(params: any) {
|
||||||
return request.get<Api.Crm.CrmListData>({
|
return request.get<any>({
|
||||||
url: '/crm/customer/delete',
|
url: '/crm/customer/delete',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//标记无效
|
||||||
|
static customerReport(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/crm/customer/report',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
//切换等级crm/customer/grade
|
//切换等级crm/customer/grade
|
||||||
static customergrade(params: any) {
|
static customergrade(params: any) {
|
||||||
return request.post<Api.Work.AddResponse>({
|
return request.post<Api.Work.AddResponse>({
|
||||||
@@ -39,10 +47,9 @@ export class CrmService {
|
|||||||
// showErrorMessage: false // 不显示错误消息
|
// showErrorMessage: false // 不显示错误消息
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//渠道下拉
|
//渠道下拉
|
||||||
static getChannel() {
|
static getChannel() {
|
||||||
return request.get<Api.Brand.BrandInfo>({
|
return request.get<any>({
|
||||||
url: '/crm/customer/getChannel'
|
url: '/crm/customer/getChannel'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -65,9 +72,10 @@ export class CrmService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
//标签列表/crm/tagGroup/list
|
//标签列表/crm/tagGroup/list
|
||||||
static tagGrouplist() {
|
static tagGrouplist(params: any) {
|
||||||
return request.get<any>({
|
return request.get<any>({
|
||||||
url: '/crm/tagGroup/list'
|
url: '/crm/tagGroup/list',
|
||||||
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//更新客户标签 /crm/tagGroup/updatePivot
|
//更新客户标签 /crm/tagGroup/updatePivot
|
||||||
@@ -153,4 +161,34 @@ static followNext(params: any) {
|
|||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//从属客户列表
|
||||||
|
static getCustomercsList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/crm/customer/index',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//从属客户转移
|
||||||
|
static cedeCustomer(params: any) {
|
||||||
|
return request.post<Api.Work.AddResponse>({
|
||||||
|
url: '/crm/customer/cede',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//所属员工
|
||||||
|
static getCustomerUserList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/common/user/team',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//标记离职
|
||||||
|
static markLeave(params: any) {
|
||||||
|
return request.get<Api.Work.AddResponse>({
|
||||||
|
url: '/common/user/resign',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,10 @@
|
|||||||
import { asyncRoutes } from '@/router/routes/asyncRoutes'
|
|
||||||
import { menuDataToRouter } from '@/router/utils/menuToRouter'
|
import { menuDataToRouter } from '@/router/utils/menuToRouter'
|
||||||
import { AppRouteRecord } from '@/types/router'
|
import { AppRouteRecord } from '@/types/router'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { UserService } from '@/api/usersApi'
|
||||||
interface MenuResponse {
|
interface MenuResponse {
|
||||||
menuList: AppRouteRecord[]
|
menuList: AppRouteRecord[]
|
||||||
}
|
}
|
||||||
// 合并方法
|
|
||||||
function mergeRoutes(dynamicRoutes: AppRouteRecord[] = [], staticRoutes: AppRouteRecord[] = []): AppRouteRecord[] {
|
|
||||||
// 深度合并逻辑
|
|
||||||
const merged = [...staticRoutes]
|
|
||||||
|
|
||||||
dynamicRoutes.forEach(dynamicRoute => {
|
|
||||||
const existingIndex = merged.findIndex(r => r.path === dynamicRoute.path)
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
// 合并children(如果存在)
|
|
||||||
if (dynamicRoute.children) {
|
|
||||||
merged[existingIndex].children = [
|
|
||||||
...(merged[existingIndex].children || []),
|
|
||||||
...dynamicRoute.children
|
|
||||||
]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
merged.push(dynamicRoute)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
// 菜单接口
|
// 菜单接口
|
||||||
export const menuService = {
|
export const menuService = {
|
||||||
async getMenuList(delay = 300): Promise<MenuResponse> {
|
async getMenuList(delay = 300): Promise<MenuResponse> {
|
||||||
@@ -36,30 +14,27 @@ export const menuService = {
|
|||||||
if (!userStore.isLogin) {
|
if (!userStore.isLogin) {
|
||||||
throw new Error('用户未登录')
|
throw new Error('用户未登录')
|
||||||
}
|
}
|
||||||
|
const { menu } = await UserService.getMenuList()
|
||||||
|
const menuData = menu
|
||||||
|
// const userStore = useUserStore()
|
||||||
// 模拟接口返回的菜单数据
|
// 模拟接口返回的菜单数据
|
||||||
const menuData = userStore.info.menu
|
|
||||||
// 2. 合并静态菜单
|
|
||||||
const finalRoutes = mergeRoutes(menuData, asyncRoutes)
|
|
||||||
// 验证菜单数据
|
// 验证菜单数据
|
||||||
if (!menuData || !Array.isArray(menuData) || menuData.length === 0) {
|
if (!menuData || !Array.isArray(menuData) || menuData.length === 0) {
|
||||||
ElMessage.error('暂无系统菜单权限')
|
ElMessage.error('暂无系统菜单权限')
|
||||||
return { menuList: [] }
|
return { menuList: [] }
|
||||||
}
|
}
|
||||||
|
console.log('menuData', menuData)
|
||||||
// 处理菜单数据
|
// 处理菜单数据
|
||||||
const menuList = finalRoutes.map((route: any) => menuDataToRouter(route))
|
const menuList = menuData.map((route: any) => menuDataToRouter(route))
|
||||||
|
|
||||||
// 验证处理后的菜单列表
|
// 验证处理后的菜单列表
|
||||||
if (!menuList || menuList.length === 0) {
|
if (!menuList || menuList.length === 0) {
|
||||||
ElMessage.error('菜单数据处理失败')
|
ElMessage.error('菜单数据处理失败')
|
||||||
return { menuList: [] }
|
return { menuList: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟接口延迟
|
// 模拟接口延迟
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
|
||||||
return { menuList }
|
return { menuList }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取菜单失败:', error)
|
console.error('获取菜单失败:', error)
|
||||||
throw error instanceof Error ? error : new Error('获取菜单失败')
|
throw error instanceof Error ? error : new Error('获取菜单失败')
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
export class systemService {
|
||||||
|
//系统列表
|
||||||
|
static getSystemList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/systems/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//系统添加
|
||||||
|
static systemAdd(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/systems/store',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//系统详情
|
||||||
|
static systemDetail(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/systems/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//系统编辑
|
||||||
|
static systemEdit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/systems/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//系统删除
|
||||||
|
static systemDelete(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/systems/destroy',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//飞书应用列表
|
||||||
|
static getFeishuAppList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/corp/feishu/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//飞书应用添加
|
||||||
|
static feishuAppAdd(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/corp/feishu/store',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//飞书应用详情
|
||||||
|
static feishuAppDetail(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/corp/feishu/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//飞书应用编辑
|
||||||
|
static feishuAppEdit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/corp/feishu/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//飞书应用删除
|
||||||
|
static feishuAppDelete(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/corp/feishu/destroy',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公司列表
|
||||||
|
static getCompanyList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/corp/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公司添加
|
||||||
|
static companyAdd(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/corp/store',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公司详情
|
||||||
|
static companyDetail(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/corp/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公司编辑
|
||||||
|
static companyEdit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/corp/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//公司删除
|
||||||
|
static companyDelete(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/corp/destroy',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色列表
|
||||||
|
static getRoleList(params: Api.Common.PaginatingParams) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/role/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色添加
|
||||||
|
static roleAdd(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/role/store',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色详情
|
||||||
|
static roleDetail(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/role/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色编辑
|
||||||
|
static roleEdit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/role/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色删除
|
||||||
|
static roleDelete(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/role/destroy',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色权限设置
|
||||||
|
static roleAuth(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/role/menu/resource',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//角色下拉框
|
||||||
|
static getRoleSelectList() {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/common/preset/roles'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//菜单列表
|
||||||
|
static getMenutreeList(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/resources/list/tree',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
static getMenuList() {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/resources/list/menu'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//菜单添加
|
||||||
|
static menuAdd(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/resources/store',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//菜单详情
|
||||||
|
static menuDetail(params: any) {
|
||||||
|
return request.get<any>({
|
||||||
|
url: '/admin/resources/show',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//菜单编辑
|
||||||
|
static menuEdit(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/resources/update',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//菜单删除
|
||||||
|
static menuDelete(params: any) {
|
||||||
|
return request.post<Api.Common.AddResponse>({
|
||||||
|
url: '/admin/resources/destroy',
|
||||||
|
params
|
||||||
|
// showErrorMessage: false // 不显示错误消息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//权限菜单
|
||||||
|
static getPermissionMenuList(params: any) {
|
||||||
|
return request.get<Api.Common.CommonListData>({
|
||||||
|
url: '/admin/resources/list/menu/tree',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@ export class UserService {
|
|||||||
url: '/common/preset/corps'
|
url: '/common/preset/corps'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//获取系统
|
||||||
|
static getsystemsList() {
|
||||||
|
return request.get<Api.Brand.BrandInfo>({
|
||||||
|
url: '/common/preset/systems'
|
||||||
|
})
|
||||||
|
}
|
||||||
//注册
|
//注册
|
||||||
static register(params: any) {
|
static register(params: any) {
|
||||||
return request.post<Api.Auth.registerResponse>({
|
return request.post<Api.Auth.registerResponse>({
|
||||||
@@ -16,7 +22,7 @@ export class UserService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
//获取 飞书信息
|
//获取 飞书信息
|
||||||
static getfeishuAppList(params: { corp_id: string }) {
|
static getfeishuAppList(params: any) {
|
||||||
return request.get<Api.Brand.BrandInfo>({
|
return request.get<Api.Brand.BrandInfo>({
|
||||||
url: '/common/preset/feishu/app',
|
url: '/common/preset/feishu/app',
|
||||||
params
|
params
|
||||||
@@ -66,4 +72,17 @@ export class UserService {
|
|||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//获取菜单
|
||||||
|
static getMenuList() {
|
||||||
|
return request.get<Api.User.UserInfo>({
|
||||||
|
url: '/common/preset/menus'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//个人信息编辑
|
||||||
|
static editUserInfo(params: any) {
|
||||||
|
return request.post<Api.Work.AddResponse>({
|
||||||
|
url: '/admin/user/edit',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ export class WorkService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
//工单状态下拉选择框
|
//工单状态下拉选择框
|
||||||
static getStatus() {
|
static getStatus(params: any) {
|
||||||
return request.get<Api.Brand.BrandInfo>({
|
return request.get<Api.Brand.BrandInfo>({
|
||||||
url: '/ticket/ticket/getStatus'
|
url: '/ticket/ticket/getStatus',
|
||||||
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//工单沟通记录
|
//工单沟通记录
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
{
|
||||||
|
"wordGeoCoordMap":{
|
||||||
|
"阿富汗": [67.709953, 33.93911],
|
||||||
|
"安哥拉": [17.873887, -11.202692],
|
||||||
|
"阿尔巴尼亚": [20.168331, 41.153332],
|
||||||
|
"阿拉伯联合酋长国": [53.847818, 23.424076],
|
||||||
|
"阿根廷": [-63.61667199999999, -38.416097],
|
||||||
|
"亚美尼亚": [45.038189, 40.069099],
|
||||||
|
"法属南半球和南极领地": [69.348557, -49.280366],
|
||||||
|
"澳大利亚": [133.775136, -25.274398],
|
||||||
|
"奥地利": [14.550072, 47.516231],
|
||||||
|
"阿塞拜疆": [47.576927, 40.143105],
|
||||||
|
"布隆迪": [29.918886, -3.373056],
|
||||||
|
"比利时": [4.469936, 50.503887],
|
||||||
|
"贝宁": [2.315834, 9.30769],
|
||||||
|
"布基纳法索": [-1.561593, 12.238333],
|
||||||
|
"孟加拉人民共和国": [90.356331, 23.684994],
|
||||||
|
"保加利亚": [25.48583, 42.733883],
|
||||||
|
"巴哈马": [-77.39627999999999, 25.03428],
|
||||||
|
"波斯尼亚和黑塞哥维那": [17.679076, 43.915886],
|
||||||
|
"白俄罗斯": [27.953389, 53.709807],
|
||||||
|
"伯利兹": [-88.49765, 17.189877],
|
||||||
|
"百慕大": [-64.7505, 32.3078],
|
||||||
|
"玻利维亚": [-63.58865299999999, -16.290154],
|
||||||
|
"巴西": [-51.92528, -14.235004],
|
||||||
|
"文莱": [114.727669, 4.535277],
|
||||||
|
"不丹": [90.433601, 27.514162],
|
||||||
|
"博茨瓦纳": [24.684866, -22.328474],
|
||||||
|
"中非共和国": [20.939444, 6.611110999999999],
|
||||||
|
"加拿大": [-106.346771, 56.130366],
|
||||||
|
"瑞士": [8.227511999999999, 46.818188],
|
||||||
|
"智利": [-71.542969, -35.675147],
|
||||||
|
"中华人民共和国": [104.195397, 35.86166],
|
||||||
|
"象牙海岸": [-5.547079999999999, 7.539988999999999],
|
||||||
|
"喀麦隆": [12.354722, 7.369721999999999],
|
||||||
|
"刚果民主共和国": [21.758664, -4.038333],
|
||||||
|
"刚果共和国": [15.827659, -0.228021],
|
||||||
|
"哥伦比亚": [-74.297333, 4.570868],
|
||||||
|
"哥斯达黎加共和国": [-83.753428, 9.748916999999999],
|
||||||
|
"古巴": [-77.781167, 21.521757],
|
||||||
|
"北塞浦路斯": [33.429859, 35.126413],
|
||||||
|
"塞浦路斯": [33.429859, 35.126413],
|
||||||
|
"捷克共和国": [15.472962, 49.81749199999999],
|
||||||
|
"德国": [10.451526, 51.165691],
|
||||||
|
"吉布提": [42.590275, 11.825138],
|
||||||
|
"丹麦": [9.501785, 56.26392],
|
||||||
|
"多明尼加共和国": [-70.162651, 18.735693],
|
||||||
|
"阿尔及利亚": [1.659626, 28.033886],
|
||||||
|
"厄瓜多尔": [-78.18340599999999, -1.831239],
|
||||||
|
"埃及": [30.802498, 26.820553],
|
||||||
|
"厄立特里亚": [39.782334, 15.179384],
|
||||||
|
"西班牙": [-3.74922, 40.46366700000001],
|
||||||
|
"爱沙尼亚": [25.013607, 58.595272],
|
||||||
|
"埃塞俄比亚": [40.489673, 9.145000000000001],
|
||||||
|
"芬兰": [25.748151, 61.92410999999999],
|
||||||
|
"斐": [178.065032, -17.713371],
|
||||||
|
"福克兰群岛": [-59.523613, -51.796253],
|
||||||
|
"法国": [2.213749, 46.227638],
|
||||||
|
"加蓬共和国": [11.609444, -0.803689],
|
||||||
|
"英国": [-3.435973, 55.378051],
|
||||||
|
"格鲁吉亚": [-82.9000751, 32.1656221],
|
||||||
|
"加纳": [-1.023194, 7.946527],
|
||||||
|
"几内亚": [-9.696645, 9.945587],
|
||||||
|
"冈比亚": [-15.310139, 13.443182],
|
||||||
|
"几内亚比绍": [-15.180413, 11.803749],
|
||||||
|
"赤道几内亚": [10.267895, 1.650801],
|
||||||
|
"希腊": [21.824312, 39.074208],
|
||||||
|
"格陵兰": [-42.604303, 71.706936],
|
||||||
|
"危地马拉": [-90.23075899999999, 15.783471],
|
||||||
|
"法属圭亚那": [-53.125782, 3.933889],
|
||||||
|
"圭亚那": [-58.93018, 4.860416],
|
||||||
|
"洪都拉斯": [-86.241905, 15.199999],
|
||||||
|
"克罗地亚": [15.2, 45.1],
|
||||||
|
"海地": [-72.285215, 18.971187],
|
||||||
|
"匈牙利共和国": [19.503304, 47.162494],
|
||||||
|
"印尼": [113.921327, -0.789275],
|
||||||
|
"爱尔兰": [-8.24389, 53.41291],
|
||||||
|
"伊朗": [53.688046, 32.427908],
|
||||||
|
"伊拉克": [43.679291, 33.223191],
|
||||||
|
"冰岛": [-19.020835, 64.963051],
|
||||||
|
"以色列": [34.851612, 31.046051],
|
||||||
|
"意大利": [12.56738, 41.87194],
|
||||||
|
"牙买加": [-77.297508, 18.109581],
|
||||||
|
"约旦": [36.238414, 30.585164],
|
||||||
|
"日本": [138.252924, 36.204824],
|
||||||
|
"哈萨克斯坦": [66.923684, 48.019573],
|
||||||
|
"肯尼亚": [37.906193, -0.023559],
|
||||||
|
"吉尔吉斯斯坦": [74.766098, 41.20438],
|
||||||
|
"柬埔寨": [104.990963, 12.565679],
|
||||||
|
"韩国": [127.766922, 35.907757],
|
||||||
|
"科索沃": [20.902977, 42.6026359],
|
||||||
|
"科威特": [47.481766, 29.31166],
|
||||||
|
"老挝": [102.495496, 19.85627],
|
||||||
|
"黎巴嫩": [35.862285, 33.854721],
|
||||||
|
"利比里亚共和国": [-9.429499000000002, 6.428055],
|
||||||
|
"利比亚": [17.228331, 26.3351],
|
||||||
|
"斯里兰卡": [80.77179699999999, 7.873053999999999],
|
||||||
|
"莱索托": [28.233608, -29.609988],
|
||||||
|
"立陶宛": [23.881275, 55.169438],
|
||||||
|
"巴林": [50.30, 26.10],
|
||||||
|
"卢森堡": [6.129582999999999, 49.815273],
|
||||||
|
"拉脱维亚共和国": [24.603189, 56.879635],
|
||||||
|
"摩洛哥": [-7.092619999999999, 31.791702],
|
||||||
|
"摩尔多瓦": [28.369885, 47.411631],
|
||||||
|
"马达加斯加": [46.869107, -18.766947],
|
||||||
|
"墨西哥": [-102.552784, 23.634501],
|
||||||
|
"马其顿": [21.745275, 41.608635],
|
||||||
|
"马里": [-3.996166, 17.570692],
|
||||||
|
"缅甸": [95.956223, 21.913965],
|
||||||
|
"黑山共和国": [19.37439, 42.708678],
|
||||||
|
"蒙古": [103.846656, 46.862496],
|
||||||
|
"莫桑比克": [35.529562, -18.665695],
|
||||||
|
"毛里塔尼亚": [-10.940835, 21.00789],
|
||||||
|
"马拉维": [34.301525, -13.254308],
|
||||||
|
"马来西亚": [101.975766, 4.210484],
|
||||||
|
"纳米比亚": [18.49041, -22.95764],
|
||||||
|
"新喀里多尼亚": [165.618042, -20.904305],
|
||||||
|
"尼日尔": [8.081666, 17.607789],
|
||||||
|
"尼日利亚": [8.675277, 9.081999],
|
||||||
|
"尼加拉瓜": [-85.207229, 12.865416],
|
||||||
|
"荷兰": [5.291265999999999, 52.132633],
|
||||||
|
"挪威": [8.468945999999999, 60.47202399999999],
|
||||||
|
"尼泊尔": [84.12400799999999, 28.394857],
|
||||||
|
"新西兰": [174.885971, -40.900557],
|
||||||
|
"阿曼": [55.923255, 21.512583],
|
||||||
|
"巴基斯坦": [69.34511599999999, 30.375321],
|
||||||
|
"巴拿马共和国": [-80.782127, 8.537981],
|
||||||
|
"秘鲁": [-75.015152, -9.189967],
|
||||||
|
"菲律宾": [121.774017, 12.879721],
|
||||||
|
"巴布亚新几内亚": [143.95555, -6.314992999999999],
|
||||||
|
"波兰": [19.145136, 51.919438],
|
||||||
|
"波多黎各": [-66.590149, 18.220833],
|
||||||
|
"北朝鲜": [127.510093, 40.339852],
|
||||||
|
"葡萄牙": [-8.224454, 39.39987199999999],
|
||||||
|
"巴拉圭": [-58.443832, -23.442503],
|
||||||
|
"卡塔尔": [51.183884, 25.354826],
|
||||||
|
"罗马尼亚": [24.96676, 45.943161],
|
||||||
|
"俄罗斯": [105.318756, 61.52401],
|
||||||
|
"俄罗斯联邦": [105.318756, 61.52401],
|
||||||
|
"卢旺达": [29.873888, -1.940278],
|
||||||
|
"西撒哈拉": [-12.885834, 24.215527],
|
||||||
|
"沙特阿拉伯": [45.079162, 23.885942],
|
||||||
|
"苏丹": [30.217636, 12.862807],
|
||||||
|
"南苏丹": [31.3069788, 6.876991899999999],
|
||||||
|
"塞内加尔": [-14.452362, 14.497401],
|
||||||
|
"所罗门群岛": [160.156194, -9.64571],
|
||||||
|
"塞拉利昂": [-11.779889, 8.460555],
|
||||||
|
"萨尔瓦多": [-88.89653, 13.794185],
|
||||||
|
"索马里兰": [46.8252838, 9.411743399999999],
|
||||||
|
"索马里": [46.199616, 5.152149],
|
||||||
|
"塞尔维亚": [21.005859, 44.016521],
|
||||||
|
"苏里南": [-56.027783, 3.919305],
|
||||||
|
"斯洛伐克共和国": [19.699024, 48.669026],
|
||||||
|
"斯洛文尼亚": [14.995463, 46.151241],
|
||||||
|
"瑞典": [18.643501, 60.12816100000001],
|
||||||
|
"斯威士兰": [31.465866, -26.522503],
|
||||||
|
"叙利亚": [38.996815, 34.80207499999999],
|
||||||
|
"乍得": [18.732207, 15.454166],
|
||||||
|
"多哥": [0.824782, 8.619543],
|
||||||
|
"泰国": [100.992541, 15.870032],
|
||||||
|
"塔吉克斯坦共和国": [71.276093, 38.861034],
|
||||||
|
"土库曼斯坦": [59.556278, 38.969719],
|
||||||
|
"东帝汶": [125.727539, -8.874217],
|
||||||
|
"特里尼达和多巴哥": [-61.222503, 10.691803],
|
||||||
|
"突尼斯": [9.537499, 33.886917],
|
||||||
|
"土耳其": [35.243322, 38.963745],
|
||||||
|
"坦桑尼亚": [34.888822, -6.369028],
|
||||||
|
"乌干达": [32.290275, 1.373333],
|
||||||
|
"乌克兰": [31.16558, 48.379433],
|
||||||
|
"乌拉圭": [-55.765835, -32.522779],
|
||||||
|
"美国": [-95.712891, 37.09024],
|
||||||
|
"乌兹别克斯坦": [64.585262, 41.377491],
|
||||||
|
"委内瑞拉": [-66.58973, 6.42375],
|
||||||
|
"越南": [108.277199, 14.058324],
|
||||||
|
"瓦努阿图": [166.959158, -15.376706],
|
||||||
|
"西岸": [35.3027226, 31.9465703],
|
||||||
|
"也门": [48.516388, 15.552727],
|
||||||
|
"南非": [22.937506, -30.559482],
|
||||||
|
"赞比亚": [27.849332, -13.133897],
|
||||||
|
"津巴布韦": [29.154857, -19.015438],
|
||||||
|
"科摩罗": [43.872219, -11.875001],
|
||||||
|
"印度尼西亚": [106.515414, -6.103040],
|
||||||
|
"印度": [78.96288, 20.593684],
|
||||||
|
"刚果": [15.12, -4.09],
|
||||||
|
"萨摩亚独立国": [-171.46, 13.5],
|
||||||
|
"朝鲜": [125.30, 39.09],
|
||||||
|
"新加坡": [103.87546, 1.30243],
|
||||||
|
"中非": [18.33, 4.21],
|
||||||
|
"巴勒斯坦": [35.25, 31.79],
|
||||||
|
"圣卢西亚": [-60.58, 14.02],
|
||||||
|
"安提瓜和巴布达": [-61.48, 17.20],
|
||||||
|
"巴巴多斯": [-59.30, 13.05],
|
||||||
|
"开曼群岛": [-81.24, 19.20],
|
||||||
|
"法罗群岛": [-6.56, 62.05],
|
||||||
|
"圣文森特和格林纳丁斯": [-61.10, 13.10],
|
||||||
|
"特立尼达和多巴哥": [-60.32, 11.18],
|
||||||
|
"台澎金马单独关税区": [121.21, 25.02],
|
||||||
|
"中国香港": [114.12, 22.26],
|
||||||
|
"中国澳门": [113.5, 22.2],
|
||||||
|
"格林纳达": [-61.4, 12.7],
|
||||||
|
"塞舌尔共和国": [55.28, -4.4],
|
||||||
|
"多米尼克": [61.23, 15.18],
|
||||||
|
"斐济": [178.30, -18.06],
|
||||||
|
"科特迪瓦": [-5.17, 6.49],
|
||||||
|
"欧盟": [4.47, 50.50]
|
||||||
|
},
|
||||||
|
"nameMap":{
|
||||||
|
"Afghanistan": "阿富汗",
|
||||||
|
"Singapore": "新加坡",
|
||||||
|
"Angola": "安哥拉",
|
||||||
|
"Albania": "阿尔巴尼亚",
|
||||||
|
"United Arab Emirates": "阿拉伯联合酋长国",
|
||||||
|
"Argentina": "阿根廷",
|
||||||
|
"Armenia": "亚美尼亚",
|
||||||
|
"French Southern and Antarctic Lands": "法属南半球和南极领地",
|
||||||
|
"Australia": "澳大利亚",
|
||||||
|
"Austria": "奥地利",
|
||||||
|
"Azerbaijan": "阿塞拜疆",
|
||||||
|
"Seychelles": "塞舌尔共和国",
|
||||||
|
"Burundi": "布隆迪",
|
||||||
|
"Belgium": "比利时",
|
||||||
|
"Benin": "贝宁",
|
||||||
|
"Burkina Faso": "布基纳法索",
|
||||||
|
"Côte d Ivoire": "科特迪瓦",
|
||||||
|
"Bangladesh": "孟加拉人民共和国",
|
||||||
|
"Bulgaria": "保加利亚",
|
||||||
|
"The Bahamas": "巴哈马",
|
||||||
|
"Bosnia and Herzegovina": "波斯尼亚和黑塞哥维那",
|
||||||
|
"Belarus": "白俄罗斯",
|
||||||
|
"Belize": "伯利兹",
|
||||||
|
"Bermuda": "百慕大",
|
||||||
|
"Bolivia": "玻利维亚",
|
||||||
|
"Brazil": "巴西",
|
||||||
|
"Brunei": "文莱",
|
||||||
|
"Bhutan": "不丹",
|
||||||
|
"Botswana": "博茨瓦纳",
|
||||||
|
"Central African Republic": "中非共和国",
|
||||||
|
"Canada": "加拿大",
|
||||||
|
"Switzerland": "瑞士",
|
||||||
|
"Chile": "智利",
|
||||||
|
"China": "中华人民共和国",
|
||||||
|
"Ivory Coast": "象牙海岸",
|
||||||
|
"Cameroon": "喀麦隆",
|
||||||
|
"Dem. Rep. Congo": "刚果民主共和国",
|
||||||
|
"Republic of the Congo": "刚果共和国",
|
||||||
|
"Colombia": "哥伦比亚",
|
||||||
|
"Costa Rica": "哥斯达黎加共和国",
|
||||||
|
"Cuba": "古巴",
|
||||||
|
"Northern Cyprus": "北塞浦路斯",
|
||||||
|
"Cyprus": "塞浦路斯",
|
||||||
|
"Czech Republic": "捷克共和国",
|
||||||
|
"Germany": "德国",
|
||||||
|
"Djibouti": "吉布提",
|
||||||
|
"Denmark": "丹麦",
|
||||||
|
"Dominican Republic": "多明尼加共和国",
|
||||||
|
"Algeria": "阿尔及利亚",
|
||||||
|
"Ecuador": "厄瓜多尔",
|
||||||
|
"Egypt": "埃及",
|
||||||
|
"Eritrea": "厄立特里亚",
|
||||||
|
"Spain": "西班牙",
|
||||||
|
"Estonia": "爱沙尼亚",
|
||||||
|
"Ethiopia": "埃塞俄比亚",
|
||||||
|
"Bahrain": "巴林",
|
||||||
|
"Finland": "芬兰",
|
||||||
|
"Fiji": "斐济",
|
||||||
|
"Falkland Islands": "福克兰群岛",
|
||||||
|
"France": "法国",
|
||||||
|
"Gabon": "加蓬共和国",
|
||||||
|
"United Kingdom": "英国",
|
||||||
|
"Georgia": "格鲁吉亚",
|
||||||
|
"Ghana": "加纳",
|
||||||
|
"Guinea": "几内亚",
|
||||||
|
"Gambia": "冈比亚",
|
||||||
|
"Guinea Bissau": "几内亚比绍",
|
||||||
|
"Greece": "希腊",
|
||||||
|
"Greenland": "格陵兰",
|
||||||
|
"Guatemala": "危地马拉",
|
||||||
|
"French Guiana": "法属圭亚那",
|
||||||
|
"Guyana": "圭亚那",
|
||||||
|
"Honduras": "洪都拉斯",
|
||||||
|
"Croatia": "克罗地亚",
|
||||||
|
"Haiti": "海地",
|
||||||
|
"Hungary": "匈牙利共和国",
|
||||||
|
"Indonesia": "印度尼西亚",
|
||||||
|
"India": "印度",
|
||||||
|
"Ireland": "爱尔兰",
|
||||||
|
"Iran": "伊朗",
|
||||||
|
"Iraq": "伊拉克",
|
||||||
|
"Iceland": "冰岛",
|
||||||
|
"Israel": "以色列",
|
||||||
|
"Italy": "意大利",
|
||||||
|
"Jamaica": "牙买加",
|
||||||
|
"Jordan": "约旦",
|
||||||
|
"Japan": "日本",
|
||||||
|
"Kazakhstan": "哈萨克斯坦",
|
||||||
|
"Kenya": "肯尼亚",
|
||||||
|
"Kyrgyzstan": "吉尔吉斯斯坦",
|
||||||
|
"Cambodia": "柬埔寨",
|
||||||
|
"Kosovo": "科索沃",
|
||||||
|
"Kuwait": "科威特",
|
||||||
|
"Laos": "老挝",
|
||||||
|
"Lebanon": "黎巴嫩",
|
||||||
|
"Liberia": "利比里亚共和国",
|
||||||
|
"Libya": "利比亚",
|
||||||
|
"Sri Lanka": "斯里兰卡",
|
||||||
|
"Lesotho": "莱索托",
|
||||||
|
"Lithuania": "立陶宛",
|
||||||
|
"Luxembourg": "卢森堡",
|
||||||
|
"Latvia": "拉脱维亚共和国",
|
||||||
|
"Morocco": "摩洛哥",
|
||||||
|
"Moldova": "摩尔多瓦",
|
||||||
|
"Madagascar": "马达加斯加",
|
||||||
|
"Mexico": "墨西哥",
|
||||||
|
"Macedonia": "马其顿",
|
||||||
|
"Mali": "马里",
|
||||||
|
"Myanmar": "缅甸",
|
||||||
|
"Montenegro": "黑山共和国",
|
||||||
|
"Mongolia": "蒙古",
|
||||||
|
"Mozambique": "莫桑比克",
|
||||||
|
"Mauritania": "毛里塔尼亚",
|
||||||
|
"Malawi": "马拉维",
|
||||||
|
"Malaysia": "马来西亚",
|
||||||
|
"Namibia": "纳米比亚",
|
||||||
|
"New Caledonia": "新喀里多尼亚",
|
||||||
|
"Niger": "尼日尔",
|
||||||
|
"Nigeria": "尼日利亚",
|
||||||
|
"Nicaragua": "尼加拉瓜",
|
||||||
|
"Netherlands": "荷兰",
|
||||||
|
"Norway": "挪威",
|
||||||
|
"Nepal": "尼泊尔",
|
||||||
|
"New Zealand": "新西兰",
|
||||||
|
"Oman": "阿曼",
|
||||||
|
"Pakistan": "巴基斯坦",
|
||||||
|
"Panama": "巴拿马共和国",
|
||||||
|
"Peru": "秘鲁",
|
||||||
|
"Philippines": "菲律宾",
|
||||||
|
"Papua New Guinea": "巴布亚新几内亚",
|
||||||
|
"Poland": "波兰",
|
||||||
|
"Puerto Rico": "波多黎各",
|
||||||
|
"North Korea": "北朝鲜",
|
||||||
|
"Portugal": "葡萄牙",
|
||||||
|
"Paraguay": "巴拉圭",
|
||||||
|
"Qatar": "卡塔尔",
|
||||||
|
"Romania": "罗马尼亚",
|
||||||
|
"Russia": "俄罗斯",
|
||||||
|
"Rwanda": "卢旺达",
|
||||||
|
"Western Sahara": "西撒哈拉",
|
||||||
|
"Saudi Arabia": "沙特阿拉伯",
|
||||||
|
"Sudan": "苏丹",
|
||||||
|
"S. Sudan": "南苏丹",
|
||||||
|
"Senegal": "塞内加尔",
|
||||||
|
"Solomon Islands": "所罗门群岛",
|
||||||
|
"Sierra Leone": "塞拉利昂",
|
||||||
|
"El Salvador": "萨尔瓦多",
|
||||||
|
"Somaliland": "索马里兰",
|
||||||
|
"Somalia": "索马里",
|
||||||
|
"Republic of Serbia": "塞尔维亚",
|
||||||
|
"Suriname": "苏里南",
|
||||||
|
"Slovakia": "斯洛伐克共和国",
|
||||||
|
"Slovenia": "斯洛文尼亚",
|
||||||
|
"Sweden": "瑞典",
|
||||||
|
"Swaziland": "斯威士兰",
|
||||||
|
"Syria": "叙利亚",
|
||||||
|
"Chad": "乍得",
|
||||||
|
"Togo": "多哥",
|
||||||
|
"Thailand": "泰国",
|
||||||
|
"Tajikistan": "塔吉克斯坦共和国",
|
||||||
|
"Turkmenistan": "土库曼斯坦",
|
||||||
|
"East Timor": "东帝汶",
|
||||||
|
"Trinidad and Tobago": "特立尼达和多巴哥",
|
||||||
|
"Tunisia": "突尼斯",
|
||||||
|
"Turkey": "土耳其",
|
||||||
|
"Uganda": "乌干达",
|
||||||
|
"Ukraine": "乌克兰",
|
||||||
|
"Uruguay": "乌拉圭",
|
||||||
|
"United States": "美国",
|
||||||
|
"Uzbekistan": "乌兹别克斯坦",
|
||||||
|
"Venezuela": "委内瑞拉",
|
||||||
|
"Vietnam": "越南",
|
||||||
|
"Vanuatu": "瓦努阿图",
|
||||||
|
"West Bank": "西岸",
|
||||||
|
"Yemen": "也门",
|
||||||
|
"South Africa": "南非",
|
||||||
|
"Zambia": "赞比亚",
|
||||||
|
"Korea": "韩国",
|
||||||
|
"Tanzania": "坦桑尼亚",
|
||||||
|
"Zimbabwe": "津巴布韦",
|
||||||
|
"Congo": "刚果",
|
||||||
|
"Central African Rep.": "中非",
|
||||||
|
"Serbia": "塞尔维亚",
|
||||||
|
"Bosnia and Herz.": "波黑",
|
||||||
|
"Czech Rep.": "捷克",
|
||||||
|
"W. Sahara": "西撒哈拉",
|
||||||
|
"Lao PDR": "老挝",
|
||||||
|
"Dem. Rep. Korea": "朝鲜",
|
||||||
|
"Falkland Is.": "福克兰群岛",
|
||||||
|
"Timor-Leste": "东帝汶",
|
||||||
|
"Solomon Is.": "所罗门群岛",
|
||||||
|
"Palestine": "巴勒斯坦",
|
||||||
|
"N. Cyprus": "北塞浦路斯",
|
||||||
|
"Aland": "奥兰群岛",
|
||||||
|
"Fr. S. Antarctic Lands": "法属南半球和南极陆地",
|
||||||
|
"Mauritius": "毛里求斯",
|
||||||
|
"Comoros": "科摩罗",
|
||||||
|
"Eq. Guinea": "赤道几内亚",
|
||||||
|
"Guinea-Bissau": "几内亚比绍",
|
||||||
|
"Dominican Rep.": "多米尼加共和国",
|
||||||
|
"Saint Lucia": "圣卢西亚",
|
||||||
|
"Dominica": "多米尼克",
|
||||||
|
"Antigua and Barb.": "安提瓜和巴布达",
|
||||||
|
"U.S. Virgin Is.": "美国原始岛屿",
|
||||||
|
"Montserrat": "蒙塞拉特",
|
||||||
|
"Grenada": "格林纳达",
|
||||||
|
"Barbados": "巴巴多斯",
|
||||||
|
"Samoa": "萨摩亚独立国",
|
||||||
|
"Bahamas": "巴哈马",
|
||||||
|
"Cayman Is.": "开曼群岛",
|
||||||
|
"Faeroe Is.": "法罗群岛",
|
||||||
|
"IsIe of Man": "马恩岛",
|
||||||
|
"Malta": "马耳他共和国",
|
||||||
|
"Jersey": "泽西",
|
||||||
|
"Cape Verde": "佛得角共和国",
|
||||||
|
"Turks and Caicos Is.": "特克斯和凯科斯群岛",
|
||||||
|
"St. Vin. and Gren.": "圣文森特和格林纳丁斯"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -54,7 +54,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
<div class="btn-box screen-box" @click="clearCachefn">
|
||||||
|
<div class="btn">
|
||||||
|
<i class="iconfont-sys"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 全屏按钮 -->
|
<!-- 全屏按钮 -->
|
||||||
<div class="btn-box screen-box" @click="toggleFullScreen">
|
<div class="btn-box screen-box" @click="toggleFullScreen">
|
||||||
<div
|
<div
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<!-- 语言 -->
|
<!-- 语言 -->
|
||||||
<div class="btn-box" v-if="showLanguage">
|
<!-- <div class="btn-box" v-if="showLanguage">
|
||||||
<el-dropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
<el-dropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
||||||
<div class="btn language-btn">
|
<div class="btn language-btn">
|
||||||
<i class="iconfont-sys"></i>
|
<i class="iconfont-sys"></i>
|
||||||
@@ -98,7 +102,7 @@
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div> -->
|
||||||
<!-- 设置 -->
|
<!-- 设置 -->
|
||||||
<!-- <div class="btn-box" @click="openSetting">
|
<!-- <div class="btn-box" @click="openSetting">
|
||||||
<el-popover :visible="showSettingGuide" placement="bottom-start" :width="190" :offset="0">
|
<el-popover :visible="showSettingGuide" placement="bottom-start" :width="190" :offset="0">
|
||||||
@@ -138,7 +142,10 @@
|
|||||||
popper-style="border: 1px solid var(--art-border-dashed-color); border-radius: calc(var(--custom-radius) / 2 + 4px); padding: 5px 16px; 5px 16px;"
|
popper-style="border: 1px solid var(--art-border-dashed-color); border-radius: calc(var(--custom-radius) / 2 + 4px); padding: 5px 16px; 5px 16px;"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<img class="cover" src="@imgs/user/avatar.webp" />
|
<img
|
||||||
|
class="cover"
|
||||||
|
:src="getUserInfo.user.avatar ? getUserInfo.user.avatar : '@imgs/user/avatar.webp'"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="user-menu-box">
|
<div class="user-menu-box">
|
||||||
@@ -155,15 +162,19 @@
|
|||||||
<span class="email">{{ userInfo.user.email }}</span>
|
<span class="email">{{ userInfo.user.email }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<ul class="user-menu">
|
<ul class="user-menu" style="border: none">
|
||||||
<li @click="goPage('/system/user-center')">
|
<li @click="gouserInfo()">
|
||||||
<i class="menu-icon iconfont-sys"></i>
|
<i class="menu-icon iconfont-sys"></i>
|
||||||
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
|
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li @click="goPage('/crmpc/recycle')">
|
<li @click="goPage('/work/myOrder')">
|
||||||
|
<i class="menu-icon iconfont-sys"></i>
|
||||||
|
<span class="menu-txt">我的工单</span>
|
||||||
|
</li>
|
||||||
|
<!-- <li @click="goPage('/crmpc/recycle')">
|
||||||
<i class="menu-icon iconfont-sys"></i>
|
<i class="menu-icon iconfont-sys"></i>
|
||||||
<span class="menu-txt">回收站</span>
|
<span class="menu-txt">回收站</span>
|
||||||
</li>
|
</li> -->
|
||||||
<!-- <li @click="goPage('/system/user-center')">
|
<!-- <li @click="goPage('/system/user-center')">
|
||||||
<i class="menu-icon iconfont-sys"></i>
|
<i class="menu-icon iconfont-sys"></i>
|
||||||
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
|
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
|
||||||
@@ -176,10 +187,10 @@
|
|||||||
<i class="menu-icon iconfont-sys"></i>
|
<i class="menu-icon iconfont-sys"></i>
|
||||||
<span class="menu-txt">{{ $t('topBar.user.github') }}</span>
|
<span class="menu-txt">{{ $t('topBar.user.github') }}</span>
|
||||||
</li> -->
|
</li> -->
|
||||||
<li @click="lockScreen()">
|
<!-- <li @click="lockScreen()">
|
||||||
<i class="menu-icon iconfont-sys"></i>
|
<i class="menu-icon iconfont-sys"></i>
|
||||||
<span class="menu-txt">{{ $t('topBar.user.lockScreen') }}</span>
|
<span class="menu-txt">{{ $t('topBar.user.lockScreen') }}</span>
|
||||||
</li>
|
</li> -->
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
<div class="logout-btn" @click="loginOut">
|
<div class="logout-btn" @click="loginOut">
|
||||||
{{ $t('topBar.user.logout') }}
|
{{ $t('topBar.user.logout') }}
|
||||||
@@ -208,14 +219,14 @@
|
|||||||
import { mittBus } from '@/utils/sys'
|
import { mittBus } from '@/utils/sys'
|
||||||
import { useMenuStore } from '@/store/modules/menu'
|
import { useMenuStore } from '@/store/modules/menu'
|
||||||
import AppConfig from '@/config'
|
import AppConfig from '@/config'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
import { languageOptions } from '@/locales'
|
import { languageOptions } from '@/locales'
|
||||||
const isWindows = navigator.userAgent.includes('Windows')
|
const isWindows = navigator.userAgent.includes('Windows')
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { getUserInfo, home_page } = useUserStore()
|
||||||
const {
|
const {
|
||||||
showMenuButton,
|
showMenuButton,
|
||||||
showRefreshButton,
|
showRefreshButton,
|
||||||
@@ -229,7 +240,7 @@
|
|||||||
tabStyle
|
tabStyle
|
||||||
} = storeToRefs(settingStore)
|
} = storeToRefs(settingStore)
|
||||||
|
|
||||||
const { language, getUserInfo: userInfo } = storeToRefs(userStore)
|
const { language } = storeToRefs(userStore)
|
||||||
|
|
||||||
const { menuList } = storeToRefs(useMenuStore())
|
const { menuList } = storeToRefs(useMenuStore())
|
||||||
|
|
||||||
@@ -268,7 +279,14 @@
|
|||||||
const toggleFullScreen = () => {
|
const toggleFullScreen = () => {
|
||||||
toggleFullscreen()
|
toggleFullscreen()
|
||||||
}
|
}
|
||||||
|
const clearCachefn = () => {
|
||||||
|
manageService.clearCache().then((res) => {
|
||||||
|
ElMessage.success(res.data?.message || '成功!')
|
||||||
|
// setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
// }, 5000)
|
||||||
|
})
|
||||||
|
}
|
||||||
const topBarWidth = (): string => {
|
const topBarWidth = (): string => {
|
||||||
const { TOP, DUAL_MENU, TOP_LEFT } = MenuTypeEnum
|
const { TOP, DUAL_MENU, TOP_LEFT } = MenuTypeEnum
|
||||||
const { getMenuOpenWidth } = settingStore
|
const { getMenuOpenWidth } = settingStore
|
||||||
@@ -296,17 +314,25 @@
|
|||||||
const goPage = (path: string) => {
|
const goPage = (path: string) => {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
const { systemsid } = useUserStore()
|
||||||
const toDocs = () => {
|
const gouserInfo = () => {
|
||||||
window.open(WEB_LINKS.DOCS)
|
if (systemsid == '1') {
|
||||||
|
router.push('/admin/user/user_info')
|
||||||
|
} else {
|
||||||
|
router.push('/system/user-center')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toGithub = () => {
|
// const toDocs = () => {
|
||||||
window.open(WEB_LINKS.GITHUB)
|
// window.open(WEB_LINKS.DOCS)
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// const toGithub = () => {
|
||||||
|
// window.open(WEB_LINKS.GITHUB)
|
||||||
|
// }
|
||||||
|
|
||||||
const toHome = () => {
|
const toHome = () => {
|
||||||
router.push(HOME_PAGE)
|
router.push(home_page)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginOut = () => {
|
const loginOut = () => {
|
||||||
|
|||||||
@@ -112,11 +112,11 @@
|
|||||||
import { isIframe } from '@/utils/navigation'
|
import { isIframe } from '@/utils/navigation'
|
||||||
import { handleMenuJump } from '@/utils/navigation'
|
import { handleMenuJump } from '@/utils/navigation'
|
||||||
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
|
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const { home_page } = useUserStore()
|
||||||
const { getMenuOpenWidth, menuType, uniqueOpened, dualMenuShowText, menuOpen, getMenuTheme } =
|
const { getMenuOpenWidth, menuType, uniqueOpened, dualMenuShowText, menuOpen, getMenuTheme } =
|
||||||
storeToRefs(settingStore)
|
storeToRefs(settingStore)
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const toHome = () => {
|
const toHome = () => {
|
||||||
router.push(HOME_PAGE)
|
router.push(home_page)
|
||||||
}
|
}
|
||||||
|
|
||||||
let screenWidth = 0
|
let screenWidth = 0
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<RouterView
|
<RouterView
|
||||||
v-if="isRefresh"
|
v-if="isRefresh"
|
||||||
|
:key="$route.fullPath"
|
||||||
v-slot="{ Component, route }"
|
v-slot="{ Component, route }"
|
||||||
:style="{ minHeight: containerMinHeight }"
|
:style="{ minHeight: containerMinHeight }"
|
||||||
>
|
>
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
import { useWorktabStore } from '@/store/modules/worktab'
|
import { useWorktabStore } from '@/store/modules/worktab'
|
||||||
import { useSettingStore } from '@/store/modules/setting'
|
import { useSettingStore } from '@/store/modules/setting'
|
||||||
|
|
||||||
// Store refs
|
// Store refs1
|
||||||
const { pageTransition, containerWidth, refresh } = storeToRefs(useSettingStore())
|
const { pageTransition, containerWidth, refresh } = storeToRefs(useSettingStore())
|
||||||
const { keepAliveExclude } = storeToRefs(useWorktabStore())
|
const { keepAliveExclude } = storeToRefs(useWorktabStore())
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<div v-if="!isLock">
|
<div v-if="!isLock">
|
||||||
<el-dialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
|
<el-dialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
|
||||||
<div class="lock-content">
|
<div class="lock-content">
|
||||||
<img class="cover" src="@imgs/user/avatar.webp" />
|
<img
|
||||||
|
class="cover"
|
||||||
|
:src="getUserInfo.user.avatar ? getUserInfo.user.avatar : '@imgs/user/avatar.webp'"
|
||||||
|
/>
|
||||||
<div class="username">{{ userInfo.userName }}</div>
|
<div class="username">{{ userInfo.userName }}</div>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" @submit.prevent="handleLock">
|
<el-form ref="formRef" :model="formData" :rules="rules" @submit.prevent="handleLock">
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
@@ -32,7 +35,10 @@
|
|||||||
|
|
||||||
<div class="unlock-content" v-else>
|
<div class="unlock-content" v-else>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<img class="cover" src="@imgs/user/avatar.webp" />
|
<img
|
||||||
|
class="cover"
|
||||||
|
:src="getUserInfo.user.avatar ? getUserInfo.user.avatar : '@imgs/user/avatar.webp'"
|
||||||
|
/>
|
||||||
<div class="username">{{ userInfo.userName }}</div>
|
<div class="username">{{ userInfo.userName }}</div>
|
||||||
<el-form
|
<el-form
|
||||||
ref="unlockFormRef"
|
ref="unlockFormRef"
|
||||||
@@ -77,7 +83,7 @@
|
|||||||
import { mittBus } from '@/utils/sys'
|
import { mittBus } from '@/utils/sys'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { getUserInfo } = useUserStore()
|
||||||
const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY
|
const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore)
|
const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore)
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { HOME_PAGE } from '@/router/routesAlias'
|
// import { HOME_PAGE } from '@/router/routesAlias'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
const { home_page } = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
interface ExceptionData {
|
interface ExceptionData {
|
||||||
/** 标题 */
|
/** 标题 */
|
||||||
title: string
|
title: string
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const backHome = () => {
|
const backHome = () => {
|
||||||
router.push(HOME_PAGE)
|
router.push(home_page)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { RoutesAlias } from '../routesAlias'
|
import { RoutesAlias } from '../routesAlias'
|
||||||
import { AppRouteRecord } from '@/types/router'
|
import { AppRouteRecord } from '@/types/router'
|
||||||
import { WEB_LINKS } from '@/utils/constants'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 菜单列表、异步路由
|
* 菜单列表、异步路由
|
||||||
*
|
*
|
||||||
@@ -13,13 +11,21 @@ import { WEB_LINKS } from '@/utils/constants'
|
|||||||
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
|
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
|
||||||
*/
|
*/
|
||||||
export const asyncRoutes: AppRouteRecord[] = [
|
export const asyncRoutes: AppRouteRecord[] = [
|
||||||
|
{
|
||||||
|
path: '/admin/inquiry_system',
|
||||||
|
name: 'Inquiry_system',
|
||||||
|
component: RoutesAlias.inquirySystem,
|
||||||
|
meta: {
|
||||||
|
title: '询单系统'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/crmpc/customer/add',
|
path: '/crmpc/customer/add',
|
||||||
name: 'CrmpcustomerAdd',
|
name: 'CrmpcustomerAdd',
|
||||||
component: RoutesAlias.CrmpcustomerAdd,
|
component: RoutesAlias.CrmpcustomerAdd,
|
||||||
meta: {
|
meta: {
|
||||||
title: '新增客户资料',
|
title: '新增客户资料',
|
||||||
isHide: true,
|
isHide: true
|
||||||
// isHideTab: true
|
// isHideTab: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -29,7 +35,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
component: RoutesAlias.CrmpcustomerShow,
|
component: RoutesAlias.CrmpcustomerShow,
|
||||||
meta: {
|
meta: {
|
||||||
title: '客户资料',
|
title: '客户资料',
|
||||||
isHide: true,
|
isHide: true
|
||||||
// isHideTab: true
|
// isHideTab: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -39,7 +45,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
component: RoutesAlias.CrmpcustomerEdit,
|
component: RoutesAlias.CrmpcustomerEdit,
|
||||||
meta: {
|
meta: {
|
||||||
title: '编辑客户资料',
|
title: '编辑客户资料',
|
||||||
isHide: true,
|
isHide: true
|
||||||
// isHideTab: true
|
// isHideTab: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -49,7 +55,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
component: RoutesAlias.CrmpcustomerFollowadd,
|
component: RoutesAlias.CrmpcustomerFollowadd,
|
||||||
meta: {
|
meta: {
|
||||||
title: '添加跟进',
|
title: '添加跟进',
|
||||||
isHide: true,
|
isHide: true
|
||||||
// isHideTab: true
|
// isHideTab: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,5 +67,5 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
title: '回收站',
|
title: '回收站',
|
||||||
isHide: true
|
isHide: true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export enum RoutesAlias {
|
|||||||
CrmpcustomerFollowadd = '/crmpc/customer/Followadd', //添加跟进
|
CrmpcustomerFollowadd = '/crmpc/customer/Followadd', //添加跟进
|
||||||
recycle = '/crmpc/recycle', //回收站
|
recycle = '/crmpc/recycle', //回收站
|
||||||
UserCenter = '/system/user-center', // 用户中心
|
UserCenter = '/system/user-center', // 用户中心
|
||||||
|
inquirySystem = '/admin/inquiry_system', // 询单系统
|
||||||
|
customer = '/admin/customer', // 管理端客户列表
|
||||||
|
customeraudit = '/admin/customer/customeraudit' // 管理端待审客户列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主页路由
|
// 主页路由
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export const useUserStore = defineStore(
|
|||||||
const refreshToken = ref('')
|
const refreshToken = ref('')
|
||||||
const companyid = ref('')
|
const companyid = ref('')
|
||||||
const companyabbr = ref('')
|
const companyabbr = ref('')
|
||||||
|
const systemsid = ref('')
|
||||||
|
const systemsabbr = ref('')
|
||||||
|
const home_page = ref('')
|
||||||
const loginType = ref<'feishu' | 'upwd'>('upwd')
|
const loginType = ref<'feishu' | 'upwd'>('upwd')
|
||||||
const tokenExpireAt = ref<number>(0)
|
const tokenExpireAt = ref<number>(0)
|
||||||
|
|
||||||
@@ -65,17 +68,28 @@ export const useUserStore = defineStore(
|
|||||||
refreshToken.value = newRefreshToken
|
refreshToken.value = newRefreshToken
|
||||||
}
|
}
|
||||||
if (expiresIn) {
|
if (expiresIn) {
|
||||||
// 记录绝对过期时间
|
|
||||||
const expireAt = Date.now() + expiresIn * 1000
|
const expireAt = Date.now() + expiresIn * 1000
|
||||||
tokenExpireAt.value = expireAt
|
tokenExpireAt.value = expireAt
|
||||||
localStorage.setItem('token_expire_at', String(expireAt))
|
localStorage.setItem('token_expire_at', String(expireAt))
|
||||||
scheduleTokenRefresh(expiresIn)
|
// 检查是否需要重新设置定时器
|
||||||
|
const currentLeftSeconds = Math.floor((expireAt - Date.now()) / 1000)
|
||||||
|
if (currentLeftSeconds > 0) {
|
||||||
|
scheduleTokenRefresh(currentLeftSeconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const setCompany = (id: string, abbr: string) => {
|
const setCompany = (id: string, abbr: string) => {
|
||||||
companyid.value = id
|
companyid.value = id
|
||||||
companyabbr.value = abbr
|
companyabbr.value = abbr
|
||||||
}
|
}
|
||||||
|
const setSystems = (id: string, abbr: string) => {
|
||||||
|
systemsid.value = id
|
||||||
|
systemsabbr.value = abbr
|
||||||
|
}
|
||||||
|
// 设置首页
|
||||||
|
const sethome_page = (page: string) => {
|
||||||
|
home_page.value = page
|
||||||
|
}
|
||||||
function restoreTokenTimer() {
|
function restoreTokenTimer() {
|
||||||
const expireAt = Number(localStorage.getItem('token_expire_at') || '0')
|
const expireAt = Number(localStorage.getItem('token_expire_at') || '0')
|
||||||
if (accessToken.value && expireAt > Date.now()) {
|
if (accessToken.value && expireAt > Date.now()) {
|
||||||
@@ -84,31 +98,56 @@ export const useUserStore = defineStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isScheduled = false
|
||||||
|
|
||||||
function scheduleTokenRefresh(expiresIn: number) {
|
function scheduleTokenRefresh(expiresIn: number) {
|
||||||
if (refreshTimeout) clearTimeout(refreshTimeout)
|
if (refreshTimeout) {
|
||||||
|
clearTimeout(refreshTimeout)
|
||||||
|
refreshTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
// 提前1分钟刷新
|
// 提前1分钟刷新
|
||||||
const refreshTime = (expiresIn - 100) * 1000
|
const refreshTime = (expiresIn - 60) * 1000
|
||||||
|
|
||||||
if (refreshTime > 0) {
|
if (refreshTime > 0) {
|
||||||
refreshTimeout = setTimeout(refreshTokenAction, refreshTime)
|
if (!isScheduled) {
|
||||||
|
isScheduled = true
|
||||||
|
refreshTimeout = setTimeout(() => {
|
||||||
|
refreshTokenAction().finally(() => {
|
||||||
|
isScheduled = false // 重置标志位
|
||||||
|
})
|
||||||
|
}, refreshTime)
|
||||||
|
} else {
|
||||||
|
console.log('scheduleTokenRefresh is already scheduled, skipping...')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Invalid refreshTime:', refreshTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTokenAction() {
|
async function refreshTokenAction() {
|
||||||
try {
|
try {
|
||||||
const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value })
|
const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value })
|
||||||
setToken(data.access_token, data.refresh_token, data.expires_in)
|
// 动态计算剩余时间
|
||||||
|
const currentTime = Date.now()
|
||||||
|
const expireAt = currentTime + data.expires_in * 1000
|
||||||
|
const leftSeconds = Math.floor((expireAt - currentTime) / 1000)
|
||||||
|
setToken(data.access_token, data.refresh_token, leftSeconds)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('Token refresh failed:', e)
|
||||||
logOut()
|
logOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logOut = () => {
|
const logOut = () => {
|
||||||
// 清空所有本地缓存
|
// 清空所有本地缓存
|
||||||
localStorage.clear();
|
localStorage.clear()
|
||||||
sessionStorage.clear();
|
sessionStorage.clear()
|
||||||
|
systemsabbr.value = ''
|
||||||
|
systemsid.value = ''
|
||||||
companyabbr.value = ''
|
companyabbr.value = ''
|
||||||
companyid.value = ''
|
companyid.value = ''
|
||||||
|
home_page.value = ''
|
||||||
info.value = {}
|
info.value = {}
|
||||||
isLogin.value = false
|
isLogin.value = false
|
||||||
isLock.value = false
|
isLock.value = false
|
||||||
@@ -116,17 +155,13 @@ export const useUserStore = defineStore(
|
|||||||
accessToken.value = ''
|
accessToken.value = ''
|
||||||
refreshToken.value = ''
|
refreshToken.value = ''
|
||||||
if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器
|
if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器
|
||||||
|
|
||||||
// 清理工作台标签页
|
// 清理工作台标签页
|
||||||
useWorktabStore().clearAll()
|
useWorktabStore().clearAll()
|
||||||
|
|
||||||
// 清理sessionStorage中的用户信息缓存
|
// 清理sessionStorage中的用户信息缓存
|
||||||
sessionStorage.removeItem('lastUserInfo')
|
sessionStorage.removeItem('lastUserInfo')
|
||||||
sessionStorage.removeItem('iframeRoutes')
|
sessionStorage.removeItem('iframeRoutes')
|
||||||
|
|
||||||
// 重置路由状态
|
// 重置路由状态
|
||||||
resetRouterState(router)
|
resetRouterState(router)
|
||||||
|
|
||||||
// 跳转到登录页
|
// 跳转到登录页
|
||||||
router.push(RoutesAlias.Login)
|
router.push(RoutesAlias.Login)
|
||||||
}
|
}
|
||||||
@@ -139,6 +174,9 @@ export const useUserStore = defineStore(
|
|||||||
info,
|
info,
|
||||||
companyid,
|
companyid,
|
||||||
companyabbr,
|
companyabbr,
|
||||||
|
systemsid,
|
||||||
|
systemsabbr,
|
||||||
|
home_page,
|
||||||
searchHistory,
|
searchHistory,
|
||||||
loginType,
|
loginType,
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -155,7 +193,9 @@ export const useUserStore = defineStore(
|
|||||||
setLockPassword,
|
setLockPassword,
|
||||||
setToken,
|
setToken,
|
||||||
setCompany,
|
setCompany,
|
||||||
|
setSystems,
|
||||||
setLoginType,
|
setLoginType,
|
||||||
|
sethome_page,
|
||||||
logOut,
|
logOut,
|
||||||
restoreTokenTimer
|
restoreTokenTimer
|
||||||
}
|
}
|
||||||
@@ -163,7 +203,10 @@ export const useUserStore = defineStore(
|
|||||||
{
|
{
|
||||||
persist: {
|
persist: {
|
||||||
key: 'user',
|
key: 'user',
|
||||||
storage: localStorage
|
storage: localStorage,
|
||||||
|
afterRestore: (ctx: { store: { restoreTokenTimer: () => void } }) => {
|
||||||
|
ctx.store.restoreTokenTimer()
|
||||||
}
|
}
|
||||||
|
} as any
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { HOME_PAGE } from '@/router/routesAlias'
|
|||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
import { LocationQueryRaw, Router } from 'vue-router'
|
import { LocationQueryRaw, Router } from 'vue-router'
|
||||||
import { WorkTab } from '@/types'
|
import { WorkTab } from '@/types'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
interface WorktabState {
|
interface WorktabState {
|
||||||
current: Partial<WorkTab>
|
current: Partial<WorkTab>
|
||||||
opened: WorkTab[]
|
opened: WorkTab[]
|
||||||
@@ -144,6 +144,7 @@ export const useWorktabStore = defineStore(
|
|||||||
* 关闭指定的选项卡
|
* 关闭指定的选项卡
|
||||||
*/
|
*/
|
||||||
const removeTab = (path: string): void => {
|
const removeTab = (path: string): void => {
|
||||||
|
const { home_page } = useUserStore()
|
||||||
const targetTab = getTab(path)
|
const targetTab = getTab(path)
|
||||||
const targetIndex = findTabIndex(path)
|
const targetIndex = findTabIndex(path)
|
||||||
|
|
||||||
@@ -167,9 +168,9 @@ export const useWorktabStore = defineStore(
|
|||||||
|
|
||||||
// 如果关闭后无标签页,跳转首页
|
// 如果关闭后无标签页,跳转首页
|
||||||
if (!hasOpenedTabs.value) {
|
if (!hasOpenedTabs.value) {
|
||||||
if (path !== HOME_PAGE) {
|
if (path !== home_page) {
|
||||||
current.value = {}
|
current.value = {}
|
||||||
safeRouterPush({ path: HOME_PAGE })
|
safeRouterPush({ path: home_page })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -287,12 +288,12 @@ export const useWorktabStore = defineStore(
|
|||||||
*/
|
*/
|
||||||
const removeAll = (): void => {
|
const removeAll = (): void => {
|
||||||
const hasFixedTabs = opened.value.some((tab) => tab.fixedTab)
|
const hasFixedTabs = opened.value.some((tab) => tab.fixedTab)
|
||||||
|
const { home_page } = useUserStore()
|
||||||
// 获取可关闭的标签页
|
// 获取可关闭的标签页
|
||||||
const closableTabs = opened.value.filter((tab) => {
|
const closableTabs = opened.value.filter((tab) => {
|
||||||
if (!isTabClosable(tab)) return false
|
if (!isTabClosable(tab)) return false
|
||||||
// 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页
|
// 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页
|
||||||
return hasFixedTabs || tab.path !== HOME_PAGE
|
return hasFixedTabs || tab.path !== home_page
|
||||||
})
|
})
|
||||||
|
|
||||||
if (closableTabs.length === 0) {
|
if (closableTabs.length === 0) {
|
||||||
@@ -305,18 +306,18 @@ export const useWorktabStore = defineStore(
|
|||||||
|
|
||||||
// 保留不可关闭的标签页和首页(当没有固定标签时)
|
// 保留不可关闭的标签页和首页(当没有固定标签时)
|
||||||
opened.value = opened.value.filter((tab) => {
|
opened.value = opened.value.filter((tab) => {
|
||||||
return !isTabClosable(tab) || (!hasFixedTabs && tab.path === HOME_PAGE)
|
return !isTabClosable(tab) || (!hasFixedTabs && tab.path === home_page)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理激活状态
|
// 处理激活状态
|
||||||
if (!hasOpenedTabs.value) {
|
if (!hasOpenedTabs.value) {
|
||||||
current.value = {}
|
current.value = {}
|
||||||
safeRouterPush({ path: HOME_PAGE })
|
safeRouterPush({ path: home_page })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择激活的标签页:优先首页,其次第一个可用标签
|
// 选择激活的标签页:优先首页,其次第一个可用标签
|
||||||
const homeTab = opened.value.find((tab) => tab.path === HOME_PAGE)
|
const homeTab = opened.value.find((tab) => tab.path === home_page)
|
||||||
const targetTab = homeTab || opened.value[0]
|
const targetTab = homeTab || opened.value[0]
|
||||||
|
|
||||||
current.value = targetTab
|
current.value = targetTab
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
|||||||
authList?: Array<{
|
authList?: Array<{
|
||||||
title: string
|
title: string
|
||||||
auth_mark: string
|
auth_mark: string
|
||||||
|
authMark?: string // 兼容旧版
|
||||||
}>
|
}>
|
||||||
/** 是否为一级菜单 */
|
/** 是否为一级菜单 */
|
||||||
isFirstLevel?: boolean
|
isFirstLevel?: boolean
|
||||||
|
|||||||
@@ -29,6 +29,32 @@ declare namespace Api {
|
|||||||
|
|
||||||
/** 启用状态 */
|
/** 启用状态 */
|
||||||
type EnableStatus = '1' | '2'
|
type EnableStatus = '1' | '2'
|
||||||
|
|
||||||
|
interface CommonInfo {
|
||||||
|
data: never[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表数据 */
|
||||||
|
interface CommonListData {
|
||||||
|
records: CommonInfo[]
|
||||||
|
current: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
interface CommonListDatad {
|
||||||
|
data: CommonInfo[]
|
||||||
|
current: number
|
||||||
|
size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
interface AddResponse {
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
interface CommonListDatad1 {
|
||||||
|
data: never[]
|
||||||
|
message: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 认证类型 */
|
/** 认证类型 */
|
||||||
@@ -165,6 +191,8 @@ declare namespace Api {
|
|||||||
|
|
||||||
/** 列表数据 */
|
/** 列表数据 */
|
||||||
interface CrmListData {
|
interface CrmListData {
|
||||||
|
country_id: any
|
||||||
|
channel_id: any
|
||||||
data: CrmInfo[]
|
data: CrmInfo[]
|
||||||
current: number
|
current: number
|
||||||
size: number
|
size: number
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ axiosInstance.interceptors.request.use(
|
|||||||
const { language } = useUserStore()
|
const { language } = useUserStore()
|
||||||
const { loginType } = useUserStore()
|
const { loginType } = useUserStore()
|
||||||
const { companyabbr } = useUserStore()
|
const { companyabbr } = useUserStore()
|
||||||
|
const { systemsabbr } = useUserStore()
|
||||||
request.headers.set('Fdesk-Language', language)
|
request.headers.set('Fdesk-Language', language)
|
||||||
request.headers.set('Fdesk-Corp-Abbr', companyabbr)
|
request.headers.set('Fdesk-Corp-Abbr', companyabbr)
|
||||||
request.headers.set('Fdesk-System-Code', 'fdesk')
|
request.headers.set('Fdesk-System-Code', systemsabbr)
|
||||||
request.headers.set('Fdesk-Login-Type', loginType)
|
request.headers.set('Fdesk-Login-Type', loginType)
|
||||||
// 设置 token 和 请求头
|
// 设置 token 和 请求头
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column prop="code" label="编号" align="center" />
|
||||||
|
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||||
|
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||||
|
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||||
|
<el-table-column prop="country_name" label="国家" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
|
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||||
|
<el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.follow)">
|
||||||
|
{{ scope.row.follow_txt }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="user_name" label="标记人"></el-table-column>
|
||||||
|
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onDetail(scope.row)"
|
||||||
|
v-if="hasAuth('crm_customer@show')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>详情
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="goaudit(scope.row)"
|
||||||
|
v-if="hasAuth('customer_audit@audet')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i> 审核
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情">
|
||||||
|
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog v-model="audetDialogVisible" title="审核" width="360px">
|
||||||
|
<el-form :model="audetForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select v-model="audetForm.scene" filterable placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="彻底删除" value="delete" />
|
||||||
|
<el-option label="恢复至公海" value="report" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="audetDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleAudetSubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const audetDialogVisible = ref(false)
|
||||||
|
const detailRow = ref<any>(null)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const audetForm = reactive({
|
||||||
|
id: '',
|
||||||
|
scene: ''
|
||||||
|
})
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
onMounted(() => {
|
||||||
|
getuserList()
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
watch([keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
scene: 'audit'
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||||
|
tableData.value = data.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
function statusClass(status: any) {
|
||||||
|
if (status === 0) return 'status-following'
|
||||||
|
if (status === 1) return 'status-dealed'
|
||||||
|
if (status === 2) return 'status-lost'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
//审核
|
||||||
|
const goaudit = async (row: any) => {
|
||||||
|
audetForm.id = row.id
|
||||||
|
audetDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const handleAudetSubmit = async () => {
|
||||||
|
const res = await manageService.crmCustomerAudit(audetForm)
|
||||||
|
ElMessage.success(res?.message || '设置成功!')
|
||||||
|
audetDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
function onDetail(row: any) {
|
||||||
|
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||||
|
detailRow.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 450px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tags {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
width: 120px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-following {
|
||||||
|
color: #00a43f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dealed {
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-table {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.tag-items {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
.tag-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.tag-itemall {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.tag-active {
|
||||||
|
background: #3471ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tag-active :deep(.el-badge__content) {
|
||||||
|
color: #3471ff !important; /* 文字颜色 */
|
||||||
|
background-color: #fff !important; /* 背景色 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="plzyDialogVisible = true"
|
||||||
|
v-if="hasAuth('highseas@allocation')"
|
||||||
|
:disabled="selectedRows.length === 0"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i> 批量分配
|
||||||
|
</el-button>
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="code" label="编号" align="center" />
|
||||||
|
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||||
|
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||||
|
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||||
|
<el-table-column prop="country_name" label="国家" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
|
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||||
|
<el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.follow)">
|
||||||
|
{{ scope.row.follow_txt }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||||
|
<!-- <el-table-column prop="follow_next" label="下次跟进"></el-table-column> -->
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onDetail(scope.row)"
|
||||||
|
v-if="hasAuth('highseas@detail')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>详情
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="gotransfer(scope.row)"
|
||||||
|
v-if="hasAuth('highseas@allocation')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>分配
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="warning"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="gofollowdetail(scope.row)"
|
||||||
|
v-if="hasAuth('highseas@follow_detail')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>跟进记录
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="danger"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onDelete(scope.row)"
|
||||||
|
v-if="hasAuth('highseas@delete')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i>删除
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||||
|
<div>电话:{{ detailRow.phone }}</div>
|
||||||
|
<div>国家:{{ detailRow.country_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>渠道:{{ detailRow.channel_name }}</div>
|
||||||
|
<div>邮箱:{{ detailRow.email }}</div>
|
||||||
|
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>发布人:{{ detailRow.enter_user_name }}</div>
|
||||||
|
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||||
|
<div>
|
||||||
|
等级:
|
||||||
|
<span>
|
||||||
|
{{ detailRow.magnitude_txt }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
状态:<span>{{ detailRow.grade_txt }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
跟进状态:<span>{{ detailRow.follow_txt }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dtitle">详情内容</div>
|
||||||
|
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 分配跟单对话框 -->
|
||||||
|
<el-dialog v-model="zyDialogVisible" title="分配" width="360px">
|
||||||
|
<el-form :model="zyForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select
|
||||||
|
v-model="zyForm.user_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择分配人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="zyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handlezySubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 批量分配对话框 -->
|
||||||
|
<el-dialog v-model="plzyDialogVisible" title="批量分配" width="360px">
|
||||||
|
<el-form :model="plzyForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select
|
||||||
|
v-model="plzyForm.user_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择分配人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="plzyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 跟进记录对话框 -->
|
||||||
|
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
|
||||||
|
<!-- 跟进记录列表 -->
|
||||||
|
<div class="follow-record-list">
|
||||||
|
<ElScrollbar>
|
||||||
|
<template v-if="followRecords.length > 0">
|
||||||
|
<ElTimeline>
|
||||||
|
<ElTimelineItem
|
||||||
|
v-for="item in followRecords"
|
||||||
|
:key="item.time"
|
||||||
|
:placement="TIMELINE_PLACEMENT"
|
||||||
|
:color="activecolor"
|
||||||
|
:center="true"
|
||||||
|
>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-content-row">
|
||||||
|
<div class="timeline-content">
|
||||||
|
<p class="timeline-text"
|
||||||
|
>{{ item.type_txt }}
|
||||||
|
<span class="timeline-mom">{{ item.created_at }}</span></p
|
||||||
|
>
|
||||||
|
<p class="timeline-mom"
|
||||||
|
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
|
||||||
|
{{ item.audio_text }}</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
v-if="item.picture_full"
|
||||||
|
:src="item.picture_full"
|
||||||
|
class="timeline-img"
|
||||||
|
alt="图片"
|
||||||
|
@click="handleImageClick(item.picture_full)"
|
||||||
|
style="cursor: pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElTimelineItem>
|
||||||
|
</ElTimeline>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- 使用 Element Plus 的空状态组件 -->
|
||||||
|
<el-empty :image="emptyimg" description="暂无跟进记录" />
|
||||||
|
</template>
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 图片预览对话框 -->
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="imageViewerVisible"
|
||||||
|
:url-list="imageList"
|
||||||
|
:initial-index="imageViewerIndex"
|
||||||
|
@close="closeImageViewer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import emptyimg from '@/assets/img/crm/empty.png'
|
||||||
|
const TIMELINE_PLACEMENT = 'top'
|
||||||
|
const activecolor = '#3471ff'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const zyDialogVisible = ref(false)
|
||||||
|
const plzyDialogVisible = ref(false)
|
||||||
|
const followRecordDialogVisible = ref(false)
|
||||||
|
const detailRow = ref<any>(null)
|
||||||
|
const currentCustomer = ref<any>(null)
|
||||||
|
const followRecords = ref<any[]>([])
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
// 图片预览相关
|
||||||
|
const imageViewerVisible = ref(false)
|
||||||
|
const imageList = ref<string[]>([])
|
||||||
|
const imageViewerIndex = ref(0)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const plzyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
const zyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
onMounted(() => {
|
||||||
|
getuserList()
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
watch([keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
scene: 'seas'
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||||
|
tableData.value = data.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
function statusClass(status: any) {
|
||||||
|
if (status === 0) return 'status-following'
|
||||||
|
if (status === 1) return 'status-dealed'
|
||||||
|
if (status === 2) return 'status-lost'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
function onDetail(row: any) {
|
||||||
|
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||||
|
detailRow.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const gotransfer = (row: any) => {
|
||||||
|
zyForm.customer_id = row.id
|
||||||
|
zyDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跟进记录
|
||||||
|
const gofollowdetail = async (row: any) => {
|
||||||
|
currentCustomer.value = row
|
||||||
|
followRecordDialogVisible.value = true
|
||||||
|
await manageService.crmCustomerFollow({ id: row.id }).then((res) => {
|
||||||
|
followRecords.value = res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览相关函数
|
||||||
|
const handleImageClick = (url: string) => {
|
||||||
|
imageList.value = [url]
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
imageViewerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImageViewer = () => {
|
||||||
|
imageViewerVisible.value = false
|
||||||
|
imageList.value = []
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
}
|
||||||
|
// 分配
|
||||||
|
const handlezySubmit = async () => {
|
||||||
|
const res = await manageService.crmCustomerAllocation({
|
||||||
|
customer_id: zyForm.customer_id,
|
||||||
|
user_id: zyForm.user_id
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.message || '分配成功!')
|
||||||
|
zyDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
// 批量分配
|
||||||
|
const handleplzySubmit = async () => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
|
||||||
|
const res = await manageService.crmCustomerAllocation(plzyForm)
|
||||||
|
ElMessage.success(res?.message || '批量分配成功!')
|
||||||
|
plzyDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
//删除
|
||||||
|
const onDelete = async (row: any) => {
|
||||||
|
try {
|
||||||
|
const res = await manageService.crmCustomerDelete({ customer_id: row.id })
|
||||||
|
ElMessage.success(res?.message || '删除成功!')
|
||||||
|
gettableList()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败,请稍后再试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 450px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tags {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
width: 120px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-following {
|
||||||
|
color: #00a43f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dealed {
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-table {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.tag-items {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
.tag-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.tag-itemall {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.tag-active {
|
||||||
|
background: #3471ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tag-active :deep(.el-badge__content) {
|
||||||
|
color: #3471ff !important; /* 文字颜色 */
|
||||||
|
background-color: #fff !important; /* 背景色 */
|
||||||
|
}
|
||||||
|
.detail-info-table {
|
||||||
|
padding-left: 15px;
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dtitle {
|
||||||
|
line-height: 40px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
.detils-content {
|
||||||
|
padding-left: 15px;
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fenpei {
|
||||||
|
color: #3471ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detil {
|
||||||
|
color: #00a43f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
color: #008477 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
color: #f00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-img {
|
||||||
|
width: 180px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-record-list {
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
.timeline-content-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.timeline-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.timeline-mom {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-mom {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="跟单人" label-width="70px">
|
||||||
|
<el-select
|
||||||
|
v-model="selectValue"
|
||||||
|
style="min-width: 120px"
|
||||||
|
filterable
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
placeholder="请选择跟单人"
|
||||||
|
>
|
||||||
|
<el-option v-for="v in userOptions" :key="v.id" :label="v.name" :value="v.id">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
|
||||||
|
<i class="iconfont-sys"></i> 导出
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选标签 -->
|
||||||
|
<div class="customer-tags boxshadow">
|
||||||
|
<!-- 前3组 -->
|
||||||
|
<div class="filter-row" v-for="item in visibleTagGroups" :key="item.id">
|
||||||
|
<span class="filter-label">{{ item.name }}</span>
|
||||||
|
<div class="filter-tags">
|
||||||
|
<span
|
||||||
|
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
|
||||||
|
@click="handleTagClick(item.id, '')"
|
||||||
|
>全部</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tag-items"
|
||||||
|
v-for="item1 in item.tags"
|
||||||
|
:key="item1.id"
|
||||||
|
@click="handleTagClick(item.id, item1.id)"
|
||||||
|
>
|
||||||
|
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
|
||||||
|
{{ item1.name }}
|
||||||
|
</span>
|
||||||
|
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 更多 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="showMore">
|
||||||
|
<div class="filter-row" v-for="item in moreTagGroups" :key="item.id">
|
||||||
|
<span class="filter-label">{{ item.name }}</span>
|
||||||
|
<div class="filter-tags">
|
||||||
|
<span
|
||||||
|
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
|
||||||
|
@click="handleTagClick(item.id, '')"
|
||||||
|
>全部</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tag-items"
|
||||||
|
v-for="item1 in item.tags"
|
||||||
|
:key="item1.id"
|
||||||
|
@click="handleTagClick(item.id, item1.id)"
|
||||||
|
>
|
||||||
|
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
|
||||||
|
{{ item1.name }}
|
||||||
|
</span>
|
||||||
|
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<div class="filter-more-btn" v-if="moreTagGroups.length">
|
||||||
|
<el-link underline="never" @click="showMore = !showMore">
|
||||||
|
{{ showMore ? '收起' : '更多' }}
|
||||||
|
<i v-if="showMore" class="iconfont-sys"> </i>
|
||||||
|
<i v-else class="iconfont-sys"></i>
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="plzyDialogVisible = true"
|
||||||
|
v-if="hasAuth('customer@transfer')"
|
||||||
|
:disabled="selectedRows.length === 0"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i> 批量转移
|
||||||
|
</el-button>
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="code" label="编号" align="center" />
|
||||||
|
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||||
|
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||||
|
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||||
|
<el-table-column prop="country_name" label="国家" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
|
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||||
|
<el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.follow)">
|
||||||
|
{{ scope.row.follow_txt }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||||
|
<el-table-column prop="user_name" label="跟单人"></el-table-column>
|
||||||
|
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onDetail(scope.row)"
|
||||||
|
v-if="hasAuth('customer@detail')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>详情
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="gotransfer(scope.row)"
|
||||||
|
v-if="hasAuth('customer@transfer')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>转移
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="warning"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="gofollowdetail(scope.row)"
|
||||||
|
v-if="hasAuth('customer@follow_detail')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>跟进记录
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||||
|
<div>电话:{{ detailRow.phone }}</div>
|
||||||
|
<div>国家:{{ detailRow.country_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>渠道:{{ detailRow.channel_name }}</div>
|
||||||
|
<div>邮箱:{{ detailRow.email }}</div>
|
||||||
|
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>发布人:{{ detailRow.enter_user_name }}</div>
|
||||||
|
<div>跟单人:{{ detailRow.user_name }}</div>
|
||||||
|
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
等级:
|
||||||
|
<span>
|
||||||
|
{{ detailRow.magnitude_txt }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
状态:<span>{{ detailRow.grade_txt }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
跟进状态:<span>{{ detailRow.follow_txt }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dtitle">详情内容</div>
|
||||||
|
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 转移跟单对话框 -->
|
||||||
|
<el-dialog v-model="zyDialogVisible" title="转移" width="360px">
|
||||||
|
<el-form :model="zyForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select
|
||||||
|
v-model="zyForm.user_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择转移人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="zyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handlezySubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 批量转移对话框 -->
|
||||||
|
<el-dialog v-model="plzyDialogVisible" title="批量转移" width="360px">
|
||||||
|
<el-form :model="plzyForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select
|
||||||
|
v-model="plzyForm.user_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择转移人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="plzyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 跟进记录对话框 -->
|
||||||
|
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
|
||||||
|
<!-- 跟进记录列表 -->
|
||||||
|
<div class="follow-record-list">
|
||||||
|
<ElScrollbar>
|
||||||
|
<template v-if="followRecords.length > 0">
|
||||||
|
<ElTimeline>
|
||||||
|
<ElTimelineItem
|
||||||
|
v-for="item in followRecords"
|
||||||
|
:key="item.time"
|
||||||
|
:placement="TIMELINE_PLACEMENT"
|
||||||
|
:color="activecolor"
|
||||||
|
:center="true"
|
||||||
|
>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-content-row">
|
||||||
|
<div class="timeline-content">
|
||||||
|
<p class="timeline-text"
|
||||||
|
>{{ item.type_txt }}
|
||||||
|
<span class="timeline-mom">{{ item.created_at }}</span></p
|
||||||
|
>
|
||||||
|
<p class="timeline-mom"
|
||||||
|
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
|
||||||
|
{{ item.audio_text }}</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
v-if="item.picture_full"
|
||||||
|
:src="item.picture_full"
|
||||||
|
class="timeline-img"
|
||||||
|
alt="图片"
|
||||||
|
@click="handleImageClick(item.picture_full)"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElTimelineItem>
|
||||||
|
</ElTimeline>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- 使用 Element Plus 的空状态组件 -->
|
||||||
|
<el-empty :image="emptyimg" description="暂无跟进记录" />
|
||||||
|
<!-- <el-empty /> -->
|
||||||
|
<!-- 或者自定义图片 -->
|
||||||
|
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
|
||||||
|
</template>
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 图片预览对话框 -->
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="imageViewerVisible"
|
||||||
|
:url-list="imageList"
|
||||||
|
:initial-index="imageViewerIndex"
|
||||||
|
@close="closeImageViewer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
|
import { CrmService } from '@/api/crmApi'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import emptyimg from '@/assets/img/crm/empty.png'
|
||||||
|
const TIMELINE_PLACEMENT = 'top'
|
||||||
|
const activecolor = '#3471ff'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const selectValue = ref<string[]>([])
|
||||||
|
const showMore = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const zyDialogVisible = ref(false)
|
||||||
|
const plzyDialogVisible = ref(false)
|
||||||
|
const followRecordDialogVisible = ref(false)
|
||||||
|
const detailRow = ref<any>(null)
|
||||||
|
const currentCustomer = ref<any>(null)
|
||||||
|
const followRecords = ref<any[]>([])
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
// 图片预览相关
|
||||||
|
const imageViewerVisible = ref(false)
|
||||||
|
const imageList = ref<string[]>([])
|
||||||
|
const imageViewerIndex = ref(0)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const plzyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
const zyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
// 获取列表数据
|
||||||
|
const tagActiveMap = reactive<{ [groupId: string]: string }>({})
|
||||||
|
//获取标签
|
||||||
|
type TagGroup = { id: string; name: string; tags: { id: string; name: string; count: number }[] }
|
||||||
|
const tagGroups = ref<TagGroup[]>([])
|
||||||
|
const tagGrouplistfn = async () => {
|
||||||
|
const res = await CrmService.tagGrouplist({ scene: 'customers' })
|
||||||
|
tagGroups.value = res || []
|
||||||
|
tagGroups.value.forEach((group) => {
|
||||||
|
if (group.tags && Array.isArray(group.tags)) {
|
||||||
|
tagActiveMap[group.id] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 计算前3组和更多组
|
||||||
|
// eslint-disable-next-line vue/valid-define-props
|
||||||
|
defineProps()
|
||||||
|
const visibleTagGroups = computed(() => tagGroups.value.slice(0, 3))
|
||||||
|
const moreTagGroups = computed(() => tagGroups.value.slice(3))
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
onMounted(() => {
|
||||||
|
getuserList()
|
||||||
|
gettableList()
|
||||||
|
tagGrouplistfn()
|
||||||
|
})
|
||||||
|
// tabs 切换联动
|
||||||
|
watch([selectValue, keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
}
|
||||||
|
if (selectValue.value && selectValue.value.length) {
|
||||||
|
params.user_id = selectValue.value.join(',')
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
// 标签参数,所有选中的标签id组成数组
|
||||||
|
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
|
||||||
|
if (selectedTagIds.length) {
|
||||||
|
params.tag_id = selectedTagIds
|
||||||
|
}
|
||||||
|
console.log(params)
|
||||||
|
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||||
|
tableData.value = data.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
function statusClass(status: any) {
|
||||||
|
if (status === 0) return 'status-following'
|
||||||
|
if (status === 1) return 'status-dealed'
|
||||||
|
if (status === 2) return 'status-lost'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
//导出
|
||||||
|
const exportfn = () => {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
user_id: selectValue.value.join(','),
|
||||||
|
tag_id: Object.values(tagActiveMap).filter(Boolean)
|
||||||
|
}
|
||||||
|
// manageService.crmCustomerExport(params)
|
||||||
|
try {
|
||||||
|
// 1. 调用接口,获取blob
|
||||||
|
// const res = await manageService.crmCustomerExport(params)
|
||||||
|
const urls =
|
||||||
|
import.meta.env.VITE_API_URL +
|
||||||
|
`/admin/crm/customer/export?current=${params.currentPage}&size=${params.pageSize}&user_id=${params.user_id}&tag_id=${params.tag_id}`
|
||||||
|
// 2. 创建blob对象
|
||||||
|
// const blob = new Blob([res], { type: 'application/vnd.ms-excel' }) // 或根据后端实际类型调整
|
||||||
|
// 3. 创建下载链接
|
||||||
|
const url = urls
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = '客户数据.xlsx' // 可自定义文件名
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('导出失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 标签选中状态,key为组id,值为选中的标签id
|
||||||
|
function handleTagClick(groupId: string, tagId: string) {
|
||||||
|
// if (tagActiveMap[groupId] === tagId) {
|
||||||
|
// tagActiveMap[groupId] = '' // 取消选中
|
||||||
|
// } else {
|
||||||
|
tagActiveMap[groupId] = tagId // 选中
|
||||||
|
// }
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
function onDetail(row: any) {
|
||||||
|
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||||
|
detailRow.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const gotransfer = (row: any) => {
|
||||||
|
zyForm.customer_id = row.id
|
||||||
|
zyDialogVisible.value = true
|
||||||
|
}
|
||||||
|
// 跟进记录
|
||||||
|
const gofollowdetail = async (row: any) => {
|
||||||
|
currentCustomer.value = row
|
||||||
|
followRecordDialogVisible.value = true
|
||||||
|
await manageService.crmCustomerFollow({ id: row.id }).then((res) => {
|
||||||
|
followRecords.value = res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览相关函数
|
||||||
|
const handleImageClick = (url: string) => {
|
||||||
|
imageList.value = [url]
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
imageViewerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImageViewer = () => {
|
||||||
|
imageViewerVisible.value = false
|
||||||
|
imageList.value = []
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
}
|
||||||
|
// 转移
|
||||||
|
const handlezySubmit = async () => {
|
||||||
|
const res = await manageService.crmCustomerTransfer({
|
||||||
|
customer_id: zyForm.customer_id,
|
||||||
|
user_id: zyForm.user_id
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.message || '转移成功!')
|
||||||
|
zyDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
// 批量转移
|
||||||
|
const handleplzySubmit = async () => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
|
||||||
|
const res = await manageService.crmCustomerTransfer(plzyForm)
|
||||||
|
ElMessage.success(res?.message || '批量转移成功!')
|
||||||
|
plzyDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.timeline-img {
|
||||||
|
width: 180px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 450px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tags {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
width: 120px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-following {
|
||||||
|
color: #00a43f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dealed {
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-table {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.tag-items {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
.tag-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.tag-itemall {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.tag-active {
|
||||||
|
background: #3471ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tag-active :deep(.el-badge__content) {
|
||||||
|
color: #3471ff !important; /* 文字颜色 */
|
||||||
|
background-color: #fff !important; /* 背景色 */
|
||||||
|
}
|
||||||
|
.detail-info-table {
|
||||||
|
padding-left: 15px;
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dtitle {
|
||||||
|
line-height: 40px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
.detils-content {
|
||||||
|
padding-left: 15px;
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-record-header {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-record-list {
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
.list-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.record-type {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-time {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-user {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-footer {
|
||||||
|
.next-follow {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-records {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,906 @@
|
|||||||
|
<!-- eslint-disable vue/no-parsing-error -->
|
||||||
|
<!-- eslint-disable vue/valid-attribute-name -->
|
||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
|
<template>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<section class="search-bar art-custom-card">
|
||||||
|
<el-form v-model:filter="formFilters" label-position="left">
|
||||||
|
<el-row class="search-form-row" :gutter="12">
|
||||||
|
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
|
||||||
|
<el-form-item label="关键字" label-width="70px">
|
||||||
|
<el-input v-model="filter.keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
|
||||||
|
<el-form-item label="状态" label-width="70px">
|
||||||
|
<el-select v-model="filter.status">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="待发布" value="0" />
|
||||||
|
<el-option label="待跟踪" value="1" />
|
||||||
|
<el-option label="已跟踪" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
|
||||||
|
<el-form-item label="渠道" label-width="70px">
|
||||||
|
<el-select v-model="filter.channel">
|
||||||
|
<el-option v-for="v in channelOptions" :key="v.id" :label="v.name" :value="v.id">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
|
||||||
|
<el-form-item label="时间" label-width="70px">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filter.time"
|
||||||
|
type="daterange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
valueFormat="YYYY-MM-DD"
|
||||||
|
rangeSeparator="至"
|
||||||
|
startPlaceholder="开始日期"
|
||||||
|
endPlaceholder="结束日期"
|
||||||
|
:style="{ width: '100%' }"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :sm="24" :md="4" :lg="4" :xl="4" class="action-column">
|
||||||
|
<div
|
||||||
|
class="action-buttons-wrapper"
|
||||||
|
:style="{
|
||||||
|
'justify-content': isMobile ? 'flex-end' : 3 <= 2 ? 'flex-start' : 'flex-end'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="form-buttons">
|
||||||
|
<el-button class="reset-button" @click="handleReset">{{
|
||||||
|
$t('table.searchBar.reset')
|
||||||
|
}}</el-button>
|
||||||
|
<el-button type="primary" class="search-button" @click="handleSearch">{{
|
||||||
|
$t('table.searchBar.search')
|
||||||
|
}}</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-btn-right">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
<div class="table">
|
||||||
|
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry#store')"><i class="iconfont-sys"></i> 录单</el-button>
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:marginTop="10"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column prop="order_no" label="SN"></el-table-column>
|
||||||
|
<el-table-column prop="contacts" label="客户名称"></el-table-column>
|
||||||
|
<el-table-column prop="tel" label="电话"></el-table-column>
|
||||||
|
<el-table-column prop="phone" label="国家">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{scope.row.countryInfo? scope.row.countryInfo.cn_addr : ''}}
|
||||||
|
</span>
|
||||||
|
<span v-if="scope.row.countryInfo">
|
||||||
|
<el-tag class="steer" type="warning" v-if="scope.row.countryInfo.steering=='左舵'">{{ scope.row.countryInfo.steering }}</el-tag>
|
||||||
|
<el-tag class="steer" type="success" v-else-if="scope.row.countryInfo.steering=='右舵'">{{ scope.row.countryInfo.steering }}</el-tag>
|
||||||
|
</span>
|
||||||
|
<!-- <span class="steer" v-if="scope.row.countryInfo">{{ scope.row.countryInfo.steering }}</span> -->
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="phone" label="渠道">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{scope.row.channelInfo? scope.row.channelInfo.name : ''}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quality" label="客户商机">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :style="scope.row.quality==1 ? { color: '#F5AB00' } : scope.row.quality==2 ? { color: '#8495B8' }: { color: '#9B7200' }">
|
||||||
|
{{scope.row.quality==1 ? '金牌商机' : scope.row.quality==2 ? '银牌商机' : '铜牌商机'}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="magnitude" label="发布人">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>
|
||||||
|
{{scope.row.enter? scope.row.enter.name : ''}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="recipient" label="跟单人">
|
||||||
|
<template #default="scope">
|
||||||
|
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry#reallocation')" @click="onFenpei(scope.row,'edit')">
|
||||||
|
{{scope.row.recipientInfo? scope.row.recipientInfo.name : ''}}
|
||||||
|
<i class="iconfont-sys"></i>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :style="scope.row.status== 0 ? { color: '#ccc' } :scope.row.status== 1 ? { color: '#00A43F' }: { color: '#3471FF' }">
|
||||||
|
{{ scope.row.status== 0 ? '未发布' : scope.row.status== 1 ? '待跟踪' : '已跟踪' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- <el-table-column label="倒计时" min-width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="getCountdownClass(scope.row.countdown)" v-if="scope.row.status==1">
|
||||||
|
<span class="custom-clock-icon" style="margin-right: 4px; vertical-align: middle"
|
||||||
|
><i class="iconfont-sys"></i></span
|
||||||
|
>
|
||||||
|
{{ getCountdown(scope.row.countdown) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="260px">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry#allocation')">
|
||||||
|
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||||
|
><i class="iconfont-sys"></i></span
|
||||||
|
>
|
||||||
|
分配
|
||||||
|
</div>
|
||||||
|
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry#show')">
|
||||||
|
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||||
|
><i class="iconfont-sys"></i></span
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</div>
|
||||||
|
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry#release')">
|
||||||
|
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||||
|
><i class="iconfont-sys"></i></span
|
||||||
|
>
|
||||||
|
发布
|
||||||
|
</div>
|
||||||
|
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry#update')">
|
||||||
|
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||||
|
><i class="iconfont-sys"></i></span
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</div>
|
||||||
|
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry#destroy')">
|
||||||
|
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||||
|
><i class="iconfont-sys"></i></span
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情" >
|
||||||
|
<div class="detail-info-table">
|
||||||
|
<div class="row">
|
||||||
|
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||||
|
<div>电话:{{ detailRow.tel }}</div>
|
||||||
|
<div>国家:{{ detailRow.countryInfo? detailRow.countryInfo.cn_addr:'' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>渠道:{{ detailRow.channelInfo?detailRow.channelInfo.name:'' }}</div>
|
||||||
|
<div>邮箱:{{ detailRow.email }}</div>
|
||||||
|
<div>WhatsApp:{{ detailRow.whatsapp }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>发布人:{{ detailRow.enter? detailRow.enter.name : ''}}</div>
|
||||||
|
<div>跟单人:{{ detailRow.recipientInfo? detailRow.recipientInfo.name : ''}}</div>
|
||||||
|
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
等级:
|
||||||
|
<span :style="detailRow.quality==1 ? { color: '#F5AB00' } : detailRow.quality==2 ? { color: '#8495B8' }: { color: '#9B7200' }">
|
||||||
|
{{detailRow.quality==1 ? '金牌商机' : detailRow.quality==2 ? '银牌商机' : '铜牌商机'}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
>状态:<span>{{ detailRow.status== 0 ? '未发布' : detailRow.status== 1 ? '待跟踪' : '已跟踪' }}</span></div
|
||||||
|
>
|
||||||
|
<div>SN:{{ detailRow.order_no }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dtitle">详情内容</div>
|
||||||
|
<div v-if="detailRow" v-html="detailRow.content" class="detils-content" @click="handleImageClick" style="cursor: pointer;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片预览组件 -->
|
||||||
|
<ElImageViewer
|
||||||
|
v-if="showImageViewer"
|
||||||
|
:url-list="imageList"
|
||||||
|
:initial-index="currentImageIndex"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
@close="showImageViewer = false"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
:title="dialogType === 'add' ? '录单' : '编辑'"
|
||||||
|
v-model="AddDialogVisible"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="addForm" ref="addFormRef" label-width="80px" class="add-form">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="渠道" prop="channel">
|
||||||
|
<el-select v-model="addForm.channel" placeholder="请选择渠道">
|
||||||
|
<el-option
|
||||||
|
v-for="item in channelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="等级" prop="quality">
|
||||||
|
<el-select v-model="addForm.quality" placeholder="请选择等级">
|
||||||
|
<el-option
|
||||||
|
v-for="item in opportunityOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="国家" prop="country">
|
||||||
|
<el-select v-model="addForm.country" filterable placeholder="请选择国家">
|
||||||
|
<el-option
|
||||||
|
v-for="item in countryOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.cn_addr"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="Email" prop="email">
|
||||||
|
<el-input v-model="addForm.email" placeholder="请输入Email" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系方式" prop="tel">
|
||||||
|
<el-input v-model="addForm.tel" placeholder="请输入联系方式" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系人" prop="contacts">
|
||||||
|
<el-input v-model="addForm.contacts" placeholder="请输入联系人" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="WhatsApp" prop="whatsapp">
|
||||||
|
<el-input v-model="addForm.whatsapp" placeholder="请输入WhatsApp" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="跟单人" prop="country">
|
||||||
|
<el-select v-model="addForm.recipient" filterable placeholder="请选择跟单人">
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="详情内容" prop="content">
|
||||||
|
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||||
|
</el-form-item>
|
||||||
|
<div class="form-submit-row">
|
||||||
|
<el-button type="primary" size="large" style="width: 50%" @click="handleAddSubmit"
|
||||||
|
>提交</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 分配 -->
|
||||||
|
<el-dialog v-model="fenpeiDialogVisible" :title="dialogTypefp === 'add' ? '分配跟单' : '重新分配跟单'" width="360px">
|
||||||
|
<el-form :model="fenpeiForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select v-model="fenpeiForm.recipient" filterable placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="fenpeiDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleFenpeiSubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { ElMessageBox, ElImageViewer } from 'element-plus'
|
||||||
|
import { CrmService } from '@/api/crmApi'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogType = ref('add')
|
||||||
|
const dialogTypefp = ref('add')
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const isMobile = computed(() => width.value < 500)
|
||||||
|
const filter = ref({
|
||||||
|
status: '',
|
||||||
|
time: '',
|
||||||
|
channel: '',
|
||||||
|
keyword: ''
|
||||||
|
})
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const editorHtml = ref('') // 编辑器内容
|
||||||
|
const timer = ref(0)
|
||||||
|
const refreshKey = ref(0)
|
||||||
|
const fenpeiDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const AddDialogVisible = ref(false)
|
||||||
|
const detailRow = ref<any>(null)
|
||||||
|
const countryOptions = ref<any>()
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
const addFormRef = ref()
|
||||||
|
let addForm = reactive({
|
||||||
|
channel: '',
|
||||||
|
quality: '',
|
||||||
|
country: '',
|
||||||
|
email: '',
|
||||||
|
tel: '',
|
||||||
|
contacts: '',
|
||||||
|
whatsapp: '',
|
||||||
|
content: '',
|
||||||
|
recipient: ''
|
||||||
|
})
|
||||||
|
// 响应式表单数据
|
||||||
|
const formFilters = reactive({ ...filter })
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
filter.value = {
|
||||||
|
status: '',
|
||||||
|
time: '',
|
||||||
|
channel: '',
|
||||||
|
keyword: ''
|
||||||
|
}
|
||||||
|
Object.assign(formFilters, { ...filter })
|
||||||
|
pagination.currentPage = 1 // 重置到第一页
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
console.log('搜索参数:', formFilters)
|
||||||
|
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const opportunityOptions = ref<any>()
|
||||||
|
//商机
|
||||||
|
const getMagnitude = async () => {
|
||||||
|
// 这里调用获取品牌列表的接口
|
||||||
|
const res = await CrmService.getMagnitude()
|
||||||
|
opportunityOptions.value = res || []
|
||||||
|
}
|
||||||
|
const channelOptions = ref<any>()
|
||||||
|
//渠道
|
||||||
|
const getgetChannel = async () => {
|
||||||
|
const res = await CrmService.getChannel()
|
||||||
|
channelOptions.value = res
|
||||||
|
}
|
||||||
|
//国家
|
||||||
|
const getarea = async () => {
|
||||||
|
const res = await CrmService.getCountry()
|
||||||
|
countryOptions.value = res || []
|
||||||
|
}
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
// 计算属性:将时间格式化为 "开始日期 - 结束日期"
|
||||||
|
const formattedTimeRange = () => {
|
||||||
|
if (!filter.value.time || filter.value.time.length !== 2) {
|
||||||
|
return '' // 如果时间范围无效,返回空字符串
|
||||||
|
}
|
||||||
|
const [startDate, endDate] = filter.value.time
|
||||||
|
return `${startDate} - ${endDate}`
|
||||||
|
}
|
||||||
|
// 获取列表数据
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
channel: filter.value.channel,
|
||||||
|
status: filter.value.status,
|
||||||
|
keyword: filter.value.keyword,
|
||||||
|
time: formattedTimeRange()
|
||||||
|
}
|
||||||
|
const { records, current, size, total } = await manageService.getinquiryList(params)
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getgetChannel()
|
||||||
|
gettableList()
|
||||||
|
getuserList()
|
||||||
|
timer.value = window.setInterval(() => {
|
||||||
|
refreshKey.value++
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function getCountdown(targetTimestamp: number) {
|
||||||
|
// 让 refreshKey 成为依赖,确保每秒刷新
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
refreshKey.value
|
||||||
|
const now = dayjs()
|
||||||
|
const target = dayjs.unix(targetTimestamp) // 使用时间戳创建 dayjs 对象
|
||||||
|
if (!target.isValid()) return '时间格式错误'
|
||||||
|
const diff = target.diff(now)
|
||||||
|
if (diff <= 0) return '已超时'
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
|
||||||
|
const minutes = Math.floor((diff / (1000 * 60)) % 60)
|
||||||
|
const seconds = Math.floor((diff / 1000) % 60)
|
||||||
|
return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
|
||||||
|
}
|
||||||
|
// function getCountdown(targetDate: string) {
|
||||||
|
// // 让 refreshKey 成为依赖,确保每秒刷新
|
||||||
|
// // eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
// refreshKey.value
|
||||||
|
// const now = dayjs()
|
||||||
|
// const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
// if (!target.isValid()) return '时间格式错误'
|
||||||
|
// const diff = target.diff(now)
|
||||||
|
// if (diff <= 0) return '已超时'
|
||||||
|
// const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
// const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
|
||||||
|
// const minutes = Math.floor((diff / (1000 * 60)) % 60)
|
||||||
|
// const seconds = Math.floor((diff / 1000) % 60)
|
||||||
|
// return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
|
||||||
|
// }
|
||||||
|
|
||||||
|
function getCountdownClass(targetDate: number) {
|
||||||
|
const now = dayjs()
|
||||||
|
// const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
const target = dayjs.unix(targetDate)
|
||||||
|
if (!target.isValid() || target.diff(now) <= 0) {
|
||||||
|
return 'countdown-danger'
|
||||||
|
}
|
||||||
|
return 'countdown-success'
|
||||||
|
}
|
||||||
|
function reactiveForm() {
|
||||||
|
Object.assign(addForm, {
|
||||||
|
channel: '',
|
||||||
|
quality: '',
|
||||||
|
country: '',
|
||||||
|
email: '',
|
||||||
|
tel: '',
|
||||||
|
contacts: '',
|
||||||
|
whatsapp: '',
|
||||||
|
content: '',
|
||||||
|
recipient: ''
|
||||||
|
})
|
||||||
|
editorHtml.value = ''
|
||||||
|
}
|
||||||
|
const addfn = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
reactiveForm()
|
||||||
|
getMagnitude()
|
||||||
|
getarea()
|
||||||
|
AddDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const onFollow = async (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要发布此询单吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await manageService.inquiryrelease({ id: row.id, status: 1 })
|
||||||
|
ElMessage.success(res?.message || '请求成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDetail(row: any) {
|
||||||
|
row.content = processImages(row.content) // 处理图片
|
||||||
|
detailRow.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
//编辑
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
const carrid = ref('')
|
||||||
|
const onEdit = async (row: any) => {
|
||||||
|
dialogType.value = 'edit'
|
||||||
|
getMagnitude()
|
||||||
|
getarea()
|
||||||
|
AddDialogVisible.value = true
|
||||||
|
carrid.value = row.id
|
||||||
|
addForm = reactive({
|
||||||
|
channel: row.channelInfo ? row.channelInfo.id : '',
|
||||||
|
quality: row.quality,
|
||||||
|
country: row.countryInfo ? row.countryInfo.id : '',
|
||||||
|
email: row.email,
|
||||||
|
tel: row.tel,
|
||||||
|
contacts: row.contacts,
|
||||||
|
whatsapp: row.whatsapp,
|
||||||
|
content: row.content,
|
||||||
|
recipient: row.recipientInfo ? row.recipientInfo.id : ''
|
||||||
|
})
|
||||||
|
editorHtml.value = row.content
|
||||||
|
// 这里可以添加编辑逻辑
|
||||||
|
// 例如打开编辑对话框或跳转到编辑页面
|
||||||
|
}
|
||||||
|
const delCodeTrim = (content: string): string => {
|
||||||
|
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||||
|
}
|
||||||
|
const handleAddSubmit = async () => {
|
||||||
|
// 提交表单逻辑
|
||||||
|
// 这里可以调用 API 提交 addForm 数据
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||||
|
addForm.content = editorHtml.value
|
||||||
|
const res = await manageService.inquiryAdd(addForm)
|
||||||
|
ElMessage.success(res?.message || '录单成功!')
|
||||||
|
} else {
|
||||||
|
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||||
|
addForm.content = editorHtml.value
|
||||||
|
const forms = { ...addForm, id: carrid.value }
|
||||||
|
const res = await manageService.inquiryedit(forms)
|
||||||
|
ElMessage.success(res?.message || '录单成功!')
|
||||||
|
}
|
||||||
|
AddDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
//删除
|
||||||
|
const onDelete = (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要删除此条询单吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await manageService.inquirydelete({ id: row.id })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fenpeiForm = reactive({
|
||||||
|
id: '',
|
||||||
|
recipient: ''
|
||||||
|
})
|
||||||
|
//分配
|
||||||
|
const onFenpei = (row: any, edits: any) => {
|
||||||
|
dialogTypefp.value = edits
|
||||||
|
fenpeiForm.id = row.id
|
||||||
|
fenpeiDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const handleFenpeiSubmit = async () => {
|
||||||
|
if (!fenpeiForm.recipient) {
|
||||||
|
ElMessage.warning('请选择跟单人')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dialogTypefp.value === 'add') {
|
||||||
|
const res = await manageService.inquiryallocation({
|
||||||
|
id: fenpeiForm.id,
|
||||||
|
recipient: fenpeiForm.recipient,
|
||||||
|
status: 2
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.message || '分配成功!')
|
||||||
|
} else {
|
||||||
|
const res = await manageService.inquiryreallocation({
|
||||||
|
id: fenpeiForm.id,
|
||||||
|
recipient: fenpeiForm.recipient,
|
||||||
|
status: 2
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.message || '分配成功!')
|
||||||
|
}
|
||||||
|
fenpeiDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageList = ref<string[]>([])
|
||||||
|
const currentImageIndex = ref(0)
|
||||||
|
const showImageViewer = ref(false)
|
||||||
|
|
||||||
|
const handleImageClick = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (target.tagName === 'IMG') {
|
||||||
|
const imgElement = target as HTMLImageElement
|
||||||
|
|
||||||
|
// 获取所有图片并设置预览列表
|
||||||
|
const contentDiv = event.currentTarget as HTMLElement
|
||||||
|
const allImages = contentDiv.querySelectorAll('img')
|
||||||
|
const imageUrls = Array.from(allImages).map(img => img.src)
|
||||||
|
|
||||||
|
imageList.value = imageUrls
|
||||||
|
currentImageIndex.value = imageUrls.indexOf(imgElement.src)
|
||||||
|
showImageViewer.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-bar {
|
||||||
|
padding: 20px 20px 0;
|
||||||
|
background-color: var(--art-main-bg-color);
|
||||||
|
border-radius: calc(var(--custom-radius) / 2 + 2px);
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-column {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.action-buttons-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
// justify-content: flex-end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
line-height: 32px;
|
||||||
|
color: var(--main-color);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-btn-right {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.card-list {
|
||||||
|
.art-custom-card {
|
||||||
|
.iconfont-sys {
|
||||||
|
background-color: #232323 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-top: 10px;
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 300px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th),
|
||||||
|
:deep(.el-table td) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-danger {
|
||||||
|
color: #f00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-success {
|
||||||
|
color: #00a43f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fenpei {
|
||||||
|
color: #3471ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detil {
|
||||||
|
color: #00a43f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
color: #008477 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
color: #f00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info-table {
|
||||||
|
padding-left: 25px;
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit-row {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.steer {
|
||||||
|
margin-left: 5px;
|
||||||
|
// display: inline-block;
|
||||||
|
// border: 1px solid #00a43f;
|
||||||
|
// padding: 0px 4px;
|
||||||
|
// color: #00a43f;
|
||||||
|
}
|
||||||
|
.dtitle {
|
||||||
|
line-height: 40px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
|
.detils-content {
|
||||||
|
padding-left: 25px;
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-content">
|
||||||
|
<div v-if="!isMobile">
|
||||||
|
<ElRow>
|
||||||
|
<div style="width: 12px"></div>
|
||||||
|
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||||
|
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@company_add')">新增</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn label="ID" prop="id" />
|
||||||
|
<ElTableColumn label="公司名称" prop="name" />
|
||||||
|
<ElTableColumn label="公司标识" prop="abbr" />
|
||||||
|
<ElTableColumn label="状态" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||||
|
{{ scope.row.status ? '启用' : '禁用' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="创建时间" prop="created_at" />
|
||||||
|
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElRow>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
@click="showDialog('edit', scope.row)"
|
||||||
|
v-if="hasAuth('system@company_update')"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="deleteRole(scope.row)"
|
||||||
|
v-if="hasAuth('system@company_destroy')"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
<ElDialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||||
|
width="40%"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
|
||||||
|
<ElFormItem label="公司名称" prop="name">
|
||||||
|
<ElInput v-model="form.name" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="公司标识" prop="abbr">
|
||||||
|
<ElInput v-model="form.abbr" />
|
||||||
|
</ElFormItem>
|
||||||
|
<!-- <ElFormItem label="飞书标识" prop="tenant_key">
|
||||||
|
<ElInput v-model="form.tenant_key" />
|
||||||
|
</ElFormItem> -->
|
||||||
|
<ElFormItem label="状态">
|
||||||
|
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mobile-hide-tip">
|
||||||
|
<p>该页面暂不支持移动端访问</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
defineOptions({ name: 'Type ' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const isMobile = computed(() => width.value < 500)
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||||
|
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
|
||||||
|
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive<any>({
|
||||||
|
name: '',
|
||||||
|
id: '',
|
||||||
|
abbr: '',
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
|
||||||
|
// 表格实例引用
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取列表数据
|
||||||
|
const getWorkList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
|
||||||
|
const { records, current, size, total } = await systemService.getCompanyList({
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
})
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogType = ref('add')
|
||||||
|
|
||||||
|
const showDialog = (type: string, row?: any) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogType.value = type
|
||||||
|
|
||||||
|
if (type === 'edit' && row) {
|
||||||
|
form.name = row.name
|
||||||
|
form.id = row.id
|
||||||
|
form.status = row.status
|
||||||
|
form.abbr = row.abbr
|
||||||
|
// form.tenant_key = row.tenant_key
|
||||||
|
} else {
|
||||||
|
form.name = ''
|
||||||
|
form.id = ''
|
||||||
|
form.status = 1
|
||||||
|
form.abbr = ''
|
||||||
|
// form.tenant_key = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deleteRole = (row?: any) => {
|
||||||
|
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await systemService.companyDelete({
|
||||||
|
id: row.id
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
const { message } = await systemService.companyAdd(form)
|
||||||
|
ElMessage.success(message)
|
||||||
|
dialogVisible.value = false
|
||||||
|
getWorkList()
|
||||||
|
} else {
|
||||||
|
const { message } = await systemService.companyEdit(form)
|
||||||
|
ElMessage.success(message)
|
||||||
|
dialogVisible.value = false
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-content">
|
||||||
|
<div v-if="!isMobile">
|
||||||
|
<ElRow>
|
||||||
|
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
||||||
|
<ElInput placeholder="名称"></ElInput>
|
||||||
|
</ElCol> -->
|
||||||
|
<div style="width: 12px"></div>
|
||||||
|
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||||
|
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
||||||
|
<ElButton @click="showDialog('add')" v-if="hasAuth('system@feishu_app_add')" v-ripple
|
||||||
|
>新增</ElButton
|
||||||
|
>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn label="ID" prop="id" width="80" />
|
||||||
|
<ElTableColumn label="公司" prop="corp_id" width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.corpInfo.name }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="系统" prop="system_id" width="140">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.systemInfo.name }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="名称" prop="name" width="160" />
|
||||||
|
<ElTableColumn label="应用ID" prop="app_id" width="180" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="基础域" prop="base_uri" width="220" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="授权地址" prop="r_url" width="260" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="授权范围" prop="scopes" width="240" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="携带参数" prop="state" width="140" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="排序" prop="sort" width="90" />
|
||||||
|
<ElTableColumn label="状态" prop="status" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||||
|
{{ scope.row.status ? '启用' : '禁用' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="创建时间" prop="created_at" width="180" />
|
||||||
|
<ElTableColumn fixed="right" label="操作" width="180px">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElRow>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
@click="showDialog('edit', scope.row)"
|
||||||
|
v-if="hasAuth('system@feishu_app_update')"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="deleteRole(scope.row)"
|
||||||
|
v-if="hasAuth('system@feishu_app_destroy')"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
<ElDialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
style="padding: 5px 20px"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="公司" prop="corp_id">
|
||||||
|
<ElSelect v-model="form.corp_id" placeholder="请选择公司" filterable style="width: 100%">
|
||||||
|
<ElOption v-for="item in companyList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="系统" prop="system_id">
|
||||||
|
<ElSelect v-model="form.system_id" placeholder="请选择系统" filterable style="width: 100%">
|
||||||
|
<ElOption v-for="item in systemsList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="名称" prop="name">
|
||||||
|
<ElInput v-model="form.name" placeholder="请输入名称" />
|
||||||
|
</ElFormItem>
|
||||||
|
<!-- <ElFormItem label="企业标识" prop="tenant_key">
|
||||||
|
<ElInput v-model="form.tenant_key" placeholder="请输入企业标识" />
|
||||||
|
</ElFormItem> -->
|
||||||
|
<ElFormItem label="应用ID" prop="app_id">
|
||||||
|
<ElInput v-model="form.app_id" placeholder="请输入应用ID" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="应用秘钥" prop="app_secret">
|
||||||
|
<ElInput v-model="form.app_secret" placeholder="请输入应用秘钥" show-password />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="基础域" prop="base_uri">
|
||||||
|
<ElInput v-model="form.base_uri" placeholder="请输入基础域" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="授权地址" prop="r_url">
|
||||||
|
<ElInput v-model="form.r_url" placeholder="请输入授权地址" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="授权范围" prop="scopes">
|
||||||
|
<ElInput v-model="form.scopes" type="textarea" :rows="3" placeholder="请输入授权范围" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="携带参数" prop="state">
|
||||||
|
<ElInput v-model="form.state" placeholder="请输入携带参数" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="排序" prop="sort">
|
||||||
|
<ElInputNumber v-model="form.sort" :min="0" placeholder="请输入排序" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="状态" prop="status">
|
||||||
|
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mobile-hide-tip">
|
||||||
|
<p>该页面暂不支持移动端访问</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { UserService } from '@/api/usersApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
defineOptions({ name: 'Type ' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const isMobile = computed(() => width.value < 500)
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
corp_id: [{ required: true, message: '请选择公司', trigger: 'change' }],
|
||||||
|
system_id: [{ required: true, message: '请选择系统', trigger: 'change' }],
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
// app_id: [{ required: true, message: '请输入应用ID', trigger: 'blur' }],
|
||||||
|
// app_secret: [{ required: true, message: '请输入应用秘钥', trigger: 'blur' }],
|
||||||
|
// base_uri: [{ required: true, message: '请输入基础域', trigger: 'blur' }],
|
||||||
|
// r_url: [{ required: true, message: '请输入授权地址', trigger: 'blur' }],
|
||||||
|
// scopes: [{ required: true, message: '请输入授权范围', trigger: 'blur' }],
|
||||||
|
// state: [{ required: true, message: '请输入携带参数', trigger: 'blur' }],
|
||||||
|
// sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||||
|
// status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
corp_id: '',
|
||||||
|
system_id: '',
|
||||||
|
name: '',
|
||||||
|
// tenant_key: '',
|
||||||
|
app_id: '',
|
||||||
|
app_secret: '',
|
||||||
|
base_uri: '',
|
||||||
|
r_url: '',
|
||||||
|
scopes: '',
|
||||||
|
state: '',
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
id: ''
|
||||||
|
})
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const companyList = ref<any[]>([])
|
||||||
|
const systemsList = ref<any[]>([])
|
||||||
|
const getcompanyList = async () => {
|
||||||
|
const response = await UserService.getCompanyList()
|
||||||
|
const list = (response as any)?.data ?? (response as any)?.records ?? response
|
||||||
|
companyList.value = Array.isArray(list) ? list : []
|
||||||
|
}
|
||||||
|
const getsystemsList = async () => {
|
||||||
|
const response = await UserService.getsystemsList()
|
||||||
|
const list = (response as any)?.data ?? (response as any)?.records ?? response
|
||||||
|
systemsList.value = Array.isArray(list) ? list : []
|
||||||
|
}
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
|
||||||
|
// 表格实例引用
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getWorkList()
|
||||||
|
getcompanyList()
|
||||||
|
getsystemsList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取列表数据
|
||||||
|
const getWorkList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
|
||||||
|
const { records, current, size, total } = await systemService.getFeishuAppList({
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
})
|
||||||
|
tableData.value = records.map((item: any, index: number) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogType = ref('add')
|
||||||
|
|
||||||
|
const showDialog = (type: string, row?: any) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogType.value = type
|
||||||
|
|
||||||
|
if (type === 'edit' && row) {
|
||||||
|
// 编辑模式,填充所有字段
|
||||||
|
Object.assign(form, {
|
||||||
|
corp_id: row.corp_id || '',
|
||||||
|
system_id: row.system_id || '',
|
||||||
|
name: row.name || '',
|
||||||
|
// tenant_key: row.tenant_key || '',
|
||||||
|
app_id: row.app_id || '',
|
||||||
|
app_secret: row.app_secret || '',
|
||||||
|
base_uri: row.base_uri || '',
|
||||||
|
r_url: row.r_url || '',
|
||||||
|
scopes: row.scopes || '',
|
||||||
|
state: row.state || '',
|
||||||
|
sort: row.sort || 0,
|
||||||
|
status: row.status !== undefined ? row.status : 1,
|
||||||
|
id: row.id || ''
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 新增模式,重置所有字段
|
||||||
|
Object.assign(form, {
|
||||||
|
corp_id: '',
|
||||||
|
system_id: '',
|
||||||
|
name: '',
|
||||||
|
// tenant_key: '',
|
||||||
|
app_id: '',
|
||||||
|
app_secret: '',
|
||||||
|
base_uri: '',
|
||||||
|
r_url: '',
|
||||||
|
scopes: '',
|
||||||
|
state: '',
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
id: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deleteRole = (row?: any) => {
|
||||||
|
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await systemService.feishuAppDelete({
|
||||||
|
id: row.id
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
await formEl.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
const { message } = await systemService.feishuAppAdd({
|
||||||
|
corp_id: form.corp_id,
|
||||||
|
system_id: form.system_id,
|
||||||
|
name: form.name,
|
||||||
|
app_id: form.app_id,
|
||||||
|
app_secret: form.app_secret,
|
||||||
|
base_uri: form.base_uri,
|
||||||
|
r_url: form.r_url,
|
||||||
|
scopes: form.scopes,
|
||||||
|
state: form.state,
|
||||||
|
sort: form.sort,
|
||||||
|
status: form.status
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
} else {
|
||||||
|
const { message } = await systemService.feishuAppEdit({
|
||||||
|
id: form.id,
|
||||||
|
corp_id: form.corp_id,
|
||||||
|
system_id: form.system_id,
|
||||||
|
name: form.name,
|
||||||
|
app_id: form.app_id,
|
||||||
|
app_secret: form.app_secret,
|
||||||
|
base_uri: form.base_uri,
|
||||||
|
r_url: form.r_url,
|
||||||
|
scopes: form.scopes,
|
||||||
|
state: form.state,
|
||||||
|
sort: form.sort,
|
||||||
|
status: form.status
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,751 @@
|
|||||||
|
<template>
|
||||||
|
<ArtTableFullScreen>
|
||||||
|
<div class="menu-page" id="table-full-screen">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<ArtSearchBar
|
||||||
|
v-model:filter="formFilters"
|
||||||
|
:items="formItems"
|
||||||
|
:showExpand="false"
|
||||||
|
@reset="handleReset"
|
||||||
|
@search="handleSearch"
|
||||||
|
></ArtSearchBar>
|
||||||
|
|
||||||
|
<ElCard shadow="never" class="art-table-card">
|
||||||
|
<!-- 表格头部 -->
|
||||||
|
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||||
|
<template #left>
|
||||||
|
<el-segmented
|
||||||
|
v-model="yearVal"
|
||||||
|
size="large"
|
||||||
|
:options="segmentedOptions"
|
||||||
|
@change="searchArticleByYear"
|
||||||
|
>
|
||||||
|
<!-- <template #default="scope">
|
||||||
|
<div class="flex flex-col items-center gap-2 p-2">
|
||||||
|
<div>{{ scope.item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</template> -->
|
||||||
|
</el-segmented>
|
||||||
|
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
|
||||||
|
<ElButton
|
||||||
|
v-auth="'system@menu_add'"
|
||||||
|
@click="showModel('menu', null)"
|
||||||
|
v-ripple
|
||||||
|
style="margin-left: 20px"
|
||||||
|
>
|
||||||
|
添加菜单
|
||||||
|
</ElButton>
|
||||||
|
<ElButton @click="toggleExpand" v-ripple>
|
||||||
|
{{ isExpanded ? '收起' : '展开' }}
|
||||||
|
</ElButton>
|
||||||
|
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
|
||||||
|
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
|
||||||
|
添加菜单
|
||||||
|
</ElButton> -->
|
||||||
|
</template>
|
||||||
|
</ArtTableHeader>
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
rowKey="path"
|
||||||
|
ref="tableRef"
|
||||||
|
:loading="loading"
|
||||||
|
:data="filteredTableData"
|
||||||
|
:marginTop="10"
|
||||||
|
:stripe="false"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
|
||||||
|
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
|
||||||
|
<ElRow :gutter="20">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="资源类型" prop="type">
|
||||||
|
<el-select v-model="form.type">
|
||||||
|
<el-option label="目录" value="directory" />
|
||||||
|
<el-option label="菜单" value="menu" />
|
||||||
|
<el-option label="权限" value="permission" />
|
||||||
|
</el-select>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="资源名称" prop="name">
|
||||||
|
<ElInput v-model="form.name" placeholder="资源名称"></ElInput>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow :gutter="20">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="权限标识" prop="code">
|
||||||
|
<ElInput v-model="form.code" placeholder="权限标识"></ElInput>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="图标" prop="icon">
|
||||||
|
<ArtIconSelector v-model="form.icon" :iconType="iconType" width="100%" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||||
|
<ElCol :span="12" v-if="form.type !== 'permission'">
|
||||||
|
<ElFormItem label="路由地址" prop="url">
|
||||||
|
<ElInput v-model="form.url" placeholder="路由地址"></ElInput>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="打开方式" prop="open_type">
|
||||||
|
<ElSelect v-model="form.open_type" class="account-select">
|
||||||
|
<el-option label="内部" value="internal" />
|
||||||
|
<el-option label="外部" value="external" />
|
||||||
|
<el-option label="嵌入" value="iframe" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="资源排序" prop="sort" style="width: 100%">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="form.sort"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleChange"
|
||||||
|
:min="1"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="外部链接" prop="link">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.link"
|
||||||
|
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
|
||||||
|
></ElInput>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow :gutter="20">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="系统" prop="system_id">
|
||||||
|
<ElSelect v-model="form.system_id" class="account-select">
|
||||||
|
<ElOption
|
||||||
|
v-for="account in systemsList"
|
||||||
|
:key="account.id"
|
||||||
|
:label="account.name"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
<span>{{ account.name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="父级" prop="parent_id">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.parent_id"
|
||||||
|
:data="parentList"
|
||||||
|
:props="{
|
||||||
|
value: 'id',
|
||||||
|
label: 'name',
|
||||||
|
children: 'children'
|
||||||
|
}"
|
||||||
|
check-strictly
|
||||||
|
:render-after-expand="false"
|
||||||
|
style="width: 240px"
|
||||||
|
/>
|
||||||
|
<!-- <ElSelect v-model="form.parent_id" class="account-select">
|
||||||
|
<ElOption
|
||||||
|
v-for="account in parentList"
|
||||||
|
:key="account.id"
|
||||||
|
:label="account.name"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
<span>{{ account.name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect> -->
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||||
|
<ElCol :span="5">
|
||||||
|
<ElFormItem label="是否启用" prop="status">
|
||||||
|
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="5">
|
||||||
|
<ElFormItem label="页面缓存" prop="keep_alive">
|
||||||
|
<ElSwitch
|
||||||
|
v-model="form.keep_alive"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="5">
|
||||||
|
<ElFormItem label="是否隐藏" prop="is_hide">
|
||||||
|
<ElSwitch v-model="form.is_hide" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="5">
|
||||||
|
<ElFormItem label="标签页是否隐藏" prop="is_hide_tab" label-width="120px">
|
||||||
|
<ElSwitch
|
||||||
|
v-model="form.is_hide_tab"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<!-- <ElCol :span="5">
|
||||||
|
<ElFormItem label="是否内嵌" prop="isMenu">
|
||||||
|
<ElSwitch v-model="form.isIframe"></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol> -->
|
||||||
|
</ElRow>
|
||||||
|
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||||
|
<ElCol :span="5">
|
||||||
|
<ElFormItem label="是否首页" prop="is_home">
|
||||||
|
<ElSwitch v-model="form.is_home" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="10">
|
||||||
|
<ElFormItem label="不需要认证" prop="no_auth" label-width="120px">
|
||||||
|
<ElSwitch v-model="form.no_auth" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||||
|
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</ArtTableFullScreen>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { IconTypeEnum } from '@/enums/appEnum'
|
||||||
|
import { formatMenuTitle } from '@/router/utils/utils'
|
||||||
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { ElPopover, ElButton } from 'element-plus'
|
||||||
|
import { AppRouteRecord } from '@/types/router'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { SearchFormItem } from '@/types'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { UserService } from '@/api/usersApi'
|
||||||
|
const yearVal = ref<any>('')
|
||||||
|
defineOptions({ name: 'Menus' })
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
|
// const { menuList } = storeToRefs(useMenuStore())
|
||||||
|
const menuList = ref<any[]>([])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const systemsListtab = ref<any>([])
|
||||||
|
const segmentedOptions = computed(() =>
|
||||||
|
Array.isArray(systemsListtab.value)
|
||||||
|
? systemsListtab.value.map((item: any) => ({ label: item.name, value: item.id }))
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
const getsystemsListtab = async () => {
|
||||||
|
const response = await UserService.getsystemsList()
|
||||||
|
const list = (response as any)?.data?.records ?? (response as any)?.records ?? response ?? []
|
||||||
|
systemsListtab.value = Array.isArray(list) ? list : []
|
||||||
|
if (!yearVal.value && systemsListtab.value.length > 0) {
|
||||||
|
yearVal.value = systemsListtab.value[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//系统
|
||||||
|
const systemsList = ref<any>()
|
||||||
|
const getsystemsList = async () => {
|
||||||
|
const response = await UserService.getsystemsList()
|
||||||
|
systemsList.value = response || []
|
||||||
|
}
|
||||||
|
//父级
|
||||||
|
const parentList = ref<any>()
|
||||||
|
const getparentList = async () => {
|
||||||
|
const response = await systemService.getMenuList()
|
||||||
|
parentList.value = response || []
|
||||||
|
}
|
||||||
|
// 定义表单搜索初始值
|
||||||
|
const initialSearchState = {
|
||||||
|
name: '',
|
||||||
|
route: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式表单数据
|
||||||
|
const formFilters = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 增加实际应用的搜索条件状态
|
||||||
|
const appliedFilters = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const handleReset = () => {
|
||||||
|
Object.assign(formFilters, { ...initialSearchState })
|
||||||
|
Object.assign(appliedFilters, { ...initialSearchState })
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
// 将当前输入的筛选条件应用到实际搜索
|
||||||
|
Object.assign(appliedFilters, { ...formFilters })
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单配置项
|
||||||
|
const formItems: SearchFormItem[] = [
|
||||||
|
{
|
||||||
|
label: '菜单名称',
|
||||||
|
prop: 'name',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '路由地址',
|
||||||
|
prop: 'route',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 构建菜单类型标签
|
||||||
|
const buildMenuTypeTag = (row: AppRouteRecord) => {
|
||||||
|
if (row.children && row.children.length > 0) {
|
||||||
|
return 'info'
|
||||||
|
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||||
|
return 'success'
|
||||||
|
} else if (row.path) {
|
||||||
|
return 'primary'
|
||||||
|
} else if (row.meta?.link) {
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建菜单类型文本
|
||||||
|
const buildMenuTypeText = (row: AppRouteRecord) => {
|
||||||
|
if (row.children && row.children.length > 0) {
|
||||||
|
return '目录'
|
||||||
|
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||||
|
return '内嵌'
|
||||||
|
} else if (row.path) {
|
||||||
|
return '菜单'
|
||||||
|
} else if (row.meta?.link) {
|
||||||
|
return '外链'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态列配置
|
||||||
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
|
{
|
||||||
|
prop: 'meta.title',
|
||||||
|
label: '菜单名称',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: (row: AppRouteRecord) => {
|
||||||
|
return formatMenuTitle(row.meta?.title)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'type',
|
||||||
|
label: '菜单类型',
|
||||||
|
formatter: (row: AppRouteRecord) => {
|
||||||
|
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'path',
|
||||||
|
label: '路由',
|
||||||
|
formatter: (row: AppRouteRecord) => {
|
||||||
|
return row.meta?.link || row.path || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'meta.authList',
|
||||||
|
label: '可操作权限',
|
||||||
|
formatter: (row: AppRouteRecord) => {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
|
||||||
|
return h(
|
||||||
|
ElPopover,
|
||||||
|
{
|
||||||
|
placement: 'top-start',
|
||||||
|
title: '操作',
|
||||||
|
width: 200,
|
||||||
|
trigger: 'click',
|
||||||
|
key: index
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () =>
|
||||||
|
h('div', { style: 'margin: 0; text-align: right' }, [
|
||||||
|
h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
type: 'primary',
|
||||||
|
onClick: () => showModel('button', item, false, row)
|
||||||
|
},
|
||||||
|
{ default: () => '编辑' }
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
type: 'danger',
|
||||||
|
onClick: () => deleteAuth(item)
|
||||||
|
},
|
||||||
|
{ default: () => '删除' }
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'date',
|
||||||
|
label: '编辑时间',
|
||||||
|
formatter: () => '2022-3-12 12:00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '隐藏菜单',
|
||||||
|
formatter: (row) => {
|
||||||
|
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
|
||||||
|
row.meta.isHide ? '是' : '否'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: AppRouteRecord) => {
|
||||||
|
return h('div', [
|
||||||
|
// 后端模式权限标识
|
||||||
|
hasAuth('system@menu_add') &&
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
type: 'add',
|
||||||
|
onClick: () => showModel('menu', row, true)
|
||||||
|
}),
|
||||||
|
hasAuth('system@menu_update') &&
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
type: 'edit',
|
||||||
|
onClick: () => showDialog('edit', row)
|
||||||
|
}),
|
||||||
|
hasAuth('system@menu_destroy') &&
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
type: 'delete',
|
||||||
|
onClick: () => deleteMenu(row)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
// 菜单
|
||||||
|
system_id: '',
|
||||||
|
type: 'menu', // 资源类型
|
||||||
|
parent_id: '',
|
||||||
|
name: '',
|
||||||
|
url: '',
|
||||||
|
code: '',
|
||||||
|
icon: '',
|
||||||
|
status: 1,
|
||||||
|
sort: 1,
|
||||||
|
is_home: 0,
|
||||||
|
keep_alive: 1,
|
||||||
|
is_hide: 0,
|
||||||
|
is_hide_tab: 0,
|
||||||
|
no_auth: 0,
|
||||||
|
link: '',
|
||||||
|
open_type: 'internal'
|
||||||
|
})
|
||||||
|
const iconType = ref(IconTypeEnum.UNICODE)
|
||||||
|
|
||||||
|
// const labelPosition = ref('menu')
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
system_id: [{ required: true, message: '请选择系统', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
|
||||||
|
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
const tableData = ref<AppRouteRecord[]>([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getsystemsListtab()
|
||||||
|
getTableData()
|
||||||
|
})
|
||||||
|
const searchArticleByYear = () => {
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
// const handleSegmentedClick = (item: any) => {
|
||||||
|
// // 若点击的是当前项也强制刷新
|
||||||
|
// yearVal.value = item?.value ?? yearVal.value
|
||||||
|
// getTableData()
|
||||||
|
// }
|
||||||
|
const getTableData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
// 清空旧数据,避免视觉上出现追加
|
||||||
|
menuList.value = []
|
||||||
|
tableData.value = []
|
||||||
|
// 并发安全:本次请求标识
|
||||||
|
requestCounter += 1
|
||||||
|
const reqId = requestCounter
|
||||||
|
const params: any = {
|
||||||
|
system_id: yearVal.value || '1'
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await systemService.getMenutreeList(params)
|
||||||
|
// 若在等待过程中又发起了新请求,则丢弃本次结果
|
||||||
|
if (reqId !== requestCounter) return
|
||||||
|
menuList.value = Array.isArray(res) ? res : []
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
menuList.value = []
|
||||||
|
}
|
||||||
|
// console.log(res)
|
||||||
|
// menuList.value = data
|
||||||
|
|
||||||
|
// 基于 appliedFilters 在这里进行过滤,并将结果赋值给 tableData
|
||||||
|
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||||
|
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||||
|
|
||||||
|
const filterMenus = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||||
|
return items.reduce((acc: AppRouteRecord[], item: AppRouteRecord) => {
|
||||||
|
// 浅拷贝节点,避免直接修改原数据
|
||||||
|
const copy: AppRouteRecord = { ...item }
|
||||||
|
if (copy.children && copy.children.length > 0) {
|
||||||
|
copy.children = filterMenus(copy.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuTitle = formatMenuTitle(copy.meta?.title || '').toLowerCase()
|
||||||
|
const menuPath = (copy.path || '').toLowerCase()
|
||||||
|
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||||
|
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||||
|
const hasChildren = Array.isArray(copy.children) && copy.children.length > 0
|
||||||
|
|
||||||
|
if (hasChildren || (nameMatch && routeMatch)) {
|
||||||
|
acc.push(copy)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData.value = filterMenus(menuList.value)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤后的表格数据改为来源于 getTableData 的结果
|
||||||
|
const filteredTableData = computed(() => tableData.value)
|
||||||
|
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
// const type = labelPosition.value === 'menu' ? '菜单' : '权限'
|
||||||
|
return isEdit.value ? `编辑资源` : `新建资源`
|
||||||
|
})
|
||||||
|
|
||||||
|
const showDialog = (type: string, row: AppRouteRecord) => {
|
||||||
|
showModel('menu', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = () => {}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
const payload = { ...form, id: itemid.value }
|
||||||
|
await systemService.menuEdit(payload as any)
|
||||||
|
} else {
|
||||||
|
await systemService.menuAdd(form)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 刷新表格数据
|
||||||
|
getTableData()
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(`${isEdit.value ? '编辑' : '新增'}失败`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const itemid = ref('')
|
||||||
|
const showModel = (type: string, row?: any, padd: boolean = false, data?: any) => {
|
||||||
|
getsystemsList()
|
||||||
|
getparentList()
|
||||||
|
dialogVisible.value = true
|
||||||
|
// labelPosition.value = type
|
||||||
|
isEdit.value = false
|
||||||
|
// lockMenuType.value = lock
|
||||||
|
resetForm()
|
||||||
|
itemid.value = ''
|
||||||
|
if (data) {
|
||||||
|
isEdit.value = true
|
||||||
|
form.type = 'permission'
|
||||||
|
form.system_id = data.meta.systemId
|
||||||
|
form.parent_id = data.id
|
||||||
|
form.name = row.title
|
||||||
|
form.code = row.authMark
|
||||||
|
itemid.value = row.id
|
||||||
|
} else if (padd) {
|
||||||
|
nextTick(() => {
|
||||||
|
form.parent_id = row.id
|
||||||
|
form.system_id = row.meta.systemId
|
||||||
|
})
|
||||||
|
} else if (row && !padd) {
|
||||||
|
isEdit.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
// 回显数据
|
||||||
|
itemid.value = row.id
|
||||||
|
form.system_id = row.meta.systemId
|
||||||
|
form.parent_id = row.meta.parentId == 0 ? '' : row.meta.parentId
|
||||||
|
form.name = row.name
|
||||||
|
form.type = row.meta.type
|
||||||
|
form.url = row.path
|
||||||
|
form.code = row.meta.code
|
||||||
|
form.icon = row.meta.icon
|
||||||
|
form.sort = row.meta.sort
|
||||||
|
form.is_home = row.meta.isHome
|
||||||
|
form.keep_alive = row.meta.keepAlive
|
||||||
|
form.is_hide = row.meta.isHide
|
||||||
|
form.is_hide_tab = row.meta.isHideTab
|
||||||
|
form.no_auth = row.meta.noAuth
|
||||||
|
form.status = row.meta.status
|
||||||
|
form.link = row.meta.link
|
||||||
|
form.open_type = row.meta.openType
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
Object.assign(form, {
|
||||||
|
// 菜单
|
||||||
|
system_id: '',
|
||||||
|
type: 'menu', // 资源类型
|
||||||
|
parent_id: '',
|
||||||
|
name: '',
|
||||||
|
url: '',
|
||||||
|
code: '',
|
||||||
|
icon: '',
|
||||||
|
status: 1,
|
||||||
|
sort: 1,
|
||||||
|
is_home: 0,
|
||||||
|
keep_alive: 1,
|
||||||
|
is_hide: 0,
|
||||||
|
is_hide_tab: 0,
|
||||||
|
no_auth: 0,
|
||||||
|
link: '',
|
||||||
|
open_type: 'internal'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMenu = async (item: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
const res = await systemService.menuDelete({ id: item.id })
|
||||||
|
ElMessage.success(res?.message || '删除成功!')
|
||||||
|
// 刷新表格数据
|
||||||
|
getTableData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAuth = async (item: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
const res = await systemService.menuDelete({ id: item.id })
|
||||||
|
ElMessage.success(res?.message || '删除成功!')
|
||||||
|
// 刷新表格数据
|
||||||
|
getTableData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 添加一个控制变量
|
||||||
|
// const lockMenuType = ref(false)
|
||||||
|
|
||||||
|
const isExpanded = ref(false)
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
|
// 数据请求并发保护计数器
|
||||||
|
let requestCounter = 0
|
||||||
|
|
||||||
|
const toggleExpand = () => {
|
||||||
|
isExpanded.value = !isExpanded.value
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRef.value) {
|
||||||
|
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.menu-page {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.small-btn) {
|
||||||
|
height: 22px !important;
|
||||||
|
padding: 0 5px !important;
|
||||||
|
font-size: 10px !important;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
:deep(.el-dialog__footer) {
|
||||||
|
z-index: inherit;
|
||||||
|
.el-button > span {
|
||||||
|
z-index: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-content">
|
||||||
|
<ElRow>
|
||||||
|
<ElCol :xs="24" :sm="12" :lg="6">
|
||||||
|
<ElInput placeholder="角色名称"></ElInput>
|
||||||
|
</ElCol>
|
||||||
|
<div style="width: 12px"></div>
|
||||||
|
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||||
|
<ElButton v-ripple>搜索</ElButton>
|
||||||
|
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')">新增角色</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="roleList"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn label="角色名称" prop="name" />
|
||||||
|
<ElTableColumn label="描述" prop="desc" />
|
||||||
|
<ElTableColumn label="启用" prop="enable">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="scope.row.status == 1 ? 'primary' : 'info'">
|
||||||
|
{{ scope.row.status == 1 ? '启用' : '禁用' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="创建时间" prop="created_at"></ElTableColumn>
|
||||||
|
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElRow>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
@click="showPermissionDialog(scope.row)"
|
||||||
|
v-if="hasAuth('system@role_perm')"
|
||||||
|
>
|
||||||
|
菜单权限
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
link
|
||||||
|
@click="showDialog('edit', scope.row)"
|
||||||
|
v-if="hasAuth('system@role_update')"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="deleteRole(scope.row)"
|
||||||
|
v-if="hasAuth('system@role_destroy')"
|
||||||
|
>删除
|
||||||
|
</el-button>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
|
||||||
|
<ElDialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||||
|
width="30%"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<ElFormItem label="角色名称" prop="name">
|
||||||
|
<ElInput v-model="form.name" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="描述" prop="desc">
|
||||||
|
<ElInput v-model="form.desc" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="排序" prop="sort">
|
||||||
|
<ElInput type="number" v-model="form.sort" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="启用">
|
||||||
|
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<ElDialog
|
||||||
|
v-model="permissionDialog"
|
||||||
|
title="菜单权限"
|
||||||
|
width="520px"
|
||||||
|
align-center
|
||||||
|
class="el-dialog-border"
|
||||||
|
>
|
||||||
|
<ElScrollbar height="70vh">
|
||||||
|
<ElTree
|
||||||
|
ref="treeRef"
|
||||||
|
:data="processedMenuList"
|
||||||
|
show-checkbox
|
||||||
|
node-key="id"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:props="defaultProps"
|
||||||
|
@check="handleTreeCheck"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div style="display: flex; align-items: center">
|
||||||
|
<span v-if="data.isAuth">
|
||||||
|
{{ data.label }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ defaultProps.label(data) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
</ElScrollbar>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||||||
|
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||||||
|
isSelectAll ? '取消全选' : '全部选择'
|
||||||
|
}}</ElButton>
|
||||||
|
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { formatMenuTitle } from '@/router/utils/utils'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
|
||||||
|
// import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||||||
|
const loading = ref(false)
|
||||||
|
defineOptions({ name: 'Role' })
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const permissionDialog = ref(false)
|
||||||
|
// 权限树数据(来自接口 getPermissionMenuList)
|
||||||
|
const permissionMenuList = ref<any[]>([])
|
||||||
|
const treeRef = ref()
|
||||||
|
const isExpandAll = ref(true)
|
||||||
|
const isSelectAll = ref(false)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||||
|
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive<any>({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
desc: '',
|
||||||
|
sort: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleList = ref<any[]>([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getTableData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// const getTableData = () => {
|
||||||
|
// roleList.value = ROLE_LIST_DATA
|
||||||
|
// }
|
||||||
|
// 获取列表数据
|
||||||
|
const getTableData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
|
||||||
|
const { records, current, size, total } = await systemService.getRoleList({
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
})
|
||||||
|
roleList.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
const dialogType = ref('add')
|
||||||
|
|
||||||
|
const showDialog = (type: string, row?: any) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogType.value = type
|
||||||
|
|
||||||
|
if (type === 'edit' && row) {
|
||||||
|
form.id = row.id
|
||||||
|
form.name = row.name
|
||||||
|
form.desc = row.desc
|
||||||
|
form.sort = row.sort
|
||||||
|
form.status = row.status
|
||||||
|
} else {
|
||||||
|
form.name = ''
|
||||||
|
form.desc = ''
|
||||||
|
form.sort = 0
|
||||||
|
form.status = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const roleId = ref('')
|
||||||
|
const showPermissionDialog = async (row: any) => {
|
||||||
|
permissionDialog.value = true
|
||||||
|
roleId.value = row.id
|
||||||
|
try {
|
||||||
|
const res = await systemService.getPermissionMenuList({ role_id: row.id })
|
||||||
|
// 兼容不同返回结构(data/records/直接数组)
|
||||||
|
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
|
||||||
|
permissionMenuList.value = Array.isArray(list) ? list : []
|
||||||
|
// 根据接口返回的 selected 字段设置默认选中
|
||||||
|
nextTick(() => {
|
||||||
|
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
|
||||||
|
treeRef.value?.setCheckedKeys(keys)
|
||||||
|
handleTreeCheck()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
permissionMenuList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 构建默认选中的 key 列表(与 node-key="id" 一致)
|
||||||
|
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
|
||||||
|
const keys: Array<string | number> = []
|
||||||
|
const walk = (node: any) => {
|
||||||
|
if (node?.meta?.selected) {
|
||||||
|
keys.push(node.id)
|
||||||
|
}
|
||||||
|
if (Array.isArray(node?.meta?.authList)) {
|
||||||
|
node.meta.authList.forEach((auth: any) => {
|
||||||
|
if (auth?.selected) {
|
||||||
|
keys.push(`${node.id}__auth__${auth.authMark}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Array.isArray(node?.children)) node.children.forEach(walk)
|
||||||
|
}
|
||||||
|
source.forEach(walk)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
// 处理菜单数据,将 authList 转换为子节点
|
||||||
|
const processedMenuList = computed(() => {
|
||||||
|
const processNode = (node: any) => {
|
||||||
|
// 规范化 id,确保树节点唯一键与 node-key 对应
|
||||||
|
const processed: any = { ...node, id: node.id }
|
||||||
|
|
||||||
|
// 如果有 authList,将其转换为子节点
|
||||||
|
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||||
|
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||||
|
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
|
||||||
|
id: `${node.id}__auth__${auth.authMark}`,
|
||||||
|
name: `${node.name}_${auth.authMark}`,
|
||||||
|
label: auth.title,
|
||||||
|
auth_mark: auth.authMark,
|
||||||
|
auth_id: auth.id,
|
||||||
|
isAuth: true,
|
||||||
|
checked: auth.selected || false
|
||||||
|
}))
|
||||||
|
|
||||||
|
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子节点
|
||||||
|
if (processed.children) {
|
||||||
|
processed.children = processed.children.map(processNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = permissionMenuList.value || []
|
||||||
|
return source.map(processNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRole = async (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const res = await systemService.roleDelete({ id: row.id })
|
||||||
|
ElMessage.success(res?.message || '删除成功!')
|
||||||
|
getTableData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
// 新增角色逻辑
|
||||||
|
const res = await systemService.roleAdd(form)
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
getTableData()
|
||||||
|
} else {
|
||||||
|
// 编辑角色逻辑
|
||||||
|
const res = await systemService.roleEdit(form)
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
formEl.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectedIds = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return { menuIds: [], authIds: [] }
|
||||||
|
|
||||||
|
const checkedNodes = tree.getCheckedNodes?.(false) || []
|
||||||
|
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
|
||||||
|
const allNodes = [...checkedNodes, ...halfCheckedNodes]
|
||||||
|
|
||||||
|
const menuIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
allNodes
|
||||||
|
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
|
||||||
|
.map((n: any) => n.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const authIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
|
||||||
|
return { menuIds, authIds, ids }
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePermission = async () => {
|
||||||
|
const { ids } = getSelectedIds()
|
||||||
|
// console.log('选中的菜单ID:', menuIds)
|
||||||
|
// console.log('选中的权限ID:', authIds)
|
||||||
|
// console.log('合并后的全部ID:', ids)
|
||||||
|
|
||||||
|
const res = await systemService.roleAuth({
|
||||||
|
role_id: roleId.value,
|
||||||
|
resources: ids
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.message || '权限保存成功')
|
||||||
|
permissionDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleExpandAll = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return
|
||||||
|
|
||||||
|
// 使用store.nodesMap直接控制所有节点的展开状态
|
||||||
|
const nodes = tree.store.nodesMap
|
||||||
|
for (const node in nodes) {
|
||||||
|
nodes[node].expanded = !isExpandAll.value
|
||||||
|
}
|
||||||
|
|
||||||
|
isExpandAll.value = !isExpandAll.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return
|
||||||
|
|
||||||
|
if (!isSelectAll.value) {
|
||||||
|
// 全选:获取所有节点的key并设置为选中
|
||||||
|
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||||
|
tree.setCheckedKeys(allKeys)
|
||||||
|
} else {
|
||||||
|
// 取消全选:清空所有选中
|
||||||
|
tree.setCheckedKeys([])
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectAll.value = !isSelectAll.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllNodeKeys = (nodes: any[]): Array<string | number> => {
|
||||||
|
const keys: Array<string | number> = []
|
||||||
|
const traverse = (nodeList: any[]) => {
|
||||||
|
nodeList.forEach((node) => {
|
||||||
|
if (node.id !== undefined && node.id !== null) {
|
||||||
|
keys.push(node.id)
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
traverse(node.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
traverse(nodes)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTreeCheck = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return
|
||||||
|
|
||||||
|
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||||
|
const checkedKeys = tree.getCheckedKeys()
|
||||||
|
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||||
|
|
||||||
|
// 判断是否全选:选中的节点数量等于总节点数量
|
||||||
|
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-content {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-content">
|
||||||
|
<div v-if="!isMobile">
|
||||||
|
<ElRow>
|
||||||
|
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
||||||
|
<ElInput placeholder="名称"></ElInput>
|
||||||
|
</ElCol> -->
|
||||||
|
<div style="width: 12px"></div>
|
||||||
|
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||||
|
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
||||||
|
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@systems_add')">新增</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn label="ID" prop="id" width="80" />
|
||||||
|
<ElTableColumn label="系统编码" prop="code" />
|
||||||
|
<ElTableColumn label="系统名称" prop="name" />
|
||||||
|
<ElTableColumn label="系统类型" prop="type">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="getTypeTagType(scope.row.type)">
|
||||||
|
{{ getTypeText(scope.row.type) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="系统描述" prop="desc" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="域名URL" prop="domain_url" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="区分角色" prop="is_role">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="scope.row.is_role ? 'success' : 'info'">
|
||||||
|
{{ scope.row.is_role ? '是' : '否' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<!-- <ElTableColumn label="排序" prop="sort" width="80" /> -->
|
||||||
|
<ElTableColumn label="状态" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||||
|
{{ scope.row.status ? '启用' : '禁用' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="创建时间" prop="created_at" />
|
||||||
|
<ElTableColumn fixed="right" label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElRow>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
@click="showDialog('edit', scope.row)"
|
||||||
|
v-if="hasAuth('system@systems_update')"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="deleteRole(scope.row)"
|
||||||
|
v-if="hasAuth('system@systems_destroy')"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
<ElDialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||||
|
width="60%"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
|
||||||
|
<ElFormItem label="系统编码" prop="code">
|
||||||
|
<ElInput v-model="form.code" placeholder="请输入系统编码" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="系统名称" prop="name">
|
||||||
|
<ElInput v-model="form.name" placeholder="请输入系统名称" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="系统类型" prop="type">
|
||||||
|
<ElSelect v-model="form.type" placeholder="请选择系统类型" style="width: 100%">
|
||||||
|
<ElOption label="管理后台" value="admin" />
|
||||||
|
<ElOption label="Web端" value="web" />
|
||||||
|
<ElOption label="小程序" value="miniapp" />
|
||||||
|
<ElOption label="工具" value="tool" />
|
||||||
|
<ElOption label="其他" value="other" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="系统描述" prop="desc">
|
||||||
|
<ElInput v-model="form.desc" type="textarea" :rows="3" placeholder="请输入系统描述" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="系统图标" prop="icon">
|
||||||
|
<ElUpload
|
||||||
|
v-model:file-list="iconFileList"
|
||||||
|
:show-file-list="false"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:action="actionImageUplode"
|
||||||
|
:before-upload="beforeIconUpload"
|
||||||
|
:on-success="handleIconSuccess"
|
||||||
|
:on-error="handleIconError"
|
||||||
|
accept="image/*"
|
||||||
|
class="icon-uploader"
|
||||||
|
>
|
||||||
|
<div v-if="form.icon" class="icon-preview">
|
||||||
|
<img :src="file_url" alt="系统图标" class="icon-image" />
|
||||||
|
<div class="icon-overlay">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
|
||||||
|
</ElUpload>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="域名URL" prop="domain_url">
|
||||||
|
<ElInput v-model="form.domain_url" placeholder="请输入域名URL" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="排序" prop="sort">
|
||||||
|
<ElInputNumber v-model="form.sort" :min="0" placeholder="请输入排序" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="是否区分角色权限" prop="is_role">
|
||||||
|
<ElSwitch v-model="form.is_role" :active-value="1" :inactive-value="0" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="状态" prop="status">
|
||||||
|
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mobile-hide-tip">
|
||||||
|
<p>该页面暂不支持移动端访问</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
defineOptions({ name: 'Type ' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogType = ref<'add' | 'edit'>('add')
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const iconFileList = ref<any[]>([])
|
||||||
|
const typeList = ref<any[]>([])
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const isMobile = computed(() => width.value < 500)
|
||||||
|
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||||
|
const { uploadHeaders } = useUploadHeaders()
|
||||||
|
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
code: [{ required: true, message: '请输入系统编码', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '请输入系统名称', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '请选择系统类型', trigger: 'change' }]
|
||||||
|
// desc: [
|
||||||
|
// { required: true, message: '请输入系统描述', trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// icon: [
|
||||||
|
// { required: true, message: '请输入系统图标', trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// domain_url: [
|
||||||
|
// { required: true, message: '请输入域名URL', trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// is_role: [
|
||||||
|
// { required: true, message: '请选择是否区分角色权限', trigger: 'change' }
|
||||||
|
// ],
|
||||||
|
// sort: [
|
||||||
|
// { required: true, message: '请输入排序', trigger: 'blur' }
|
||||||
|
// ],
|
||||||
|
// status: [
|
||||||
|
// { required: true, message: '请选择状态', trigger: 'change' }
|
||||||
|
// ]
|
||||||
|
})
|
||||||
|
const file_url = ref('')
|
||||||
|
const form = reactive({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
desc: '',
|
||||||
|
icon: '',
|
||||||
|
domain_url: '',
|
||||||
|
is_role: false,
|
||||||
|
sort: 0,
|
||||||
|
status: true,
|
||||||
|
id: ''
|
||||||
|
})
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
|
||||||
|
// 表格实例引用
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取列表数据
|
||||||
|
const getWorkList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
|
||||||
|
const { records, current, size, total } = await systemService.getSystemList({
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
})
|
||||||
|
tableData.value = records.map((item: any, index: number) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统类型相关辅助函数
|
||||||
|
const getTypeText = (type: string) => {
|
||||||
|
const typeMap: { [key: string]: string } = {
|
||||||
|
'admin': '管理后台',
|
||||||
|
'web': 'Web端',
|
||||||
|
'miniapp': '小程序',
|
||||||
|
'tool': '工具',
|
||||||
|
'other': '其他'
|
||||||
|
}
|
||||||
|
return typeMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeTagType = (type: string) => {
|
||||||
|
const typeMap: { [key: string]: 'primary' | 'success' | 'warning' | 'info' | 'danger' } = {
|
||||||
|
'admin': 'danger',
|
||||||
|
'web': 'primary',
|
||||||
|
'miniapp': 'success',
|
||||||
|
'tool': 'warning',
|
||||||
|
'other': 'info'
|
||||||
|
}
|
||||||
|
return typeMap[type] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDialog = (type: string, row?: any) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogType.value = type as 'add' | 'edit'
|
||||||
|
|
||||||
|
if (type === 'edit' && row) {
|
||||||
|
file_url.value = row.icon
|
||||||
|
// 编辑模式,填充所有字段
|
||||||
|
Object.assign(form, {
|
||||||
|
code: row.code || '',
|
||||||
|
name: row.name || '',
|
||||||
|
type: row.type || 'web',
|
||||||
|
desc: row.desc || '',
|
||||||
|
icon: row.icon || row.icon_url || '', // 优先使用 icon,其次 icon_url
|
||||||
|
domain_url: row.domain_url || '',
|
||||||
|
is_role: row.is_role !== undefined ? row.is_role : 0,
|
||||||
|
sort: row.sort || 0,
|
||||||
|
status: row.status !== undefined ? row.status : 1,
|
||||||
|
id: row.id || '' // 编辑时填充文件名,如果没有则使用 icon
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
file_url.value = ''
|
||||||
|
// 新增模式,重置所有字段
|
||||||
|
Object.assign(form, {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
type: 'web',
|
||||||
|
desc: '',
|
||||||
|
icon: '',
|
||||||
|
domain_url: '',
|
||||||
|
is_role: 0,
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
id: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deleteRole = (row?: any) => {
|
||||||
|
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await systemService.systemDelete({
|
||||||
|
id: row.id,
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
getWorkList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
await formEl.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
const { message } = await systemService.systemAdd({
|
||||||
|
code: form.code,
|
||||||
|
name: form.name,
|
||||||
|
type: form.type,
|
||||||
|
desc: form.desc,
|
||||||
|
icon: form.icon,
|
||||||
|
domain_url: form.domain_url,
|
||||||
|
is_role: form.is_role,
|
||||||
|
sort: form.sort,
|
||||||
|
status: form.status
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
} else {
|
||||||
|
const { message } = await systemService.systemEdit({
|
||||||
|
id: form.id,
|
||||||
|
code: form.code,
|
||||||
|
name: form.name,
|
||||||
|
type: form.type,
|
||||||
|
desc: form.desc,
|
||||||
|
icon: form.icon,
|
||||||
|
domain_url: form.domain_url,
|
||||||
|
is_role: form.is_role,
|
||||||
|
sort: form.sort,
|
||||||
|
status: form.status
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
getWorkList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图标上传相关函数
|
||||||
|
const beforeIconUpload = (file: File) => {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isLt2M) {
|
||||||
|
ElMessage.error('图片大小不能超过 2MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIconSuccess = (response: any, file: any) => {
|
||||||
|
// 接口返回 file_url 用于回显,file_name 用于传参
|
||||||
|
if (response.data) {
|
||||||
|
form.icon = response.data.file_url || response.data.file_name // 回显用 file_url
|
||||||
|
file_url.value = response.data.file_url
|
||||||
|
} else {
|
||||||
|
form.icon = response.file_url || response.file_name
|
||||||
|
file_url.value = response.file_url
|
||||||
|
}
|
||||||
|
ElMessage.success('图标上传成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIconError = () => {
|
||||||
|
ElMessage.error('图标上传失败')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
.svg-icon {
|
||||||
|
width: 1.8em;
|
||||||
|
height: 1.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -8px;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-table {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-uploader {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
line-height: 64px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-uploader-icon:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-overlay .el-icon {
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-preview:hover .icon-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,860 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" label-width="50px">
|
||||||
|
<el-select v-model="selectValue" style="min-width: 120px">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="正常" value="1" />
|
||||||
|
<el-option label="离职" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<!-- <el-table-column prop="id" label="用户ID" align="center" /> -->
|
||||||
|
<el-table-column prop="sn" label="SN" align="center" />
|
||||||
|
<el-table-column prop="follow" label="头像" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<img
|
||||||
|
:src="scope.row.avatar"
|
||||||
|
alt="头像"
|
||||||
|
class="avatar"
|
||||||
|
@click="handleAvatarClick(scope.row.avatar)"
|
||||||
|
style="cursor: pointer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="名字" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
|
<el-table-column prop="corp_name" label="所属公司" align="center" />
|
||||||
|
<el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.status)">
|
||||||
|
{{ scope.row.status_txt }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="customer_count" label="客户数量" align="center" />
|
||||||
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="showPermissionDialog(scope.row)"
|
||||||
|
v-if="hasAuth('user@direct_perm')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i>直接授权
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="warning"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="showroleDialog(scope.row)"
|
||||||
|
v-if="hasAuth('user@role_system')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i>角色&系统
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="gotransfer(scope.row)"
|
||||||
|
v-if="hasAuth('user@customer_transfer')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>客户转移
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onEdit(scope.row)"
|
||||||
|
v-if="hasAuth('user@edit')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>编辑
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情" width="75%">
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" @click="plzyDialogVisiblefn">
|
||||||
|
<i class="iconfont-sys"></i> 批量转移
|
||||||
|
</el-button>
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loadingkh"
|
||||||
|
:data="tableDatakh"
|
||||||
|
:currentPage="paginationkh.currentPage"
|
||||||
|
:pageSize="paginationkh.pageSize"
|
||||||
|
:total="paginationkh.total"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChangekh"
|
||||||
|
@current-change="handleCurrentChangekh"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||||
|
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||||
|
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||||
|
<el-table-column prop="country_name" label="国家" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
|
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||||
|
<el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.follow)">
|
||||||
|
{{ scope.row.follow_txt }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 批量转移对话框 -->
|
||||||
|
<el-dialog v-model="plzyDialogVisible" title="批量转移" width="360px">
|
||||||
|
<el-form :model="plzyForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select
|
||||||
|
v-model="plzyForm.user_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择转移人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="plzyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
title="编辑用户"
|
||||||
|
v-model="AddDialogVisible"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="addForm" ref="addFormRef" label-width="80px" class="add-form">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="用户名" prop="country">
|
||||||
|
<el-input v-model="addForm.name" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="Email" prop="email">
|
||||||
|
<el-input v-model="addForm.email" placeholder="请输入Email" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="电话" prop="tel">
|
||||||
|
<el-input v-model="addForm.phone" placeholder="请输入电话" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="上级" prop="report_to">
|
||||||
|
<el-select
|
||||||
|
v-model="addForm.report_to"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择上级"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="addForm.status">
|
||||||
|
<el-option label="正常" :value="1" />
|
||||||
|
<el-option label="离职" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div class="form-submit-row">
|
||||||
|
<el-button type="primary" size="large" style="width: 50%" @click="handleAddSubmit"
|
||||||
|
>提交</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 图片预览组件 -->
|
||||||
|
<ElImageViewer
|
||||||
|
v-if="showImageViewer"
|
||||||
|
:url-list="[currentImageUrl]"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
@close="showImageViewer = false"
|
||||||
|
/>
|
||||||
|
<ElDialog
|
||||||
|
v-model="permissionDialog"
|
||||||
|
title="菜单权限"
|
||||||
|
width="520px"
|
||||||
|
align-center
|
||||||
|
class="el-dialog-border"
|
||||||
|
>
|
||||||
|
<ElScrollbar height="70vh">
|
||||||
|
<ElTree
|
||||||
|
ref="treeRef"
|
||||||
|
:data="processedMenuList"
|
||||||
|
show-checkbox
|
||||||
|
node-key="id"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:props="defaultProps"
|
||||||
|
@check="handleTreeCheck"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div style="display: flex; align-items: center">
|
||||||
|
<span v-if="data.isAuth">
|
||||||
|
{{ data.label }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ defaultProps.label(data) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
</ElScrollbar>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||||||
|
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||||||
|
isSelectAll ? '取消全选' : '全部选择'
|
||||||
|
}}</ElButton>
|
||||||
|
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
<el-dialog
|
||||||
|
title="角色&系统设置"
|
||||||
|
v-model="roleDialogVisible"
|
||||||
|
width="700px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="roleForm" ref="addFormRef" label-width="80px" class="add-form">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="角色" prop="status">
|
||||||
|
<ElSelect
|
||||||
|
v-model="roleForm.role_ids"
|
||||||
|
multiple
|
||||||
|
class="account-select"
|
||||||
|
placeholder="请选择角色"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="account in roleOptions"
|
||||||
|
:key="account.id"
|
||||||
|
:label="account.name"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
<span>{{ account.name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="系统" prop="status">
|
||||||
|
<ElSelect
|
||||||
|
v-model="roleForm.system_ids"
|
||||||
|
multiple
|
||||||
|
class="account-select"
|
||||||
|
placeholder="请选择系统"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="account in systemsList"
|
||||||
|
:key="account.id"
|
||||||
|
:label="account.name"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
<span>{{ account.name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div class="form-submit-row">
|
||||||
|
<el-button type="primary" size="large" style="width: 70%" @click="handleRoleSubmit"
|
||||||
|
>提交</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { ElImageViewer } from 'element-plus'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { systemService } from '@/api/systemApi'
|
||||||
|
import { formatMenuTitle } from '@/router/utils/utils'
|
||||||
|
import { UserService } from '@/api/usersApi'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const selectValue = ref<string[]>([])
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingkh = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const plzyDialogVisible = ref(false)
|
||||||
|
const AddDialogVisible = ref(false)
|
||||||
|
const permissionDialog = ref(false)
|
||||||
|
const roleDialogVisible = ref(false)
|
||||||
|
// 权限树数据(来自接口 getPermissionMenuList)
|
||||||
|
const permissionMenuList = ref<any[]>([])
|
||||||
|
const treeRef = ref()
|
||||||
|
const isExpandAll = ref(true)
|
||||||
|
const isSelectAll = ref(false)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const paginationkh = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const plzyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
const zyForm = reactive({
|
||||||
|
customer_id: ''
|
||||||
|
})
|
||||||
|
const addForm = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
report_to: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
const roleForm = reactive({
|
||||||
|
user_id: '',
|
||||||
|
role_ids: '',
|
||||||
|
system_ids: ''
|
||||||
|
})
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
//系统
|
||||||
|
const systemsList = ref<any>()
|
||||||
|
const getsystemsList = async () => {
|
||||||
|
const response = await UserService.getsystemsList()
|
||||||
|
systemsList.value = response || []
|
||||||
|
}
|
||||||
|
//角色下拉框
|
||||||
|
const roleOptions = ref<any>()
|
||||||
|
const getroleOptions = async () => {
|
||||||
|
const response = await systemService.getRoleSelectList()
|
||||||
|
roleOptions.value = response || []
|
||||||
|
}
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
const tableDatakh = ref<any[]>([])
|
||||||
|
onMounted(() => {
|
||||||
|
getuserList()
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
// tabs 切换联动
|
||||||
|
watch([selectValue, keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
}
|
||||||
|
if (selectValue.value) {
|
||||||
|
params.status = selectValue.value
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
const { records, current, size, total } = await manageService.getUserList(params)
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gettableListkh = async (user_id: any) => {
|
||||||
|
loadingkh.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = paginationkh
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
user_id: user_id
|
||||||
|
}
|
||||||
|
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||||
|
tableDatakh.value = data.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(paginationkh, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loadingkh.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChangekh = (newPageSize: number) => {
|
||||||
|
paginationkh.pageSize = newPageSize
|
||||||
|
gettableListkh(zyForm.customer_id)
|
||||||
|
}
|
||||||
|
const handleCurrentChangekh = (newCurrentPage: number) => {
|
||||||
|
paginationkh.currentPage = newCurrentPage
|
||||||
|
gettableListkh(zyForm.customer_id)
|
||||||
|
}
|
||||||
|
function statusClass(status: any) {
|
||||||
|
if (status === 1) return 'status-following'
|
||||||
|
if (status === 0) return 'status-lost'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const gotransfer = (row: any) => {
|
||||||
|
paginationkh.currentPage = 1
|
||||||
|
paginationkh.pageSize = 10
|
||||||
|
paginationkh.total = 0
|
||||||
|
gettableListkh(row.id)
|
||||||
|
zyForm.customer_id = row.id
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
// 显示角色&系统对话框
|
||||||
|
const showroleDialog = (row: any) => {
|
||||||
|
getsystemsList()
|
||||||
|
getroleOptions()
|
||||||
|
roleDialogVisible.value = true
|
||||||
|
roleForm.user_id = row.id
|
||||||
|
roleForm.role_ids = row.role_ids || []
|
||||||
|
roleForm.system_ids = row.system_ids || []
|
||||||
|
}
|
||||||
|
// 提交角色&系统
|
||||||
|
const handleRoleSubmit = async () => {
|
||||||
|
const res = await manageService.userSetRole(roleForm)
|
||||||
|
ElMessage.success(res?.message || '角色&系统设置成功!')
|
||||||
|
roleDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
// 显示权限对话框
|
||||||
|
const userId = ref('')
|
||||||
|
const showPermissionDialog = async (row: any) => {
|
||||||
|
permissionDialog.value = true
|
||||||
|
userId.value = row.id
|
||||||
|
try {
|
||||||
|
const res = await systemService.getPermissionMenuList({ user_id: row.id })
|
||||||
|
// 兼容不同返回结构(data/records/直接数组)
|
||||||
|
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
|
||||||
|
permissionMenuList.value = Array.isArray(list) ? list : []
|
||||||
|
// 根据接口返回的 selected 字段设置默认选中
|
||||||
|
nextTick(() => {
|
||||||
|
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
|
||||||
|
treeRef.value?.setCheckedKeys(keys)
|
||||||
|
handleTreeCheck()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
permissionMenuList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 构建默认选中的 key 列表(与 node-key="id" 一致)
|
||||||
|
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
|
||||||
|
const keys: Array<string | number> = []
|
||||||
|
const walk = (node: any) => {
|
||||||
|
if (node?.meta?.selected) {
|
||||||
|
keys.push(node.id)
|
||||||
|
}
|
||||||
|
if (Array.isArray(node?.meta?.authList)) {
|
||||||
|
node.meta.authList.forEach((auth: any) => {
|
||||||
|
if (auth?.selected) {
|
||||||
|
keys.push(`${node.id}__auth__${auth.authMark}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Array.isArray(node?.children)) node.children.forEach(walk)
|
||||||
|
}
|
||||||
|
source.forEach(walk)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
// 处理菜单数据,将 authList 转换为子节点
|
||||||
|
const processedMenuList = computed(() => {
|
||||||
|
const processNode = (node: any) => {
|
||||||
|
// 规范化 id,确保树节点唯一键与 node-key 对应
|
||||||
|
const processed: any = { ...node, id: node.id }
|
||||||
|
|
||||||
|
// 如果有 authList,将其转换为子节点
|
||||||
|
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||||
|
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||||
|
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
|
||||||
|
id: `${node.id}__auth__${auth.authMark}`,
|
||||||
|
name: `${node.name}_${auth.authMark}`,
|
||||||
|
label: auth.title,
|
||||||
|
auth_mark: auth.authMark,
|
||||||
|
auth_id: auth.id,
|
||||||
|
isAuth: true,
|
||||||
|
checked: auth.selected || false
|
||||||
|
}))
|
||||||
|
|
||||||
|
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子节点
|
||||||
|
if (processed.children) {
|
||||||
|
processed.children = processed.children.map(processNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = permissionMenuList.value || []
|
||||||
|
return source.map(processNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||||||
|
}
|
||||||
|
const toggleExpandAll = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return
|
||||||
|
|
||||||
|
// 使用store.nodesMap直接控制所有节点的展开状态
|
||||||
|
const nodes = tree.store.nodesMap
|
||||||
|
for (const node in nodes) {
|
||||||
|
nodes[node].expanded = !isExpandAll.value
|
||||||
|
}
|
||||||
|
|
||||||
|
isExpandAll.value = !isExpandAll.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return
|
||||||
|
|
||||||
|
if (!isSelectAll.value) {
|
||||||
|
// 全选:获取所有节点的key并设置为选中
|
||||||
|
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||||
|
tree.setCheckedKeys(allKeys)
|
||||||
|
} else {
|
||||||
|
// 取消全选:清空所有选中
|
||||||
|
tree.setCheckedKeys([])
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectAll.value = !isSelectAll.value
|
||||||
|
}
|
||||||
|
const getAllNodeKeys = (nodes: any[]): Array<string | number> => {
|
||||||
|
const keys: Array<string | number> = []
|
||||||
|
const traverse = (nodeList: any[]) => {
|
||||||
|
nodeList.forEach((node) => {
|
||||||
|
if (node.id !== undefined && node.id !== null) {
|
||||||
|
keys.push(node.id)
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
traverse(node.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
traverse(nodes)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
const handleTreeCheck = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return
|
||||||
|
|
||||||
|
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||||
|
const checkedKeys = tree.getCheckedKeys()
|
||||||
|
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||||
|
|
||||||
|
// 判断是否全选:选中的节点数量等于总节点数量
|
||||||
|
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||||
|
}
|
||||||
|
const getSelectedIds = () => {
|
||||||
|
const tree = treeRef.value
|
||||||
|
if (!tree) return { menuIds: [], authIds: [] }
|
||||||
|
|
||||||
|
const checkedNodes = tree.getCheckedNodes?.(false) || []
|
||||||
|
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
|
||||||
|
const allNodes = [...checkedNodes, ...halfCheckedNodes]
|
||||||
|
|
||||||
|
const menuIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
allNodes
|
||||||
|
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
|
||||||
|
.map((n: any) => n.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const authIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
|
||||||
|
return { menuIds, authIds, ids }
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePermission = async () => {
|
||||||
|
const { ids } = getSelectedIds()
|
||||||
|
const res = await manageService.userMenuAuth({
|
||||||
|
user_id: userId.value,
|
||||||
|
resources: ids
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.message || '权限保存成功')
|
||||||
|
permissionDialog.value = false
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
const plzyDialogVisiblefn = () => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plzyDialogVisible.value = true
|
||||||
|
}
|
||||||
|
// 批量转移
|
||||||
|
const handleplzySubmit = async () => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
|
||||||
|
const res = await manageService.crmCustomerTransfer(plzyForm)
|
||||||
|
ElMessage.success(res?.message || '批量转移成功!')
|
||||||
|
plzyDialogVisible.value = false
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
// 编辑用户
|
||||||
|
const onEdit = (row: any) => {
|
||||||
|
AddDialogVisible.value = true
|
||||||
|
addForm.id = row.id
|
||||||
|
addForm.name = row.name
|
||||||
|
addForm.email = row.email
|
||||||
|
addForm.phone = row.phone
|
||||||
|
addForm.status = row.status
|
||||||
|
addForm.report_to = row.report_to || ''
|
||||||
|
}
|
||||||
|
const handleAddSubmit = async () => {
|
||||||
|
const res = await manageService.userEdit(addForm)
|
||||||
|
ElMessage.success(res?.message || '编辑成功!')
|
||||||
|
AddDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览相关
|
||||||
|
const showImageViewer = ref(false)
|
||||||
|
const currentImageUrl = ref('')
|
||||||
|
|
||||||
|
const handleAvatarClick = (avatarUrl: string) => {
|
||||||
|
if (avatarUrl) {
|
||||||
|
currentImageUrl.value = avatarUrl
|
||||||
|
showImageViewer.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 450px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tags {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
width: 120px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-following {
|
||||||
|
color: #00a43f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dealed {
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-table {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.form-submit-row {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-content user">
|
||||||
|
<div class="content">
|
||||||
|
<div class="left-wrap">
|
||||||
|
<div class="user-wrap box-style">
|
||||||
|
<!-- <img class="bg" src="@imgs/user/bg.webp" /> -->
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
:src="userInfo.avatar != '' ? userInfo.avatar : '/src/assets/img/user/avatar.webp'"
|
||||||
|
/>
|
||||||
|
<h2 class="name">{{ userInfo.name }}</h2>
|
||||||
|
<p class="des">SN:{{ userInfo.sn }}</p>
|
||||||
|
<div class="outer-info">
|
||||||
|
<!-- <div class="info-row">
|
||||||
|
<span class="info-label">昵 称:</span>
|
||||||
|
<span class="info-value">{{ userInfo.phone }}</span>
|
||||||
|
</div> -->
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">电 话:</span>
|
||||||
|
<span class="info-value">{{ userInfo.phone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">邮 箱:</span>
|
||||||
|
<span class="info-value">{{ userInfo.email }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">公 司:</span>
|
||||||
|
<span class="info-value">{{ userInfo.corp_name }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="info-row">
|
||||||
|
<span class="info-label">国 家:</span>
|
||||||
|
<span class="info-value">{{ userInfo.country_txt }}</span>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-wrap">
|
||||||
|
<div class="info box-style">
|
||||||
|
<h1 class="title"><span class="border" style="margin-right: 5px"></span>基本信息</h1>
|
||||||
|
|
||||||
|
<ElForm
|
||||||
|
:model="form"
|
||||||
|
class="form"
|
||||||
|
ref="ruleFormRef"
|
||||||
|
label-width="86px"
|
||||||
|
label-position="left"
|
||||||
|
>
|
||||||
|
<ElFormItem label="头像" prop="avatar">
|
||||||
|
<el-upload
|
||||||
|
class="qrcode-upload"
|
||||||
|
:action="actionImageUplode"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleUploadavatar"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
>
|
||||||
|
<div class="qrcode-upload-box">
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
:src="
|
||||||
|
imageUrlavatar != '' ? imageUrlavatar : '/src/assets/img/user/avatar.webp'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElRow>
|
||||||
|
<ElFormItem label="昵称" prop="name">
|
||||||
|
<el-input v-model="form.name" :disabled="!isEdit" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="性别" prop="sex" class="right-input">
|
||||||
|
<ElSelect v-model="form.sex" placeholder="未知" :disabled="!isEdit">
|
||||||
|
<ElOption
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<ElRow>
|
||||||
|
<ElFormItem label="联系方式" prop="phone">
|
||||||
|
<ElInput v-model="form.phone" disabled />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="公司" prop="company" class="right-input">
|
||||||
|
<ElInput v-model="form.corp_name" disabled />
|
||||||
|
</ElFormItem>
|
||||||
|
<!-- <ElFormItem label="职务" prop="position" class="right-input">
|
||||||
|
<ElInput v-model="form.position" :disabled="!isEdit" />
|
||||||
|
</ElFormItem> -->
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<!-- <ElRow>
|
||||||
|
<ElFormItem label="公司" prop="company">
|
||||||
|
<ElInput v-model="form.corp_name" disabled />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="国家" prop="address" class="right-input">
|
||||||
|
<el-select
|
||||||
|
v-model="form.country_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择国家"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in countryOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.cn_addr"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElRow> -->
|
||||||
|
<ElRow>
|
||||||
|
<ElFormItem label="邮箱" prop="email">
|
||||||
|
<ElInput v-model="form.email" :disabled="!isEdit" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElRow>
|
||||||
|
<!-- <ElRow>
|
||||||
|
<ElFormItem
|
||||||
|
label="二维码"
|
||||||
|
prop="socialize"
|
||||||
|
:class="isEdit ? 'qrcode-radio-group' : ''"
|
||||||
|
>
|
||||||
|
<el-radio-group v-model="form.socialize" v-if="isEdit">
|
||||||
|
<el-radio label="微信">微信</el-radio>
|
||||||
|
<el-radio label="WhatsApp">WhatsApp</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<template v-else>
|
||||||
|
<img
|
||||||
|
v-if="form.socialize_pic_full"
|
||||||
|
:src="form.socialize_pic_full"
|
||||||
|
class="qrcode-img"
|
||||||
|
/>
|
||||||
|
<span v-else class="qrcode-upload-text">暂无二维码</span>
|
||||||
|
</template>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem v-if="isEdit" label="上传二维码" prop="address" class="right-input">
|
||||||
|
<el-upload
|
||||||
|
class="qrcode-upload"
|
||||||
|
:action="actionImageUplode"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleUploadavatarqrcode"
|
||||||
|
>
|
||||||
|
<div class="qrcode-upload-box">
|
||||||
|
<img
|
||||||
|
v-if="form.socialize_pic_full"
|
||||||
|
:src="socialize_pic_full"
|
||||||
|
class="qrcode-img"
|
||||||
|
/>
|
||||||
|
<span v-else class="qrcode-upload-text">
|
||||||
|
<img :src="imageUrls" class="qrcode-img" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElRow> -->
|
||||||
|
|
||||||
|
<div class="el-form-item-right">
|
||||||
|
<ElButton type="primary" style="width: 90px" v-ripple @click="edit">
|
||||||
|
<i v-if="!isEdit" class="iconfont-sys"> </i>
|
||||||
|
{{ isEdit ? '保存' : '编辑' }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info box-style" style="margin-top: 20px">
|
||||||
|
<h1 class="title">更改密码</h1>
|
||||||
|
|
||||||
|
<ElForm :model="pwdForm" class="form" label-width="86px" label-position="left">
|
||||||
|
<ElFormItem label="当前密码" prop="password">
|
||||||
|
<ElInput
|
||||||
|
v-model="pwdForm.password"
|
||||||
|
type="password"
|
||||||
|
:disabled="!isEditPwd"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="新密码" prop="password_new">
|
||||||
|
<ElInput
|
||||||
|
v-model="pwdForm.password_new"
|
||||||
|
type="password"
|
||||||
|
:disabled="!isEditPwd"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="确认新密码" prop="confirmPassword">
|
||||||
|
<ElInput
|
||||||
|
v-model="pwdForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
:disabled="!isEditPwd"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<div class="el-form-item-right">
|
||||||
|
<ElButton type="primary" style="width: 90px" v-ripple @click="editPwd">
|
||||||
|
<i v-if="!isEditPwd" class="iconfont-sys"> </i>
|
||||||
|
{{ isEditPwd ? '保存' : '编辑' }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElForm, FormInstance } from 'element-plus'
|
||||||
|
// import imageUrl from '@/assets/img/crm/upimg.png'
|
||||||
|
import { CrmService } from '@/api/crmApi'
|
||||||
|
import { UserService } from '@/api/usersApi'
|
||||||
|
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
const { uploadHeaders } = useUploadHeaders()
|
||||||
|
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
|
||||||
|
defineOptions({ name: 'UserCenter' })
|
||||||
|
// 定义客户详情类型
|
||||||
|
interface CustomerInfo {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
// const imageUrls = ref(imageUrl)
|
||||||
|
const imageUrlavatar = ref('')
|
||||||
|
// const socialize_pic_full = ref('')
|
||||||
|
// const countryOptions = ref<any>()
|
||||||
|
const form = ref<CustomerInfo>({})
|
||||||
|
//获取公司 getCompanyList
|
||||||
|
// const companyList = ref<any>()
|
||||||
|
//国家
|
||||||
|
// const getarea = async () => {
|
||||||
|
// try {
|
||||||
|
// // 这里调用获取品牌列表的接口
|
||||||
|
// const res = await CrmService.getCountry()
|
||||||
|
// countryOptions.value = res||[]
|
||||||
|
// } catch (error) { countryOptions.value = [] }
|
||||||
|
// }
|
||||||
|
const { getUserInfo } = useUserStore()
|
||||||
|
const userInfo = ref<CustomerInfo>({})
|
||||||
|
const getuserInfo = async () => {
|
||||||
|
// const data = await CrmService.userget()
|
||||||
|
userInfo.value = getUserInfo.user
|
||||||
|
form.value = getUserInfo.user
|
||||||
|
imageUrlavatar.value = getUserInfo.user.avatar
|
||||||
|
// socialize_pic_full.value = data.socialize_pic_full
|
||||||
|
}
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const isEditPwd = ref(false)
|
||||||
|
const pwdForm = reactive({
|
||||||
|
password: '',
|
||||||
|
password_new: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const ruleFormRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: '未知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: '男'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: '女'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
onMounted(async () => {
|
||||||
|
// await getarea()
|
||||||
|
await getuserInfo()
|
||||||
|
})
|
||||||
|
const edit = async () => {
|
||||||
|
if (isEdit.value) {
|
||||||
|
const res = await UserService.editUserInfo(form.value)
|
||||||
|
ElMessage.success(res?.message || '操作成功')
|
||||||
|
getuserInfo()
|
||||||
|
}
|
||||||
|
isEdit.value = !isEdit.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const editPwd = async () => {
|
||||||
|
if (isEditPwd.value) {
|
||||||
|
// 判断新密码和确认密码是否一致
|
||||||
|
if (pwdForm.password_new !== pwdForm.confirmPassword) {
|
||||||
|
ElMessage.error('新密码和确认密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await CrmService.savePassword(pwdForm)
|
||||||
|
ElMessage.success(res?.message || '操作成功')
|
||||||
|
}
|
||||||
|
isEditPwd.value = !isEditPwd.value
|
||||||
|
}
|
||||||
|
//头像上传
|
||||||
|
function handleUploadavatar(response: any) {
|
||||||
|
// 假设返回图片地址字段为 response.url 或 response.data.url
|
||||||
|
imageUrlavatar.value = response.url || (response.data && response.data.file_url) || ''
|
||||||
|
// 如需同步到 form.picture
|
||||||
|
form.value.avatar = response.url || (response.data && response.data.file_name) || ''
|
||||||
|
}
|
||||||
|
//二维码上传
|
||||||
|
// function handleUploadavatarqrcode(response: any) {
|
||||||
|
// // 假设返回图片地址字段为 response.url 或 response.data.url
|
||||||
|
// socialize_pic_full.value = response.url || (response.data && response.data.file_url) || ''
|
||||||
|
// imageUrls.value = response.url || (response.data && response.data.file_url) || ''
|
||||||
|
// // 如需同步到 form.picture
|
||||||
|
// form.value.socialize_pic = response.url || (response.data && response.data.file_name) || ''
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user {
|
||||||
|
.icon {
|
||||||
|
width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.qrcode-radio-group .el-form-item__content){
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.qrcode-img{
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
$box-radius: calc(var(--custom-radius) + 4px);
|
||||||
|
|
||||||
|
.box-style {
|
||||||
|
border: 1px solid var(--art-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.left-wrap {
|
||||||
|
width: 350px;
|
||||||
|
margin-right: 25px;
|
||||||
|
|
||||||
|
.user-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 400px;
|
||||||
|
padding: 35px 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
// background: var(--art-main-bg-color);
|
||||||
|
background: linear-gradient(180deg, #E9F0FF 0%, #FFFFFF 67.9%);
|
||||||
|
|
||||||
|
border-radius: $box-radius;
|
||||||
|
.avatar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
// margin-top: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.des {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outer-info {
|
||||||
|
// width: 260px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
|
color: #222;
|
||||||
|
text-align: right;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
color: #333;
|
||||||
|
margin-left: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lables {
|
||||||
|
margin-top: 40px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: 3px 6px;
|
||||||
|
margin: 0 10px 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border: 1px solid var(--art-border-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
margin-top: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-wrap {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: $box-radius;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: $box-radius;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 15px 25px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--art-text-gray-800);
|
||||||
|
// border-bottom: 1px solid var(--art-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 30px 25px;
|
||||||
|
|
||||||
|
> .el-row {
|
||||||
|
.el-form-item {
|
||||||
|
width: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input,
|
||||||
|
.el-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-input {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 110px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $device-ipad-vertical) {
|
||||||
|
.page-content {
|
||||||
|
.content {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.left-wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-wrap {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,7 +35,10 @@ onMounted(async () => {
|
|||||||
const userInfo = await UserService.getUserInfo()
|
const userInfo = await UserService.getUserInfo()
|
||||||
userStore.setUserInfo(userInfo)
|
userStore.setUserInfo(userInfo)
|
||||||
userStore.setLoginStatus(true)
|
userStore.setLoginStatus(true)
|
||||||
router.replace('/')
|
userStore.sethome_page(data.home_url)
|
||||||
|
localStorage.setItem('home_page', data.home_url)
|
||||||
|
router.push(data.home_url)
|
||||||
|
// router.replace('/')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.response?.data?.message || '登录失败,请重试'
|
error.value = e?.response?.data?.message || '登录失败,请重试'
|
||||||
router.replace('/')
|
router.replace('/')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{{ isDark ? '' : '' }}
|
{{ isDark ? '' : '' }}
|
||||||
</i>
|
</i>
|
||||||
</div> -->
|
</div> -->
|
||||||
<ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
<!-- <ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
||||||
<div class="btn language-btn">
|
<div class="btn language-btn">
|
||||||
<i class="iconfont-sys icon-language"></i>
|
<i class="iconfont-sys icon-language"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
</ElDropdown>
|
</ElDropdown> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<ArtLogo class="icon" />
|
<ArtLogo class="icon" />
|
||||||
@@ -36,8 +36,16 @@
|
|||||||
<div class="form">
|
<div class="form">
|
||||||
<!-- 登录方式切换 tab -->
|
<!-- 登录方式切换 tab -->
|
||||||
<div class="login-type-tabs">
|
<div class="login-type-tabs">
|
||||||
<span :class="{active: activeLoginType === 'password'}" @click="activeLoginType = 'password'">账号密码登录</span>
|
<!-- <span
|
||||||
<span :class="{active: activeLoginType === 'qrcode'}" @click="activeLoginType = 'qrcode'">飞书登录</span>
|
:class="{ active: activeLoginType === 'password' }"
|
||||||
|
@click="activeLoginType = 'password'"
|
||||||
|
>账号密码登录</span
|
||||||
|
> -->
|
||||||
|
<span
|
||||||
|
:class="{ active: activeLoginType === 'qrcode' }"
|
||||||
|
@click="activeLoginType = 'qrcode'"
|
||||||
|
>飞书登录</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeLoginType === 'password'">
|
<div v-if="activeLoginType === 'password'">
|
||||||
<ElForm
|
<ElForm
|
||||||
@@ -48,7 +56,10 @@
|
|||||||
style="margin-top: 25px"
|
style="margin-top: 25px"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="username" label="账号">
|
<ElFormItem prop="username" label="账号">
|
||||||
<ElInput :placeholder="$t('login.placeholder[0]')" v-model.trim="formData.username" />
|
<ElInput
|
||||||
|
:placeholder="$t('login.placeholder[0]')"
|
||||||
|
v-model.trim="formData.username"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="password" label="密码">
|
<ElFormItem prop="password" label="密码">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -72,6 +83,18 @@
|
|||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="company" label="系统">
|
||||||
|
<ElSelect v-model="formData.systems" class="account-select" @change="changesystems">
|
||||||
|
<ElOption
|
||||||
|
v-for="account in systemsList"
|
||||||
|
:key="account.id"
|
||||||
|
:label="account.name"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
<span>{{ account.name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
<div class="drag-verify">
|
<div class="drag-verify">
|
||||||
<div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
|
<div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
|
||||||
<ArtDragVerify
|
<ArtDragVerify
|
||||||
@@ -93,9 +116,13 @@
|
|||||||
<div class="login-bottom-row">
|
<div class="login-bottom-row">
|
||||||
<p>
|
<p>
|
||||||
{{ $t('login.noAccount') }}
|
{{ $t('login.noAccount') }}
|
||||||
<RouterLink :to="RoutesAlias.Register" style="color:#5D87FF;">{{ $t('login.register') }}</RouterLink>
|
<RouterLink :to="RoutesAlias.Register" style="color: #5d87ff">{{
|
||||||
|
$t('login.register')
|
||||||
|
}}</RouterLink>
|
||||||
</p>
|
</p>
|
||||||
<ElCheckbox v-model="formData.rememberPassword">{{ $t('login.rememberPwd') }}</ElCheckbox>
|
<ElCheckbox v-model="formData.rememberPassword">{{
|
||||||
|
$t('login.rememberPwd')
|
||||||
|
}}</ElCheckbox>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 30px">
|
<div style="margin-top: 30px">
|
||||||
<ElButton
|
<ElButton
|
||||||
@@ -111,10 +138,7 @@
|
|||||||
</ElForm>
|
</ElForm>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ElForm
|
<ElForm :model="formData" style="margin-top: 25px">
|
||||||
:model="formData"
|
|
||||||
style="margin-top: 25px"
|
|
||||||
>
|
|
||||||
<ElFormItem prop="company" label="公司">
|
<ElFormItem prop="company" label="公司">
|
||||||
<ElSelect v-model="formData.company" class="account-select" @change="changeCompany">
|
<ElSelect v-model="formData.company" class="account-select" @change="changeCompany">
|
||||||
<ElOption
|
<ElOption
|
||||||
@@ -127,6 +151,18 @@
|
|||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="company" label="系统">
|
||||||
|
<ElSelect v-model="formData.systems" class="account-select" @change="changesystems">
|
||||||
|
<ElOption
|
||||||
|
v-for="account in systemsList"
|
||||||
|
:key="account.id"
|
||||||
|
:label="account.name"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
|
<span>{{ account.name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<div class="qrcode-login" v-if="!isInFeishu">
|
<div class="qrcode-login" v-if="!isInFeishu">
|
||||||
<div id="login_container"></div>
|
<div id="login_container"></div>
|
||||||
@@ -160,7 +196,7 @@
|
|||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const { isDark, systemThemeType } = storeToRefs(settingStore)
|
const { systemThemeType } = storeToRefs(settingStore)
|
||||||
|
|
||||||
const dragVerify = ref()
|
const dragVerify = ref()
|
||||||
|
|
||||||
@@ -169,25 +205,29 @@
|
|||||||
const isPassing = ref(false)
|
const isPassing = ref(false)
|
||||||
const isClickPass = ref(false)
|
const isClickPass = ref(false)
|
||||||
const activeLoginType = ref<'password' | 'qrcode'>('qrcode')
|
const activeLoginType = ref<'password' | 'qrcode'>('qrcode')
|
||||||
const isInFeishu = /Lark|Feishu/i.test(navigator.userAgent);
|
const isInFeishu = /Lark|Feishu/i.test(navigator.userAgent)
|
||||||
// const isInFeishu = true;
|
// const isInFeishu = true;
|
||||||
const systemName = AppConfig.systemInfo.name
|
const systemName = AppConfig.systemInfo.name
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const companyid = ref('')
|
const companyid = ref('')
|
||||||
companyid.value = useUserStore().companyid
|
companyid.value = useUserStore().companyid
|
||||||
|
const systemsid = ref('')
|
||||||
|
systemsid.value = useUserStore().systemsid
|
||||||
const iscompany = ref(false)
|
const iscompany = ref(false)
|
||||||
const { companyabbr } = useUserStore()
|
const { companyabbr, systemsabbr } = useUserStore()
|
||||||
//获取公司 getCompanyList
|
//获取公司 getCompanyList
|
||||||
const companyList = ref<any>()
|
const companyList = ref<any>()
|
||||||
|
//系统
|
||||||
|
const systemsList = ref<any>()
|
||||||
const feishuresponse = ref()
|
const feishuresponse = ref()
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
account: '',
|
account: '',
|
||||||
|
systems: useUserStore().systemsid ? Number(useUserStore().systemsid) : '',
|
||||||
company: useUserStore().companyid ? Number(useUserStore().companyid) : '',
|
company: useUserStore().companyid ? Number(useUserStore().companyid) : '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
rememberPassword: true
|
rememberPassword: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = computed<FormRules>(() => ({
|
const rules = computed<FormRules>(() => ({
|
||||||
username: [{ required: true, message: t('login.placeholder[0]'), trigger: 'blur' }],
|
username: [{ required: true, message: t('login.placeholder[0]'), trigger: 'blur' }],
|
||||||
password: [{ required: true, message: t('login.placeholder[1]'), trigger: 'blur' }],
|
password: [{ required: true, message: t('login.placeholder[1]'), trigger: 'blur' }],
|
||||||
@@ -197,11 +237,12 @@
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
const getcompanyList = async () => {
|
const getcompanyList = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const response = await UserService.getCompanyList()
|
const response = await UserService.getCompanyList()
|
||||||
companyList.value = response || []
|
companyList.value = response || []
|
||||||
} catch (error) { }
|
}
|
||||||
|
const getsystemsList = async () => {
|
||||||
|
const response = await UserService.getsystemsList()
|
||||||
|
systemsList.value = response || []
|
||||||
}
|
}
|
||||||
// 切换语言
|
// 切换语言
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
@@ -213,26 +254,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getfeishuAppList = async () => {
|
const getfeishuAppList = async () => {
|
||||||
try {
|
feishuresponse.value = await UserService.getfeishuAppList({
|
||||||
// 这里调用获取品牌列表的接口
|
corp_id: useUserStore().companyid,
|
||||||
feishuresponse.value = await UserService.getfeishuAppList({ corp_id: companyid.value })
|
system_id: useUserStore().systemsid
|
||||||
|
})
|
||||||
if (activeLoginType.value == 'qrcode') {
|
if (activeLoginType.value == 'qrcode') {
|
||||||
feishulogin()
|
feishulogin()
|
||||||
}
|
}
|
||||||
console.log(feishuresponse)
|
}
|
||||||
} catch (error) { }
|
//切换系统
|
||||||
|
const changesystems = (systems: any) => {
|
||||||
|
const systemsdata = systemsList.value.find((c: any) => c.id === systems)
|
||||||
|
systemsid.value = systems
|
||||||
|
userStore.setSystems(systems, systemsdata?.code)
|
||||||
|
if (useUserStore().companyid && systemsid.value) {
|
||||||
|
getfeishuAppList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//切换公司
|
//切换公司
|
||||||
|
|
||||||
const changeCompany = (company: any) => {
|
const changeCompany = (company: any) => {
|
||||||
// if (companyid.value === company) return
|
// if (companyid.value === company) return
|
||||||
const companyt = companyList.value.find((c: any) => c.id === company)
|
const companyt = companyList.value.find((c: any) => c.id === company)
|
||||||
companyid.value = company
|
companyid.value = company
|
||||||
iscompany.value = company
|
iscompany.value = company
|
||||||
userStore.setCompany(company, companyt?.abbr)
|
userStore.setCompany(company, companyt?.abbr)
|
||||||
|
if (companyid.value && systemsid.value) {
|
||||||
getfeishuAppList()
|
getfeishuAppList()
|
||||||
}
|
}
|
||||||
if(companyabbr!=''){
|
}
|
||||||
|
if (companyabbr != '' && systemsabbr != '') {
|
||||||
iscompany.value = true
|
iscompany.value = true
|
||||||
getfeishuAppList()
|
getfeishuAppList()
|
||||||
}
|
}
|
||||||
@@ -249,7 +299,7 @@
|
|||||||
const waitForQRLogin = () =>
|
const waitForQRLogin = () =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
const check = () => {
|
const check = () => {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
if (window.QRLogin) resolve()
|
if (window.QRLogin) resolve()
|
||||||
else setTimeout(check, 100)
|
else setTimeout(check, 100)
|
||||||
}
|
}
|
||||||
@@ -271,7 +321,7 @@
|
|||||||
goto: feishuGotoUrl,
|
goto: feishuGotoUrl,
|
||||||
width: '250',
|
width: '250',
|
||||||
height: '250',
|
height: '250',
|
||||||
style: 'width:350px;height:350px;border:none'
|
style: 'width:350px;height:270px;border:none'
|
||||||
})
|
})
|
||||||
const handleMessage = function (event: MessageEvent) {
|
const handleMessage = function (event: MessageEvent) {
|
||||||
if (QRLoginObj.matchOrigin(event.origin) && QRLoginObj.matchData(event.data)) {
|
if (QRLoginObj.matchOrigin(event.origin) && QRLoginObj.matchData(event.data)) {
|
||||||
@@ -283,7 +333,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
watch(
|
watch(
|
||||||
(activeLoginType as import('vue').Ref<string>),
|
activeLoginType as import('vue').Ref<string>,
|
||||||
async (val) => {
|
async (val) => {
|
||||||
if (val === 'qrcode') {
|
if (val === 'qrcode') {
|
||||||
userStore.setLoginType('feishu')
|
userStore.setLoginType('feishu')
|
||||||
@@ -343,10 +393,11 @@
|
|||||||
const userInfo = await UserService.getUserInfo()
|
const userInfo = await UserService.getUserInfo()
|
||||||
userStore.setUserInfo(userInfo)
|
userStore.setUserInfo(userInfo)
|
||||||
userStore.setLoginStatus(true)
|
userStore.setLoginStatus(true)
|
||||||
|
userStore.sethome_page(data.home_url)
|
||||||
|
localStorage.setItem('home_page', data.home_url)
|
||||||
// 登录成功处理
|
// 登录成功处理
|
||||||
showLoginSuccessNotice()
|
showLoginSuccessNotice()
|
||||||
router.push(HOME_PAGE)
|
router.push(data.home_url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 处理 HttpError
|
// 处理 HttpError
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
@@ -383,16 +434,34 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getcompanyList()
|
getcompanyList()
|
||||||
|
getsystemsList()
|
||||||
// 自动读取 query 参数并存入 userStore
|
// 自动读取 query 参数并存入 userStore
|
||||||
const companyid = Array.isArray(route.query.corp_id) ? route.query.corp_id[0] : route.query.corp_id
|
const companyid = Array.isArray(route.query.corp_id)
|
||||||
const companyabbr = Array.isArray(route.query.corp_abbr) ? route.query.corp_abbr[0] : route.query.corp_abbr
|
? route.query.corp_id[0]
|
||||||
|
: route.query.corp_id
|
||||||
|
const companyabbr = Array.isArray(route.query.corp_abbr)
|
||||||
|
? route.query.corp_abbr[0]
|
||||||
|
: route.query.corp_abbr
|
||||||
|
// const system_id = Array.isArray(route.query.system_id)
|
||||||
|
// ? route.query.system_id[0]
|
||||||
|
// : route.query.system_id
|
||||||
|
// const system_code = Array.isArray(route.query.system_code)
|
||||||
|
// ? route.query.system_code[0]
|
||||||
|
// : route.query.system_code
|
||||||
if (companyid) {
|
if (companyid) {
|
||||||
userStore.setCompany(companyid as string, (companyabbr as string) || '')
|
userStore.setCompany(companyid as string, (companyabbr as string) || '')
|
||||||
formData.company = Number(companyid)
|
formData.company = Number(companyid)
|
||||||
}
|
}
|
||||||
|
// if (system_id) {
|
||||||
|
// userStore.setSystems(companyid as string, (system_code as string) || '')
|
||||||
|
// formData.systems = Number(system_id)
|
||||||
|
// }
|
||||||
|
// 只有两个 id 都有值时才调用
|
||||||
|
if (companyid && systemsid.value) {
|
||||||
|
getfeishuAppList()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 切换主题
|
// 切换主题
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { UserService } from '@/api/usersApi'
|
import { UserService } from '@/api/usersApi'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="customer-form-page">
|
<div class="customer-form-page">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<el-link :underline="false" @click="goBack">
|
<el-link underline="never" @click="goBack">
|
||||||
<i class="iconfont-sys icon-reverse"></i> 返回
|
<i class="iconfont-sys icon-reverse"></i> 返回
|
||||||
</el-link>
|
</el-link>
|
||||||
<span class="border"></span><span class="form-title">新增客户资料</span>
|
<span class="border"></span><span class="form-title">新增客户资料</span>
|
||||||
@@ -18,19 +18,29 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="客户渠道" prop="channel_id">
|
<el-form-item label="客户渠道" prop="channel_id">
|
||||||
<el-select v-model="form.channel_id" filterable placeholder="请选择客户渠道">
|
<el-select v-model="form.channel_id" filterable placeholder="请选择客户渠道">
|
||||||
<el-option v-for="item in channelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in channelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户商机" prop="magnitude">
|
<el-form-item label="客户商机" prop="magnitude">
|
||||||
<el-select v-model="form.magnitude" placeholder="请选择客户商机">
|
<el-select v-model="form.magnitude" placeholder="请选择客户商机">
|
||||||
<el-option v-for="item in opportunityOptions" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in opportunityOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户名称" prop="contacts">
|
<el-form-item label="客户名称" prop="contacts">
|
||||||
<el-input v-model="form.contacts" placeholder="请输入客户名称" />
|
<el-input v-model="form.contacts" placeholder="请输入客户名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户性别" prop="sex">
|
<el-form-item label="客户性别" prop="sex">
|
||||||
<el-radio-group v-model="form.sex" style="justify-content: flex-end;width: 100%;">
|
<el-radio-group v-model="form.sex">
|
||||||
<el-radio label="0">未知</el-radio>
|
<el-radio label="0">未知</el-radio>
|
||||||
<el-radio label="1">男</el-radio>
|
<el-radio label="1">男</el-radio>
|
||||||
<el-radio label="2">女</el-radio>
|
<el-radio label="2">女</el-radio>
|
||||||
@@ -52,16 +62,31 @@
|
|||||||
</el-form-item> -->
|
</el-form-item> -->
|
||||||
<el-form-item label="客户分级" prop="grade">
|
<el-form-item label="客户分级" prop="grade">
|
||||||
<el-select v-model="form.grade" placeholder="请选择客户分级">
|
<el-select v-model="form.grade" placeholder="请选择客户分级">
|
||||||
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in levelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="国家" prop="country_id">
|
<el-form-item label="国家" prop="country_id">
|
||||||
<el-select v-model="form.country_id" filterable placeholder="请选择国家">
|
<el-select v-model="form.country_id" filterable placeholder="请选择国家">
|
||||||
<el-option v-for="item in countryOptions" :key="item.id" :label="item.cn_addr" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in countryOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.cn_addr"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="公司" prop="company">
|
<el-form-item label="公司" prop="company">
|
||||||
<el-input v-model="form.company" placeholder="请输入公司" @click="showMapDialog = true" readonly />
|
<el-input
|
||||||
|
v-model="form.company"
|
||||||
|
placeholder="请输入公司"
|
||||||
|
@click="showMapDialog = true"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="describe">
|
<el-form-item label="备注" prop="describe">
|
||||||
<el-input v-model="form.describe" type="textarea" :rows="3" placeholder="请输入备注" />
|
<el-input v-model="form.describe" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||||
@@ -75,9 +100,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 谷歌地图选点弹窗 -->
|
<!-- 谷歌地图选点弹窗 -->
|
||||||
<el-dialog v-model="showMapDialog" title="选择公司地址" width="800px">
|
<el-dialog v-model="showMapDialog" title="选择公司地址" width="800px">
|
||||||
<div style="height:400px;position:relative;">
|
<div style="height: 400px; position: relative">
|
||||||
<!-- <input id="map-search" type="text" placeholder="搜索地点" style="position:absolute;z-index:2;top:10px;left:10px;width:300px;" /> -->
|
<!-- <input id="map-search" type="text" placeholder="搜索地点" style="position:absolute;z-index:2;top:10px;left:10px;width:300px;" /> -->
|
||||||
<div id="google-map" style="width:100%;height:100%;"></div>
|
<div id="google-map" style="width: 100%; height: 100%"></div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showMapDialog = false">取消</el-button>
|
<el-button @click="showMapDialog = false">取消</el-button>
|
||||||
@@ -110,13 +135,12 @@ const form = ref({
|
|||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
|
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
|
||||||
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
|
||||||
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
|
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelOptions = ref<any>()
|
const channelOptions = ref<any>()
|
||||||
const opportunityOptions = ref<any>()
|
const opportunityOptions = ref<any>()
|
||||||
const intentionOptions = ref<any>()
|
|
||||||
const levelOptions = ref<any>()
|
const levelOptions = ref<any>()
|
||||||
const countryOptions = ref<any>()
|
const countryOptions = ref<any>()
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -127,35 +151,23 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
//渠道
|
//渠道
|
||||||
const getgetChannel = async () => {
|
const getgetChannel = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getChannel()
|
const res = await CrmService.getChannel()
|
||||||
channelOptions.value = res || []
|
channelOptions.value = res || []
|
||||||
} catch (error) { channelOptions.value = [] }
|
|
||||||
}
|
}
|
||||||
//商机
|
//商机
|
||||||
const getMagnitude = async () => {
|
const getMagnitude = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getMagnitude()
|
const res = await CrmService.getMagnitude()
|
||||||
opportunityOptions.value = res || []
|
opportunityOptions.value = res || []
|
||||||
} catch (error) { opportunityOptions.value = [] }
|
|
||||||
}
|
}
|
||||||
//客户等级
|
//客户等级
|
||||||
const getGrade = async () => {
|
const getGrade = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getGrade()
|
const res = await CrmService.getGrade()
|
||||||
levelOptions.value = res || []
|
levelOptions.value = res || []
|
||||||
} catch (error) { levelOptions.value = [] }
|
|
||||||
}
|
}
|
||||||
//国家
|
//国家
|
||||||
const getarea = async () => {
|
const getarea = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getCountry()
|
const res = await CrmService.getCountry()
|
||||||
countryOptions.value = res || []
|
countryOptions.value = res || []
|
||||||
} catch (error) { countryOptions.value = [] }
|
|
||||||
}
|
}
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -188,11 +200,11 @@ function confirmMapSelect() {
|
|||||||
let map: any, marker: any, autocomplete: any
|
let map: any, marker: any, autocomplete: any
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
const google = (window as any).google;
|
const google = (window as any).google
|
||||||
const input = document.getElementById('map-search') as HTMLInputElement;
|
const input = document.getElementById('map-search') as HTMLInputElement
|
||||||
if (!input) {
|
if (!input) {
|
||||||
setTimeout(initMap, 100);
|
setTimeout(initMap, 100)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
map = new google.maps.Map(document.getElementById('google-map'), {
|
map = new google.maps.Map(document.getElementById('google-map'), {
|
||||||
center: { lat: 39.9042, lng: 116.4074 }, // 默认北京
|
center: { lat: 39.9042, lng: 116.4074 }, // 默认北京
|
||||||
@@ -225,7 +237,7 @@ function initMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddressByLatLng(latlng: any) {
|
function getAddressByLatLng(latlng: any) {
|
||||||
const google = (window as any).google;
|
const google = (window as any).google
|
||||||
const geocoder = new google.maps.Geocoder()
|
const geocoder = new google.maps.Geocoder()
|
||||||
geocoder.geocode({ location: latlng }, (results: any, status: any) => {
|
geocoder.geocode({ location: latlng }, (results: any, status: any) => {
|
||||||
if (status === 'OK' && results[0]) {
|
if (status === 'OK' && results[0]) {
|
||||||
@@ -242,7 +254,7 @@ watch(showMapDialog, (val) => {
|
|||||||
if (val) {
|
if (val) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const google = (window as any).google;
|
const google = (window as any).google
|
||||||
if (!google || !google.maps) {
|
if (!google || !google.maps) {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyCiQdUlA2QrEBEaZthoj7B18HTz6sxU_3I&libraries=places`
|
script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyCiQdUlA2QrEBEaZthoj7B18HTz6sxU_3I&libraries=places`
|
||||||
@@ -260,7 +272,6 @@ watch(showMapDialog, (val) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.border {
|
.border {
|
||||||
|
|
||||||
margin: 0px 10px 0px 40px;
|
margin: 0px 10px 0px 40px;
|
||||||
}
|
}
|
||||||
.submit-row .el-form-item__content {
|
.submit-row .el-form-item__content {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="customer-form-page">
|
<div class="customer-form-page">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<el-link :underline="false" @click="goBack">
|
<el-link underline="never" @click="goBack">
|
||||||
<i class="iconfont-sys icon-reverse"></i> 返回
|
<i class="iconfont-sys icon-reverse"></i> 返回
|
||||||
</el-link>
|
</el-link>
|
||||||
<span class="border"></span><span class="form-title">编辑客户资料</span>
|
<span class="border"></span><span class="form-title">编辑客户资料</span>
|
||||||
@@ -18,19 +18,29 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="客户渠道" prop="channel_id">
|
<el-form-item label="客户渠道" prop="channel_id">
|
||||||
<el-select v-model="form.channel_id" filterable placeholder="请选择客户渠道">
|
<el-select v-model="form.channel_id" filterable placeholder="请选择客户渠道">
|
||||||
<el-option v-for="item in channelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in channelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户商机" prop="magnitude">
|
<el-form-item label="客户商机" prop="magnitude">
|
||||||
<el-select v-model="form.magnitude" placeholder="请选择客户商机">
|
<el-select v-model="form.magnitude" placeholder="请选择客户商机">
|
||||||
<el-option v-for="item in opportunityOptions" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in opportunityOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户名称" prop="contacts">
|
<el-form-item label="客户名称" prop="contacts">
|
||||||
<el-input v-model="form.contacts" placeholder="请输入客户名称" />
|
<el-input v-model="form.contacts" placeholder="请输入客户名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户性别" prop="sex">
|
<el-form-item label="客户性别" prop="sex">
|
||||||
<el-radio-group v-model="form.sex" style="justify-content: flex-end;width: 100%;">
|
<el-radio-group v-model="form.sex">
|
||||||
<el-radio :label="0">未知</el-radio>
|
<el-radio :label="0">未知</el-radio>
|
||||||
<el-radio :label="1">男</el-radio>
|
<el-radio :label="1">男</el-radio>
|
||||||
<el-radio :label="2">女</el-radio>
|
<el-radio :label="2">女</el-radio>
|
||||||
@@ -52,12 +62,22 @@
|
|||||||
</el-form-item> -->
|
</el-form-item> -->
|
||||||
<el-form-item label="客户分级" prop="grade">
|
<el-form-item label="客户分级" prop="grade">
|
||||||
<el-select v-model="form.grade" placeholder="请选择客户分级">
|
<el-select v-model="form.grade" placeholder="请选择客户分级">
|
||||||
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in levelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="国家" prop="country_id">
|
<el-form-item label="国家" prop="country_id">
|
||||||
<el-select v-model="form.country_id" filterable placeholder="请选择国家">
|
<el-select v-model="form.country_id" filterable placeholder="请选择国家">
|
||||||
<el-option v-for="item in countryOptions" :key="item.id" :label="item.cn_addr" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in countryOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.cn_addr"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="公司" prop="company">
|
<el-form-item label="公司" prop="company">
|
||||||
@@ -85,16 +105,14 @@ const customerId = route.query.id
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
|
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
|
||||||
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
|
||||||
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
|
// car_type_id: [{ required: true, message: '请选择客户偏好', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelOptions = ref<any>()
|
const channelOptions = ref<any>()
|
||||||
const opportunityOptions = ref<any>()
|
const opportunityOptions = ref<any>()
|
||||||
const intentionOptions = ref<any>()
|
|
||||||
const levelOptions = ref<any>()
|
const levelOptions = ref<any>()
|
||||||
const countryOptions = ref<any>()
|
const countryOptions = ref<any>()
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -117,9 +135,14 @@ const getdetilfn = async() => {
|
|||||||
try {
|
try {
|
||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const res = await CrmService.getcustomerdetail({ id: customerId })
|
const res = await CrmService.getcustomerdetail({ id: customerId })
|
||||||
|
res.channel_id = res.channel_id==0 ? '' : res.channel_id
|
||||||
|
res.country_id = res.country_id==0 ? '' : res.country_id
|
||||||
customerInfo.value = res
|
customerInfo.value = res
|
||||||
form.value = res
|
form.value = res
|
||||||
} catch (error) { }
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//渠道
|
//渠道
|
||||||
const getgetChannel = async () => {
|
const getgetChannel = async () => {
|
||||||
@@ -127,7 +150,9 @@ const getgetChannel = async () => {
|
|||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const res = await CrmService.getChannel()
|
const res = await CrmService.getChannel()
|
||||||
channelOptions.value = res || []
|
channelOptions.value = res || []
|
||||||
} catch (error) { channelOptions.value = [] }
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//商机
|
//商机
|
||||||
const getMagnitude = async () => {
|
const getMagnitude = async () => {
|
||||||
@@ -135,7 +160,9 @@ const getMagnitude = async () => {
|
|||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const res = await CrmService.getMagnitude()
|
const res = await CrmService.getMagnitude()
|
||||||
opportunityOptions.value = res || []
|
opportunityOptions.value = res || []
|
||||||
} catch (error) { opportunityOptions.value = [] }
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//客户等级
|
//客户等级
|
||||||
const getGrade = async () => {
|
const getGrade = async () => {
|
||||||
@@ -143,7 +170,9 @@ const getGrade = async () => {
|
|||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const res = await CrmService.getGrade()
|
const res = await CrmService.getGrade()
|
||||||
levelOptions.value = res || []
|
levelOptions.value = res || []
|
||||||
} catch (error) { levelOptions.value = [] }
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//国家
|
//国家
|
||||||
const getarea = async () => {
|
const getarea = async () => {
|
||||||
@@ -151,7 +180,9 @@ const getarea = async () => {
|
|||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const res = await CrmService.getCountry()
|
const res = await CrmService.getCountry()
|
||||||
countryOptions.value = res || []
|
countryOptions.value = res || []
|
||||||
} catch (error) { countryOptions.value = [] }
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -174,7 +205,6 @@ function submitForm() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.border {
|
.border {
|
||||||
|
|
||||||
margin: 0px 10px 0px 40px;
|
margin: 0px 10px 0px 40px;
|
||||||
}
|
}
|
||||||
.submit-row .el-form-item__content {
|
.submit-row .el-form-item__content {
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="follow-add-page">
|
<div class="follow-add-page">
|
||||||
<div class="boxshadow pad20">
|
<div class="boxshadow pad20">
|
||||||
<el-link :underline="false" @click="goBack">
|
<el-link underline="never" @click="goBack">
|
||||||
<i class="iconfont-sys icon-reverse"></i> 返回
|
<i class="iconfont-sys icon-reverse"></i> 返回
|
||||||
</el-link>
|
</el-link>
|
||||||
<span class="border"></span><span class="edit-title" style="margin-left: 12px;">添加跟进</span>
|
<span class="border"></span><span class="edit-title" style="margin-left: 12px">添加跟进</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-header boxshadow" style="position:relative;">
|
<div class="edit-header boxshadow" style="position: relative">
|
||||||
<!-- 右上角三角形角标 -->
|
<!-- 右上角三角形角标 -->
|
||||||
<div class="corner-tag"><span class="corner-text">{{customerInfo.follow_txt}}</span></div>
|
<div class="corner-tag"
|
||||||
<el-row :gutter="20" style="flex: 1;">
|
><span class="corner-text">{{ customerInfo.follow_txt }}</span></div
|
||||||
|
>
|
||||||
|
<el-row :gutter="20" style="flex: 1">
|
||||||
<el-col :sm="24" :md="6" :lg="4">
|
<el-col :sm="24" :md="6" :lg="4">
|
||||||
<span>客户名称:{{ customerInfo.contacts }}</span>
|
<span>客户名称:{{ customerInfo.contacts }}</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -27,15 +29,25 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="edit-title">客户分级</span>
|
<span class="edit-title">客户分级</span>
|
||||||
<el-select v-model="customerInfo.grade" placeholder="请选择客户分级" style="width: 120px; margin-left: 16px;" @change="handleGradeChange">
|
<el-select
|
||||||
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
v-model="customerInfo.grade"
|
||||||
|
placeholder="请选择客户分级"
|
||||||
|
style="width: 120px; margin-left: 16px"
|
||||||
|
@change="handleGradeChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in levelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="edit-title">跟进方式</span>
|
<span class="edit-title">跟进方式</span>
|
||||||
<el-radio-group v-model="form.type">
|
<el-radio-group v-model="form.type">
|
||||||
@@ -44,13 +56,18 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="edit-title">跟进时间</span>
|
<span class="edit-title">跟进时间</span>
|
||||||
<el-date-picker v-model="form.firstFollowtime" type="datetime" :value-format="'YYYY-MM-DD HH:mm:ss'" style="width: 200px;" />
|
<el-date-picker
|
||||||
|
v-model="form.firstFollowtime"
|
||||||
|
type="datetime"
|
||||||
|
:value-format="'YYYY-MM-DD HH:mm:ss'"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card v-if="form.type=='1'" class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card v-if="form.type == '1'" class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-top">
|
<div class="info-top">
|
||||||
<span class="edit-title">上传跟进截图/聊天截图</span>
|
<span class="edit-title">上传跟进截图/聊天截图</span>
|
||||||
<el-upload
|
<el-upload
|
||||||
@@ -64,20 +81,26 @@
|
|||||||
</el-upload>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card v-if="form.type=='1'" class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card v-if="form.type == '1'" class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-top">
|
<div class="info-top">
|
||||||
<span class="edit-title">跟进描述</span>
|
<span class="edit-title">跟进描述</span>
|
||||||
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入跟进描述" />
|
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入跟进描述" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card v-if="form.type=='0'" class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card v-if="form.type == '0'" class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="edit-title">当前位置</span>
|
<span class="edit-title">当前位置</span>
|
||||||
<el-input v-model="followDesc" type="input" placeholder="请输入当前位置" style="width: 200px;" disabled>
|
<el-input
|
||||||
|
v-model="followDesc"
|
||||||
|
type="input"
|
||||||
|
placeholder="请输入当前位置"
|
||||||
|
style="width: 200px"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card v-if="form.type=='0'" class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card v-if="form.type == '0'" class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-top">
|
<div class="info-top">
|
||||||
<span class="edit-title">当前位置拍照上传</span>
|
<span class="edit-title">当前位置拍照上传</span>
|
||||||
<el-upload
|
<el-upload
|
||||||
@@ -91,21 +114,22 @@
|
|||||||
</el-upload>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card v-if="form.type=='0'" class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card v-if="form.type == '0'" class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-top">
|
<div class="info-top">
|
||||||
<span class="edit-title">拜访内容</span>
|
<span class="edit-title">拜访内容</span>
|
||||||
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入拜访内容" />
|
<el-input v-model="form.audio_text" type="textarea" placeholder="请输入拜访内容" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-top">
|
<div class="info-top">
|
||||||
<span class="edit-title">备注</span>
|
<span class="edit-title">备注</span>
|
||||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div>
|
<div>
|
||||||
<span class="border" style="margin: 0px 10px 0px 0px;"></span><span class="edit-title">跟进记录</span>
|
<span class="border" style="margin: 0px 10px 0px 0px"></span
|
||||||
|
><span class="edit-title">跟进记录</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="follow-list">
|
<div class="follow-list">
|
||||||
<ElScrollbar>
|
<ElScrollbar>
|
||||||
@@ -121,10 +145,21 @@
|
|||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-content-row">
|
<div class="timeline-content-row">
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<p class="timeline-text">{{item.type_txt}} <span class="timeline-mom">{{ item.created_at }}</span></p>
|
<p class="timeline-text"
|
||||||
<p class="timeline-mom"><span v-if="item.type==1">跟进描述</span><span v-else>拜访内容</span> {{ item.audio_text }}</p>
|
>{{ item.type_txt }}
|
||||||
|
<span class="timeline-mom">{{ item.created_at }}</span></p
|
||||||
|
>
|
||||||
|
<p class="timeline-mom"
|
||||||
|
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
|
||||||
|
{{ item.audio_text }}</p
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="item.picture_full" :src="item.picture_full" class="timeline-img" alt="图片" />
|
<img
|
||||||
|
v-if="item.picture_full"
|
||||||
|
:src="item.picture_full"
|
||||||
|
class="timeline-img"
|
||||||
|
alt="图片"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ElTimelineItem>
|
</ElTimelineItem>
|
||||||
@@ -158,7 +193,7 @@
|
|||||||
<div class="remind-title">下次跟进提醒设置</div>
|
<div class="remind-title">下次跟进提醒设置</div>
|
||||||
<div class="remind-options">
|
<div class="remind-options">
|
||||||
<div
|
<div
|
||||||
v-for="(item, idx) in remindOptions.filter(opt => opt.value !== 'custom')"
|
v-for="item in remindOptions.filter((opt) => opt.value !== 'custom')"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:class="['remind-btn', { active: remindValue === item.value }]"
|
:class="['remind-btn', { active: remindValue === item.value }]"
|
||||||
@click="remindValue = item.value"
|
@click="remindValue = item.value"
|
||||||
@@ -170,7 +205,7 @@
|
|||||||
v-model="customDate"
|
v-model="customDate"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
placeholder="自定义"
|
placeholder="自定义"
|
||||||
style="width:100%;"
|
style="width: 100%"
|
||||||
CircleClose
|
CircleClose
|
||||||
:disabled-date="disabledPastDate"
|
:disabled-date="disabledPastDate"
|
||||||
/>
|
/>
|
||||||
@@ -197,7 +232,7 @@ const route = useRoute()
|
|||||||
const customerId = route.query.id
|
const customerId = route.query.id
|
||||||
const levelOptions = ref<any>()
|
const levelOptions = ref<any>()
|
||||||
const followDesc = ref('')
|
const followDesc = ref('')
|
||||||
const activecolor = "#3471FF"
|
const activecolor = '#3471FF'
|
||||||
|
|
||||||
const successDialogVisible = ref(false) // 控制弹窗显示
|
const successDialogVisible = ref(false) // 控制弹窗显示
|
||||||
const remindValue = ref('1')
|
const remindValue = ref('1')
|
||||||
@@ -206,7 +241,7 @@ const imageUrl = ref(imageUrl1)
|
|||||||
const imageUrls = ref(imageUrl1)
|
const imageUrls = ref(imageUrl1)
|
||||||
function getNowStr() {
|
function getNowStr() {
|
||||||
const d = new Date()
|
const d = new Date()
|
||||||
const pad = (n: any) => n < 10 ? '0' + n : n
|
const pad = (n: any) => (n < 10 ? '0' + n : n)
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
}
|
}
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@@ -231,20 +266,13 @@ const customerInfo = ref<CustomerInfo>({})
|
|||||||
const followList = ref<CustomerInfo>({})
|
const followList = ref<CustomerInfo>({})
|
||||||
//获取详情
|
//获取详情
|
||||||
const getdetilfn = async () => {
|
const getdetilfn = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getcustomerdetail({ id: customerId })
|
const res = await CrmService.getcustomerdetail({ id: customerId })
|
||||||
customerInfo.value = res
|
customerInfo.value = res
|
||||||
console.log(res)
|
|
||||||
} catch (error) { }
|
|
||||||
}
|
}
|
||||||
//客户等级
|
//客户等级
|
||||||
const getGrade = async () => {
|
const getGrade = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getGrade()
|
const res = await CrmService.getGrade()
|
||||||
levelOptions.value = res || []
|
levelOptions.value = res || []
|
||||||
} catch (error) { levelOptions.value = [] }
|
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getGrade()
|
await getGrade()
|
||||||
@@ -255,22 +283,20 @@ onMounted(async () => {
|
|||||||
//修改等级
|
//修改等级
|
||||||
function handleGradeChange(newGrade: number) {
|
function handleGradeChange(newGrade: number) {
|
||||||
if (!customerId) return
|
if (!customerId) return
|
||||||
CrmService.customergrade({ id: customerId, grade: newGrade }).then(res => {
|
CrmService.customergrade({ id: customerId, grade: newGrade })
|
||||||
|
.then((res) => {
|
||||||
ElMessage.success(res?.message || '修改成功')
|
ElMessage.success(res?.message || '修改成功')
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
ElMessage.error('修改失败')
|
ElMessage.error('修改失败')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//跟进记录getfollowlist
|
//跟进记录getfollowlist
|
||||||
const getfollowlist = async () => {
|
const getfollowlist = async () => {
|
||||||
try {
|
|
||||||
const res = await CrmService.getfollowlist({ customer_id: route.query.id })
|
const res = await CrmService.getfollowlist({ customer_id: route.query.id })
|
||||||
followList.value = res || []
|
followList.value = res || []
|
||||||
} catch (error) { followList.value = [] }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const remindOptions = [
|
const remindOptions = [
|
||||||
{ label: '1天后', value: '1' },
|
{ label: '1天后', value: '1' },
|
||||||
{ label: '3天后', value: '3' },
|
{ label: '3天后', value: '3' },
|
||||||
@@ -298,7 +324,7 @@ const handleSuccessConfirm = async () =>{
|
|||||||
successDialogVisible.value = false
|
successDialogVisible.value = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push('/crmpc/customer')
|
router.push('/crmpc/customer')
|
||||||
}, 1000);
|
}, 1000)
|
||||||
}
|
}
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -332,10 +358,12 @@ function getCurrentLocation() {
|
|||||||
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
|
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('获取地址失败:', e)
|
||||||
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
|
followDesc.value = `纬度: ${latitude}, 经度: ${longitude}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
console.log('定位失败:', error)
|
||||||
followDesc.value = '无法获取当前位置'
|
followDesc.value = '无法获取当前位置'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -363,7 +391,8 @@ function handleUploadSuccess1(response: any) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.avatar,.timeline-img{
|
.avatar,
|
||||||
|
.timeline-img {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
.border {
|
.border {
|
||||||
@@ -426,7 +455,7 @@ function handleUploadSuccess1(response: any) {
|
|||||||
right: -9px;
|
right: -9px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 75px;
|
height: 75px;
|
||||||
background: #3471FF;
|
background: #3471ff;
|
||||||
clip-path: polygon(100% 0, 0 0, 100% 100%);
|
clip-path: polygon(100% 0, 0 0, 100% 100%);
|
||||||
border-top-right-radius: 12px;
|
border-top-right-radius: 12px;
|
||||||
}
|
}
|
||||||
@@ -489,7 +518,7 @@ function handleUploadSuccess1(response: any) {
|
|||||||
}
|
}
|
||||||
.remind-btn {
|
.remind-btn {
|
||||||
background: #f4f7fe;
|
background: #f4f7fe;
|
||||||
color: #3471FF;
|
color: #3471ff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 6px 18px;
|
padding: 6px 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -501,9 +530,9 @@ function handleUploadSuccess1(response: any) {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.remind-btn.active {
|
.remind-btn.active {
|
||||||
background: #3471FF;
|
background: #3471ff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: 1px solid #3471FF;
|
border: 1px solid #3471ff;
|
||||||
}
|
}
|
||||||
.confirm-btn {
|
.confirm-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="customer-edit-page">
|
<div class="customer-edit-page">
|
||||||
<div class="boxshadow pad20">
|
<div class="boxshadow pad20">
|
||||||
<el-link :underline="false" @click="goBack">
|
<el-link underline="never" @click="goBack">
|
||||||
<i class="iconfont-sys icon-reverse"></i> 返回
|
<i class="iconfont-sys icon-reverse"></i> 返回
|
||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-header boxshadow pad20" style="position:relative;">
|
<div class="edit-header boxshadow pad20" style="position: relative">
|
||||||
<span class="border"></span><span class="edit-title">客户资料</span>
|
<span class="border"></span><span class="edit-title">客户资料</span>
|
||||||
<!-- 右上角三角形角标 -->
|
<!-- 右上角三角形角标 -->
|
||||||
<div class="corner-tag"><span class="corner-text">{{customerInfo.follow_txt}}</span></div>
|
<div class="corner-tag"
|
||||||
|
><span class="corner-text">{{ customerInfo.follow_txt }}</span></div
|
||||||
|
>
|
||||||
<div class="info-row-bar">
|
<div class="info-row-bar">
|
||||||
<el-row :gutter="20" style="flex: 1;">
|
<el-row :gutter="20" style="flex: 1">
|
||||||
<el-col :sm="24" :md="6" :lg="4">
|
<el-col :sm="24" :md="6" :lg="4">
|
||||||
<span>客户名称:{{ customerInfo.contacts }}</span>
|
<span>客户名称:{{ customerInfo.contacts }}</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -27,33 +29,51 @@
|
|||||||
<span>客户商机:{{ customerInfo.magnitude_txt }}</span>
|
<span>客户商机:{{ customerInfo.magnitude_txt }}</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-button @click="goEdit" type="primary" class="edit-btn" size="small"><i class="iconfont-sys"> </i> 编辑资料</el-button>
|
<el-button @click="goEdit" type="primary" class="edit-btn" size="small"
|
||||||
|
><i class="iconfont-sys"> </i> 编辑资料</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="">
|
<div class="">
|
||||||
<span class="border"></span><span class="edit-title">客户画像</span>
|
<span class="border"></span><span class="edit-title">客户画像</span>
|
||||||
<el-button @click="addtag" type="primary" size="small" style="float: right;">添加标签</el-button>
|
<el-button @click="addtag" type="primary" size="small" style="float: right"
|
||||||
|
>添加标签</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-list">
|
<div class="tag-list">
|
||||||
<el-tag type="primary" v-for="(item, index) in customerInfo.tags" :key="index">{{item.name}}</el-tag>
|
<el-tag type="primary" v-for="(item, index) in customerInfo.tags" :key="index">{{
|
||||||
|
item.name
|
||||||
|
}}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="edit-title">客户分级</span>
|
<span class="edit-title">客户分级</span>
|
||||||
<el-select v-model="customerInfo.grade" placeholder="请选择客户分级" style="width: 120px; margin-left: 16px;" @change="handleGradeChange">
|
<el-select
|
||||||
<el-option v-for="item in levelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
v-model="customerInfo.grade"
|
||||||
|
placeholder="请选择客户分级"
|
||||||
|
style="width: 120px; margin-left: 16px"
|
||||||
|
@change="handleGradeChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in levelOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="info-card" shadow="never" style="margin-top: 16px;">
|
<el-card class="info-card" shadow="never" style="margin-top: 16px">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="edit-title">跟进记录</span>
|
<span class="edit-title">跟进记录</span>
|
||||||
<el-button type="primary" size="small" style="float: right;" @click="goAddFollow">+ 添加跟进</el-button>
|
<el-button type="primary" size="small" style="float: right" @click="goAddFollow"
|
||||||
|
>+ 添加跟进</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="follow-list">
|
<div class="follow-list">
|
||||||
<ElScrollbar>
|
<ElScrollbar>
|
||||||
@@ -69,10 +89,21 @@
|
|||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-content-row">
|
<div class="timeline-content-row">
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<p class="timeline-text">{{item.type_txt}} <span class="timeline-mom">{{ item.created_at }}</span></p>
|
<p class="timeline-text"
|
||||||
<p class="timeline-mom"><span v-if="item.type==1">跟进描述</span><span v-else>拜访内容</span> {{ item.audio_text }}</p>
|
>{{ item.type_txt }}
|
||||||
|
<span class="timeline-mom">{{ item.created_at }}</span></p
|
||||||
|
>
|
||||||
|
<p class="timeline-mom"
|
||||||
|
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
|
||||||
|
{{ item.audio_text }}</p
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="item.picture_full" :src="item.picture_full" class="timeline-img" alt="图片" />
|
<img
|
||||||
|
v-if="item.picture_full"
|
||||||
|
:src="item.picture_full"
|
||||||
|
class="timeline-img"
|
||||||
|
alt="图片"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ElTimelineItem>
|
</ElTimelineItem>
|
||||||
@@ -97,12 +128,12 @@
|
|||||||
:show-close="false"
|
:show-close="false"
|
||||||
>
|
>
|
||||||
<div class="tag-dialog-scroll">
|
<div class="tag-dialog-scroll">
|
||||||
<div v-for="group in tagGroups" :key="group.name" style="margin-bottom: 12px;">
|
<div v-for="group in tagGroups" :key="group.name" style="margin-bottom: 12px">
|
||||||
<div style="font-weight: bold; margin-bottom: 6px;">{{ group.name }}</div>
|
<div style="font-weight: bold; margin-bottom: 6px">{{ group.name }}</div>
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
<div style="display: flex; flex-wrap: wrap; gap: 8px">
|
||||||
<div
|
<div
|
||||||
class="col4 tagitem"
|
class="col4 tagitem"
|
||||||
style="cursor: pointer;"
|
style="cursor: pointer"
|
||||||
v-for="(item1, index1) in group.tags"
|
v-for="(item1, index1) in group.tags"
|
||||||
:class="{ 'tagactive': activeMap[group.id] === item1.id }"
|
:class="{ 'tagactive': activeMap[group.id] === item1.id }"
|
||||||
@click="tagSelect(item1)"
|
@click="tagSelect(item1)"
|
||||||
@@ -132,10 +163,8 @@ import { ElMessage } from 'element-plus'
|
|||||||
const TIMELINE_PLACEMENT = 'top'
|
const TIMELINE_PLACEMENT = 'top'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const customerLevel = ref('A')
|
const activecolor = '#3471ff'
|
||||||
const activecolor = "#3471FF"
|
|
||||||
const tagDialogVisible = ref(false)
|
const tagDialogVisible = ref(false)
|
||||||
const selectedTags = ref<string[]>([])
|
|
||||||
const customerId = route.query.id
|
const customerId = route.query.id
|
||||||
const levelOptions = ref<any>()
|
const levelOptions = ref<any>()
|
||||||
const tagGroups = ref<any[]>([])
|
const tagGroups = ref<any[]>([])
|
||||||
@@ -149,28 +178,19 @@ interface CustomerInfo {
|
|||||||
const customerInfo = ref<CustomerInfo>({})
|
const customerInfo = ref<CustomerInfo>({})
|
||||||
//获取详情
|
//获取详情
|
||||||
const getdetilfn = async () => {
|
const getdetilfn = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getcustomerdetail({ id: customerId })
|
const res = await CrmService.getcustomerdetail({ id: customerId })
|
||||||
customerInfo.value = res
|
customerInfo.value = res
|
||||||
console.log(res)
|
|
||||||
} catch (error) { }
|
|
||||||
}
|
}
|
||||||
//客户等级
|
//客户等级
|
||||||
const getGrade = async () => {
|
const getGrade = async () => {
|
||||||
try {
|
|
||||||
// 这里调用获取品牌列表的接口
|
|
||||||
const res = await CrmService.getGrade()
|
const res = await CrmService.getGrade()
|
||||||
levelOptions.value = res || []
|
levelOptions.value = res || []
|
||||||
} catch (error) { levelOptions.value = [] }
|
|
||||||
}
|
}
|
||||||
//跟进记录getfollowlist
|
//跟进记录getfollowlist
|
||||||
const followList = ref<CustomerInfo>({})
|
const followList = ref<CustomerInfo>({})
|
||||||
const getfollowlist = async () => {
|
const getfollowlist = async () => {
|
||||||
try {
|
|
||||||
const res = await CrmService.getfollowlist({ customer_id: route.query.id })
|
const res = await CrmService.getfollowlist({ customer_id: route.query.id })
|
||||||
followList.value = res || []
|
followList.value = res || []
|
||||||
} catch (error) { followList.value = [] }
|
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getGrade()
|
await getGrade()
|
||||||
@@ -184,12 +204,11 @@ function addtag(){
|
|||||||
tagGrouplistfn()
|
tagGrouplistfn()
|
||||||
}
|
}
|
||||||
const tagGrouplistfn = async () => {
|
const tagGrouplistfn = async () => {
|
||||||
try {
|
const res = await CrmService.tagGrouplist({})
|
||||||
const res = await CrmService.tagGrouplist()
|
|
||||||
tagGroups.value = res || []
|
tagGroups.value = res || []
|
||||||
// 回显选中
|
// 回显选中
|
||||||
if (customerInfo.value && Array.isArray(customerInfo.value.tags)) {
|
if (customerInfo.value && Array.isArray(customerInfo.value.tags)) {
|
||||||
tagGroups.value.forEach(group => {
|
tagGroups.value.forEach((group) => {
|
||||||
if (group.tags && Array.isArray(group.tags)) {
|
if (group.tags && Array.isArray(group.tags)) {
|
||||||
const matched = group.tags.find((item: any) =>
|
const matched = group.tags.find((item: any) =>
|
||||||
customerInfo.value.tags.some((t: any) => t.id === item.id)
|
customerInfo.value.tags.some((t: any) => t.id === item.id)
|
||||||
@@ -200,7 +219,6 @@ const tagGrouplistfn = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 点击标签
|
// 4. 点击标签
|
||||||
@@ -217,11 +235,16 @@ async function tagSelect(item: { group_id: string | number; id: number }) {
|
|||||||
tag_id = String(item.id)
|
tag_id = String(item.id)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await CrmService.tagGroupupdatePivot({ customer_id: customerId, group_id: key, tag_id })
|
const res = await CrmService.tagGroupupdatePivot({
|
||||||
|
customer_id: customerId,
|
||||||
|
group_id: key,
|
||||||
|
tag_id
|
||||||
|
})
|
||||||
ElMessage.success(res?.message || '操作成功')
|
ElMessage.success(res?.message || '操作成功')
|
||||||
await getdetilfn()
|
await getdetilfn()
|
||||||
await tagGrouplistfn()
|
await tagGrouplistfn()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
ElMessage.error('操作失败')
|
ElMessage.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,9 +287,11 @@ function goBack() {
|
|||||||
//修改等级
|
//修改等级
|
||||||
function handleGradeChange(newGrade: number) {
|
function handleGradeChange(newGrade: number) {
|
||||||
if (!customerId) return
|
if (!customerId) return
|
||||||
CrmService.customergrade({ id: customerId, grade: newGrade }).then(res => {
|
CrmService.customergrade({ id: customerId, grade: newGrade })
|
||||||
|
.then((res) => {
|
||||||
ElMessage.success(res?.message || '修改成功')
|
ElMessage.success(res?.message || '修改成功')
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
ElMessage.error('修改失败')
|
ElMessage.error('修改失败')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,60 +2,85 @@
|
|||||||
<div class="customer-page">
|
<div class="customer-page">
|
||||||
<!-- 顶部操作栏 -->
|
<!-- 顶部操作栏 -->
|
||||||
<div class="customer-toolbar boxshadow">
|
<div class="customer-toolbar boxshadow">
|
||||||
<el-button type="primary" @click="goAdd"><el-icon><Plus /></el-icon>添加</el-button>
|
<el-button type="primary" @click="goAdd" v-if="hasAuth('crm_customer@add')">
|
||||||
|
<el-icon><Plus /> </el-icon>
|
||||||
|
添加
|
||||||
|
</el-button>
|
||||||
<div class="customer-search-group">
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
<el-input v-model="searchValue" placeholder="请输入客户名称或电话" />
|
<el-input v-model="searchValue" placeholder="请输入客户名称或电话" />
|
||||||
<el-select v-model="selectValue">
|
</el-form-item>
|
||||||
|
<el-form-item label="国家" label-width="70px">
|
||||||
|
<el-select v-model="country_id" filterable placeholder="请选择国家">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="v in selectOptions"
|
v-for="item in countryOptions"
|
||||||
:key="v.value"
|
:key="item.id"
|
||||||
:label="v.label"
|
:label="item.cn_addr"
|
||||||
:value="v.value"
|
:value="item.id"
|
||||||
>
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="时间" label-width="70px">
|
||||||
|
<el-select v-model="selectValue" placeholder="请选择时间" style="width: 120px">
|
||||||
|
<el-option v-for="v in selectOptions" :key="v.value" :label="v.label" :value="v.value">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 筛选标签 -->
|
<!-- 筛选标签 -->
|
||||||
<div class="customer-tags boxshadow">
|
<div class="customer-tags boxshadow">
|
||||||
<div class="filter-row">
|
<!-- 前3组 -->
|
||||||
<span class="filter-label">价值标签</span>
|
<div class="filter-row" v-for="item in visibleTagGroups" :key="item.id">
|
||||||
|
<span class="filter-label">{{ item.name }}</span>
|
||||||
<div class="filter-tags">
|
<div class="filter-tags">
|
||||||
<span v-for="item in valueTags" :key="item.label" :class="['tag-item', { 'tag-active': tagActiveMap['value'] === item.label }]" @click="handleTagClick('value', item.label)">
|
<span
|
||||||
{{ item.label }}({{ item.count }})
|
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
|
||||||
|
@click="handleTagClick(item.id, '')"
|
||||||
|
>全部</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tag-items"
|
||||||
|
v-for="item1 in item.tags"
|
||||||
|
:key="item1.id"
|
||||||
|
@click="handleTagClick(item.id, item1.id)"
|
||||||
|
>
|
||||||
|
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
|
||||||
|
{{ item1.name }}
|
||||||
|
</span>
|
||||||
|
<el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-row">
|
<!-- 更多 -->
|
||||||
<span class="filter-label">生命周期标签</span>
|
|
||||||
<div class="filter-tags">
|
|
||||||
<span v-for="item in lifeTags" :key="item.label" :class="['tag-item', { 'tag-active': tagActiveMap['life'] === item.label }]" @click="handleTagClick('life', item.label)">
|
|
||||||
{{ item.label }}({{ item.count }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row">
|
|
||||||
<span class="filter-label">行为标签</span>
|
|
||||||
<div class="filter-tags">
|
|
||||||
<span v-for="item in actionTags" :key="item.label" :class="['tag-item', { 'tag-active': tagActiveMap['action'] === item.label }]" @click="handleTagClick('action', item.label)">
|
|
||||||
{{ item.label }}({{ item.count }})
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="showMore" class="filter-row">
|
<div v-if="showMore">
|
||||||
<span class="filter-label">更多标签</span>
|
<div class="filter-row" v-for="item in moreTagGroups" :key="item.id">
|
||||||
|
<span class="filter-label">{{ item.name }}</span>
|
||||||
<div class="filter-tags">
|
<div class="filter-tags">
|
||||||
<span v-for="item in moreTags" :key="item.label" :class="['tag-item', { 'tag-active': tagActiveMap['more'] === item.label }]" @click="handleTagClick('more', item.label)">
|
<span
|
||||||
{{ item.label }}({{ item.count }})
|
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
|
||||||
|
@click="handleTagClick(item.id, '')"
|
||||||
|
>全部</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tag-items"
|
||||||
|
v-for="item1 in item.tags"
|
||||||
|
:key="item1.id"
|
||||||
|
@click="handleTagClick(item.id, item1.id)"
|
||||||
|
>
|
||||||
|
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
|
||||||
|
{{ item1.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<div class="filter-more-btn">
|
<div class="filter-more-btn" v-if="moreTagGroups.length">
|
||||||
<el-link :underline="false" @click="showMore = !showMore">
|
<el-link underline="never" @click="showMore = !showMore">
|
||||||
{{ showMore ? '收起' : '更多' }}
|
{{ showMore ? '收起' : '更多' }}
|
||||||
<i v-if="showMore" class="iconfont-sys"> </i>
|
<i v-if="showMore" class="iconfont-sys"> </i>
|
||||||
<i v-else class="iconfont-sys"></i>
|
<i v-else class="iconfont-sys"></i>
|
||||||
@@ -72,44 +97,104 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="tableData" :currentPage="pagination.currentPage"
|
<ArtTable
|
||||||
:pageSize="pagination.pageSize" :total="pagination.total" @size-change="handleSizeChange"
|
ref="tableRef"
|
||||||
@current-change="handleCurrentChange" lass="customer-table">
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
<el-table-column prop="code" label="编号" align="center" />
|
||||||
<el-table-column prop="contacts" label="客户名称" align="center" />
|
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||||
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||||
<el-table-column prop="channel_name" label="渠道" align="center" />
|
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||||
|
<el-table-column prop="country_name" label="国家" align="center" />
|
||||||
<el-table-column prop="phone" label="电话" align="center" />
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<!-- <el-table-column prop="WhatsApp" label="WhatsApp" align="center" /> -->
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
<el-table-column prop="grade_txt" label="等级" align="center" />
|
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||||
<el-table-column prop="follow" label="状态" align="center">
|
<el-table-column prop="describe" label="备注" align="center" />
|
||||||
|
<!-- <el-table-column prop="follow" label="状态" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span :class="statusClass(scope.row.follow)">
|
<span :class="statusClass(scope.row.follow)">
|
||||||
{{ scope.row.follow_txt }}
|
{{ scope.row.follow_txt }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
|
<el-table-column prop="follow_next" label="下次跟进时间"></el-table-column>
|
||||||
|
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-link type="primary" :underline="false" style="margin-right: 8px;" @click="goEdit(scope.row)"> <i class="iconfont-sys"> </i>查看</el-link>
|
<el-link
|
||||||
<el-link type="primary" :underline="false" @click="goAddFollow(scope.row)"> <i class="iconfont-sys"> </i> 添加跟进</el-link>
|
type="success"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="onDetail(scope.row)"
|
||||||
|
v-if="hasAuth('crm_customer@show')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>详情
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="goEdit(scope.row)"
|
||||||
|
v-if="hasAuth('crm_customer@show')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"> </i>查看
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="goAddFollow(scope.row)"
|
||||||
|
v-if="hasAuth('crm_customer@follow_add')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i> 跟进
|
||||||
|
</el-link>
|
||||||
|
<el-link type="danger" underline="never" @click="reportfn(scope.row)">
|
||||||
|
<i class="iconfont-sys"></i> 标记无效
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
type="danger"
|
||||||
|
underline="never"
|
||||||
|
@click="delectfn(scope.row)"
|
||||||
|
v-if="hasAuth('crm_customer@delete')"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i> 删除
|
||||||
|
</el-link>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<el-dialog v-model="detailDialogVisible" title="详情">
|
||||||
|
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import { CrmService } from '@/api/crmApi'
|
import { CrmService } from '@/api/crmApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
const selectValue = ref('')
|
const selectValue = ref('')
|
||||||
const activeTab = ref('')
|
const activeTab = ref('')
|
||||||
|
const country_id = ref('')
|
||||||
const showMore = ref(false)
|
const showMore = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const detailRow = ref<any>(null)
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -121,47 +206,46 @@ const selectOptions = [
|
|||||||
{ label: '近三天', value: '1' },
|
{ label: '近三天', value: '1' },
|
||||||
{ label: '近七天', value: '2' }
|
{ label: '近七天', value: '2' }
|
||||||
]
|
]
|
||||||
|
const countryOptions = ref<any>()
|
||||||
const valueTags = [
|
//国家
|
||||||
{ label: '高价值', count: 10 },
|
const getarea = async () => {
|
||||||
{ label: '中价值', count: 10 },
|
|
||||||
{ label: '低价值', count: 10 },
|
|
||||||
{ label: '潜在高价值', count: 10 }
|
|
||||||
]
|
|
||||||
const lifeTags = [
|
|
||||||
{ label: '新客户', count: 10 },
|
|
||||||
{ label: '活跃', count: 10 },
|
|
||||||
{ label: '深度', count: 10 },
|
|
||||||
{ label: '流失', count: 10 }
|
|
||||||
]
|
|
||||||
const actionTags = [
|
|
||||||
{ label: '高频购买', count: 10 },
|
|
||||||
{ label: '大单', count: 10 },
|
|
||||||
{ label: '促销敏感', count: 10 }
|
|
||||||
]
|
|
||||||
const moreTags = [
|
|
||||||
{ label: '标签A', count: 5 },
|
|
||||||
{ label: '标签B', count: 3 }
|
|
||||||
]
|
|
||||||
//获取标签
|
|
||||||
const tagGroups = ref<any[]>([])
|
|
||||||
const tagGrouplistfn = async () => {
|
|
||||||
try {
|
try {
|
||||||
const res = await CrmService.tagGrouplist()
|
// 这里调用获取品牌列表的接口
|
||||||
tagGroups.value = res || []
|
const res = await CrmService.getCountry()
|
||||||
} catch (error) { }
|
countryOptions.value = res||[]
|
||||||
|
} catch (error) { countryOptions.value = [] }
|
||||||
}
|
}
|
||||||
|
// 获取列表数据
|
||||||
|
const tagActiveMap = reactive<{ [groupId: string]: string }>({})
|
||||||
|
//获取标签
|
||||||
|
type TagGroup = { id: string; name: string; tags: { id: string; name: string; count: number }[] }
|
||||||
|
const tagGroups = ref<TagGroup[]>([])
|
||||||
|
const tagGrouplistfn = async () => {
|
||||||
|
const res = await CrmService.tagGrouplist({ scene: 'customers' })
|
||||||
|
tagGroups.value = res || []
|
||||||
|
tagGroups.value.forEach((group) => {
|
||||||
|
if (group.tags && Array.isArray(group.tags)) {
|
||||||
|
tagActiveMap[group.id] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 计算前3组和更多组
|
||||||
|
// eslint-disable-next-line vue/valid-define-props
|
||||||
|
defineProps()
|
||||||
|
const visibleTagGroups = computed(() => tagGroups.value.slice(0, 3))
|
||||||
|
const moreTagGroups = computed(() => tagGroups.value.slice(3))
|
||||||
// 表格数据
|
// 表格数据
|
||||||
const tableData = ref<any[]>([])
|
const tableData = ref<any[]>([])
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getarea()
|
||||||
gettableList()
|
gettableList()
|
||||||
|
tagGrouplistfn()
|
||||||
})
|
})
|
||||||
// tabs 切换联动
|
// tabs 切换联动
|
||||||
watch([activeTab, searchValue, selectValue], () => {
|
watch([activeTab, searchValue, selectValue, country_id], () => {
|
||||||
pagination.currentPage = 1
|
pagination.currentPage = 1
|
||||||
gettableList()
|
gettableList()
|
||||||
})
|
})
|
||||||
// 获取列表数据
|
|
||||||
const gettableList = async () => {
|
const gettableList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -172,14 +256,16 @@ const gettableList = async () => {
|
|||||||
}
|
}
|
||||||
if (searchValue.value) params.keyword = searchValue.value
|
if (searchValue.value) params.keyword = searchValue.value
|
||||||
if (selectValue.value) params.follow_last = selectValue.value
|
if (selectValue.value) params.follow_last = selectValue.value
|
||||||
|
if (country_id.value) params.country_id = country_id.value
|
||||||
if (activeTab.value && activeTab.value !== '') params.follow = activeTab.value
|
if (activeTab.value && activeTab.value !== '') params.follow = activeTab.value
|
||||||
// 标签参数
|
// 标签参数,所有选中的标签id组成数组
|
||||||
if (tagActiveMap.value) params.value_tag = tagActiveMap.value
|
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
|
||||||
if (tagActiveMap.life) params.life_tag = tagActiveMap.life
|
if (selectedTagIds.length) {
|
||||||
if (tagActiveMap.action) params.action_tag = tagActiveMap.action
|
params.tag_id = selectedTagIds
|
||||||
if (tagActiveMap.more) params.more_tag = tagActiveMap.more
|
}
|
||||||
|
console.log(params)
|
||||||
const { data, current, size, total } = await CrmService.getcustomerList(params)
|
const { data, current, size, total } = await CrmService.getcustomerList(params)
|
||||||
tableData.value = data.map((item: any, index: number) => ({
|
tableData.value = data.map((item: any) => ({
|
||||||
...item
|
...item
|
||||||
}))
|
}))
|
||||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
@@ -192,12 +278,11 @@ const gettableList = async () => {
|
|||||||
// 处理表格分页变化
|
// 处理表格分页变化
|
||||||
const handleSizeChange = (newPageSize: number) => {
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
pagination.pageSize = newPageSize
|
pagination.pageSize = newPageSize
|
||||||
// getWorkList()
|
gettableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCurrentChange = (newCurrentPage: number) => {
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
pagination.currentPage = newCurrentPage
|
pagination.currentPage = newCurrentPage
|
||||||
// getWorkList()
|
gettableList()
|
||||||
}
|
}
|
||||||
function statusClass(status: any) {
|
function statusClass(status: any) {
|
||||||
if (status === 0) return 'status-following'
|
if (status === 0) return 'status-following'
|
||||||
@@ -205,7 +290,6 @@ function statusClass(status: any) {
|
|||||||
if (status === 2) return 'status-lost'
|
if (status === 2) return 'status-lost'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
function goAdd() {
|
function goAdd() {
|
||||||
router.push('/crmpc/customer/add')
|
router.push('/crmpc/customer/add')
|
||||||
@@ -222,76 +306,140 @@ const router = useRouter()
|
|||||||
query: { id: row.id } // 可带客户名等参数
|
query: { id: row.id } // 可带客户名等参数
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 标签选中状态,key为组id,值为选中的标签id
|
||||||
// 标签选中状态,key为组名,值为选中的标签label
|
function handleTagClick(groupId: string, tagId: string) {
|
||||||
const tagActiveMap = reactive<{ [group: string]: string }>({})
|
// if (tagActiveMap[groupId] === tagId) {
|
||||||
|
// tagActiveMap[groupId] = '' // 取消选中
|
||||||
function handleTagClick(group: string, label: string) {
|
// } else {
|
||||||
if (tagActiveMap[group] === label) {
|
tagActiveMap[groupId] = tagId // 选中
|
||||||
tagActiveMap[group] = '' // 取消选中
|
// }
|
||||||
} else {
|
|
||||||
tagActiveMap[group] = label // 选中
|
|
||||||
}
|
|
||||||
pagination.currentPage = 1
|
pagination.currentPage = 1
|
||||||
gettableList()
|
gettableList()
|
||||||
}
|
}
|
||||||
|
//删除
|
||||||
|
const delectfn = (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要删除此用户吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await CrmService.getcustomerdelete({ id: row.id })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
// 你可以在这里调用接口或做后续处理
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 标记无效
|
||||||
|
const reportfn = (row: any) => {
|
||||||
|
ElMessageBox.confirm('确定要标记无效吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await CrmService.customerReport({ id: row.id })
|
||||||
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户点击了取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
function onDetail(row: any) {
|
||||||
|
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||||
|
detailRow.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
:deep(.el-table__header th) {
|
:deep(.el-table__header th) {
|
||||||
background: #f7f9fa !important;
|
background: #f7f9fa !important;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
:deep(.el-table__body tr.custom-table-row td) {
|
:deep(.el-table__empty-block) {
|
||||||
border-bottom: 1px solid #F7F9FA !important;
|
height: 450px !important;
|
||||||
border-right: 1px solid #F7F9FA !important;
|
|
||||||
}
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-table__body tr td:last-child) {
|
:deep(.el-table__body tr td:last-child) {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__body tr:last-child td) {
|
:deep(.el-table__body tr:last-child td) {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__header-wrapper th) {
|
:deep(.el-table__header-wrapper th) {
|
||||||
border-bottom: 1px solid #F7F9FA !important;
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
border-right: 1px solid #F7F9FA !important;
|
border-right: 1px solid #f7f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__header-wrapper th:last-child) {
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border: 1px solid #F7F9FA !important;
|
border: 1px solid #f7f9fa !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-page {
|
.customer-page {
|
||||||
background: var(--art-main-bg-color);
|
background: var(--art-main-bg-color);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-toolbar {
|
.customer-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 20px 10px;
|
padding: 20px 20px;
|
||||||
background: var(--art-main-bg-color);
|
background: var(--art-main-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-search-group {
|
.customer-search-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-tags {
|
.customer-tags {
|
||||||
background: var(--art-main-bg-color);
|
background: var(--art-main-bg-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px 20px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-row {
|
.filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -299,45 +447,77 @@ function handleTagClick(group: string, label: string) {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tags {
|
.filter-tags {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
span{
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-tabs {
|
.customer-tabs {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-following {
|
.status-following {
|
||||||
color: #00A43F;
|
color: #00a43f;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dealed {
|
.status-dealed {
|
||||||
color: #3471FF;
|
color: #3471ff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-lost {
|
.status-lost {
|
||||||
color: #FF0000;
|
color: #ff0000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-table {
|
.customer-table {
|
||||||
background: var(--art-main-bg-color);
|
background: var(--art-main-bg-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-more-btn {
|
.filter-more-btn {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.fade-enter-active, .fade-leave-active {
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
.fade-enter-from, .fade-leave-to {
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.tag-item { cursor: pointer; border-radius: 4px; padding: 2px 10px; margin-right: 8px; transition: background 0.2s; }
|
.tag-items {
|
||||||
.tag-active { background: #3471FF; color: #fff; }
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
.tag-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.tag-itemall {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.tag-active {
|
||||||
|
background: #3471ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tag-active :deep(.el-badge__content) {
|
||||||
|
color: #3471ff !important; /* 文字颜色 */
|
||||||
|
background-color: #fff !important; /* 背景色 */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -29,26 +29,27 @@
|
|||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-table-column prop="contacts" label="客户名称" min-width="120"></el-table-column>
|
<el-table-column prop="contacts" label="客户名称"></el-table-column>
|
||||||
<el-table-column prop="phone" label="电话" min-width="120"></el-table-column>
|
<el-table-column prop="phone" label="电话"></el-table-column>
|
||||||
<el-table-column prop="magnitude" label="客户商机" min-width="140"></el-table-column>
|
<el-table-column prop="WhatsApp" label="WhatsApp"></el-table-column>
|
||||||
<el-table-column label="状态" min-width="140">
|
<el-table-column prop="email" label="邮箱"></el-table-column>
|
||||||
<template #default="scope">
|
<el-table-column prop="magnitude_txt" label="客户商机"></el-table-column>
|
||||||
<span :class="getCountdownClass(scope.row.deleted_at)">
|
<el-table-column prop="channel_name" label="渠道"></el-table-column>
|
||||||
|
<el-table-column prop="country_name" label="国家"></el-table-column>
|
||||||
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px;"><i class="iconfont-sys"></i></span>
|
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||||
{{ getCountdown(scope.row.deleted_at) }}
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column fixed="right" label="操作" width="160px">
|
<el-table-column fixed="right" label="操作" width="160px">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="follow-detli" @click="onDetail(scope.row)">
|
<div class="follow-detli" @click="onDetail(scope.row)">
|
||||||
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px;"><i class="iconfont-sys"></i></span>
|
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
|
||||||
|
<i class="iconfont-sys"></i>
|
||||||
|
</span>
|
||||||
详情
|
详情
|
||||||
</div>
|
</div>
|
||||||
<div class="follow-action" @click="onFollow(scope.row)">
|
<div class="follow-action" @click="onFollow(scope.row)">
|
||||||
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px;"><i class="iconfont-sys"></i></span>
|
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
|
||||||
|
<i class="iconfont-sys"></i>
|
||||||
|
</span>
|
||||||
领单
|
领单
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,10 +58,8 @@
|
|||||||
</ArtTable>
|
</ArtTable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="detailDialogVisible" title="详情" width="400px">
|
<el-dialog v-model="detailDialogVisible" title="详情">
|
||||||
<div v-if="detailRow" v-html="detailRow.desc_collect" >
|
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ import { CrmService } from '@/api/crmApi'
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 10,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
const remindRange = ref('')
|
const remindRange = ref('')
|
||||||
@@ -93,7 +92,7 @@ const gettableList = async () => {
|
|||||||
}
|
}
|
||||||
if (remindRange.value) params.follow_last = remindRange.value
|
if (remindRange.value) params.follow_last = remindRange.value
|
||||||
const { data, current, size, total } = await CrmService.getcustomerList(params)
|
const { data, current, size, total } = await CrmService.getcustomerList(params)
|
||||||
tableData.value = data.map((item: any, index: number) => ({
|
tableData.value = data.map((item: any) => ({
|
||||||
...item
|
...item
|
||||||
}))
|
}))
|
||||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
@@ -139,6 +138,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
function getCountdown(targetDate: string) {
|
function getCountdown(targetDate: string) {
|
||||||
// 让 refreshKey 成为依赖,确保每秒刷新
|
// 让 refreshKey 成为依赖,确保每秒刷新
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
refreshKey.value
|
refreshKey.value
|
||||||
const now = dayjs()
|
const now = dayjs()
|
||||||
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||||
@@ -161,36 +161,49 @@ function getCountdownClass(targetDate: string) {
|
|||||||
return 'countdown-success'
|
return 'countdown-success'
|
||||||
}
|
}
|
||||||
const onFollow = async (row: any) => {
|
const onFollow = async (row: any) => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm('确定要领取此客户吗?', '提示', {
|
||||||
'确定要领取此客户吗?',
|
|
||||||
'提示',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning'
|
||||||
}
|
})
|
||||||
).then(async () => {
|
.then(async () => {
|
||||||
const res = await CrmService.customerclaim({ customer_id: row.id })
|
const res = await CrmService.customerclaim({ customer_id: row.id })
|
||||||
ElMessage.success(res?.message || '提交成功!')
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
gettableList()
|
gettableList()
|
||||||
// 你可以在这里调用接口或做后续处理
|
// 你可以在这里调用接口或做后续处理
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// 用户点击了取消
|
// 用户点击了取消
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
function processImages(html: string) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
function onDetail(row: any) {
|
function onDetail(row: any) {
|
||||||
|
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||||
detailRow.value = row
|
detailRow.value = row
|
||||||
detailDialogVisible.value = true
|
detailDialogVisible.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.detils-content {
|
||||||
|
padding-left: 15px;
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
.boxshadow {
|
.boxshadow {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 10, 118, 0.1);
|
box-shadow: 0 2px 12px 0 rgba(0, 10, 118, 0.1);
|
||||||
background: var(--el-card-bg-color);
|
background: var(--el-card-bg-color);
|
||||||
padding: 10px;
|
padding: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +216,7 @@ function onDetail(row: any) {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-left: 4px solid #3471FF;
|
border-left: 4px solid #3471ff;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -227,16 +240,18 @@ function onDetail(row: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 300px !important;
|
||||||
|
}
|
||||||
:deep(.el-table__header th) {
|
:deep(.el-table__header th) {
|
||||||
background: #f7f9fa !important;
|
background: #f7f9fa !important;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
:deep(.el-table__body tr.custom-table-row td) {
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
border-bottom: 1px solid #F7F9FA !important;
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
border-right: 1px solid #F7F9FA !important;
|
border-right: 1px solid #f7f9fa !important;
|
||||||
}
|
}
|
||||||
:deep(.el-table__body tr td:last-child) {
|
:deep(.el-table__body tr td:last-child) {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
@@ -245,14 +260,14 @@ function onDetail(row: any) {
|
|||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
:deep(.el-table__header-wrapper th) {
|
:deep(.el-table__header-wrapper th) {
|
||||||
border-bottom: 1px solid #F7F9FA !important;
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
border-right: 1px solid #F7F9FA !important;
|
border-right: 1px solid #f7f9fa !important;
|
||||||
}
|
}
|
||||||
:deep(.el-table__header-wrapper th:last-child) {
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border: 1px solid #F7F9FA !important;
|
border: 1px solid #f7f9fa !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
:deep(.el-table th),
|
:deep(.el-table th),
|
||||||
@@ -261,16 +276,16 @@ function onDetail(row: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.countdown-danger {
|
.countdown-danger {
|
||||||
color: #FF0000 !important;
|
color: #ff0000 !important;
|
||||||
}
|
}
|
||||||
.countdown-success {
|
.countdown-success {
|
||||||
color: #00A43F !important;
|
color: #00a43f !important;
|
||||||
}
|
}
|
||||||
.follow-detli {
|
.follow-detli {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #00A43F;
|
color: #00a43f;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
@@ -282,7 +297,7 @@ function onDetail(row: any) {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #409EFF;
|
color: #409eff;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
@@ -290,5 +305,4 @@ function onDetail(row: any) {
|
|||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="homeheard">
|
<div class="homeheard">
|
||||||
您已陪伴领客 CRM <span class="daynum"><span class="day">{{homestatistics.day}}</span>天</span>
|
您已陪伴领客 CRM
|
||||||
|
<span class="daynum"
|
||||||
|
><span class="day">{{ homestatistics.day }}</span
|
||||||
|
>天</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="boxshadow">
|
<div class="boxshadow">
|
||||||
<div class="heard">
|
<div class="heard">
|
||||||
@@ -16,7 +20,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-row :gutter="20" class="card-list">
|
<el-row :gutter="20" class="card-list">
|
||||||
<el-col :sm="12" :md="6" :lg="4">
|
<el-col :sm="12" :md="6" :lg="4">
|
||||||
<div class="card art-custom-card" :style="{ backgroundImage: `url(${adduser})`, backgroundSize: 'cover', backgroundPosition: 'center' }">
|
<div
|
||||||
|
class="card art-custom-card"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${adduser})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<CountTo
|
<CountTo
|
||||||
class="number box-title"
|
class="number box-title"
|
||||||
:endVal="homestatistics.customer"
|
:endVal="homestatistics.customer"
|
||||||
@@ -29,7 +40,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :md="6" :lg="4">
|
<el-col :sm="12" :md="6" :lg="4">
|
||||||
<div class="card art-custom-card" :style="{ backgroundImage: `url(${addbf})`, backgroundSize: 'cover', backgroundPosition: 'center' }">
|
<div
|
||||||
|
class="card art-custom-card"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${addbf})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<CountTo
|
<CountTo
|
||||||
class="number box-title"
|
class="number box-title"
|
||||||
:endVal="homestatistics.visit"
|
:endVal="homestatistics.visit"
|
||||||
@@ -42,7 +60,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :md="6" :lg="4">
|
<el-col :sm="12" :md="6" :lg="4">
|
||||||
<div class="card art-custom-card" :style="{ backgroundImage: `url(${addgj})`, backgroundSize: 'cover', backgroundPosition: 'center' }">
|
<div
|
||||||
|
class="card art-custom-card"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${addgj})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<CountTo
|
<CountTo
|
||||||
class="number box-title"
|
class="number box-title"
|
||||||
:endVal="homestatistics.follow"
|
:endVal="homestatistics.follow"
|
||||||
@@ -55,39 +80,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :md="6" :lg="4">
|
<el-col :sm="12" :md="6" :lg="4">
|
||||||
<div class="card art-custom-card" :style="{ backgroundImage: `url(${deal})`, backgroundSize: 'cover', backgroundPosition: 'center' }">
|
<div
|
||||||
|
class="card art-custom-card"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${deal})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<CountTo
|
<CountTo
|
||||||
class="number box-title"
|
class="number box-title"
|
||||||
:endVal="homestatistics.customer_init"
|
:endVal="homestatistics.customer_init"
|
||||||
:duration="1000"
|
:duration="1000"
|
||||||
separator=""
|
separator=""
|
||||||
></CountTo>
|
>
|
||||||
|
</CountTo>
|
||||||
<div class="change-box">
|
<div class="change-box">
|
||||||
<span class="change-text">跟进中</span>
|
<span class="change-text">跟进中</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :md="6" :lg="4">
|
<el-col :sm="12" :md="6" :lg="4">
|
||||||
<div class="card art-custom-card" :style="{ backgroundImage: `url(${gjing})`, backgroundSize: 'cover', backgroundPosition: 'center' }">
|
<div
|
||||||
|
class="card art-custom-card"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${gjing})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<CountTo
|
<CountTo
|
||||||
class="number box-title"
|
class="number box-title"
|
||||||
:endVal="homestatistics.customero_fllow"
|
:endVal="homestatistics.customero_fllow"
|
||||||
:duration="1000"
|
:duration="1000"
|
||||||
separator=""
|
separator=""
|
||||||
></CountTo>
|
>
|
||||||
|
</CountTo>
|
||||||
<div class="change-box">
|
<div class="change-box">
|
||||||
<span class="change-text">已成交</span>
|
<span class="change-text">已成交</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :md="6" :lg="4">
|
<el-col :sm="12" :md="6" :lg="4">
|
||||||
<div class="card art-custom-card" :style="{ backgroundImage: `url(${loss})`, backgroundSize: 'cover', backgroundPosition: 'center' }">
|
<div
|
||||||
|
class="card art-custom-card"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${loss})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<CountTo
|
<CountTo
|
||||||
class="number box-title"
|
class="number box-title"
|
||||||
:endVal="homestatistics.customer_deal"
|
:endVal="homestatistics.customer_deal"
|
||||||
:duration="1000"
|
:duration="1000"
|
||||||
separator=""
|
separator=""
|
||||||
></CountTo>
|
>
|
||||||
|
</CountTo>
|
||||||
<div class="change-box">
|
<div class="change-box">
|
||||||
<span class="change-text">已丢失</span>
|
<span class="change-text">已丢失</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,18 +160,32 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="tableData" :currentPage="pagination.currentPage"
|
<ArtTable
|
||||||
:pageSize="pagination.pageSize" :total="pagination.total" @size-change="handleSizeChange"
|
ref="tableRef"
|
||||||
@current-change="handleCurrentChange" lass="customer-table">
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-table-column prop="contacts" label="客户名称" min-width="120"></el-table-column>
|
<el-table-column prop="contacts" label="客户名称" min-width="120"></el-table-column>
|
||||||
<el-table-column prop="phone" label="电话" min-width="120"></el-table-column>
|
<el-table-column prop="phone" label="电话" min-width="120"></el-table-column>
|
||||||
<el-table-column prop="follow_next" label="下次跟进时间" min-width="140"></el-table-column>
|
<el-table-column
|
||||||
|
prop="follow_next"
|
||||||
|
label="下次跟进时间"
|
||||||
|
min-width="140"
|
||||||
|
></el-table-column>
|
||||||
<el-table-column label="状态" min-width="140">
|
<el-table-column label="状态" min-width="140">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span :class="getCountdownClass(scope.row.follow_next)">
|
<span :class="getCountdownClass(scope.row.follow_next)">
|
||||||
|
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
|
||||||
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px;"><i class="iconfont-sys"></i></span>
|
<i class="iconfont-sys"></i>
|
||||||
|
</span>
|
||||||
{{ getCountdown(scope.row.follow_next) }}
|
{{ getCountdown(scope.row.follow_next) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -130,7 +193,9 @@
|
|||||||
<el-table-column label="操作" min-width="100">
|
<el-table-column label="操作" min-width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="follow-action" @click="goAddFollow(scope.row)">
|
<div class="follow-action" @click="goAddFollow(scope.row)">
|
||||||
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px;"><i class="iconfont-sys"></i></span>
|
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
|
||||||
|
<i class="iconfont-sys"></i>
|
||||||
|
</span>
|
||||||
跟进
|
跟进
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -206,7 +271,7 @@ const gettableList = async () => {
|
|||||||
|
|
||||||
if (remindRange.value) params.filter = remindRange.value
|
if (remindRange.value) params.filter = remindRange.value
|
||||||
const { data, current, size, total } = await CrmService.waitFollow(params)
|
const { data, current, size, total } = await CrmService.waitFollow(params)
|
||||||
tableData.value = data.map((item: any, index: number) => ({
|
tableData.value = data.map((item: any) => ({
|
||||||
...item
|
...item
|
||||||
}))
|
}))
|
||||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
@@ -230,7 +295,7 @@ const timer = ref(0)
|
|||||||
const refreshKey = ref(0)
|
const refreshKey = ref(0)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gethomestatistics()
|
gethomestatistics('')
|
||||||
gettableList()
|
gettableList()
|
||||||
timer.value = window.setInterval(() => {
|
timer.value = window.setInterval(() => {
|
||||||
refreshKey.value++
|
refreshKey.value++
|
||||||
@@ -245,6 +310,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
function getCountdown(targetDate: string) {
|
function getCountdown(targetDate: string) {
|
||||||
// 让 refreshKey 成为依赖,确保每秒刷新
|
// 让 refreshKey 成为依赖,确保每秒刷新
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
refreshKey.value
|
refreshKey.value
|
||||||
const now = dayjs()
|
const now = dayjs()
|
||||||
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||||
@@ -276,26 +342,38 @@ function goAddFollow(row: any) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.table {
|
||||||
|
::v-deep(.el-scrollbar__view) {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 400px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
.homeheard {
|
.homeheard {
|
||||||
background:
|
background:
|
||||||
url('@/assets/img/crm/hometop.png') no-repeat center/cover,
|
url('@/assets/img/crm/hometop.png') no-repeat center/cover,
|
||||||
linear-gradient(90deg, #EDF2FF 0%, #CBE5FC 72.55%);
|
linear-gradient(90deg, #edf2ff 0%, #cbe5fc 72.55%);
|
||||||
padding: 10px;
|
padding: 10px 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center; /* 垂直居中 */
|
align-items: center;
|
||||||
|
/* 垂直居中 */
|
||||||
color: #333333;
|
color: #333333;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.daynum {
|
.daynum {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
color: #3471FF;
|
color: #3471ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day {
|
.day {
|
||||||
font-size: 70px;
|
font-size: 70px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.card-list {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -382,16 +460,18 @@ display: flex;
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
.heard-title {
|
.heard-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-left: 4px solid #3471FF;
|
border-left: 4px solid #3471ff;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heard-select {
|
.heard-select {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
@@ -409,46 +489,56 @@ display: flex;
|
|||||||
|
|
||||||
.table {
|
.table {
|
||||||
::v-deep .custom-table-header {
|
::v-deep .custom-table-header {
|
||||||
background: #F7F9FA !important;
|
background: #f7f9fa !important;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table__body tr.custom-table-row td {
|
::v-deep .el-table__body tr.custom-table-row td {
|
||||||
border-bottom: 1px solid #F7F9FA !important;
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
border-right: 1px solid #F7F9FA !important;
|
border-right: 1px solid #f7f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table__body tr td:last-child {
|
::v-deep .el-table__body tr td:last-child {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table__body tr:last-child td {
|
::v-deep .el-table__body tr:last-child td {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table__header-wrapper th {
|
::v-deep .el-table__header-wrapper th {
|
||||||
border-bottom: 1px solid #F7F9FA !important;
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
border-right: 1px solid #F7F9FA !important;
|
border-right: 1px solid #f7f9fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table__header-wrapper th:last-child {
|
::v-deep .el-table__header-wrapper th:last-child {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table {
|
::v-deep .el-table {
|
||||||
border: 1px solid #F7F9FA !important;
|
border: 1px solid #f7f9fa !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.countdown-danger {
|
.countdown-danger {
|
||||||
color: #FF0000 !important;
|
color: #ff0000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.countdown-success {
|
.countdown-success {
|
||||||
color: #00A43F !important;
|
color: #00a43f !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.follow-action {
|
.follow-action {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #409EFF;
|
color: #409eff;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="recycle-page">
|
<div class="recycle-page">
|
||||||
<el-card class="recycle-card">
|
<el-card class="recycle-card">
|
||||||
<div class="recycle-title">
|
<div class="recycle-title">
|
||||||
<span class="title-dot"></span>回收站
|
<span class="title-dot"></span>
|
||||||
|
回收站
|
||||||
</div>
|
</div>
|
||||||
<ArtTable
|
<ArtTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
@@ -22,7 +23,9 @@
|
|||||||
<el-table-column prop="phone" label="联系方式" min-width="120"></el-table-column>
|
<el-table-column prop="phone" label="联系方式" min-width="120"></el-table-column>
|
||||||
<el-table-column fixed="right" label="操作" width="160px">
|
<el-table-column fixed="right" label="操作" width="160px">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" @click="onDetail(scope.row)" size="small">恢复客户</el-button>
|
<el-button type="primary" @click="onDetail(scope.row)" size="small"
|
||||||
|
>恢复客户
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
@@ -32,13 +35,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import { CrmService } from '@/api/crmApi'
|
import { CrmService } from '@/api/crmApi'
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 10,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -54,7 +57,7 @@ const gettableList = async () => {
|
|||||||
recycle: 'recycle'
|
recycle: 'recycle'
|
||||||
}
|
}
|
||||||
const { data, current, size, total } = await CrmService.getcustomerList(params)
|
const { data, current, size, total } = await CrmService.getcustomerList(params)
|
||||||
tableData.value = data.map((item: any, index: number) => ({
|
tableData.value = data.map((item: any) => ({
|
||||||
...item
|
...item
|
||||||
}))
|
}))
|
||||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
@@ -84,20 +87,18 @@ onMounted(() => {
|
|||||||
gettableList()
|
gettableList()
|
||||||
})
|
})
|
||||||
const onDetail = async (row: any) => {
|
const onDetail = async (row: any) => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm('确定要恢复此客户吗?', '提示', {
|
||||||
'确定要恢复此客户吗?',
|
|
||||||
'提示',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning'
|
||||||
}
|
})
|
||||||
).then(async () => {
|
.then(async () => {
|
||||||
const res = await CrmService.customerrestore({ id: row.id })
|
const res = await CrmService.customerrestore({ id: row.id })
|
||||||
ElMessage.success(res?.message || '提交成功!')
|
ElMessage.success(res?.message || '提交成功!')
|
||||||
gettableList()
|
gettableList()
|
||||||
// 你可以在这里调用接口或做后续处理
|
// 你可以在这里调用接口或做后续处理
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// 用户点击了取消
|
// 用户点击了取消
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -131,4 +132,7 @@ const onDetail = async (row: any) => {
|
|||||||
.el-table .hide-header th {
|
.el-table .hide-header th {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 400px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="组员:" label-width="70px">
|
||||||
|
<el-select
|
||||||
|
v-model="selectValue"
|
||||||
|
style="min-width: 120px"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择组员"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="v in userOptions"
|
||||||
|
:key="v.id"
|
||||||
|
:label="v.name"
|
||||||
|
:value="v.id"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table boxshadow">
|
||||||
|
<el-button type="primary" @click="gotransfer" :disabled="selectedRows.length === 0">
|
||||||
|
<i class="iconfont-sys"></i> 批量转移
|
||||||
|
</el-button>
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:marginTop="10"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="code" label="编号" align="center" />
|
||||||
|
<el-table-column prop="contacts" label="客户名称"></el-table-column>
|
||||||
|
<el-table-column prop="phone" label="电话"></el-table-column>
|
||||||
|
<el-table-column prop="WhatsApp" label="WhatsApp"></el-table-column>
|
||||||
|
<el-table-column prop="email" label="邮箱"></el-table-column>
|
||||||
|
<el-table-column prop="magnitude_txt" label="客户商机"></el-table-column>
|
||||||
|
<el-table-column prop="channel_name" label="渠道"></el-table-column>
|
||||||
|
<el-table-column prop="country_name" label="国家"></el-table-column>
|
||||||
|
<el-table-column prop="user_name" label="跟单人"></el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="zyDialogVisible" title="转移" width="360px">
|
||||||
|
<el-form :model="zyForm" label-width="0">
|
||||||
|
<el-form-item>
|
||||||
|
<el-select
|
||||||
|
v-model="zyForm.user_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择转移人"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||||
|
<el-button @click="zyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handlezySubmit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { CrmService } from '@/api/crmApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { manageService } from '@/api/adminApi'
|
||||||
|
const selectValue = ref<string[]>([])
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const zyForm = reactive({
|
||||||
|
customer_id: '',
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
watch([selectValue, keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const userOptions = ref<any>()
|
||||||
|
const getuserList = async () => {
|
||||||
|
// 这里调用获取用户列表的接口
|
||||||
|
const res = await manageService.presetusers()
|
||||||
|
userOptions.value = res || []
|
||||||
|
// return res || []
|
||||||
|
}
|
||||||
|
const remindRange = ref('')
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
watch([remindRange], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
// 获取列表数据
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
}
|
||||||
|
if (selectValue.value) {
|
||||||
|
params.user_id = selectValue.value
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
const { records, current, size, total } = await CrmService.getCustomercsList(params)
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
// 处理表格行选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const timer = ref(0)
|
||||||
|
const refreshKey = ref(0)
|
||||||
|
const zyDialogVisible = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getuserList()
|
||||||
|
gettableList()
|
||||||
|
timer.value = window.setInterval(() => {
|
||||||
|
refreshKey.value++
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const gotransfer = () => {
|
||||||
|
zyDialogVisible.value = true
|
||||||
|
}
|
||||||
|
// 转移
|
||||||
|
const handlezySubmit = async () => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
|
||||||
|
const res = await CrmService.cedeCustomer(zyForm)
|
||||||
|
ElMessage.success(res?.message || '批量转移成功!')
|
||||||
|
zyDialogVisible.value = false
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.detils-content {
|
||||||
|
padding-left: 15px;
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.boxshadow {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 10, 118, 0.1);
|
||||||
|
background: var(--el-card-bg-color);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heard {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.card-list {
|
||||||
|
.art-custom-card {
|
||||||
|
.iconfont-sys {
|
||||||
|
background-color: #232323 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
margin-top: 10px;
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 300px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
:deep(.el-table th),
|
||||||
|
:deep(.el-table td) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.countdown-danger {
|
||||||
|
color: #ff0000 !important;
|
||||||
|
}
|
||||||
|
.countdown-success {
|
||||||
|
color: #00a43f !important;
|
||||||
|
}
|
||||||
|
.follow-detli {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #00a43f;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.follow-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-page">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="customer-toolbar boxshadow">
|
||||||
|
<div class="customer-search-group">
|
||||||
|
<el-form-item label="关键字:" label-width="70px">
|
||||||
|
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" label-width="50px">
|
||||||
|
<el-select v-model="selectValue" style="min-width: 120px">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="正常" value="1" />
|
||||||
|
<el-option label="离职" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxshadow">
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
lass="customer-table"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<!-- <el-table-column prop="id" label="用户ID" align="center" /> -->
|
||||||
|
<el-table-column prop="sn" label="SN" align="center" />
|
||||||
|
<el-table-column prop="follow" label="头像" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<img
|
||||||
|
:src="scope.row.avatar"
|
||||||
|
alt="头像"
|
||||||
|
class="avatar"
|
||||||
|
@click="handleAvatarClick(scope.row.avatar)"
|
||||||
|
style="cursor: pointer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="名字" align="center" />
|
||||||
|
<el-table-column prop="phone" label="电话" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" align="center" />
|
||||||
|
<!-- <el-table-column prop="corp_name" label="所属公司" align="center" /> -->
|
||||||
|
<el-table-column prop="follow" label="状态" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="statusClass(scope.row.status)">
|
||||||
|
{{ scope.row.status == 1 ? '在职' : '离职' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="customer_count" label="客户数量" align="center" />
|
||||||
|
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="warning"
|
||||||
|
underline="never"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
@click="statusfn(scope.row)"
|
||||||
|
v-if="hasAuth('crm_customer@status') && scope.row.status == 1"
|
||||||
|
>
|
||||||
|
<i class="iconfont-sys"></i>标记离职
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</div>
|
||||||
|
<!-- 图片预览组件 -->
|
||||||
|
<ElImageViewer
|
||||||
|
v-if="showImageViewer"
|
||||||
|
:url-list="[currentImageUrl]"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
@close="showImageViewer = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { ElImageViewer } from 'element-plus'
|
||||||
|
import { CrmService } from '@/api/crmApi'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const selectValue = ref<string[]>([])
|
||||||
|
const keyword = ref<string>('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
onMounted(() => {
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
// tabs 切换联动
|
||||||
|
watch([selectValue, keyword], () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
const gettableList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { currentPage, pageSize } = pagination
|
||||||
|
const params: any = {
|
||||||
|
current: currentPage,
|
||||||
|
size: pageSize
|
||||||
|
}
|
||||||
|
if (selectValue.value) {
|
||||||
|
params.status = selectValue.value
|
||||||
|
}
|
||||||
|
if (keyword.value) {
|
||||||
|
params.keyword = keyword.value
|
||||||
|
}
|
||||||
|
const { records, current, size, total } = await CrmService.getCustomerUserList(params)
|
||||||
|
tableData.value = records.map((item: any) => ({
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.currentPage = newCurrentPage
|
||||||
|
gettableList()
|
||||||
|
}
|
||||||
|
function statusClass(status: any) {
|
||||||
|
if (status === 1) return 'status-following'
|
||||||
|
if (status === 0) return 'status-lost'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
// 图片预览相关
|
||||||
|
const showImageViewer = ref(false)
|
||||||
|
const currentImageUrl = ref('')
|
||||||
|
|
||||||
|
const handleAvatarClick = (avatarUrl: string) => {
|
||||||
|
if (avatarUrl) {
|
||||||
|
currentImageUrl.value = avatarUrl
|
||||||
|
showImageViewer.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const statusfn = (row?: any) => {
|
||||||
|
ElMessageBox.confirm('确定标记离职吗?', '标记确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}).then(async () => {
|
||||||
|
const { message } = await CrmService.markLeave({
|
||||||
|
user_id: row.id
|
||||||
|
})
|
||||||
|
ElMessage.success(message)
|
||||||
|
gettableList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-badge) {
|
||||||
|
--el-badge-size: 16px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.itemtag {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f7f9fa !important;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
:deep(.el-table__empty-block) {
|
||||||
|
height: 450px !important;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr.custom-table-row td) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr td:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body tr:last-child td) {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th) {
|
||||||
|
border-bottom: 1px solid #f7f9fa !important;
|
||||||
|
border-right: 1px solid #f7f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper th:last-child) {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border: 1px solid #f7f9fa !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-page {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tags {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
width: 120px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-following {
|
||||||
|
color: #00a43f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dealed {
|
||||||
|
color: #3471ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-table {
|
||||||
|
background: var(--art-main-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-more-btn {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.form-submit-row {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<div class="system-notification-demo">
|
||||||
|
<h2>浏览器原生Notification API测试Demo</h2>
|
||||||
|
<div class="permission-status">
|
||||||
|
<p
|
||||||
|
>当前通知权限状态: <strong>{{ permissionStatus }}</strong></p
|
||||||
|
>
|
||||||
|
<el-button v-if="permissionStatus !== 'granted'" @click="requestPermission" type="primary">
|
||||||
|
请求通知权限
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<el-button @click="showBasicNotification">基本通知</el-button>
|
||||||
|
<el-button @click="showIconNotification">带图标通知</el-button>
|
||||||
|
<el-button @click="showPersistentNotification">持久化通知</el-button>
|
||||||
|
<el-button @click="showDataNotification">带数据的通知</el-button>
|
||||||
|
<el-button @click="showEventNotification">带事件监听的通知</el-button>
|
||||||
|
<el-button @click="checkSupport">检查浏览器支持</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="log-area">
|
||||||
|
<h3>操作日志</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(log, index) in logs" :key="index">{{ log }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
// import { ElButton } from 'element-plus'
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
const permissionStatus = ref('default')
|
||||||
|
const logs = ref([])
|
||||||
|
const isSupported = ref(false)
|
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
const addLog = (message) => {
|
||||||
|
const timestamp = new Date().toLocaleTimeString()
|
||||||
|
logs.value.unshift(`[${timestamp}] ${message}`)
|
||||||
|
// 限制日志数量
|
||||||
|
if (logs.value.length > 10) {
|
||||||
|
logs.value.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查浏览器支持情况
|
||||||
|
const checkSupport = () => {
|
||||||
|
if ('Notification' in window) {
|
||||||
|
isSupported.value = true
|
||||||
|
addLog('浏览器支持原生Notification API')
|
||||||
|
addLog(`当前权限状态: ${Notification.permission}`)
|
||||||
|
} else {
|
||||||
|
isSupported.value = false
|
||||||
|
addLog('⚠️ 浏览器不支持原生Notification API')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求通知权限
|
||||||
|
const requestPermission = async () => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
addLog('浏览器不支持原生Notification API')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const permission = await Notification.requestPermission()
|
||||||
|
permissionStatus.value = permission
|
||||||
|
if (permission === 'granted') {
|
||||||
|
addLog('用户已授予通知权限')
|
||||||
|
} else if (permission === 'denied') {
|
||||||
|
addLog('用户拒绝了通知权限')
|
||||||
|
} else {
|
||||||
|
addLog('用户暂时未授予通知权限')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`请求权限时出错: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示基本通知
|
||||||
|
const showBasicNotification = () => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
addLog('浏览器不支持原生Notification API')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
addLog('请先授予通知权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const notification = new Notification('基本通知', {
|
||||||
|
body: '这是一个使用浏览器原生API的系统通知'
|
||||||
|
})
|
||||||
|
notification.onclick = () => {
|
||||||
|
addLog('用户点击了基本通知')
|
||||||
|
notification.close()
|
||||||
|
}
|
||||||
|
addLog('已发送基本通知')
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`发送通知失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示带图标通知
|
||||||
|
const showIconNotification = () => {
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
addLog('请先授予通知权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const notification = new Notification('带图标通知', {
|
||||||
|
body: '这个通知包含自定义图标',
|
||||||
|
icon: 'https://element-plus.org/images/element-plus-logo.svg', // 替换为实际图标URL
|
||||||
|
badge: 'https://element-plus.org/images/element-plus-logo.svg' // 小图标(移动端)
|
||||||
|
})
|
||||||
|
addLog('已发送带图标通知')
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`发送通知失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示持久化通知
|
||||||
|
const showPersistentNotification = () => {
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
addLog('请先授予通知权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const notification = new Notification('持久化通知', {
|
||||||
|
body: '这个通知需要用户手动关闭',
|
||||||
|
requireInteraction: true, // 持久化显示,直到用户交互
|
||||||
|
icon: 'https://element-plus.org/images/element-plus-logo.svg'
|
||||||
|
})
|
||||||
|
notification.onclose = () => {
|
||||||
|
addLog('用户关闭了持久化通知')
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog('已发送持久化通知')
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`发送通知失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示带数据的通知
|
||||||
|
const showDataNotification = () => {
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
addLog('请先授予通知权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notification = new Notification('带数据的通知', {
|
||||||
|
body: '这个通知包含自定义数据',
|
||||||
|
data: {
|
||||||
|
url: 'https://example.com',
|
||||||
|
id: 12345,
|
||||||
|
message: '这是附加数据'
|
||||||
|
},
|
||||||
|
icon: 'https://element-plus.org/images/element-plus-logo.svg'
|
||||||
|
})
|
||||||
|
|
||||||
|
notification.onclick = () => {
|
||||||
|
addLog(`通知数据: ${JSON.stringify(notification.data)}`)
|
||||||
|
window.open(notification.data.url)
|
||||||
|
notification.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog('已发送带数据的通知')
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`发送通知失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示带事件监听的通知
|
||||||
|
const showEventNotification = () => {
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
addLog('请先授予通知权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notification = new Notification('带事件监听的通知', {
|
||||||
|
body: '这个通知演示各种事件监听',
|
||||||
|
icon: 'https://element-plus.org/images/element-plus-logo.svg'
|
||||||
|
})
|
||||||
|
|
||||||
|
notification.onshow = () => {
|
||||||
|
addLog('通知显示事件触发')
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.onclick = () => {
|
||||||
|
addLog('通知点击事件触发')
|
||||||
|
notification.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.onclose = () => {
|
||||||
|
addLog('通知关闭事件触发')
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.onerror = (error) => {
|
||||||
|
addLog(`通知错误事件: ${error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog('已发送带事件监听的通知')
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`发送通知失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查权限状态
|
||||||
|
onMounted(() => {
|
||||||
|
addLog('页面加载完成,初始化Demo')
|
||||||
|
|
||||||
|
// 检查浏览器支持
|
||||||
|
checkSupport()
|
||||||
|
|
||||||
|
// 检查当前权限状态
|
||||||
|
if ('Notification' in window) {
|
||||||
|
permissionStatus.value = Notification.permission
|
||||||
|
addLog(`初始化权限状态: ${Notification.permission}`)
|
||||||
|
|
||||||
|
// 监听权限变化
|
||||||
|
if (Notification.permissionChange) {
|
||||||
|
Notification.permissionChange.then((permission) => {
|
||||||
|
permissionStatus.value = permission
|
||||||
|
addLog(`通知权限已变更为: ${permission}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-notification-demo {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-status {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-area {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-area ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-area li {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-area li:first-child {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,9 +4,14 @@
|
|||||||
<div class="left-wrap">
|
<div class="left-wrap">
|
||||||
<div class="user-wrap box-style">
|
<div class="user-wrap box-style">
|
||||||
<!-- <img class="bg" src="@imgs/user/bg.webp" /> -->
|
<!-- <img class="bg" src="@imgs/user/bg.webp" /> -->
|
||||||
<img class="avatar" :src="userInfo.avatar_full" />
|
<img
|
||||||
|
class="avatar"
|
||||||
|
:src="
|
||||||
|
userInfo.avatar_full != '' ? userInfo.avatar_full : '/src/assets/img/user/avatar.webp'
|
||||||
|
"
|
||||||
|
/>
|
||||||
<h2 class="name">{{ userInfo.name }}</h2>
|
<h2 class="name">{{ userInfo.name }}</h2>
|
||||||
<p class="des">ID:{{userInfo.id}}</p>
|
<p class="des">SN:{{ userInfo.sn }}</p>
|
||||||
<div class="outer-info">
|
<div class="outer-info">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">电 话:</span>
|
<span class="info-label">电 话:</span>
|
||||||
@@ -29,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="right-wrap">
|
<div class="right-wrap">
|
||||||
<div class="info box-style">
|
<div class="info box-style">
|
||||||
<h1 class="title"><span class="border" style="margin-right: 5px;"></span>基本信息</h1>
|
<h1 class="title"><span class="border" style="margin-right: 5px"></span>基本信息</h1>
|
||||||
|
|
||||||
<ElForm
|
<ElForm
|
||||||
:model="form"
|
:model="form"
|
||||||
@@ -45,9 +50,15 @@
|
|||||||
:headers="uploadHeaders"
|
:headers="uploadHeaders"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleUploadavatar"
|
:on-success="handleUploadavatar"
|
||||||
|
:disabled="!isEdit"
|
||||||
>
|
>
|
||||||
<div class="qrcode-upload-box">
|
<div class="qrcode-upload-box">
|
||||||
<img class="avatar" :src="imageUrlavatar"/>
|
<img
|
||||||
|
class="avatar"
|
||||||
|
:src="
|
||||||
|
imageUrlavatar != '' ? imageUrlavatar : '/src/assets/img/user/avatar.webp'
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -56,7 +67,7 @@
|
|||||||
<el-input v-model="form.name" :disabled="!isEdit" />
|
<el-input v-model="form.name" :disabled="!isEdit" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="性别" prop="sex" class="right-input">
|
<ElFormItem label="性别" prop="sex" class="right-input">
|
||||||
<ElSelect v-model="form.sex" placeholder="Select" :disabled="!isEdit">
|
<ElSelect v-model="form.sex" placeholder="未知" :disabled="!isEdit">
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in options"
|
v-for="item in options"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
@@ -69,7 +80,7 @@
|
|||||||
|
|
||||||
<ElRow>
|
<ElRow>
|
||||||
<ElFormItem label="联系方式" prop="phone">
|
<ElFormItem label="联系方式" prop="phone">
|
||||||
<ElInput v-model="form.phone" :disabled="!isEdit" />
|
<ElInput v-model="form.phone" disabled />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="职务" prop="position" class="right-input">
|
<ElFormItem label="职务" prop="position" class="right-input">
|
||||||
<ElInput v-model="form.position" :disabled="!isEdit" />
|
<ElInput v-model="form.position" :disabled="!isEdit" />
|
||||||
@@ -81,19 +92,42 @@
|
|||||||
<ElInput v-model="form.corp_name" disabled />
|
<ElInput v-model="form.corp_name" disabled />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="国家" prop="address" class="right-input">
|
<ElFormItem label="国家" prop="address" class="right-input">
|
||||||
<el-select v-model="form.country_id" filterable placeholder="请选择国家" :disabled="!isEdit" >
|
<el-select
|
||||||
<el-option v-for="item in countryOptions" :key="item.id" :label="item.cn_addr" :value="item.id" />
|
v-model="form.country_id"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择国家"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in countryOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.cn_addr"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
<ElRow>
|
<ElRow>
|
||||||
<ElFormItem label="二维码" prop="socialize" :class="isEdit?'qrcode-radio-group':''">
|
<ElFormItem label="邮箱" prop="email">
|
||||||
|
<ElInput v-model="form.email" :disabled="!isEdit" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow>
|
||||||
|
<ElFormItem
|
||||||
|
label="二维码"
|
||||||
|
prop="socialize"
|
||||||
|
:class="isEdit ? 'qrcode-radio-group' : ''"
|
||||||
|
>
|
||||||
<el-radio-group v-model="form.socialize" v-if="isEdit">
|
<el-radio-group v-model="form.socialize" v-if="isEdit">
|
||||||
<el-radio label="微信">微信</el-radio>
|
<el-radio label="微信">微信</el-radio>
|
||||||
<el-radio label="WhatsApp">WhatsApp</el-radio>
|
<el-radio label="WhatsApp">WhatsApp</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<img v-if="form.socialize_pic_full" :src="form.socialize_pic_full" class="qrcode-img" />
|
<img
|
||||||
|
v-if="form.socialize_pic_full"
|
||||||
|
:src="form.socialize_pic_full"
|
||||||
|
class="qrcode-img"
|
||||||
|
/>
|
||||||
<span v-else class="qrcode-upload-text">暂无二维码</span>
|
<span v-else class="qrcode-upload-text">暂无二维码</span>
|
||||||
</template>
|
</template>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -106,9 +140,13 @@
|
|||||||
:on-success="handleUploadavatarqrcode"
|
:on-success="handleUploadavatarqrcode"
|
||||||
>
|
>
|
||||||
<div class="qrcode-upload-box">
|
<div class="qrcode-upload-box">
|
||||||
<img v-if="form.socialize_pic_full" :src="socialize_pic_full" class="qrcode-img" />
|
<img
|
||||||
|
v-if="form.socialize_pic_full"
|
||||||
|
:src="socialize_pic_full"
|
||||||
|
class="qrcode-img"
|
||||||
|
/>
|
||||||
<span v-else class="qrcode-upload-text">
|
<span v-else class="qrcode-upload-text">
|
||||||
<img :src="imageUrl" class="qrcode-img" />
|
<img :src="imageUrls" class="qrcode-img" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
@@ -169,8 +207,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { ElForm, FormInstance } from 'element-plus'
|
||||||
import { ElForm, FormInstance, FormRules } from 'element-plus'
|
|
||||||
import imageUrl from '@/assets/img/crm/upimg.png'
|
import imageUrl from '@/assets/img/crm/upimg.png'
|
||||||
import { CrmService } from '@/api/crmApi'
|
import { CrmService } from '@/api/crmApi'
|
||||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||||
@@ -181,6 +218,7 @@
|
|||||||
interface CustomerInfo {
|
interface CustomerInfo {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
const imageUrls = ref(imageUrl)
|
||||||
const imageUrlavatar = ref('')
|
const imageUrlavatar = ref('')
|
||||||
const socialize_pic_full = ref('')
|
const socialize_pic_full = ref('')
|
||||||
const countryOptions = ref<any>()
|
const countryOptions = ref<any>()
|
||||||
@@ -231,6 +269,10 @@
|
|||||||
// })
|
// })
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: '未知'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: '1',
|
value: '1',
|
||||||
label: '男'
|
label: '男'
|
||||||
@@ -277,6 +319,8 @@
|
|||||||
function handleUploadavatarqrcode(response: any) {
|
function handleUploadavatarqrcode(response: any) {
|
||||||
// 假设返回图片地址字段为 response.url 或 response.data.url
|
// 假设返回图片地址字段为 response.url 或 response.data.url
|
||||||
socialize_pic_full.value = response.url || (response.data && response.data.file_url) || ''
|
socialize_pic_full.value = response.url || (response.data && response.data.file_url) || ''
|
||||||
|
imageUrls.value = response.url || (response.data && response.data.file_url) || ''
|
||||||
|
|
||||||
// 如需同步到 form.picture
|
// 如需同步到 form.picture
|
||||||
form.value.socialize_pic = response.url || (response.data && response.data.file_name) || ''
|
form.value.socialize_pic = response.url || (response.data && response.data.file_name) || ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div v-if="!isMobile">
|
<div v-if="!isMobile">
|
||||||
|
|
||||||
<ElRow>
|
<ElRow>
|
||||||
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
||||||
<ElInput placeholder="工单编号"></ElInput>
|
<ElInput placeholder="工单编号"></ElInput>
|
||||||
@@ -9,13 +8,23 @@
|
|||||||
<div style="width: 12px"></div>
|
<div style="width: 12px"></div>
|
||||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||||
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
||||||
<ElButton v-if="hasAuth('work_order_self@add')" @click="showDialog('add')" v-ripple>新增</ElButton>
|
<ElButton v-if="hasAuth('work_order_self@add')" @click="showDialog('add')" v-ripple>
|
||||||
|
新增
|
||||||
|
</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
|
|
||||||
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="tableData" :currentPage="pagination.currentPage"
|
<ArtTable
|
||||||
:pageSize="pagination.pageSize" :total="pagination.total" @size-change="handleSizeChange"
|
ref="tableRef"
|
||||||
@current-change="handleCurrentChange">
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
:currentPage="pagination.currentPage"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn label="工单编号" prop="code" />
|
<ElTableColumn label="工单编号" prop="code" />
|
||||||
<ElTableColumn label="问题分类" prop="categorie_id">
|
<ElTableColumn label="问题分类" prop="categorie_id">
|
||||||
@@ -40,9 +49,27 @@
|
|||||||
<ElTableColumn fixed="right" label="操作" width="240px">
|
<ElTableColumn fixed="right" label="操作" width="240px">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<ElRow>
|
<ElRow>
|
||||||
<el-button v-if="scope.row.status == 1&&hasAuth('work_order_self@edit')" link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
<el-button
|
||||||
<el-button v-if="hasAuth('work_order_self@status')" link @click="statusEdit(scope.row)"> 状态编辑 </el-button>
|
v-if="scope.row.status == 1 && hasAuth('work_order_self@edit')"
|
||||||
<el-button v-if="hasAuth('work_order_self@conversation_list')" link @click="openChat(scope.row)"> 查看/回复 </el-button>
|
link
|
||||||
|
@click="showDialog('edit', scope.row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="hasAuth('work_order_self@status')"
|
||||||
|
link
|
||||||
|
@click="statusEdit(scope.row)"
|
||||||
|
>
|
||||||
|
状态编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="hasAuth('work_order_self@conversation_list')"
|
||||||
|
link
|
||||||
|
@click="openChat(scope.row)"
|
||||||
|
>
|
||||||
|
查看/回复
|
||||||
|
</el-button>
|
||||||
<!-- <el-button link @click="deleteRole()"> 删除 </el-button> -->
|
<!-- <el-button link @click="deleteRole()"> 删除 </el-button> -->
|
||||||
</ElRow>
|
</ElRow>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,8 +77,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" width="60%" align-center>
|
<ElDialog
|
||||||
<ElForm ref="formRef" :model="form" label-width="160px">
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||||
|
width="60%"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="form" label-width="120px" style="padding-right: 30px">
|
||||||
<ElFormItem label="问题分类" prop="categorie_id">
|
<ElFormItem label="问题分类" prop="categorie_id">
|
||||||
<ElSelect v-model="form.categorie_id">
|
<ElSelect v-model="form.categorie_id">
|
||||||
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
|
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
|
||||||
@@ -64,27 +96,63 @@
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="上传附件" prop="attachment">
|
<ElFormItem label="上传附件" prop="attachment">
|
||||||
<ElUpload
|
<ElUpload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
class="upload-demo"
|
||||||
|
:action="actionImageUplode"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
multiple
|
||||||
|
:limit="3"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
>
|
||||||
|
<el-button type="primary" :disabled="fileList.length >= 3">上传附件</el-button>
|
||||||
|
</ElUpload>
|
||||||
|
<!-- <ElUpload
|
||||||
v-model:file-list="fileList"
|
v-model:file-list="fileList"
|
||||||
class="upload-demo"
|
class="upload-demo"
|
||||||
:action="actionImageUplode"
|
:action="actionImageUplode"
|
||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:headers="uploadHeaders"
|
:headers="uploadHeaders"
|
||||||
multiple
|
multiple
|
||||||
:limit="3">
|
:limit="3"
|
||||||
<el-button type="primary">Click to upload</el-button>
|
>
|
||||||
</ElUpload>
|
<el-button type="primary">上传附件</el-button>
|
||||||
|
</ElUpload> -->
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="" prop="">
|
||||||
<!-- 附件预览区 -->
|
<!-- 附件预览区 -->
|
||||||
<div class="attachment-preview" v-if="fileList.length">
|
<div class="attachment-preview">
|
||||||
<div class="attachment-list">
|
<div v-if="fileList.length" class="attachment-list">
|
||||||
<div v-for="(file, idx) in fileList" :key="file.name" class="attachment-item">
|
<div v-for="(file, idx) in fileList" :key="file.name" class="attachment-item">
|
||||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
||||||
<img :src="file.file_url?file.file_url:file.response.data.file_url" :alt="file.name" class="attachment-thumb" @click="handleImageClick(imageList.findIndex(url => url === file.file_url?file.file_url:file.response.data.file_url))" />
|
<img
|
||||||
|
:src="file.file_url ? file.file_url : file.response.data.file_url"
|
||||||
|
:alt="file.name"
|
||||||
|
class="attachment-thumb"
|
||||||
|
@click="
|
||||||
|
handleImageClick(
|
||||||
|
imageList.findIndex((url) =>
|
||||||
|
url === file.file_url ? file.file_url : file.response.data.file_url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(file.name)">
|
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(file.name)">
|
||||||
<video :src="file.file_url?file.file_url:file.response.data.file_url" controls class="attachment-thumb" @click.prevent="handleVideoClick(file.file_url?file.file_url:file.response.data.file_url)"></video>
|
<video
|
||||||
|
:src="file.file_url ? file.file_url : file.response.data.file_url"
|
||||||
|
controls
|
||||||
|
class="attachment-thumb"
|
||||||
|
@click.prevent="
|
||||||
|
handleVideoClick(file.file_url ? file.file_url : file.response.data.file_url)
|
||||||
|
"
|
||||||
|
></video>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a :href="file.file_url?file.file_url:file.response.data.file_url" target="_blank">{{ file.name }}</a>
|
<a
|
||||||
|
:href="file.file_url ? file.file_url : file.response.data.file_url"
|
||||||
|
target="_blank"
|
||||||
|
>{{ file.name }}</a
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<div class="attachment-filename">{{ file.name }}</div>
|
<div class="attachment-filename">{{ file.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,18 +183,96 @@
|
|||||||
<div class="chat-messages" ref="messageContainer">
|
<div class="chat-messages" ref="messageContainer">
|
||||||
<template v-for="(message, index) in messages" :key="index">
|
<template v-for="(message, index) in messages" :key="index">
|
||||||
<div :class="['message-item', String(message.user_id) === String(userid) ? 'message-right' : 'message-left']">
|
<div :class="['message-item', String(message.user_id) === String(userid) ? 'message-right' : 'message-left']">
|
||||||
<el-avatar :size="32" :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" class="message-avatar" />
|
<el-avatar :size="32" :src="message.user_avatar" class="message-avatar" />
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<div class="message-info">
|
<div class="message-info">
|
||||||
<span class="sender-name">{{ message.user_id }}</span>
|
<span class="sender-name">{{ message.user_name }}</span>
|
||||||
<span class="message-time">{{ message.updated_at }}</span>
|
<span class="message-time">{{ message.updated_at }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-text">{{ message.content }}</div>
|
<div class="message-text">{{ message.content }}</div>
|
||||||
|
<!-- 附件显示区域 -->
|
||||||
|
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
|
||||||
|
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
|
||||||
|
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
|
||||||
|
<img
|
||||||
|
:src="attachment.storage_path_full"
|
||||||
|
:alt="attachment.original_name"
|
||||||
|
class="attachment-thumb"
|
||||||
|
@click="handleChatImageClick(attachment.storage_path_full)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
|
||||||
|
<video
|
||||||
|
:src="attachment.storage_path_full"
|
||||||
|
controls
|
||||||
|
class="attachment-thumb"
|
||||||
|
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
|
||||||
|
></video>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="file-attachment">
|
||||||
|
<el-icon class="file-icon"><Document /></el-icon>
|
||||||
|
<span class="file-name">{{ attachment.original_name }}</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click="downloadAttachment(attachment)"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input" v-if="hasAuth('work_order_self@conversation_add')">
|
<div class="chat-input" v-if="hasAuth('work_order_self@conversation_add')">
|
||||||
|
<!-- 附件上传区域 -->
|
||||||
|
<div class="chat-attachment-upload">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="chatFileList"
|
||||||
|
class="chat-upload"
|
||||||
|
:action="actionImageUplode"
|
||||||
|
:on-success="handleChatUploadSuccess"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
multiple
|
||||||
|
:limit="5"
|
||||||
|
:on-exceed="handleChatExceed"
|
||||||
|
:show-file-list="false"
|
||||||
|
>
|
||||||
|
<el-button type="primary" link>
|
||||||
|
<el-icon><Paperclip /></el-icon>
|
||||||
|
添加附件
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<!-- 已上传附件预览 -->
|
||||||
|
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
|
||||||
|
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
|
||||||
|
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
||||||
|
<img
|
||||||
|
:src="file.file_url || file.response?.data?.file_url"
|
||||||
|
:alt="file.name"
|
||||||
|
class="preview-thumb"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-icon class="file-icon"><Document /></el-icon>
|
||||||
|
</template>
|
||||||
|
<span class="file-name">{{ file.name }}</span>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click="removeChatFile(idx)"
|
||||||
|
>
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
|
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
|
||||||
@keyup.enter.prevent="sendMessage" />
|
@keyup.enter.prevent="sendMessage" />
|
||||||
<ElInput v-model="ticket_id" type="hidden" />
|
<ElInput v-model="ticket_id" type="hidden" />
|
||||||
@@ -158,7 +304,7 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 图片预览弹窗 -->
|
<!-- 图片预览弹窗 -->
|
||||||
<ElImageViewer v-if="imageViewerVisible" :url-list="imageList" :initial-index="imageViewerIndex" @close="imageViewerVisible = false" />
|
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
|
||||||
<!-- 视频预览弹窗 -->
|
<!-- 视频预览弹窗 -->
|
||||||
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
||||||
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
|
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
|
||||||
@@ -179,6 +325,7 @@ import { ElImageViewer } from 'element-plus'
|
|||||||
import { useAuth } from "@/composables/useAuth";
|
import { useAuth } from "@/composables/useAuth";
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||||
|
import { Paperclip, Document, Close } from '@element-plus/icons-vue'
|
||||||
const { uploadHeaders } = useUploadHeaders()
|
const { uploadHeaders } = useUploadHeaders()
|
||||||
const { info } = useUserStore()
|
const { info } = useUserStore()
|
||||||
const userid = info.user.id
|
const userid = info.user.id
|
||||||
@@ -211,6 +358,10 @@ const scrollToBottom = () => {
|
|||||||
}
|
}
|
||||||
const openChat = (row: any) => {
|
const openChat = (row: any) => {
|
||||||
isDrawerVisible.value = true
|
isDrawerVisible.value = true
|
||||||
|
// 清空数据
|
||||||
|
messageText.value = ''
|
||||||
|
messages.value = []
|
||||||
|
chatFileList.value = []
|
||||||
getConversationList(row.id)
|
getConversationList(row.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +370,10 @@ const closeChat = () => {
|
|||||||
}
|
}
|
||||||
//获取状态 下拉框 getStatus
|
//获取状态 下拉框 getStatus
|
||||||
const statusList = ref<any>()
|
const statusList = ref<any>()
|
||||||
const getstatusList = async () => {
|
const getstatusList = async (id: any) => {
|
||||||
try {
|
try {
|
||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const response = await WorkService.getStatus()
|
const response = await WorkService.getStatus({ id: id })
|
||||||
statusList.value = response || []
|
statusList.value = response || []
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
@@ -312,14 +463,28 @@ const getConversationList = async (id:any) => {
|
|||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
const text = messageText.value.trim()
|
const text = messageText.value.trim()
|
||||||
const ids = ticket_id.value
|
const ids = ticket_id.value
|
||||||
if (!text) return
|
if (!text && chatFileList.value.length === 0) return
|
||||||
|
|
||||||
|
// 准备附件数据
|
||||||
|
const attachments = chatFileList.value.map(file => {
|
||||||
|
const data = file.response?.data
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
original_name: data.original_name,
|
||||||
|
storage_path: data.storage_path || data.file_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter(Boolean)
|
||||||
|
|
||||||
const { message } = await WorkService.conversationAdd({
|
const { message } = await WorkService.conversationAdd({
|
||||||
content: text,
|
content: text,
|
||||||
ticket_id: ids,
|
ticket_id: ids,
|
||||||
|
attachment: attachments
|
||||||
})
|
})
|
||||||
ElMessage.success(message)
|
ElMessage.success(message)
|
||||||
messageText.value = ''
|
messageText.value = ''
|
||||||
|
chatFileList.value = [] // 清空附件列表
|
||||||
getConversationList(ids)
|
getConversationList(ids)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
@@ -362,7 +527,7 @@ const statusform = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const statusEdit = (row: any) => {
|
const statusEdit = (row: any) => {
|
||||||
getstatusList()
|
getstatusList(row.id)
|
||||||
dialogVisiblestatus.value = true
|
dialogVisiblestatus.value = true
|
||||||
statusform.id = row.id
|
statusform.id = row.id
|
||||||
}
|
}
|
||||||
@@ -379,11 +544,14 @@ const delCodeTrim = (content: string): string => {
|
|||||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||||
}
|
}
|
||||||
//添加工单
|
//添加工单
|
||||||
|
const handleExceed = () => {
|
||||||
|
ElMessage.warning('最多上传3个文件')
|
||||||
|
}
|
||||||
const uploadedFiles = ref<any[]>([])
|
const uploadedFiles = ref<any[]>([])
|
||||||
const handleUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
const getupfiledata = () => {
|
||||||
// 只处理上传成功的文件
|
uploadedFiles.value = fileList.value
|
||||||
uploadedFiles.value = uploadFiles.filter(item => item.status === 'success').map(item => {
|
.filter((item) => item.status === 'success')
|
||||||
|
.map((item) => {
|
||||||
const data = item.response?.data
|
const data = item.response?.data
|
||||||
if (data) {
|
if (data) {
|
||||||
return {
|
return {
|
||||||
@@ -397,8 +565,10 @@ const handleUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[])
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
getupfiledata()
|
||||||
const { categorie_id} = form
|
const { categorie_id} = form
|
||||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||||
|
console.log(uploadedFiles.value)
|
||||||
if(dialogType.value === 'add'){
|
if(dialogType.value === 'add'){
|
||||||
const { message } = await WorkService.WorkAdd({
|
const { message } = await WorkService.WorkAdd({
|
||||||
content: editorHtml.value,
|
content: editorHtml.value,
|
||||||
@@ -450,6 +620,8 @@ const statusSubmit = async () => {
|
|||||||
const imageViewerVisible = ref(false)
|
const imageViewerVisible = ref(false)
|
||||||
const imageViewerIndex = ref(0)
|
const imageViewerIndex = ref(0)
|
||||||
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
|
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
|
||||||
|
// 聊天图片预览列表
|
||||||
|
const chatImageList = ref<string[]>([])
|
||||||
function handleImageClick(idx: number) {
|
function handleImageClick(idx: number) {
|
||||||
imageViewerIndex.value = idx
|
imageViewerIndex.value = idx
|
||||||
imageViewerVisible.value = true
|
imageViewerVisible.value = true
|
||||||
@@ -460,6 +632,52 @@ const currentVideoUrl = ref('')
|
|||||||
function handleVideoClick(url: string) {
|
function handleVideoClick(url: string) {
|
||||||
currentVideoUrl.value = url
|
currentVideoUrl.value = url
|
||||||
videoDialogVisible.value = true
|
videoDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聊天相关
|
||||||
|
const chatFileList = ref<any[]>([])
|
||||||
|
const handleChatUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
||||||
|
chatFileList.value = uploadFiles.filter(item => item.status === 'success').map(item => {
|
||||||
|
const data = item.response?.data
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
name: data.original_name,
|
||||||
|
file_url: data.storage_path_full,
|
||||||
|
status: 'success',
|
||||||
|
response: { data: data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter(Boolean)
|
||||||
|
}
|
||||||
|
const handleChatExceed = () => {
|
||||||
|
ElMessage.warning('最多上传5个文件')
|
||||||
|
}
|
||||||
|
const removeChatFile = (index: number) => {
|
||||||
|
chatFileList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
const handleChatImageClick = (url: string) => {
|
||||||
|
// 设置聊天图片预览列表
|
||||||
|
chatImageList.value = [url]
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
imageViewerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImageViewer = () => {
|
||||||
|
imageViewerVisible.value = false
|
||||||
|
chatImageList.value = [] // 清空聊天图片列表
|
||||||
|
}
|
||||||
|
const handleChatVideoClick = (url: string) => {
|
||||||
|
currentVideoUrl.value = url
|
||||||
|
videoDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const downloadAttachment = (attachment: any) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = attachment.storage_path_full
|
||||||
|
link.download = attachment.original_name
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -545,6 +763,7 @@ function handleVideoClick(url: string) {
|
|||||||
|
|
||||||
.message-info {
|
.message-info {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
@@ -561,6 +780,7 @@ function handleVideoClick(url: string) {
|
|||||||
|
|
||||||
.message-info {
|
.message-info {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
@@ -684,4 +904,101 @@ function handleVideoClick(url: string) {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 聊天附件样式
|
||||||
|
.message-right .message-attachments{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.message-left .message-attachments{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.message-attachments {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.attachment-thumb {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 150px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-attachment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-attachment-upload {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.chat-upload {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.attachment-preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
max-width: 200px;
|
||||||
|
|
||||||
|
.preview-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
word-break: break-all;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" width="60%" align-center>
|
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" width="60%" align-center>
|
||||||
<ElForm ref="formRef" :model="form" label-width="160px">
|
<ElForm ref="formRef" :model="form" label-width="120px" style="padding-right: 30px">
|
||||||
<ElFormItem label="问题分类" prop="categorie_id">
|
<ElFormItem label="问题分类" prop="categorie_id">
|
||||||
<ElSelect v-model="form.categorie_id">
|
<ElSelect v-model="form.categorie_id">
|
||||||
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
|
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
|
||||||
@@ -69,9 +69,13 @@
|
|||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:headers="uploadHeaders"
|
:headers="uploadHeaders"
|
||||||
multiple
|
multiple
|
||||||
:limit="3">
|
:limit="3"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
>
|
||||||
<el-button type="primary">上传</el-button>
|
<el-button type="primary">上传</el-button>
|
||||||
</ElUpload>
|
</ElUpload>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="" prop="">
|
||||||
<!-- 附件预览区 -->
|
<!-- 附件预览区 -->
|
||||||
<div class="attachment-preview" v-if="fileList.length">
|
<div class="attachment-preview" v-if="fileList.length">
|
||||||
<div class="attachment-list">
|
<div class="attachment-list">
|
||||||
@@ -113,21 +117,115 @@
|
|||||||
<!-- 聊天消息区域 -->
|
<!-- 聊天消息区域 -->
|
||||||
<div class="chat-messages" ref="messageContainer">
|
<div class="chat-messages" ref="messageContainer">
|
||||||
<template v-for="(message, index) in messages" :key="index">
|
<template v-for="(message, index) in messages" :key="index">
|
||||||
<div :class="['message-item', String(message.user_id) === String(userid) ? 'message-right' : 'message-left']">
|
<div
|
||||||
<el-avatar :size="32" :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" class="message-avatar" />
|
:class="[
|
||||||
|
'message-item',
|
||||||
|
String(message.user_id) === String(userid) ? 'message-right' : 'message-left'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" -->
|
||||||
|
<el-avatar
|
||||||
|
:size="32"
|
||||||
|
:src="message.user_avatar"
|
||||||
|
class="message-avatar"
|
||||||
|
/>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<div class="message-info">
|
<div class="message-info">
|
||||||
<span class="sender-name">{{ message.user_id }}</span>
|
<span class="sender-name">{{ message.user_name }}</span>
|
||||||
<span class="message-time">{{ message.updated_at }}</span>
|
<span class="message-time">{{ message.updated_at }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-text">{{ message.content }}</div>
|
<div class="message-text">{{ message.content }}</div>
|
||||||
|
<!-- 附件显示区域 -->
|
||||||
|
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
|
||||||
|
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
|
||||||
|
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
|
||||||
|
<img
|
||||||
|
:src="attachment.storage_path_full"
|
||||||
|
:alt="attachment.original_name"
|
||||||
|
class="attachment-thumb"
|
||||||
|
@click="handleChatImageClick(attachment.storage_path_full)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
|
||||||
|
<video
|
||||||
|
:src="attachment.storage_path_full"
|
||||||
|
controls
|
||||||
|
class="attachment-thumb"
|
||||||
|
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
|
||||||
|
></video>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="file-attachment">
|
||||||
|
<el-icon class="file-icon"><Document /></el-icon>
|
||||||
|
<span class="file-name">{{ attachment.original_name }}</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click="downloadAttachment(attachment)"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input" v-if="hasAuth('work_order@conversation_add')">
|
<div class="chat-input" v-if="hasAuth('work_order@conversation_add')">
|
||||||
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
|
<!-- 附件上传区域 -->
|
||||||
@keyup.enter.prevent="sendMessage" />
|
<div class="chat-attachment-upload">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="chatFileList"
|
||||||
|
class="chat-upload"
|
||||||
|
:action="actionImageUplode"
|
||||||
|
:on-success="handleChatUploadSuccess"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
multiple
|
||||||
|
:limit="5"
|
||||||
|
:on-exceed="handleChatExceed"
|
||||||
|
:show-file-list="false"
|
||||||
|
>
|
||||||
|
<el-button type="primary" link>
|
||||||
|
<el-icon><Paperclip /></el-icon>
|
||||||
|
添加附件
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<!-- 已上传附件预览 -->
|
||||||
|
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
|
||||||
|
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
|
||||||
|
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
||||||
|
<img
|
||||||
|
:src="file.file_url || file.response?.data?.file_url"
|
||||||
|
:alt="file.name"
|
||||||
|
class="preview-thumb"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-icon class="file-icon"><Document /></el-icon>
|
||||||
|
</template>
|
||||||
|
<span class="file-name">{{ file.name }}</span>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click="removeChatFile(idx)"
|
||||||
|
>
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="messageText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="输入消息"
|
||||||
|
resize="none"
|
||||||
|
@keyup.enter.prevent="sendMessage"
|
||||||
|
/>
|
||||||
<ElInput v-model="ticket_id" type="hidden" />
|
<ElInput v-model="ticket_id" type="hidden" />
|
||||||
<div class="chat-input-actions">
|
<div class="chat-input-actions">
|
||||||
<el-button type="primary" @click="sendMessage" v-ripple>发送</el-button>
|
<el-button type="primary" @click="sendMessage" v-ripple>发送</el-button>
|
||||||
@@ -157,7 +255,7 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 图片预览弹窗 -->
|
<!-- 图片预览弹窗 -->
|
||||||
<ElImageViewer v-if="imageViewerVisible" :url-list="imageList" :initial-index="imageViewerIndex" @close="imageViewerVisible = false" />
|
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
|
||||||
<!-- 视频预览弹窗 -->
|
<!-- 视频预览弹窗 -->
|
||||||
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
||||||
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
|
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
|
||||||
@@ -178,6 +276,7 @@ import { ElImageViewer } from 'element-plus'
|
|||||||
import { useAuth } from "@/composables/useAuth";
|
import { useAuth } from "@/composables/useAuth";
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||||
|
import { Paperclip, Document, Close } from '@element-plus/icons-vue'
|
||||||
const { uploadHeaders } = useUploadHeaders()
|
const { uploadHeaders } = useUploadHeaders()
|
||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
defineOptions({ name: 'Order' })
|
defineOptions({ name: 'Order' })
|
||||||
@@ -213,6 +312,10 @@ const scrollToBottom = () => {
|
|||||||
}
|
}
|
||||||
const openChat = (row: any) => {
|
const openChat = (row: any) => {
|
||||||
isDrawerVisible.value = true
|
isDrawerVisible.value = true
|
||||||
|
// 清空数据
|
||||||
|
messageText.value = ''
|
||||||
|
messages.value = []
|
||||||
|
chatFileList.value = []
|
||||||
getConversationList(row.id)
|
getConversationList(row.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +327,7 @@ const statusList = ref<any>()
|
|||||||
const getstatusList = async () => {
|
const getstatusList = async () => {
|
||||||
try {
|
try {
|
||||||
// 这里调用获取品牌列表的接口
|
// 这里调用获取品牌列表的接口
|
||||||
const response = await WorkService.getStatus()
|
const response = await WorkService.getStatus({})
|
||||||
statusList.value = response || []
|
statusList.value = response || []
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
@@ -314,14 +417,28 @@ const getConversationList = async (id:any) => {
|
|||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
const text = messageText.value.trim()
|
const text = messageText.value.trim()
|
||||||
const ids = ticket_id.value
|
const ids = ticket_id.value
|
||||||
if (!text) return
|
if (!text && chatFileList.value.length === 0) return
|
||||||
|
|
||||||
|
// 准备附件数据
|
||||||
|
const attachments = chatFileList.value.map(file => {
|
||||||
|
const data = file.response?.data
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
original_name: data.original_name,
|
||||||
|
storage_path: data.storage_path || data.file_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter(Boolean)
|
||||||
|
|
||||||
const { message } = await WorkService.conversationAdd({
|
const { message } = await WorkService.conversationAdd({
|
||||||
content: text,
|
content: text,
|
||||||
ticket_id: ids,
|
ticket_id: ids,
|
||||||
|
attachment: attachments
|
||||||
})
|
})
|
||||||
ElMessage.success(message)
|
ElMessage.success(message)
|
||||||
messageText.value = ''
|
messageText.value = ''
|
||||||
|
chatFileList.value = [] // 清空附件列表
|
||||||
getConversationList(ids)
|
getConversationList(ids)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
@@ -381,7 +498,9 @@ const delCodeTrim = (content: string): string => {
|
|||||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||||
}
|
}
|
||||||
//添加工单
|
//添加工单
|
||||||
|
const handleExceed = () => {
|
||||||
|
ElMessage.warning('最多上传3个文件')
|
||||||
|
}
|
||||||
const uploadedFiles = ref<any[]>([])
|
const uploadedFiles = ref<any[]>([])
|
||||||
const handleUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
const handleUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
||||||
// 只处理上传成功的文件
|
// 只处理上传成功的文件
|
||||||
@@ -452,6 +571,8 @@ const statusSubmit = async () => {
|
|||||||
const imageViewerVisible = ref(false)
|
const imageViewerVisible = ref(false)
|
||||||
const imageViewerIndex = ref(0)
|
const imageViewerIndex = ref(0)
|
||||||
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
|
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
|
||||||
|
// 聊天图片预览列表
|
||||||
|
const chatImageList = ref<string[]>([])
|
||||||
function handleImageClick(idx: number) {
|
function handleImageClick(idx: number) {
|
||||||
imageViewerIndex.value = idx
|
imageViewerIndex.value = idx
|
||||||
imageViewerVisible.value = true
|
imageViewerVisible.value = true
|
||||||
@@ -463,6 +584,60 @@ function handleVideoClick(url: string) {
|
|||||||
currentVideoUrl.value = url
|
currentVideoUrl.value = url
|
||||||
videoDialogVisible.value = true
|
videoDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 聊天相关
|
||||||
|
const chatFileList = ref<any[]>([])
|
||||||
|
const handleChatUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
||||||
|
chatFileList.value = uploadFiles.filter(item => item.status === 'success').map(item => {
|
||||||
|
const data = item.response?.data
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
name: data.original_name,
|
||||||
|
file_url: data.storage_path_full,
|
||||||
|
status: 'success',
|
||||||
|
response: { data: data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter(Boolean)
|
||||||
|
}
|
||||||
|
const handleChatExceed = () => {
|
||||||
|
ElMessage.warning('最多上传5个文件')
|
||||||
|
}
|
||||||
|
const removeChatFile = (index: number) => {
|
||||||
|
chatFileList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
const handleChatImageClick = (url: string) => {
|
||||||
|
// 设置聊天图片预览列表
|
||||||
|
chatImageList.value = [url]
|
||||||
|
imageViewerIndex.value = 0
|
||||||
|
imageViewerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImageViewer = () => {
|
||||||
|
imageViewerVisible.value = false
|
||||||
|
chatImageList.value = [] // 清空聊天图片列表
|
||||||
|
}
|
||||||
|
const handleChatVideoClick = (url: string) => {
|
||||||
|
currentVideoUrl.value = url
|
||||||
|
videoDialogVisible.value = true
|
||||||
|
}
|
||||||
|
const downloadAttachment = (attachment: any) => {
|
||||||
|
try {
|
||||||
|
const fileName = attachment.original_name || 'download'
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = attachment.storage_path_full
|
||||||
|
link.download = fileName
|
||||||
|
link.target = '_blank'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
ElMessage.success('开始下载')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('下载失败')
|
||||||
|
console.error('下载失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -547,6 +722,7 @@ function handleVideoClick(url: string) {
|
|||||||
|
|
||||||
.message-info {
|
.message-info {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
@@ -563,6 +739,7 @@ function handleVideoClick(url: string) {
|
|||||||
|
|
||||||
.message-info {
|
.message-info {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
@@ -686,4 +863,101 @@ function handleVideoClick(url: string) {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 聊天附件样式
|
||||||
|
.message-right .message-attachments{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.message-left .message-attachments{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.message-attachments {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.attachment-thumb {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 150px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-attachment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-attachment-upload {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.chat-upload {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.attachment-preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
max-width: 200px;
|
||||||
|
|
||||||
|
.preview-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
word-break: break-all;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||