Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
This commit is contained in:
commit
5ac92cf362
214 files changed
+16344
-2560
No files matched your search
+41
-34
@@ -62,7 +62,14 @@
|
||||
<template #header>
|
||||
<div class="h-3 flex justify-between">
|
||||
<span>{{ t('workplace.project') }}</span>
|
||||
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
href="https://github.com/yudaocode"
|
||||
target="_blank"
|
||||
>
|
||||
{{ t('action.more') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<el-skeleton :loading="loading" animated>
|
||||
@@ -76,13 +83,13 @@
|
||||
:sm="24"
|
||||
:xs="24"
|
||||
>
|
||||
<el-card shadow="hover">
|
||||
<el-card shadow="hover" class="mr-5px mt-5px">
|
||||
<div class="flex items-center">
|
||||
<Icon :icon="item.icon" :size="25" class="mr-8px" />
|
||||
<span class="text-16px">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div>
|
||||
<div class="mt-16px flex justify-between text-12px text-gray-400">
|
||||
<div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
|
||||
<div class="mt-12px flex justify-between text-12px text-gray-400">
|
||||
<span>{{ item.personal }}</span>
|
||||
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
|
||||
</div>
|
||||
@@ -204,45 +211,45 @@ let projects = reactive<Project[]>([])
|
||||
const getProject = async () => {
|
||||
const data = [
|
||||
{
|
||||
name: 'Github',
|
||||
name: 'ruoyi-vue-pro',
|
||||
icon: 'akar-icons:github-fill',
|
||||
message: 'workplace.introduction',
|
||||
personal: 'Archer',
|
||||
message: 'https://github.com/YunaiV/ruoyi-vue-pro',
|
||||
personal: 'Spring Boot 单体架构',
|
||||
time: new Date()
|
||||
},
|
||||
{
|
||||
name: 'Vue',
|
||||
name: 'yudao-ui-admin-vue3',
|
||||
icon: 'logos:vue',
|
||||
message: 'workplace.introduction',
|
||||
personal: 'Archer',
|
||||
message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
|
||||
personal: 'Vue3 + element-plus',
|
||||
time: new Date()
|
||||
},
|
||||
{
|
||||
name: 'Angular',
|
||||
icon: 'logos:angular-icon',
|
||||
message: 'workplace.introduction',
|
||||
personal: 'Archer',
|
||||
name: 'yudao-ui-admin-vben',
|
||||
icon: 'logos:vue',
|
||||
message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
|
||||
personal: 'Vue3 + vben(antd)',
|
||||
time: new Date()
|
||||
},
|
||||
{
|
||||
name: 'React',
|
||||
icon: 'logos:react',
|
||||
message: 'workplace.introduction',
|
||||
personal: 'Archer',
|
||||
name: 'yudao-cloud',
|
||||
icon: 'akar-icons:github',
|
||||
message: 'https://github.com/YunaiV/yudao-cloud',
|
||||
personal: 'Spring Cloud 微服务架构',
|
||||
time: new Date()
|
||||
},
|
||||
{
|
||||
name: 'Webpack',
|
||||
icon: 'logos:webpack',
|
||||
message: 'workplace.introduction',
|
||||
personal: 'Archer',
|
||||
name: 'yudao-ui-mall-uniapp',
|
||||
icon: 'logos:vue',
|
||||
message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
|
||||
personal: 'Vue3 + uniapp',
|
||||
time: new Date()
|
||||
},
|
||||
{
|
||||
name: 'Vite',
|
||||
icon: 'vscode-icons:file-type-vite',
|
||||
message: 'workplace.introduction',
|
||||
personal: 'Archer',
|
||||
name: 'yudao-ui-admin-vue2',
|
||||
icon: 'logos:vue',
|
||||
message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
|
||||
personal: 'Vue2 + element-ui',
|
||||
time: new Date()
|
||||
}
|
||||
]
|
||||
@@ -254,27 +261,27 @@ let notice = reactive<Notice[]>([])
|
||||
const getNotice = async () => {
|
||||
const data = [
|
||||
{
|
||||
title: '系统升级版本',
|
||||
title: '系统支持 JDK 8/17/21,Vue 2/3',
|
||||
type: '通知',
|
||||
keys: ['通知', '升级'],
|
||||
keys: ['通知', '8', '17', '21', '2', '3'],
|
||||
date: new Date()
|
||||
},
|
||||
{
|
||||
title: '系统凌晨维护',
|
||||
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
|
||||
type: '公告',
|
||||
keys: ['公告', '维护'],
|
||||
keys: ['公告', 'Boot', 'Cloud'],
|
||||
date: new Date()
|
||||
},
|
||||
{
|
||||
title: '系统升级版本',
|
||||
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
|
||||
type: '通知',
|
||||
keys: ['通知', '升级'],
|
||||
keys: ['通知', '无需授权'],
|
||||
date: new Date()
|
||||
},
|
||||
{
|
||||
title: '系统凌晨维护',
|
||||
title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
|
||||
type: '公告',
|
||||
keys: ['公告', '维护'],
|
||||
keys: ['公告', '最广泛'],
|
||||
date: new Date()
|
||||
}
|
||||
]
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-form-item v-if="loginData.tenantEnable" prop="tenantName">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
@@ -207,7 +207,7 @@ const loginData = reactive({
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接登录
|
||||
if (loginData.captchaEnable) {
|
||||
if (!loginData.captchaEnable) {
|
||||
await handleLogin({})
|
||||
} else {
|
||||
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
|
||||
|
||||
@@ -188,7 +188,7 @@ const loginData = reactive({
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
captchaVerification: '',
|
||||
rememberMe: false
|
||||
rememberMe: true // 默认记录我。如果不需要,可手动修改
|
||||
}
|
||||
})
|
||||
|
||||
@@ -218,14 +218,14 @@ const getTenantId = async () => {
|
||||
}
|
||||
}
|
||||
// 记住我
|
||||
const getCookie = () => {
|
||||
const getLoginFormCache = () => {
|
||||
const loginForm = authUtil.getLoginForm()
|
||||
if (loginForm) {
|
||||
loginData.loginForm = {
|
||||
...loginData.loginForm,
|
||||
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
|
||||
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
|
||||
rememberMe: loginForm.rememberMe ? true : false,
|
||||
rememberMe: loginForm.rememberMe,
|
||||
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ watch(
|
||||
}
|
||||
)
|
||||
onMounted(() => {
|
||||
getCookie()
|
||||
getLoginFormCache()
|
||||
getTenantByWebsite()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,4 +1,6 @@
|
||||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
||||
@@ -45,6 +45,7 @@ import * as FormApi from '@/api/bpm/form'
|
||||
import FcDesigner from '@form-create/designer'
|
||||
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { useFormCreateDesigner } from '@/components/FormCreate'
|
||||
|
||||
defineOptions({ name: 'BpmFormEditor' })
|
||||
|
||||
@@ -55,6 +56,7 @@ const { query } = useRoute() // 路由信息
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
|
||||
const designer = ref() // 表单设计器
|
||||
useFormCreateDesigner(designer) // 表单设计器增强
|
||||
const dialogVisible = ref(false) // 弹窗是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:提交的按钮禁用
|
||||
const formData = ref({
|
||||
|
||||
@@ -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>
|
||||
<!-- 搜索工作栏 -->
|
||||
|
||||
@@ -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 属性
|
||||
|
||||
@@ -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>
|
||||
<!-- 搜索工作栏 -->
|
||||
|
||||
@@ -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,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>
|
||||
<!-- 搜索工作栏 -->
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<template>
|
||||
<doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
<!-- 搜索工作栏 -->
|
||||
|
||||
@@ -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>
|
||||
<!-- 搜索工作栏 -->
|
||||
|
||||
@@ -64,7 +64,7 @@ import BusinessDetailsHeader from './BusinessDetailsHeader.vue'
|
||||
import BusinessDetailsInfo from './BusinessDetailsInfo.vue'
|
||||
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
|
||||
import { BizTypeEnum } from '@/api/crm/permission'
|
||||
import { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import { OperateLogVO } from '@/api/system/operatelog'
|
||||
import { getOperateLogPage } from '@/api/crm/operateLog'
|
||||
import BusinessForm from '@/views/crm/business/BusinessForm.vue'
|
||||
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
|
||||
@@ -113,7 +113,7 @@ const transfer = () => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async (contactId: number) => {
|
||||
if (!contactId) {
|
||||
return
|
||||
|
||||
@@ -5,35 +5,43 @@
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="商机名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入商机名称"
|
||||
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: []
|
||||
|
||||
@@ -57,7 +57,7 @@ import PermissionList from '@/views/crm/permission/components/PermissionList.vue
|
||||
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
|
||||
import FollowUpList from '@/views/crm/followup/index.vue'
|
||||
import { BizTypeEnum } from '@/api/crm/permission'
|
||||
import type { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import type { OperateLogVO } from '@/api/system/operatelog'
|
||||
import { getOperateLogPage } from '@/api/crm/operateLog'
|
||||
|
||||
defineOptions({ name: 'CrmClueDetail' })
|
||||
@@ -103,7 +103,7 @@ const handleTransform = async () => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async () => {
|
||||
const data = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_CLUE,
|
||||
|
||||
@@ -49,7 +49,7 @@ import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vu
|
||||
import BusinessList from '@/views/crm/business/components/BusinessList.vue' // 商机列表
|
||||
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
|
||||
import { BizTypeEnum } from '@/api/crm/permission'
|
||||
import { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import { OperateLogVO } from '@/api/system/operatelog'
|
||||
import { getOperateLogPage } from '@/api/crm/operateLog'
|
||||
import ContactForm from '@/views/crm/contact/ContactForm.vue'
|
||||
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
|
||||
@@ -88,7 +88,7 @@ const transfer = () => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async (contactId: number) => {
|
||||
if (!contactId) {
|
||||
return
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import { OperateLogVO } from '@/api/system/operatelog'
|
||||
import * as ContractApi from '@/api/crm/contract'
|
||||
import ContractDetailsInfo from './ContractDetailsInfo.vue'
|
||||
import ContractDetailsHeader from './ContractDetailsHeader.vue'
|
||||
@@ -94,7 +94,7 @@ const getContractData = async () => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async (contractId: number) => {
|
||||
if (!contractId) {
|
||||
return
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ import PermissionList from '@/views/crm/permission/components/PermissionList.vue
|
||||
import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
|
||||
import FollowUpList from '@/views/crm/followup/index.vue'
|
||||
import { BizTypeEnum } from '@/api/crm/permission'
|
||||
import type { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import type { OperateLogVO } from '@/api/system/operatelog'
|
||||
import { getOperateLogPage } from '@/api/crm/operateLog'
|
||||
import CustomerDistributeForm from '@/views/crm/customer/pool/CustomerDistributeForm.vue'
|
||||
|
||||
@@ -185,7 +185,7 @@ const handlePutPool = async () => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async () => {
|
||||
if (!customerId.value) {
|
||||
return
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery(undefined)">
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
@@ -113,7 +113,7 @@
|
||||
<el-tab-pane label="下属负责的" name="3" />
|
||||
</el-tabs>
|
||||
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column align="center" label="客户名称" fixed="left" prop="name" width="160">
|
||||
<el-table-column align="center" fixed="left" label="客户名称" prop="name" width="160">
|
||||
<template #default="scope">
|
||||
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
|
||||
{{ scope.row.name }}
|
||||
@@ -125,9 +125,9 @@
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机" align="center" prop="mobile" width="120" />
|
||||
<el-table-column label="电话" align="center" prop="telephone" width="130" />
|
||||
<el-table-column label="邮箱" align="center" prop="email" width="180" />
|
||||
<el-table-column align="center" label="手机" prop="mobile" width="120" />
|
||||
<el-table-column align="center" label="电话" prop="telephone" width="130" />
|
||||
<el-table-column align="center" label="邮箱" prop="email" width="180" />
|
||||
<el-table-column align="center" label="客户级别" prop="level" width="135">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
|
||||
@@ -164,7 +164,7 @@
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" />
|
||||
<el-table-column label="地址" align="center" prop="detailAddress" width="180" />
|
||||
<el-table-column align="center" label="地址" prop="detailAddress" width="180" />
|
||||
<el-table-column align="center" label="距离进入公海天数" prop="poolDay" width="140">
|
||||
<template #default="scope"> {{ scope.row.poolDay }} 天</template>
|
||||
</el-table-column>
|
||||
@@ -254,7 +254,7 @@ const activeName = ref('1') // 列表 tab
|
||||
|
||||
/** tab 切换 */
|
||||
const handleTabClick = (tab: TabsPaneContext) => {
|
||||
queryParams.sceneType = tab.paneName
|
||||
queryParams.sceneType = tab.paneName as string
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="老负责人">
|
||||
<el-radio-group v-model="oldOwnerHandler" @change="formData.oldOwnerPermissionLevel">
|
||||
<el-radio-group v-model="oldOwnerHandler" @change="handleOwnerChange">
|
||||
<el-radio :label="false" size="large">移除</el-radio>
|
||||
<el-radio :label="true" size="large">加入团队</el-radio>
|
||||
</el-radio-group>
|
||||
@@ -86,10 +86,16 @@ const open = async (bizId: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = getDialogTitle()
|
||||
resetForm()
|
||||
formData.value.bizId = bizId
|
||||
formData.value.id = bizId
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
// 老负责人负责方式
|
||||
const handleOwnerChange = (val: boolean) => {
|
||||
if (!val) {
|
||||
// 移除的话提交不带 oldOwnerPermissionLevel 参数
|
||||
formData.value.oldOwnerPermissionLevel = undefined
|
||||
}
|
||||
}
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import { OperateLogVO } from '@/api/system/operatelog'
|
||||
import * as ProductApi from '@/api/crm/product'
|
||||
import ProductDetailsHeader from '@/views/crm/product/detail/ProductDetailsHeader.vue'
|
||||
import ProductDetailsInfo from '@/views/crm/product/detail/ProductDetailsInfo.vue'
|
||||
@@ -40,7 +40,7 @@ const getProductData = async (id: number) => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async (productId: number) => {
|
||||
if (!productId) {
|
||||
return
|
||||
|
||||
@@ -34,7 +34,7 @@ import ReceivableDetailsHeader from './ReceivableDetailsHeader.vue'
|
||||
import ReceivableDetailsInfo from './ReceivableDetailsInfo.vue'
|
||||
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
|
||||
import { BizTypeEnum } from '@/api/crm/permission'
|
||||
import { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import { OperateLogVO } from '@/api/system/operatelog'
|
||||
import { getOperateLogPage } from '@/api/crm/operateLog'
|
||||
import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
|
||||
|
||||
@@ -66,7 +66,7 @@ const openForm = (type: string, id?: number) => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async (receivableId: number) => {
|
||||
if (!receivableId) {
|
||||
return
|
||||
|
||||
@@ -37,7 +37,7 @@ import ReceivablePlanDetailsHeader from './ReceivablePlanDetailsHeader.vue'
|
||||
import ReceivablePlanDetailsInfo from './ReceivablePlanDetailsInfo.vue'
|
||||
import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
|
||||
import { BizTypeEnum } from '@/api/crm/permission'
|
||||
import { OperateLogV2VO } from '@/api/system/operatelog'
|
||||
import { OperateLogVO } from '@/api/system/operatelog'
|
||||
import { getOperateLogPage } from '@/api/crm/operateLog'
|
||||
import ReceivablePlanForm from '@/views/crm/receivable/plan/ReceivablePlanForm.vue'
|
||||
|
||||
@@ -70,7 +70,7 @@ const openForm = (type: string, id?: number) => {
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
|
||||
const logList = ref<OperateLogVO[]>([]) // 操作日志列表
|
||||
const getOperateLog = async (receivablePlanId: number) => {
|
||||
if (!receivablePlanId) {
|
||||
return
|
||||
|
||||
@@ -10,11 +10,39 @@
|
||||
<!-- 统计列表 -->
|
||||
<el-card shadow="never" class="mt-16px">
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="序号" align="center" type="index" width="80" />
|
||||
<el-table-column label="客户名称" align="center" prop="customerName" min-width="200" />
|
||||
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
|
||||
<el-table-column
|
||||
label="客户名称"
|
||||
align="center"
|
||||
prop="customerName"
|
||||
min-width="200"
|
||||
fixed="left"
|
||||
/>
|
||||
<el-table-column label="合同名称" align="center" prop="contractName" min-width="200" />
|
||||
<el-table-column label="合同总金额" align="center" prop="totalPrice" min-width="200" />
|
||||
<el-table-column label="回款金额" align="center" prop="receivablePrice" min-width="200" />
|
||||
<el-table-column
|
||||
label="合同总金额"
|
||||
align="center"
|
||||
prop="totalPrice"
|
||||
min-width="200"
|
||||
:formatter="erpPriceTableColumnFormatter"
|
||||
/>
|
||||
<el-table-column
|
||||
label="回款金额"
|
||||
align="center"
|
||||
prop="receivablePrice"
|
||||
min-width="200"
|
||||
:formatter="erpPriceTableColumnFormatter"
|
||||
/>
|
||||
<el-table-column align="center" label="客户来源" prop="source" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" align="center" prop="ownerUserName" min-width="200" />
|
||||
<el-table-column label="创建人" align="center" prop="creatorUserName" min-width="200" />
|
||||
<el-table-column
|
||||
@@ -28,8 +56,9 @@
|
||||
label="下单日期"
|
||||
align="center"
|
||||
prop="orderDate"
|
||||
:formatter="dateFormatter2"
|
||||
:formatter="dateFormatter"
|
||||
min-width="200"
|
||||
fixed="right"
|
||||
/>
|
||||
</el-table>
|
||||
</el-card>
|
||||
@@ -40,10 +69,12 @@ import {
|
||||
CrmStatisticsCustomerSummaryByDateRespVO
|
||||
} from '@/api/crm/statistics/customer'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { round } from 'lodash-es'
|
||||
import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { erpPriceTableColumnFormatter } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
|
||||
defineOptions({ name: 'CustomerConversionStat' })
|
||||
|
||||
const props = defineProps<{ queryParams: any }>() // 搜索参数
|
||||
|
||||
const loading = ref(false) // 加载中
|
||||
@@ -53,7 +84,7 @@ const list = ref<CrmStatisticsCustomerSummaryByDateRespVO[]>([]) // 列表的数
|
||||
const echartsOption = reactive<EChartsOption>({
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true
|
||||
},
|
||||
@@ -93,10 +124,9 @@ const echartsOption = reactive<EChartsOption>({
|
||||
}
|
||||
}) as EChartsOption
|
||||
|
||||
/** 获取统计数据 */
|
||||
const loadData = async () => {
|
||||
/** 获取数据并填充图表 */
|
||||
const fetchAndFill = async () => {
|
||||
// 1. 加载统计数据
|
||||
loading.value = true
|
||||
const customerCount = await StatisticsCustomerApi.getCustomerSummaryByDate(props.queryParams)
|
||||
const contractSummary = await StatisticsCustomerApi.getContractSummary(props.queryParams)
|
||||
// 2.1 更新 Echarts 数据
|
||||
@@ -111,7 +141,7 @@ const loadData = async () => {
|
||||
return {
|
||||
name: item.time,
|
||||
value: item.customerCreateCount
|
||||
? round((item.customerDealCount / item.customerCreateCount) * 100, 2)
|
||||
? ((item.customerDealCount / item.customerCreateCount) * 100).toFixed(2)
|
||||
: 0
|
||||
}
|
||||
}
|
||||
@@ -119,8 +149,18 @@ const loadData = async () => {
|
||||
}
|
||||
// 2.2 更新列表数据
|
||||
list.value = contractSummary
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/** 获取统计数据 */
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await fetchAndFill()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ loadData })
|
||||
|
||||
/** 初始化 */
|
||||
|
||||
@@ -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>
|
||||
+41
-14
@@ -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 })
|
||||
|
||||
+54
-22
@@ -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 })
|
||||
|
||||
+34
-19
@@ -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 })
|
||||
|
||||
/** 初始化 */
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+1
-1
@@ -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) // 加载中
|
||||
File renamed without changes.
+8
-1
@@ -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 }>() // 搜索参数
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-1
@@ -13,7 +13,13 @@
|
||||
<el-table-column label="公司排名" align="center" type="index" width="80" />
|
||||
<el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
|
||||
<el-table-column label="部门" align="center" prop="deptName" min-width="200" />
|
||||
<el-table-column label="回款金额(元)" align="center" prop="count" min-width="200" />
|
||||
<el-table-column
|
||||
label="回款金额(元)"
|
||||
align="center"
|
||||
prop="count"
|
||||
min-width="200"
|
||||
:formatter="erpPriceTableColumnFormatter"
|
||||
/>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -21,6 +27,7 @@
|
||||
import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { clone } from 'lodash-es'
|
||||
import { erpPriceTableColumnFormatter } from '@/utils'
|
||||
|
||||
defineOptions({ name: 'ReceivablePriceRank' })
|
||||
const props = defineProps<{ queryParams: any }>() // 搜索参数
|
||||
@@ -29,6 +29,7 @@
|
||||
check-strictly
|
||||
node-key="id"
|
||||
placeholder="请选择归属部门"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -62,8 +63,8 @@
|
||||
<CustomerCountRank :query-params="queryParams" ref="customerCountRankRef" />
|
||||
</el-tab-pane>
|
||||
<!-- 新增联系人数排行 -->
|
||||
<el-tab-pane label="新增联系人数排行" name="contactsCountRank" lazy>
|
||||
<ContactsCountRank :query-params="queryParams" ref="contactsCountRankRef" />
|
||||
<el-tab-pane label="新增联系人数排行" name="contactCountRank" lazy>
|
||||
<ContactCountRank :query-params="queryParams" ref="contactCountRankRef" />
|
||||
</el-tab-pane>
|
||||
<!-- 跟进次数排行 -->
|
||||
<el-tab-pane label="跟进次数排行" name="followCountRank" lazy>
|
||||
@@ -77,14 +78,14 @@
|
||||
</el-col>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import ContractPriceRank from './ContractPriceRank.vue'
|
||||
import ReceivablePriceRank from './ReceivablePriceRank.vue'
|
||||
import ContractCountRank from './ContractCountRank.vue'
|
||||
import ProductSalesRank from './ProductSalesRank.vue'
|
||||
import CustomerCountRank from './CustomerCountRank.vue'
|
||||
import ContactsCountRank from './ContactsCountRank.vue'
|
||||
import FollowCountRank from './FollowCountRank.vue'
|
||||
import FollowCustomerCountRank from './FollowCustomerCountRank.vue'
|
||||
import ContractPriceRank from './components/ContractPriceRank.vue'
|
||||
import ReceivablePriceRank from './components/ReceivablePriceRank.vue'
|
||||
import ContractCountRank from './components/ContractCountRank.vue'
|
||||
import ProductSalesRank from './components/ProductSalesRank.vue'
|
||||
import CustomerCountRank from './components/CustomerCountRank.vue'
|
||||
import ContactCountRank from './components/ContactCountRank.vue'
|
||||
import FollowCountRank from './components/FollowCountRank.vue'
|
||||
import FollowCustomerCountRank from './components/FollowCustomerCountRank.vue'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import * as DeptApi from '@/api/system/dept'
|
||||
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
|
||||
@@ -109,35 +110,35 @@ const receivablePriceRankRef = ref() // ReceivablePriceRank 组件的引用
|
||||
const contractCountRankRef = ref() // ContractCountRank 组件的引用
|
||||
const productSalesRankRef = ref() // ProductSalesRank 组件的引用
|
||||
const customerCountRankRef = ref() // CustomerCountRank 组件的引用
|
||||
const contactsCountRankRef = ref() // ContactsCountRank 组件的引用
|
||||
const contactCountRankRef = ref() // ContactCountRank 组件的引用
|
||||
const followCountRankRef = ref() // FollowCountRank 组件的引用
|
||||
const followCustomerCountRankRef = ref() // FollowCustomerCountRank 组件的引用
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
switch (activeTab.value) {
|
||||
case 'contractPriceRank':
|
||||
case 'contractPriceRank': // 合同金额排行
|
||||
contractPriceRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'receivablePriceRank':
|
||||
case 'receivablePriceRank': // 回款金额排行
|
||||
receivablePriceRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'contractCountRank':
|
||||
case 'contractCountRank': // 签约合同排行
|
||||
contractCountRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'productSalesRank':
|
||||
case 'productSalesRank': // 产品销量排行
|
||||
productSalesRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'customerCountRank':
|
||||
case 'customerCountRank': // 新增客户数排行
|
||||
customerCountRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'contactsCountRank':
|
||||
contactsCountRankRef.value?.loadData?.()
|
||||
case 'contactCountRank': // 新增联系人数排行
|
||||
contactCountRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'followCountRank':
|
||||
case 'followCountRank': // 跟进次数排行
|
||||
followCountRankRef.value?.loadData?.()
|
||||
break
|
||||
case 'followCustomerCountRank':
|
||||
case 'followCustomerCountRank': // 跟进客户数排行
|
||||
followCustomerCountRankRef.value?.loadData?.()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { ProductCategoryApi } from '@/api/erp/product/category'
|
||||
import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
@@ -66,7 +66,7 @@ const formData = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
sort: undefined,
|
||||
status: undefined
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
const formRules = reactive({
|
||||
parentId: [{ required: true, message: '上级编号不能为空', trigger: 'blur' }],
|
||||
@@ -105,7 +105,7 @@ const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as ProductCategoryApi.ProductCategoryVO
|
||||
const data = formData.value as unknown as ProductCategoryVO
|
||||
if (formType.value === 'create') {
|
||||
await ProductCategoryApi.createProductCategory(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
<el-descriptions-item label="请求参数">
|
||||
{{ detailData.requestParams }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求结果">
|
||||
{{ detailData.responseBody }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求时间">
|
||||
{{ formatDate(detailData.beginTime) }} ~ {{ formatDate(detailData.endTime) }}
|
||||
</el-descriptions-item>
|
||||
@@ -36,6 +39,15 @@
|
||||
失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作模块">
|
||||
{{ detailData.operateModule }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作名">
|
||||
{{ detailData.operateName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作名">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="detailData.operateType" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['infra:api-error-log:export']"
|
||||
v-hasPermi="['infra:api-access-log:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
@@ -91,16 +91,16 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="日志编号" align="center" prop="id" />
|
||||
<el-table-column label="日志编号" align="center" prop="id" width="100" fix="right" />
|
||||
<el-table-column label="用户编号" align="center" prop="userId" />
|
||||
<el-table-column label="用户类型" align="center" prop="userType">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应用名" align="center" prop="applicationName" />
|
||||
<el-table-column label="应用名" align="center" prop="applicationName" width="150" />
|
||||
<el-table-column label="请求方法" align="center" prop="requestMethod" width="80" />
|
||||
<el-table-column label="请求地址" align="center" prop="requestUrl" width="250" />
|
||||
<el-table-column label="请求地址" align="center" prop="requestUrl" width="500" />
|
||||
<el-table-column label="请求时间" align="center" prop="beginTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ formatDate(scope.row.beginTime) }}</span>
|
||||
@@ -114,7 +114,14 @@
|
||||
{{ scope.row.resultCode === 0 ? '成功' : '失败(' + scope.row.resultMsg + ')' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column label="操作模块" align="center" prop="operateModule" width="180" />
|
||||
<el-table-column label="操作名" align="center" prop="operateName" width="180" />
|
||||
<el-table-column label="操作类型" align="center" prop="operateType">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="scope.row.operateType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right" width="60">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
|
||||
@@ -8,11 +8,9 @@
|
||||
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- 表单设计器 -->
|
||||
<el-col>
|
||||
<FcDesigner ref="designer" height="780px" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 表单设计器 -->
|
||||
<FcDesigner ref="designer" height="780px" />
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 弹窗:表单预览 -->
|
||||
@@ -23,15 +21,14 @@
|
||||
</el-button>
|
||||
<el-scrollbar height="580">
|
||||
<div>
|
||||
<pre><code class="hljs" v-dompurify-html="highlightedCode(formData)"></code></pre>
|
||||
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'InfraBuild' })
|
||||
import FcDesigner from '@form-create/designer'
|
||||
import { useFormCreateDesigner } from '@/components/FormCreate'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { isString } from '@/utils/is'
|
||||
|
||||
@@ -41,6 +38,8 @@ import xml from 'highlight.js/lib/languages/java'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
import formCreate from '@form-create/element-ui'
|
||||
|
||||
defineOptions({ name: 'InfraBuild' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息
|
||||
|
||||
@@ -49,6 +48,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formType = ref(-1) // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件
|
||||
const formData = ref('') // 表单数据
|
||||
useFormCreateDesigner(designer) // 表单设计器增强
|
||||
|
||||
/** 打开弹窗 */
|
||||
const openModel = (title: string) => {
|
||||
@@ -82,14 +82,13 @@ const makeTemplate = () => {
|
||||
const opt = designer.value.getOption()
|
||||
return `<template>
|
||||
<form-create
|
||||
v-model="fapi"
|
||||
v-model:api="fApi"
|
||||
:rule="rule"
|
||||
:option="option"
|
||||
@submit="onSubmit"
|
||||
></form-create>
|
||||
</template>
|
||||
<script setup lang=ts>
|
||||
import formCreate from "@form-create/element-ui";
|
||||
const faps = ref(null)
|
||||
const rule = ref('')
|
||||
const option = ref('')
|
||||
|
||||
@@ -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>
|
||||
@@ -96,7 +96,7 @@
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['infra:config:delete']"
|
||||
v-hasPermi="['infra:file:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['infra:config:delete']"
|
||||
v-hasPermi="['infra:file-config:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</template>
|
||||
<div class="max-h-80 overflow-auto">
|
||||
<ul>
|
||||
<li v-for="msg in messageList.reverse()" :key="msg.time" class="mt-2">
|
||||
<li v-for="msg in messageReverseList" :key="msg.time" class="mt-2">
|
||||
<div class="flex items-center">
|
||||
<span class="text-primary mr-2 font-medium">收到消息:</span>
|
||||
<span>{{ formatDate(msg.time) }}</span>
|
||||
@@ -92,6 +92,7 @@ const { status, data, send, close, open } = useWebSocket(server.value, {
|
||||
|
||||
/** 监听接收到的数据 */
|
||||
const messageList = ref([] as { time: number; text: string }[]) // 消息列表
|
||||
const messageReverseList = computed(() => messageList.value.slice().reverse())
|
||||
watchEffect(() => {
|
||||
if (!data.value) {
|
||||
return
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
tag="今日"
|
||||
title="销售额"
|
||||
prefix="¥"
|
||||
::decimals="2"
|
||||
:decimals="2"
|
||||
:value="fenToYuan(orderComparison?.value?.orderPayPrice || 0)"
|
||||
:reference="fenToYuan(orderComparison?.reference?.orderPayPrice || 0)"
|
||||
/>
|
||||
@@ -26,8 +26,8 @@
|
||||
<ComparisonCard
|
||||
tag="今日"
|
||||
title="订单量"
|
||||
:value="fenToYuan(orderComparison?.value?.orderPayCount || 0)"
|
||||
:reference="fenToYuan(orderComparison?.reference?.orderPayCount || 0)"
|
||||
:value="orderComparison?.value?.orderPayCount || 0"
|
||||
:reference="orderComparison?.reference?.orderPayCount || 0"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="6" :sm="12" :xs="24" :loading="loading">
|
||||
|
||||
@@ -151,6 +151,7 @@ import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivit
|
||||
import BargainActivityForm from './BargainActivityForm.vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { fenToYuanFormat } from '@/utils/formatter'
|
||||
import { closeBargainActivity } from '@/api/mall/promotion/bargain/bargainActivity'
|
||||
|
||||
defineOptions({ name: 'PromotionBargainActivity' })
|
||||
|
||||
@@ -206,7 +207,7 @@ const handleClose = async (id: number) => {
|
||||
// 关闭的二次确认
|
||||
await message.confirm('确认关闭该砍价活动吗?')
|
||||
// 发起关闭
|
||||
await BargainActivityApi.closeSeckillActivity(id)
|
||||
await BargainActivityApi.closeBargainActivity(id)
|
||||
message.success('关闭成功')
|
||||
// 刷新列表
|
||||
await getList()
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
|
||||
import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
|
||||
import { SeckillConfigApi } from '@/api/mall/promotion/seckill/seckillConfig'
|
||||
import SeckillActivityForm from './SeckillActivityForm.vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { fenToYuanFormat } from '@/utils/formatter'
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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%' }"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<doc-alert title="大屏设计器" url="https://doc.iocoder.cn/report/screen/" />
|
||||
|
||||
<IFrame :src="src" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -47,7 +47,7 @@
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['infra:config:export']"
|
||||
v-hasPermi="['infra:login-log:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
@@ -85,7 +85,7 @@
|
||||
link
|
||||
type="primary"
|
||||
@click="openDetail(scope.row)"
|
||||
v-hasPermi="['infra:config:query']"
|
||||
v-hasPermi="['infra:login-log:query']"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
|
||||
@@ -16,7 +16,8 @@ export const rules = reactive({
|
||||
password: [required],
|
||||
host: [required],
|
||||
port: [required],
|
||||
sslEnable: [required]
|
||||
sslEnable: [required],
|
||||
starttlsEnable: [required]
|
||||
})
|
||||
|
||||
// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
|
||||
@@ -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
Reference in new issue
Block a user