Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
@@ -44,7 +44,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
const searchName = ref<string>(''); // 对话搜索
|
||||
const activeConversationId = ref<null | number>(null); // 选中的对话,默认为 null
|
||||
const hoverConversationId = ref<null | number>(null); // 悬浮上去的对话
|
||||
const conversationList = ref([] as AiChatConversationApi.ChatConversationVO[]); // 对话列表
|
||||
const conversationList = ref([] as AiChatConversationApi.ChatConversation[]); // 对话列表
|
||||
const conversationMap = ref<any>({}); // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const loading = ref<boolean>(false); // 加载中
|
||||
const loadingTime = ref<any>();
|
||||
@@ -118,7 +118,7 @@ async function getChatConversationList() {
|
||||
|
||||
/** 按照 creteTime 创建时间,进行分组 */
|
||||
async function getConversationGroupByCreateTime(
|
||||
list: AiChatConversationApi.ChatConversationVO[],
|
||||
list: AiChatConversationApi.ChatConversation[],
|
||||
) {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
// noinspection NonAsciiCharacters
|
||||
@@ -164,7 +164,7 @@ async function getConversationGroupByCreateTime(
|
||||
async function createConversation() {
|
||||
// 1. 新建对话
|
||||
const conversationId = await createChatConversationMy(
|
||||
{} as unknown as AiChatConversationApi.ChatConversationVO,
|
||||
{} as unknown as AiChatConversationApi.ChatConversation,
|
||||
);
|
||||
// 2. 获取对话内容
|
||||
await getChatConversationList();
|
||||
@@ -176,7 +176,7 @@ async function createConversation() {
|
||||
|
||||
/** 修改对话的标题 */
|
||||
async function updateConversationTitle(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
conversation: AiChatConversationApi.ChatConversation,
|
||||
) {
|
||||
// 1. 二次确认
|
||||
prompt({
|
||||
@@ -188,7 +188,7 @@ async function updateConversationTitle(
|
||||
await updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: scope.value,
|
||||
} as AiChatConversationApi.ChatConversationVO);
|
||||
} as AiChatConversationApi.ChatConversation);
|
||||
message.success('重命名成功');
|
||||
// 3. 刷新列表
|
||||
await getChatConversationList();
|
||||
@@ -230,7 +230,7 @@ async function updateConversationTitle(
|
||||
|
||||
/** 删除聊天对话 */
|
||||
async function deleteChatConversation(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
conversation: AiChatConversationApi.ChatConversation,
|
||||
) {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
@@ -260,9 +260,7 @@ async function handleClearConversation() {
|
||||
}
|
||||
|
||||
/** 对话置顶 */
|
||||
async function handleTop(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
async function handleTop(conversation: AiChatConversationApi.ChatConversation) {
|
||||
// 更新对话置顶
|
||||
conversation.pinned = !conversation.pinned;
|
||||
await updateChatConversationMy(conversation);
|
||||
@@ -307,7 +305,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Layout.Sider
|
||||
width="280px"
|
||||
class="!bg-primary-foreground conversation-container relative flex h-full flex-col justify-between overflow-hidden p-4"
|
||||
class="conversation-container relative flex h-full flex-col justify-between overflow-hidden p-4"
|
||||
>
|
||||
<Drawer />
|
||||
<!-- 左顶部:对话 -->
|
||||
@@ -360,7 +358,9 @@ onMounted(async () => {
|
||||
<div
|
||||
class="conversation flex cursor-pointer flex-row items-center justify-between rounded-lg px-2 leading-10"
|
||||
:class="[
|
||||
conversation.id === activeConversationId ? 'bg-gray-100' : '',
|
||||
conversation.id === activeConversationId
|
||||
? 'bg-primary-200'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<div class="title-wrapper flex items-center">
|
||||
@@ -420,7 +420,7 @@ onMounted(async () => {
|
||||
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div
|
||||
class="absolute bottom-1 left-0 right-0 mb-4 flex items-center justify-between bg-gray-50 px-5 leading-9 text-gray-400 shadow-sm"
|
||||
class="bg-card absolute bottom-1 left-0 right-0 mb-4 flex items-center justify-between px-5 leading-9 text-gray-400 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="flex cursor-pointer items-center text-gray-400"
|
||||
|
||||
@@ -17,7 +17,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiChatConversationApi.ChatConversationVO>();
|
||||
const formData = ref<AiChatConversationApi.ChatConversation>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
@@ -41,7 +41,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as AiChatConversationApi.ChatConversationVO;
|
||||
(await formApi.getValues()) as AiChatConversationApi.ChatConversation;
|
||||
try {
|
||||
await updateChatConversationMy(data);
|
||||
|
||||
@@ -59,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiChatConversationApi.ChatConversationVO>();
|
||||
const data = modalApi.getData<AiChatConversationApi.ChatConversation>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import MessageKnowledge from './MessageKnowledge.vue';
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
conversation: {
|
||||
type: Object as PropType<AiChatConversationApi.ChatConversationVO>,
|
||||
type: Object as PropType<AiChatConversationApi.ChatConversation>,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Array as PropType<AiChatMessageApi.ChatMessageVO[]>,
|
||||
type: Array as PropType<AiChatMessageApi.ChatMessage[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
@@ -95,12 +95,12 @@ async function onDelete(id: number) {
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
async function onRefresh(message: AiChatMessageApi.ChatMessageVO) {
|
||||
async function onRefresh(message: AiChatMessageApi.ChatMessage) {
|
||||
emits('onRefresh', message);
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
async function onEdit(message: AiChatMessageApi.ChatMessageVO) {
|
||||
async function onEdit(message: AiChatMessageApi.ChatMessage) {
|
||||
emits('onEdit', message);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
roleList: {
|
||||
type: Array as PropType<AiModelChatRoleApi.ChatRoleVO[]>,
|
||||
type: Array as PropType<AiModelChatRoleApi.ChatRole[]>,
|
||||
required: true,
|
||||
},
|
||||
showMore: {
|
||||
@@ -79,7 +79,7 @@ async function handleTabsScroll() {
|
||||
}"
|
||||
>
|
||||
<!-- 更多操作 -->
|
||||
<div v-if="showMore" class="absolute right-3 top-0">
|
||||
<div v-if="showMore" class="absolute right-2 top-0">
|
||||
<Dropdown>
|
||||
<Button type="link">
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
@@ -89,7 +89,7 @@ async function handleTabsScroll() {
|
||||
<Menu.Item @click="handleMoreClick(['edit', role])">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:edit" color="#787878" />
|
||||
<span>编辑</span>
|
||||
<span class="text-primary">编辑</span>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item @click="handleMoreClick(['delete', role])">
|
||||
@@ -108,12 +108,12 @@ async function handleTabsScroll() {
|
||||
<Avatar :src="role.avatar" class="h-10 w-10 overflow-hidden" />
|
||||
</div>
|
||||
|
||||
<div class="ml-2 w-full">
|
||||
<div class="ml-2 w-4/5">
|
||||
<div class="h-20">
|
||||
<div class="max-w-36 text-lg font-bold text-gray-600">
|
||||
<div class="max-w-32 text-lg font-bold">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-400">
|
||||
<div class="mt-2 text-sm">
|
||||
{{ role.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,12 @@ const myRoleParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
const myRoleList = ref<AiModelChatRoleApi.ChatRoleVO[]>([]); // my 分页大小
|
||||
const myRoleList = ref<AiModelChatRoleApi.ChatRole[]>([]); // my 分页大小
|
||||
const publicRoleParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
const publicRoleList = ref<AiModelChatRoleApi.ChatRoleVO[]>([]); // public 分页大小
|
||||
const publicRoleList = ref<AiModelChatRoleApi.ChatRole[]>([]); // public 分页大小
|
||||
const activeCategory = ref<string>('全部'); // 选择中的分类
|
||||
const categoryList = ref<string[]>([]); // 角色分类类别
|
||||
|
||||
@@ -55,7 +55,7 @@ async function handleTabsClick(tab: any) {
|
||||
|
||||
/** 获取 my role 我的角色 */
|
||||
async function getMyRole(append?: boolean) {
|
||||
const params: AiModelChatRoleApi.ChatRolePageReqVO = {
|
||||
const params: AiModelChatRoleApi.ChatRolePageReq = {
|
||||
...myRoleParams,
|
||||
name: search.value,
|
||||
publicStatus: false,
|
||||
@@ -70,7 +70,7 @@ async function getMyRole(append?: boolean) {
|
||||
|
||||
/** 获取 public role 公共角色 */
|
||||
async function getPublicRole(append?: boolean) {
|
||||
const params: AiModelChatRoleApi.ChatRolePageReqVO = {
|
||||
const params: AiModelChatRoleApi.ChatRolePageReq = {
|
||||
...publicRoleParams,
|
||||
category: activeCategory.value === '全部' ? '' : activeCategory.value,
|
||||
name: search.value,
|
||||
@@ -148,9 +148,9 @@ async function handlerCardPage(type: string) {
|
||||
/** 选择 card 角色:新建聊天对话 */
|
||||
async function handlerCardUse(role: any) {
|
||||
// 1. 创建对话
|
||||
const data: AiChatConversationApi.ChatConversationVO = {
|
||||
const data: AiChatConversationApi.ChatConversation = {
|
||||
roleId: role.id,
|
||||
} as unknown as AiChatConversationApi.ChatConversationVO;
|
||||
} as unknown as AiChatConversationApi.ChatConversation;
|
||||
const conversationId = await createChatConversationMy(data);
|
||||
|
||||
// 2. 跳转页面
|
||||
@@ -174,7 +174,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Drawer>
|
||||
<Layout
|
||||
class="absolute inset-0 flex h-full w-full flex-col overflow-hidden bg-white"
|
||||
class="bg-card absolute inset-0 flex h-full w-full flex-col overflow-hidden"
|
||||
>
|
||||
<FormModal @success="handlerAddRoleSuccess" />
|
||||
|
||||
|
||||
@@ -34,14 +34,14 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
// 聊天对话
|
||||
const conversationListRef = ref();
|
||||
const activeConversationId = ref<null | number>(null); // 选中的对话编号
|
||||
const activeConversation = ref<AiChatConversationApi.ChatConversationVO | null>(
|
||||
const activeConversation = ref<AiChatConversationApi.ChatConversation | null>(
|
||||
null,
|
||||
); // 选中的 Conversation
|
||||
const conversationInProgress = ref(false); // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
||||
|
||||
// 消息列表
|
||||
const messageRef = ref();
|
||||
const activeMessageList = ref<AiChatMessageApi.ChatMessageVO[]>([]); // 选中对话的消息列表
|
||||
const activeMessageList = ref<AiChatMessageApi.ChatMessage[]>([]); // 选中对话的消息列表
|
||||
const activeMessageListLoading = ref<boolean>(false); // activeMessageList 是否正在加载中
|
||||
const activeMessageListLoadingTimer = ref<any>(); // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中
|
||||
// 消息滚动
|
||||
@@ -65,7 +65,7 @@ async function getConversation(id: null | number) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const conversation: AiChatConversationApi.ChatConversationVO =
|
||||
const conversation: AiChatConversationApi.ChatConversation =
|
||||
await getChatConversationMy(id);
|
||||
if (!conversation) {
|
||||
return;
|
||||
@@ -81,7 +81,7 @@ async function getConversation(id: null | number) {
|
||||
* @return 是否切换成功
|
||||
*/
|
||||
async function handleConversationClick(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
conversation: AiChatConversationApi.ChatConversation,
|
||||
) {
|
||||
// 对话进行中,不允许切换
|
||||
if (conversationInProgress.value) {
|
||||
@@ -103,7 +103,7 @@ async function handleConversationClick(
|
||||
|
||||
/** 删除某个对话*/
|
||||
async function handlerConversationDelete(
|
||||
delConversation: AiChatConversationApi.ChatConversationVO,
|
||||
delConversation: AiChatConversationApi.ChatConversation,
|
||||
) {
|
||||
// 删除的对话如果是当前选中的,那么就重置
|
||||
if (activeConversationId.value === delConversation.id) {
|
||||
@@ -303,13 +303,11 @@ async function doSendMessage(content: string) {
|
||||
await doSendMessageStream({
|
||||
conversationId: activeConversationId.value,
|
||||
content,
|
||||
} as AiChatMessageApi.ChatMessageVO);
|
||||
} as AiChatMessageApi.ChatMessage);
|
||||
}
|
||||
|
||||
/** 真正执行【发送】消息操作 */
|
||||
async function doSendMessageStream(
|
||||
userMessage: AiChatMessageApi.ChatMessageVO,
|
||||
) {
|
||||
async function doSendMessageStream(userMessage: AiChatMessageApi.ChatMessage) {
|
||||
// 创建 AbortController 实例,以便中止请求
|
||||
conversationInAbortController.value = new AbortController();
|
||||
// 标记对话进行中
|
||||
@@ -326,14 +324,14 @@ async function doSendMessageStream(
|
||||
type: 'user',
|
||||
content: userMessage.content,
|
||||
createTime: new Date(),
|
||||
} as AiChatMessageApi.ChatMessageVO,
|
||||
} as AiChatMessageApi.ChatMessage,
|
||||
{
|
||||
id: -2,
|
||||
conversationId: activeConversationId.value,
|
||||
type: 'assistant',
|
||||
content: '思考中...',
|
||||
createTime: new Date(),
|
||||
} as AiChatMessageApi.ChatMessageVO,
|
||||
} as AiChatMessageApi.ChatMessage,
|
||||
);
|
||||
// 1.2 滚动到最下面
|
||||
await nextTick();
|
||||
@@ -398,12 +396,12 @@ async function stopStream() {
|
||||
}
|
||||
|
||||
/** 编辑 message:设置为 prompt,可以再次编辑 */
|
||||
function handleMessageEdit(message: AiChatMessageApi.ChatMessageVO) {
|
||||
function handleMessageEdit(message: AiChatMessageApi.ChatMessage) {
|
||||
prompt.value = message.content;
|
||||
}
|
||||
|
||||
/** 刷新 message:基于指定消息,再次发起对话 */
|
||||
function handleMessageRefresh(message: AiChatMessageApi.ChatMessageVO) {
|
||||
function handleMessageRefresh(message: AiChatMessageApi.ChatMessage) {
|
||||
doSendMessage(message.content);
|
||||
}
|
||||
|
||||
@@ -497,6 +495,7 @@ onMounted(async () => {
|
||||
<Layout class="absolute left-0 top-0 m-4 h-full w-full flex-1">
|
||||
<!-- 左侧:对话列表 -->
|
||||
<ConversationList
|
||||
class="!bg-card"
|
||||
:active-id="activeConversationId as any"
|
||||
ref="conversationListRef"
|
||||
@on-conversation-create="handleConversationCreateSuccess"
|
||||
@@ -506,9 +505,9 @@ onMounted(async () => {
|
||||
/>
|
||||
|
||||
<!-- 右侧:详情部分 -->
|
||||
<Layout class="mx-4 bg-white">
|
||||
<Layout class="bg-card mx-4">
|
||||
<Layout.Header
|
||||
class="flex items-center justify-between !bg-gray-50 shadow-none"
|
||||
class="!bg-card border-border flex items-center justify-between border-b"
|
||||
>
|
||||
<div class="text-lg font-bold">
|
||||
{{ activeConversation?.title ? activeConversation?.title : '对话' }}
|
||||
@@ -567,9 +566,9 @@ onMounted(async () => {
|
||||
</div>
|
||||
</Layout.Content>
|
||||
|
||||
<Layout.Footer class="m-0 flex flex-col !bg-white p-0">
|
||||
<Layout.Footer class="!bg-card m-0 flex flex-col p-0">
|
||||
<form
|
||||
class="my-5 mb-5 mt-2 flex flex-col rounded-xl border border-gray-200 px-2 py-2.5"
|
||||
class="border-border my-5 mb-5 mt-2 flex flex-col rounded-xl border px-2 py-2.5"
|
||||
>
|
||||
<textarea
|
||||
class="box-border h-24 resize-none overflow-auto border-none px-0 py-1 focus:outline-none"
|
||||
|
||||
@@ -46,7 +46,7 @@ export function useGridColumnsConversation(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
width: 180,
|
||||
minWidth: 180,
|
||||
slots: { default: 'userId' },
|
||||
},
|
||||
{
|
||||
@@ -141,7 +141,7 @@ export function useGridColumnsMessage(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
width: 180,
|
||||
minWidth: 180,
|
||||
slots: { default: 'userId' },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiChatConversationApi.ChatConversationVO) {
|
||||
async function handleDelete(row: AiChatConversationApi.ChatConversation) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -72,7 +72,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiChatConversationApi.ChatConversationVO>,
|
||||
} as VxeTableGridOptions<AiChatConversationApi.ChatConversation>,
|
||||
separator: false,
|
||||
});
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiChatConversationApi.ChatConversationVO) {
|
||||
async function handleDelete(row: AiChatConversationApi.ChatConversation) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -69,7 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiChatConversationApi.ChatConversationVO>,
|
||||
} as VxeTableGridOptions<AiChatConversationApi.ChatConversation>,
|
||||
separator: false,
|
||||
});
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AiImageStatusEnum } from '#/utils';
|
||||
|
||||
const props = defineProps({
|
||||
detail: {
|
||||
type: Object as PropType<AiImageApi.ImageVO>,
|
||||
type: Object as PropType<AiImageApi.Image>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
@@ -25,13 +25,13 @@ const emits = defineEmits(['onBtnClick', 'onMjBtnClick']);
|
||||
const cardImageRef = ref<any>(); // 卡片 image ref
|
||||
|
||||
/** 处理点击事件 */
|
||||
async function handleButtonClick(type: string, detail: AiImageApi.ImageVO) {
|
||||
async function handleButtonClick(type: string, detail: AiImageApi.Image) {
|
||||
emits('onBtnClick', type, detail);
|
||||
}
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
async function handleMidjourneyBtnClick(
|
||||
button: AiImageApi.ImageMidjourneyButtonsVO,
|
||||
button: AiImageApi.ImageMidjourneyButtons,
|
||||
) {
|
||||
// 确认窗体
|
||||
await confirm(`确认操作 "${button.label} ${button.emoji}" ?`);
|
||||
|
||||
@@ -23,7 +23,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const detail = ref<AiImageApi.ImageVO>({} as AiImageApi.ImageVO);
|
||||
const detail = ref<AiImageApi.Image>({} as AiImageApi.Image);
|
||||
|
||||
/** 获取图片详情 */
|
||||
async function getImageDetail(id: number) {
|
||||
|
||||
@@ -35,7 +35,7 @@ const queryParams = reactive({
|
||||
pageSize: 10,
|
||||
});
|
||||
const pageTotal = ref<number>(0); // page size
|
||||
const imageList = ref<AiImageApi.ImageVO[]>([]); // image 列表
|
||||
const imageList = ref<AiImageApi.Image[]>([]); // image 列表
|
||||
const imageListRef = ref<any>(); // ref
|
||||
// 图片轮询相关的参数(正在生成中的)
|
||||
const inProgressImageMap = ref<{}>({}); // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||
@@ -85,7 +85,7 @@ async function refreshWatchImages() {
|
||||
if (imageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const list = (await getImageListMyByIds(imageIds)) as AiImageApi.ImageVO[];
|
||||
const list = (await getImageListMyByIds(imageIds)) as AiImageApi.Image[];
|
||||
const newWatchImages: any = {};
|
||||
list.forEach((image) => {
|
||||
if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
@@ -106,7 +106,7 @@ async function refreshWatchImages() {
|
||||
/** 图片的点击事件 */
|
||||
async function handleImageButtonClick(
|
||||
type: string,
|
||||
imageDetail: AiImageApi.ImageVO,
|
||||
imageDetail: AiImageApi.Image,
|
||||
) {
|
||||
// 详情
|
||||
if (type === 'more') {
|
||||
@@ -138,14 +138,14 @@ async function handleImageButtonClick(
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
async function handleImageMidjourneyButtonClick(
|
||||
button: AiImageApi.ImageMidjourneyButtonsVO,
|
||||
imageDetail: AiImageApi.ImageVO,
|
||||
button: AiImageApi.ImageMidjourneyButtons,
|
||||
imageDetail: AiImageApi.Image,
|
||||
) {
|
||||
// 1. 构建 params 参数
|
||||
const data = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId,
|
||||
} as AiImageApi.ImageMidjourneyActionVO;
|
||||
} as AiImageApi.ImageMidjourneyAction;
|
||||
// 2. 发送 action
|
||||
await midjourneyAction(data);
|
||||
// 3. 刷新列表
|
||||
|
||||
@@ -17,8 +17,8 @@ import { AiPlatformEnum, ImageHotWords, OtherPlatformEnum } from '#/utils';
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.ModelVO>,
|
||||
default: () => [] as AiModelModelApi.ModelVO[],
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
@@ -31,7 +31,7 @@ const prompt = ref<string>(''); // 提示词
|
||||
const width = ref<number>(512); // 图片宽度
|
||||
const height = ref<number>(512); // 图片高度
|
||||
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI); // 平台
|
||||
const platformModels = ref<AiModelModelApi.ModelVO[]>([]); // 模型列表
|
||||
const platformModels = ref<AiModelModelApi.Model[]>([]); // 模型列表
|
||||
const modelId = ref<number>(); // 选中的模型
|
||||
|
||||
/** 选择热词 */
|
||||
@@ -64,7 +64,7 @@ async function handleGenerateImage() {
|
||||
width: width.value, // 图片宽度
|
||||
height: height.value, // 图片高度
|
||||
options: {},
|
||||
} as unknown as AiImageApi.ImageDrawReqVO;
|
||||
} as unknown as AiImageApi.ImageDrawReq;
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
@@ -75,7 +75,7 @@ async function handleGenerateImage() {
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.ImageVO) {
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
prompt.value = detail.prompt;
|
||||
width.value = detail.width;
|
||||
height.value = detail.height;
|
||||
@@ -85,7 +85,7 @@ async function settingValues(detail: AiImageApi.ImageVO) {
|
||||
async function handlerPlatformChange(platform: any) {
|
||||
// 根据选择的平台筛选模型
|
||||
platformModels.value = props.models.filter(
|
||||
(item: AiModelModelApi.ModelVO) => item.platform === platform,
|
||||
(item: AiModelModelApi.Model) => item.platform === platform,
|
||||
);
|
||||
modelId.value =
|
||||
platformModels.value.length > 0 && platformModels.value[0]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
import type { ImageModelVO, ImageSizeVO } from '#/utils';
|
||||
import type { ImageModel, ImageSize } from '#/utils';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.ModelVO>,
|
||||
default: () => [] as AiModelModelApi.ModelVO[],
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
@@ -50,7 +50,7 @@ async function handleHotWordClick(hotWord: string) {
|
||||
}
|
||||
|
||||
/** 选择 model 模型 */
|
||||
async function handleModelClick(model: ImageModelVO) {
|
||||
async function handleModelClick(model: ImageModel) {
|
||||
selectModel.value = model.key;
|
||||
// 可以在这里添加模型特定的处理逻辑
|
||||
// 例如,如果未来需要根据不同模型设置不同参数
|
||||
@@ -76,12 +76,12 @@ async function handleModelClick(model: ImageModelVO) {
|
||||
}
|
||||
|
||||
/** 选择 style 样式 */
|
||||
async function handleStyleClick(imageStyle: ImageModelVO) {
|
||||
async function handleStyleClick(imageStyle: ImageModel) {
|
||||
style.value = imageStyle.key;
|
||||
}
|
||||
|
||||
/** 选择 size 大小 */
|
||||
async function handleSizeClick(imageSize: ImageSizeVO) {
|
||||
async function handleSizeClick(imageSize: ImageSize) {
|
||||
selectSize.value = imageSize.key;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ async function handleGenerateImage() {
|
||||
emits('onDrawStart', AiPlatformEnum.OPENAI);
|
||||
const imageSize = Dall3SizeList.find(
|
||||
(item) => item.key === selectSize.value,
|
||||
) as ImageSizeVO;
|
||||
) as ImageSize;
|
||||
const form = {
|
||||
platform: AiPlatformEnum.OPENAI,
|
||||
prompt: prompt.value, // 提示词
|
||||
@@ -118,7 +118,7 @@ async function handleGenerateImage() {
|
||||
options: {
|
||||
style: style.value, // 图像生成的风格
|
||||
},
|
||||
} as AiImageApi.ImageDrawReqVO;
|
||||
} as AiImageApi.ImageDrawReq;
|
||||
// 发送请求
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
@@ -130,13 +130,13 @@ async function handleGenerateImage() {
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.ImageVO) {
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
prompt.value = detail.prompt;
|
||||
selectModel.value = detail.model;
|
||||
style.value = detail.options?.style;
|
||||
const imageSize = Dall3SizeList.find(
|
||||
(item) => item.key === `${detail.width}x${detail.height}`,
|
||||
) as ImageSizeVO;
|
||||
) as ImageSize;
|
||||
await handleSizeClick(imageSize);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiImageApi } from '#/api/ai/image';
|
||||
import type { AiModelModelApi } from '#/api/ai/model/model';
|
||||
import type { ImageModelVO, ImageSizeVO } from '#/utils';
|
||||
import type { ImageModel, ImageSize } from '#/utils';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -33,8 +33,8 @@ import {
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.ModelVO>,
|
||||
default: () => [] as AiModelModelApi.ModelVO[],
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']);
|
||||
@@ -64,12 +64,12 @@ async function handleHotWordClick(hotWord: string) {
|
||||
}
|
||||
|
||||
/** 点击 size 尺寸 */
|
||||
async function handleSizeClick(imageSize: ImageSizeVO) {
|
||||
async function handleSizeClick(imageSize: ImageSize) {
|
||||
selectSize.value = imageSize.key;
|
||||
}
|
||||
|
||||
/** 点击 model 模型 */
|
||||
async function handleModelClick(model: ImageModelVO) {
|
||||
async function handleModelClick(model: ImageModel) {
|
||||
selectModel.value = model.key;
|
||||
versionList.value =
|
||||
model.key === 'niji' ? NijiVersionList : MidjourneyVersions;
|
||||
@@ -99,7 +99,7 @@ async function handleGenerateImage() {
|
||||
// 发送请求
|
||||
const imageSize = MidjourneySizeList.find(
|
||||
(item) => selectSize.value === item.key,
|
||||
) as ImageSizeVO;
|
||||
) as ImageSize;
|
||||
const req = {
|
||||
prompt: prompt.value,
|
||||
modelId: matchedModel.id,
|
||||
@@ -107,7 +107,7 @@ async function handleGenerateImage() {
|
||||
height: imageSize.height,
|
||||
version: selectVersion.value,
|
||||
referImageUrl: referImageUrl.value,
|
||||
} as AiImageApi.ImageMidjourneyImagineReqVO;
|
||||
} as AiImageApi.ImageMidjourneyImagineReq;
|
||||
await midjourneyImagine(req);
|
||||
} finally {
|
||||
// 回调
|
||||
@@ -118,18 +118,18 @@ async function handleGenerateImage() {
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.ImageVO) {
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
// 提示词
|
||||
prompt.value = detail.prompt;
|
||||
// image size
|
||||
const imageSize = MidjourneySizeList.find(
|
||||
(item) => item.key === `${detail.width}:${detail.height}`,
|
||||
) as ImageSizeVO;
|
||||
) as ImageSize;
|
||||
selectSize.value = imageSize.key;
|
||||
// 选中模型
|
||||
const model = MidjourneyModels.find(
|
||||
(item) => item.key === detail.options?.model,
|
||||
) as ImageModelVO;
|
||||
) as ImageModel;
|
||||
await handleModelClick(model);
|
||||
// 版本
|
||||
selectVersion.value = versionList.value.find(
|
||||
|
||||
@@ -28,8 +28,8 @@ import {
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array<AiModelModelApi.ModelVO>,
|
||||
default: () => [] as AiModelModelApi.ModelVO[],
|
||||
type: Array<AiModelModelApi.Model>,
|
||||
default: () => [] as AiModelModelApi.Model[],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ async function handleGenerateImage() {
|
||||
clipGuidancePreset: clipGuidancePreset.value, // 文本提示相匹配的图像 CLIP
|
||||
stylePreset: stylePreset.value, // 风格
|
||||
},
|
||||
} as unknown as AiImageApi.ImageDrawReqVO;
|
||||
} as unknown as AiImageApi.ImageDrawReq;
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
@@ -117,7 +117,7 @@ async function handleGenerateImage() {
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
async function settingValues(detail: AiImageApi.ImageVO) {
|
||||
async function settingValues(detail: AiImageApi.Image) {
|
||||
prompt.value = detail.prompt;
|
||||
width.value = detail.width;
|
||||
height.value = detail.height;
|
||||
|
||||
@@ -44,7 +44,7 @@ const platformOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const models = ref<AiModelModelApi.ModelVO[]>([]); // 模型列表
|
||||
const models = ref<AiModelModelApi.Model[]>([]); // 模型列表
|
||||
|
||||
/** 绘画 start */
|
||||
const handleDrawStart = async () => {};
|
||||
@@ -55,7 +55,7 @@ const handleDrawComplete = async () => {
|
||||
};
|
||||
|
||||
/** 重新生成:将画图详情填充到对应平台 */
|
||||
const handleRegeneration = async (image: AiImageApi.ImageVO) => {
|
||||
const handleRegeneration = async (image: AiImageApi.Image) => {
|
||||
// 切换平台
|
||||
selectPlatform.value = image.platform;
|
||||
// 根据不同平台填充 image
|
||||
@@ -90,9 +90,9 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="bg-card absolute inset-0 flex h-full w-full flex-row">
|
||||
<div class="left-0 flex w-96 flex-col p-4">
|
||||
<div class="segmented flex justify-center">
|
||||
<div class="absolute inset-0 m-4 flex h-full w-full flex-row">
|
||||
<div class="bg-card left-0 mr-4 flex w-96 flex-col rounded-lg p-4">
|
||||
<div class="flex justify-center">
|
||||
<Segmented
|
||||
v-model:value="selectPlatform"
|
||||
:options="platformOptions"
|
||||
@@ -125,7 +125,7 @@ onMounted(async () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-card ml-4 flex-1">
|
||||
<div class="bg-card flex-1">
|
||||
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiImageApi.ImageVO) {
|
||||
async function handleDelete(row: AiImageApi.Image) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -41,7 +41,7 @@ async function handleDelete(row: AiImageApi.ImageVO) {
|
||||
}
|
||||
}
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: AiImageApi.ImageVO) => {
|
||||
const handleUpdatePublicStatusChange = async (row: AiImageApi.Image) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.publicStatus ? '公开' : '私有';
|
||||
@@ -82,7 +82,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiImageApi.ImageVO>,
|
||||
} as VxeTableGridOptions<AiImageApi.Image>,
|
||||
});
|
||||
onMounted(async () => {
|
||||
// 获得下拉数据
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Image, Input, Pagination } from 'ant-design-vue';
|
||||
import { getImagePageMy } from '#/api/ai/image';
|
||||
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<AiImageApi.ImageVO[]>([]); // 列表的数据
|
||||
const list = ref<AiImageApi.Image[]>([]); // 列表的数据
|
||||
const total = ref(0); // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
|
||||
@@ -49,7 +49,7 @@ function handleEdit(id: number) {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiKnowledgeDocumentApi.KnowledgeDocumentVO) {
|
||||
async function handleDelete(row: AiKnowledgeDocumentApi.KnowledgeDocument) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
@@ -74,7 +74,7 @@ const handleSegment = (id: number) => {
|
||||
};
|
||||
/** 修改是否发布 */
|
||||
const handleStatusChange = async (
|
||||
row: AiKnowledgeDocumentApi.KnowledgeDocumentVO,
|
||||
row: AiKnowledgeDocumentApi.KnowledgeDocument,
|
||||
) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
@@ -120,7 +120,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiKnowledgeDocumentApi.KnowledgeDocumentVO>,
|
||||
} as VxeTableGridOptions<AiKnowledgeDocumentApi.KnowledgeDocument>,
|
||||
});
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
|
||||
@@ -34,12 +34,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: AiKnowledgeKnowledgeApi.KnowledgeVO) {
|
||||
function handleEdit(row: AiKnowledgeKnowledgeApi.Knowledge) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiKnowledgeKnowledgeApi.KnowledgeVO) {
|
||||
async function handleDelete(row: AiKnowledgeKnowledgeApi.Knowledge) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
@@ -98,7 +98,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiKnowledgeKnowledgeApi.KnowledgeVO>,
|
||||
} as VxeTableGridOptions<AiKnowledgeKnowledgeApi.Knowledge>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiKnowledgeKnowledgeApi.KnowledgeVO>();
|
||||
const formData = ref<AiKnowledgeKnowledgeApi.Knowledge>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['AI 知识库'])
|
||||
@@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as AiKnowledgeKnowledgeApi.KnowledgeVO;
|
||||
(await formApi.getValues()) as AiKnowledgeKnowledgeApi.Knowledge;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateKnowledge(data)
|
||||
@@ -66,7 +66,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiKnowledgeKnowledgeApi.KnowledgeVO>();
|
||||
const data = modalApi.getData<AiKnowledgeKnowledgeApi.Knowledge>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: AiKnowledgeKnowledgeApi.KnowledgeVO) {
|
||||
function handleEdit(row: AiKnowledgeKnowledgeApi.Knowledge) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiKnowledgeKnowledgeApi.KnowledgeVO) {
|
||||
async function handleDelete(row: AiKnowledgeKnowledgeApi.Knowledge) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -89,13 +89,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiKnowledgeKnowledgeApi.KnowledgeVO>,
|
||||
} as VxeTableGridOptions<AiKnowledgeKnowledgeApi.Knowledge>,
|
||||
});
|
||||
|
||||
/** 修改是否发布 */
|
||||
async function handleStatusChange(
|
||||
row: AiKnowledgeSegmentApi.KnowledgeSegmentVO,
|
||||
) {
|
||||
async function handleStatusChange(row: AiKnowledgeSegmentApi.KnowledgeSegment) {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.status ? '启用' : '禁用';
|
||||
|
||||
@@ -18,7 +18,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiKnowledgeSegmentApi.KnowledgeSegmentVO>();
|
||||
const formData = ref<AiKnowledgeSegmentApi.KnowledgeSegment>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['分段'])
|
||||
@@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as AiKnowledgeSegmentApi.KnowledgeSegmentVO;
|
||||
(await formApi.getValues()) as AiKnowledgeSegmentApi.KnowledgeSegment;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateKnowledgeSegment(data)
|
||||
@@ -66,7 +66,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiKnowledgeSegmentApi.KnowledgeSegmentVO>();
|
||||
const data = modalApi.getData<AiKnowledgeSegmentApi.KnowledgeSegment>();
|
||||
if (!data || !data.id) {
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
|
||||
@@ -27,7 +27,7 @@ function directGenerate(existPrompt: string) {
|
||||
isEnd.value = true;
|
||||
}
|
||||
/** 提交生成 */
|
||||
function submit(data: AiMindmapApi.AiMindMapGenerateReqVO) {
|
||||
function submit(data: AiMindmapApi.AiMindMapGenerateReq) {
|
||||
isGenerating.value = true;
|
||||
isStart.value = true;
|
||||
isEnd.value = false;
|
||||
|
||||
@@ -31,7 +31,7 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiMindmapApi.MindMapVO) {
|
||||
async function handleDelete(row: AiMindmapApi.MindMap) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -73,9 +73,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiMindmapApi.MindMapVO>,
|
||||
} as VxeTableGridOptions<AiMindmapApi.MindMap>,
|
||||
});
|
||||
async function openPreview(row: AiMindmapApi.MindMapVO) {
|
||||
async function openPreview(row: AiMindmapApi.MindMap) {
|
||||
previewVisible.value = false;
|
||||
drawerApi.open();
|
||||
await nextTick();
|
||||
|
||||
@@ -29,12 +29,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: AiModelApiKeyApi.ApiKeyVO) {
|
||||
function handleEdit(row: AiModelApiKeyApi.ApiKey) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiModelApiKeyApi.ApiKeyVO) {
|
||||
async function handleDelete(row: AiModelApiKeyApi.ApiKey) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
@@ -77,7 +77,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiModelApiKeyApi.ApiKeyVO>,
|
||||
} as VxeTableGridOptions<AiModelApiKeyApi.ApiKey>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiModelApiKeyApi.ApiKeyVO>();
|
||||
const formData = ref<AiModelApiKeyApi.ApiKey>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['API 密钥'])
|
||||
@@ -42,7 +42,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as AiModelApiKeyApi.ApiKeyVO;
|
||||
const data = (await formApi.getValues()) as AiModelApiKeyApi.ApiKey;
|
||||
try {
|
||||
await (formData.value?.id ? updateApiKey(data) : createApiKey(data));
|
||||
// 关闭并提示
|
||||
@@ -59,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiModelApiKeyApi.ApiKeyVO>();
|
||||
const data = modalApi.getData<AiModelApiKeyApi.ApiKey>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: AiModelChatRoleApi.ChatRoleVO) {
|
||||
function handleEdit(row: AiModelChatRoleApi.ChatRole) {
|
||||
formModalApi.setData({ formType: 'update', ...row }).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiModelChatRoleApi.ChatRoleVO) {
|
||||
async function handleDelete(row: AiModelChatRoleApi.ChatRole) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
@@ -77,7 +77,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiModelChatRoleApi.ChatRoleVO>,
|
||||
} as VxeTableGridOptions<AiModelChatRoleApi.ChatRole>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiModelChatRoleApi.ChatRoleVO>();
|
||||
const formData = ref<AiModelChatRoleApi.ChatRole>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['聊天角色'])
|
||||
@@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as AiModelChatRoleApi.ChatRoleVO;
|
||||
const data = (await formApi.getValues()) as AiModelChatRoleApi.ChatRole;
|
||||
try {
|
||||
await (formData.value?.id ? updateChatRole(data) : createChatRole(data));
|
||||
// 关闭并提示
|
||||
@@ -64,7 +64,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiModelChatRoleApi.ChatRoleVO>();
|
||||
const data = modalApi.getData<AiModelChatRoleApi.ChatRole>();
|
||||
if (!data || !data.id) {
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { $t } from '#/locales';
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const apiKeyList = ref([] as AiModelApiKeyApi.ApiKeyVO[]);
|
||||
const apiKeyList = ref([] as AiModelApiKeyApi.ApiKey[]);
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
@@ -34,12 +34,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: AiModelModelApi.ModelVO) {
|
||||
function handleEdit(row: AiModelModelApi.Model) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiModelModelApi.ModelVO) {
|
||||
async function handleDelete(row: AiModelModelApi.Model) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
@@ -82,7 +82,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiModelModelApi.ModelVO>,
|
||||
} as VxeTableGridOptions<AiModelModelApi.Model>,
|
||||
});
|
||||
onMounted(async () => {
|
||||
// 获得下拉数据
|
||||
|
||||
@@ -15,7 +15,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiModelModelApi.ModelVO>();
|
||||
const formData = ref<AiModelModelApi.Model>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['模型配置'])
|
||||
@@ -43,7 +43,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as AiModelModelApi.ModelVO;
|
||||
const data = (await formApi.getValues()) as AiModelModelApi.Model;
|
||||
try {
|
||||
await (formData.value?.id ? updateModel(data) : createModel(data));
|
||||
// 关闭并提示
|
||||
@@ -60,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiModelModelApi.ModelVO>();
|
||||
const data = modalApi.getData<AiModelModelApi.Model>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: AiModelToolApi.ToolVO) {
|
||||
function handleEdit(row: AiModelToolApi.Tool) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiModelToolApi.ToolVO) {
|
||||
async function handleDelete(row: AiModelToolApi.Tool) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
@@ -77,7 +77,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiModelToolApi.ToolVO>,
|
||||
} as VxeTableGridOptions<AiModelToolApi.Tool>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { $t } from '#/locales';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiModelToolApi.ToolVO>();
|
||||
const formData = ref<AiModelToolApi.Tool>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['工具'])
|
||||
@@ -42,7 +42,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as AiModelToolApi.ToolVO;
|
||||
const data = (await formApi.getValues()) as AiModelToolApi.Tool;
|
||||
try {
|
||||
await (formData.value?.id ? updateTool(data) : createTool(data));
|
||||
// 关闭并提示
|
||||
@@ -59,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiModelToolApi.ToolVO>();
|
||||
const data = modalApi.getData<AiModelToolApi.Tool>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ defineExpose({
|
||||
>
|
||||
<Textarea
|
||||
v-model:value="formData.desc"
|
||||
:autosize="{ minRows: 6, maxRows: 6 }"
|
||||
:auto-size="{ minRows: 6, maxRows: 6 }"
|
||||
:maxlength="1200"
|
||||
:show-count="true"
|
||||
placeholder="一首关于糟糕分手的欢快歌曲"
|
||||
|
||||
@@ -28,7 +28,7 @@ defineExpose({
|
||||
<Title title="歌词" desc="自己编写歌词或使用Ai生成歌词,两节/8行效果最佳">
|
||||
<Textarea
|
||||
v-model:value="formData.lyric"
|
||||
:autosize="{ minRows: 6, maxRows: 6 }"
|
||||
:auto-size="{ minRows: 6, maxRows: 6 }"
|
||||
:maxlength="1200"
|
||||
:show-count="true"
|
||||
placeholder="请输入您自己的歌词"
|
||||
@@ -60,7 +60,7 @@ defineExpose({
|
||||
>
|
||||
<Textarea
|
||||
v-model="formData.style"
|
||||
:autosize="{ minRows: 4, maxRows: 4 }"
|
||||
:auto-size="{ minRows: 4, maxRows: 4 }"
|
||||
:maxlength="256"
|
||||
show-count
|
||||
placeholder="输入音乐风格(英文)"
|
||||
|
||||
@@ -100,15 +100,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
minWidth: 180,
|
||||
slots: { default: 'content' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '绘画状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.AI_IMAGE_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
title: '时长(秒)',
|
||||
@@ -139,9 +130,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'tags',
|
||||
title: '风格标签',
|
||||
minWidth: 180,
|
||||
slots: { default: 'tags' },
|
||||
cellRender: {
|
||||
name: 'CellTags',
|
||||
},
|
||||
},
|
||||
{
|
||||
minWidth: 100,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { confirm, DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, message, Switch, Tag } from 'ant-design-vue';
|
||||
import { Button, message, Switch } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteMusic, getMusicPage, updateMusic } from '#/api/ai/music';
|
||||
@@ -24,7 +24,7 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiMusicApi.MusicVO) {
|
||||
async function handleDelete(row: AiMusicApi.Music) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -41,7 +41,7 @@ async function handleDelete(row: AiMusicApi.MusicVO) {
|
||||
}
|
||||
}
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: AiMusicApi.MusicVO) => {
|
||||
const handleUpdatePublicStatusChange = async (row: AiMusicApi.Music) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.publicStatus ? '公开' : '私有';
|
||||
@@ -82,7 +82,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiMusicApi.MusicVO>,
|
||||
} as VxeTableGridOptions<AiMusicApi.Music>,
|
||||
});
|
||||
onMounted(async () => {
|
||||
// 获得下拉数据
|
||||
@@ -101,9 +101,9 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<template #userId="{ row }">
|
||||
<span>{{
|
||||
userList.find((item) => item.id === row.userId)?.nickname
|
||||
}}</span>
|
||||
<span>
|
||||
{{ userList.find((item) => item.id === row.userId)?.nickname }}
|
||||
</span>
|
||||
</template>
|
||||
<template #content="{ row }">
|
||||
<Button
|
||||
@@ -141,11 +141,6 @@ onMounted(async () => {
|
||||
:disabled="row.status !== AiMusicStatusEnum.SUCCESS"
|
||||
/>
|
||||
</template>
|
||||
<template #tags="{ row }">
|
||||
<Tag v-for="tag in row.tags" :key="tag" class="ml-1">
|
||||
{{ tag }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
|
||||
import Tag from './Tag.vue';
|
||||
|
||||
type TabType = AiWriteApi.WriteVO['type'];
|
||||
type TabType = AiWriteApi.Write['type'];
|
||||
|
||||
defineProps<{
|
||||
isWriting: boolean;
|
||||
@@ -26,7 +26,7 @@ defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(e: 'example', param: 'reply' | 'write'): void;
|
||||
(e: 'reset'): void;
|
||||
(e: 'submit', params: Partial<AiWriteApi.WriteVO>): void;
|
||||
(e: 'submit', params: Partial<AiWriteApi.Write>): void;
|
||||
}>();
|
||||
|
||||
function omit(obj: Record<string, any>, keysToOmit: string[]) {
|
||||
@@ -74,7 +74,7 @@ const [DefineLabel, ReuseLabel] = createReusableTemplate<{
|
||||
label: string;
|
||||
}>();
|
||||
|
||||
const initData: AiWriteApi.WriteVO = {
|
||||
const initData: AiWriteApi.Write = {
|
||||
type: 1,
|
||||
prompt: '',
|
||||
originalContent: '',
|
||||
@@ -84,10 +84,10 @@ const initData: AiWriteApi.WriteVO = {
|
||||
format: 1,
|
||||
};
|
||||
|
||||
const formData = ref<AiWriteApi.WriteVO>({ ...initData });
|
||||
const formData = ref<AiWriteApi.Write>({ ...initData });
|
||||
|
||||
/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 */
|
||||
const recordFormData = {} as Record<AiWriteTypeEnum, AiWriteApi.WriteVO>;
|
||||
const recordFormData = {} as Record<AiWriteTypeEnum, AiWriteApi.Write>;
|
||||
/** 切换tab */
|
||||
function switchTab(value: TabType) {
|
||||
if (value !== selectedTab.value) {
|
||||
@@ -123,8 +123,10 @@ function submit() {
|
||||
<template>
|
||||
<DefineTab v-slot="{ active, text, itemClick }">
|
||||
<span
|
||||
:class="active ? 'text-black shadow-md' : 'hover:bg-gray-200'"
|
||||
class="relative z-10 inline-block w-1/2 cursor-pointer rounded-full text-center leading-7 text-gray-400 hover:text-black"
|
||||
:class="
|
||||
active ? 'bg-primary-600 text-white shadow-md' : 'hover:bg-primary-200'
|
||||
"
|
||||
class="relative z-10 inline-block w-1/2 cursor-pointer rounded-full text-center leading-7 hover:text-black"
|
||||
@click="itemClick"
|
||||
>
|
||||
{{ text }}
|
||||
@@ -136,7 +138,7 @@ function submit() {
|
||||
<span>{{ label }}</span>
|
||||
<span
|
||||
v-if="hint"
|
||||
class="flex cursor-pointer select-none items-center text-xs text-purple-500"
|
||||
class="text-primary-500 flex cursor-pointer select-none items-center text-xs"
|
||||
@click="hintClick"
|
||||
>
|
||||
<IconifyIcon icon="lucide:circle-help" />
|
||||
@@ -145,14 +147,14 @@ function submit() {
|
||||
</h3>
|
||||
</DefineLabel>
|
||||
<div class="flex flex-col" v-bind="$attrs">
|
||||
<div class="flex w-full justify-center bg-gray-50 pt-2">
|
||||
<div class="z-10 w-72 rounded-full bg-gray-200 p-1">
|
||||
<div class="bg-card flex w-full justify-center pt-2">
|
||||
<div class="bg-card z-10 w-72 rounded-full p-1">
|
||||
<div
|
||||
:class="
|
||||
selectedTab === AiWriteTypeEnum.REPLY &&
|
||||
'after:translate-x-[100%] after:transform'
|
||||
"
|
||||
class="relative flex items-center after:absolute after:left-0 after:top-0 after:block after:h-7 after:w-1/2 after:rounded-full after:bg-white after:transition-transform after:content-['']"
|
||||
class="after:bg-card relative flex items-center after:absolute after:left-0 after:top-0 after:block after:h-7 after:w-1/2 after:rounded-full after:transition-transform after:content-['']"
|
||||
>
|
||||
<ReuseTab
|
||||
v-for="tab in tabs"
|
||||
@@ -166,7 +168,7 @@ function submit() {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border h-full w-96 flex-grow overflow-y-auto bg-gray-50 px-7 pb-2 lg:block"
|
||||
class="bg-card box-border h-full w-96 flex-grow overflow-y-auto px-7 pb-2 lg:block"
|
||||
>
|
||||
<div>
|
||||
<template v-if="selectedTab === 1">
|
||||
@@ -233,11 +235,7 @@ function submit() {
|
||||
<Button :disabled="isWriting" class="mr-2" @click="reset">
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
:loading="isWriting"
|
||||
class="bg-purple-500 text-white"
|
||||
@click="submit"
|
||||
>
|
||||
<Button type="primary" :loading="isWriting" @click="submit">
|
||||
生成
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -54,22 +54,18 @@ watch(copied, (val) => {
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Card class="my-card flex h-full flex-col">
|
||||
<Card class="flex h-full flex-col">
|
||||
<template #title>
|
||||
<h3 class="m-0 flex shrink-0 items-center justify-between px-7">
|
||||
<span>预览</span>
|
||||
<!-- 展示在右上角 -->
|
||||
<Button
|
||||
class="flex bg-purple-500 text-white"
|
||||
type="primary"
|
||||
v-show="showCopy"
|
||||
@click="copyContent"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</div>
|
||||
</template>
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
复制
|
||||
</Button>
|
||||
</h3>
|
||||
@@ -79,7 +75,7 @@ watch(copied, (val) => {
|
||||
class="hide-scroll-bar box-border h-full overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="relative box-border min-h-full w-full flex-grow bg-white p-3 sm:p-7"
|
||||
class="bg-card relative box-border min-h-full w-full flex-grow p-3 sm:p-7"
|
||||
>
|
||||
<!-- 终止生成内容的按钮 -->
|
||||
<Button
|
||||
@@ -98,7 +94,7 @@ watch(copied, (val) => {
|
||||
<Textarea
|
||||
id="inputId"
|
||||
v-model:value="compContent"
|
||||
autosize
|
||||
auto-size
|
||||
:bordered="false"
|
||||
placeholder="生成的内容……"
|
||||
/>
|
||||
|
||||
@@ -21,8 +21,10 @@ const emits = defineEmits<{
|
||||
<span
|
||||
v-for="tag in props.tags"
|
||||
:key="tag.value"
|
||||
class="mb-2 cursor-pointer rounded border-2 border-solid border-gray-200 bg-gray-200 px-1 text-xs leading-6"
|
||||
:class="modelValue === tag.value && '!border-purple-500 !text-purple-500'"
|
||||
class="bg-card border-card-100 mb-2 cursor-pointer rounded border-2 border-solid px-1 text-xs leading-6"
|
||||
:class="
|
||||
modelValue === tag.value && '!border-primary-500 !text-primary-500'
|
||||
"
|
||||
@click="emits('update:modelValue', tag.value)"
|
||||
>
|
||||
{{ tag.label }}
|
||||
|
||||
@@ -24,7 +24,7 @@ function stopStream() {
|
||||
/** 执行写作 */
|
||||
const rightRef = ref<InstanceType<typeof Right>>();
|
||||
|
||||
function submit(data: Partial<AiWriteApi.WriteVO>) {
|
||||
function submit(data: Partial<AiWriteApi.Write>) {
|
||||
abortController.value = new AbortController();
|
||||
writeResult.value = '';
|
||||
isWriting.value = true;
|
||||
@@ -66,10 +66,10 @@ function reset() {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="absolute bottom-0 left-0 right-0 top-0 flex">
|
||||
<div class="absolute bottom-0 left-0 right-0 top-0 m-4 flex">
|
||||
<Left
|
||||
:is-writing="isWriting"
|
||||
class="h-full"
|
||||
class="mr-4 h-full rounded-lg"
|
||||
@submit="submit"
|
||||
@reset="reset"
|
||||
@example="handleExampleClick"
|
||||
|
||||
@@ -23,7 +23,7 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: AiWriteApi.AiWritePageReqVO) {
|
||||
async function handleDelete(row: AiWriteApi.AiWritePageReq) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
@@ -65,7 +65,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiWriteApi.AiWritePageReqVO>,
|
||||
} as VxeTableGridOptions<AiWriteApi.AiWritePageReq>,
|
||||
});
|
||||
onMounted(async () => {
|
||||
// 获得下拉数据
|
||||
|
||||
@@ -21,7 +21,7 @@ const props = defineProps<{
|
||||
type: 'copy' | 'create' | 'edit';
|
||||
}>();
|
||||
|
||||
// 流程表单详情
|
||||
/** 流程表单详情 */
|
||||
const flowFormConfig = ref();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
@@ -31,7 +31,7 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
|
||||
const designerRef = ref<InstanceType<typeof FcDesigner>>();
|
||||
|
||||
// 表单设计器配置
|
||||
/** 表单设计器配置 */
|
||||
const designerConfig = ref({
|
||||
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
|
||||
autoActive: true, // 是否自动选中拖入的组件
|
||||
@@ -80,7 +80,7 @@ const currentFormId = computed(() => {
|
||||
});
|
||||
|
||||
// 加载表单配置
|
||||
async function loadFormConfig(id: number | string) {
|
||||
async function loadFormConfig(id: number) {
|
||||
try {
|
||||
const formDetail = await getFormDetail(id);
|
||||
flowFormConfig.value = formDetail;
|
||||
@@ -106,8 +106,7 @@ async function initializeDesigner() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @ziye:注释使用 /** */ 风格,高亮更明显哈,方法注释;
|
||||
// 保存表单
|
||||
/** 保存表单 */
|
||||
function handleSave() {
|
||||
formModalApi
|
||||
.setData({
|
||||
@@ -118,7 +117,7 @@ function handleSave() {
|
||||
.open();
|
||||
}
|
||||
|
||||
// 返回列表页
|
||||
/** 返回列表页 */
|
||||
function onBack() {
|
||||
router.push({
|
||||
path: '/bpm/manager/form',
|
||||
@@ -137,7 +136,11 @@ onMounted(() => {
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onBack" />
|
||||
|
||||
<FcDesigner class="my-designer" ref="designerRef" :config="designerConfig">
|
||||
<FcDesigner
|
||||
class="h-full min-h-[500px]"
|
||||
ref="designerRef"
|
||||
:config="designerConfig"
|
||||
>
|
||||
<template #handle>
|
||||
<Button size="small" type="primary" @click="handleSave">
|
||||
<IconifyIcon icon="mdi:content-save" />
|
||||
@@ -147,10 +150,3 @@ onMounted(() => {
|
||||
</FcDesigner>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.my-designer {
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,13 @@ export function useGridColumns(): VxeTableGridOptions<BpmProcessDefinitionApi.Pr
|
||||
field: 'icon',
|
||||
title: '流程图标',
|
||||
minWidth: 100,
|
||||
slots: { default: 'icon' },
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'startUsers',
|
||||
@@ -47,7 +53,9 @@ export function useGridColumns(): VxeTableGridOptions<BpmProcessDefinitionApi.Pr
|
||||
field: 'version',
|
||||
title: '流程版本',
|
||||
minWidth: 80,
|
||||
slots: { default: 'version' },
|
||||
cellRender: {
|
||||
name: 'CellTag',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'deploymentTime',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, Tag, Tooltip } from 'ant-design-vue';
|
||||
import { Button, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getProcessDefinitionPage } from '#/api/bpm/definition';
|
||||
@@ -93,16 +93,6 @@ onMounted(() => {
|
||||
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
</template>
|
||||
<Grid table-title="流程定义列表">
|
||||
<template #icon="{ row }">
|
||||
<Image
|
||||
v-if="row.icon"
|
||||
:src="row.icon"
|
||||
:width="24"
|
||||
:height="24"
|
||||
class="rounded"
|
||||
/>
|
||||
<span v-else> 无图标 </span>
|
||||
</template>
|
||||
<template #startUsers="{ row }">
|
||||
<template v-if="!row.startUsers?.length">全部可见</template>
|
||||
<template v-else-if="row.startUsers.length === 1">
|
||||
@@ -135,9 +125,6 @@ onMounted(() => {
|
||||
</Button>
|
||||
<span v-else>暂无表单</span>
|
||||
</template>
|
||||
<template #version="{ row }">
|
||||
<Tag>v{{ row.version }}</Tag>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -102,7 +102,7 @@ defineExpose({ validate });
|
||||
DICT_TYPE.BPM_MODEL_FORM_TYPE,
|
||||
'number',
|
||||
)"
|
||||
:key="dict.value as string"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
@@ -186,12 +186,12 @@ export function useDetailFormSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
label: '开始时间',
|
||||
field: 'startTime',
|
||||
content: (data) => dayjs(data?.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
content: (data) => formatDateTime(data?.startTime) as string,
|
||||
},
|
||||
{
|
||||
label: '结束时间',
|
||||
field: 'endTime',
|
||||
content: (data) => dayjs(data?.endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
content: (data) => formatDateTime(data?.endTime) as string,
|
||||
},
|
||||
{
|
||||
label: '原因',
|
||||
|
||||
@@ -88,6 +88,12 @@ export function useGridColumns(
|
||||
onTaskClick: (task: BpmProcessInstanceApi.Task) => void,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '流程编号',
|
||||
minWidth: 320,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '流程名称',
|
||||
@@ -167,12 +173,6 @@ export function useGridColumns(
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: 'id',
|
||||
title: '流程编号',
|
||||
minWidth: 320,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
|
||||
@@ -41,12 +41,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '发起人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发起时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '当前任务',
|
||||
|
||||
@@ -93,12 +93,13 @@ onMounted(async () => {
|
||||
<Card class="w-1/5">
|
||||
<List item-layout="horizontal" :data-source="leftSides">
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<List.Item
|
||||
@click="sideClick(item)"
|
||||
class="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<List.Item.Meta>
|
||||
<template #title>
|
||||
<a @click="sideClick(item)">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
{{ item.name }}
|
||||
</template>
|
||||
</List.Item.Meta>
|
||||
<template #extra>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { erpPriceMultiply } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -35,6 +38,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -50,7 +54,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => !values.customerId,
|
||||
disabled: (values) => values.customerDefault,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -103,8 +107,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
rules: 'required',
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'discountPercent',
|
||||
@@ -114,15 +119,19 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
rules: 'required',
|
||||
rules: z.number().min(0).max(100).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '折扣后金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
disabled: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['totalProductPrice', 'discountPercent'],
|
||||
disabled: () => true,
|
||||
trigger(values, form) {
|
||||
const discountPrice =
|
||||
erpPriceMultiply(
|
||||
@@ -157,69 +166,83 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
minWidth: 140,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
formatter: 'formatDate',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDate',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
fixed: 'right',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
fixed: 'right',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -86,7 +86,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBusinessPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBusinessPageByCustomer({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.customerId,
|
||||
...formValues,
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
getBusinessPageByContact,
|
||||
getBusinessPageByCustomer,
|
||||
} from '#/api/crm/business';
|
||||
import { createContactBusinessList } from '#/api/crm/contact';
|
||||
import {
|
||||
createContactBusinessList,
|
||||
deleteContactBusinessList,
|
||||
} from '#/api/crm/contact';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
@@ -73,7 +76,7 @@ async function handleDeleteContactBusinessList() {
|
||||
content: `确定要将${checkedRows.value.map((item) => item.name).join(',')}解除关联吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await createContactBusinessList({
|
||||
const res = await deleteContactBusinessList({
|
||||
contactId: props.bizId,
|
||||
businessIds: checkedRows.value.map((item) => item.id),
|
||||
});
|
||||
@@ -121,14 +124,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
query: async ({ page }, formValues) => {
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
return await getBusinessPageByCustomer({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.customerId,
|
||||
...formValues,
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_CONTACT) {
|
||||
return await getBusinessPageByContact({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
contactId: props.contactId,
|
||||
...formValues,
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getDeptList } from '#/api/system/dept';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
@@ -38,17 +36,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请选择应用部门',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
help: '不选择部门时,默认全公司生效',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
fieldName: 'statuses',
|
||||
label: '阶段设置',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBusinessStatusPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Input, InputNumber, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
createBusinessStatus,
|
||||
DEFAULT_STATUSES,
|
||||
getBusinessStatus,
|
||||
updateBusinessStatus,
|
||||
} from '#/api/crm/business/status';
|
||||
@@ -49,6 +51,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const data =
|
||||
(await formApi.getValues()) as CrmBusinessStatusApi.BusinessStatus;
|
||||
try {
|
||||
if (formData.value?.statuses && formData.value.statuses.length > 0) {
|
||||
data.statuses = formData.value.statuses;
|
||||
data.statuses.splice(-3, 3);
|
||||
}
|
||||
await (formData.value?.id
|
||||
? updateBusinessStatus(data)
|
||||
: createBusinessStatus(data));
|
||||
@@ -62,30 +68,159 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmBusinessStatusApi.BusinessStatus>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getBusinessStatus(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
if (!data || !data.id) {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
deptIds: [],
|
||||
statuses: [],
|
||||
};
|
||||
addStatus();
|
||||
} else {
|
||||
formData.value = await getBusinessStatus(data.id as number);
|
||||
if (
|
||||
!formData.value?.statuses?.length ||
|
||||
formData.value?.statuses?.length === 0
|
||||
) {
|
||||
addStatus();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
|
||||
await formApi.setValues(formData.value as any);
|
||||
gridApi.grid.reloadData(
|
||||
(formData.value!.statuses =
|
||||
formData.value?.statuses?.concat(DEFAULT_STATUSES)) as any,
|
||||
);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 添加状态 */
|
||||
async function addStatus() {
|
||||
formData.value!.statuses!.unshift({
|
||||
name: '',
|
||||
percent: undefined,
|
||||
} as any);
|
||||
await nextTick();
|
||||
gridApi.grid.reloadData(formData.value!.statuses as any);
|
||||
}
|
||||
|
||||
/** 删除状态 */
|
||||
async function deleteStatusArea(row: any, rowIndex: number) {
|
||||
gridApi.grid.remove(row);
|
||||
formData.value!.statuses!.splice(rowIndex, 1);
|
||||
gridApi.grid.reloadData(formData.value!.statuses as any);
|
||||
}
|
||||
|
||||
/** 表格配置 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'defaultStatus',
|
||||
title: '阶段',
|
||||
minWidth: 100,
|
||||
slots: { default: 'defaultStatus' },
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '阶段名称',
|
||||
minWidth: 100,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'percent',
|
||||
title: '赢单率(%)',
|
||||
minWidth: 100,
|
||||
slots: { default: 'percent' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
],
|
||||
data: formData.value?.statuses?.concat(DEFAULT_STATUSES),
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'row_id',
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/2">
|
||||
<Form class="mx-4" />
|
||||
<Form class="mx-4">
|
||||
<template #statuses>
|
||||
<Grid class="w-full">
|
||||
<template #defaultStatus="{ row, rowIndex }">
|
||||
<span>
|
||||
{{ row.defaultStatus ? '结束' : `阶段${rowIndex + 1}` }}
|
||||
</span>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<Input v-if="!row.endStatus" v-model:value="row.name" />
|
||||
<span v-else>{{ row.name }}</span>
|
||||
</template>
|
||||
<template #percent="{ row }">
|
||||
<InputNumber
|
||||
v-if="!row.endStatus"
|
||||
v-model:value="row.percent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
/>
|
||||
<span v-else>{{ row.percent }}</span>
|
||||
</template>
|
||||
<template #actions="{ row, rowIndex }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create'),
|
||||
type: 'link',
|
||||
ifShow: () => !row.endStatus,
|
||||
onClick: addStatus,
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
ifShow: () => !row.endStatus,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: deleteStatusArea.bind(null, row, rowIndex),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -46,6 +49,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
allowClear: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -164,6 +168,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'name',
|
||||
title: '线索名称',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: {
|
||||
default: 'name',
|
||||
},
|
||||
@@ -171,6 +176,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'source',
|
||||
title: '线索来源',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
@@ -179,22 +185,27 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
@@ -203,6 +214,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
@@ -211,34 +223,41 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -79,7 +79,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCluePage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
|
||||
@@ -26,9 +26,10 @@ const [Form, formApi] = useVbenForm({
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
labelWidth: 100,
|
||||
},
|
||||
// 一共3列
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getSimpleContactList } from '#/api/crm/contact';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
@@ -9,6 +11,7 @@ import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -35,6 +38,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
@@ -43,7 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
@@ -188,17 +192,20 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'name',
|
||||
title: '联系人姓名',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
@@ -207,26 +214,32 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
title: '职位',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '关键决策人',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
@@ -235,34 +248,41 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'parentId',
|
||||
title: '直属上级',
|
||||
minWidth: 120,
|
||||
slots: { default: 'parentId' },
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -90,7 +90,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContactPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
|
||||
@@ -108,7 +108,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContactPageByCustomer({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.customerId,
|
||||
...formValues,
|
||||
|
||||
@@ -121,14 +121,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
query: async ({ page }, formValues) => {
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
return await getContactPageByCustomer({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.bizId,
|
||||
...formValues,
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
|
||||
return await getContactPageByBusiness({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
businessId: props.bizId,
|
||||
...formValues,
|
||||
|
||||
@@ -60,6 +60,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmContactApi.Contact>();
|
||||
if (!data || !data.id) {
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { erpPriceMultiply, floatToFixed2 } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
@@ -12,6 +13,7 @@ import { DICT_TYPE } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -27,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '保存时自动生成',
|
||||
disabled: () => true,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -50,6 +52,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -58,22 +61,45 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'businessId',
|
||||
label: '商机名称',
|
||||
component: 'ApiSelect',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
api: getSimpleBusinessList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
options: [],
|
||||
placeholder: '请选择商机',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['customerId'],
|
||||
disabled: (values) => !values.customerId,
|
||||
async componentProps(values) {
|
||||
if (!values.customerId) {
|
||||
return {
|
||||
options: [],
|
||||
placeholder: '请选择客户',
|
||||
};
|
||||
}
|
||||
const res = await getSimpleBusinessList();
|
||||
const list = res.filter(
|
||||
(item) => item.customerId === values.customerId,
|
||||
);
|
||||
return {
|
||||
options: list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
placeholder: '请选择商机',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'orderDate',
|
||||
@@ -117,17 +143,39 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
fieldName: 'signContactId',
|
||||
label: '客户签约人',
|
||||
component: 'ApiSelect',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
api: getSimpleContactList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
options: [],
|
||||
placeholder: '请选择客户签约人',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['customerId'],
|
||||
disabled: (values) => !values.customerId,
|
||||
async componentProps(values) {
|
||||
if (!values.customerId) {
|
||||
return {
|
||||
options: [],
|
||||
placeholder: '请选择客户',
|
||||
};
|
||||
}
|
||||
const res = await getSimpleContactList();
|
||||
const list = res.filter(
|
||||
(item) => item.customerId === values.customerId,
|
||||
);
|
||||
return {
|
||||
options: list.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
placeholder: '请选择客户签约人',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
@@ -150,25 +198,31 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'discountPercent',
|
||||
label: '整单折扣(%)',
|
||||
component: 'InputNumber',
|
||||
rules: z.number().min(0).max(100).default(0),
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
rules: z.number().min(0).max(100).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '折扣后金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
disabled: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['totalProductPrice', 'discountPercent'],
|
||||
disabled: () => true,
|
||||
trigger(values, form) {
|
||||
const discountPrice =
|
||||
erpPriceMultiply(
|
||||
@@ -203,9 +257,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
@@ -223,20 +279,20 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
title: '合同名称',
|
||||
field: 'name',
|
||||
minWidth: 150,
|
||||
minWidth: 220,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
minWidth: 240,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
title: '商机名称',
|
||||
field: 'businessName',
|
||||
minWidth: 150,
|
||||
minWidth: 220,
|
||||
slots: { default: 'businessName' },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -72,13 +72,13 @@ async function handleDelete(row: CrmContractApi.Contract) {
|
||||
/** 提交审核 */
|
||||
async function handleSubmit(row: CrmContractApi.Contract) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.submitting', [row.name]),
|
||||
content: '提交审核中...',
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await submitContract(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.submitSuccess', [row.name]),
|
||||
content: '提交审核成功',
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
@@ -127,7 +127,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContractPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
|
||||
@@ -45,7 +45,15 @@ function onRefresh() {
|
||||
|
||||
/** 创建合同 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
formModalApi
|
||||
.setData(
|
||||
props.bizType === BizTypeEnum.CRM_CUSTOMER
|
||||
? {
|
||||
customerId: props.bizId,
|
||||
}
|
||||
: { businessId: props.bizId },
|
||||
)
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 查看合同详情 */
|
||||
@@ -63,14 +71,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
query: async ({ page }, formValues) => {
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
return await getContractPageByCustomer({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.bizId,
|
||||
...formValues,
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_CONTACT) {
|
||||
return await getContractPageByBusiness({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
businessId: props.bizId,
|
||||
...formValues,
|
||||
|
||||
@@ -90,6 +90,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmContractApi.Contract>();
|
||||
if (!data || !data.id) {
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -27,7 +30,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '客户来源',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE),
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -47,6 +50,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -74,7 +78,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '客户行业',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY),
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -82,7 +86,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '客户级别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL),
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -154,6 +158,8 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'name',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
align: 'left',
|
||||
minWidth: 280,
|
||||
slots: {
|
||||
default: 'name',
|
||||
},
|
||||
@@ -161,6 +167,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'source',
|
||||
title: '客户来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
@@ -169,22 +176,32 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
title: '地址',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
@@ -193,6 +210,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
@@ -201,30 +219,36 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -96,7 +96,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
|
||||
@@ -73,7 +73,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerLimitConfigPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: configType.value,
|
||||
...formValues,
|
||||
|
||||
@@ -8,13 +8,20 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { Button, Card, message, Tabs } from 'ant-design-vue';
|
||||
import { Card, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { getCustomer, updateCustomerDealStatus } from '#/api/crm/customer';
|
||||
import {
|
||||
getCustomer,
|
||||
lockCustomer,
|
||||
putCustomerPool,
|
||||
receiveCustomer,
|
||||
updateCustomerDealStatus,
|
||||
} from '#/api/crm/customer';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { BusinessDetailsList } from '#/views/crm/business';
|
||||
import { ContactDetailsList } from '#/views/crm/contact';
|
||||
import { ContractDetailsList } from '#/views/crm/contract';
|
||||
@@ -99,18 +106,45 @@ function handleTransfer() {
|
||||
}
|
||||
|
||||
/** 锁定客户 */
|
||||
function handleLock() {
|
||||
transferModalApi.setData({ id: customerId.value }).open();
|
||||
}
|
||||
|
||||
/** 解锁客户 */
|
||||
function handleUnlock() {
|
||||
transferModalApi.setData({ id: customerId.value }).open();
|
||||
function handleLock(lockStatus: boolean): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定锁定客户【${customer.value.name}】吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await lockCustomer(customerId.value, lockStatus);
|
||||
if (res) {
|
||||
message.success(lockStatus ? '锁定客户成功' : '解锁客户成功');
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(lockStatus ? '锁定客户失败' : '解锁客户失败'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 领取客户 */
|
||||
function handleReceive() {
|
||||
transferModalApi.setData({ id: customerId.value }).open();
|
||||
function handleReceive(): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定领取客户【${customer.value.name}】吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await receiveCustomer([customerId.value]);
|
||||
if (res) {
|
||||
message.success('领取客户成功');
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('领取客户失败'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 分配客户 */
|
||||
@@ -119,8 +153,24 @@ function handleDistributeForm() {
|
||||
}
|
||||
|
||||
/** 客户放入公海 */
|
||||
function handlePutPool() {
|
||||
transferModalApi.setData({ id: customerId.value }).open();
|
||||
function handlePutPool(): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定将客户【${customer.value.name}】放入公海吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await putCustomerPool(customerId.value);
|
||||
if (res) {
|
||||
message.success('放入公海成功');
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('放入公海失败'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新成交状态操作 */
|
||||
@@ -161,61 +211,62 @@ onMounted(() => {
|
||||
<TransferModal @success="loadCustomerDetail" />
|
||||
<DistributeModal @success="loadCustomerDetail" />
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="permissionListRef?.validateWrite"
|
||||
type="primary"
|
||||
@click="handleEdit"
|
||||
v-access:code="['crm:customer:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="permissionListRef?.validateOwnerUser"
|
||||
type="primary"
|
||||
@click="handleTransfer"
|
||||
>
|
||||
转移
|
||||
</Button>
|
||||
<Button
|
||||
v-if="permissionListRef?.validateWrite"
|
||||
@click="handleUpdateDealStatus"
|
||||
>
|
||||
更改成交状态
|
||||
</Button>
|
||||
<Button
|
||||
v-if="customer.lockStatus && permissionListRef?.validateOwnerUser"
|
||||
@click="handleUnlock"
|
||||
>
|
||||
解锁
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!customer.lockStatus && permissionListRef?.validateOwnerUser"
|
||||
@click="handleLock"
|
||||
>
|
||||
锁定
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!customer.ownerUserId"
|
||||
type="primary"
|
||||
@click="handleReceive"
|
||||
>
|
||||
领取
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!customer.ownerUserId"
|
||||
type="primary"
|
||||
@click="handleDistributeForm"
|
||||
>
|
||||
分配
|
||||
</Button>
|
||||
<Button
|
||||
v-if="customer.ownerUserId && permissionListRef?.validateOwnerUser"
|
||||
@click="handlePutPool"
|
||||
>
|
||||
放入公海
|
||||
</Button>
|
||||
</div>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.edit'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:customer:update'],
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
label: '转移',
|
||||
type: 'primary',
|
||||
ifShow: permissionListRef?.validateOwnerUser,
|
||||
onClick: handleTransfer,
|
||||
},
|
||||
{
|
||||
label: '更改成交状态',
|
||||
type: 'default',
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleUpdateDealStatus,
|
||||
},
|
||||
{
|
||||
label: '锁定',
|
||||
type: 'default',
|
||||
ifShow:
|
||||
!customer.lockStatus && permissionListRef?.validateOwnerUser,
|
||||
onClick: handleLock.bind(null, true),
|
||||
},
|
||||
{
|
||||
label: '解锁',
|
||||
type: 'default',
|
||||
ifShow: customer.lockStatus && permissionListRef?.validateOwnerUser,
|
||||
onClick: handleLock.bind(null, false),
|
||||
},
|
||||
{
|
||||
label: '领取',
|
||||
type: 'primary',
|
||||
ifShow: !customer.ownerUserId,
|
||||
onClick: handleReceive,
|
||||
},
|
||||
{
|
||||
label: '分配',
|
||||
type: 'default',
|
||||
ifShow: !customer.ownerUserId,
|
||||
onClick: handleDistributeForm,
|
||||
},
|
||||
{
|
||||
label: '放入公海',
|
||||
type: 'default',
|
||||
ifShow:
|
||||
!!customer.ownerUserId && permissionListRef?.validateOwnerUser,
|
||||
onClick: handlePutPool,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<Card class="min-h-[10%]">
|
||||
<Description :data="customer" />
|
||||
|
||||
@@ -40,7 +40,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
pool: true,
|
||||
...formValues,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
@@ -10,6 +11,8 @@ import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
@@ -39,6 +42,7 @@ const [Form, formApi] = useVbenForm({
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
@@ -10,6 +11,7 @@ import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -31,9 +33,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '负责人',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
fieldNames: { label: 'nickname', value: 'id' },
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -124,15 +130,18 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'name',
|
||||
title: '产品名称',
|
||||
minWidth: 240,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '产品类型',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'unit',
|
||||
title: '产品单位',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
|
||||
@@ -141,15 +150,18 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'no',
|
||||
title: '产品编码',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '价格(元)',
|
||||
formatter: 'formatAmount2',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '产品描述',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
@@ -158,24 +170,29 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_PRODUCT_STATUS },
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -112,7 +112,7 @@ watch(
|
||||
item.sellingPrice = item.contractPrice;
|
||||
});
|
||||
}
|
||||
gridApi.grid?.loadData(tableData.value);
|
||||
gridApi.grid.reloadData(tableData.value);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getContractSimpleList } from '#/api/crm/contract';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getReceivablePlanSimpleList } from '#/api/crm/receivable/plan';
|
||||
import {
|
||||
getReceivablePlan,
|
||||
getReceivablePlanSimpleList,
|
||||
} from '#/api/crm/receivable/plan';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -22,7 +28,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'no',
|
||||
label: '回款编号',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '保存时自动生成',
|
||||
disabled: true,
|
||||
@@ -34,11 +39,14 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择负责人',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
@@ -46,9 +54,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
@@ -68,7 +78,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
options: contracts.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
disabled: item.auditStatus === 20,
|
||||
disabled: item.auditStatus !== 20,
|
||||
})),
|
||||
placeholder: '请选择合同',
|
||||
} as any;
|
||||
@@ -97,6 +107,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
value: item.id,
|
||||
})),
|
||||
placeholder: '请选择回款期数',
|
||||
onChange: async (value: any) => {
|
||||
const plan = await getReceivablePlan(value);
|
||||
values.returnTime = plan?.returnTime;
|
||||
values.price = plan?.price;
|
||||
values.returnType = plan?.returnType;
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
},
|
||||
@@ -159,9 +175,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -72,13 +72,13 @@ async function handleDelete(row: CrmReceivableApi.Receivable) {
|
||||
/** 提交审核 */
|
||||
async function handleSubmit(row: CrmReceivableApi.Receivable) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.submitting', [row.no]),
|
||||
content: '提交审核中...',
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await submitReceivable(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.submitSuccess', [row.no]),
|
||||
content: '提交审核成功',
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
@@ -122,7 +122,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
|
||||
@@ -33,7 +33,12 @@ function onRefresh() {
|
||||
|
||||
/** 创建回款 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
formModalApi
|
||||
.setData({
|
||||
contractId: props.contractId,
|
||||
customerId: props.customerId,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 编辑回款 */
|
||||
|
||||
@@ -66,6 +66,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData();
|
||||
if (!data) {
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
const { receivable, plan } = data;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { floatToFixed2 } from '@vben/utils';
|
||||
|
||||
import { getContractSimpleList } from '#/api/crm/contract';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
@@ -15,23 +19,43 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'contractId',
|
||||
label: '合同',
|
||||
component: 'ApiSelect',
|
||||
component: 'Select',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
options: [],
|
||||
placeholder: '请选择合同',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['customerId'],
|
||||
disabled: (values) => !values.customerId,
|
||||
async componentProps(values) {
|
||||
if (!values.customerId) {
|
||||
return {
|
||||
options: [],
|
||||
placeholder: '请选择客户',
|
||||
};
|
||||
}
|
||||
const res = await getContractSimpleList(values.customerId);
|
||||
return {
|
||||
options: res.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
placeholder: '请选择合同',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'period',
|
||||
@@ -42,6 +66,24 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => !values.id,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'price',
|
||||
label: '计划回款金额',
|
||||
@@ -60,6 +102,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择计划回款日期',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -102,9 +147,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
@@ -224,7 +271,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
width: 180,
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePlanPage({
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
@@ -195,8 +195,6 @@ function onChangeSceneType(key: number | string) {
|
||||
auth: ['crm:receivable-plan:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
|
||||
@@ -40,7 +40,12 @@ function onRefresh() {
|
||||
|
||||
/** 创建回款计划 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
formModalApi
|
||||
.setData({
|
||||
contractId: props.contractId,
|
||||
customerId: props.customerId,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 创建回款 */
|
||||
@@ -80,7 +85,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const queryParams: CrmReceivablePlanApi.PlanPageParam = {
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
|
||||
@@ -66,6 +66,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmReceivablePlanApi.Plan>();
|
||||
if (!data || !data.id) {
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
|
||||
529
apps/web-antd/src/views/crm/statistics/customer/chartOptions.ts
Normal file
529
apps/web-antd/src/views/crm/statistics/customer/chartOptions.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
import { DICT_TYPE, getDictLabel } from '#/utils';
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'conversionStat': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '客户转化率',
|
||||
type: 'line',
|
||||
data: res.map((item: any) => {
|
||||
return {
|
||||
name: item.time,
|
||||
value: item.customerCreateCount
|
||||
? (
|
||||
(item.customerDealCount / item.customerCreateCount) *
|
||||
100
|
||||
).toFixed(2)
|
||||
: 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '客户转化率分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '转化率(%)',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
name: '日期',
|
||||
data: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'customerSummary': {
|
||||
return {
|
||||
grid: {
|
||||
bottom: '5%',
|
||||
containLabel: true,
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '5 %',
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
data: res.map((item: any) => item.customerCreateCount),
|
||||
},
|
||||
{
|
||||
name: '成交客户数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((item: any) => item.customerDealCount),
|
||||
},
|
||||
],
|
||||
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: res.map((item: any) => item.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'dealCycleByArea': {
|
||||
const data = res.map((s: any) => {
|
||||
return {
|
||||
areaName: s.areaName,
|
||||
customerDealCycle: s.customerDealCycle,
|
||||
customerDealCount: s.customerDealCount,
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
type: 'bar',
|
||||
data: data.map((s: any) => s.customerDealCycle),
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
name: '成交客户数',
|
||||
type: 'bar',
|
||||
data: data.map((s: any) => s.customerDealCount),
|
||||
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: data.map((s: any) => s.areaName),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'dealCycleByProduct': {
|
||||
const data = res.map((s: any) => {
|
||||
return {
|
||||
productName: s.productName ?? '未知',
|
||||
customerDealCycle: s.customerDealCount,
|
||||
customerDealCount: s.customerDealCount,
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
type: 'bar',
|
||||
data: data.map((s: any) => s.customerDealCycle),
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
name: '成交客户数',
|
||||
type: 'bar',
|
||||
data: data.map((s: any) => s.customerDealCount),
|
||||
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: data.map((s: any) => s.productName),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'dealCycleByUser': {
|
||||
const customerDealCycleByDate = res.customerDealCycleByDate;
|
||||
const customerDealCycleByUser = res.customerDealCycleByUser;
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
type: 'bar',
|
||||
data: customerDealCycleByDate.map((s: any) => s.customerDealCycle),
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
name: '成交客户数',
|
||||
type: 'bar',
|
||||
data: customerDealCycleByUser.map((s: any) => s.customerDealCount),
|
||||
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: customerDealCycleByDate.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'followUpSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '跟进客户数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
data: res.map((s: any) => s.followUpCustomerCount),
|
||||
},
|
||||
{
|
||||
name: '跟进次数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) => s.followUpRecordCount),
|
||||
},
|
||||
],
|
||||
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: '日期',
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
data: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'followUpType': {
|
||||
return {
|
||||
title: {
|
||||
text: '客户跟进方式分析',
|
||||
left: 'center',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b} : {c}% ',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '客户跟进方式分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '跟进方式',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: res.map((s: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_FOLLOW_UP_TYPE,
|
||||
s.followUpType,
|
||||
),
|
||||
value: s.followUpRecordCount,
|
||||
};
|
||||
}),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
case 'poolSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '进入公海客户数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
data: res.map((s: any) => s.customerPutCount),
|
||||
},
|
||||
{
|
||||
name: '公海领取客户数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) => s.customerTakeCount),
|
||||
},
|
||||
],
|
||||
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: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
396
apps/web-antd/src/views/crm/statistics/customer/data.ts
Normal file
396
apps/web-antd/src/views/crm/statistics/customer/data.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import {
|
||||
beginOfDay,
|
||||
endOfDay,
|
||||
erpCalculatePercentage,
|
||||
formatDateTime,
|
||||
handleTree,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const customerSummaryTabs = [
|
||||
{
|
||||
tab: '客户总量分析',
|
||||
key: 'customerSummary',
|
||||
},
|
||||
{
|
||||
tab: '客户跟进次数分析',
|
||||
key: 'followUpSummary',
|
||||
},
|
||||
{
|
||||
tab: '客户跟进方式分析',
|
||||
key: 'followUpType',
|
||||
},
|
||||
{
|
||||
tab: '客户转化率分析',
|
||||
key: 'conversionStat',
|
||||
},
|
||||
{
|
||||
tab: '公海客户分析',
|
||||
key: 'poolSummary',
|
||||
},
|
||||
{
|
||||
tab: '员工客户成交周期分析',
|
||||
key: 'dealCycleByUser',
|
||||
},
|
||||
{
|
||||
tab: '地区客户成交周期分析',
|
||||
key: 'dealCycleByArea',
|
||||
},
|
||||
{
|
||||
tab: '产品客户成交周期分析',
|
||||
key: 'dealCycleByProduct',
|
||||
},
|
||||
];
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'times',
|
||||
label: '时间范围',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
},
|
||||
{
|
||||
fieldName: 'interval',
|
||||
label: '时间间隔',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
||||
},
|
||||
defaultValue: 2,
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
label: '归属部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '员工',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
allowClear: true,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
activeTabName: any,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
switch (activeTabName) {
|
||||
case 'conversionStat': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'contractName',
|
||||
title: '合同名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '合同总金额',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'receivablePrice',
|
||||
title: '回款金额',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
title: '客户来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'creatorUserName',
|
||||
title: '创建人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
title: '下单日期',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'customerSummary': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '员工姓名',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'customerCreateCount',
|
||||
title: '新增客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCount',
|
||||
title: '成交客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealRate',
|
||||
title: '客户成交率(%)',
|
||||
minWidth: 200,
|
||||
formatter: ({ row }) => {
|
||||
return erpCalculatePercentage(
|
||||
row.customerDealCount,
|
||||
row.customerCreateCount,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contractPrice',
|
||||
title: '合同总金额',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'receivablePrice',
|
||||
title: '回款金额',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'creceivablePrice',
|
||||
title: '未回款金额',
|
||||
minWidth: 200,
|
||||
formatter: ({ row }) => {
|
||||
return erpCalculatePercentage(
|
||||
row.receivablePrice,
|
||||
row.contractPrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'ccreceivablePrice',
|
||||
title: '回款完成率(%)',
|
||||
formatter: ({ row }) => {
|
||||
return erpCalculatePercentage(
|
||||
row.receivablePrice,
|
||||
row.contractPrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'dealCycleByArea': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
title: '区域',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCycle',
|
||||
title: '成交周期(天)',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCount',
|
||||
title: '成交客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'dealCycleByProduct': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'productName',
|
||||
title: '产品名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCycle',
|
||||
title: '成交周期(天)',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCount',
|
||||
title: '成交客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'dealCycleByUser': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '日期',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCycle',
|
||||
title: '成交周期(天)',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerDealCount',
|
||||
title: '成交客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'followUpSummary': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '员工姓名',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'followUpRecordCount',
|
||||
title: '跟进次数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'followUpCustomerCount',
|
||||
title: '跟进客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'followUpType': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'followUpType',
|
||||
title: '跟进方式',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_FOLLOW_UP_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'followUpRecordCount',
|
||||
title: '个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'portion',
|
||||
title: '占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'poolSummary': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '员工姓名',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerPutCount',
|
||||
title: '进入公海客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'customerTakeCount',
|
||||
title: '公海领取客户数',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getChartDatas, getDatas } from '#/api/crm/statistics/customer';
|
||||
|
||||
import { getChartOptions } from './chartOptions';
|
||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const activeTabName = ref('customerSummary');
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(activeTabName.value),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues) => {
|
||||
const res = await getChartDatas(activeTabName.value, formValues);
|
||||
renderEcharts(getChartOptions(activeTabName.value, res));
|
||||
return await getDatas(activeTabName.value, formValues);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
|
||||
});
|
||||
|
||||
async function handleTabChange(key: any) {
|
||||
activeTabName.value = key;
|
||||
gridApi.setGridOptions({
|
||||
columns: useGridColumns(key),
|
||||
});
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/customer/index.vue"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/customer/index.vue
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs v-model:active-key="activeTabName" @change="handleTabChange">
|
||||
<Tabs.TabPane
|
||||
v-for="item in customerSummaryTabs"
|
||||
:key="item.key"
|
||||
:tab="item.tab"
|
||||
:force-render="true"
|
||||
/>
|
||||
</Tabs>
|
||||
<EchartsUI class="mb-20 h-full w-full" ref="chartRef" />
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
271
apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts
Normal file
271
apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { erpCalculatePercentage } from '@vben/utils';
|
||||
|
||||
export function getChartOptions(
|
||||
activeTabName: any,
|
||||
active: boolean,
|
||||
res: any,
|
||||
): any {
|
||||
switch (activeTabName) {
|
||||
case 'businessInversionRateSummary': {
|
||||
return {
|
||||
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: res.map((s: any) => s.time),
|
||||
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: res.map((s: any) =>
|
||||
erpCalculatePercentage(s.businessWinCount, s.businessCount),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '商机总数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
barWidth: 15,
|
||||
data: res.map((s: any) => s.businessCount),
|
||||
},
|
||||
{
|
||||
name: '赢单商机数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
barWidth: 15,
|
||||
data: res.map((s: any) => s.businessWinCount),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
case 'businessSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 30,
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '新增商机数量',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
data: res.map((s: any) => s.businessCreateCount),
|
||||
},
|
||||
{
|
||||
name: '新增商机金额',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) => s.totalPrice),
|
||||
},
|
||||
],
|
||||
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: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'funnel': {
|
||||
// tips:写死 value 值是为了保持漏斗顺序不变
|
||||
const list: { name: string; value: number }[] = [];
|
||||
if (active) {
|
||||
list.push(
|
||||
{ value: 60, name: `客户-${res.customerCount || 0}个` },
|
||||
{ value: 40, name: `商机-${res.businessCount || 0}个` },
|
||||
{ value: 20, name: `赢单-${res.businessWinCount || 0}个` },
|
||||
);
|
||||
} else {
|
||||
list.push(
|
||||
{
|
||||
value: res.customerCount || 0,
|
||||
name: `客户-${res.customerCount || 0}个`,
|
||||
},
|
||||
{
|
||||
value: res.businessCount || 0,
|
||||
name: `商机-${res.businessCount || 0}个`,
|
||||
},
|
||||
{
|
||||
value: res.businessWinCount || 0,
|
||||
name: `赢单-${res.businessWinCount || 0}个`,
|
||||
},
|
||||
);
|
||||
}
|
||||
return {
|
||||
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: list,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
266
apps/web-antd/src/views/crm/statistics/funnel/data.ts
Normal file
266
apps/web-antd/src/views/crm/statistics/funnel/data.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
|
||||
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const customerSummaryTabs = [
|
||||
{
|
||||
tab: '销售漏斗分析',
|
||||
key: 'funnel',
|
||||
},
|
||||
{
|
||||
tab: '新增商机分析',
|
||||
key: 'businessSummary',
|
||||
},
|
||||
{
|
||||
tab: '商机转化率分析',
|
||||
key: 'businessInversionRateSummary',
|
||||
},
|
||||
];
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'times',
|
||||
label: '时间范围',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
},
|
||||
{
|
||||
fieldName: 'interval',
|
||||
label: '时间间隔',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
||||
},
|
||||
defaultValue: 2,
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
label: '归属部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '员工',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
allowClear: true,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
activeTabName: any,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
switch (activeTabName) {
|
||||
case 'businessInversionRateSummary': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
minWidth: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'businessSummary': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 200,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
minWidth: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'funnel': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'endStatus',
|
||||
title: '阶段',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'businessCount',
|
||||
title: '商机数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机总金额(元)',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmStatisticsFunnelApi } from '#/api/crm/statistics/funnel';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Button, ButtonGroup, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getChartDatas, getDatas } from '#/api/crm/statistics/funnel';
|
||||
|
||||
import { getChartOptions } from './chartOptions';
|
||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const activeTabName = ref('funnel');
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const active = ref(true);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(activeTabName.value),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const res = await getChartDatas(activeTabName.value, formValues);
|
||||
renderEcharts(
|
||||
getChartOptions(activeTabName.value, active.value, res),
|
||||
);
|
||||
return await getDatas(activeTabName.value, {
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmStatisticsFunnelApi.BusinessSummaryByDate>,
|
||||
});
|
||||
|
||||
async function handleTabChange(key: any) {
|
||||
activeTabName.value = key;
|
||||
gridApi.setGridOptions({
|
||||
columns: useGridColumns(key),
|
||||
pagerConfig: {
|
||||
enabled: activeTabName.value !== 'funnelRef',
|
||||
},
|
||||
});
|
||||
gridApi.reload();
|
||||
}
|
||||
|
||||
function handleActive(value: boolean) {
|
||||
active.value = value;
|
||||
renderEcharts(
|
||||
getChartOptions(
|
||||
activeTabName.value,
|
||||
active.value,
|
||||
gridApi.formApi.getValues(),
|
||||
),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/funnel/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/funnel/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs v-model:active-key="activeTabName" @change="handleTabChange">
|
||||
<Tabs.TabPane
|
||||
v-for="item in customerSummaryTabs"
|
||||
:key="item.key"
|
||||
:tab="item.tab"
|
||||
:force-render="true"
|
||||
/>
|
||||
</Tabs>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
:type="active ? 'primary' : 'default'"
|
||||
v-if="activeTabName === 'funnel'"
|
||||
@click="handleActive(true)"
|
||||
>
|
||||
客户视角
|
||||
</Button>
|
||||
<Button
|
||||
:type="active ? 'default' : 'primary'"
|
||||
v-if="activeTabName === 'funnel'"
|
||||
@click="handleActive(false)"
|
||||
>
|
||||
动态视角
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<EchartsUI class="mb-20 h-2/5 w-full" ref="chartRef" />
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'ContractCountPerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '当月合同数量(个)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.currentMonthCount),
|
||||
},
|
||||
{
|
||||
name: '上月合同数量(个)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.lastMonthCount),
|
||||
},
|
||||
{
|
||||
name: '去年同月合同数量(个)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.lastYearCount),
|
||||
},
|
||||
{
|
||||
name: '环比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) =>
|
||||
s.lastMonthCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((s.currentMonthCount - s.lastMonthCount) /
|
||||
s.lastMonthCount) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '同比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) =>
|
||||
s.lastYearCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((s.currentMonthCount - s.lastYearCount) /
|
||||
s.lastYearCount) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
},
|
||||
],
|
||||
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: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'ContractPricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '当月合同金额(元)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.currentMonthCount),
|
||||
},
|
||||
{
|
||||
name: '上月合同金额(元)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.lastMonthCount),
|
||||
},
|
||||
{
|
||||
name: '去年同月合同金额(元)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.lastYearCount),
|
||||
},
|
||||
{
|
||||
name: '环比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) =>
|
||||
s.lastMonthCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((s.currentMonthCount - s.lastMonthCount) /
|
||||
s.lastMonthCount) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '同比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) =>
|
||||
s.lastYearCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((s.currentMonthCount - s.lastYearCount) /
|
||||
s.lastYearCount) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
},
|
||||
],
|
||||
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: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'ReceivablePricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
series: [
|
||||
{
|
||||
name: '当月回款金额(元)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.currentMonthCount),
|
||||
},
|
||||
{
|
||||
name: '上月回款金额(元)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.lastMonthCount),
|
||||
},
|
||||
{
|
||||
name: '去年同月回款金额(元)',
|
||||
type: 'line',
|
||||
data: res.map((s: any) => s.lastYearCount),
|
||||
},
|
||||
{
|
||||
name: '环比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) =>
|
||||
s.lastMonthCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((s.currentMonthCount - s.lastMonthCount) /
|
||||
s.lastMonthCount) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '同比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: res.map((s: any) =>
|
||||
s.lastYearCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((s.currentMonthCount - s.lastYearCount) /
|
||||
s.lastYearCount) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
},
|
||||
],
|
||||
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: res.map((s: any) => s.time),
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
74
apps/web-antd/src/views/crm/statistics/performance/data.ts
Normal file
74
apps/web-antd/src/views/crm/statistics/performance/data.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
|
||||
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const customerSummaryTabs = [
|
||||
{
|
||||
tab: '员工合同数量统计',
|
||||
key: 'ContractCountPerformance',
|
||||
},
|
||||
{
|
||||
tab: '员工合同金额统计',
|
||||
key: 'ContractPricePerformance',
|
||||
},
|
||||
{
|
||||
tab: '员工回款金额统计',
|
||||
key: 'ReceivablePricePerformance',
|
||||
},
|
||||
];
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'times',
|
||||
label: '时间范围',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
picker: 'year',
|
||||
showTime: false,
|
||||
format: 'YYYY',
|
||||
ranges: {},
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
|
||||
formatDateTime(endOfDay(new Date(new Date().getFullYear(), 11, 31))),
|
||||
] as [Date, Date],
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
label: '归属部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '员工',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
allowClear: true,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,28 +1,156 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getContractCountPerformance,
|
||||
getContractPricePerformance,
|
||||
getReceivablePricePerformance,
|
||||
} from '#/api/crm/statistics/performance';
|
||||
|
||||
import { getChartOptions } from './chartOptions';
|
||||
import { customerSummaryTabs, useGridFormSchema } from './data';
|
||||
|
||||
const activeTabName = ref('ContractCountPerformance');
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
handleSubmit: async () => {
|
||||
await handleTabChange(activeTabName.value);
|
||||
},
|
||||
},
|
||||
gridOptions: {
|
||||
columns: [],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
data: [],
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
|
||||
});
|
||||
|
||||
async function handleTabChange(key: any) {
|
||||
activeTabName.value = key;
|
||||
const params = (await gridApi.formApi.getValues()) as any;
|
||||
let data: any[] = [];
|
||||
const columnsData: any[] = [];
|
||||
let tableData: any[] = [];
|
||||
switch (key) {
|
||||
case 'ContractCountPerformance': {
|
||||
tableData = [
|
||||
{ title: '当月合同数量统计(个)' },
|
||||
{ title: '上月合同数量统计(个)' },
|
||||
{ title: '去年当月合同数量统计(个)' },
|
||||
{ title: '环比增长率(%)' },
|
||||
{ title: '同比增长率(%)' },
|
||||
];
|
||||
data = await getContractCountPerformance(params);
|
||||
break;
|
||||
}
|
||||
case 'ContractPricePerformance': {
|
||||
tableData = [
|
||||
{ title: '当月合同金额统计(元)' },
|
||||
{ title: '上月合同金额统计(元)' },
|
||||
{ title: '去年当月合同金额统计(元)' },
|
||||
{ title: '环比增长率(%)' },
|
||||
{ title: '同比增长率(%)' },
|
||||
];
|
||||
data = await getContractPricePerformance(params);
|
||||
break;
|
||||
}
|
||||
case 'ReceivablePricePerformance': {
|
||||
tableData = [
|
||||
{ title: '当月回款金额统计(元)' },
|
||||
{ title: '上月回款金额统计(元)' },
|
||||
{ title: '去年当月回款金额统计(元)' },
|
||||
{ title: '环比增长率(%)' },
|
||||
{ title: '同比增长率(%)' },
|
||||
];
|
||||
data = await getReceivablePricePerformance(params);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const columnObj = {
|
||||
title: '日期',
|
||||
field: 'title',
|
||||
minWidth: 200,
|
||||
align: 'left',
|
||||
};
|
||||
columnsData.splice(0); // 清空数组
|
||||
columnsData.push(columnObj);
|
||||
data.forEach((item: any, index: number) => {
|
||||
const columnObj = { title: item.time, field: `field${index}` };
|
||||
columnsData.push(columnObj);
|
||||
tableData[0][`field${index}`] = item.currentMonthCount;
|
||||
tableData[1][`field${index}`] = item.lastMonthCount;
|
||||
tableData[2][`field${index}`] = item.lastYearCount;
|
||||
tableData[3][`field${index}`] =
|
||||
item.lastMonthCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((item.currentMonthCount - item.lastMonthCount) /
|
||||
item.lastMonthCount) *
|
||||
100
|
||||
).toFixed(2);
|
||||
tableData[4][`field${index}`] =
|
||||
item.lastYearCount === 0
|
||||
? 'NULL'
|
||||
: (
|
||||
((item.currentMonthCount - item.lastYearCount) /
|
||||
item.lastYearCount) *
|
||||
100
|
||||
).toFixed(2);
|
||||
});
|
||||
renderEcharts(getChartOptions(key, data), true);
|
||||
gridApi.grid.reloadColumn(columnsData);
|
||||
gridApi.grid.reloadData(tableData);
|
||||
}
|
||||
onMounted(() => {
|
||||
handleTabChange(activeTabName.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/performance/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/performance/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs v-model:active-key="activeTabName" @change="handleTabChange">
|
||||
<Tabs.TabPane
|
||||
v-for="item in customerSummaryTabs"
|
||||
:key="item.key"
|
||||
:tab="item.tab"
|
||||
:force-render="true"
|
||||
/>
|
||||
</Tabs>
|
||||
<EchartsUI class="mb-20 h-full w-full" ref="chartRef" />
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
439
apps/web-antd/src/views/crm/statistics/portrait/chartOptions.ts
Normal file
439
apps/web-antd/src/views/crm/statistics/portrait/chartOptions.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
import { DICT_TYPE, getDictLabel } from '#/utils';
|
||||
|
||||
function areaReplace(areaName: string) {
|
||||
if (!areaName) {
|
||||
return areaName;
|
||||
}
|
||||
return areaName
|
||||
.replace('维吾尔自治区', '')
|
||||
.replace('壮族自治区', '')
|
||||
.replace('回族自治区', '')
|
||||
.replace('自治区', '')
|
||||
.replace('省', '');
|
||||
}
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'area': {
|
||||
const data = res.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
areaName: areaReplace(item.areaName),
|
||||
};
|
||||
});
|
||||
let leftMin = 0;
|
||||
let leftMax = 0;
|
||||
let rightMin = 0;
|
||||
let rightMax = 0;
|
||||
data.forEach((item: any) => {
|
||||
leftMin = Math.min(leftMin, item.customerCount || 0);
|
||||
leftMax = Math.max(leftMax, item.customerCount || 0);
|
||||
rightMin = Math.min(rightMin, item.dealCount || 0);
|
||||
rightMax = Math.max(rightMax, item.dealCount || 0);
|
||||
});
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
showDelay: 0,
|
||||
transitionDuration: 0.2,
|
||||
},
|
||||
visualMap: {
|
||||
text: ['高', '低'],
|
||||
realtime: false,
|
||||
calculable: true,
|
||||
top: 'middle',
|
||||
inRange: {
|
||||
color: ['yellow', 'lightskyblue', 'orangered'],
|
||||
},
|
||||
min: leftMin,
|
||||
max: leftMax,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '客户地域分布',
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
roam: false,
|
||||
selectedMode: false,
|
||||
data: data.map((item: any) => {
|
||||
return {
|
||||
name: item.areaName,
|
||||
value: item.customerCount || 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
showDelay: 0,
|
||||
transitionDuration: 0.2,
|
||||
},
|
||||
visualMap: {
|
||||
text: ['高', '低'],
|
||||
realtime: false,
|
||||
calculable: true,
|
||||
top: 'middle',
|
||||
inRange: {
|
||||
color: ['yellow', 'lightskyblue', 'orangered'],
|
||||
},
|
||||
min: rightMin,
|
||||
max: rightMax,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '客户地域分布',
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
roam: false,
|
||||
selectedMode: false,
|
||||
data: data.map((item: any) => {
|
||||
return {
|
||||
name: item.areaName,
|
||||
value: item.dealCount || 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'industry': {
|
||||
return {
|
||||
left: {
|
||||
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: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
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: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'level': {
|
||||
return {
|
||||
left: {
|
||||
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: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
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: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'source': {
|
||||
return {
|
||||
left: {
|
||||
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: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
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: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
199
apps/web-antd/src/views/crm/statistics/portrait/data.ts
Normal file
199
apps/web-antd/src/views/crm/statistics/portrait/data.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
|
||||
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const customerSummaryTabs = [
|
||||
{
|
||||
tab: '城市分布分析',
|
||||
key: 'area',
|
||||
},
|
||||
{
|
||||
tab: '客户级别分析',
|
||||
key: 'level',
|
||||
},
|
||||
{
|
||||
tab: '客户来源分析',
|
||||
key: 'source',
|
||||
},
|
||||
{
|
||||
tab: '客户行业分析',
|
||||
key: 'industry',
|
||||
},
|
||||
];
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'times',
|
||||
label: '时间范围',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
format: 'YYYY-MM-DD',
|
||||
picker: 'year',
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
label: '归属部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '员工',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
allowClear: true,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
activeTabName: any,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
switch (activeTabName) {
|
||||
case 'industry': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'customerCount',
|
||||
title: '客户个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'dealCount',
|
||||
title: '成交个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'industryPortion',
|
||||
title: '行业占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'dealPortion',
|
||||
title: '成交占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'level': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'customerCount',
|
||||
title: '客户个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'dealCount',
|
||||
title: '成交个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'industryPortion',
|
||||
title: '行业占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'dealPortion',
|
||||
title: '成交占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'source': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '序号',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
title: '客户来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'customerCount',
|
||||
title: '客户个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'dealCount',
|
||||
title: '成交个数',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'industryPortion',
|
||||
title: '行业占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'dealPortion',
|
||||
title: '成交占比(%)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDatas } from '#/api/crm/statistics/portrait';
|
||||
|
||||
import { getChartOptions } from './chartOptions';
|
||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const activeTabName = ref('area');
|
||||
const leftChartRef = ref<EchartsUIType>();
|
||||
const rightChartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts: renderLeftEcharts } = useEcharts(leftChartRef);
|
||||
const { renderEcharts: renderRightEcharts } = useEcharts(rightChartRef);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(activeTabName.value),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues) => {
|
||||
const res = await getDatas(activeTabName.value, formValues);
|
||||
renderLeftEcharts(getChartOptions(activeTabName.value, res).left);
|
||||
renderRightEcharts(getChartOptions(activeTabName.value, res).right);
|
||||
return res;
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
|
||||
});
|
||||
|
||||
async function handleTabChange(key: any) {
|
||||
activeTabName.value = key;
|
||||
gridApi.setGridOptions({
|
||||
columns: useGridColumns(key),
|
||||
});
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/portrait/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/portrait/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs v-model:active-key="activeTabName" @change="handleTabChange">
|
||||
<Tabs.TabPane
|
||||
v-for="item in customerSummaryTabs"
|
||||
:key="item.key"
|
||||
:tab="item.tab"
|
||||
:force-render="true"
|
||||
/>
|
||||
</Tabs>
|
||||
<div class="mt-5 flex">
|
||||
<EchartsUI class="m-4 w-1/2" ref="leftChartRef" />
|
||||
<EchartsUI class="m-4 w-1/2" ref="rightChartRef" />
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
394
apps/web-antd/src/views/crm/statistics/rank/chartOptions.ts
Normal file
394
apps/web-antd/src/views/crm/statistics/rank/chartOptions.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'contactCountRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增联系人数排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '新增联系人数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增联系人数(个)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '创建人',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'contractCountRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '签约合同排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '签约合同排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '签约合同数(个)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '签订人',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'contractPriceRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '合同金额排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '合同金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '合同金额(元)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '签订人',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'customerCountRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '新增客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增客户数(个)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '创建人',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'followCountRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '跟进次数排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '跟进次数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进次数(次)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '员工',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'followCustomerCountRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '跟进客户数排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '跟进客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进客户数(个)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '员工',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'productSalesRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '产品销量排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '产品销量排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '产品销量',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '员工',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'receivablePriceRank': {
|
||||
return {
|
||||
dataset: {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).reverse(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '回款金额排行',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
|
||||
},
|
||||
brush: {
|
||||
type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
|
||||
},
|
||||
saveAsImage: { show: true, name: '回款金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '回款金额(元)',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: '签订人',
|
||||
nameGap: 30,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
276
apps/web-antd/src/views/crm/statistics/rank/data.ts
Normal file
276
apps/web-antd/src/views/crm/statistics/rank/data.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
|
||||
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const customerSummaryTabs = [
|
||||
{
|
||||
tab: '合同金额排行',
|
||||
key: 'contractPriceRank',
|
||||
},
|
||||
{
|
||||
tab: '回款金额排行',
|
||||
key: 'receivablePriceRank',
|
||||
},
|
||||
{
|
||||
tab: '签约合同排行',
|
||||
key: 'contractCountRank',
|
||||
},
|
||||
{
|
||||
tab: '产品销量排行',
|
||||
key: 'productSalesRank',
|
||||
},
|
||||
{
|
||||
tab: '新增客户数排行',
|
||||
key: 'customerCountRank',
|
||||
},
|
||||
{
|
||||
tab: '新增联系人数排行',
|
||||
key: 'contactCountRank',
|
||||
},
|
||||
{
|
||||
tab: '跟进次数排行',
|
||||
key: 'followCountRank',
|
||||
},
|
||||
{
|
||||
tab: '跟进客户数排行',
|
||||
key: 'followCustomerCountRank',
|
||||
},
|
||||
];
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'times',
|
||||
label: '时间范围',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
label: '归属部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
activeTabName: any,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
switch (activeTabName) {
|
||||
case 'contactCountRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '创建人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '新增联系人数(个)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'contractCountRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '签约合同数(个)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'contractPriceRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '合同金额(元)',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'customerCountRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '新增客户数(个)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'followCountRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '跟进次数(次)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'followCustomerCountRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '跟进客户数(个)',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'productSalesRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '产品销量',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'receivablePriceRank': {
|
||||
return [
|
||||
{
|
||||
type: 'seq',
|
||||
title: '公司排名',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '签订人',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '回款金额(元)',
|
||||
minWidth: 200,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user