Merge branch 'master-yly'

# Conflicts:
#	CLAUDE.md
This commit is contained in:
2026-03-18 16:43:40 +08:00
61 changed files with 3343 additions and 633 deletions

168
ruoyi-ui/CLAUDE.md Normal file
View File

@@ -0,0 +1,168 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
**数字支行辅助管理系统** - 基于 RuoYi-Vue 框架的银行客户网格化管理平台。
这是一个全栈项目:
- **前端**: `ruoyi-ui/` - Vue 2.6 + Element UI
- **后端**: `../ibs/` - Java Spring Boot (若依框架)
## 常用命令
### 开发
```bash
# 安装依赖(建议使用国内镜像)
npm install --registry=https://registry.npmmirror.com
# 启动开发服务器 (默认端口80)
npm run dev
# Node.js 版本兼容性问题的启动方式
npm run dev_t
```
### 构建
```bash
# 构建生产环境
npm run build:prod
# 构建测试环境
npm run build:stage
# 构建预发布环境
npm run build:pre
```
### 代码质量
```bash
# ESLint 检查
npm run lint
```
### Mock 服务
```bash
# 启动 Mock 服务器
npm run mock
```
## 项目架构
### 目录结构
```
ruoyi-ui/
├── src/
│ ├── api/ # API 接口层,按业务模块分组
│ ├── assets/ # 静态资源图片、样式、图标SVG
│ ├── components/ # 全局通用组件
│ ├── directive/ # Vue 自定义指令
│ ├── layout/ # 布局组件(侧边栏、头部等)
│ ├── map/ # 地图相关工具/配置
│ ├── plugins/ # 插件配置
│ ├── router/ # 路由配置
│ ├── store/ # Vuex 状态管理
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ ├── main.js # 入口文件
│ └── permission.js # 路由权限控制
├── public/
│ ├── baidu/ # 百度地图静态资源
│ └── index.html
├── mock/ # Mock 数据
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
└── .env.staging # 测试环境配置
```
### 核心业务模块
| 目录 | 功能 |
|------|------|
| `views/grid/` | 网格管理(创建、列表、地图、走访、绩效) |
| `views/customer/` | 客户管理360视图、建档、客群、标签 |
| `views/system/` | 系统管理(用户、角色、菜单、字典) |
| `views/monitor/` | 系统监控(日志、在线用户、服务监控) |
| `views/configure/` | 配置管理(级别配置、参数设置、模板) |
| `views/approveCenter/` | 审批中心 |
| `views/gridSearch/` | 网格搜索(管户、绩效、公海池) |
### API 层组织
API 请求按业务模块组织在 `src/api/` 目录:
- 每个模块导出具体的请求函数
- 使用 `src/utils/request.js` 中的 axios 实例
- 默认添加 Bearer Token 认证
- 统一错误处理401/500/601等状态码
### 状态管理 (Vuex)
Store 模块位于 `src/store/modules/`
- `app` - 应用状态(侧边栏、设备类型)
- `user` - 用户信息
- `permission` - 权限路由
- `settings` - 系统设置
- `dict` - 字典数据
- `tagsView` - 标签页视图
- `featuredAreas` - 特色区域
- `rate` - 汇率相关
使用 `vuex-persistedstate` 将 settings 持久化到 localStorage。
### 路由系统
- 路由分为 `constantRoutes`(静态路由)和 `dynamicRoutes`(动态路由)
- 动态路由基于用户权限permissions动态加载
- 使用 `src/permission.js` 进行路由守卫和权限控制
- 支持嵌套路由和路由元信息配置
### 百度地图集成
项目深度集成百度地图 API
- 静态资源位于 `public/baidu/`
- 地图相关 API 在 `src/api/grid/`
- 支持 WebGL 渲染、绘制管理器、热力图等
## 环境变量配置
| 变量 | 说明 |
|------|------|
| `VUE_APP_BASE_API` | 后端 API 基础路径 |
| `VUE_APP_MOCK_API` | Mock API 路径 |
| `VUE_APP_MOCK` | 是否启用 Mock |
| `VUE_APP_STAGE_URL` | 测试环境地址 |
| `VUE_APP_BAIDU_URL` | 百度地图服务地址 |
| `VUE_APP_SHARP_URL` | SHARP 服务地址 |
## 代码规范
- ESLint 配置基于 `plugin:vue/recommended`
- 使用单引号、2空格缩进
- 组件名使用 PascalCase
- pre-commit 钩子自动执行 lint-staged
## 全局组件
以下组件已全局注册,可直接使用:
- `Pagination` - 分页组件
- `RightToolbar` - 表格工具栏
- `Editor` - 富文本编辑器
- `FileUpload` - 文件上传
- `ImageUpload` - 图片上传
- `ImagePreview` - 图片预览
- `DictTag` - 字典标签
- `treeselect` - 树形选择器
- `DownloadBtn` - 下载按钮
## 全局方法
通过 `Vue.prototype` 挂载的全局方法:
- `getDicts()` - 获取字典数据
- `getConfigKey()` - 获取配置项
- `parseTime()` - 时间格式化
- `resetForm()` - 重置表单
- `download()` - 文件下载
- `handleTree()` - 处理树形数据

View File

@@ -287,6 +287,15 @@ export function getFeaturegrid(data) {
})
}
// 获取网格列表(用于客群创建,简化查询)
export function getSimpleGridList(data) {
return request({
url: `/draw/grid/simpleList`,
method: 'get',
params: data
})
}
// 删除网格
export function featureGridDelete(data) {
return request({

View File

@@ -0,0 +1,137 @@
import request from '@/utils/request'
// 查询客群列表
export function listCustGroup(query) {
return request({
url: '/group/cust/list',
method: 'get',
params: query
})
}
// 获取客群详情
export function getCustGroup(id) {
return request({
url: '/group/cust/' + id,
method: 'get'
})
}
// 异步创建客群(网格导入)
export function createCustGroupByGrid(data) {
return request({
url: '/group/cust/createByGrid',
method: 'post',
data: data
})
}
// 异步创建客群(模板导入)
export function createCustGroupByTemplate(data) {
return request({
url: '/group/cust/createByTemplate',
method: 'post',
data: data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 更新客群
export function updateCustGroup(data) {
return request({
url: '/group/cust/update',
method: 'post',
data: data
})
}
// 更新客群(网格导入)
export function updateCustGroupByGrid(data) {
return request({
url: '/group/cust/updateByGrid',
method: 'post',
data: data
})
}
// 更新客群(模板导入)
export function updateCustGroupByTemplate(data) {
return request({
url: '/group/cust/updateByTemplate',
method: 'post',
data: data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 轮询客群创建状态
export function getCreateStatus(id) {
return request({
url: '/group/cust/createStatus/' + id,
method: 'get'
})
}
// 客户信息模板下载
export function downloadTemplate() {
return request({
url: '/group/cust/download',
method: 'post',
responseType: 'blob'
})
}
// 删除客群
export function deleteCustGroup(idList) {
return request({
url: '/group/cust/delete',
method: 'post',
data: idList
})
}
// 手动移除客群客户
export function removeMembers(groupId, memberIds) {
return request({
url: '/group/cust/removeMembers',
method: 'post',
params: { groupId: groupId },
data: memberIds
})
}
// 检查客群名称是否存在
export function checkGroupNameExist(groupName) {
return request({
url: '/group/cust/checkName',
method: 'get',
params: { groupName: groupName }
})
}
// 根据网格类型获取客户经理列表
export function getManagerList(gridType) {
return request({
url: '/grid/cmpm/managerList',
method: 'get',
params: { gridType: gridType }
})
}
// 分页查询客群成员列表
export function listCustGroupMembers(groupId, query) {
return request({
url: '/group/member/list/' + groupId,
method: 'get',
params: query
})
}
// 查询地理网格列表(专用于客群创建)
export function getRegionGridListForGroup(query) {
return request({
url: '/grid/region/groupList',
method: 'get',
params: query
})
}

View File

@@ -257,4 +257,12 @@ export function warningCardNum(query) {
method: 'get',
params: query
})
}
// 查询所有预警类型
export function getAlterTypes() {
return request({
url: '/work/record/alter/types',
method: 'get'
})
}

View File

@@ -227,6 +227,17 @@ export const constantRoutes = [
}]
},
{
path: '/group/custGroup/detail',
component: Layout,
hidden: true,
children: [{
path: '/group/custGroup/detail',
name: 'CustGroupDetail',
meta: { title: '客群详情', activeMenu: '/group/custGroup' },
component: () => import('@/views/group/custGroup/detail')
}]
},
{
path: '/checklist/customerlist',
component: Layout,

View File

@@ -11,6 +11,7 @@ const user = {
permissions: [],
nickName: '',
userName: '',
deptId: '',
groupErr:{},
},

View File

@@ -2,9 +2,16 @@
<div class="customer-wrap">
<div>
<el-radio-group class="header-radio" v-model="selectedTab" @change="handleTabChange">
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
</template>
<template v-else>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
</template>
</el-radio-group>
<div class="customerMain">
<div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'">
@@ -317,6 +324,9 @@ export default {
},
computed: {
...mapGetters(['roles', 'userName']),
deptId() {
return this.$store.state.user.deptId
},
filtereDate() {
if (this.searchQuery) {
return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(this.searchQuery))
@@ -776,6 +786,12 @@ export default {
if (query.backUrl) {
this.selectedTab = query.selectedTab
this.queryParams.custPattern = query.selectedTab
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
this.selectedTab = deptIdStr.startsWith('875') ? '0' : '2'
this.queryParams.custPattern = this.selectedTab
}
},
mounted() {

View File

@@ -1,5 +1,22 @@
const commonCol = function (isCom, type) {
return [
// 当headId=875时需要隐藏的字段列表
const HIDDEN_PROPS_FOR_875 = [
'regionTopGridName', // 总行行政网格名称
'regionSecGridName', // 支行行政网格名称
'belongBranchName', // 行政网格归属支行
'belongOutletName', // 行政网格归属网点
'belongUserNameList', // 行政网格客户经理
'drawGridName', // 自绘地图网格名称
'drawBranchNames', // 自绘地图网格归属支行
'drawOutletNames', // 自绘地图网格归属网点
'drawUserNames', // 自绘地图网格客户经理
'virtualGridName', // 自建名单网格名称
'virtualBranchNames', // 自建名单网格归属支行
'virtualOutletNames', // 自建名单网格归属网点
'virtualUserNames', // 自建名单网格客户经理
]
const commonCol = function (isCom, type, headId) {
const allColumns = [
{
prop: 'regionTopGridName',
label: '总行行政网格名称',
@@ -101,9 +118,16 @@ const commonCol = function (isCom, type) {
showOverflowTooltip: true,
},
]
// 当headId=875时过滤掉指定的列
if (String(headId).startsWith('875')) {
return allColumns.filter(col => !HIDDEN_PROPS_FOR_875.includes(col.prop))
}
return allColumns
}
export const placeholderMap = (type) => ({
export const placeholderMap = (type, headId) => ({
'2': {
placeholder: '搜索企业名称/法人名字',
custPattern: '2',
@@ -138,7 +162,7 @@ export const placeholderMap = (type) => ({
type: 'myCustLevel',
// desc: '建档输入/取新华社数据',
},
...commonCol('2', type),
...commonCol('2', type, headId),
{
prop: 'lpName',
label: '法人姓名',
@@ -393,7 +417,7 @@ export const placeholderMap = (type) => ({
type: 'myCustIdsn',
// desc: '建档输入/取新华社数据',
},
...commonCol('1', type),
...commonCol('1', type, headId),
{
prop: 'lpName',
label: '经营者姓名',
@@ -634,7 +658,7 @@ export const placeholderMap = (type) => ({
type: 'myCustLevel',
// desc: '取大信贷数据',
},
...commonCol('0', type),
...commonCol('0', type, headId),
{
prop: 'custPhone',
label: '联系方式',

View File

@@ -2,9 +2,16 @@
<div class="customer-wrap">
<div>
<el-radio-group class="header-radio" v-model="selectedTab" @change="handleTabChange">
<el-radio-button label="2" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button> // :disabled="isPrivate"
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
</template>
<template v-else>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
</template>
</el-radio-group>
<div class="customerMain">
<div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'">
@@ -283,8 +290,9 @@ export default {
const type = this.roles.includes('headPublic') ? 'isPublic' :
this.roles.includes('headPrivate') ? 'isPrivate' :
this.roles.includes('headOps') ? 'isOps' : ""
this.tableColoumns = placeholderMap(type)[val].tableColoumns
this.placeholder = placeholderMap(type)[val].placeholder
const headId = this.deptId
this.tableColoumns = placeholderMap(type, headId)[val].tableColoumns
this.placeholder = placeholderMap(type, headId)[val].placeholder
this.queryParams.custPattern = val
this.searchColoumns = this.getSearchColoumns()
}
@@ -310,6 +318,9 @@ export default {
},
computed: {
...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
filtereDate() {
if (this.searchQuery) {
return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(this.searchQuery))
@@ -777,6 +788,12 @@ export default {
if (query.backUrl) {
this.selectedTab = query.selectedTab
this.queryParams.custPattern = query.selectedTab
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
this.selectedTab = deptIdStr.startsWith('875') ? '0' : '2'
this.queryParams.custPattern = this.selectedTab
}
},
mounted() {

View File

@@ -0,0 +1,680 @@
<template>
<el-dialog
:title="isEdit ? '编辑客群' : '创建客群'"
:visible.sync="visible"
width="700px"
:before-close="handleClose"
append-to-body
>
<el-alert
v-if="isEdit && isProcessing"
title="客群正在处理中,请等待处理完成后再进行编辑"
type="warning"
:closable="false"
style="margin-bottom: 15px"
/>
<el-form ref="form" :model="form" :rules="rules" label-width="120px" v-loading="submitting">
<el-form-item label="客群名称" prop="groupName">
<el-input v-model="form.groupName" placeholder="请输入客群名称" maxlength="50" />
</el-form-item>
<el-form-item label="客群模式" prop="groupMode">
<el-radio-group v-model="form.groupMode" :disabled="isEdit && form.createStatus === '1'">
<el-radio label="0">静态客群</el-radio>
<el-radio label="1">动态客群</el-radio>
</el-radio-group>
<div class="form-tip">
静态客群创建后客户列表固定除非手动更新
<br />
动态客群系统定期根据原始条件自动刷新客户列表
</div>
</el-form-item>
<el-form-item label="有效期" prop="validTime">
<el-date-picker
v-model="form.validTime"
type="datetime"
placeholder="选择有效期截止时间"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
/>
<div class="form-tip">留空表示永久有效</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="2" />
</el-form-item>
<el-form-item label="开启共享">
<el-switch v-model="shareEnabled" />
</el-form-item>
<el-form-item label="可见部门" v-if="shareEnabled">
<el-select
v-model="form.shareDeptIdList"
multiple
placeholder="请选择可见部门"
style="width: 100%"
>
<el-option
v-for="dept in deptOptions"
:key="dept.deptId"
:label="dept.deptName"
:value="dept.deptId"
/>
</el-select>
</el-form-item>
<el-form-item label="创建方式" prop="createMode">
<el-radio-group v-model="form.createMode" :disabled="isEdit">
<el-radio label="1">模板导入</el-radio>
<el-radio label="2">绩效网格</el-radio>
<el-radio label="3">地理网格</el-radio>
<el-radio label="4">绘制网格</el-radio>
</el-radio-group>
</el-form-item>
<!-- 模板导入 -->
<template v-if="form.createMode === '1'">
<el-form-item label="客户文件" prop="file">
<el-upload
ref="upload"
:action="uploadUrl"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-change="handleFileChange"
:auto-upload="false"
:show-file-list="true"
:limit="1"
accept=".xlsx,.xls"
>
<el-button slot="trigger" size="small" icon="el-icon-upload">选择文件</el-button>
<el-button size="small" type="text" @click.stop="downloadTemplate" style="margin-left: 15px">
下载模板
</el-button>
<div slot="tip" class="el-upload__tip">
仅支持Excel文件文件大小不超过10MB
</div>
</el-upload>
</el-form-item>
</template>
<!-- 绩效网格 -->
<template v-if="form.createMode === '2'">
<el-form-item label="业务类型" prop="cmpmBizType">
<el-select v-model="gridForm.cmpmBizType" placeholder="请选择业务类型" style="width: 100%" @change="handleCmpmBizTypeChange">
<el-option label="零售" value="retail" />
<el-option label="对公" value="corporate" />
<el-option label="对公账户" value="corporate_account" />
</el-select>
</el-form-item>
<el-form-item label="客户经理" prop="userNames">
<el-select
v-model="gridForm.userNames"
multiple
filterable
placeholder="请选择客户经理"
style="width: 100%"
>
<el-option
v-for="user in userOptions"
:key="user.userName"
:label="user.nickName"
:value="user.userName"
/>
</el-select>
</el-form-item>
</template>
<!-- 地理网格 -->
<template v-if="form.createMode === '3'">
<el-form-item label="网格级别">
<el-radio-group v-model="regionQuery.gridLevel">
<el-radio label="1">总行行政网格</el-radio>
<el-radio label="2">支行行政网格</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="责任类型">
<el-select v-model="regionQuery.gridDutyType" placeholder="请选择责任类型" style="width: 100%" clearable>
<el-option label="责任网格" value="1" />
<el-option label="竞争网格" value="2" />
</el-select>
</el-form-item>
<el-form-item label="网格名称">
<el-input v-model="regionQuery.gridName" placeholder="请输入网格名称(可选)" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadRegionGridOptions" :loading="regionLoading">查询网格</el-button>
<el-button icon="el-icon-refresh" @click="resetRegionQuery">重置条件</el-button>
</el-form-item>
<el-form-item label="选择网格">
<el-select
v-model="gridForm.regionGridIds"
multiple
filterable
collapse-tags
placeholder="请先查询网格,然后选择"
style="width: 100%"
>
<el-option
v-for="grid in regionGridOptions"
:key="grid.gridId"
:label="grid.gridName"
:value="grid.gridId"
/>
</el-select>
</el-form-item>
</template>
<!-- 绘制网格 -->
<template v-if="form.createMode === '4'">
<el-form-item label="绘制网格" prop="drawGridIds">
<el-select
v-model="gridForm.drawGridIds"
multiple
filterable
placeholder="请选择绘制网格"
style="width: 100%"
>
<el-option
v-for="grid in drawGridOptions"
:key="grid.gridId"
:label="grid.gridName"
:value="grid.gridId"
/>
</el-select>
</el-form-item>
</template>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" :loading="submitting" :disabled="isProcessing" @click="handleSubmit">
{{ submitButtonText }}
</el-button>
</div>
</el-dialog>
</template>
<script>
import {
createCustGroupByGrid,
createCustGroupByTemplate,
updateCustGroupByGrid,
updateCustGroupByTemplate,
downloadTemplate,
getManagerList,
getRegionGridListForGroup
} from '@/api/group/custGroup'
import { getToken } from '@/utils/auth'
import { listUser } from '@/api/system/user'
import { listDept } from '@/api/system/dept'
import { getSimpleGridList } from '@/api/grid/list/gridlist'
export default {
name: 'CreateDialog',
props: {
visible: {
type: Boolean,
default: false
},
groupData: {
type: Object,
default: () => ({})
},
isEdit: {
type: Boolean,
default: false
}
},
data() {
return {
// 提交中状态
submitting: false,
// 上传地址
uploadUrl: process.env.VUE_APP_BASE_API + '/group/cust/createByTemplate',
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
// 处理中提示
processTipVisible: false
}
},
computed: {
// 是否正在处理中(创建或更新)
isProcessing() {
return this.form.createStatus === '0'
},
// 提交按钮文本
submitButtonText() {
if (this.submitting) return '提交中...'
if (this.isEdit && this.isProcessing) return '客群处理中,请稍后...'
return '确 定'
}
},
data() {
return {
// 提交中状态
submitting: false,
// 上传地址
uploadUrl: process.env.VUE_APP_BASE_API + '/group/cust/createByTemplate',
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
// 处理中提示
processTipVisible: false,
// 表单数据
form: {
id: null,
groupName: null,
groupMode: '0',
createMode: '1',
groupStatus: '0',
shareEnabled: 0,
shareDeptIdList: [],
remark: null,
validTime: null
},
// 共享开关
shareEnabled: false,
// 网格表单数据
gridForm: {
gridType: '0',
cmpmBizType: null,
userNames: [],
regionGridIds: [],
drawGridIds: []
},
// 上传文件
uploadFile: null,
// 表单验证规则
rules: {
groupName: [
{ required: true, message: '请输入客群名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
groupMode: [{ required: true, message: '请选择客群模式', trigger: 'change' }],
createMode: [{ required: true, message: '请选择创建方式', trigger: 'change' }]
},
// 部门选项
deptOptions: [],
// 用户选项
userOptions: [],
// 地理网格选项
regionGridOptions: [],
// 地理网格查询条件
regionQuery: {
gridLevel: '1',
gridDutyType: null,
gridName: null
},
// 地理网格加载状态
regionLoading: false,
// 绘制网格选项
drawGridOptions: []
}
},
watch: {
visible(val) {
if (val) {
this.init()
}
},
groupData: {
handler(val) {
if (val && Object.keys(val).length > 0) {
this.form = { ...val }
this.shareEnabled = val.shareEnabled === 1
// 解析 shareDeptIds 字符串为 shareDeptIdList 数组
if (val.shareDeptIds) {
this.form.shareDeptIdList = val.shareDeptIds.split(',').filter(id => id)
} else {
this.form.shareDeptIdList = []
}
// 反显网格数据(需要在 form.createMode watch 之后执行)
this.$nextTick(() => {
this.restoreGridData(val)
})
}
},
immediate: true
},
'form.createMode'(val) {
// 重置网格表单
this.gridForm = {
gridType: val === '2' ? '0' : val === '3' ? '1' : '2',
regionGridIds: [],
drawGridIds: []
}
// 切换到地理网格模式时,重置查询条件
if (val === '3') {
this.resetRegionQuery()
}
// 切换到绘制网格模式时,加载绘制网格列表
if (val === '4') {
this.loadDrawGridOptions()
}
},
'gridForm.cmpmBizType'(val) {
// 当业务类型改变时,清空已选择的客户经理
if (val) {
this.gridForm.userNames = []
this.loadManagerOptions()
}
},
'form.createStatus'(val) {
// 监听创建状态变化,显示提示
if (this.isEdit && val === '0') {
this.processTipVisible = true
} else if (this.processTipVisible) {
this.processTipVisible = false
}
}
},
created() {
this.loadDeptOptions()
},
methods: {
/** 初始化 */
init() {
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
// 编辑模式下,恢复网格数据
if (this.isEdit && this.groupData && Object.keys(this.groupData).length > 0) {
this.restoreGridData(this.groupData)
}
})
},
/** 反显网格数据 */
restoreGridData(data) {
if (!this.isEdit) return
const createMode = String(data.createMode)
// 根据创建方式反显网格数据
if (createMode === '2' && data.gridType === '0') {
// 绩效网格
this.gridForm.gridType = '0'
this.gridForm.cmpmBizType = data.cmpmBizType
if (data.gridUserNames) {
this.gridForm.userNames = data.gridUserNames.split(',').filter(n => n)
// 加载客户经理选项
this.loadManagerOptions()
}
} else if (createMode === '3' && data.gridType === '1') {
// 地理网格 - 需要查询所有网格以便正确反显
this.gridForm.gridType = '1'
// 设置默认查询条件(获取所有网格)
this.regionQuery = {
gridLevel: '1',
gridDutyType: null,
gridName: null
}
if (data.regionGridIds) {
this.gridForm.regionGridIds = data.regionGridIds.split(',').map(id => parseInt(id)).filter(id => id)
// 加载地理网格选项(无查询条件,获取全部)
this.loadRegionGridOptions()
}
} else if (createMode === '4' && data.gridType === '2') {
// 绘制网格
this.gridForm.gridType = '2'
if (data.drawGridIds) {
this.gridForm.drawGridIds = data.drawGridIds.split(',').map(id => parseInt(id)).filter(id => id)
// 加载绘制网格选项
this.loadDrawGridOptions()
}
}
},
/** 加载部门选项 */
loadDeptOptions() {
listDept().then(response => {
this.deptOptions = response.data || []
}).catch(() => {
this.deptOptions = []
})
},
/** 加载用户选项 */
loadUserOptions() {
listUser().then(response => {
this.userOptions = response.rows || []
}).catch(() => {
this.userOptions = []
})
},
/** 根据业务类型加载客户经理选项 */
loadManagerOptions() {
if (!this.gridForm.cmpmBizType) {
this.userOptions = []
return
}
getManagerList(this.gridForm.cmpmBizType).then(response => {
this.userOptions = response.data || []
}).catch(() => {
this.userOptions = []
})
},
/** 业务类型改变处理 */
handleCmpmBizTypeChange(val) {
// 清空已选择的客户经理
this.gridForm.userNames = []
},
/** 加载地理网格选项 */
loadRegionGridOptions() {
this.regionLoading = true
const params = {
gridLevel: this.regionQuery.gridLevel
}
if (this.regionQuery.gridDutyType) {
params.gridDutyType = this.regionQuery.gridDutyType
}
if (this.regionQuery.gridName) {
params.gridName = this.regionQuery.gridName
}
getRegionGridListForGroup(params).then(response => {
this.regionGridOptions = response.data || []
}).catch(() => {
this.regionGridOptions = []
}).finally(() => {
this.regionLoading = false
})
},
/** 重置地理网格查询条件 */
resetRegionQuery() {
this.regionQuery = {
gridLevel: '1',
gridDutyType: null,
gridName: null
}
this.gridForm.regionGridIds = []
},
/** 加载绘制网格选项 */
loadDrawGridOptions() {
getSimpleGridList().then(response => {
this.drawGridOptions = response.data || []
}).catch(() => {
this.drawGridOptions = []
})
},
/** 文件改变 */
handleFileChange(file) {
this.uploadFile = file.raw
},
/** 上传成功 */
handleUploadSuccess(response) {
this.$modal.msgSuccess('创建成功')
this.submitting = false
this.$emit('submit', { id: response.data })
this.handleClose()
},
/** 上传失败 */
handleUploadError() {
this.$modal.msgError('创建失败')
this.submitting = false
},
/** 下载模板 */
downloadTemplate() {
downloadTemplate().then(response => {
const url = window.URL.createObjectURL(new Blob([response]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', '客户信息模板.xlsx')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
},
/** 提交表单 */
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
// 处理共享设置
this.form.shareEnabled = this.shareEnabled ? 1 : 0
// 转换 shareDeptIdList 数组为 shareDeptIds 逗号分隔字符串
this.form.shareDeptIds = this.form.shareDeptIdList.join(',')
// 根据创建方式处理提交数据
if (this.form.createMode === '1') {
this.handleTemplateSubmit()
} else {
this.handleGridSubmit()
}
}
})
},
/** 模板导入提交 */
handleTemplateSubmit() {
if (!this.uploadFile && !this.isEdit) {
this.$modal.msgWarning('请选择要上传的文件')
return
}
this.submitting = true
// 构建表单数据
const formData = new FormData()
formData.append('dto', JSON.stringify(this.form))
if (this.uploadFile) {
formData.append('file', this.uploadFile)
}
if (this.isEdit) {
// 编辑模式:重新导入模板文件
updateCustGroupByTemplate(formData).then(response => {
this.$modal.msgSuccess('客群更新中,请稍后刷新查看')
this.submitting = false
this.$emit('submit', { id: this.form.id })
this.handleClose()
}).catch(() => {
this.submitting = false
})
} else {
// 新增模式
createCustGroupByTemplate(formData).then(response => {
this.$modal.msgSuccess('客群创建中,请稍后刷新查看')
this.submitting = false
this.$emit('submit', { id: response.data })
this.handleClose()
}).catch(() => {
this.submitting = false
})
}
},
/** 网格导入提交 */
handleGridSubmit() {
this.submitting = true
// 构建提交数据
const submitData = {
custGroup: { ...this.form },
gridType: this.form.createMode === '2' ? '0' : this.form.createMode === '3' ? '1' : '2',
cmpmBizType: this.gridForm.cmpmBizType,
userNames: this.gridForm.userNames,
regionGridIds: this.gridForm.regionGridIds,
drawGridIds: this.gridForm.drawGridIds
}
if (this.isEdit) {
// 编辑模式:重新导入网格
updateCustGroupByGrid(submitData).then(response => {
// 使用后端返回的消息
const msg = response.msg || '客群更新成功'
if (msg.includes('更新中')) {
this.$modal.msgSuccess('客群更新中,请稍后刷新查看')
} else {
this.$modal.msgSuccess(msg)
}
this.submitting = false
this.$emit('submit', { id: this.form.id })
this.handleClose()
}).catch(() => {
this.submitting = false
})
} else {
// 新增模式
createCustGroupByGrid(submitData).then(response => {
this.$modal.msgSuccess('客群创建中,请稍后刷新查看')
this.submitting = false
this.$emit('submit', { id: response.data })
this.handleClose()
}).catch(() => {
this.submitting = false
})
}
},
/** 关闭弹窗 */
handleClose() {
this.$emit('update:visible', false)
this.resetForm()
},
/** 重置表单 */
resetForm() {
this.form = {
id: null,
groupName: null,
groupMode: '0',
createMode: '1',
groupStatus: '0',
shareEnabled: 0,
shareDeptIdList: [],
remark: null,
validTime: null
}
this.shareEnabled = false
this.gridForm = {
gridType: '0',
cmpmBizType: null,
userNames: [],
regionGridIds: [],
drawGridIds: []
}
this.uploadFile = null
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
})
}
}
}
</script>
<style scoped>
.form-tip {
font-size: 12px;
color: #909399;
line-height: 1.5;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<div class="customer-wrap detail-wrap">
<!-- 页面头部 -->
<div class="page-header">
<el-button icon="el-icon-arrow-left" size="small" @click="goBack">返回</el-button>
<span class="page-title">客群客户列表</span>
</div>
<!-- 搜索区域 -->
<div class="search-area">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="客户类型" prop="custType">
<el-select v-model="queryParams.custType" placeholder="请选择" clearable style="width: 150px">
<el-option label="个人" value="0" />
<el-option label="商户" value="1" />
<el-option label="企业" value="2" />
</el-select>
</el-form-item>
<el-form-item label="客户姓名" prop="custName">
<el-input
v-model="queryParams.custName"
placeholder="请输入客户姓名"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格区域 -->
<div class="main_table">
<el-table v-loading="loading" :data="memberList">
<el-table-column label="序号" type="index" width="60" align="center" :index="indexMethod" />
<el-table-column label="客户类型" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.custType === '0'" size="small">个人</el-tag>
<el-tag v-else-if="scope.row.custType === '1'" size="small" type="warning">商户</el-tag>
<el-tag v-else-if="scope.row.custType === '2'" size="small" type="success">企业</el-tag>
</template>
</el-table-column>
<el-table-column label="客户号" prop="custId" width="150" />
<el-table-column label="客户姓名" prop="custName" width="120" />
<el-table-column label="身份证号" prop="custIdc" show-overflow-tooltip />
<el-table-column label="统信码" prop="socialCreditCode" show-overflow-tooltip />
<el-table-column label="添加时间" prop="createTime" width="180" />
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
</template>
<script>
import { listCustGroupMembers } from '@/api/group/custGroup'
export default {
name: 'CustGroupDetail',
data() {
return {
loading: false,
groupId: null,
memberList: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
custType: null,
custName: null
}
}
},
created() {
this.groupId = this.$route.query.groupId
if (this.groupId) {
this.getList()
} else {
this.$modal.msgError('缺少客群ID参数')
this.goBack()
}
},
watch: {
'$route.query.groupId'(newGroupId) {
if (newGroupId && newGroupId !== this.groupId) {
this.groupId = newGroupId
this.queryParams.pageNum = 1
this.getList()
}
}
},
methods: {
/** 查询客户列表 */
getList(param) {
if (param) {
this.queryParams.pageNum = param.page
this.queryParams.pageSize = param.limit
}
this.loading = true
listCustGroupMembers(this.groupId, this.queryParams).then(response => {
this.memberList = response.rows
this.total = response.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 搜索 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
/** 返回 */
goBack() {
this.$router.push({ path: '/group/custGroup' })
},
/** 序号计算方法 */
indexMethod(index) {
return (this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1
}
}
}
</script>
<style lang="scss" scoped>
.customer-wrap {
background-color: #ffffff;
overflow: hidden;
box-shadow: 0 3px 8px 0 #00000017;
border-radius: 16px 16px 0 0;
padding: 24px 30px;
&.detail-wrap {
.page-header {
display: flex;
align-items: center;
margin-bottom: 24px;
.page-title {
font-size: 16px;
font-weight: 500;
color: #222222;
margin-left: 16px;
}
}
}
.search-area {
padding: 16px 0;
border-bottom: 1px solid #ebebeb;
margin-bottom: 16px;
.el-form {
margin-bottom: -8px;
.el-form-item {
margin-bottom: 8px;
}
}
}
.main_table {
::v-deep .el-pagination {
margin-top: 15px;
}
}
}
</style>

View File

@@ -0,0 +1,288 @@
<template>
<div class="customer-wrap">
<!-- 搜索区域 -->
<div class="search-area" v-show="showSearch">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="客群名称" prop="groupName">
<el-input
v-model="queryParams.groupName"
placeholder="请输入客群名称"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="客群模式" prop="groupMode">
<el-select v-model="queryParams.groupMode" placeholder="请选择" clearable style="width: 150px">
<el-option label="静态" value="0" />
<el-option label="动态" value="1" />
</el-select>
</el-form-item>
<el-form-item label="创建方式" prop="createMode">
<el-select v-model="queryParams.createMode" placeholder="请选择" clearable style="width: 150px">
<el-option label="模板导入" value="1" />
<el-option label="绩效网格" value="2" />
<el-option label="地理网格" value="3" />
<el-option label="绘制网格" value="4" />
</el-select>
</el-form-item>
<el-form-item label="客群状态" prop="groupStatus">
<el-select v-model="queryParams.groupStatus" placeholder="请选择" clearable style="width: 150px">
<el-option label="正常" value="0" />
<el-option label="已禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 操作栏 -->
<section class="operate-cnt">
<div class="operate-left">
<el-button type="primary" icon="el-icon-plus" size="small" @click="handleAdd">新增</el-button>
<el-button type="danger" icon="el-icon-delete" size="small" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</section>
<!-- 表格区域 -->
<div class="main_table">
<el-table v-loading="loading" :data="groupList" @selection-change="handleSelectionChange" style="width: 100%" max-height="625">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="客群名称" prop="groupName" min-width="180" show-overflow-tooltip />
<el-table-column label="客群模式" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.groupMode === '0'" type="info" size="small">静态</el-tag>
<el-tag v-else-if="scope.row.groupMode === '1'" type="success" size="small">动态</el-tag>
</template>
</el-table-column>
<el-table-column label="创建方式" align="center" width="120">
<template slot-scope="scope">
<span v-if="scope.row.createMode === '1'">模板导入</span>
<span v-else-if="scope.row.createMode === '2'">绩效网格</span>
<span v-else-if="scope.row.createMode === '3'">地理网格</span>
<span v-else-if="scope.row.createMode === '4'">绘制网格</span>
</template>
</el-table-column>
<el-table-column label="客户数量" align="center" prop="custCount" width="100" />
<el-table-column label="客群状态" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.groupStatus === '0'" type="success" size="small">正常</el-tag>
<el-tag v-else-if="scope.row.groupStatus === '1'" type="danger" size="small">已禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="创建者" prop="nickName" width="120" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="180" />
<el-table-column label="操作" align="center" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 创建/编辑客群弹窗 -->
<create-dialog
:visible.sync="dialogVisible"
:group-data="form"
:is-edit="isEdit"
@submit="handleSubmit"
/>
</div>
</template>
<script>
import { listCustGroup, getCustGroup, deleteCustGroup } from '@/api/group/custGroup'
import CreateDialog from './components/create-dialog'
export default {
name: 'CustGroup',
components: { CreateDialog },
data() {
return {
// 加载状态
loading: false,
// 显示搜索
showSearch: true,
// 选中ID数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 客群列表
groupList: [],
// 弹窗显示
dialogVisible: false,
// 是否编辑
isEdit: false,
// 表单数据
form: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
groupName: null,
groupMode: null,
createMode: null,
groupStatus: null
}
}
},
created() {
this.getList()
},
methods: {
/** 查询客群列表 */
getList() {
this.loading = true
listCustGroup(this.queryParams).then(response => {
this.groupList = response.rows
this.total = response.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.isEdit = false
this.dialogVisible = true
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids[0]
getCustGroup(id).then(response => {
this.form = response.data
this.isEdit = true
this.dialogVisible = true
})
},
/** 查看按钮操作 */
handleView(row) {
this.$router.push({
path: '/group/custGroup/detail',
query: { groupId: row.id }
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id ? [row.id] : this.ids
this.$modal.confirm('是否确认删除选中的客群?').then(() => {
return deleteCustGroup(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 提交表单 */
handleSubmit() {
this.dialogVisible = false
this.getList()
},
/** 表单重置 */
reset() {
this.form = {
id: null,
groupName: null,
groupMode: '0',
createMode: null,
groupStatus: '0',
shareEnabled: 0,
shareDeptIdList: [],
remark: null,
validTime: null
}
}
}
}
</script>
<style lang="scss" scoped>
.customer-wrap {
background-color: #ffffff;
overflow: hidden;
box-shadow: 0 3px 8px 0 #00000017;
border-radius: 16px 16px 0 0;
padding: 24px 30px;
.search-area {
padding: 16px 0;
border-bottom: 1px solid #ebebeb;
margin-bottom: 16px;
.el-form {
margin-bottom: -8px;
.el-form-item {
margin-bottom: 8px;
}
}
}
.operate-cnt {
display: flex;
justify-content: space-between;
align-items: center;
margin: 24px 0 16px 0;
.operate-left {
display: flex;
gap: 8px;
}
.el-button {
border-radius: 4px;
}
}
.main_table {
::v-deep .el-pagination {
margin-top: 15px;
}
}
}
</style>

View File

@@ -20,6 +20,7 @@
<span style="font-size: 14px;">较上月变动</span>
<span v-if="String(item.inc).includes('-')" style=" font-size: 14px;color: #00B453">{{ changeData(item.inc)
}}<i class="el-icon-caret-bottom"></i></span>
<span v-else-if="item.inc == 0 || item.inc === '0'" style=" font-size: 14px;color: #409EFF">{{ changeData(item.inc) }}</span>
<span v-else style=" font-size: 14px;color: #EF3F35">{{ changeData(item.inc) }}<i
class="el-icon-caret-top"></i></span>
</div>
@@ -43,6 +44,11 @@
@click="goToCustManager(item.itemNm, 'fall')">
{{ item.curAmt }}<i class="el-icon-caret-bottom"></i>
</span>
<span v-else-if="item.curAmt == 0 || item.curAmt === '0'"
style="font-size: 14px; color: #409EFF; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'rise')">
{{ item.curAmt }}
</span>
<span v-else
style="font-size: 14px; color: #EF3F35; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'rise')">
@@ -60,7 +66,7 @@
<el-radio-button label="4">预警任务</el-radio-button>
<el-radio-button label="5">二次走访提醒</el-radio-button>
<el-radio-button label="6">走访资源提醒</el-radio-button>
<el-radio-button label="7">营销任务</el-radio-button>
<el-radio-button label="7" v-if="!shouldHideFor875">营销任务</el-radio-button>
</el-radio-group>
</div>
<el-table v-if="selectedTab==='3'" key="3" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
@@ -152,7 +158,7 @@
</template>
</el-table-column>
</el-table>
<el-table v-if="selectedTab==='7'" key="7" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
<el-table v-if="selectedTab==='7' && !shouldHideFor875" key="7" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
<el-table-column label="营销任务" width="200" prop="marketTaskName" min-width="100" show-overflow-tooltip />
<el-table-column label="客户姓名" prop="custName" min-width="100" show-overflow-tooltip />
<el-table-column label="客户号" width="200" prop="custId" min-width="140" show-overflow-tooltip />
@@ -194,9 +200,9 @@
<el-table-column label="结束时间" width="200" prop="endTime" min-width="100" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
</el-table>
<el-pagination @size-change="handleAgentSizeChange" @current-change="handleAgentCurrentChange"
class="warnPagination" :page-sizes="[5, 10, 20, 30]" :page-size="agentPageSize"
layout="->,total,sizes,prev,pager,next" :total="agentTotal" :current-page="agentPageNum"></el-pagination>
<el-pagination @current-change="handleAgentCurrentChange"
class="warnPagination" :page-size="5"
layout="->,total,prev,pager,next" :total="agentTotal" :current-page="agentPageNum"></el-pagination>
</div>
</div>
<div class="page-vr">
@@ -210,7 +216,7 @@
</span>
</p>
<ul class="operate-cnt">
<li v-for="(it, ind) in optArr" :key="ind" class="setLi" @mouseenter="onMouseOver(ind, true)"
<li v-for="(it, ind) in filteredOptArr" :key="ind" class="setLi" @mouseenter="onMouseOver(ind, true)"
@mouseleave="onMouseOver(ind, false)">
<div class="noSetting" v-if="it.setShow">
<svg-icon :icon-class="getCurrentImg(ind)" :class="isSetting ? 'svg-icon-imgSetting' : 'svg-icon-img'"
@@ -230,7 +236,7 @@
</li>
</ul>
</div>
<div class="page-vr-middle page-common-wrap no-padding-cnt page-vr-top" id="yjxxMain">
<div class="page-vr-middle page-common-wrap no-padding-cnt page-vr-top" id="yjxxMain" v-if="!shouldHideFor875">
<p class="page-title page-btm">预警信息</p>
<ul class="common-ul">
<li v-for="(it, ind) in warnArr" :key="ind" style="cursor: pointer;" @click="handleWarn(it)" class="yjxxLi">
@@ -857,6 +863,27 @@ export default {
},
showposition() {
return this.userName.slice(0, 3) === '875'
},
// headId为875时隐藏预警信息和营销任务
shouldHideFor875() {
return this.userName && (this.userName + '').substring(0, 3) === '875'
},
// headId为875时便捷操作只显示快速入门
filteredOptArr() {
// 当deptId以875开头时只保留快速入门
if (this.deptId && String(this.deptId).startsWith('875')) {
return this.optArr.filter(item => item.name === '快速入门')
}
return this.optArr
}
},
watch: {
selectedTab(newVal, oldVal) {
// 切换tab时重置分页为第1页
if (newVal !== oldVal) {
this.pageNum = 1
this.agentPageNum = 1
}
}
},
mounted() {
@@ -1135,8 +1162,7 @@ export default {
this.getData()
},
handleAgentCurrentChange(val) {
// this.agentPageNum = val
// this.initBranchList()
this.agentPageNum = val
this.pageNum = val
this.getData()
},
@@ -1565,6 +1591,8 @@ p {
.page-vr {
width: 24%;
margin-left: 1%;
display: flex;
flex-direction: column;
}
.page-title {
@@ -1602,7 +1630,11 @@ p {
}
.page-vr-middle {
margin: 22px 0;
margin-bottom: 22px;
}
.page-vr-top {
margin-bottom: 22px;
}
.page-vl-top {
@@ -1873,7 +1905,7 @@ p {
.page-vr-btm {
height: 350px;
overflow-y: scroll;
margin-bottom: 30px;
margin-bottom: 22px;
}
::v-deep .el-badge__content {
@@ -1943,8 +1975,10 @@ p {
}
.url-box {
max-height: 210px;
flex: 1;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.yjxxLi {

View File

@@ -6,24 +6,46 @@
v-model="selectedTab"
@input="handleChange"
>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button
>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button
>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button
>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button
>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button
>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button
>
</template>
<template v-else>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button
>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button
>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button
>
</template>
</el-radio-group>
<div class="searchForm">
<el-form
@@ -403,6 +425,9 @@ export default {
},
computed: {
...mapGetters(["roles", "userName"]),
deptId() {
return this.$store.state.user.deptId
},
//总行
isHeadAdmin() {
return this.roles.includes("headAdmin");
@@ -441,12 +466,17 @@ export default {
if (selectedTab) {
this.selectedTab = selectedTab;
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) {
this.selectedTab = '2'
} else if (this.isPrivate) {
this.selectedTab = '0'
this.selectedTab = defaultTab
} else {
this.selectedTab = '2'
this.selectedTab = defaultTab
}
}
this.initVisitingTaskList();

View File

@@ -64,9 +64,16 @@
</div>
<div class="content">
<el-radio-group class="header-radio" v-model="selectedTab" @input="handleChange">
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
</template>
<template v-else>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
</template>
</el-radio-group>
<!-- <div class="taskTop">
<div class="taskTop_left">
@@ -292,6 +299,9 @@ export default {
},
computed: {
...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
//总行
isHeadAdmin() {
return this.roles.includes('headAdmin')
@@ -334,12 +344,17 @@ export default {
delete this.$route.query[key];
}
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) {
this.selectedTab = '2'
} else if (this.isPrivate) {
this.selectedTab = '0'
this.selectedTab = defaultTab
} else {
this.selectedTab = '2'
this.selectedTab = defaultTab
}
}
if (this.isBranchAdmin) {

View File

@@ -6,21 +6,40 @@
class="header-radio"
@input="handleChange"
>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button>
</template>
<template v-else>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button>
</template>
</el-radio-group>
<div class="taskTop">
<div class="taskTop_left">
@@ -1180,6 +1199,9 @@ export default {
},
computed: {
...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
// 总行
isHeadAdmin() {
return this.roles.includes('headAdmin')
@@ -1215,17 +1237,22 @@ export default {
created() {
// getGroupInfoByGroupId({})
this.isUserType()
// 根据deptId动态设置默认tab默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) {
this.selectedTab = '2'
this.custTypeList = [{ label: '企业', value: '2' }]
} else if (this.isPrivate) {
this.selectedTab = '0'
this.selectedTab = defaultTab
this.custTypeList = [
{ label: '个人', value: '0' },
{ label: '商户', value: '1' }
]
} else {
this.selectedTab = '2'
this.selectedTab = defaultTab
this.custTypeList = [
{ label: '个人', value: '0' },
{ label: '商户', value: '1' },

View File

@@ -1,29 +1,25 @@
<template>
<div class="app-container" style="padding: 0px !important;">
<el-date-picker
v-model="reportTime"
placeholder="请选择日期"
@change="initCardList"
class="time-picker"
/>
<el-row :gutter="24" style="padding: 0 30px;margin-left: -15px;">
<el-row :gutter="24" style="padding: 0 30px;">
<el-col :span="6">
<el-card class="box-card">
<div class="my-span-checklist-title">
总预警推送次数
<el-card class="stat-card stat-card-blue">
<div class="stat-icon">
<i class="el-icon-bell"></i>
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.alterCount }}</span>
<div class="stat-content">
<div class="stat-title">总预警推送次数</div>
<div class="stat-value">{{ cardInfo.alterCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="box-card1">
<div class="my-span-checklist-title">
反馈完成率
<el-card class="stat-card stat-card-blue2">
<div class="stat-icon">
<i class="el-icon-data-analysis"></i>
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.completeRate + '%' }}</span>
<div class="stat-content">
<div class="stat-title">反馈完成率</div>
<div class="stat-value">{{ cardInfo.completeRate + '%' }}</div>
</div>
</el-card>
</el-col>
@@ -35,6 +31,14 @@
:inline="true"
style="margin-top:20px;"
>
<el-form-item label="日期">
<el-date-picker
v-model="reportTime"
placeholder="请选择日期"
@change="initCardList"
style="width:100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchArray.status" placeholder="请选择状态" style="width: 100%" clearable>
<el-option
@@ -46,20 +50,26 @@
</el-select>
</el-form-item>
<el-form-item label="预警类型" prop="alterType">
<el-input
<el-select
v-model="searchArray.alterType"
placeholder="请输入预警类型"
placeholder="请选择预警类型"
clearable
style="width:100%"
>
</el-input>
<el-option
v-for="item in alterTypeOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="searchFn">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetFn">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableData" :height="dyHeight" :key="tableKey">
<el-table v-loading="loading" :data="tableData">
<template>
<el-table-column label="序号" prop="xh" width="80">
<template slot-scope="scope">
@@ -91,17 +101,15 @@
</el-table-column> -->
</template>
</el-table>
<div class="pagination_end">
<el-pagination
layout="total, prev, pager, next, jumper"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
@current-change="currentChangeFn"
:total="total"
@size-change="sizeChangeFn"
></el-pagination>
</div>
<el-pagination
:page-sizes="[10, 20, 30, 50]"
:page-size="pageSize"
layout="->,total,prev,pager,next,sizes"
:total="total"
:current-page="pageNum"
@current-change="currentChangeFn"
@size-change="sizeChangeFn"
></el-pagination>
<el-dialog
:title="dialogTitle"
@@ -199,7 +207,8 @@
import {
warningworkRecordList,
warningworkRecordSubmit,
warningCardNum
warningCardNum,
getAlterTypes
} from "@/api/system/home";
import _ from "lodash";
import dayjs from "dayjs";
@@ -243,6 +252,8 @@ export default {
}
],
alterTypeOptions: [],
searchArray: {
status: "",
alterType: ""
@@ -252,8 +263,6 @@ export default {
tableData: [],
total: 0,
loading: false,
dyHeight: {},
tableKey: false,
dialogTitle: '',
dialogForm: {
custName: '',
@@ -277,12 +286,9 @@ export default {
};
},
created() {
window.addEventListener("resize", () => {
this.dyHeight = Number(window.innerHeight) - 330;
});
this.dyHeight = Number(window.innerHeight) - 330;
this.resetFn();
this.initCardList()
this.initCardList();
this.getAlterTypeList();
},
filters: {
formatFilter(v, type, list) {
@@ -293,6 +299,13 @@ export default {
}
},
methods: {
getAlterTypeList() {
getAlterTypes().then(res => {
if (res.code === 200) {
this.alterTypeOptions = res.data || [];
}
});
},
initCardList() {
warningCardNum({ reportTime: this.reportTime ? dayjs(this.reportTime).format('YYYY-MM-DD') + ' 23:59:59' : '' }).then(res => {
this.cardInfo = res
@@ -326,7 +339,6 @@ export default {
} else {
this.$message.error(response.msg || "操作失败");
}
this.tableKey = !this.tableKey;
});
},
currentChangeFn(val) {
@@ -369,45 +381,79 @@ export default {
</script>
<style lang="scss" scoped>
.pagination_end {
margin-top: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
::v-deep .el-pagination {
margin-top: 15px;
}
.time-picker {
margin: 20px 0 0 0px;
}
.box-card {
// 统计卡片样式
.stat-card {
height: 100px;
border-radius: 8px;
margin-top: 25px;
margin-left: -30px;
border: 3px solid #a8c2f5;
}
border: none;
display: flex;
align-items: center;
padding: 0 20px;
position: relative;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.box-card1 {
height: 100px;
border-radius: 8px;
margin-top: 25px;
border: 3px solid #a8c2f5;
}
// 左侧装饰条
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: rgba(255, 255, 255, 0.5);
}
.my-span-checklist-title {
font-size: 14px;
color: #666;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
margin-right: 12px;
flex-shrink: 0;
.my-span-checklist-main {
line-height: 40px;
font-size: 30px;
color: #222;
font-weight: bold;
i {
font-size: 22px;
color: #fff;
}
}
.stat-content {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
.stat-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
.stat-value {
font-size: 20px;
color: #fff;
font-weight: bold;
}
}
// 蓝色渐变卡片1左上深→右下浅
&.stat-card-blue {
background: linear-gradient(135deg, #1e7ee6 0%, #a0cfff 100%);
}
// 蓝色渐变卡片2左上浅→右下深
&.stat-card-blue2 {
background: linear-gradient(135deg, #a0cfff 0%, #1e7ee6 100%);
}
}
.header-radio {
display: flex;