Commit 5ac92cf3 by YunaiV

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm

parents 4f241416 93bee8c8
# 开发环境:本地只启动前端项目,依赖开发环境(后端、APP)
NODE_ENV=development
NODE_ENV=production
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
# 接口地址
VITE_API_URL=/admin-api
......@@ -37,4 +33,4 @@ VITE_OUT_DIR=dist
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false
VITE_APP_CAPTCHA_ENABLE=true
......@@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
# 接口地址
VITE_API_URL=/admin-api
......
......@@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=
# 接口地址
VITE_API_URL=/admin-api
......
......@@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=
# 接口地址
VITE_API_URL=/admin-api
......
......@@ -11,9 +11,6 @@ VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=
# 接口地址
VITE_API_URL=/admin-api
......
......@@ -68,6 +68,8 @@ module.exports = defineConfig({
],
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off',
'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
'@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
'@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
}
})
......@@ -4,7 +4,6 @@ dist
dist-ssr
*.local
/dist*
*-lock.*
pnpm-debug
auto-*.d.ts
.idea
......
......@@ -3,7 +3,6 @@
"christian-kohler.path-intellisense",
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"mrmlnc.vscode-less",
......
......@@ -117,6 +117,8 @@
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
![功能图](/.image/common/system-feature.png)
### 工作流程
| | 功能 | 描述 |
......@@ -129,6 +131,8 @@
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 |
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
![功能图](/.image/common/bpm-feature.png)
### 支付系统
| | 功能 | 描述 |
......@@ -142,27 +146,27 @@ ps:核心功能已经实现,正在对接微信小程序中...
### 基础设施
| | 功能 | 描述 |
|-----|----------|----------------------------------------------|
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 |
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 |
| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 |
| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 |
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
| | 功能 | 描述 |
|----|----------|----------------------------------------------|
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 |
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
| 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
![功能图](/.image/common/infra-feature.png)
### 数据报表
......
{
"name": "yudao-ui-admin-vue3",
"version": "2.0.0-snapshot",
"version": "2.1.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
"scripts": {
"i": "pnpm install",
"dev": "vite --mode local-dev",
"dev": "vite",
"dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit",
"build:local-dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev",
"build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local-dev && vite preview",
"preview": "pnpm build:local && vite preview",
"clean": "npx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
......@@ -30,12 +30,12 @@
"@form-create/element-ui": "^3.1.24",
"@iconify/iconify": "^3.1.1",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.6.1",
"@vueuse/core": "^10.9.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "^1.6.1",
"axios": "^1.6.8",
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1",
......@@ -44,9 +44,9 @@
"dayjs": "^1.11.10",
"diagram-js": "^12.8.0",
"driver.js": "^1.3.1",
"echarts": "^5.4.3",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.4.2",
"element-plus": "2.6.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
......@@ -55,76 +55,78 @@
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",
"qs": "^6.11.2",
"qs": "^6.12.0",
"steady-xml": "^0.1.0",
"url": "^0.11.3",
"video.js": "^7.21.5",
"vue": "^3.3.8",
"vue": "3.4.21",
"vue-dompurify-html": "^4.1.4",
"vue-i18n": "^9.6.5",
"vue-router": "^4.2.5",
"vue-i18n": "9.10.2",
"vue-router": "^4.3.0",
"vue-types": "^5.1.1",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@commitlint/cli": "^18.4.1",
"@commitlint/config-conventional": "^18.4.0",
"@iconify/json": "^2.2.142",
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@commitlint/cli": "^19.0.1",
"@commitlint/config-conventional": "^19.0.0",
"@iconify/json": "^2.2.187",
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@purge-icons/generated": "^0.9.0",
"@types/lodash-es": "^4.17.11",
"@types/node": "^20.9.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.21",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.10",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@unocss/transformer-variant-group": "^0.57.4",
"@types/qs": "^6.9.12",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@unocss/transformer-variant-group": "^0.58.5",
"@unocss/eslint-config": "^0.57.4",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.4.1",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"autoprefixer": "^10.4.16",
"@vitejs/plugin-legacy": "^5.3.1",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.17",
"bpmn-js": "8.9.0",
"bpmn-js-properties-panel": "0.46.0",
"consola": "^3.2.3",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-define-config": "^1.24.1",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.18.1",
"lint-staged": "^15.1.0",
"postcss": "^8.4.31",
"postcss-html": "^1.5.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.22.0",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"postcss-html": "^1.6.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.1.0",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"rimraf": "^5.0.5",
"rollup": "^4.4.1",
"rollup": "^4.12.0",
"sass": "^1.69.5",
"stylelint": "^15.11.0",
"stylelint": "^16.2.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"terser": "^5.24.0",
"typescript": "5.2.2",
"unocss": "^0.57.4",
"stylelint-config-recommended": "^14.0.0",
"stylelint-config-standard": "^36.0.0",
"stylelint-order": "^6.0.4",
"terser": "^5.28.1",
"typescript": "5.3.3",
"unocss": "^0.58.5",
"unplugin-auto-import": "^0.16.7",
"unplugin-element-plus": "^0.8.0",
"unplugin-vue-components": "^0.25.2",
"vite": "4.5.0",
"vite": "5.1.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2",
"vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.1",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.22"
"vue-tsc": "^1.8.27"
},
"license": "MIT",
"repository": {
......@@ -135,7 +137,6 @@
"url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
},
"homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
"packageManager": "pnpm@8.6.0",
"engines": {
"node": ">= 16.0.0",
"pnpm": ">=8.6.0"
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -15,9 +15,9 @@ export interface PermissionVO {
}
export interface TransferReqVO {
bizId: number // 模块编号
id: number // 模块编号
newOwnerUserId: number // 新负责人的用户编号
oldOwnerPermissionLevel: number // 老负责人加入团队后的权限级别
oldOwnerPermissionLevel?: number // 老负责人加入团队后的权限级别
toBizTypes?: number[] // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择
}
......
......@@ -14,21 +14,21 @@ export interface CrmStatisticsCustomerSummaryByUserRespVO {
receivablePrice: number
}
export interface CrmStatisticsFollowupSummaryByDateRespVO {
export interface CrmStatisticsFollowUpSummaryByDateRespVO {
time: string
followupRecordCount: number
followupCustomerCount: number
followUpRecordCount: number
followUpCustomerCount: number
}
export interface CrmStatisticsFollowupSummaryByUserRespVO {
export interface CrmStatisticsFollowUpSummaryByUserRespVO {
ownerUserName: string
followupRecordCount: number
followupCustomerCount: number
}
export interface CrmStatisticsFollowupSummaryByTypeRespVO {
followupType: string
followupRecordCount: number
export interface CrmStatisticsFollowUpSummaryByTypeRespVO {
followUpType: string
followUpRecordCount: number
}
export interface CrmStatisticsCustomerContractSummaryRespVO {
......@@ -44,6 +44,18 @@ export interface CrmStatisticsCustomerContractSummaryRespVO {
orderDate: Date
}
export interface CrmStatisticsPoolSummaryByDateRespVO {
time: string
customerPutCount: number
customerTakeCount: number
}
export interface CrmStatisticsPoolSummaryByUserRespVO {
ownerUserName: string
customerPutCount: number
customerTakeCount: number
}
export interface CrmStatisticsCustomerDealCycleByDateRespVO {
time: string
customerDealCycle: number
......@@ -55,6 +67,18 @@ export interface CrmStatisticsCustomerDealCycleByUserRespVO {
customerDealCount: number
}
export interface CrmStatisticsCustomerDealCycleByAreaRespVO {
areaName: string
customerDealCycle: number
customerDealCount: number
}
export interface CrmStatisticsCustomerDealCycleByProductRespVO {
productName: string
customerDealCycle: number
customerDealCount: number
}
// 客户分析 API
export const StatisticsCustomerApi = {
// 1.1 客户总量分析(按日期)
......@@ -72,23 +96,23 @@ export const StatisticsCustomerApi = {
})
},
// 2.1 客户跟进次数分析(按日期)
getFollowupSummaryByDate: (params: any) => {
getFollowUpSummaryByDate: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-date',
url: '/crm/statistics-customer/get-follow-up-summary-by-date',
params
})
},
// 2.2 客户跟进次数分析(按用户)
getFollowupSummaryByUser: (params: any) => {
getFollowUpSummaryByUser: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-user',
url: '/crm/statistics-customer/get-follow-up-summary-by-user',
params
})
},
// 3.1 获取客户跟进方式统计数
getFollowupSummaryByType: (params: any) => {
getFollowUpSummaryByType: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-type',
url: '/crm/statistics-customer/get-follow-up-summary-by-type',
params
})
},
......@@ -99,18 +123,46 @@ export const StatisticsCustomerApi = {
params
})
},
// 5.1 获取客户成交周期(按日期)
// 5.1 获取客户公海分析(按日期)
getPoolSummaryByDate: (param: any) => {
return request.get({
url: '/crm/statistics-customer/get-pool-summary-by-date',
params: param
})
},
// 5.2 获取客户公海分析(按用户)
getPoolSummaryByUser: (param: any) => {
return request.get({
url: '/crm/statistics-customer/get-pool-summary-by-user',
params: param
})
},
// 6.1 获取客户成交周期(按日期)
getCustomerDealCycleByDate: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-date',
params
})
},
// 5.2 获取客户成交周期(按用户)
// 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByUser: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',
params
})
},
// 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByArea: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-area',
params
})
},
// 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByProduct: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-product',
params
})
}
}
import request from '@/config/axios'
export interface CrmStatisticFunnelRespVO {
customerCount: number // 客户数
businessCount: number // 商机数
businessWinCount: number // 赢单数
}
export interface CrmStatisticsBusinessSummaryByDateRespVO {
time: string // 时间
businessCreateCount: number // 商机数
totalPrice: number | string // 商机金额
}
export interface CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
time: string // 时间
businessCount: number // 商机数量
businessWinCount: number // 赢单商机数
}
// 客户分析 API
export const StatisticFunnelApi = {
// 1. 获取销售漏斗统计数据
getFunnelSummary: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-funnel-summary',
params
})
},
// 2. 获取商机结束状态统计
getBusinessSummaryByEndStatus: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-summary-by-end-status',
params
})
},
// 3. 获取新增商机分析(按日期)
getBusinessSummaryByDate: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-summary-by-date',
params
})
},
// 4. 获取商机转化率分析(按日期)
getBusinessInversionRateSummaryByDate: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-inversion-rate-summary-by-date',
params
})
},
// 5. 获取商机列表(按日期)
getBusinessPageByDate: (params: any) => {
return request.get({
url: '/crm/statistics-funnel/get-business-page-by-date',
params
})
}
}
import request from '@/config/axios'
export interface StatisticsPerformanceRespVO {
time: string
currentMonthCount: number
lastMonthCount: number
lastYearCount: number
}
// 排行 API
export const StatisticsPerformanceApi = {
// 员工获得合同金额统计
getContractPricePerformance: (params: any) => {
return request.get({
url: '/crm/statistics-performance/get-contract-price-performance',
params
})
},
// 员工获得回款统计
getReceivablePricePerformance: (params: any) => {
return request.get({
url: '/crm/statistics-performance/get-receivable-price-performance',
params
})
},
//员工获得签约合同数量统计
getContractCountPerformance: (params: any) => {
return request.get({
url: '/crm/statistics-performance/get-contract-count-performance',
params
})
}
}
import request from '@/config/axios'
export interface CrmStatisticCustomerBaseRespVO {
customerCount: number
dealCount: number
dealPortion: string | number
}
export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
industryId: number
industryPortion: string | number
}
export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
source: number
sourcePortion: string | number
}
export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
level: number
levelPortion: string | number
}
export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
areaId: number
areaName: string
areaPortion: string | number
}
// 客户分析 API
export const StatisticsPortraitApi = {
// 1. 获取客户行业统计数据
getCustomerIndustry: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-industry-summary',
params
})
},
// 2. 获取客户来源统计数据
getCustomerSource: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-source-summary',
params
})
},
// 3. 获取客户级别统计数据
getCustomerLevel: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-level-summary',
params
})
},
// 4. 获取客户地区统计数据
getCustomerArea: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-area-summary',
params
})
}
}
......@@ -13,8 +13,8 @@ export interface ProductCategoryVO {
// ERP 产品分类 API
export const ProductCategoryApi = {
// 查询产品分类列表
getProductCategoryList: async (params) => {
return await request.get({ url: `/erp/product-category/list`, params })
getProductCategoryList: async () => {
return await request.get({ url: `/erp/product-category/list` })
},
// 查询产品分类精简列表
......
......@@ -8,11 +8,15 @@ export interface ApiAccessLogVO {
applicationName: string
requestMethod: string
requestParams: string
responseBody: string
requestUrl: string
userIp: string
userAgent: string
operateModule: string
operateName: string
operateType: number
beginTime: Date
endTIme: Date
endTime: Date
duration: number
resultCode: number
resultMsg: string
......
......@@ -28,7 +28,6 @@ export type CodegenColumnVO = {
columnComment: string
nullable: number
primaryKey: number
autoIncrement: boolean
ordinalPosition: number
javaType: string
javaField: string
......
import request from '@/config/axios'
// 导出Html
export const exportHtml = () => {
return request.download({ url: '/infra/db-doc/export-html' })
}
// 导出Word
export const exportWord = () => {
return request.download({ url: '/infra/db-doc/export-word' })
}
// 导出Markdown
export const exportMarkdown = () => {
return request.download({ url: '/infra/db-doc/export-markdown' })
}
......@@ -7,8 +7,8 @@ export interface Demo02CategoryVO {
}
// 查询示例分类列表
export const getDemo02CategoryList = async (params) => {
return await request.get({ url: `/infra/demo02-category/list`, params })
export const getDemo02CategoryList = async () => {
return await request.get({ url: `/infra/demo02-category/list` })
}
// 查询示例分类详情
......
import request from '@/config/axios'
// 秒杀时段 VO
export interface SeckillConfigVO {
id: number
name: string
startTime: string
endTime: string
sliderPicUrls: string[]
status: number
}
// 查询秒杀时段配置列表
export const getSeckillConfigPage = async (params) => {
return await request.get({ url: '/promotion/seckill-config/page', params })
}
// 查询秒杀时段配置详情
export const getSeckillConfig = async (id: number) => {
return await request.get({ url: '/promotion/seckill-config/get?id=' + id })
}
// 获得所有开启状态的秒杀时段精简列表
export const getSimpleSeckillConfigList = async () => {
return await request.get({ url: '/promotion/seckill-config/list-all-simple' })
}
// 新增秒杀时段配置
export const createSeckillConfig = async (data: SeckillConfigVO) => {
return await request.post({ url: '/promotion/seckill-config/create', data })
}
// 修改秒杀时段配置
export const updateSeckillConfig = async (data: SeckillConfigVO) => {
return await request.put({ url: '/promotion/seckill-config/update', data })
}
// 修改时段配置状态
export const updateSeckillConfigStatus = (id: number, status: number) => {
const data = {
id,
status
id: number // 编号
name: string // 秒杀时段名称
startTime: string // 开始时间点
endTime: string // 结束时间点
sliderPicUrls: string[] // 秒杀轮播图
status: number // 活动状态
}
// 秒杀时段 API
export const SeckillConfigApi = {
// 查询秒杀时段分页
getSeckillConfigPage: async (params: any) => {
return await request.get({ url: `/promotion/seckill-config/page`, params })
},
// 查询秒杀时段列表
getSimpleSeckillConfigList: async () => {
return await request.get({ url: `/promotion/seckill-config/simple-list` })
},
// 查询秒杀时段详情
getSeckillConfig: async (id: number) => {
return await request.get({ url: `/promotion/seckill-config/get?id=` + id })
},
// 新增秒杀时段
createSeckillConfig: async (data: SeckillConfigVO) => {
return await request.post({ url: `/promotion/seckill-config/create`, data })
},
// 修改秒杀时段
updateSeckillConfig: async (data: SeckillConfigVO) => {
return await request.put({ url: `/promotion/seckill-config/update`, data })
},
// 删除秒杀时段
deleteSeckillConfig: async (id: number) => {
return await request.delete({ url: `/promotion/seckill-config/delete?id=` + id })
},
// 修改时段配置状态
updateSeckillConfigStatus: async (id: number, status: number) => {
const data = {
id,
status
}
return request.put({ url: '/promotion/seckill-config/update-status', data: data })
}
return request.put({ url: '/promotion/seckill-config/update-status', data: data })
}
// 删除秒杀时段配置
export const deleteSeckillConfig = async (id: number) => {
return await request.delete({ url: '/promotion/seckill-config/delete?id=' + id })
}
......@@ -5,7 +5,7 @@ import { formatDate } from '@/utils/formatTime'
/** 会员分析 Request VO */
export interface MemberAnalyseReqVO {
times: [dayjs.ConfigType, dayjs.ConfigType]
times: dayjs.ConfigType[]
}
/** 会员分析 Response VO */
......
......@@ -141,7 +141,7 @@ export const getExpressTrackList = async (id: number | null) => {
}
export interface DeliveryVO {
id: number // 订单编号
id?: number // 订单编号
logisticsId: number | null // 物流公司编号
logisticsNo: string // 物流编号
}
......
import request from '@/config/axios'
export interface UReportDataVO {
id: number
name: string
status: number
content: string
remark: string
}
// 查询Ureport2报表分页
export const getUReportDataPage = async (params) => {
return await request.get({ url: `/report/ureport-data/page`, params })
}
// 查询Ureport2报表详情
export const getUReportData = async (id: number) => {
return await request.get({ url: `/report/ureport-data/get?id=` + id })
}
// 新增Ureport2报表
export const createUReportData = async (data: UReportDataVO) => {
return await request.post({ url: `/report/ureport-data/create`, data })
}
// 修改Ureport2报表
export const updateUReportData = async (data: UReportDataVO) => {
return await request.put({ url: `/report/ureport-data/update`, data })
}
// 删除Ureport2报表
export const deleteUReportData = async (id: number) => {
return await request.delete({ url: `/report/ureport-data/delete?id=` + id })
}
// 导出Ureport2报表 Excel
export const exportUReportData = async (params) => {
return await request.download({ url: `/report/ureport-data/export-excel`, params })
}
import request from '@/config/axios'
export interface ErrorCodeVO {
id: number | undefined
type: number
applicationName: string
code: number | undefined
message: string
memo: string
createTime: Date
}
// 查询错误码列表
export const getErrorCodePage = (params: PageParam) => {
return request.get({ url: '/system/error-code/page', params })
}
// 查询错误码详情
export const getErrorCode = (id: number) => {
return request.get({ url: '/system/error-code/get?id=' + id })
}
// 新增错误码
export const createErrorCode = (data: ErrorCodeVO) => {
return request.post({ url: '/system/error-code/create', data })
}
// 修改错误码
export const updateErrorCode = (data: ErrorCodeVO) => {
return request.put({ url: '/system/error-code/update', data })
}
// 删除错误码
export const deleteErrorCode = (id: number) => {
return request.delete({ url: '/system/error-code/delete?id=' + id })
}
// 导出错误码
export const excelErrorCode = (params) => {
return request.download({ url: '/system/error-code/export-excel', params })
}
......@@ -8,6 +8,7 @@ export interface MailAccountVO {
host: string
port: number
sslEnable: boolean
starttlsEnable: boolean
}
// 查询邮箱账号列表
......
......@@ -2,30 +2,6 @@ import request from '@/config/axios'
export type OperateLogVO = {
id: number
userNickname: string
traceId: string
userId: number
module: string
name: string
type: number
content: string
exts: Map<String, Object>
requestMethod: string
requestUrl: string
userIp: string
userAgent: string
javaMethod: string
javaMethodArgs: string
startTime: Date
duration: number
resultCode: number
resultMsg: string
resultData: string
}
export type OperateLogV2VO = {
id: number
userNickname: string
traceId: string
userType: number
userId: number
......@@ -42,11 +18,6 @@ export type OperateLogV2VO = {
creator: string
creatorName: string
createTime: Date
// 数据扩展,渲染时使用
title: string // 操作标题(如果为空则取 name 值)
colSize: number // 变更记录行数
contentStrList: string[]
tagsContentList: string[]
}
// 查询操作日志列表
......@@ -54,6 +25,6 @@ export const getOperateLogPage = (params: PageParam) => {
return request.get({ url: '/system/operate-log/page', params })
}
// 导出操作日志
export const exportOperateLog = (params) => {
export const exportOperateLog = (params: any) => {
return request.download({ url: '/system/operate-log/export', params })
}
import request from '@/config/axios'
import qs from 'qs'
export interface SensitiveWordVO {
id: number
name: string
status: number
description: string
tags: string[]
createTime: Date
}
export interface SensitiveWordTestReqVO {
text: string
tag: string[]
}
// 查询敏感词列表
export const getSensitiveWordPage = (params: PageParam) => {
return request.get({ url: '/system/sensitive-word/page', params })
}
// 查询敏感词详情
export const getSensitiveWord = (id: number) => {
return request.get({ url: '/system/sensitive-word/get?id=' + id })
}
// 新增敏感词
export const createSensitiveWord = (data: SensitiveWordVO) => {
return request.post({ url: '/system/sensitive-word/create', data })
}
// 修改敏感词
export const updateSensitiveWord = (data: SensitiveWordVO) => {
return request.put({ url: '/system/sensitive-word/update', data })
}
// 删除敏感词
export const deleteSensitiveWord = (id: number) => {
return request.delete({ url: '/system/sensitive-word/delete?id=' + id })
}
// 导出敏感词
export const exportSensitiveWord = (params) => {
return request.download({ url: '/system/sensitive-word/export-excel', params })
}
// 获取所有敏感词的标签数组
export const getSensitiveWordTagList = () => {
return request.get({ url: '/system/sensitive-word/get-tags' })
}
// 获得文本所包含的不合法的敏感词数组
export const validateText = (query: SensitiveWordTestReqVO) => {
return request.get({
url: '/system/sensitive-word/validate-text?' + qs.stringify(query, { arrayFormat: 'repeat' })
})
}
......@@ -25,6 +25,9 @@ defineProps({
</template>
<Icon :size="14" class="ml-5px" icon="ep:question-filled" />
</ElTooltip>
<div class="flex flex-grow pl-20px">
<slot name="header"></slot>
</div>
</div>
</template>
<div>
......
......@@ -503,9 +503,13 @@ const submit = () => {
emit('update:modelValue', defaultValue.value)
dialogVisible.value = false
}
const inputChange = () => {
emit('update:modelValue', defaultValue.value)
}
</script>
<template>
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs">
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
<template #append>
<el-select v-model="select" placeholder="生成器" style="width: 115px">
<el-option label="每分钟" value="0 * * * * ?" />
......
<template>
<div class="h-40px flex items-center justify-center">
<MagicCubeEditor
v-model="cellList"
class="m-b-16px"
:rows="1"
:cols="cellCount"
:cube-size="38"
@hot-area-selected="handleHotAreaSelected"
/>
<img src="@/assets/imgs/diy/app-nav-bar-mp.png" alt="" class="h-30px w-76px" v-if="isMp" />
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`">
<el-radio-group v-model="cell.type">
<el-radio label="text">文字</el-radio>
<el-radio label="image">图片</el-radio>
<el-radio label="search">搜索框</el-radio>
</el-radio-group>
</el-form-item>
<!-- 1. 文字 -->
<template v-if="cell.type === 'text'">
<el-form-item label="内容" :prop="`cell[${cellIndex}].text`">
<el-input v-model="cell!.text" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="颜色" :prop="`cell[${cellIndex}].text`">
<ColorInput v-model="cell!.textColor" />
</el-form-item>
</template>
<!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'">
<el-form-item label="图片" :prop="`cell[${cellIndex}].imgUrl`">
<UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px">
<template #tip>建议尺寸 56*56</template>
</UploadImg>
</el-form-item>
<el-form-item label="链接" :prop="`cell[${cellIndex}].url`">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</template>
<!-- 3. 搜索框 -->
<template v-else>
<el-form-item label="提示文字" :prop="`cell[${cellIndex}].placeholder`">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="圆角" :prop="`cell[${cellIndex}].borderRadius`">
<el-slider
v-model="cell.borderRadius"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</template>
</template>
</template>
</template>
<script setup lang="ts">
import { NavigationBarCellProperty } from '../config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarCellProperty' })
const props = defineProps<{
modelValue: NavigationBarCellProperty[]
isMp: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const { formData: cellList } = usePropertyForm(props.modelValue, emit)
if (!cellList.value) cellList.value = []
// 单元格数量:小程序6个(右侧胶囊按钮占了2个),其它平台8个
const cellCount = computed(() => (props.isMp ? 6 : 8))
// 选中的热区
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
selectedHotAreaIndex.value = index
if (!cellValue.type) {
cellValue.type = 'text'
cellValue.textColor = '#111111'
}
}
</script>
<style scoped lang="scss"></style>
......@@ -2,22 +2,53 @@ import { DiyComponent } from '@/components/DiyEditor/util'
/** 顶部导航栏属性 */
export interface NavigationBarProperty {
// 页面标题
title: string
// 页面描述
description: string
// 顶部导航高度
navBarHeight: number
// 页面背景颜色
backgroundColor: string
// 页面背景图片
backgroundImage: string
// 背景类型
bgType: 'color' | 'img'
// 背景颜色
bgColor: string
// 图片链接
bgImg: string
// 样式类型:默认 | 沉浸式
styleType: 'default' | 'immersion'
styleType: 'normal' | 'inner'
// 常驻显示
alwaysShow: boolean
// 是否显示返回按钮
showGoBack: boolean
// 小程序单元格列表
mpCells: NavigationBarCellProperty[]
// 其它平台单元格列表
otherCells: NavigationBarCellProperty[]
// 本地变量
_local: {
// 预览顶部导航(小程序)
previewMp: boolean
// 预览顶部导航(非小程序)
previewOther: boolean
}
}
/** 顶部导航栏 - 单元格 属性 */
export interface NavigationBarCellProperty {
// 类型:文字 | 图片 | 搜索框
type: 'text' | 'image' | 'search'
// 宽度
width: number
// 高度
height: number
// 顶部位置
top: number
// 左侧位置
left: number
// 文字内容
text: string
// 文字颜色
textColor: string
// 图片地址
imgUrl: string
// 图片链接
url: string
// 搜索框:提示文字
placeholder: string
// 搜索框:边框圆角半径
borderRadius: number
}
// 定义组件
......@@ -26,13 +57,26 @@ export const component = {
name: '顶部导航栏',
icon: 'tabler:layout-navbar',
property: {
title: '页面标题',
description: '',
navBarHeight: 35,
backgroundColor: '#fff',
backgroundImage: '',
styleType: 'default',
bgType: 'color',
bgColor: '#fff',
bgImg: '',
styleType: 'normal',
alwaysShow: true,
showGoBack: true
mpCells: [
{
type: 'text',
textColor: '#111111'
}
],
otherCells: [
{
type: 'text',
textColor: '#111111'
}
],
_local: {
previewMp: true,
previewOther: false
}
}
} as DiyComponent<NavigationBarProperty>
<template>
<div
class="navigation-bar"
:style="{
height: `${property.navBarHeight}px`,
backgroundColor: property.backgroundColor,
backgroundImage: `url(${property.backgroundImage})`
}"
>
<!-- 左侧 -->
<div class="left">
<Icon icon="ep:arrow-left" v-show="property.showGoBack" />
<div class="navigation-bar" :style="bgStyle">
<div class="h-full w-full flex items-center">
<div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
<span v-if="cell.type === 'text'">{{ cell.text }}</span>
<img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
<SearchBar v-else :property="getSearchProp" />
</div>
</div>
<!-- 中间 -->
<div
class="center"
:style="{
height: `${property.navBarHeight}px`,
lineHeight: `${property.navBarHeight}px`
}"
>
{{ property.title }}
</div>
<!-- 右侧 -->
<div class="right"></div>
<img
v-if="property._local?.previewMp"
src="@/assets/imgs/diy/app-nav-bar-mp.png"
alt=""
class="h-30px w-86px"
/>
</div>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { NavigationBarCellProperty, NavigationBarProperty } from './config'
import SearchBar from '@/components/DiyEditor/components/mobile/SearchBar/index.vue'
import { StyleValue } from 'vue'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
/** 页面顶部导航栏 */
defineOptions({ name: 'NavigationBar' })
defineProps<{ property: NavigationBarProperty }>()
const props = defineProps<{ property: NavigationBarProperty }>()
// 背景
const bgStyle = computed(() => {
const background =
props.property.bgType === 'img' && props.property.bgImg
? `url(${props.property.bgImg}) no-repeat top center / 100% 100%`
: props.property.bgColor
return { background }
})
// 单元格列表
const cellList = computed(() =>
props.property._local?.previewMp ? props.property.mpCells : props.property.otherCells
)
// 单元格宽度
const cellWidth = computed(() => {
return props.property._local?.previewMp ? (375 - 80 - 86) / 6 : (375 - 90) / 8
})
// 获得单元格样式
const getCellStyle = (cell: NavigationBarCellProperty) => {
return {
width: cell.width * cellWidth.value + (cell.width - 1) * 10 + 'px',
left: cell.left * cellWidth.value + (cell.left + 1) * 10 + 'px',
position: 'absolute'
} as StyleValue
}
// 获得搜索框属性
const getSearchProp = (cell: NavigationBarCellProperty) => {
return {
height: 30,
showScan: false,
placeholder: cell.placeholder,
borderRadius: cell.borderRadius
} as SearchProperty
}
</script>
<style lang="scss" scoped>
.navigation-bar {
display: flex;
height: 35px;
height: 50px;
background: #fff;
justify-content: space-between;
align-items: center;
padding: 0 6px;
/* 左边 */
.left {
......
<template>
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="页面标题" prop="title">
<el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
</el-form-item>
<el-form-item label="页面描述" prop="description">
<el-input
type="textarea"
v-model="formData!.description"
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
/>
</el-form-item>
<el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType">
<el-radio label="default">默认</el-radio>
<el-radio label="immersion">沉浸式</el-radio>
<el-radio label="normal">标准</el-radio>
<el-tooltip
content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件"
placement="top"
>
<el-radio label="inner">沉浸式</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'">
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'">
<el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio>
<el-radio :label="true">开启</el-radio>
<el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top">
<el-radio :label="true">开启</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="高度" prop="navBarHeight">
<el-slider
v-model="formData!.navBarHeight"
:max="100"
:min="35"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="返回按钮" prop="showGoBack">
<el-switch v-model="formData!.showGoBack" />
<el-form-item label="背景类型" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData!.backgroundColor" />
<el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'">
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData!.backgroundImage" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
<el-form-item label="背景图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
</el-form-item>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容(小程序)</span>
<el-form-item prop="_local.previewMp" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewMp"
@change="formData._local.previewOther = !formData._local.previewMp"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.mpCells" is-mp />
</el-card>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容(非小程序)</span>
<el-form-item prop="_local.previewOther" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewOther"
@change="formData._local.previewMp = !formData._local.previewOther"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
</el-card>
</el-form>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarProperty' })
// 表单校验
......@@ -58,6 +78,9 @@ const rules = {
const props = defineProps<{ modelValue: NavigationBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
if (!formData.value._local) {
formData.value._local = { previewMp: true, previewOther: false }
}
</script>
<style scoped lang="scss"></style>
......@@ -180,12 +180,12 @@ defineExpose({
</script>
<template>
<div class="z-99 border-1 border-[var(--el-border-color)] border-solid">
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
<!-- 工具栏 -->
<Toolbar
:editor="editorRef"
:editorId="editorId"
class="border-0 b-b-1 border-[var(--el-border-color)] border-solid"
class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
/>
<!-- 编辑器 -->
<Editor
......
import { useFormCreateDesigner } from './src/useFormCreateDesigner'
import { useApiSelect } from './src/components/useApiSelect'
export { useFormCreateDesigner, useApiSelect }
<!-- 数据字典 Select 选择器 -->
<template>
<el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
<el-option
v-for="(dict, index) in getDictOptions"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
<el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
<el-checkbox
v-for="(dict, index) in getDictOptions"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-checkbox-group>
</template>
<script lang="ts" setup>
import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
defineOptions({ name: 'DictSelect' })
const attrs = useAttrs()
// 接受父组件参数
interface Props {
dictType: string // 字典类型
valueType?: 'str' | 'int' | 'bool' // 字典值类型
selectType?: 'select' | 'radio' | 'checkbox' // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
formCreateInject?: any
}
const props = withDefaults(defineProps<Props>(), {
valueType: 'str',
selectType: 'select'
})
// 获得字典配置
const getDictOptions = computed(() => {
switch (props.valueType) {
case 'str':
return getStrDictOptions(props.dictType)
case 'int':
return getIntDictOptions(props.dictType)
case 'bool':
return getBoolDictOptions(props.dictType)
default:
return []
}
})
</script>
import request from '@/config/axios'
import { isEmpty } from '@/utils/is'
import { ApiSelectProps } from '@/components/FormCreate/src/type'
import { jsonParse } from '@/utils'
export const useApiSelect = (option: ApiSelectProps) => {
return defineComponent({
name: option.name,
props: {
// 选项标签
labelField: {
type: String,
default: () => option.labelField ?? 'label'
},
// 选项的值
valueField: {
type: String,
default: () => option.valueField ?? 'value'
},
// api 接口
url: {
type: String,
default: () => option.url ?? ''
},
// 请求类型
method: {
type: String,
default: 'GET'
},
// 请求参数
data: {
type: String,
default: ''
},
// 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
selectType: {
type: String,
default: 'select'
},
// 是否多选
multiple: {
type: Boolean,
default: false
}
},
setup(props) {
const attrs = useAttrs()
const options = ref<any[]>([]) // 下拉数据
const getOptions = async () => {
options.value = []
// 接口选择器
if (isEmpty(props.url)) {
return
}
let data = []
switch (props.method) {
case 'GET':
data = await request.get({ url: props.url })
break
case 'POST':
data = await request.post({ url: props.url, data: jsonParse(props.data) })
break
}
if (Array.isArray(data)) {
options.value = data.map((item: any) => ({
label: item[props.labelField],
value: item[props.valueField]
}))
return
}
console.error(`接口[${props.url}] 返回结果不是一个数组`)
}
onMounted(async () => {
await getOptions()
})
const buildSelect = () => {
if (props.multiple) {
// fix:多写此步是为了解决 multiple 属性问题
return (
<el-select class="w-1/1" {...attrs} multiple>
{options.value.map((item, index) => (
<el-option key={index} label={item.label} value={item.value} />
))}
</el-select>
)
}
return (
<el-select class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-option key={index} label={item.label} value={item.value} />
))}
</el-select>
)
}
const buildCheckbox = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' }
]
}
return (
<el-checkbox-group class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-checkbox key={index} label={item.label} value={item.value} />
))}
</el-checkbox-group>
)
}
const buildRadio = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' }
]
}
return (
<el-radio-group class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-radio key={index} value={item.value}>
{item.label}
</el-radio>
))}
</el-radio-group>
)
}
return () => (
<>
{props.selectType === 'select'
? buildSelect()
: props.selectType === 'radio'
? buildRadio()
: props.selectType === 'checkbox'
? buildCheckbox()
: buildSelect()}
</>
)
}
})
}
import { useUploadFileRule } from './useUploadFileRule'
import { useUploadImgRule } from './useUploadImgRule'
import { useUploadImgsRule } from './useUploadImgsRule'
import { useDictSelectRule } from './useDictSelectRule'
import { useEditorRule } from './useEditorRule'
import { useSelectRule } from './useSelectRule'
export {
useUploadFileRule,
useUploadImgRule,
useUploadImgsRule,
useDictSelectRule,
useEditorRule,
useSelectRule
}
const selectRule = [
{
type: 'select',
field: 'selectType',
title: '选择器类型',
value: 'select',
options: [
{ label: '下拉框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '多选框', value: 'checkbox' }
],
// 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
control: [
{
value: 'select',
condition: '=',
method: 'hidden',
rule: ['multiple']
}
]
},
{ type: 'switch', field: 'multiple', title: '是否多选' },
{
type: 'switch',
field: 'disabled',
title: '是否禁用'
},
{ type: 'switch', field: 'clearable', title: '是否可以清空选项' },
{
type: 'switch',
field: 'collapseTags',
title: '多选时是否将选中值按文字的形式展示'
},
{
type: 'inputNumber',
field: 'multipleLimit',
title: '多选时用户最多可以选择的项目数,为 0 则不限制',
props: { min: 0 }
},
{
type: 'input',
field: 'autocomplete',
title: 'autocomplete 属性'
},
{ type: 'input', field: 'placeholder', title: '占位符' },
{
type: 'switch',
field: 'filterable',
title: '是否可搜索'
},
{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
{
type: 'input',
field: 'noMatchText',
title: '搜索条件无匹配时显示的文字'
},
{
type: 'switch',
field: 'remote',
title: '其中的选项是否从服务器远程加载'
},
{
type: 'Struct',
field: 'remoteMethod',
title: '自定义远程搜索方法'
},
{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
{
type: 'switch',
field: 'reserveKeyword',
title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词'
},
{
type: 'switch',
field: 'defaultFirstOption',
title: '在输入框按下回车,选择第一个匹配项'
},
{
type: 'switch',
field: 'popperAppendToBody',
title: '是否将弹出框插入至 body 元素',
value: true
},
{
type: 'switch',
field: 'automaticDropdown',
title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单'
}
]
const apiSelectRule = [
{
type: 'input',
field: 'url',
title: 'url 地址',
props: {
placeholder: '/system/user/simple-list'
}
},
{
type: 'select',
field: 'method',
title: '请求类型',
value: 'GET',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' }
],
control: [
{
value: 'GET',
condition: '!=',
method: 'hidden',
rule: [
{
type: 'input',
field: 'data',
title: '请求参数 JSON 格式',
props: {
autosize: true,
type: 'textarea',
placeholder: '{"type": 1}'
}
}
]
}
]
},
{
type: 'input',
field: 'labelField',
title: 'label 属性',
props: {
placeholder: 'nickname'
}
},
{
type: 'input',
field: 'valueField',
title: 'value 属性',
props: {
placeholder: 'id'
}
}
]
export { selectRule, apiSelectRule }
import { generateUUID } from '@/utils'
import * as DictDataApi from '@/api/system/dict/dict.type'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
/**
* 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule
*/
export const useDictSelectRule = () => {
const label = '字典选择器'
const name = 'DictSelect'
const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据
onMounted(async () => {
const data = await DictDataApi.getSimpleDictTypeList()
if (!data || data.length === 0) {
return
}
dictOptions.value =
data?.map((item: DictDataApi.DictTypeVO) => ({
label: item.name,
value: item.type
})) ?? []
})
return {
icon: 'icon-doc-text',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'dictType',
title: '字典类型',
value: '',
options: dictOptions.value
},
{
type: 'select',
field: 'dictValueType',
title: '字典值类型',
value: 'str',
options: [
{ label: '数字', value: 'int' },
{ label: '字符串', value: 'str' },
{ label: '布尔值', value: 'bool' }
]
},
...selectRule
])
}
}
}
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useEditorRule = () => {
const label = '富文本'
const name = 'Editor'
return {
icon: 'icon-editor',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'input',
field: 'height',
title: '高度'
},
{ type: 'switch', field: 'readonly', title: '是否只读' }
])
}
}
}
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
import { SelectRuleOption } from '@/components/FormCreate/src/type'
/**
* 通用选择器规则 hook
*
* @param option 规则配置
*/
export const useSelectRule = (option: SelectRuleOption) => {
const label = option.label
const name = option.name
return {
icon: option.icon,
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
if (!option.props) {
option.props = []
}
return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule])
}
}
}
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadFileRule = () => {
const label = '文件上传'
const name = 'UploadFile'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'fileType',
title: '文件类型',
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
options: [
{ label: 'doc', value: 'doc' },
{ label: 'xls', value: 'xls' },
{ label: 'ppt', value: 'ppt' },
{ label: 'txt', value: 'txt' },
{ label: 'pdf', value: 'pdf' }
],
props: {
multiple: true
}
},
{
type: 'switch',
field: 'autoUpload',
title: '是否在选取文件后立即进行上传',
value: true
},
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'switch',
field: 'isShowTip',
title: '是否显示提示',
value: true
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'inputNumber',
field: 'limit',
title: '数量限制',
value: 5,
props: { min: 0 }
},
{
type: 'switch',
field: 'disabled',
title: '是否禁用',
value: false
}
])
}
}
}
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadImgRule = () => {
const label = '单图上传'
const name = 'UploadImg'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'select',
field: 'fileType',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
{ label: 'image/apng', value: 'image/apng' },
{ label: 'image/bmp', value: 'image/bmp' },
{ label: 'image/gif', value: 'image/gif' },
{ label: 'image/jpeg', value: 'image/jpeg' },
{ label: 'image/pjpeg', value: 'image/pjpeg' },
{ label: 'image/svg+xml', value: 'image/svg+xml' },
{ label: 'image/tiff', value: 'image/tiff' },
{ label: 'image/webp', value: 'image/webp' },
{ label: 'image/x-icon', value: 'image/x-icon' }
],
props: {
multiple: true
}
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px'
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px'
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px'
},
{
type: 'switch',
field: 'disabled',
title: '是否显示删除按钮',
value: true
},
{
type: 'switch',
field: 'showBtnText',
title: '是否显示按钮文字',
value: true
}
])
}
}
}
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadImgsRule = () => {
const label = '多图上传'
const name = 'UploadImgs'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'select',
field: 'fileType',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
{ label: 'image/apng', value: 'image/apng' },
{ label: 'image/bmp', value: 'image/bmp' },
{ label: 'image/gif', value: 'image/gif' },
{ label: 'image/jpeg', value: 'image/jpeg' },
{ label: 'image/pjpeg', value: 'image/pjpeg' },
{ label: 'image/svg+xml', value: 'image/svg+xml' },
{ label: 'image/tiff', value: 'image/tiff' },
{ label: 'image/webp', value: 'image/webp' },
{ label: 'image/x-icon', value: 'image/x-icon' }
],
props: {
multiple: true
}
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'inputNumber',
field: 'limit',
title: '数量限制',
value: 5,
props: { min: 0 }
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px'
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px'
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px'
}
])
}
}
}
import { Rule } from '@form-create/element-ui' //左侧拖拽按钮
// 左侧拖拽按钮
export interface MenuItem {
label: string
name: string
icon: string
}
// 左侧拖拽按钮分类
export interface Menu {
title: string
name: string
list: MenuItem[]
}
export interface MenuList extends Array<Menu> {}
// 拖拽组件的规则
export interface DragRule {
icon: string
name: string
label: string
children?: string
inside?: true
drag?: true | String
dragBtn?: false
mask?: false
rule(): Rule
props(v: any, v1: any): Rule[]
}
// 通用下拉组件 Props 类型
export interface ApiSelectProps {
name: string // 组件名称
labelField?: string // 选项标签
valueField?: string // 选项的值
url?: string // url 接口
isDict?: boolean // 是否字典选择器
}
// 选择组件规则配置类型
export interface SelectRuleOption {
label: string // label 名称
name: string // 组件名称
icon: string // 组件图标
props?: any[] // 组件规则
}
import {
useDictSelectRule,
useEditorRule,
useSelectRule,
useUploadFileRule,
useUploadImgRule,
useUploadImgsRule
} from './config'
import { Ref } from 'vue'
import { Menu } from '@/components/FormCreate/src/type'
import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
/**
* 表单设计器增强 hook
* 新增
* - 文件上传
* - 单图上传
* - 多图上传
* - 字典选择器
* - 用户选择器
* - 部门选择器
* - 富文本
*/
export const useFormCreateDesigner = async (designer: Ref) => {
const editorRule = useEditorRule()
const uploadFileRule = useUploadFileRule()
const uploadImgRule = useUploadImgRule()
const uploadImgsRule = useUploadImgsRule()
/**
* 构建表单组件
*/
const buildFormComponents = () => {
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
designer.value?.removeMenuItem('upload')
// 移除自带的富文本组件规则,使用 editorRule 替代
designer.value?.removeMenuItem('fc-editor')
const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule]
components.forEach((component) => {
// 插入组件规则
designer.value?.addComponent(component)
// 插入拖拽按钮到 `main` 分类下
designer.value?.appendMenuItem('main', {
icon: component.icon,
name: component.name,
label: component.label
})
})
}
const userSelectRule = useSelectRule({
name: 'UserSelect',
label: '用户选择器',
icon: 'icon-user-o'
})
const deptSelectRule = useSelectRule({
name: 'DeptSelect',
label: '部门选择器',
icon: 'icon-address-card-o'
})
const dictSelectRule = useDictSelectRule()
const apiSelectRule0 = useSelectRule({
name: 'ApiSelect',
label: '接口选择器',
icon: 'icon-server',
props: [...apiSelectRule]
})
/**
* 构建系统字段菜单
*/
const buildSystemMenu = () => {
// 移除自带的下拉选择器组件,使用 currencySelectRule 替代
designer.value?.removeMenuItem('select')
designer.value?.removeMenuItem('radio')
designer.value?.removeMenuItem('checkbox')
const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0]
const menu: Menu = {
name: 'system',
title: '系统字段',
list: components.map((component) => {
// 插入组件规则
designer.value?.addComponent(component)
// 插入拖拽按钮到 `system` 分类下
return {
icon: component.icon,
name: component.name,
label: component.label
}
})
}
designer.value?.addMenu(menu)
}
onMounted(async () => {
await nextTick()
buildFormComponents()
buildSystemMenu()
})
}
// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下)
export function makeRequiredRule() {
return {
type: 'Required',
field: 'formCreate$required',
title: '是否必填'
}
}
export const localeProps = (t, prefix, rules) => {
return rules.map((rule) => {
if (rule.field === 'formCreate$required') {
rule.title = t('props.required') || rule.title
} else if (rule.field && rule.field !== '_optionType') {
rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
}
return rule
})
}
export function upper(str) {
return str.replace(str[0], str[0].toLocaleUpperCase())
}
export function makeOptionsRule(t, to, userOptions) {
console.log(userOptions[0])
const options = [
{ label: t('props.optionsType.struct'), value: 0 },
{ label: t('props.optionsType.json'), value: 1 },
{ label: '用户数据', value: 2 }
]
const control = [
{
value: 0,
rule: [
{
type: 'TableOptions',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { defaultValue: [] }
}
]
},
{
value: 1,
rule: [
{
type: 'Struct',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { defaultValue: [] }
}
]
},
{
value: 2,
rule: [
{
type: 'TableOptions',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { modelValue: [] }
}
]
}
]
options.splice(0, 0)
control.push()
return {
type: 'radio',
title: t('props.options'),
field: '_optionType',
value: 0,
options,
props: {
type: 'button'
},
control
}
}
......@@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
initialIndex = 0,
infinite = true,
hideOnClickModal = false,
appendToBody = false,
teleported = false,
zIndex = 2000,
show = true
} = options
......@@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
propsData.initialIndex = initialIndex
propsData.infinite = infinite
propsData.hideOnClickModal = hideOnClickModal
propsData.appendToBody = appendToBody
propsData.teleported = teleported
propsData.zIndex = zIndex
propsData.show = show
......
......@@ -13,7 +13,7 @@ const props = defineProps({
initialIndex: propTypes.number.def(0),
infinite: propTypes.bool.def(true),
hideOnClickModal: propTypes.bool.def(false),
appendToBody: propTypes.bool.def(false),
teleported: propTypes.bool.def(false),
show: propTypes.bool.def(false)
})
......
......@@ -4,6 +4,6 @@ export interface ImageViewerProps {
initialIndex?: number
infinite?: boolean
hideOnClickModal?: boolean
appendToBody?: boolean
teleported?: boolean
show?: boolean
}
......@@ -189,7 +189,7 @@ const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
// 热区选中
const selectedHotAreaIndex = ref(-1)
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index
emit('hotAreaSelected', hotArea, index)
......
......@@ -23,7 +23,7 @@
</template>
<script lang="ts" setup>
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
import { ElTag } from 'element-plus'
......@@ -31,7 +31,7 @@ import { ElTag } from 'element-plus'
defineOptions({ name: 'OperateLogV2' })
interface Props {
logList: OperateLogV2VO[] // 操作日志列表
logList: OperateLogVO[] // 操作日志列表
}
withDefaults(defineProps<Props>(), {
......
......@@ -53,7 +53,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination'])
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
const currentPage = computed({
get() {
return props.page
......
......@@ -26,7 +26,7 @@
placeholder="请输入菜单内容"
:remote-method="remoteMethod"
class="overflow-hidden transition-all-600"
:class="showTopSearch ? 'w-220px ml2' : 'w-0'"
:class="showTopSearch ? '!w-220px ml2' : '!w-0'"
@change="handleChange"
>
<el-option
......
......@@ -6,7 +6,9 @@
:action="uploadUrl"
:auto-upload="autoUpload"
:before-upload="beforeUpload"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:limit="props.limit"
:multiple="props.limit > 1"
:on-error="excelUploadError"
......@@ -15,15 +17,14 @@
:on-remove="handleRemove"
:on-success="handleFileSuccess"
:show-file-list="true"
:http-request="httpRequest"
class="upload-file-uploader"
name="file"
>
<el-button type="primary">
<el-button v-if="!disabled" type="primary">
<Icon icon="ep:upload-filled" />
选取文件
</el-button>
<template v-if="isShowTip" #tip>
<template v-if="isShowTip && !disabled" #tip>
<div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</div>
......@@ -31,6 +32,25 @@
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
</div>
</template>
<template #file="row">
<div class="flex items-center">
<span>{{ row.file.name }}</span>
<div class="ml-10px">
<el-link
:href="row.file.url"
:underline="false"
download
target="_blank"
type="primary"
>
下载
</el-link>
</div>
<div class="ml-10px">
<el-button link type="danger" @click="handleRemove(row.file)"> 删除</el-button>
</div>
</div>
</template>
</el-upload>
</div>
</template>
......@@ -48,13 +68,13 @@ const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
title: propTypes.string.def('文件上传'),
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // 大小限制(MB)
limit: propTypes.number.def(5), // 数量限制
autoUpload: propTypes.bool.def(true), // 自动上传
drag: propTypes.bool.def(false), // 拖拽上传
isShowTip: propTypes.bool.def(true) // 是否显示提示
isShowTip: propTypes.bool.def(true), // 是否显示提示
disabled: propTypes.bool.def(false) // 是否禁用上传组件 ==> 非必传(默认为 false)
})
// ========== 上传相关 ==========
......
......@@ -6,17 +6,18 @@
:action="uploadUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:multiple="false"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="false"
:http-request="httpRequest"
>
<template v-if="modelValue">
<img :src="modelValue" class="upload-image" />
<div class="upload-handle" @click.stop>
<div class="handle-icon" @click="editImg" v-if="!disabled">
<div v-if="!disabled" class="handle-icon" @click="editImg">
<Icon icon="ep:edit" />
<span v-if="showBtnText">{{ t('action.edit') }}</span>
</div>
......@@ -77,10 +78,8 @@ const props = defineProps({
height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
// 是否显示删除按钮
showDelete: propTypes.bool.def(true),
// 是否显示按钮文字
showBtnText: propTypes.bool.def(true)
showDelete: propTypes.bool.def(true), // 是否显示删除按钮
showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
......
......@@ -6,13 +6,14 @@
:action="uploadUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:limit="limit"
:multiple="true"
:on-error="uploadError"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
:http-request="httpRequest"
list-type="picture-card"
>
<div class="upload-empty">
......
......@@ -17,7 +17,11 @@ export const useUpload = () => {
// 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
return axios.put(presignedInfo.uploadUrl, options.file).then(() => {
return axios.put(presignedInfo.uploadUrl, options.file, {
headers: {
'Content-Type': options.file.type,
}
}).then(() => {
// 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, fileName, options.file)
// 通知成功,数据格式保持与后端上传的返回结果一致
......
......@@ -3,13 +3,6 @@
<el-form label-width="90px" :model="needProps" :rules="rules">
<div v-if="needProps.type == 'bpmn:Process'">
<!-- 如果是 Process 信息的时候,使用自定义表单 -->
<el-link
href="https://doc.iocoder.cn/bpm/#_3-%E6%B5%81%E7%A8%8B%E5%9B%BE%E7%A4%BA%E4%BE%8B"
type="danger"
target="_blank"
>
如何实现实现会签、或签?
</el-link>
<el-form-item label="流程标识" prop="id">
<el-input
v-model="needProps.id"
......@@ -139,14 +132,6 @@ const updateBaseInfo = (key) => {
}
}
onMounted(() => {
// 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
setTimeout(() => {
handleKeyUpdate(props.model.key)
handleNameUpdate(props.model.name)
}, 110)
})
watch(
() => props.businessObject,
(val) => {
......
......@@ -79,13 +79,13 @@ const resetFlowCondition = () => {
bpmnElement.value = bpmnInstances().bpmnElement
bpmnElementSource.value = bpmnElement.value.source
bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef
// 初始化默认type为default
flowConditionForm.value = { type: 'default' }
if (
bpmnElementSourceRef.value &&
bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id &&
flowConditionForm.value.type == 'default'
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
) {
// 默认
flowConditionForm.value = { type: 'default' }
} else if (!bpmnElement.value.businessObject.conditionExpression) {
// 普通
......
......@@ -13,7 +13,7 @@ import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } f
import errorCode from './errorCode'
import { resetRouter } from '@/router'
import { useCache } from '@/hooks/web/useCache'
import { deleteUserCache } from '@/hooks/web/useCache'
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
const { result_code, base_url, request_timeout } = config
......@@ -217,9 +217,8 @@ const handleAuthorized = () => {
confirmButtonText: t('login.relogin'),
type: 'warning'
}).then(() => {
const { wsCache } = useCache()
resetRouter() // 重置静态路由表
wsCache.clear()
deleteUserCache() // 删除用户缓存
removeToken()
isRelogin.show = false
// 干掉token后再走一次路由让它过router.beforeEach的校验
......
......@@ -7,13 +7,18 @@ import WebStorageCache from 'web-storage-cache'
type CacheType = 'localStorage' | 'sessionStorage'
export const CACHE_KEY = {
IS_DARK: 'isDark',
// 用户相关
ROLE_ROUTERS: 'roleRouters',
USER: 'user',
// 系统设置
IS_DARK: 'isDark',
LANG: 'lang',
THEME: 'theme',
LAYOUT: 'layout',
ROLE_ROUTERS: 'roleRouters',
DICT_CACHE: 'dictCache'
DICT_CACHE: 'dictCache',
// 登录表单
LoginForm: 'loginForm',
TenantId: 'tenantId'
}
export const useCache = (type: CacheType = 'localStorage') => {
......@@ -25,3 +30,10 @@ export const useCache = (type: CacheType = 'localStorage') => {
wsCache
}
}
export const deleteUserCache = () => {
const { wsCache } = useCache()
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
// 注意,不要清理 LoginForm 登录表单
}
......@@ -24,13 +24,12 @@ const toggleCollapse = () => {
</script>
<template>
<div :class="prefixCls">
<div :class="prefixCls" @click="toggleCollapse">
<Icon
:color="color"
:icon="collapse ? 'ep:expand' : 'ep:fold'"
:size="18"
class="cursor-pointer"
@click="toggleCollapse"
/>
</div>
</template>
......@@ -124,16 +124,6 @@ export default defineComponent({
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-menu;
.is-active--after {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background-color: var(--el-color-primary);
content: '';
}
.#{$prefix-cls} {
position: relative;
transition: width var(--transition-time-02);
......@@ -159,7 +149,6 @@ $prefix-cls: #{$namespace}-menu;
}
// 设置选中时的高亮背景和高亮颜色
.#{$elNamespace}-sub-menu.is-active,
.#{$elNamespace}-menu-item.is-active {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-active-color) !important;
......@@ -171,10 +160,6 @@ $prefix-cls: #{$namespace}-menu;
.#{$elNamespace}-menu-item.is-active {
position: relative;
&::after {
@extend .is-active--after;
}
}
// 设置子菜单的背景颜色
......@@ -194,10 +179,6 @@ $prefix-cls: #{$namespace}-menu;
& > .is-active > .#{$elNamespace}-sub-menu__title {
position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important;
&::after {
@extend .is-active--after;
}
}
}
......@@ -245,16 +226,6 @@ $prefix-cls: #{$namespace}-menu;
<style lang="scss">
$prefix-cls: #{$namespace}-menu-popper;
.is-active--after {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background-color: var(--el-color-primary);
content: '';
}
.#{$prefix-cls}--vertical,
.#{$prefix-cls}--horizontal {
// 设置选中时子标题的颜色
......@@ -281,10 +252,6 @@ $prefix-cls: #{$namespace}-menu-popper;
&:hover {
background-color: var(--left-menu-bg-active-color) !important;
}
&::after {
@extend .is-active--after;
}
}
}
</style>
import { ElSubMenu, ElMenuItem } from 'element-plus'
import type { RouteMeta } from 'vue-router'
import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle'
import { useDesign } from '@/hooks/web/useDesign'
import { pathResolve } from '@/utils/routerHelper'
export const useRenderMenuItem = (
// allRouters: AppRouteRecordRaw[] = [],
menuMode: 'vertical' | 'horizontal'
) => {
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers.map((v) => {
const meta = (v.meta ?? {}) as RouteMeta
if (!meta.hidden) {
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
const { renderMenuTitle } = useRenderMenuTitle()
const { renderMenuTitle } = useRenderMenuTitle()
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
const { getPrefixCls } = useDesign()
export const useRenderMenuItem = () =>
// allRouters: AppRouteRecordRaw[] = [],
{
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers
.filter((v) => !v.meta?.hidden)
.map((v) => {
const meta = v.meta ?? {}
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
const preFixCls = getPrefixCls('menu-popper')
return (
<ElSubMenu
index={fullPath}
popperClass={
menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
}
>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
}
})
}
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
return (
<ElSubMenu index={fullPath}>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
})
}
return {
renderMenuItem
return {
renderMenuItem
}
}
}
import type { RouteMeta } from 'vue-router'
import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n'
export const useRenderMenuTitle = () => {
const renderMenuTitle = (meta: RouteMeta) => {
......@@ -9,10 +10,14 @@ export const useRenderMenuTitle = () => {
return icon ? (
<>
<Icon icon={meta.icon}></Icon>
<span class="v-menu__title">{t(title as string)}</span>
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
</>
) : (
<span class="v-menu__title">{t(title as string)}</span>
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
)
}
......
......@@ -139,7 +139,7 @@ export default defineComponent({
id={`${variables.namespace}-menu`}
class={[
prefixCls,
'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right',
'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
{
'w-[var(--tab-menu-max-width)]': !unref(collapse),
'w-[var(--tab-menu-min-width)]': unref(collapse)
......@@ -195,7 +195,7 @@ export default defineComponent({
</div>
<Menu
class={[
'!absolute top-0',
'!absolute top-0 z-11',
{
'!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
......
......@@ -5,6 +5,9 @@ import avatarImg from '@/assets/imgs/avatar.gif'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useUserStore } from '@/store/modules/user'
import LockDialog from './components/LockDialog.vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/store/modules/lock'
defineOptions({ name: 'UserInfo' })
......@@ -23,6 +26,14 @@ const prefixCls = getPrefixCls('user-info')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
// 锁定屏幕
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const dialogVisible = ref<boolean>(false)
const lockScreen = () => {
dialogVisible.value = true
}
const loginOut = async () => {
try {
await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
......@@ -33,8 +44,7 @@ const loginOut = async () => {
await userStore.loginOut()
tagsViewStore.delAllViews()
replace('/login?redirect=/index')
}
catch { }
} catch {}
}
const toProfile = async () => {
push('/user/profile')
......@@ -62,6 +72,10 @@ const toDocument = () => {
<Icon icon="ep:menu" />
<div @click="toDocument">{{ t('common.document') }}</div>
</ElDropdownItem>
<ElDropdownItem divided>
<Icon icon="ep:lock" />
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<ElDropdownItem divided @click="loginOut">
<Icon icon="ep:switch-button" />
<div>{{ t('common.loginOut') }}</div>
......@@ -69,4 +83,31 @@ const toDocument = () => {
</ElDropdownMenu>
</template>
</ElDropdown>
<LockDialog v-if="dialogVisible" v-model="dialogVisible" />
<teleport to="body">
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />
</transition>
</teleport>
</template>
<style scoped lang="scss">
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition:
opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>
<script setup lang="ts">
import { useValidator } from '@/hooks/web/useValidator'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/store/modules/user'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-dialog')
const { required } = useValidator()
const { t } = useI18n()
const lockStore = useLockStore()
const props = defineProps({
modelValue: {
type: Boolean
}
})
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const emit = defineEmits(['update:modelValue'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
console.log('set: ', val)
emit('update:modelValue', val)
}
})
const dialogTitle = ref(t('lock.lockScreen'))
const formData = ref({
password: undefined
})
const formRules = reactive({
password: [required()]
})
const formRef = ref() // 表单 Ref
const handleLock = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
dialogVisible.value = false
lockStore.setLockInfo({
...formData.value,
isLock: true
})
}
</script>
<template>
<Dialog
v-model="dialogVisible"
width="500px"
max-height="170px"
:class="prefixCls"
:title="dialogTitle"
>
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">
{{ userName }}
</span>
</div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item :label="t('lock.lockPassword')" prop="password">
<el-input
type="password"
v-model="formData.password"
:placeholder="'请输入' + t('lock.lockPassword')"
clearable
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
</template>
</Dialog>
</template>
<style lang="scss" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
max-width: calc(100vw - 16px);
}
}
</style>
<script lang="ts" setup>
import { resetRouter } from '@/router'
import { deleteUserCache } from '@/hooks/web/useCache'
import { useLockStore } from '@/store/modules/lock'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useUserStore } from '@/store/modules/user'
import avatarImg from '@/assets/imgs/avatar.gif'
const tagsViewStore = useTagsViewStore()
const { replace } = useRouter()
const userStore = useUserStore()
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
// 解锁
async function unLock() {
if (!password.value) {
return
}
let pwd = password.value
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
} finally {
loading.value = false
}
}
// 返回登录
async function goLogin() {
await userStore.loginOut().catch(() => {})
// 登出后清理
deleteUserCache() // 清空用户缓存
tagsViewStore.delAllViews()
resetRouter() // 重置静态路由表
lockStore.resetLockInfo()
replace('/login')
}
function handleShowForm(show = false) {
showDate.value = show
}
</script>
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<Icon icon="ep:lock" />
<span>{{ t('lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">
{{ userName }}
</span>
</div>
<ElInput
type="password"
:placeholder="t('lock.placeholder')"
class="enter-x"
v-model="password"
/>
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</ElButton>
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</ElButton>
<ElButton
type="primary"
class="mt-2"
size="small"
link
@click="unLock()"
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</ElButton>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: '#{$namespace}-lock-page';
// Small screen / tablet
$screen-sm: 576px;
// Medium screen / desktop
$screen-md: 768px;
// Large screen / wide desktop
$screen-lg: 992px;
// Extra large screen / full hd
$screen-xl: 1200px;
// Extra extra large screen / large desktop
$screen-2xl: 1600px;
$error-color: #ed6f6f;
.#{$prefix-cls} {
z-index: 3000;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: $screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: $screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: $screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: $screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: $error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>
......@@ -56,6 +56,16 @@ export default {
copySuccess: 'Copy Success',
copyError: 'Copy Error'
},
lock: {
lockScreen: 'Lock screen',
lock: 'Lock',
lockPassword: 'Lock screen password',
unlock: 'Click to unlock',
backToLogin: 'Back to login',
entrySystem: 'Entry the system',
placeholder: 'Please enter the lock screen password',
message: 'Lock screen password error'
},
error: {
noPermission: `Sorry, you don't have permission to access this page.`,
pageError: 'Sorry, the page you visited does not exist.',
......
......@@ -56,6 +56,16 @@ export default {
copySuccess: '复制成功',
copyError: '复制失败'
},
lock: {
lockScreen: '锁定屏幕',
lock: '锁定',
lockPassword: '锁屏密码',
unlock: '点击解锁',
backToLogin: '返回登录',
entrySystem: '进入系统',
placeholder: '请输入锁屏密码',
message: '锁屏密码错误'
},
error: {
noPermission: `抱歉,您无权访问此页面。`,
pageError: '抱歉,您访问的页面不存在。',
......
......@@ -2,23 +2,24 @@ import * as echarts from 'echarts/core'
import {
BarChart,
FunnelChart,
GaugeChart,
LineChart,
PieChart,
MapChart,
PictorialBarChart,
RadarChart,
GaugeChart
PieChart,
RadarChart
} from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
GridComponent,
PolarComponent,
AriaComponent,
ParallelComponent,
GridComponent,
LegendComponent,
ParallelComponent,
PolarComponent,
TitleComponent,
ToolboxComponent,
TooltipComponent,
VisualMapComponent
} from 'echarts/components'
......@@ -41,7 +42,8 @@ echarts.use([
CanvasRenderer,
PictorialBarChart,
RadarChart,
GaugeChart
GaugeChart,
FunnelChart
])
export default echarts
import type { App } from 'vue'
// 👇使用 form-create 需额外全局引入 element plus 组件
import {
ElAlert,
ElAside,
ElPopconfirm,
ElHeader,
ElMain,
ElContainer,
ElDivider,
ElTransfer,
ElAlert,
ElTabs,
ElHeader,
ElMain,
ElPopconfirm,
ElTable,
ElTableColumn,
ElTabPane
ElTabPane,
ElTabs,
ElTransfer
} from 'element-plus'
import FcDesigner from '@form-create/designer'
import formCreate from '@form-create/element-ui'
import install from '@form-create/element-ui/auto-import'
//======================= 自定义组件 =======================
import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'
import { useApiSelect } from '@/components/FormCreate'
import { Editor } from '@/components/Editor'
import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue'
const UserSelect = useApiSelect({
name: 'UserSelect',
labelField: 'nickname',
valueField: 'id',
url: '/system/user/simple-list'
})
const DeptSelect = useApiSelect({
name: 'DeptSelect',
labelField: 'name',
valueField: 'id',
url: '/system/dept/simple-list'
})
const ApiSelect = useApiSelect({
name: 'ApiSelect'
})
const components = [
ElAside,
ElPopconfirm,
......@@ -30,7 +52,15 @@ const components = [
ElTabs,
ElTable,
ElTableColumn,
ElTabPane
ElTabPane,
UploadImg,
UploadImgs,
UploadFile,
DictSelect,
UserSelect,
DeptSelect,
ApiSelect,
Editor
]
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
......@@ -40,4 +70,5 @@ export const setupFormCreate = (app: App<Element>) => {
})
formCreate.use(install)
app.use(formCreate)
app.use(FcDesigner)
}
import type { App } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => {
app.use(store)
......
......@@ -268,7 +268,8 @@ export const useAppStore = defineStore('app', {
setFooter(footer: boolean) {
this.footer = footer
}
}
},
persist: false
})
export const useAppStoreWithOut = () => {
......
import { defineStore } from 'pinia'
import { store } from '@/store'
interface lockInfo {
isLock?: boolean
password?: string
}
interface LockState {
lockInfo: lockInfo
}
export const useLockStore = defineStore('lock', {
state: (): LockState => {
return {
lockInfo: {
// isLock: false, // 是否锁定屏幕
// password: '' // 锁屏密码
}
}
},
getters: {
getLockInfo(): lockInfo {
return this.lockInfo
}
},
actions: {
setLockInfo(lockInfo: lockInfo) {
this.lockInfo = lockInfo
},
resetLockInfo() {
this.lockInfo = {}
},
unLock(password: string) {
if (this.lockInfo?.password === password) {
this.resetLockInfo()
return true
} else {
return false
}
}
},
persist: true
})
export const useLockStoreWithOut = () => {
return useLockStore(store)
}
import { defineStore } from 'pinia'
import { store } from '../index'
import { store } from '@/store'
import { cloneDeep } from 'lodash-es'
import remainingRouter from '@/router/modules/remaining'
import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'
......@@ -59,7 +59,8 @@ export const usePermissionStore = defineStore('permission', {
setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
this.menuTabRouters = routers
}
}
},
persist: false
})
export const usePermissionStoreWithOut = () => {
......
......@@ -132,7 +132,8 @@ export const useTagsViewStore = defineStore('tagsView', {
}
}
}
}
},
persist: false
})
export const useTagsViewStoreWithOut = () => {
......
import { store } from '../index'
import { store } from '@/store'
import { defineStore } from 'pinia'
import { getAccessToken, removeToken } from '@/utils/auth'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
import { getInfo, loginOut } from '@/api/login'
const { wsCache } = useCache()
......@@ -14,6 +14,7 @@ interface UserVO {
}
interface UserInfoVO {
// USER 缓存
permissions: string[]
roles: string[]
isSetUser: boolean
......@@ -80,7 +81,7 @@ export const useUserStore = defineStore('admin-user', {
async loginOut() {
await loginOut()
removeToken()
wsCache.clear()
deleteUserCache() // 删除用户缓存
this.resetState()
},
resetState() {
......
// 使用字体图标来源 https://fontello.com/
@font-face {
font-family: 'fc-icon';
src: url('@/styles/FormCreate/fonts/fontello.woff') format('woff');
}
.icon-doc-text:before {
content: '\f0f6';
}
.icon-server:before {
content: '\f233';
}
.icon-address-card-o:before {
content: '\f2bc';
}
.icon-user-o:before {
content: '\f2c0';
}
@import './var.css';
@import './FormCreate/index.scss';
@import 'element-plus/theme-chalk/dark/css-vars.css';
.reset-margin [class*='el-icon'] + span {
......
import { useCache } from '@/hooks/web/useCache'
import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
import { TokenType } from '@/api/login/types'
import { decrypt, encrypt } from '@/utils/jsencrypt'
......@@ -36,8 +36,6 @@ export const formatToken = (token: string): string => {
}
// ========== 账号相关 ==========
const LoginFormKey = 'LOGINFORM'
export type LoginFormType = {
tenantName: string
username: string
......@@ -46,7 +44,7 @@ export type LoginFormType = {
}
export const getLoginForm = () => {
const loginForm: LoginFormType = wsCache.get(LoginFormKey)
const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm)
if (loginForm) {
loginForm.password = decrypt(loginForm.password) as string
}
......@@ -55,38 +53,19 @@ export const getLoginForm = () => {
export const setLoginForm = (loginForm: LoginFormType) => {
loginForm.password = encrypt(loginForm.password) as string
wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 })
wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 })
}
export const removeLoginForm = () => {
wsCache.delete(LoginFormKey)
wsCache.delete(CACHE_KEY.LoginForm)
}
// ========== 租户相关 ==========
const TenantIdKey = 'TENANT_ID'
const TenantNameKey = 'TENANT_NAME'
export const getTenantName = () => {
return wsCache.get(TenantNameKey)
}
export const setTenantName = (username: string) => {
wsCache.set(TenantNameKey, username, { exp: 30 * 24 * 60 * 60 })
}
export const removeTenantName = () => {
wsCache.delete(TenantNameKey)
}
export const getTenantId = () => {
return wsCache.get(TenantIdKey)
return wsCache.get(CACHE_KEY.TenantId)
}
export const setTenantId = (username: string) => {
wsCache.set(TenantIdKey, username)
}
export const removeTenantId = () => {
wsCache.delete(TenantIdKey)
wsCache.set(CACHE_KEY.TenantId, username)
}
......@@ -248,15 +248,15 @@ export const CouponTemplateTakeTypeEnum = {
*/
export const PromotionProductScopeEnum = {
ALL: {
scope: 10,
scope: 1,
name: '通用劵'
},
SPU: {
scope: 20,
scope: 2,
name: '商品劵'
},
CATEGORY: {
scope: 30,
scope: 3,
name: '品类劵'
}
}
......
/**
* Independent time operation tool to facilitate subsequent switch to dayjs
*/
// TODO 芋艿:【锁屏】可能后面删除掉
import dayjs from 'dayjs'
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const DATE_FORMAT = 'YYYY-MM-DD'
export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
return dayjs(date).format(format)
}
export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
return dayjs(date).format(format)
}
export const dateUtil = dayjs
/**
* 数据字典工具类
*/
import { useDictStoreWithOut } from '@/store/modules/dict'
import { ElementPlusInfoType } from '@/types/elementPlus'
import {useDictStoreWithOut} from '@/store/modules/dict'
import {ElementPlusInfoType} from '@/types/elementPlus'
const dictStore = useDictStoreWithOut()
......@@ -104,6 +104,7 @@ export enum DICT_TYPE {
USER_TYPE = 'user_type',
COMMON_STATUS = 'common_status',
TERMINAL = 'terminal', // 终端
DATE_INTERVAL = 'date_interval', // 数据间隔
// ========== SYSTEM 模块 ==========
SYSTEM_USER_SEX = 'system_user_sex',
......@@ -111,7 +112,6 @@ export enum DICT_TYPE {
SYSTEM_ROLE_TYPE = 'system_role_type',
SYSTEM_DATA_SCOPE = 'system_data_scope',
SYSTEM_NOTICE_TYPE = 'system_notice_type',
SYSTEM_OPERATE_TYPE = 'system_operate_type',
SYSTEM_LOGIN_TYPE = 'system_login_type',
SYSTEM_LOGIN_RESULT = 'system_login_result',
SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
......@@ -134,6 +134,7 @@ export enum DICT_TYPE {
INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
INFRA_FILE_STORAGE = 'infra_file_storage',
INFRA_OPERATE_TYPE = 'infra_operate_type',
// ========== BPM 模块 ==========
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
......@@ -196,14 +197,15 @@ export enum DICT_TYPE {
// ========== CRM - 客户管理模块 ==========
CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态
CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型
CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型
CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式
CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry',
CRM_CUSTOMER_LEVEL = 'crm_customer_level',
CRM_CUSTOMER_SOURCE = 'crm_customer_source',
CRM_PRODUCT_STATUS = 'crm_product_status',
CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业
CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别
CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源
CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态
CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别
CRM_PRODUCT_UNIT = 'crm_product_unit', // 产品单位
CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // 跟进方式
CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位
CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式
// ========== ERP - 企业资源计划模块 ==========
ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态
......
......@@ -40,7 +40,7 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri
export const setConfAndFields2 = (
detailPreview: object,
conf: string,
fields: string,
fields: string[],
value?: object
) => {
if (isRef(detailPreview)) {
......
......@@ -329,10 +329,11 @@ const ERP_PRICE_DIGIT = 2
* 例如说:库存数量
*
* @param num 数量
* @package digit 保留的小数位数
* @return 格式化后的数量
*/
export const erpNumberFormatter = (num: number | string | undefined, digit: number) => {
if (num === null) {
if (num == null) {
return ''
}
if (typeof num === 'string') {
......@@ -404,3 +405,47 @@ export const erpPriceMultiply = (price: number, count: number) => {
}
return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT))
}
/**
* 【ERP】百分比计算,四舍五入保留两位小数
*
* 如果 total 为 0,则返回 0
*
* @param value 当前值
* @param total 总值
*/
export const erpCalculatePercentage = (value: number, total: number) => {
if (total === 0) return 0
return ((value / total) * 100).toFixed(2)
}
/**
* 适配 echarts map 的地名
*
* @param areaName 地区名称
*/
export const areaReplace = (areaName: string) => {
if (!areaName) {
return areaName
}
return areaName
.replace('维吾尔自治区', '')
.replace('壮族自治区', '')
.replace('回族自治区', '')
.replace('自治区', '')
.replace('省', '')
}
/**
* 解析 JSON 字符串
*
* @param str
*/
export function jsonParse(str: string) {
try {
return JSON.parse(str)
} catch (e) {
console.error(`str[${str}] 不是一个 JSON 字符串`)
return ''
}
}
......@@ -2,6 +2,7 @@ import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { isUrl } from '@/utils/is'
import { cloneDeep, omit } from 'lodash-es'
import qs from 'qs'
const modules = import.meta.glob('../views/**/*.{vue,tsx}')
/**
......@@ -64,6 +65,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
const res: AppRouteRecordRaw[] = []
const modulesRoutesKeys = Object.keys(modules)
for (const route of routes) {
// 1. 生成 meta 菜单元数据
const meta = {
title: route.name,
icon: route.icon,
......@@ -73,10 +75,20 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
route.children &&
route.children.length === 1 &&
(route.alwaysShow !== undefined ? route.alwaysShow : true)
} as any
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
// 此时,我们需要解析参数,并且将参数放到 meta.query 中
// 这样,后续在 Vue 文件中,可以通过 const { currentRoute } = useRouter() 中,通过 meta.query 获取到参数
if (route.component && route.component.indexOf('?') > -1) {
const query = route.component.split('?')[1]
route.component = route.component.split('?')[0]
meta.query = qs.parse(query)
}
// 2. 生成 data(AppRouteRecordRaw)
// 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive
let data: AppRouteRecordRaw = {
path: route.path,
path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path,
name:
route.componentName && route.componentName.length > 0
? route.componentName
......
......@@ -62,7 +62,14 @@
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
<el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
......@@ -76,13 +83,13 @@
:sm="24"
:xs="24"
>
<el-card shadow="hover">
<el-card shadow="hover" class="mr-5px mt-5px">
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-8px" />
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-16px flex justify-between text-12px text-gray-400">
<div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
......@@ -204,45 +211,45 @@ let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'Github',
name: 'ruoyi-vue-pro',
icon: 'akar-icons:github-fill',
message: 'workplace.introduction',
personal: 'Archer',
message: 'https://github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date()
},
{
name: 'Vue',
name: 'yudao-ui-admin-vue3',
icon: 'logos:vue',
message: 'workplace.introduction',
personal: 'Archer',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus',
time: new Date()
},
{
name: 'Angular',
icon: 'logos:angular-icon',
message: 'workplace.introduction',
personal: 'Archer',
name: 'yudao-ui-admin-vben',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben(antd)',
time: new Date()
},
{
name: 'React',
icon: 'logos:react',
message: 'workplace.introduction',
personal: 'Archer',
name: 'yudao-cloud',
icon: 'akar-icons:github',
message: 'https://github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date()
},
{
name: 'Webpack',
icon: 'logos:webpack',
message: 'workplace.introduction',
personal: 'Archer',
name: 'yudao-ui-mall-uniapp',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp',
time: new Date()
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
message: 'workplace.introduction',
personal: 'Archer',
name: 'yudao-ui-admin-vue2',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
personal: 'Vue2 + element-ui',
time: new Date()
}
]
......@@ -254,27 +261,27 @@ let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统升级版本',
title: '系统支持 JDK 8/17/21,Vue 2/3',
type: '通知',
keys: ['通知', '升级'],
keys: ['通知', '8', '17', '21', '2', '3'],
date: new Date()
},
{
title: '系统凌晨维护',
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '公告',
keys: ['公告', '维护'],
keys: ['公告', 'Boot', 'Cloud'],
date: new Date()
},
{
title: '系统升级版本',
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '通知',
keys: ['通知', '升级'],
keys: ['通知', '无需授权'],
date: new Date()
},
{
title: '系统凌晨维护',
title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
type: '公告',
keys: ['公告', '维护'],
keys: ['公告', '最广泛'],
date: new Date()
}
]
......
......@@ -64,7 +64,7 @@
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-form-item v-if="loginData.tenantEnable" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
......@@ -207,7 +207,7 @@ const loginData = reactive({
// 获取验证码
const getCode = async () => {
// 情况一,未开启:则直接登录
if (loginData.captchaEnable) {
if (!loginData.captchaEnable) {
await handleLogin({})
} else {
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
......
......@@ -188,7 +188,7 @@ const loginData = reactive({
username: 'admin',
password: 'admin123',
captchaVerification: '',
rememberMe: false
rememberMe: true // 默认记录我。如果不需要,可手动修改
}
})
......@@ -218,14 +218,14 @@ const getTenantId = async () => {
}
}
// 记住我
const getCookie = () => {
const getLoginFormCache = () => {
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe ? true : false,
rememberMe: loginForm.rememberMe,
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
}
}
......@@ -326,7 +326,7 @@ watch(
}
)
onMounted(() => {
getCookie()
getLoginFormCache()
getTenantByWebsite()
})
</script>
......
<template>
<el-container>
<!-- 左侧:会话列表 -->
<el-aside width="260px" class="conversation-container">
<!-- 左顶部:新建对话 -->
<el-button class="w-1/1" type="primary">
<Icon icon="ep:plus" class="mr-5px" />
新建对话
</el-button>
<!-- 左顶部:搜索对话 -->
<el-input
v-model="searchName"
class="mt-10px"
placeholder="搜索历史记录"
@keyup="searchConversation"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<!-- 左中间:对话列表 -->
<div class="conversation-list" :style="{ height: leftHeight + 'px' }">
<el-row v-for="conversation in conversationList" :key="conversation.id">
<div
:class="conversation.id === conversationId ? 'conversation active' : 'conversation'"
@click="changeConversation(conversation)"
>
<el-image :src="conversation.avatar" class="avatar" />
<span class="title">{{ conversation.title }}</span>
<span class="button">
<!-- TODO 芋艿:缺置顶按钮 -->
<el-icon title="编辑" @click="updateConversationTitle(conversation)">
<Icon icon="ep:edit" />
</el-icon>
<el-icon title="删除会话" @click="deleteConversationTitle(conversation)">
<Icon icon="ep:delete" />
</el-icon>
</span>
</div>
</el-row>
</div>
<!-- 左底部:工具栏 TODO 芋艿:50% 不太对 -->
<div class="tool-box">
<sapn class="w-1/2"> <Icon icon="ep:user" /> 角色仓库 </sapn>
<sapn class="w-1/2"> <Icon icon="ep:delete" />清空未置顶对话</sapn>
</div>
</el-aside>
<!-- 右侧:会话详情 -->
<el-container class="detail-container">
<!-- 右顶部 TODO 芋艿:右对齐 -->
<el-header class="header">
<el-button>3.5-turbo-0125 <Icon icon="ep:setting" /></el-button>
<el-button>
<Icon icon="ep:user" />
</el-button>
<el-button>
<Icon icon="ep:download" />
</el-button>
<el-button>
<Icon icon="ep:arrow-up" />
</el-button>
</el-header>
<el-main>对话列表</el-main>
<el-footer>发送消息框</el-footer>
</el-container>
</el-container>
</template>
<script setup lang="ts">
const conversationList = [
{
id: 1,
title: '测试标题',
avatar:
'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
},
{
id: 2,
title: '测试对话',
avatar:
'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
}
]
const conversationId = ref(1)
const searchName = ref('')
const leftHeight = window.innerHeight - 240 // TODO 芋艿:这里还不太对
const changeConversation = (conversation) => {
console.log(conversation)
conversationId.value = conversation.id
// TODO 芋艿:待实现
}
const updateConversationTitle = (conversation) => {
console.log(conversation)
// TODO 芋艿:待实现
}
const deleteConversationTitle = (conversation) => {
console.log(conversation)
// TODO 芋艿:待实现
}
const searchConversation = () => {
// TODO 芋艿:待实现
}
</script>
<style lang="scss" scoped>
.conversation-container {
.conversation-list {
.conversation {
display: flex;
justify-content: flex-start;
width: 100%;
padding: 5px 5px 0 0;
cursor: pointer;
&.active {
// TODO 芋艿:这里不太对
background-color: #343540;
.button {
display: inline;
}
}
.title {
padding: 5px 10px;
max-width: 220px;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
}
.button {
position: absolute;
right: 2px;
top: 16px;
.el-icon {
margin-right: 5px;
}
}
}
.tool-box {
display: flex;
justify-content: flex-start;
padding: 0 20px 10px 20px;
border-top: 1px solid black;
}
}
}
.detail-container {
.header {
width: 100%;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: 10px;
}
}
</style>
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
......
......@@ -45,6 +45,7 @@ import * as FormApi from '@/api/bpm/form'
import FcDesigner from '@form-create/designer'
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useFormCreateDesigner } from '@/components/FormCreate'
defineOptions({ name: 'BpmFormEditor' })
......@@ -55,6 +56,7 @@ const { query } = useRoute() // 路由信息
const { delView } = useTagsViewStore() // 视图操作
const designer = ref() // 表单设计器
useFormCreateDesigner(designer) // 表单设计器增强
const dialogVisible = ref(false) // 弹窗是否展示
const formLoading = ref(false) // 表单的加载中:提交的按钮禁用
const formData = ref({
......
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
<ContentWrap>
<!-- 搜索工作栏 -->
......
......@@ -89,6 +89,16 @@ onMounted(async () => {
}
// 查询模型
const data = await ModelApi.getModel(modelId)
if (!data.bpmnXml) {
// 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的
data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
<process id="${data.key}" name="${data.name}" isExecutable="true" />
<bpmndi:BPMNDiagram id="BPMNDiagram">
<bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" />
</bpmndi:BPMNDiagram>
</definitions>`
}
model.value = {
...data,
bpmnXml: undefined // 清空 bpmnXml 属性
......
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
<doc-alert
title="流程设计器(钉钉、飞书)"
url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
/>
<doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
<doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
<ContentWrap>
<!-- 搜索工作栏 -->
......
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
<ContentWrap>
<!-- 搜索工作栏 -->
......@@ -83,7 +83,7 @@
<el-table-column align="center" label="申请编号" prop="id" />
<el-table-column align="center" label="状态" prop="result">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.result" />
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
......
<template>
<doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
......
<template>
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
<ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
<el-tabs tab-position="left" v-model="categoryActive">
......
......@@ -24,15 +24,15 @@
{{ processInstance?.startUser.nickname }}
<el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
</el-form-item>
<el-card class="mb-15px !-mt-10px" v-if="runningTasks[index].formId > 0">
<el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
<template #header>
<span class="el-icon-picture-outline">
填写表单【{{ runningTasks[index]?.formName }}
</span>
</template>
<form-create
v-model:api="approveFormFApis[index]"
v-model="approveForms[index].value"
v-model:api="approveFormFApis[index]"
:option="approveForms[index].option"
:rule="approveForms[index].rule"
/>
......@@ -92,8 +92,8 @@
<!-- 情况一:流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
<form-create
ref="fApi"
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
......@@ -280,9 +280,9 @@ const getProcessInstance = async () => {
data.formVariables
)
nextTick().then(() => {
fApi.value?.fapi?.btn.show(false)
fApi.value?.fapi?.resetBtn.show(false)
fApi.value?.fapi?.disabled(true)
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
})
} else {
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
......
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<ContentWrap>
<!-- 搜索工作栏 -->
......
<template>
<doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
......
<!-- 工作流 - 抄送我的流程 -->
<template>
<doc-alert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px">
......
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<doc-alert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<ContentWrap>
<!-- 搜索工作栏 -->
......
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<doc-alert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<ContentWrap>
<!-- 搜索工作栏 -->
......
......@@ -64,7 +64,7 @@ import BusinessDetailsHeader from './BusinessDetailsHeader.vue'
import BusinessDetailsInfo from './BusinessDetailsInfo.vue'
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
import { BizTypeEnum } from '@/api/crm/permission'
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog'
import BusinessForm from '@/views/crm/business/BusinessForm.vue'
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
......@@ -113,7 +113,7 @@ const transfer = () => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async (contactId: number) => {
if (!contactId) {
return
......
......@@ -5,35 +5,43 @@
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="商机名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入商机名称"
class="!w-240px"
clearable
placeholder="请输入商机名称"
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:business:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['crm:business:create']" type="primary" @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button
type="success"
v-hasPermi="['crm:business:export']"
:loading="exportLoading"
plain
type="success"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:business:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button>
</el-form-item>
</el-form>
......@@ -46,8 +54,8 @@
<el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column align="center" label="商机名称" fixed="left" prop="name" width="160">
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" fixed="left" label="商机名称" prop="name" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
......@@ -66,17 +74,17 @@
</template>
</el-table-column>
<el-table-column
label="商机金额(元)"
:formatter="erpPriceTableColumnFormatter"
align="center"
label="商机金额(元)"
prop="totalPrice"
width="140"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="预计成交日期"
:formatter="dateFormatter"
align="center"
label="预计成交日期"
prop="dealTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
......@@ -97,49 +105,49 @@
width="180px"
/>
<el-table-column
label="更新时间"
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="updateTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="创建时间"
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
<el-table-column
label="商机状态组"
align="center"
prop="statusTypeName"
fixed="right"
label="商机状态组"
prop="statusTypeName"
width="140"
/>
<el-table-column
label="商机阶段"
align="center"
prop="statusName"
fixed="right"
label="商机阶段"
prop="statusName"
width="120"
/>
<el-table-column label="操作" align="center" fixed="right" width="130px">
<el-table-column align="center" fixed="right" label="操作" width="130px">
<template #default="scope">
<el-button
v-hasPermi="['crm:business:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:business:update']"
>
编辑
</el-button>
<el-button
v-hasPermi="['crm:business:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:business:delete']"
>
删除
</el-button>
......@@ -148,9 +156,9 @@
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
......@@ -159,7 +167,7 @@
<BusinessForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as BusinessApi from '@/api/crm/business'
......@@ -216,7 +224,7 @@ const handleTabClick = (tab: TabsPaneContext) => {
}
/** 打开客户详情 */
const { currentRoute, push } = useRouter()
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmBusinessDetail', params: { id } })
}
......
......@@ -100,7 +100,7 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的组:create - 新增;update - 修改
const formData = ref({
id: 0,
id: undefined,
name: '',
deptIds: [],
statuses: []
......@@ -168,7 +168,7 @@ const submitForm = async () => {
const resetForm = () => {
checkStrictly.value = true
formData.value = {
id: 0,
id: undefined,
name: '',
deptIds: [],
statuses: []
......
......@@ -57,7 +57,7 @@ import PermissionList from '@/views/crm/permission/components/PermissionList.vue
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
import FollowUpList from '@/views/crm/followup/index.vue'
import { BizTypeEnum } from '@/api/crm/permission'
import type { OperateLogV2VO } from '@/api/system/operatelog'
import type { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog'
defineOptions({ name: 'CrmClueDetail' })
......@@ -103,7 +103,7 @@ const handleTransform = async () => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async () => {
const data = await getOperateLogPage({
bizType: BizTypeEnum.CRM_CLUE,
......
......@@ -49,7 +49,7 @@ import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vu
import BusinessList from '@/views/crm/business/components/BusinessList.vue' // 商机列表
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
import { BizTypeEnum } from '@/api/crm/permission'
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog'
import ContactForm from '@/views/crm/contact/ContactForm.vue'
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
......@@ -88,7 +88,7 @@ const transfer = () => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async (contactId: number) => {
if (!contactId) {
return
......
......@@ -52,7 +52,7 @@
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import * as ContractApi from '@/api/crm/contract'
import ContractDetailsInfo from './ContractDetailsInfo.vue'
import ContractDetailsHeader from './ContractDetailsHeader.vue'
......@@ -94,7 +94,7 @@ const getContractData = async () => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async (contractId: number) => {
if (!contractId) {
return
......
<!-- 客户导入窗口 -->
<template>
<Dialog v-model="dialogVisible" title="客户导入" width="400">
<div class="flex items-center my-10px">
<span class="mr-10px">负责人</span>
<el-select v-model="ownerUserId" class="!w-240px" clearable>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".xlsx, .xls"
action="none"
drag
......@@ -43,9 +51,10 @@
</template>
<script lang="ts" setup>
import * as CustomerApi from '@/api/crm/customer'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
import type { UploadUserFile } from 'element-plus'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'SystemUserImportForm' })
......@@ -54,15 +63,18 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const uploadRef = ref()
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref<UploadUserFile[]>([]) // 文件列表
const updateSupport = ref(false) // 是否更新已经存在的客户数据
const ownerUserId = ref<undefined | number>() // 负责人编号
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
/** 打开弹窗 */
const open = () => {
const open = async () => {
dialogVisible.value = true
fileList.value = []
resetForm()
await resetForm()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
ownerUserId.value = useUserStore().getUser.id
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
......@@ -72,17 +84,20 @@ const submitForm = async () => {
message.error('请上传文件')
return
}
// 提交请求
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
const formData = new FormData()
formData.append('updateSupport', updateSupport.value)
formData.append('file', fileList.value[0].raw)
// TODO @芋艿:后面是不是可以采用这种形式,去掉 uploadHeaders
await CustomerApi.handleImport(formData)
try {
const formData = new FormData()
formData.append('updateSupport', String(updateSupport.value))
formData.append('file', fileList.value[0].raw as Blob)
formData.append('ownerUserId', String(ownerUserId.value))
const res = await CustomerApi.handleImport(formData)
submitFormSuccess(res)
} catch {
submitFormError()
} finally {
formLoading.value = false
}
}
/** 文件上传成功 */
......@@ -108,6 +123,8 @@ const submitFormSuccess = (response: any) => {
text += '< ' + customerName + ': ' + data.failureCustomerNames[customerName] + ' >'
}
message.alert(text)
formLoading.value = false
dialogVisible.value = false
// 发送操作成功的事件
emits('success')
}
......@@ -119,9 +136,12 @@ const submitFormError = (): void => {
}
/** 重置表单 */
const resetForm = () => {
const resetForm = async () => {
// 重置上传状态和文件
formLoading.value = false
fileList.value = []
updateSupport.value = false
ownerUserId.value = undefined
await nextTick()
uploadRef.value?.clearFiles()
}
......
......@@ -93,7 +93,7 @@ import PermissionList from '@/views/crm/permission/components/PermissionList.vue
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
import FollowUpList from '@/views/crm/followup/index.vue'
import { BizTypeEnum } from '@/api/crm/permission'
import type { OperateLogV2VO } from '@/api/system/operatelog'
import type { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog'
import CustomerDistributeForm from '@/views/crm/customer/pool/CustomerDistributeForm.vue'
......@@ -185,7 +185,7 @@ const handlePutPool = async () => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async () => {
if (!customerId.value) {
return
......
......@@ -79,7 +79,7 @@
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery(undefined)">
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
......@@ -113,7 +113,7 @@
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="客户名称" fixed="left" prop="name" width="160">
<el-table-column align="center" fixed="left" label="客户名称" prop="name" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
......@@ -125,9 +125,9 @@
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column label="手机" align="center" prop="mobile" width="120" />
<el-table-column label="电话" align="center" prop="telephone" width="130" />
<el-table-column label="邮箱" align="center" prop="email" width="180" />
<el-table-column align="center" label="手机" prop="mobile" width="120" />
<el-table-column align="center" label="电话" prop="telephone" width="130" />
<el-table-column align="center" label="邮箱" prop="email" width="180" />
<el-table-column align="center" label="客户级别" prop="level" width="135">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
......@@ -164,7 +164,7 @@
width="180px"
/>
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" />
<el-table-column label="地址" align="center" prop="detailAddress" width="180" />
<el-table-column align="center" label="地址" prop="detailAddress" width="180" />
<el-table-column align="center" label="距离进入公海天数" prop="poolDay" width="140">
<template #default="scope"> {{ scope.row.poolDay }}</template>
</el-table-column>
......@@ -254,7 +254,7 @@ const activeName = ref('1') // 列表 tab
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
queryParams.sceneType = tab.paneName as string
handleQuery()
}
......
......@@ -19,7 +19,7 @@
</el-select>
</el-form-item>
<el-form-item label="老负责人">
<el-radio-group v-model="oldOwnerHandler" @change="formData.oldOwnerPermissionLevel">
<el-radio-group v-model="oldOwnerHandler" @change="handleOwnerChange">
<el-radio :label="false" size="large">移除</el-radio>
<el-radio :label="true" size="large">加入团队</el-radio>
</el-radio-group>
......@@ -86,10 +86,16 @@ const open = async (bizId: number) => {
dialogVisible.value = true
dialogTitle.value = getDialogTitle()
resetForm()
formData.value.bizId = bizId
formData.value.id = bizId
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 老负责人负责方式
const handleOwnerChange = (val: boolean) => {
if (!val) {
// 移除的话提交不带 oldOwnerPermissionLevel 参数
formData.value.oldOwnerPermissionLevel = undefined
}
}
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
......
......@@ -13,7 +13,7 @@
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import * as ProductApi from '@/api/crm/product'
import ProductDetailsHeader from '@/views/crm/product/detail/ProductDetailsHeader.vue'
import ProductDetailsInfo from '@/views/crm/product/detail/ProductDetailsInfo.vue'
......@@ -40,7 +40,7 @@ const getProductData = async (id: number) => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async (productId: number) => {
if (!productId) {
return
......
......@@ -34,7 +34,7 @@ import ReceivableDetailsHeader from './ReceivableDetailsHeader.vue'
import ReceivableDetailsInfo from './ReceivableDetailsInfo.vue'
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
import { BizTypeEnum } from '@/api/crm/permission'
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog'
import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
......@@ -66,7 +66,7 @@ const openForm = (type: string, id?: number) => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async (receivableId: number) => {
if (!receivableId) {
return
......
......@@ -37,7 +37,7 @@ import ReceivablePlanDetailsHeader from './ReceivablePlanDetailsHeader.vue'
import ReceivablePlanDetailsInfo from './ReceivablePlanDetailsInfo.vue'
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
import { BizTypeEnum } from '@/api/crm/permission'
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog'
import ReceivablePlanForm from '@/views/crm/receivable/plan/ReceivablePlanForm.vue'
......@@ -70,7 +70,7 @@ const openForm = (type: string, id?: number) => {
}
/** 获取操作日志 */
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
const getOperateLog = async (receivablePlanId: number) => {
if (!receivablePlanId) {
return
......
......@@ -10,11 +10,39 @@
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="客户名称" align="center" prop="customerName" min-width="200" />
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
<el-table-column
label="客户名称"
align="center"
prop="customerName"
min-width="200"
fixed="left"
/>
<el-table-column label="合同名称" align="center" prop="contractName" min-width="200" />
<el-table-column label="合同总金额" align="center" prop="totalPrice" min-width="200" />
<el-table-column label="回款金额" align="center" prop="receivablePrice" min-width="200" />
<el-table-column
label="合同总金额"
align="center"
prop="totalPrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="回款金额"
align="center"
prop="receivablePrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column align="center" label="客户来源" prop="source" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column label="负责人" align="center" prop="ownerUserName" min-width="200" />
<el-table-column label="创建人" align="center" prop="creatorUserName" min-width="200" />
<el-table-column
......@@ -28,8 +56,9 @@
label="下单日期"
align="center"
prop="orderDate"
:formatter="dateFormatter2"
:formatter="dateFormatter"
min-width="200"
fixed="right"
/>
</el-table>
</el-card>
......@@ -40,10 +69,12 @@ import {
CrmStatisticsCustomerSummaryByDateRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
import { round } from 'lodash-es'
import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
import { dateFormatter } from '@/utils/formatTime'
import { erpPriceTableColumnFormatter } from '@/utils'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ name: 'CustomerConversionStat' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
......@@ -53,7 +84,7 @@ const list = ref<CrmStatisticsCustomerSummaryByDateRespVO[]>([]) // 列表的数
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 40, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
......@@ -93,10 +124,9 @@ const echartsOption = reactive<EChartsOption>({
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
loading.value = true
const customerCount = await StatisticsCustomerApi.getCustomerSummaryByDate(props.queryParams)
const contractSummary = await StatisticsCustomerApi.getContractSummary(props.queryParams)
// 2.1 更新 Echarts 数据
......@@ -111,7 +141,7 @@ const loadData = async () => {
return {
name: item.time,
value: item.customerCreateCount
? round((item.customerDealCount / item.customerCreateCount) * 100, 2)
? ((item.customerDealCount / item.customerCreateCount) * 100).toFixed(2)
: 0
}
}
......@@ -119,8 +149,18 @@ const loadData = async () => {
}
// 2.2 更新列表数据
list.value = contractSummary
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
......
<!-- 成交周期分析 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="区域" align="center" prop="areaName" min-width="200" />
<el-table-column
label="成交周期(天)"
align="center"
prop="customerDealCycle"
min-width="200"
/>
<el-table-column label="成交客户数" align="center" prop="customerDealCount" min-width="200" />
</el-table>
</el-card>
</template>
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsCustomerDealCycleByAreaRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerDealCycleByArea' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticsCustomerDealCycleByAreaRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 40, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '成交周期(天)',
type: 'bar',
data: [],
yAxisIndex: 0
},
{
name: '成交客户数',
type: 'bar',
data: [],
yAxisIndex: 1
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '成交周期分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '成交周期(天)',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '成交客户数',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '区域',
data: []
}
}) as EChartsOption
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
const customerDealCycleByArea = (
await StatisticsCustomerApi.getCustomerDealCycleByArea(props.queryParams)
).map((s: CrmStatisticsCustomerDealCycleByAreaRespVO) => {
return {
areaName: s.areaName,
customerDealCycle: s.customerDealCycle,
customerDealCount: s.customerDealCount
}
})
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = customerDealCycleByArea.map(
(s: CrmStatisticsCustomerDealCycleByAreaRespVO) => s.areaName
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = customerDealCycleByArea.map(
(s: CrmStatisticsCustomerDealCycleByAreaRespVO) => s.customerDealCycle
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = customerDealCycleByArea.map(
(s: CrmStatisticsCustomerDealCycleByAreaRespVO) => s.customerDealCount
)
}
// 2.2 更新列表数据
list.value = customerDealCycleByArea
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 成交周期分析 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="产品名称" align="center" prop="productName" min-width="200" />
<el-table-column
label="成交周期(天)"
align="center"
prop="customerDealCycle"
min-width="200"
/>
<el-table-column label="成交客户数" align="center" prop="customerDealCount" min-width="200" />
</el-table>
</el-card>
</template>
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsCustomerDealCycleByProductRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerDealCycleByProduct' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticsCustomerDealCycleByProductRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 40, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '成交周期(天)',
type: 'bar',
data: [],
yAxisIndex: 0
},
{
name: '成交客户数',
type: 'bar',
data: [],
yAxisIndex: 1
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '成交周期分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '成交周期(天)',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '成交客户数',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '产品名称',
data: []
}
}) as EChartsOption
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
const customerDealCycleByProduct = (
await StatisticsCustomerApi.getCustomerDealCycleByProduct(props.queryParams)
).map((s: CrmStatisticsCustomerDealCycleByProductRespVO) => {
return {
productName: s.productName ?? '未知',
customerDealCycle: s.customerDealCount,
customerDealCount: s.customerDealCount
}
})
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = customerDealCycleByProduct.map(
(s: CrmStatisticsCustomerDealCycleByProductRespVO) => s.productName
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = customerDealCycleByProduct.map(
(s: CrmStatisticsCustomerDealCycleByProductRespVO) => s.customerDealCycle
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = customerDealCycleByProduct.map(
(s: CrmStatisticsCustomerDealCycleByProductRespVO) => s.customerDealCount
)
}
// 2.2 更新列表数据
list.value = customerDealCycleByProduct
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
......@@ -26,11 +26,12 @@
import {
StatisticsCustomerApi,
CrmStatisticsCustomerDealCycleByDateRespVO,
CrmStatisticsCustomerSummaryByDateRespVO,
CrmStatisticsCustomerSummaryByDateRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerDealCycle' })
defineOptions({ name: 'CustomerDealCycleByUser' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
......@@ -40,7 +41,7 @@ const list = ref<CrmStatisticsCustomerDealCycleByDateRespVO[]>([]) // 列表的
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 40, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
......@@ -49,12 +50,14 @@ const echartsOption = reactive<EChartsOption>({
{
name: '成交周期(天)',
type: 'bar',
data: []
data: [],
yAxisIndex: 0
},
{
name: '成交客户数',
type: 'bar',
data: []
data: [],
yAxisIndex: 1
}
],
toolbox: {
......@@ -74,10 +77,26 @@ const echartsOption = reactive<EChartsOption>({
type: 'shadow'
}
},
yAxis: {
type: 'value',
name: '数量(个)'
},
yAxis: [
{
type: 'value',
name: '成交周期(天)',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '成交客户数',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
......@@ -85,14 +104,13 @@ const echartsOption = reactive<EChartsOption>({
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
loading.value = true
const customerDealCycleByDate = await StatisticsCustomerApi.getCustomerDealCycleByDate(
props.queryParams
)
const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
props.queryParams
)
const customerDealCycleByUser = await StatisticsCustomerApi.getCustomerDealCycleByUser(
......@@ -116,7 +134,16 @@ const loadData = async () => {
}
// 2.2 更新列表数据
list.value = customerDealCycleByUser
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
......
......@@ -12,11 +12,11 @@
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="员工姓名" align="center" prop="ownerUserName" min-width="200" />
<el-table-column label="跟进次数" align="right" prop="followupRecordCount" min-width="200" />
<el-table-column label="跟进次数" align="right" prop="followUpRecordCount" min-width="200" />
<el-table-column
label="跟进客户数"
align="right"
prop="followupCustomerCount"
prop="followUpCustomerCount"
min-width="200"
/>
</el-table>
......@@ -25,22 +25,24 @@
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsFollowupSummaryByDateRespVO,
CrmStatisticsFollowupSummaryByUserRespVO
CrmStatisticsFollowUpSummaryByDateRespVO,
CrmStatisticsFollowUpSummaryByUserRespVO
} from '@/api/crm/statistics/customer'
import Echart from '@/components/Echart/src/Echart.vue'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerFollowupSummary' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticsFollowupSummaryByUserRespVO[]>([]) // 列表的数据
const list = ref<CrmStatisticsFollowUpSummaryByUserRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 30, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
......@@ -49,11 +51,13 @@ const echartsOption = reactive<EChartsOption>({
{
name: '跟进客户数',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '跟进次数',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
......@@ -74,46 +78,74 @@ const echartsOption = reactive<EChartsOption>({
type: 'shadow'
}
},
yAxis: {
type: 'value',
name: '数量(个)'
},
yAxis: [
{
type: 'value',
name: '跟进客户数',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '跟进次数',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
axisTick: {
alignWithLabel: true
},
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
loading.value = true
const followupSummaryByDate = await StatisticsCustomerApi.getFollowupSummaryByDate(
const followUpSummaryByDate = await StatisticsCustomerApi.getFollowUpSummaryByDate(
props.queryParams
)
const followupSummaryByUser = await StatisticsCustomerApi.getFollowupSummaryByUser(
const followUpSummaryByUser = await StatisticsCustomerApi.getFollowUpSummaryByUser(
props.queryParams
)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = followupSummaryByDate.map(
(s: CrmStatisticsFollowupSummaryByDateRespVO) => s.time
echartsOption.xAxis['data'] = followUpSummaryByDate.map(
(s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.time
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = followupSummaryByDate.map(
(s: CrmStatisticsFollowupSummaryByDateRespVO) => s.followupCustomerCount
echartsOption.series[0]['data'] = followUpSummaryByDate.map(
(s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.followUpCustomerCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = followupSummaryByDate.map(
(s: CrmStatisticsFollowupSummaryByDateRespVO) => s.followupRecordCount
echartsOption.series[1]['data'] = followUpSummaryByDate.map(
(s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.followUpRecordCount
)
}
// 2.2 更新列表数据
list.value = followupSummaryByUser
loading.value = false
list.value = followUpSummaryByUser
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
......
......@@ -11,8 +11,12 @@
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="跟进方式" align="center" prop="followupType" min-width="200" />
<el-table-column label="个数" align="center" prop="followupRecordCount" min-width="200" />
<el-table-column label="跟进方式" align="center" prop="followUpType" min-width="200">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_FOLLOW_UP_TYPE" :value="scope.row.followUpType" />
</template>
</el-table-column>
<el-table-column label="个数" align="center" prop="followUpRecordCount" min-width="200" />
<el-table-column label="占比(%)" align="center" prop="portion" min-width="200" />
</el-table>
</el-card>
......@@ -20,16 +24,19 @@
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsFollowupSummaryByTypeRespVO
CrmStatisticsFollowUpSummaryByTypeRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
import { round, sumBy } from 'lodash-es'
import { sumBy } from 'lodash-es'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { erpCalculatePercentage } from '@/utils'
defineOptions({ name: 'CustomerFollowupType' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticsFollowupSummaryByTypeRespVO[]>([]) // 列表的数据
const list = ref<CrmStatisticsFollowUpSummaryByTypeRespVO[]>([]) // 列表的数据
/** 饼图配置 */
const echartsOption = reactive<EChartsOption>({
......@@ -67,35 +74,43 @@ const echartsOption = reactive<EChartsOption>({
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
loading.value = true
const followupSummaryByType = await StatisticsCustomerApi.getFollowupSummaryByType(
const followUpSummaryByType = await StatisticsCustomerApi.getFollowUpSummaryByType(
props.queryParams
)
// 2.1 更新 Echarts 数据
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = followupSummaryByType.map(
(r: CrmStatisticsFollowupSummaryByTypeRespVO) => {
echartsOption.series[0]['data'] = followUpSummaryByType.map(
(row: CrmStatisticsFollowUpSummaryByTypeRespVO) => {
return {
name: r.followupType,
value: r.followupRecordCount
name: getDictLabel(DICT_TYPE.CRM_FOLLOW_UP_TYPE, row.followUpType),
value: row.followUpRecordCount
}
}
)
}
// 2.2 更新列表数据
const totalCount = sumBy(followupSummaryByType, 'followupRecordCount')
list.value = followupSummaryByType.map((r: CrmStatisticsFollowupSummaryByTypeRespVO) => {
const totalCount = sumBy(followUpSummaryByType, 'followUpRecordCount')
list.value = followUpSummaryByType.map((row: CrmStatisticsFollowUpSummaryByTypeRespVO) => {
return {
followupType: r.followupType,
followupRecordCount: r.followupRecordCount,
portion: round((r.followupRecordCount / totalCount) * 100, 2)
...row,
portion: erpCalculatePercentage(row.followUpRecordCount, totalCount)
}
})
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
......
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
<el-table-column label="员工姓名" prop="ownerUserName" min-width="100" fixed="left" />
<el-table-column
label="进入公海客户数"
align="right"
prop="customerPutCount"
min-width="200"
/>
<el-table-column
label="公海领取客户数"
align="right"
prop="customerTakeCount"
min-width="200"
/>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsPoolSummaryByDateRespVO,
CrmStatisticsPoolSummaryByUserRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerPoolSummary' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticsPoolSummaryByUserRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 40, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '进入公海客户数',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '公海领取客户数',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '公海客户分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '进入公海客户数',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '公海领取客户数',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
const poolSummaryByDate = await StatisticsCustomerApi.getPoolSummaryByDate(props.queryParams)
const poolSummaryByUser = await StatisticsCustomerApi.getPoolSummaryByUser(props.queryParams)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = poolSummaryByDate.map(
(s: CrmStatisticsPoolSummaryByDateRespVO) => s.time
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = poolSummaryByDate.map(
(s: CrmStatisticsPoolSummaryByDateRespVO) => s.customerPutCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = poolSummaryByDate.map(
(s: CrmStatisticsPoolSummaryByDateRespVO) => s.customerTakeCount
)
}
// 2.2 更新列表数据
list.value = poolSummaryByUser
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
......@@ -10,8 +10,8 @@
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="员工姓名" prop="ownerUserName" min-width="100" />
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
<el-table-column label="员工姓名" prop="ownerUserName" min-width="100" fixed="left" />
<el-table-column
label="新增客户数"
align="right"
......@@ -21,28 +21,31 @@
<el-table-column label="成交客户数" align="right" prop="customerDealCount" min-width="200" />
<el-table-column label="客户成交率(%)" align="right" min-width="200">
<template #default="scope">
{{
scope.row.customerCreateCount !== 0
? round((scope.row.customerDealCount / scope.row.customerCreateCount) * 100, 2)
: 0
}}
{{ erpCalculatePercentage(scope.row.customerDealCount, scope.row.customerCreateCount) }}
</template>
</el-table-column>
<el-table-column label="合同总金额" align="right" prop="contractPrice" min-width="200" />
<el-table-column label="回款金额" align="right" prop="receivablePrice" min-width="200" />
<el-table-column
label="合同总金额"
align="right"
prop="contractPrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="回款金额"
align="right"
prop="receivablePrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column label="未回款金额" align="right" min-width="200">
<!-- TODO @dhb52:参考 util/index.ts 的 // ========== ERP 专属方法 ========== 部分,搞个两个方法,一个格式化百分比,一个计算百分比 -->
<template #default="scope">
{{ round(scope.row.contractPrice - scope.row.receivablePrice, 2) }}
{{ erpCalculatePercentage(scope.row.receivablePrice, scope.row.contractPrice) }}
</template>
</el-table-column>
<el-table-column label="回款完成率(%)" align="right" min-width="200">
<el-table-column label="回款完成率(%)" align="right" min-width="200" fixed="right">
<template #default="scope">
{{
scope.row.contractPrice !== 0
? round((scope.row.receivablePrice / scope.row.contractPrice) * 100, 2)
: 0
}}
{{ erpCalculatePercentage(scope.row.receivablePrice, scope.row.contractPrice) }}
</template>
</el-table-column>
</el-table>
......@@ -55,9 +58,10 @@ import {
CrmStatisticsCustomerSummaryByUserRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
import { round } from 'lodash-es'
import { erpCalculatePercentage, erpPriceTableColumnFormatter } from '@/utils'
defineOptions({ name: 'CustomerSummary' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
......@@ -67,7 +71,7 @@ const list = ref<CrmStatisticsCustomerSummaryByUserRespVO[]>([]) // 列表的数
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 30, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
......@@ -76,11 +80,13 @@ const echartsOption = reactive<EChartsOption>({
{
name: '新增客户数',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '成交客户数',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
......@@ -101,10 +107,26 @@ const echartsOption = reactive<EChartsOption>({
type: 'shadow'
}
},
yAxis: {
type: 'value',
name: '数量(个)'
},
yAxis: [
{
type: 'value',
name: '新增客户数',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '成交客户数',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
......@@ -112,10 +134,9 @@ const echartsOption = reactive<EChartsOption>({
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
loading.value = true
const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
props.queryParams
)
......@@ -138,10 +159,21 @@ const loadData = async () => {
(s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerDealCount
)
}
// 2.2 更新列表数据
list.value = customerSummaryByUser
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
......
......@@ -3,49 +3,77 @@
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="时间范围" prop="orderDate">
<el-date-picker
v-model="queryParams.times"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="defaultShortcuts"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="时间间隔" prop="interval">
<el-select
v-model="queryParams.interval"
class="!w-240px"
placeholder="间隔类型"
@change="handleQuery"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
class="!w-240px"
:data="deptList"
:props="defaultProps"
check-strictly
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="queryParams.userId = undefined"
@change="(queryParams.userId = undefined), handleQuery()"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" placeholder="员工" clearable>
<el-select
v-model="queryParams.userId"
class="!w-240px"
clearable
placeholder="员工"
@change="handleQuery"
>
<el-option
v-for="(user, index) in userListByDeptId"
:key="index"
:label="user.nickname"
:value="user.id"
:key="index"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 搜索 </el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
查询
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
......@@ -54,24 +82,34 @@
<el-col>
<el-tabs v-model="activeTab">
<!-- 客户总量分析 -->
<el-tab-pane label="客户总量分析" name="customerSummary" lazy>
<CustomerSummary :query-params="queryParams" ref="customerSummaryRef" />
<el-tab-pane label="客户总量分析" lazy name="customerSummary">
<CustomerSummary ref="customerSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户跟进次数分析 -->
<el-tab-pane label="客户跟进次数分析" name="followupSummary" lazy>
<CustomerFollowupSummary :query-params="queryParams" ref="followupSummaryRef" />
<el-tab-pane label="客户跟进次数分析" lazy name="followUpSummary">
<CustomerFollowUpSummary ref="followUpSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户跟进方式分析 -->
<el-tab-pane label="客户跟进方式分析" name="followupType" lazy>
<CustomerFollowupType :query-params="queryParams" ref="followupTypeRef" />
<el-tab-pane label="客户跟进方式分析" lazy name="followUpType">
<CustomerFollowUpType ref="followUpTypeRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户转化率分析 -->
<el-tab-pane label="客户转化率分析" name="conversionStat" lazy>
<CustomerConversionStat :query-params="queryParams" ref="conversionStatRef" />
<el-tab-pane label="客户转化率分析" lazy name="conversionStat">
<CustomerConversionStat ref="conversionStatRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 公海客户分析 -->
<el-tab-pane label="公海客户分析" lazy name="poolSummary">
<CustomerPoolSummary ref="customerPoolSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 成交周期分析 -->
<el-tab-pane label="成交周期分析" name="dealCycle" lazy>
<CustomerDealCycle :query-params="queryParams" ref="dealCycleRef" />
<el-tab-pane label="员工客户成交周期分析" lazy name="dealCycleByUser">
<CustomerDealCycleByUser ref="dealCycleByUserRef" :query-params="queryParams" />
</el-tab-pane>
<el-tab-pane label="地区客户成交周期分析" lazy name="dealCycleByArea">
<CustomerDealCycleByArea ref="dealCycleByAreaRef" :query-params="queryParams" />
</el-tab-pane>
<el-tab-pane label="产品客户成交周期分析" lazy name="dealCycleByProduct">
<CustomerDealCycleByProduct ref="dealCycleByProductRef" :query-params="queryParams" />
</el-tab-pane>
</el-tabs>
</el-col>
......@@ -81,17 +119,22 @@
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import CustomerSummary from './components/CustomerSummary.vue'
import CustomerFollowupSummary from './components/CustomerFollowupSummary.vue'
import CustomerFollowupType from './components/CustomerFollowupType.vue'
import CustomerConversionStat from './components/CustomerConversionStat.vue'
import CustomerDealCycle from './components/CustomerDealCycle.vue'
import CustomerDealCycleByUser from './components/CustomerDealCycleByUser.vue'
import CustomerDealCycleByArea from './components/CustomerDealCycleByArea.vue'
import CustomerDealCycleByProduct from './components/CustomerDealCycleByProduct.vue'
import CustomerFollowUpSummary from './components/CustomerFollowUpSummary.vue'
import CustomerFollowUpType from './components/CustomerFollowUpType.vue'
import CustomerSummary from './components/CustomerSummary.vue'
import CustomerPoolSummary from './components/CustomerPoolSummary.vue'
defineOptions({ name: 'CrmStatisticsCustomer' })
const queryParams = reactive({
interval: 2, // WEEK, 周
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
......@@ -104,50 +147,55 @@ const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单
const deptList = ref<Tree[]>([]) // 部门树形结构
const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
// 根据选择的部门筛选员工清单
/** 根据选择的部门筛选员工清单 */
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
// 活跃标签
const activeTab = ref('customerSummary')
// 1.客户总量分析
const customerSummaryRef = ref()
// 2.客户跟进次数分析
const followupSummaryRef = ref()
// 3.客户跟进方式分析
const followupTypeRef = ref()
// 4.客户转化率分析
const conversionStatRef = ref()
// 5.公海客户分析
// 缺 crm_owner_record 表
// 6.成交周期分析
const dealCycleRef = ref()
const activeTab = ref('customerSummary') // 活跃标签
const customerSummaryRef = ref() // 1. 客户总量分析
const followUpSummaryRef = ref() // 2. 客户跟进次数分析
const followUpTypeRef = ref() // 3. 客户跟进方式分析
const conversionStatRef = ref() // 4. 客户转化率分析
const customerPoolSummaryRef = ref() // 5. 客户公海分析
const dealCycleByUserRef = ref() // 6. 成交周期分析(按员工)
const dealCycleByAreaRef = ref() // 7. 成交周期分析(按地区)
const dealCycleByProductRef = ref() // 8. 成交周期分析(按产品)
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'customerSummary':
case 'customerSummary': // 客户总量分析
customerSummaryRef.value?.loadData?.()
break
case 'followupSummary':
followupSummaryRef.value?.loadData?.()
case 'followUpSummary': // 客户跟进次数分析
followUpSummaryRef.value?.loadData?.()
break
case 'followupType':
followupTypeRef.value?.loadData?.()
case 'followUpType': // 客户跟进方式分析
followUpTypeRef.value?.loadData?.()
break
case 'conversionStat':
case 'conversionStat': // 客户转化率分析
conversionStatRef.value?.loadData?.()
break
case 'dealCycle':
dealCycleRef.value?.loadData?.()
case 'poolSummary': // 公海客户分析
customerPoolSummaryRef.value?.loadData?.()
break
case 'dealCycleByUser': // 成交周期分析
dealCycleByUserRef.value?.loadData?.()
break
case 'dealCycleByArea': // 成交周期分析
dealCycleByAreaRef.value?.loadData?.()
break
case 'dealCycleByProduct': // 成交周期分析
dealCycleByProductRef.value?.loadData?.()
break
}
}
// 当 activeTab 改变时,刷新当前活动的 tab
/** 当 activeTab 改变时,刷新当前活动的 tab */
watch(activeTab, () => {
handleQuery()
})
......@@ -158,7 +206,7 @@ const resetQuery = () => {
handleQuery()
}
// 加载部门树
/** 初始化 */
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())
......
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list">
<el-table-column align="center" fixed="left" label="序号" type="index" width="80" />
<el-table-column align="center" fixed="left" label="商机名称" prop="name" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column align="center" fixed="left" label="客户名称" prop="customerName" width="120">
<template #default="scope">
<el-link
:underline="false"
type="primary"
@click="openCustomerDetail(scope.row.customerId)"
>
{{ scope.row.customerName }}
</el-link>
</template>
</el-table-column>
<el-table-column
:formatter="erpPriceTableColumnFormatter"
align="center"
label="商机金额(元)"
prop="totalPrice"
width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="预计成交日期"
prop="dealTime"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="最后跟进时间"
prop="contactLastTime"
width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="updateTime"
width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
<el-table-column
align="center"
fixed="right"
label="商机状态组"
prop="statusTypeName"
width="140"
/>
<el-table-column
align="center"
fixed="right"
label="商机阶段"
prop="statusName"
width="120"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams0.pageSize"
v-model:page="queryParams0.pageNo"
:total="total"
@pagination="getList"
/>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticsBusinessInversionRateSummaryByDateRespVO,
StatisticFunnelApi
} from '@/api/crm/statistics/funnel'
import { EChartsOption } from 'echarts'
import { erpCalculatePercentage, erpPriceTableColumnFormatter } from '@/utils'
import { dateFormatter } from '@/utils/formatTime'
defineOptions({ name: 'BusinessSummary' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const queryParams0 = reactive({
pageNo: 1,
pageSize: 10
})
const loading = ref(false) // 加载中
const list = ref([]) // 列表的数据
const total = ref(0)
/** 将传进来的值赋值给 queryParams0 */
watch(
() => props.queryParams,
(data) => {
if (!data) {
return
}
const newObj = { ...queryParams0, ...data }
Object.assign(queryParams0, newObj)
},
{
immediate: true
}
)
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
color: ['#6ca2ff', '#6ac9d7', '#ff7474'],
tooltip: {
trigger: 'axis',
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
legend: {
data: ['赢单转化率', '商机总数', '赢单商机数'],
bottom: '0px',
itemWidth: 14
},
grid: {
top: '40px',
left: '40px',
right: '40px',
bottom: '40px',
containLabel: true,
borderColor: '#fff'
},
xAxis: [
{
type: 'category',
data: [],
axisTick: {
alignWithLabel: true,
lineStyle: { width: 0 }
},
axisLabel: {
color: '#BDBDBD'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: { color: '#BDBDBD' }
},
splitLine: {
show: false
}
}
],
yAxis: [
{
type: 'value',
name: '赢单转化率',
axisTick: {
alignWithLabel: true,
lineStyle: { width: 0 }
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: { color: '#BDBDBD' }
},
splitLine: {
show: false
}
},
{
type: 'value',
name: '商机数',
axisTick: {
alignWithLabel: true,
lineStyle: { width: 0 }
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}个'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: { color: '#BDBDBD' }
},
splitLine: {
show: false
}
}
],
series: [
{
name: '赢单转化率',
type: 'line',
yAxisIndex: 0,
data: []
},
{
name: '商机总数',
type: 'bar',
yAxisIndex: 1,
barWidth: 15,
data: []
},
{
name: '赢单商机数',
type: 'bar',
yAxisIndex: 1,
barWidth: 15,
data: []
}
]
}) as EChartsOption
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
const businessSummaryByDate = await StatisticFunnelApi.getBusinessInversionRateSummaryByDate(
props.queryParams
)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis[0] && echartsOption.xAxis[0]['data']) {
echartsOption.xAxis[0]['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) => s.time
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) =>
erpCalculatePercentage(s.businessWinCount, s.businessCount)
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) => s.businessCount
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
echartsOption.series[2]['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) => s.businessWinCount
)
}
// 2.2 更新列表数据
await getList()
}
/** 获取商机列表 */
const getList = async () => {
const data = await StatisticFunnelApi.getBusinessPageByDate(props.queryParams)
list.value = data.list
total.value = data.total
}
/** 打开客户详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmBusinessDetail', params: { id } })
}
/** 打开客户详情 */
const openCustomerDetail = (id: number) => {
push({ name: 'CrmCustomerDetail', params: { id } })
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list">
<el-table-column align="center" fixed="left" label="序号" type="index" width="80" />
<el-table-column align="center" fixed="left" label="商机名称" prop="name" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column align="center" fixed="left" label="客户名称" prop="customerName" width="120">
<template #default="scope">
<el-link
:underline="false"
type="primary"
@click="openCustomerDetail(scope.row.customerId)"
>
{{ scope.row.customerName }}
</el-link>
</template>
</el-table-column>
<el-table-column
:formatter="erpPriceTableColumnFormatter"
align="center"
label="商机金额(元)"
prop="totalPrice"
width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="预计成交日期"
prop="dealTime"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="最后跟进时间"
prop="contactLastTime"
width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="updateTime"
width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
<el-table-column
align="center"
fixed="right"
label="商机状态组"
prop="statusTypeName"
width="140"
/>
<el-table-column
align="center"
fixed="right"
label="商机阶段"
prop="statusName"
width="120"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams0.pageSize"
v-model:page="queryParams0.pageNo"
:total="total"
@pagination="getList"
/>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticsBusinessSummaryByDateRespVO,
StatisticFunnelApi
} from '@/api/crm/statistics/funnel'
import { EChartsOption } from 'echarts'
import { erpPriceTableColumnFormatter } from '@/utils'
import { dateFormatter } from '@/utils/formatTime'
defineOptions({ name: 'BusinessSummary' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const queryParams0 = reactive({
pageNo: 1,
pageSize: 10
})
const loading = ref(false) // 加载中
const list = ref([]) // 列表的数据
const total = ref(0)
/** 将传进来的值赋值给 queryParams0 */
watch(
() => props.queryParams,
(data) => {
if (!data) {
return
}
const newObj = { ...queryParams0, ...data }
Object.assign(queryParams0, newObj)
},
{
immediate: true
}
)
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 30,
right: 30, // 让 X 轴右侧显示完整
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '新增商机数量',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '新增商机金额',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '新增商机分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '新增商机数量',
min: 0,
minInterval: 1 // 显示整数刻度
},
{
type: 'value',
name: '新增商机金额',
min: 0,
minInterval: 1, // 显示整数刻度
splitLine: {
lineStyle: {
type: 'dotted', // 右侧网格线虚化, 减少混乱
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1. 加载统计数据
const businessSummaryByDate = await StatisticFunnelApi.getBusinessSummaryByDate(props.queryParams)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessSummaryByDateRespVO) => s.time
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessSummaryByDateRespVO) => s.businessCreateCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = businessSummaryByDate.map(
(s: CrmStatisticsBusinessSummaryByDateRespVO) => s.totalPrice
)
}
// 2.2 更新列表数据
await getList()
}
/** 获取商机列表 */
const getList = async () => {
const data = await StatisticFunnelApi.getBusinessPageByDate(props.queryParams)
list.value = data.list
total.value = data.total
}
/** 打开客户详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'CrmBusinessDetail', params: { id } })
}
/** 打开客户详情 */
const openCustomerDetail = (id: number) => {
push({ name: 'CrmCustomerDetail', params: { id } })
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 销售漏斗分析 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-row>
<el-col :span="24">
<el-button-group class="mb-10px">
<el-button type="primary" @click="handleActive(true)">客户视角</el-button>
<el-button type="primary" @click="handleActive(false)">动态视角</el-button>
</el-button-group>
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-col>
</el-row>
</el-card>
<!-- 统计列表 -->
<el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="阶段" prop="endStatus" width="200">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE" :value="scope.row.endStatus" />
</template>
</el-table-column>
<el-table-column align="center" label="商机数" min-width="200" prop="businessCount" />
<el-table-column align="center" label="商机总金额(元)" min-width="200" prop="totalPrice" />
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import { CrmStatisticFunnelRespVO, StatisticFunnelApi } from '@/api/crm/statistics/funnel'
import { EChartsOption } from 'echarts'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ name: 'FunnelBusiness' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const active = ref(true)
const loading = ref(false) // 加载中
const list = ref<CrmStatisticFunnelRespVO[]>([]) // 列表的数据
/** 销售漏斗 */
const echartsOption = reactive<EChartsOption>({
title: {
text: '销售漏斗'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}'
},
toolbox: {
feature: {
dataView: { readOnly: false },
restore: {},
saveAsImage: {}
}
},
legend: {
data: ['客户', '商机', '赢单']
},
series: [
{
name: '销售漏斗',
type: 'funnel',
left: '10%',
top: 60,
bottom: 60,
width: '80%',
min: 0,
max: 100,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside'
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid'
}
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
label: {
fontSize: 20
}
},
data: [
{ value: 60, name: '客户-0' },
{ value: 40, name: '商机-0' },
{ value: 20, name: '赢单-0' }
]
}
]
}) as EChartsOption
const handleActive = async (val: boolean) => {
active.value = val
await loadData()
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
// 1. 加载漏斗数据
const data = (await StatisticFunnelApi.getFunnelSummary(
props.queryParams
)) as CrmStatisticFunnelRespVO
// 2.1 更新 Echarts 数据
if (
!!data &&
echartsOption.series &&
echartsOption.series[0] &&
echartsOption.series[0]['data']
) {
// tips:写死 value 值是为了保持漏斗顺序不变
const list: { value: number; name: string }[] = []
if (active.value) {
list.push({ value: 60, name: `客户-${data.customerCount || 0}个` })
list.push({ value: 40, name: `商机-${data.businessCount || 0}个` })
list.push({ value: 20, name: `赢单-${data.businessWinCount || 0}个` })
} else {
list.push({ value: data.customerCount || 0, name: `客户-${data.customerCount || 0}个` })
list.push({ value: data.businessCount || 0, name: `商机-${data.businessCount || 0}个` })
list.push({ value: data.businessWinCount || 0, name: `赢单-${data.businessWinCount || 0}个` })
}
echartsOption.series[0]['data'] = list
}
// 2.2 获取商机结束状态统计
list.value = await StatisticFunnelApi.getBusinessSummaryByEndStatus(props.queryParams)
loading.value = false
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 数据统计 - 客户画像 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="时间范围" prop="orderDate">
<el-date-picker
v-model="queryParams.times"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="defaultShortcuts"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="时间间隔" prop="interval">
<el-select
v-model="queryParams.interval"
class="!w-240px"
placeholder="间隔类型"
@change="handleQuery"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
:data="deptList"
:props="defaultProps"
check-strictly
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="(queryParams.userId = undefined), handleQuery()"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select
v-model="queryParams.userId"
class="!w-240px"
clearable
placeholder="员工"
@change="handleQuery"
>
<el-option
v-for="(user, index) in userListByDeptId"
:key="index"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
查询
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 客户统计 -->
<el-col>
<el-tabs v-model="activeTab">
<el-tab-pane label="销售漏斗分析" lazy name="funnelRef">
<FunnelBusiness ref="funnelRef" :query-params="queryParams" />
</el-tab-pane>
<el-tab-pane label="新增商机分析" lazy name="businessSummaryRef">
<BusinessSummary ref="businessSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<el-tab-pane label="商机转化率分析" lazy name="businessInversionRateSummaryRef">
<BusinessInversionRateSummary
ref="businessInversionRateSummaryRef"
:query-params="queryParams"
/>
</el-tab-pane>
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import FunnelBusiness from './components/FunnelBusiness.vue'
import BusinessSummary from './components/BusinessSummary.vue'
import BusinessInversionRateSummary from './components/BusinessInversionRateSummary.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
defineOptions({ name: 'CrmStatisticsFunnel' })
const queryParams = reactive({
interval: 2, // WEEK, 周
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
// 默认显示最近一周的数据
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
]
})
const queryFormRef = ref() // 搜索的表单
const deptList = ref<Tree[]>([]) // 部门树形结构
const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
/** 根据选择的部门筛选员工清单 */
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
const activeTab = ref('funnelRef') // 活跃标签
const funnelRef = ref() // 销售漏斗
const businessSummaryRef = ref() // 新增商机分析
const businessInversionRateSummaryRef = ref() // 商机转化率分析
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'funnelRef':
funnelRef.value?.loadData?.()
break
case 'businessSummaryRef':
businessSummaryRef.value?.loadData?.()
break
case 'businessInversionRateSummaryRef':
businessInversionRateSummaryRef.value?.loadData?.()
break
}
}
/** 当 activeTab 改变时,刷新当前活动的 tab */
watch(activeTab, () => {
handleQuery()
})
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 初始化 */
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())
})
</script>
<!-- 员工业绩统计 -->
<!-- TODO @scholar:参考 ReceivablePricePerformance 建议改 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="tableData">
<el-table-column
v-for="item in columnsData"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import {
StatisticsPerformanceApi,
StatisticsPerformanceRespVO
} from '@/api/crm/statistics/performance'
defineOptions({ name: 'ContractCountPerformance' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<StatisticsPerformanceRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '当月合同数量(个)',
type: 'line',
data: []
},
{
name: '上月合同数量(个)',
type: 'line',
data: []
},
{
name: '去年同月合同数量(个)',
type: 'line',
data: []
},
{
name: '同比增长率(%)',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '环比增长率(%)',
type: 'line',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '客户总量分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '数量(个)',
axisTick: {
show: false
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
},
{
type: 'value',
name: '',
axisTick: {
alignWithLabel: true,
lineStyle: {
width: 0
}
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const performanceList = await StatisticsPerformanceApi.getContractCountPerformance(
props.queryParams
)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.currentMonthCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
echartsOption.series[2]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
)
}
// 2.2 更新列表数据
list.value = performanceList
convertListData()
loading.value = false
}
// 初始化数据
const columnsData = reactive([])
const tableData = reactive([
{ title: '当月合同数量统计(个)' },
{ title: '上月合同数量统计(个)' },
{ title: '去年当月合同数量统计(个)' },
{ title: '同比增长率(%)' },
{ title: '环比增长率(%)' }
])
// 定义 convertListData 方法,数据行列转置,展示每月数据
const convertListData = () => {
const columnObj = { label: '日期', prop: 'title' }
columnsData.splice(0, columnsData.length) //清空数组
columnsData.push(columnObj)
list.value.forEach((item, index) => {
const columnObj = { label: item.time, prop: 'prop' + index }
columnsData.push(columnObj)
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
tableData[4]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
})
}
defineExpose({ loadData })
/** 初始化 */
onMounted(async () => {
await loadData()
})
</script>
<!-- 员工业绩统计 -->
<!-- TODO @scholar:参考 ReceivablePricePerformance 建议改 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="tableData">
<el-table-column
v-for="item in columnsData"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import {
StatisticsPerformanceApi,
StatisticsPerformanceRespVO
} from '@/api/crm/statistics/performance'
defineOptions({ name: 'ContractPricePerformance' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<StatisticsPerformanceRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '当月合同金额(元)',
type: 'line',
data: []
},
{
name: '上月合同金额(元)',
type: 'line',
data: []
},
{
name: '去年同月合同金额(元)',
type: 'line',
data: []
},
{
name: '同比增长率(%)',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '环比增长率(%)',
type: 'line',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '客户总量分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '金额(元)',
axisTick: {
show: false
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
},
{
type: 'value',
name: '',
axisTick: {
alignWithLabel: true,
lineStyle: {
width: 0
}
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const performanceList = await StatisticsPerformanceApi.getContractPricePerformance(
props.queryParams
)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.currentMonthCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
echartsOption.series[2]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
)
}
// 2.2 更新列表数据
list.value = performanceList
convertListData()
loading.value = false
}
// 初始化数据
const columnsData = reactive([])
const tableData = reactive([
{ title: '当月合同金额统计(元)' },
{ title: '上月合同金额统计(元)' },
{ title: '去年当月合同金额统计(元)' },
{ title: '同比增长率(%)' },
{ title: '环比增长率(%)' }
])
// 定义 init 方法
const convertListData = () => {
const columnObj = { label: '日期', prop: 'title' }
columnsData.splice(0, columnsData.length) //清空数组
columnsData.push(columnObj)
list.value.forEach((item, index) => {
const columnObj = { label: item.time, prop: 'prop' + index }
columnsData.push(columnObj)
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
tableData[4]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
})
}
defineExpose({ loadData })
/** 初始化 */
onMounted(async () => {
await loadData()
})
</script>
<!-- 员工业绩统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="tableData">
<el-table-column
v-for="item in columnsData"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<!-- TODO @scholar:IDEA 爆红的处理 -->
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import {
StatisticsPerformanceApi,
StatisticsPerformanceRespVO
} from '@/api/crm/statistics/performance'
defineOptions({ name: 'ContractPricePerformance' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<StatisticsPerformanceRespVO[]>([]) // 列表的数据
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '当月回款金额(元)',
type: 'line',
data: []
},
{
name: '上月回款金额(元)',
type: 'line',
data: []
},
{
name: '去年同月回款金额(元)',
type: 'line',
data: []
},
{
name: '同比增长率(%)',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '环比增长率(%)',
type: 'line',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // 数据区域缩放:Y 轴不缩放
},
brush: {
type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
},
saveAsImage: { show: true, name: '客户总量分析' } // 保存为图片
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '金额(元)',
axisTick: {
show: false
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
},
{
type: 'value',
name: '',
axisTick: {
// TODO @scholar:IDEA 爆红的处理
alignWithLabel: true,
lineStyle: {
width: 0
}
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const performanceList = await StatisticsPerformanceApi.getReceivablePricePerformance(
props.queryParams
)
// 2.1 更新 Echarts 数据
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.currentMonthCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[1]['data']) {
echartsOption.series[2]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
)
}
// 2.2 更新列表数据
list.value = performanceList
convertListData()
loading.value = false
}
// 初始化数据
// TODO @scholar:加个 as any[],避免 idea 爆红
const columnsData = reactive([] as any[])
const tableData = reactive([
{ title: '当月回款金额统计(元)' },
{ title: '上月回款金额统计(元)' },
{ title: '去年当月回款金额统计(元)' },
{ title: '同比增长率(%)' },
{ title: '环比增长率(%)' }
])
// 定义 init 方法
const convertListData = () => {
const columnObj = { label: '日期', prop: 'title' }
columnsData.splice(0, columnsData.length) //清空数组
columnsData.push(columnObj)
list.value.forEach((item, index) => {
const columnObj = { label: item.time, prop: 'prop' + index }
columnsData.push(columnObj)
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
// TODO @scholar:百分比,使用 erpCalculatePercentage 直接计算;如果是 0,则返回 0,统一就好哈;
tableData[3]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
tableData[4]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
})
}
defineExpose({ loadData })
/** 初始化 */
onMounted(async () => {
await loadData()
})
</script>
<!-- 数据统计 - 员工业绩分析 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="选择年份" prop="orderDate">
<el-date-picker
v-model="queryParams.times[0]"
class="!w-240px"
type="year"
value-format="YYYY"
:default-time="[new Date().getFullYear()]"
/>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
class="!w-240px"
:data="deptList"
:props="defaultProps"
check-strictly
node-key="id"
placeholder="请选择归属部门"
@change="queryParams.userId = undefined"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" placeholder="员工" clearable>
<el-option
v-for="(user, index) in userListByDeptId"
:label="user.nickname"
:value="user.id"
:key="index"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 搜索 </el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 员工业绩统计 -->
<el-col>
<el-tabs v-model="activeTab">
<!-- 员工合同统计 -->
<el-tab-pane label="员工合同数量统计" name="ContractCountPerformance" lazy>
<ContractCountPerformance :query-params="queryParams" ref="ContractCountPerformanceRef" />
</el-tab-pane>
<!-- 员工合同金额统计 -->
<el-tab-pane label="员工合同金额统计" name="ContractPricePerformance" lazy>
<ContractPricePerformance :query-params="queryParams" ref="ContractPricePerformanceRef" />
</el-tab-pane>
<!-- 员工回款金额统计 -->
<el-tab-pane label="员工回款金额统计" name="ReceivablePricePerformance" lazy>
<ReceivablePricePerformance
:query-params="queryParams"
ref="ReceivablePricePerformanceRef"
/>
</el-tab-pane>
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { beginOfDay, formatDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import ContractCountPerformance from './components/ContractCountPerformance.vue'
import ContractPricePerformance from './components/ContractPricePerformance.vue'
import ReceivablePricePerformance from './components/ReceivablePricePerformance.vue'
defineOptions({ name: 'CrmStatisticsCustomer' })
const queryParams = reactive({
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
// 默认显示当年的数据
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)))
]
})
const queryFormRef = ref() // 搜索的表单
const deptList = ref<Tree[]>([]) // 部门树形结构
const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
// 根据选择的部门筛选员工清单
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
// TODO @scholar:改成尾注释,保证 vue 内容短一点;变量名小写
// 活跃标签
const activeTab = ref('ContractCountPerformance')
// 1.员工合同数量统计
const ContractCountPerformanceRef = ref()
// 2.员工合同金额统计
const ContractPricePerformanceRef = ref()
// 3.员工回款金额统计
const ReceivablePricePerformanceRef = ref()
/** 搜索按钮操作 */
const handleQuery = () => {
// 从 queryParams.times[0] 中获取到了年份
const selectYear = parseInt(queryParams.times[0])
// 创建一个新的 Date 对象,设置为指定的年份的第一天
const fullDate = new Date(selectYear, 0, 1, 0, 0, 0)
// 将完整的日期时间格式化为需要的字符串形式,比如 2004-01-01 00:00:00
// TODO @scholar:看看,是不是可以使用 year 哈
queryParams.times[0] = `${fullDate.getFullYear()}-${String(fullDate.getMonth() + 1).padStart(
2,
'0'
)}-${String(fullDate.getDate()).padStart(2, '0')} ${String(fullDate.getHours()).padStart(2, '0')}:${String(fullDate.getMinutes()).padStart(2, '0')}:${String(fullDate.getSeconds()).padStart(2, '0')}`
switch (activeTab.value) {
case 'ContractCountPerformance':
ContractCountPerformanceRef.value?.loadData?.()
break
case 'ContractPricePerformance':
ContractPricePerformanceRef.value?.loadData?.()
break
case 'ReceivablePricePerformance':
ReceivablePricePerformanceRef.value?.loadData?.()
break
}
}
// 当 activeTab 改变时,刷新当前活动的 tab
watch(activeTab, () => {
handleQuery()
})
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
// 加载部门树
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())
})
</script>
<!-- 客户城市分布 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-row :gutter="20">
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-col>
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption2" />
</el-skeleton>
</el-col>
</el-row>
</el-card>
</template>
<script lang="ts" setup>
import { EChartsOption } from 'echarts'
import china from '@/assets/map/json/china.json'
import echarts from '@/plugins/echarts'
import {
CrmStatisticCustomerAreaRespVO,
StatisticsPortraitApi
} from '@/api/crm/statistics/portrait'
import { areaReplace } from '@/utils'
defineOptions({ name: 'PortraitCustomerArea' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
// 注册地图
echarts?.registerMap('china', china as any)
const loading = ref(false) // 加载中
const areaStatisticsList = ref<CrmStatisticCustomerAreaRespVO[]>([]) // 列表的数据
/** 地图配置(全部客户) */
const echartsOption = reactive<EChartsOption>({
title: {
text: '全部客户',
left: 'center'
},
tooltip: {
trigger: 'item',
showDelay: 0,
transitionDuration: 0.2
},
visualMap: {
text: ['高', '低'],
realtime: false,
calculable: true,
top: 'middle',
inRange: {
color: ['#fff', '#3b82f6']
}
},
series: [
{
name: '客户地域分布',
type: 'map',
map: 'china',
roam: false,
selectedMode: false,
data: []
}
]
}) as EChartsOption
/** 地图配置(成交客户) */
const echartsOption2 = reactive<EChartsOption>({
title: {
text: '成交客户',
left: 'center'
},
tooltip: {
trigger: 'item',
showDelay: 0,
transitionDuration: 0.2
},
visualMap: {
text: ['高', '低'],
realtime: false,
calculable: true,
top: 'middle',
inRange: {
color: ['#fff', '#3b82f6']
}
},
series: [
{
name: '客户地域分布',
type: 'map',
map: 'china',
roam: false,
selectedMode: false,
data: []
}
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const areaList = await StatisticsPortraitApi.getCustomerArea(props.queryParams)
areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => {
return {
...item,
areaName: areaReplace(item.areaName)
}
})
buildLeftMap()
buildRightMap()
loading.value = false
}
defineExpose({ loadData })
const buildLeftMap = () => {
let min = 0
let max = 0
echartsOption.series![0].data = areaStatisticsList.value.map((item) => {
min = Math.min(min, item.customerCount || 0)
max = Math.max(max, item.customerCount || 0)
return { ...item, name: item.areaName, value: item.customerCount || 0 }
})
echartsOption.visualMap!['min'] = min
echartsOption.visualMap!['max'] = max
}
const buildRightMap = () => {
let min = 0
let max = 0
echartsOption2.series![0].data = areaStatisticsList.value.map((item) => {
min = Math.min(min, item.dealCount || 0)
max = Math.max(max, item.dealCount || 0)
return { ...item, name: item.areaName, value: item.dealCount || 0 }
})
echartsOption2.visualMap!['min'] = min
echartsOption2.visualMap!['max'] = max
}
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 客户行业分析 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-row :gutter="20">
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-col>
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption2" />
</el-skeleton>
</el-col>
</el-row>
</el-card>
<!-- 统计列表 -->
<el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
<el-table-column align="center" label="行业占比(%)" min-width="200" prop="industryPortion" />
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticCustomerIndustryRespVO,
StatisticsPortraitApi
} from '@/api/crm/statistics/portrait'
import { EChartsOption } from 'echarts'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { erpCalculatePercentage, getSumValue } from '@/utils'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'PortraitCustomerIndustry' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticCustomerIndustryRespVO[]>([]) // 列表的数据
/** 饼图配置(全部客户) */
const echartsOption = reactive<EChartsOption>({
title: {
text: '全部客户',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
toolbox: {
feature: {
saveAsImage: { show: true, name: '全部客户' } // 保存为图片
}
},
series: [
{
name: '全部客户',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}) as EChartsOption
/** 饼图配置(成交客户) */
const echartsOption2 = reactive<EChartsOption>({
title: {
text: '成交客户',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
toolbox: {
feature: {
saveAsImage: { show: true, name: '成交客户' } // 保存为图片
}
},
series: [
{
name: '成交客户',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const industryList = await StatisticsPortraitApi.getCustomerIndustry(props.queryParams)
// 2.1 更新 Echarts 数据
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
return {
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
value: r.customerCount
}
})
}
// 2.2 更新 Echarts2 数据
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
return {
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
value: r.dealCount
}
})
}
// 3. 计算比例
calculateProportion(industryList)
list.value = industryList
loading.value = false
}
defineExpose({ loadData })
/** 计算比例 */
const calculateProportion = (sourceList: CrmStatisticCustomerIndustryRespVO[]) => {
if (isEmpty(sourceList)) {
return
}
// 这里类型丢失了所以重新搞个变量
const list = sourceList as unknown as CrmStatisticCustomerIndustryRespVO[]
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
const sumDealCount = getSumValue(list.map((item) => item.dealCount))
list.forEach((item) => {
item.industryPortion =
item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount)
item.dealPortion =
item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount)
})
}
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 客户来源分析 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-row :gutter="20">
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-col>
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption2" />
</el-skeleton>
</el-col>
</el-row>
</el-card>
<!-- 统计列表 -->
<el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="客户级别" prop="level" width="200">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
<el-table-column align="center" label="级别占比(%)" min-width="200" prop="levelPortion" />
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticCustomerLevelRespVO,
StatisticsPortraitApi
} from '@/api/crm/statistics/portrait'
import { EChartsOption } from 'echarts'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { erpCalculatePercentage, getSumValue } from '@/utils'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'PortraitCustomerLevel' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticCustomerLevelRespVO[]>([]) // 列表的数据
/** 饼图配置(全部客户) */
const echartsOption = reactive<EChartsOption>({
title: {
text: '全部客户',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
toolbox: {
feature: {
saveAsImage: { show: true, name: '全部客户' } // 保存为图片
}
},
series: [
{
name: '全部客户',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}) as EChartsOption
/** 饼图配置(成交客户) */
const echartsOption2 = reactive<EChartsOption>({
title: {
text: '成交客户',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
toolbox: {
feature: {
saveAsImage: { show: true, name: '成交客户' } // 保存为图片
}
},
series: [
{
name: '成交客户',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const levelList = await StatisticsPortraitApi.getCustomerLevel(props.queryParams)
// 2.1 更新 Echarts 数据
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
return {
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
value: r.customerCount
}
})
}
// 2.2 更新 Echarts2 数据
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
return {
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
value: r.dealCount
}
})
}
// 3. 计算比例
calculateProportion(levelList)
list.value = levelList
loading.value = false
}
defineExpose({ loadData })
/** 计算比例 */
const calculateProportion = (levelList: CrmStatisticCustomerLevelRespVO[]) => {
if (isEmpty(levelList)) {
return
}
// 这里类型丢失了所以重新搞个变量
const list = levelList as unknown as CrmStatisticCustomerLevelRespVO[]
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
const sumDealCount = getSumValue(list.map((item) => item.dealCount))
list.forEach((item) => {
item.levelPortion =
item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount)
item.dealPortion =
item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount)
})
}
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 客户来源分析 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-row :gutter="20">
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-col>
<el-col :span="12">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption2" />
</el-skeleton>
</el-col>
</el-row>
</el-card>
<!-- 统计列表 -->
<el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="客户来源" prop="source" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
<el-table-column align="center" label="来源占比(%)" min-width="200" prop="sourcePortion" />
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticCustomerSourceRespVO,
StatisticsPortraitApi
} from '@/api/crm/statistics/portrait'
import { EChartsOption } from 'echarts'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import { erpCalculatePercentage, getSumValue } from '@/utils'
defineOptions({ name: 'PortraitCustomerSource' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
const list = ref<CrmStatisticCustomerSourceRespVO[]>([]) // 列表的数据
/** 饼图配置(全部客户) */
const echartsOption = reactive<EChartsOption>({
title: {
text: '全部客户',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
toolbox: {
feature: {
saveAsImage: { show: true, name: '全部客户' } // 保存为图片
}
},
series: [
{
name: '全部客户',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}) as EChartsOption
/** 饼图配置(成交客户) */
const echartsOption2 = reactive<EChartsOption>({
title: {
text: '成交客户',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
toolbox: {
feature: {
saveAsImage: { show: true, name: '成交客户' } // 保存为图片
}
},
series: [
{
name: '成交客户',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1. 加载统计数据
loading.value = true
const sourceList = await StatisticsPortraitApi.getCustomerSource(props.queryParams)
// 2.1 更新 Echarts 数据
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
return {
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
value: r.customerCount
}
})
}
// 2.2 更新 Echarts2 数据
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
return {
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
value: r.dealCount
}
})
}
// 3. 计算比例
calculateProportion(sourceList)
list.value = sourceList
loading.value = false
}
defineExpose({ loadData })
/** 计算比例 */
const calculateProportion = (sourceList: CrmStatisticCustomerSourceRespVO[]) => {
if (isEmpty(sourceList)) {
return
}
// 这里类型丢失了所以重新搞个变量
const list = sourceList as unknown as CrmStatisticCustomerSourceRespVO[]
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
const sumDealCount = getSumValue(list.map((item) => item.dealCount))
list.forEach((item) => {
item.sourcePortion =
item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount)
item.dealPortion =
item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount)
})
}
/** 初始化 */
onMounted(() => {
loadData()
})
</script>
<!-- 数据统计 - 客户画像 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="时间范围" prop="orderDate">
<el-date-picker
v-model="queryParams.times"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="defaultShortcuts"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
:data="deptList"
:props="defaultProps"
check-strictly
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="queryParams.userId = undefined"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" clearable placeholder="员工">
<el-option
v-for="(user, index) in userListByDeptId"
:key="index"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 客户统计 -->
<el-col>
<el-tabs v-model="activeTab">
<!-- 城市分布分析 -->
<el-tab-pane label="城市分布分析" lazy name="areaRef">
<PortraitCustomerArea ref="areaRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户级别分析 -->
<el-tab-pane label="客户级别分析" lazy name="levelRef">
<PortraitCustomerLevel ref="levelRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户来源分析 -->
<el-tab-pane label="客户来源分析" lazy name="sourceRef">
<PortraitCustomerSource ref="sourceRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户行业分析 -->
<el-tab-pane label="客户行业分析" lazy name="industryRef">
<PortraitCustomerIndustry ref="industryRef" :query-params="queryParams" />
</el-tab-pane>
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import PortraitCustomerArea from './components/PortraitCustomerArea.vue'
import PortraitCustomerIndustry from './components/PortraitCustomerIndustry.vue'
import PortraitCustomerSource from './components/PortraitCustomerSource.vue'
import PortraitCustomerLevel from './components/PortraitCustomerLevel.vue'
defineOptions({ name: 'CrmStatisticsPortrait' })
const queryParams = reactive({
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
// 默认显示最近一周的数据
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
]
})
const queryFormRef = ref() // 搜索的表单
const deptList = ref<Tree[]>([]) // 部门树形结构
const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
/** 根据选择的部门筛选员工清单 */
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
const activeTab = ref('areaRef') // 活跃标签
const areaRef = ref() // 客户地区分布
const levelRef = ref() // 客户级别
const sourceRef = ref() // 客户来源
const industryRef = ref() // 客户行业
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'areaRef':
areaRef.value?.loadData?.()
break
case 'levelRef':
levelRef.value?.loadData?.()
break
case 'sourceRef':
sourceRef.value?.loadData?.()
break
case 'industryRef':
industryRef.value?.loadData?.()
break
}
}
/** 当 activeTab 改变时,刷新当前活动的 tab */
watch(activeTab, () => {
handleQuery()
})
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 初始化 */
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())
})
</script>
......@@ -22,7 +22,7 @@ import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/ra
import { EChartsOption } from 'echarts'
import { clone } from 'lodash-es'
defineOptions({ name: 'ContactsCountRank' })
defineOptions({ name: 'ContactCountRank' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
const loading = ref(false) // 加载中
......
......@@ -13,7 +13,13 @@
<el-table-column label="公司排名" align="center" type="index" width="80" />
<el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
<el-table-column label="部门" align="center" prop="deptName" min-width="200" />
<el-table-column label="合同金额(元)" align="center" prop="count" min-width="200" />
<el-table-column
label="合同金额(元)"
align="center"
prop="count"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
</el-table>
</el-card>
</template>
......@@ -21,6 +27,7 @@
import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
import { EChartsOption } from 'echarts'
import { clone } from 'lodash-es'
import { erpPriceTableColumnFormatter } from '@/utils'
defineOptions({ name: 'ContractPriceRank' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
......
......@@ -13,7 +13,13 @@
<el-table-column label="公司排名" align="center" type="index" width="80" />
<el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
<el-table-column label="部门" align="center" prop="deptName" min-width="200" />
<el-table-column label="回款金额(元)" align="center" prop="count" min-width="200" />
<el-table-column
label="回款金额(元)"
align="center"
prop="count"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
</el-table>
</el-card>
</template>
......@@ -21,6 +27,7 @@
import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
import { EChartsOption } from 'echarts'
import { clone } from 'lodash-es'
import { erpPriceTableColumnFormatter } from '@/utils'
defineOptions({ name: 'ReceivablePriceRank' })
const props = defineProps<{ queryParams: any }>() // 搜索参数
......
......@@ -29,6 +29,7 @@
check-strictly
node-key="id"
placeholder="请选择归属部门"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
......@@ -62,8 +63,8 @@
<CustomerCountRank :query-params="queryParams" ref="customerCountRankRef" />
</el-tab-pane>
<!-- 新增联系人数排行 -->
<el-tab-pane label="新增联系人数排行" name="contactsCountRank" lazy>
<ContactsCountRank :query-params="queryParams" ref="contactsCountRankRef" />
<el-tab-pane label="新增联系人数排行" name="contactCountRank" lazy>
<ContactCountRank :query-params="queryParams" ref="contactCountRankRef" />
</el-tab-pane>
<!-- 跟进次数排行 -->
<el-tab-pane label="跟进次数排行" name="followCountRank" lazy>
......@@ -77,14 +78,14 @@
</el-col>
</template>
<script lang="ts" setup>
import ContractPriceRank from './ContractPriceRank.vue'
import ReceivablePriceRank from './ReceivablePriceRank.vue'
import ContractCountRank from './ContractCountRank.vue'
import ProductSalesRank from './ProductSalesRank.vue'
import CustomerCountRank from './CustomerCountRank.vue'
import ContactsCountRank from './ContactsCountRank.vue'
import FollowCountRank from './FollowCountRank.vue'
import FollowCustomerCountRank from './FollowCustomerCountRank.vue'
import ContractPriceRank from './components/ContractPriceRank.vue'
import ReceivablePriceRank from './components/ReceivablePriceRank.vue'
import ContractCountRank from './components/ContractCountRank.vue'
import ProductSalesRank from './components/ProductSalesRank.vue'
import CustomerCountRank from './components/CustomerCountRank.vue'
import ContactCountRank from './components/ContactCountRank.vue'
import FollowCountRank from './components/FollowCountRank.vue'
import FollowCustomerCountRank from './components/FollowCustomerCountRank.vue'
import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
......@@ -109,35 +110,35 @@ const receivablePriceRankRef = ref() // ReceivablePriceRank 组件的引用
const contractCountRankRef = ref() // ContractCountRank 组件的引用
const productSalesRankRef = ref() // ProductSalesRank 组件的引用
const customerCountRankRef = ref() // CustomerCountRank 组件的引用
const contactsCountRankRef = ref() // ContactsCountRank 组件的引用
const contactCountRankRef = ref() // ContactCountRank 组件的引用
const followCountRankRef = ref() // FollowCountRank 组件的引用
const followCustomerCountRankRef = ref() // FollowCustomerCountRank 组件的引用
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'contractPriceRank':
case 'contractPriceRank': // 合同金额排行
contractPriceRankRef.value?.loadData?.()
break
case 'receivablePriceRank':
case 'receivablePriceRank': // 回款金额排行
receivablePriceRankRef.value?.loadData?.()
break
case 'contractCountRank':
case 'contractCountRank': // 签约合同排行
contractCountRankRef.value?.loadData?.()
break
case 'productSalesRank':
case 'productSalesRank': // 产品销量排行
productSalesRankRef.value?.loadData?.()
break
case 'customerCountRank':
case 'customerCountRank': // 新增客户数排行
customerCountRankRef.value?.loadData?.()
break
case 'contactsCountRank':
contactsCountRankRef.value?.loadData?.()
case 'contactCountRank': // 新增联系人数排行
contactCountRankRef.value?.loadData?.()
break
case 'followCountRank':
case 'followCountRank': // 跟进次数排行
followCountRankRef.value?.loadData?.()
break
case 'followCustomerCountRank':
case 'followCustomerCountRank': // 跟进客户数排行
followCustomerCountRankRef.value?.loadData?.()
break
}
......
......@@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { ProductCategoryApi } from '@/api/erp/product/category'
import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
import { defaultProps, handleTree } from '@/utils/tree'
import { CommonStatusEnum } from '@/utils/constants'
......@@ -66,7 +66,7 @@ const formData = ref({
name: undefined,
code: undefined,
sort: undefined,
status: undefined
status: CommonStatusEnum.ENABLE
})
const formRules = reactive({
parentId: [{ required: true, message: '上级编号不能为空', trigger: 'blur' }],
......@@ -105,7 +105,7 @@ const submitForm = async () => {
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as ProductCategoryApi.ProductCategoryVO
const data = formData.value as unknown as ProductCategoryVO
if (formType.value === 'create') {
await ProductCategoryApi.createProductCategory(data)
message.success(t('common.createSuccess'))
......
......@@ -26,6 +26,9 @@
<el-descriptions-item label="请求参数">
{{ detailData.requestParams }}
</el-descriptions-item>
<el-descriptions-item label="请求结果">
{{ detailData.responseBody }}
</el-descriptions-item>
<el-descriptions-item label="请求时间">
{{ formatDate(detailData.beginTime) }} ~ {{ formatDate(detailData.endTime) }}
</el-descriptions-item>
......@@ -36,6 +39,15 @@
失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}
</div>
</el-descriptions-item>
<el-descriptions-item label="操作模块">
{{ detailData.operateModule }}
</el-descriptions-item>
<el-descriptions-item label="操作名">
{{ detailData.operateName }}
</el-descriptions-item>
<el-descriptions-item label="操作名">
<dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="detailData.operateType" />
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
......
......@@ -80,7 +80,7 @@
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:api-error-log:export']"
v-hasPermi="['infra:api-access-log:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
......@@ -91,16 +91,16 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="日志编号" align="center" prop="id" width="100" fix="right" />
<el-table-column label="用户编号" align="center" prop="userId" />
<el-table-column label="用户类型" align="center" prop="userType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
</template>
</el-table-column>
<el-table-column label="应用名" align="center" prop="applicationName" />
<el-table-column label="应用名" align="center" prop="applicationName" width="150" />
<el-table-column label="请求方法" align="center" prop="requestMethod" width="80" />
<el-table-column label="请求地址" align="center" prop="requestUrl" width="250" />
<el-table-column label="请求地址" align="center" prop="requestUrl" width="500" />
<el-table-column label="请求时间" align="center" prop="beginTime" width="180">
<template #default="scope">
<span>{{ formatDate(scope.row.beginTime) }}</span>
......@@ -114,7 +114,14 @@
{{ scope.row.resultCode === 0 ? '成功' : '失败(' + scope.row.resultMsg + ')' }}
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<el-table-column label="操作模块" align="center" prop="operateModule" width="180" />
<el-table-column label="操作名" align="center" prop="operateName" width="180" />
<el-table-column label="操作类型" align="center" prop="operateType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="scope.row.operateType" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="60">
<template #default="scope">
<el-button
link
......
......@@ -8,11 +8,9 @@
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
</div>
</el-col>
<!-- 表单设计器 -->
<el-col>
<FcDesigner ref="designer" height="780px" />
</el-col>
</el-row>
<!-- 表单设计器 -->
<FcDesigner ref="designer" height="780px" />
</ContentWrap>
<!-- 弹窗:表单预览 -->
......@@ -23,15 +21,14 @@
</el-button>
<el-scrollbar height="580">
<div>
<pre><code class="hljs" v-dompurify-html="highlightedCode(formData)"></code></pre>
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
</div>
</el-scrollbar>
</div>
</Dialog>
</template>
<script lang="ts" setup>
defineOptions({ name: 'InfraBuild' })
import FcDesigner from '@form-create/designer'
import { useFormCreateDesigner } from '@/components/FormCreate'
import { useClipboard } from '@vueuse/core'
import { isString } from '@/utils/is'
......@@ -41,6 +38,8 @@ import xml from 'highlight.js/lib/languages/java'
import json from 'highlight.js/lib/languages/json'
import formCreate from '@form-create/element-ui'
defineOptions({ name: 'InfraBuild' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息
......@@ -49,6 +48,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formType = ref(-1) // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件
const formData = ref('') // 表单数据
useFormCreateDesigner(designer) // 表单设计器增强
/** 打开弹窗 */
const openModel = (title: string) => {
......@@ -82,14 +82,13 @@ const makeTemplate = () => {
const opt = designer.value.getOption()
return `<template>
<form-create
v-model="fapi"
v-model:api="fApi"
:rule="rule"
:option="option"
@submit="onSubmit"
></form-create>
</template>
<script setup lang=ts>
import formCreate from "@form-create/element-ui";
const faps = ref(null)
const rule = ref('')
const option = ref('')
......
<template>
<doc-alert title="数据库文档" url="https://doc.iocoder.cn/db-doc/" />
<ContentWrap title="数据库文档">
<div class="mb-10px">
<el-button type="primary" plain @click="handleExport('HTML')">
<Icon icon="ep:download" /> 导出 HTML
</el-button>
<el-button type="primary" plain @click="handleExport('Word')">
<Icon icon="ep:download" /> 导出 Word
</el-button>
<el-button type="primary" plain @click="handleExport('Markdown')">
<Icon icon="ep:download" /> 导出 Markdown
</el-button>
</div>
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import download from '@/utils/download'
import * as DbDocApi from '@/api/infra/dbDoc'
defineOptions({ name: 'InfraDBDoc' })
const loading = ref(true) // 是否加载中
const src = ref('') // HTML 的地址
/** 页面加载 */
const init = async () => {
try {
const data = await DbDocApi.exportHtml()
const blob = new Blob([data], { type: 'text/html' })
src.value = window.URL.createObjectURL(blob)
} finally {
loading.value = false
}
}
/** 处理导出 */
const handleExport = async (type: string) => {
if (type === 'HTML') {
const res = await DbDocApi.exportHtml()
download.html(res, '数据库文档.html')
}
if (type === 'Word') {
const res = await DbDocApi.exportWord()
download.word(res, '数据库文档.doc')
}
if (type === 'Markdown') {
const res = await DbDocApi.exportMarkdown()
download.markdown(res, '数据库文档.md')
}
}
/** 初始化 */
onMounted(async () => {
await init()
})
</script>
......@@ -96,7 +96,7 @@
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:config:delete']"
v-hasPermi="['infra:file:delete']"
>
删除
</el-button>
......
......@@ -107,7 +107,7 @@
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:config:delete']"
v-hasPermi="['infra:file-config:delete']"
>
删除
</el-button>
......
......@@ -54,7 +54,7 @@
</template>
<div class="max-h-80 overflow-auto">
<ul>
<li v-for="msg in messageList.reverse()" :key="msg.time" class="mt-2">
<li v-for="msg in messageReverseList" :key="msg.time" class="mt-2">
<div class="flex items-center">
<span class="text-primary mr-2 font-medium">收到消息:</span>
<span>{{ formatDate(msg.time) }}</span>
......@@ -92,6 +92,7 @@ const { status, data, send, close, open } = useWebSocket(server.value, {
/** 监听接收到的数据 */
const messageList = ref([] as { time: number; text: string }[]) // 消息列表
const messageReverseList = computed(() => messageList.value.slice().reverse())
watchEffect(() => {
if (!data.value) {
return
......
......@@ -9,7 +9,7 @@
tag="今日"
title="销售额"
prefix="¥"
::decimals="2"
:decimals="2"
:value="fenToYuan(orderComparison?.value?.orderPayPrice || 0)"
:reference="fenToYuan(orderComparison?.reference?.orderPayPrice || 0)"
/>
......@@ -26,8 +26,8 @@
<ComparisonCard
tag="今日"
title="订单量"
:value="fenToYuan(orderComparison?.value?.orderPayCount || 0)"
:reference="fenToYuan(orderComparison?.reference?.orderPayCount || 0)"
:value="orderComparison?.value?.orderPayCount || 0"
:reference="orderComparison?.reference?.orderPayCount || 0"
/>
</el-col>
<el-col :md="6" :sm="12" :xs="24" :loading="loading">
......
......@@ -151,6 +151,7 @@ import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivit
import BargainActivityForm from './BargainActivityForm.vue'
import { formatDate } from '@/utils/formatTime'
import { fenToYuanFormat } from '@/utils/formatter'
import { closeBargainActivity } from '@/api/mall/promotion/bargain/bargainActivity'
defineOptions({ name: 'PromotionBargainActivity' })
......@@ -206,7 +207,7 @@ const handleClose = async (id: number) => {
// 关闭的二次确认
await message.confirm('确认关闭该砍价活动吗?')
// 发起关闭
await BargainActivityApi.closeSeckillActivity(id)
await BargainActivityApi.closeBargainActivity(id)
message.success('关闭成功')
// 刷新列表
await getList()
......
......@@ -157,7 +157,7 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
import { SeckillConfigApi } from '@/api/mall/promotion/seckill/seckillConfig'
import SeckillActivityForm from './SeckillActivityForm.vue'
import { formatDate } from '@/utils/formatTime'
import { fenToYuanFormat } from '@/utils/formatter'
......
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
import { getSimpleSeckillConfigList } from '@/api/mall/promotion/seckill/seckillConfig'
import { dateFormatter2 } from '@/utils/formatTime'
import { SeckillConfigApi } from '@/api/mall/promotion/seckill/seckillConfig'
// 表单校验
export const rules = reactive({
......@@ -88,7 +88,7 @@ const crudSchemas = reactive<CrudSchema[]>([
valueField: 'id'
}
},
api: getSimpleSeckillConfigList
api: SeckillConfigApi.getSimpleSeckillConfigList
},
table: {
width: 300
......
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Form ref="formRef" v-loading="formLoading" :rules="rules" :schema="allSchemas.formSchema" />
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="秒杀时段名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入秒杀时段名称" />
</el-form-item>
<el-form-item label="开始时间点" prop="startTime">
<el-time-picker
v-model="formData.startTime"
value-format="HH:mm:ss"
placeholder="选择开始时间点"
/>
</el-form-item>
<el-form-item label="结束时间点" prop="endTime">
<el-time-picker
v-model="formData.endTime"
value-format="HH:mm:ss"
placeholder="选择结束时间点"
/>
</el-form-item>
<el-form-item label="秒杀轮播图" prop="sliderPicUrls">
<UploadImgs v-model="formData.sliderPicUrls" placeholder="请输入秒杀轮播图" />
</el-form-item>
<el-form-item label="活动状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="SeckillConfigForm" setup>
import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
import { allSchemas, rules } from './seckillConfig.data'
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { SeckillConfigApi, SeckillConfigVO } from '@/api/mall/promotion/seckill/seckillConfig.ts'
import { CommonStatusEnum } from '@/utils/constants'
/** 秒杀时段 表单 */
defineOptions({ name: 'SeckillConfigForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
......@@ -18,6 +60,20 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: undefined,
startTime: undefined,
endTime: undefined,
sliderPicUrls: undefined,
status: undefined
})
const formRules = reactive({
name: [{ required: true, message: '秒杀时段名称不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间点不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间点不能为空', trigger: 'blur' }],
status: [{ required: true, message: '活动状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
......@@ -25,15 +81,12 @@ const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const data = await SeckillConfigApi.getSeckillConfig(id)
data.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
url: item
}))
formRef.value.setValues(data)
formData.value = await SeckillConfigApi.getSeckillConfig(id)
} finally {
formLoading.value = false
}
......@@ -45,24 +98,11 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.getElFormRef().validate()
if (!valid) return
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
// 处理轮播图列表
const sliderPicUrls = []
formRef.value.formModel.sliderPicUrls.forEach((item) => {
// 如果是前端选的图
typeof item === 'object' ? sliderPicUrls.push(item.url) : sliderPicUrls.push(item)
})
// 真正提交
const data = {
...formRef.value.formModel,
sliderPicUrls
} as SeckillConfigApi.SeckillConfigVO
const data = formData.value as unknown as SeckillConfigVO
if (formType.value === 'create') {
await SeckillConfigApi.createSeckillConfig(data)
message.success(t('common.createSuccess'))
......@@ -77,4 +117,17 @@ const submitForm = async () => {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
startTime: undefined,
endTime: undefined,
sliderPicUrls: [],
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
</script>
<template>
<doc-alert title="【营销】秒杀活动" url="https://doc.iocoder.cn/mall/promotion-seckill/" />
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="108px"
>
<el-form-item label="秒杀时段名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入秒杀时段名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="活动状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择活动状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
v-hasPermi="['promotion:seckill-config:create']"
plain
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['promotion:seckill-config:create']"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</template>
</Search>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<Table
v-model:currentPage="tableObject.currentPage"
v-model:pageSize="tableObject.pageSize"
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
>
<template #sliderPicUrls="{ row }">
<el-image
v-for="(item, index) in row.sliderPicUrls"
:key="index"
:src="item"
class="mr-10px h-60px w-60px"
@click="imagePreview(row.sliderPicUrls)"
/>
</template>
<template #status="{ row }">
<el-switch
v-model="row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(row)"
/>
</template>
<template #action="{ row }">
<el-button
v-hasPermi="['promotion:seckill-config:update']"
link
type="primary"
@click="openForm('update', row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['promotion:seckill-config:delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</Table>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="秒杀时段名称" align="center" prop="name" />
<el-table-column label="开始时间点" align="center" prop="startTime" />
<el-table-column label="结束时间点" align="center" prop="endTime" />
<el-table-column label="秒杀轮播图" align="center" prop="sliderPicUrls">
<template #default="scope">
<el-image
class="h-40px max-w-40px"
v-for="(url, index) in scope?.row.sliderPicUrls"
:key="index"
:src="url"
:preview-src-list="scope?.row.sliderPicUrls"
:initial-index="index"
preview-teleported
/>
</template>
</el-table-column>
<el-table-column label="活动状态" align="center" prop="status">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['promotion:seckill-config:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['promotion:seckill-config:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<SeckillConfigForm ref="formRef" @success="getList" />
</template>
<script lang="ts" name="PromotionSeckillConfig" setup>
import { allSchemas } from './seckillConfig.data'
import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { SeckillConfigApi, SeckillConfigVO } from '@/api/mall/promotion/seckill/seckillConfig.ts'
import SeckillConfigForm from './SeckillConfigForm.vue'
import { createImageViewer } from '@/components/ImageViewer'
import { CommonStatusEnum } from '@/utils/constants'
/** 秒杀时段 列表 */
defineOptions({ name: 'SeckillConfig' })
const message = useMessage() // 消息弹窗
// tableObject:表格的属性对象,可获得分页大小、条数等属性
// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
const { tableObject, tableMethods } = useTable({
getListApi: SeckillConfigApi.getSeckillConfigPage, // 分页接口
delListApi: SeckillConfigApi.deleteSeckillConfig // 删除接口
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<SeckillConfigVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined
})
// 获得表格的各种操作
const { getList, setSearchParams } = tableMethods
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SeckillConfigApi.getSeckillConfigPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
......@@ -97,16 +175,24 @@ const openForm = (type: string, id?: number) => {
}
/** 删除按钮操作 */
const handleDelete = (id: number) => {
tableMethods.delList(id, false)
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SeckillConfigApi.deleteSeckillConfig(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 修改用户状态 */
const handleStatusChange = async (row: SeckillConfigApi.SeckillConfigVO) => {
const handleStatusChange = async (row: SeckillConfigVO) => {
try {
// 修改状态的二次确认
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
await message.confirm('确认要"' + text + '""' + row.name + '?')
await message.confirm('确认要' + text + '"' + row.name + '"活动吗?')
// 发起修改状态
await SeckillConfigApi.updateSeckillConfigStatus(row.id, row.status)
// 刷新列表
......@@ -118,13 +204,6 @@ const handleStatusChange = async (row: SeckillConfigApi.SeckillConfigVO) => {
}
}
/** 轮播图预览预览 */
const imagePreview = (args) => {
createImageViewer({
urlList: args
})
}
/** 初始化 **/
onMounted(() => {
getList()
......
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter } from '@/utils/formatTime'
// 表单校验
export const rules = reactive({
name: [required],
startTime: [required],
endTime: [required],
picUrl: [required],
status: [required]
})
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive<CrudSchema[]>([
{
label: '秒杀时段名称',
field: 'name',
isSearch: true
},
{
label: '开始时间点',
field: 'startTime',
isSearch: false,
search: {
component: 'TimePicker'
},
form: {
component: 'TimePicker',
componentProps: {
valueFormat: 'HH:mm:ss'
}
}
},
{
label: '结束时间点',
field: 'endTime',
isSearch: false,
search: {
component: 'TimePicker'
},
form: {
component: 'TimePicker',
componentProps: {
valueFormat: 'HH:mm:ss'
}
}
},
{
label: '秒杀轮播图',
field: 'sliderPicUrls',
isSearch: false,
form: {
component: 'UploadImgs'
},
table: {
width: 300
}
},
{
label: '状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
form: {
component: 'Radio'
}
},
{
label: '创建时间',
field: 'createTime',
isForm: false,
isSearch: false,
formatter: dateFormatter
},
{
label: '操作',
field: 'action',
isForm: false
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)
......@@ -3,44 +3,44 @@
<div class="flex flex-col">
<el-row :gutter="16" class="summary">
<el-col :sm="6" :xs="12" v-loading="loading">
<el-col v-loading="loading" :sm="6" :xs="12">
<SummaryCard
title="累计会员数"
:value="summary?.userCount || 0"
icon="fa-solid:users"
icon-color="bg-blue-100"
icon-bg-color="text-blue-500"
:value="summary?.userCount || 0"
icon-color="bg-blue-100"
title="累计会员数"
/>
</el-col>
<el-col :sm="6" :xs="12" v-loading="loading">
<el-col v-loading="loading" :sm="6" :xs="12">
<SummaryCard
title="累计充值人数"
:value="summary?.rechargeUserCount || 0"
icon="fa-solid:user"
icon-color="bg-purple-100"
icon-bg-color="text-purple-500"
:value="summary?.rechargeUserCount || 0"
icon-color="bg-purple-100"
title="累计充值人数"
/>
</el-col>
<el-col :sm="6" :xs="12" v-loading="loading">
<el-col v-loading="loading" :sm="6" :xs="12">
<SummaryCard
title="累计充值金额"
:decimals="2"
:value="fenToYuan(summary?.rechargePrice || 0)"
icon="fa-solid:money-check-alt"
icon-color="bg-yellow-100"
icon-bg-color="text-yellow-500"
icon-color="bg-yellow-100"
prefix="¥"
:decimals="2"
:value="fenToYuan(summary?.rechargePrice || 0)"
title="累计充值金额"
/>
</el-col>
<el-col :sm="6" :xs="12" v-loading="loading">
<el-col v-loading="loading" :sm="6" :xs="12">
<SummaryCard
title="累计消费金额"
:decimals="2"
:value="fenToYuan(summary?.expensePrice || 0)"
icon="fa-solid:yen-sign"
icon-color="bg-green-100"
icon-bg-color="text-green-500"
icon-color="bg-green-100"
prefix="¥"
:decimals="2"
:value="fenToYuan(summary?.expensePrice || 0)"
title="累计消费金额"
/>
</el-col>
</el-row>
......@@ -67,42 +67,42 @@
<el-col :span="14">
<el-table :data="areaStatisticsList" :height="300">
<el-table-column
label="省份"
prop="areaName"
:sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
align="center"
label="省份"
min-width="80"
prop="areaName"
show-overflow-tooltip
sortable
:sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
/>
<el-table-column
label="会员数量"
prop="userCount"
align="center"
label="会员数量"
min-width="105"
prop="userCount"
sortable
/>
<el-table-column
label="订单创建数量"
prop="orderCreateUserCount"
align="center"
label="订单创建数量"
min-width="135"
prop="orderCreateUserCount"
sortable
/>
<el-table-column
label="订单支付数量"
prop="orderPayUserCount"
align="center"
label="订单支付数量"
min-width="135"
prop="orderPayUserCount"
sortable
/>
<el-table-column
label="订单支付金额"
prop="orderPayPrice"
:formatter="fenToYuanFormat"
align="center"
label="订单支付金额"
min-width="135"
prop="orderPayPrice"
sortable
:formatter="fenToYuanFormat"
/>
</el-table>
</el-col>
......@@ -110,7 +110,7 @@
</el-card>
</el-col>
<el-col :md="6" :sm="24">
<el-card shadow="never" v-loading="loading">
<el-card v-loading="loading" shadow="never">
<template #header>
<CardTitle title="会员性别比例" />
</template>
......@@ -122,16 +122,16 @@
</template>
<script lang="ts" setup>
import * as MemberStatisticsApi from '@/api/mall/statistics/member'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { EChartsOption } from 'echarts'
import china from '@/assets/map/json/china.json'
import { fenToYuan } from '@/utils'
import {
MemberAreaStatisticsRespVO,
MemberSexStatisticsRespVO,
MemberSummaryRespVO,
MemberTerminalStatisticsRespVO
} from '@/api/mall/statistics/member'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { EChartsOption } from 'echarts'
import china from '@/assets/map/json/china.json'
import { areaReplace, fenToYuan } from '@/utils'
import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
import echarts from '@/plugins/echarts'
import { fenToYuanFormat } from '@/utils/formatter'
......@@ -246,12 +246,7 @@ const getMemberAreaStatisticsList = async () => {
areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
return {
...item,
areaName: item.areaName
.replace('维吾尔自治区', '')
.replace('壮族自治区', '')
.replace('回族自治区', '')
.replace('自治区', '')
.replace('省', '')
areaName: areaReplace(item.areaName)
}
})
let min = 0
......
......@@ -26,9 +26,9 @@
icon="ep:view"
icon-color="bg-blue-100"
icon-bg-color="text-blue-500"
prefix=""
:decimals="2"
:value="fenToYuan(trendSummary?.value?.browseCount || 0)"
prefix=""
:decimals="0"
:value="trendSummary?.value?.browseCount || 0"
:percent="
calculateRelativeRate(
trendSummary?.value?.browseCount,
......@@ -44,9 +44,9 @@
icon="ep:user-filled"
icon-color="bg-purple-100"
icon-bg-color="text-purple-500"
prefix=""
:decimals="2"
:value="fenToYuan(trendSummary?.value?.browseUserCount || 0)"
prefix=""
:decimals="0"
:value="trendSummary?.value?.browseUserCount || 0"
:percent="
calculateRelativeRate(
trendSummary?.value?.browseUserCount,
......@@ -62,9 +62,9 @@
icon="fa-solid:money-check-alt"
icon-color="bg-yellow-100"
icon-bg-color="text-yellow-500"
prefix=""
:decimals="2"
:value="fenToYuan(trendSummary?.value?.orderPayCount || 0)"
prefix=""
:decimals="0"
:value="trendSummary?.value?.orderPayCount || 0"
:percent="
calculateRelativeRate(
trendSummary?.value?.orderPayCount,
......@@ -98,9 +98,9 @@
icon="fa-solid:wallet"
icon-color="bg-cyan-100"
icon-bg-color="text-cyan-500"
prefix=""
:decimals="2"
:value="fenToYuan(trendSummary?.value?.afterSaleCount || 0)"
prefix=""
:decimals="0"
:value="trendSummary?.value?.afterSaleCount || 0"
:percent="
calculateRelativeRate(
trendSummary?.value?.afterSaleCount,
......
......@@ -27,7 +27,7 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: 0, // 售后订单编号
id: undefined, // 售后订单编号
auditReason: '' // 审批备注
})
const formRef = ref() // 表单 Ref
......@@ -62,7 +62,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0, // 售后订单编号
id: undefined, // 售后订单编号
auditReason: '' // 审批备注
}
formRef.value?.resetFields()
......
......@@ -103,7 +103,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0,
id: undefined,
bindUserId: undefined
}
formRef.value?.resetFields()
......
......@@ -43,7 +43,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const expressType = ref('express') // 如果值是 express,则是快递;none 则是无;未来做同城配送;
const formData = ref<TradeOrderApi.DeliveryVO>({
id: 0, // 订单编号
id: undefined, // 订单编号
logisticsId: null, // 物流公司编号
logisticsNo: '' // 物流编号
})
......@@ -86,7 +86,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0, // 订单编号
id: undefined, // 订单编号
logisticsId: null, // 物流公司编号
logisticsNo: '' // 物流编号
}
......
......@@ -44,7 +44,7 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: 0, // 订单编号
id: undefined, // 订单编号
receiverName: '', // 收件人名称
receiverMobile: '', // 收件人手机
receiverAreaId: null, //收件人地区编号
......@@ -82,7 +82,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0, // 订单编号
id: undefined, // 订单编号
receiverName: '', // 收件人名称
receiverMobile: '', // 收件人手机
receiverAreaId: null, //收件人地区编号
......
......@@ -6,7 +6,7 @@
</el-form-item>
<el-form-item label="订单调价">
<el-input-number v-model="formData.adjustPrice" :precision="2" :step="0.1" class="w-100%" />
<el-tag class="mt-10px" type="warning">订单调价。 正数,加价;负数,减价</el-tag>
<el-tag class="ml-10px" type="warning">订单调价。 正数,加价;负数,减价</el-tag>
</el-form-item>
<el-form-item label="调价后">
<el-input v-model="formData.newPayPrice" disabled />
......@@ -31,17 +31,20 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: 0, // 订单编号
id: undefined, // 订单编号
adjustPrice: 0, // 订单调价
payPrice: '', // 应付金额(总)
newPayPrice: '' // 调价后应付金额(总)
})
watch(
() => formData.value.adjustPrice,
(data: number) => {
const num = formData.value.payPrice!.replace('元', '')
// @ts-ignore
formData.value.newPayPrice = (num * 1 + data).toFixed(2) + '元'
(adjustPrice: number | string) => {
const numMatch = formData.value.payPrice.match(/\d+(\.\d+)?/)
if (numMatch) {
const payPriceNum = parseFloat(numMatch[0])
adjustPrice = typeof adjustPrice === 'string' ? parseFloat(adjustPrice) : adjustPrice
formData.value.newPayPrice = (payPriceNum + adjustPrice).toFixed(2) + '元'
}
}
)
......@@ -82,7 +85,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0, // 订单编号
id: undefined, // 订单编号
adjustPrice: 0, // 订单调价
payPrice: '', // 应付金额(总)
newPayPrice: '' // 调价后应付金额(总)
......
......@@ -27,7 +27,7 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: 0, // 订单编号
id: undefined, // 订单编号
remark: '' // 订单备注
})
const formRef = ref() // 表单 Ref
......@@ -62,7 +62,7 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0, // 订单编号
id: undefined, // 订单编号
remark: '' // 订单备注
}
formRef.value?.resetFields()
......
......@@ -63,7 +63,7 @@ import { getAccessToken } from '@/utils/auth'
import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
const props = defineProps<{
......
......@@ -67,7 +67,7 @@ import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
const props = defineProps<{
......
......@@ -58,7 +58,7 @@ import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
const props = defineProps<{
......
......@@ -61,7 +61,7 @@ import { getAccessToken } from '@/utils/auth'
import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
const props = defineProps<{
......
......@@ -18,10 +18,10 @@
<template #append>%</template>
</el-input>
</el-form-item>
<el-form-item label-width="180px" label="公众号 APPID" prop="config.appId">
<el-form-item label-width="180px" label="微信 APPID" prop="config.appId">
<el-input
v-model="formData.config.appId"
placeholder="请输入公众号 APPID"
placeholder="请输入微信 APPID"
clearable
:style="{ width: '100%' }"
/>
......
<template>
<ContentWrap>
<doc-alert title="大屏设计器" url="https://doc.iocoder.cn/report/screen/" />
<IFrame :src="src" />
</ContentWrap>
</template>
......
<template>
<ContentWrap>
<doc-alert title="报表设计器" url="https://doc.iocoder.cn/report/" />
<IFrame :src="src" />
</ContentWrap>
</template>
......
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="文件名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入文件名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="文件内容" prop="content">
<Editor v-model="formData.content" height="150px" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import * as UReportDataApi from '@/api/report/ureport'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: undefined,
status: undefined,
content: undefined,
remark: undefined,
})
const formRules = reactive({
name: [{ required: true, message: '文件名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await UReportDataApi.getUReportData(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as UReportDataApi.UReportDataVO
if (formType.value === 'create') {
await UReportDataApi.createUReportData(data)
message.success(t('common.createSuccess'))
} else {
await UReportDataApi.updateUReportData(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
status: undefined,
content: undefined,
remark: undefined,
}
formRef.value?.resetFields()
}
</script>
\ No newline at end of file
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { getAccessToken } from '@/utils/auth'
defineOptions({ name: 'UReportData' })
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/ureport/designer?token=' + getAccessToken())
</script>
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="文件名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入文件名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['report:ureport-data:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['report:ureport-data:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="ID" align="center" prop="id" />
<el-table-column label="文件名称" align="center" prop="name" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="文件内容" align="center" prop="content" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['report:ureport-data:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['report:ureport-data:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<UReportDataForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as UReportDataApi from '@/api/report/ureport'
import UReportDataForm from './UReportDataForm.vue'
defineOptions({ name: 'UReportData' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
status: null,
remark: null,
createTime: [],
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await UReportDataApi.getUReportDataPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await UReportDataApi.deleteUReportData(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await UReportDataApi.exportUReportData(queryParams)
download.excel(data, 'Ureport2报表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
......@@ -8,7 +8,7 @@
:inline="true"
label-width="68px"
>
<el-form-item label="部门名称" prop="title">
<el-form-item label="部门名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入部门名称"
......
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="应用名" prop="applicationName">
<el-input v-model="formData.applicationName" clearable placeholder="请输入应用名" />
</el-form-item>
<el-form-item label="错误码编码" prop="code">
<el-input v-model="formData.code" clearable placeholder="请输入错误码编码" />
</el-form-item>
<el-form-item label="错误码提示" prop="message">
<el-input v-model="formData.message" clearable placeholder="请输入错误码提示" />
</el-form-item>
<el-form-item label="备注" prop="memo">
<el-input v-model="formData.memo" clearable placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as ErrorCodeApi from '@/api/system/errorCode'
defineOptions({ name: 'SystemErrorCodeForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
// 表单参数
const formData = ref({
id: undefined,
code: undefined,
applicationName: '',
message: '',
memo: ''
})
// 表单校验
const formRules = reactive({
applicationName: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
code: [{ required: true, message: '错误码编码不能为空', trigger: 'blur' }],
message: [{ required: true, message: '错误码提示不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await ErrorCodeApi.getErrorCode(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as ErrorCodeApi.ErrorCodeVO
if (formType.value === 'create') {
await ErrorCodeApi.createErrorCode(data)
message.success(t('common.createSuccess'))
} else {
await ErrorCodeApi.updateErrorCode(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 表单重置 */
const resetForm = () => {
formData.value = {
id: undefined,
applicationName: '',
code: undefined,
message: '',
memo: ''
}
formRef.value?.resetFields()
}
</script>
<template>
<doc-alert title="异常处理(错误码)" url="https://doc.iocoder.cn/exception/" />
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="90px"
>
<el-form-item label="错误码类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择错误码类型" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_ERROR_CODE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
class="!w-240px"
/>
</el-select>
</el-form-item>
<el-form-item label="应用名" prop="applicationName">
<el-input
v-model="queryParams.applicationName"
placeholder="请输入应用名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="错误码编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入错误码编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="错误码提示" prop="message">
<el-input
v-model="queryParams.message"
placeholder="请输入错误码提示"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:error-code:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:error-code:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="类型" align="center" prop="type" width="80">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_ERROR_CODE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="应用名" align="center" prop="applicationName" width="200" />
<el-table-column label="错误码编码" align="center" prop="code" width="120" />
<el-table-column label="错误码提示" align="center" prop="message" width="300" />
<el-table-column label="备注" align="center" prop="memo" width="200" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" class-name="small-paddingfixed-width">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:error-code:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:error-code:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<ErrorCodeForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ErrorCodeApi from '@/api/system/errorCode'
import ErrorCodeForm from './ErrorCodeForm.vue'
defineOptions({ name: 'SystemErrorCode' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 遮罩层
const exportLoading = ref(false) // 导出遮罩层
const total = ref(0) // 总条数
const list = ref([]) // 错误码列表
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: undefined,
applicationName: undefined,
code: undefined,
message: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ErrorCodeApi.getErrorCodePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
await ErrorCodeApi.deleteErrorCode(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await ErrorCodeApi.excelErrorCode(queryParams)
download.excel(data, '错误码.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
......@@ -47,7 +47,7 @@
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:config:export']"
v-hasPermi="['infra:login-log:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
......@@ -85,7 +85,7 @@
link
type="primary"
@click="openDetail(scope.row)"
v-hasPermi="['infra:config:query']"
v-hasPermi="['infra:login-log:query']"
>
详情
</el-button>
......
......@@ -16,7 +16,8 @@ export const rules = reactive({
password: [required],
host: [required],
port: [required],
sslEnable: [required]
sslEnable: [required],
starttlsEnable: [required]
})
// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
......@@ -58,6 +59,15 @@ const crudSchemas = reactive<CrudSchema[]>([
}
},
{
label: '是否开启 STARTTLS',
field: 'starttlsEnable',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean',
form: {
component: 'Radio'
}
},
{
label: '创建时间',
field: 'createTime',
isForm: false,
......
......@@ -10,7 +10,7 @@
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:mail-account:create']"
v-hasPermi="['system:mail-template:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
......
......@@ -130,7 +130,7 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: 0,
id: undefined,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
......@@ -231,7 +231,7 @@ const getTree = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0,
id: undefined,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
......
......@@ -4,14 +4,14 @@
<el-descriptions-item label="日志主键" min-width="120">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="链路追踪">
<el-descriptions-item label="链路追踪" v-if="detailData.traceId">
{{ detailData.traceId }}
</el-descriptions-item>
<el-descriptions-item label="操作人编号">
{{ detailData.userId }}
</el-descriptions-item>
<el-descriptions-item label="操作人名字">
{{ detailData.userNickname }}
{{ detailData.userName }}
</el-descriptions-item>
<el-descriptions-item label="操作人 IP">
{{ detailData.userIp }}
......@@ -20,39 +20,25 @@
{{ detailData.userAgent }}
</el-descriptions-item>
<el-descriptions-item label="操作模块">
{{ detailData.module }}
{{ detailData.type }}
</el-descriptions-item>
<el-descriptions-item label="操作名">
{{ detailData.name }}
{{ detailData.subType }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.content" label="操作内容">
{{ detailData.content }}
<el-descriptions-item label="操作内容">
{{ detailData.action }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.exts" label="操作拓展参数">
{{ detailData.exts }}
<el-descriptions-item v-if="detailData.extra" label="操作拓展参数">
{{ detailData.extra }}
</el-descriptions-item>
<el-descriptions-item label="请求 URL">
{{ detailData.requestMethod }} {{ detailData.requestUrl }}
</el-descriptions-item>
<el-descriptions-item label="Java 方法名">
{{ detailData.javaMethod }}
</el-descriptions-item>
<el-descriptions-item label="Java 方法参数">
{{ detailData.javaMethodArgs }}
</el-descriptions-item>
<el-descriptions-item label="操作时间">
{{ formatDate(detailData.startTime) }}
</el-descriptions-item>
<el-descriptions-item label="执行时长">{{ detailData.duration }} ms</el-descriptions-item>
<el-descriptions-item label="操作结果">
<div v-if="detailData.resultCode === 0">正常</div>
<div v-else>失败({{ detailData.resultCode }})</div>
</el-descriptions-item>
<el-descriptions-item v-if="detailData.resultCode === 0" label="操作结果">
{{ detailData.resultData }}
{{ formatDate(detailData.createTime) }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.resultCode > 0" label="失败提示">
{{ detailData.resultMsg }}
<el-descriptions-item label="业务编号">
{{ detailData.bizId }}
</el-descriptions-item>
</el-descriptions>
</Dialog>
......
......@@ -10,58 +10,65 @@
:inline="true"
label-width="68px"
>
<el-form-item label="系统模块" prop="module">
<el-form-item label="操作人" prop="userId">
<el-select
v-model="queryParams.userId"
multiple
placeholder="请输入操作人员"
class="!w-240px"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item label="操作模块" prop="type">
<el-input
v-model="queryParams.module"
placeholder="请输入系统模块"
v-model="queryParams.type"
placeholder="请输入操作模块"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="操作人员" prop="userNickname">
<el-form-item label="操作模块" prop="subType">
<el-input
v-model="queryParams.userNickname"
placeholder="请输入操作人员"
v-model="queryParams.subType"
placeholder="请输入操作模块"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="操作类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择操作类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_OPERATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作状态" prop="success">
<el-select
v-model="queryParams.success"
placeholder="请选择操作状态"
<el-form-item label="操作内容" prop="action">
<el-input
v-model="queryParams.action"
placeholder="请输入操作名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
>
<el-option key="true" label="成功" :value="true" />
<el-option key="false" label="失败" :value="false" />
</el-select>
/>
</el-form-item>
<el-form-item label="操作时间" prop="startTime">
<el-form-item label="操作时间" prop="createTime">
<el-date-picker
v-model="queryParams.startTime"
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="业务编号" prop="bizId">
<el-input
v-model="queryParams.bizId"
placeholder="请输入业务编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
......@@ -73,7 +80,7 @@
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:config:export']"
v-hasPermi="['infra:operate-log:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
......@@ -84,39 +91,27 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="操作模块" align="center" prop="module" width="180" />
<el-table-column label="操作名" align="center" prop="name" width="180" />
<el-table-column label="操作类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_OPERATE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="操作人" align="center" prop="userNickname" />
<el-table-column label="操作结果" align="center" prop="status">
<template #default="scope">
<span>{{ scope.row.resultCode === 0 ? '成功' : '失败' }}</span>
</template>
</el-table-column>
<el-table-column label="日志编号" align="center" prop="id" width="100" />
<el-table-column label="操作人" align="center" prop="userName" width="120" />
<el-table-column label="操作模块" align="center" prop="type" width="120" />
<el-table-column label="操作名" align="center" prop="subType" width="160" />
<el-table-column label="操作内容" align="center" prop="action" />
<el-table-column
label="操作时间"
align="center"
prop="startTime"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="执行时长" align="center" prop="startTime">
<template #default="scope">
<span>{{ scope.row.duration }} ms</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<el-table-column label="业务编号" align="center" prop="bizId" width="120" />
<el-table-column label="IP" align="center" prop="userIp" width="120" />
<el-table-column label="操作" align="center" fixed="right" width="60">
<template #default="scope">
<el-button
link
type="primary"
@click="openDetail(scope.row)"
v-hasPermi="['infra:config:query']"
v-hasPermi="['infra:operate-log:query']"
>
详情
</el-button>
......@@ -136,11 +131,12 @@
<OperateLogDetail ref="detailRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as OperateLogApi from '@/api/system/operatelog'
import OperateLogDetail from './OperateLogDetail.vue'
import * as UserApi from '@/api/system/user'
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
defineOptions({ name: 'SystemOperateLog' })
......@@ -152,11 +148,12 @@ const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
module: undefined,
userNickname: undefined,
userId: undefined,
type: undefined,
success: undefined,
startTime: []
subType: undefined,
action: undefined,
createTime: [],
bizId: undefined
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
......@@ -207,7 +204,9 @@ const handleExport = async () => {
}
/** 初始化 **/
onMounted(() => {
getList()
onMounted(async () => {
await getList()
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
})
</script>
......@@ -41,7 +41,7 @@
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:notice:create']"
v-hasPermi="['system:post:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
......@@ -50,7 +50,7 @@
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:config:export']"
v-hasPermi="['system:post:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
......
......@@ -58,7 +58,7 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = reactive({
id: 0,
id: undefined,
name: '',
code: '',
menuIds: []
......@@ -126,7 +126,7 @@ const resetForm = () => {
menuExpand.value = false
// 重置表单
formData.value = {
id: 0,
id: undefined,
name: '',
code: '',
menuIds: []
......
......@@ -78,7 +78,7 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = reactive({
id: 0,
id: undefined,
name: '',
code: '',
dataScope: undefined,
......@@ -102,7 +102,9 @@ const open = async (row: RoleApi.RoleVO) => {
formData.name = row.name
formData.code = row.code
formData.dataScope = row.dataScope
row.dataScopeDeptIds?.forEach((deptId: number) => {
await nextTick()
// 需要在 DOM 渲染完成后,再设置选中状态
row.dataScopeDeptIds?.forEach((deptId: number): void => {
treeRef.value.setChecked(deptId, true, false)
})
}
......@@ -139,7 +141,7 @@ const resetForm = () => {
checkStrictly.value = true
// 重置表单
formData.value = {
id: 0,
id: undefined,
name: '',
code: '',
dataScope: undefined,
......
......@@ -59,11 +59,11 @@ const formData = ref({
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '角色标识不能为空', trigger: 'change' }],
sort: [{ required: true, message: '显示顺序不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
remark: [{ required: false, message: '备注不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
......
......@@ -87,7 +87,11 @@
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="角色编号" prop="id" />
<el-table-column align="center" label="角色名称" prop="name" />
<el-table-column align="center" label="角色类型" prop="type" />
<el-table-column label="角色类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_ROLE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column align="center" label="角色标识" prop="code" />
<el-table-column align="center" label="显示顺序" prop="sort" />
<el-table-column align="center" label="备注" prop="remark" />
......
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="敏感词" prop="name">
<el-input v-model="formData.name" placeholder="请输入敏感词" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="description">
<el-input v-model="formData.description" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="标签" prop="tags">
<el-select
v-model="formData.tags"
allow-create
filterable
multiple
placeholder="请选择文章标签"
style="width: 380px"
>
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemSensitiveWordForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: '',
status: CommonStatusEnum.ENABLE,
description: '',
tags: []
})
const formRules = reactive({
name: [{ required: true, message: '敏感词不能为空', trigger: 'blur' }],
tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const tagList = ref([]) // 标签数组
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await SensitiveWordApi.getSensitiveWord(id)
} finally {
formLoading.value = false
}
}
// 获得 Tag 标签列表
tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as SensitiveWordApi.SensitiveWordVO
if (formType.value === 'create') {
await SensitiveWordApi.createSensitiveWord(data)
message.success(t('common.createSuccess'))
} else {
await SensitiveWordApi.updateSensitiveWord(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
status: CommonStatusEnum.ENABLE,
description: '',
tags: []
}
formRef.value?.resetFields()
}
</script>
<template>
<Dialog v-model="dialogVisible" title="检测敏感词">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="文本" prop="text">
<el-input v-model="formData.text" placeholder="请输入测试文本" type="textarea" />
</el-form-item>
<el-form-item label="标签" prop="tags">
<el-select
v-model="formData.tags"
allow-create
filterable
multiple
placeholder="请选择标签"
style="width: 380px"
>
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">检 测</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
defineOptions({ name: 'SystemSensitiveWordTestForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
text: '',
tags: []
})
const formRules = reactive({
text: [{ required: true, message: '测试文本不能为空', trigger: 'blur' }],
tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const tagList = ref([]) // 标签数组
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
// 获得 Tag 标签列表
tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const form = formData.value as unknown as SensitiveWordApi.SensitiveWordTestReqVO
const data = await SensitiveWordApi.validateText(form)
if (data.length === 0) {
message.success('不包含敏感词!')
return
}
message.warning('包含敏感词:' + data.join(', '))
dialogVisible.value = false
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
text: '',
tags: []
}
formRef.value?.resetFields()
}
</script>
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="敏感词" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入敏感词"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="标签" prop="tag">
<el-select
v-model="queryParams.tag"
class="!w-240px"
clearable
placeholder="请选择标签"
@keyup.enter="handleQuery"
>
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="请选择启用状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
class="!w-240px"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button
v-hasPermi="['system:sensitive-word:create']"
plain
type="primary"
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button
v-hasPermi="['system:sensitive-word:export']"
:loading="exportLoading"
plain
type="success"
@click="handleExport"
>
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button>
<el-button plain type="warning" @click="openTestForm">
<Icon class="mr-5px" icon="ep:document-checked" />
测试
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="编号" prop="id" />
<el-table-column align="center" label="敏感词" prop="name" />
<el-table-column align="center" label="状态" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="描述" prop="description" />
<el-table-column align="center" label="标签" prop="tags">
<template #default="scope">
<el-tag
v-for="tag in scope.row.tags"
:key="tag"
:disable-transitions="true"
class="mr-5px"
>
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180"
/>
<el-table-column align="center" label="操作">
<template #default="scope">
<el-button
v-hasPermi="['infra:config:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['infra:config:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<SensitiveWordForm ref="formRef" @success="getList" />
<!-- 表单弹窗:测试敏感词 -->
<SensitiveWordTestForm ref="testFormRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
import SensitiveWordForm from './SensitiveWordForm.vue'
import SensitiveWordTestForm from './SensitiveWordTestForm.vue'
defineOptions({ name: 'SystemSensitiveWord' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
tag: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const tagList = ref([]) // 标签数组
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SensitiveWordApi.getSensitiveWordPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 测试敏感词按钮操作 */
const testFormRef = ref()
const openTestForm = () => {
testFormRef.value.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SensitiveWordApi.deleteSensitiveWord(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await SensitiveWordApi.exportSensitiveWord(queryParams)
download.excel(data, '敏感词.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 获得 Tag 标签列表
tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
})
</script>
......@@ -61,6 +61,7 @@ const updateSupport = ref(0) // 是否更新已经存在的用户数据
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
updateSupport.value = 0
fileList.value = []
resetForm()
}
......@@ -104,6 +105,8 @@ const submitFormSuccess = (response: any) => {
text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
}
message.alert(text)
formLoading.value = false
dialogVisible.value = false
// 发送操作成功的事件
emits('success')
}
......@@ -115,9 +118,10 @@ const submitFormError = (): void => {
}
/** 重置表单 */
const resetForm = () => {
const resetForm = async (): Promise<void> => {
// 重置上传状态和文件
formLoading.value = false
await nextTick()
uploadRef.value?.clearFiles()
}
......
......@@ -17,7 +17,6 @@ interface ImportMetaEnv {
readonly VITE_APP_DOCALERT_ENABLE: string
readonly VITE_BASE_URL: string
readonly VITE_UPLOAD_URL: string
readonly VITE_API_BASEPATH: string
readonly VITE_API_URL: string
readonly VITE_BASE_PATH: string
readonly VITE_DROP_DEBUGGER: string
......
......@@ -14,6 +14,9 @@ declare global {
type LocaleType = 'zh-CN' | 'en'
declare type TimeoutHandle = ReturnType<typeof setTimeout>
declare type IntervalHandle = ReturnType<typeof setInterval>
type AxiosHeaders =
| 'application/json'
| 'application/x-www-form-urlencoded'
......
......@@ -25,10 +25,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
root: root,
// 服务端渲染
server: {
// 是否开启 https
https: false,
// 端口号
port: env.VITE_PORT,
port: env.VITE_PORT, // 端口号
host: "0.0.0.0",
open: env.VITE_OPEN === 'true',
// 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment