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

This commit is contained in:
YunaiV committed 2024-05-19 10:54:11 +08:00
commit 5ac92cf362
214 files changed
+16344 -2560

No files matched your search

+41 -34
View File
@@ -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()
}
]
+2 -2
View File
@@ -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 {
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
+4 -4
View File
@@ -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>
+166
View File
@@ -0,0 +1,166 @@
<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>
+2
View File
@@ -1,4 +1,6 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
+2
View File
@@ -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({
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
<ContentWrap>
<!-- 搜索工作栏 -->
+10
View File
@@ -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 属性
+7 -1
View File
@@ -1,5 +1,11 @@
<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>
<!-- 搜索工作栏 -->
+2 -2
View File
@@ -1,5 +1,5 @@
<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
@@ -1,4 +1,6 @@
<template>
<doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
@@ -1,4 +1,6 @@
<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
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<ContentWrap>
<!-- 搜索工作栏 -->
+2
View File
@@ -1,4 +1,6 @@
<template>
<doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
+5
View File
@@ -1,5 +1,10 @@
<!-- 工作流 - 抄送我的流程 -->
<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">
+7 -1
View File
@@ -1,5 +1,11 @@
<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>
<!-- 搜索工作栏 -->
+7 -1
View File
@@ -1,5 +1,11 @@
<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>
<!-- 搜索工作栏 -->
+2 -2
View File
@@ -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
+46 -38
View File
@@ -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="请输入商机名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入商机名称"
@keyup.enter="handleQuery"
/>
</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"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:business:export']"
:loading="exportLoading"
plain
type="success"
@click="handleExport"
>
<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="预计成交日期"
align="center"
prop="dealTime"
:formatter="dateFormatter"
align="center"
label="预计成交日期"
prop="dealTime"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
@@ -97,49 +105,49 @@
width="180px"
/>
<el-table-column
label="更新时间"
align="center"
prop="updateTime"
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="updateTime"
width="180px"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
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: []
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+40 -20
View File
@@ -1,16 +1,24 @@
<!-- 客户导入窗口 -->
<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()
}
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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 () => {
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 })
/** 初始化 */
@@ -0,0 +1,153 @@
<!-- 成交周期分析 -->
<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>
@@ -0,0 +1,153 @@
<!-- 成交周期分析 -->
<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 })
/** 初始化 */
@@ -0,0 +1,154 @@
<!-- 客户总量统计 -->
<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 })
/** 初始化 */
+96 -48
View File
@@ -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())
@@ -0,0 +1,307 @@
<!-- 客户总量统计 -->
<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>
@@ -0,0 +1,259 @@
<!-- 客户总量统计 -->
<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>
@@ -0,0 +1,149 @@
<!-- 销售漏斗分析 -->
<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>
+171
View File
@@ -0,0 +1,171 @@
<!-- 数据统计 - 客户画像 -->
<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>
@@ -0,0 +1,229 @@
<!-- 员工业绩统计 -->
<!-- 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>
@@ -0,0 +1,229 @@
<!-- 员工业绩统计 -->
<!-- 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>
@@ -0,0 +1,232 @@
<!-- 员工业绩统计 -->
<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>
@@ -0,0 +1,157 @@
<!-- 数据统计 - 员工业绩分析 -->
<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>
@@ -0,0 +1,147 @@
<!-- 客户城市分布 -->
<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>
@@ -0,0 +1,198 @@
<!-- 客户行业分析 -->
<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>
@@ -0,0 +1,198 @@
<!-- 客户来源分析 -->
<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>
@@ -0,0 +1,198 @@
<!-- 客户来源分析 -->
<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>
+156
View File
@@ -0,0 +1,156 @@
<!-- 数据统计 - 客户画像 -->
<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 }>() // 搜索参数
+21 -20
View File
@@ -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>
+12 -5
View File
@@ -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 -9
View File
@@ -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('')
-59
View File
@@ -1,59 +0,0 @@
<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>
+1 -1
View File
@@ -96,7 +96,7 @@
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:config:delete']"
v-hasPermi="['infra:file:delete']"
>
删除
</el-button>
+1 -1
View File
@@ -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>
+2 -1
View File
@@ -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
+3 -3
View File
@@ -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'
@@ -1,6 +1,6 @@
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
@@ -1,15 +1,57 @@
<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>
+161 -82
View File
@@ -1,94 +1,172 @@
<template>
<doc-alert title="【营销】秒杀活动" url="https://doc.iocoder.cn/mall/promotion-seckill/" />
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<el-button
v-hasPermi="['promotion:seckill-config:create']"
plain
type="primary"
@click="openForm('create')"
<!-- 搜索工作栏 -->
<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"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
<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
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['promotion:seckill-config:create']"
>
<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()
@@ -1,82 +0,0 @@
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)
+42 -47
View File
@@ -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="累计会员数"
icon="fa-solid:users"
icon-color="bg-blue-100"
icon-bg-color="text-blue-500"
:value="summary?.userCount || 0"
icon="fa-solid:users"
icon-bg-color="text-blue-500"
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="累计充值人数"
icon="fa-solid:user"
icon-color="bg-purple-100"
icon-bg-color="text-purple-500"
:value="summary?.rechargeUserCount || 0"
icon="fa-solid:user"
icon-bg-color="text-purple-500"
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="累计充值金额"
icon="fa-solid:money-check-alt"
icon-color="bg-yellow-100"
icon-bg-color="text-yellow-500"
prefix="¥"
:decimals="2"
:value="fenToYuan(summary?.rechargePrice || 0)"
icon="fa-solid:money-check-alt"
icon-bg-color="text-yellow-500"
icon-color="bg-yellow-100"
prefix="¥"
title="累计充值金额"
/>
</el-col>
<el-col :sm="6" :xs="12" v-loading="loading">
<el-col v-loading="loading" :sm="6" :xs="12">
<SummaryCard
title="累计消费金额"
icon="fa-solid:yen-sign"
icon-color="bg-green-100"
icon-bg-color="text-green-500"
prefix="¥"
:decimals="2"
:value="fenToYuan(summary?.expensePrice || 0)"
icon="fa-solid:yen-sign"
icon-bg-color="text-green-500"
icon-color="bg-green-100"
prefix="¥"
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
align="center"
label="会员数量"
prop="userCount"
align="center"
min-width="105"
prop="userCount"
sortable
/>
<el-table-column
align="center"
label="订单创建数量"
min-width="135"
prop="orderCreateUserCount"
align="center"
min-width="135"
sortable
/>
<el-table-column
align="center"
label="订单支付数量"
prop="orderPayUserCount"
align="center"
min-width="135"
prop="orderPayUserCount"
sortable
/>
<el-table-column
label="订单支付金额"
prop="orderPayPrice"
align="center"
min-width="135"
sortable
:formatter="fenToYuanFormat"
align="center"
label="订单支付金额"
min-width="135"
prop="orderPayPrice"
sortable
/>
</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%' }"
/>
+2
View File
@@ -1,5 +1,7 @@
<template>
<ContentWrap>
<doc-alert title="大屏设计器" url="https://doc.iocoder.cn/report/screen/" />
<IFrame :src="src" />
</ContentWrap>
</template>
+2
View File
@@ -1,5 +1,7 @@
<template>
<ContentWrap>
<doc-alert title="报表设计器" url="https://doc.iocoder.cn/report/" />
<IFrame :src="src" />
</ContentWrap>
</template>
@@ -1,113 +0,0 @@
<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>
-13
View File
@@ -1,13 +0,0 @@
<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>
-220
View File
@@ -1,220 +0,0 @@
<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>
+1 -1
View File
@@ -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="请输入部门名称"
@@ -1,112 +0,0 @@
<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>
-228
View File
@@ -1,228 +0,0 @@
<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>
+2 -2
View File
@@ -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>
+11 -1
View File
@@ -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/
@@ -57,6 +58,15 @@ const crudSchemas = reactive<CrudSchema[]>([
component: 'Radio'
}
},
{
label: '是否开启 STARTTLS',
field: 'starttlsEnable',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean',
form: {
component: 'Radio'
}
},
{
label: '创建时间',
field: 'createTime',
Loaded 100 of 214 files, more files were not shown because too many files have changed in this diff. Show more