Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
@@ -42,10 +42,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入温度参数',
|
||||
class: 'w-full',
|
||||
precision: 2,
|
||||
min: 0,
|
||||
max: 2,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -55,9 +56,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入回复数 Token 数',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
max: 8192,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -67,9 +69,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入上下文数量',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
max: 20,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
||||
@@ -547,7 +547,10 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElContainer class="absolute left-0 top-0 m-4 h-full w-full flex-1">
|
||||
<ElContainer
|
||||
direction="horizontal"
|
||||
class="absolute left-0 top-0 m-4 h-full w-full flex-1"
|
||||
>
|
||||
<!-- 左侧:对话列表 -->
|
||||
<ConversationList
|
||||
class="!bg-card"
|
||||
@@ -560,7 +563,7 @@ onMounted(async () => {
|
||||
/>
|
||||
|
||||
<!-- 右侧:详情部分 -->
|
||||
<ElContainer class="bg-card mx-4">
|
||||
<ElContainer direction="vertical" class="bg-card mx-4 flex-1">
|
||||
<ElHeader
|
||||
class="!bg-card border-border flex !h-12 items-center justify-between border-b !px-4"
|
||||
>
|
||||
@@ -571,30 +574,30 @@ onMounted(async () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex w-72 justify-end" v-if="activeConversation">
|
||||
<div class="flex w-72 justify-end gap-2" v-if="activeConversation">
|
||||
<ElButton
|
||||
type="primary"
|
||||
plain
|
||||
class="mr-2 px-2"
|
||||
class="!px-2"
|
||||
size="small"
|
||||
@click="openChatConversationUpdateForm"
|
||||
>
|
||||
<span v-html="activeConversation?.modelName"></span>
|
||||
<IconifyIcon icon="lucide:settings" class="ml-2 size-4" />
|
||||
<IconifyIcon icon="lucide:settings" class="!ml-2 size-4" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
size="small"
|
||||
class="mr-2 px-2"
|
||||
class="!ml-0 !px-2"
|
||||
@click="handlerMessageClear"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash-2" color="#787878" />
|
||||
</ElButton>
|
||||
<ElButton size="small" class="mr-2 px-2">
|
||||
<ElButton size="small" class="!ml-0 !px-2">
|
||||
<IconifyIcon icon="lucide:download" color="#787878" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
size="small"
|
||||
class="mr-2 px-2"
|
||||
class="!ml-0 !px-2"
|
||||
@click="handleGoTopMessage"
|
||||
>
|
||||
<IconifyIcon icon="lucide:arrow-up" color="#787878" />
|
||||
@@ -629,7 +632,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</ElMain>
|
||||
|
||||
<ElFooter class="!bg-card flex flex-col !p-0">
|
||||
<ElFooter height="auto" class="!bg-card flex flex-col !p-0">
|
||||
<form
|
||||
class="border-border mx-4 mb-8 mt-2 flex flex-col rounded-xl border p-2"
|
||||
>
|
||||
@@ -673,7 +676,8 @@ onMounted(async () => {
|
||||
{{ conversationInProgress ? '进行中' : '发送' }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="danger"
|
||||
type="primary"
|
||||
:danger="true"
|
||||
@click="stopStream()"
|
||||
v-if="conversationInProgress === true"
|
||||
>
|
||||
|
||||
@@ -166,232 +166,277 @@ async function getConversationGroupByCreateTime(
|
||||
return groupMap;
|
||||
}
|
||||
|
||||
/** 新建对话 */
|
||||
async function handleConversationCreate() {
|
||||
// 1. 创建对话
|
||||
const conversationId = await createChatConversationMy({
|
||||
roleId: undefined,
|
||||
} as unknown as AiChatConversationApi.ChatConversation);
|
||||
|
||||
// 2. 刷新列表
|
||||
async function createConversation() {
|
||||
// 1. 新建对话
|
||||
const conversationId = await createChatConversationMy(
|
||||
{} as unknown as AiChatConversationApi.ChatConversation,
|
||||
);
|
||||
// 2. 获取对话内容
|
||||
await getChatConversationList();
|
||||
// 3. 选中对话
|
||||
await handleConversationClick(conversationId);
|
||||
// 4. 回调
|
||||
emits('onConversationCreate');
|
||||
}
|
||||
|
||||
// 3. 回调
|
||||
emits('onConversationCreate', conversationId);
|
||||
/** 修改对话的标题 */
|
||||
async function updateConversationTitle(
|
||||
conversation: AiChatConversationApi.ChatConversation,
|
||||
) {
|
||||
// 1. 二次确认
|
||||
await prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
if (scope.value) {
|
||||
try {
|
||||
// 2. 发起修改
|
||||
await updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: scope.value,
|
||||
} as AiChatConversationApi.ChatConversation);
|
||||
ElMessage.success('重命名成功');
|
||||
// 3. 刷新列表
|
||||
await getChatConversationList();
|
||||
// 4. 过滤当前切换的
|
||||
const filterConversationList = conversationList.value.filter(
|
||||
(item) => {
|
||||
return item.id === conversation.id;
|
||||
},
|
||||
);
|
||||
if (
|
||||
filterConversationList.length > 0 &&
|
||||
filterConversationList[0] && // tip:避免切换对话
|
||||
activeConversationId.value === filterConversationList[0].id!
|
||||
) {
|
||||
emits('onConversationClick', filterConversationList[0]);
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请输入标题');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
component: () => {
|
||||
return h(ElInput, {
|
||||
placeholder: '请输入标题',
|
||||
clearable: true,
|
||||
modelValue: conversation.title,
|
||||
});
|
||||
},
|
||||
content: '请输入标题',
|
||||
title: '修改标题',
|
||||
modelPropName: 'modelValue',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除聊天对话 */
|
||||
async function deleteChatConversation(
|
||||
conversation: AiChatConversationApi.ChatConversation,
|
||||
) {
|
||||
// 删除的二次确认
|
||||
await confirm(`是否确认删除对话 - ${conversation.title}?`);
|
||||
// 发起删除
|
||||
await deleteChatConversationMy(conversation.id);
|
||||
ElMessage.success('对话已删除');
|
||||
// 刷新列表
|
||||
await getChatConversationList();
|
||||
// 回调
|
||||
emits('onConversationDelete', conversation);
|
||||
}
|
||||
|
||||
/** 清空未置顶的对话 */
|
||||
async function handleConversationClear() {
|
||||
await confirm({
|
||||
title: '清空未置顶的对话',
|
||||
content: h('div', {}, [
|
||||
h('p', '确认清空未置顶的对话吗?'),
|
||||
h('p', '清空后,未置顶的对话将被删除,无法恢复!'),
|
||||
]),
|
||||
});
|
||||
// 清空
|
||||
async function handleClearConversation() {
|
||||
await confirm('确认后对话会全部清空,置顶的对话除外。');
|
||||
await deleteChatConversationMyByUnpinned();
|
||||
// 刷新列表
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
// 清空对话、对话内容
|
||||
activeConversationId.value = null;
|
||||
// 获取对话列表
|
||||
await getChatConversationList();
|
||||
// 回调
|
||||
// 回调 方法
|
||||
emits('onConversationClear');
|
||||
}
|
||||
|
||||
/** 删除对话 */
|
||||
async function handleConversationDelete(id: number) {
|
||||
await confirm({
|
||||
title: '删除对话',
|
||||
content: h('div', {}, [
|
||||
h('p', '确认删除该对话吗?'),
|
||||
h('p', '删除后,该对话将被删除,无法恢复!'),
|
||||
]),
|
||||
});
|
||||
// 删除
|
||||
await deleteChatConversationMy(id);
|
||||
// 刷新列表
|
||||
await getChatConversationList();
|
||||
// 回调
|
||||
emits('onConversationDelete', id);
|
||||
}
|
||||
|
||||
/** 置顶对话 */
|
||||
async function handleConversationPin(conversation: any) {
|
||||
// 更新
|
||||
await updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
pinned: !conversation.pinned,
|
||||
} as AiChatConversationApi.ChatConversation);
|
||||
// 刷新列表
|
||||
/** 对话置顶 */
|
||||
async function handleTop(conversation: AiChatConversationApi.ChatConversation) {
|
||||
// 更新对话置顶
|
||||
conversation.pinned = !conversation.pinned;
|
||||
await updateChatConversationMy(conversation);
|
||||
// 刷新对话
|
||||
await getChatConversationList();
|
||||
}
|
||||
|
||||
/** 编辑对话 */
|
||||
async function handleConversationEdit(conversation: any) {
|
||||
const title = await prompt({
|
||||
title: '编辑对话',
|
||||
content: '请输入对话标题',
|
||||
defaultValue: conversation.title,
|
||||
});
|
||||
// 更新
|
||||
await updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title,
|
||||
} as AiChatConversationApi.ChatConversation);
|
||||
// 刷新列表
|
||||
await getChatConversationList();
|
||||
// 提示
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
}
|
||||
// ============ 角色仓库 ============
|
||||
|
||||
/** 打开角色仓库 */
|
||||
async function handleRoleRepositoryOpen() {
|
||||
/** 角色仓库抽屉 */
|
||||
const handleRoleRepository = async () => {
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
/** 监听 activeId 变化 */
|
||||
watch(
|
||||
() => props.activeId,
|
||||
(newValue) => {
|
||||
activeConversationId.value = newValue;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/** 监听选中的对话 */
|
||||
const { activeId } = toRefs(props);
|
||||
watch(activeId, async (newValue) => {
|
||||
activeConversationId.value = newValue;
|
||||
});
|
||||
|
||||
defineExpose({ createConversation });
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取对话列表
|
||||
// 获取 对话列表
|
||||
await getChatConversationList();
|
||||
// 设置选中的对话
|
||||
if (activeId.value) {
|
||||
activeConversationId.value = activeId.value;
|
||||
// 默认选中
|
||||
if (props.activeId) {
|
||||
activeConversationId.value = props.activeId;
|
||||
} else {
|
||||
// 首次默认选中第一个
|
||||
if (conversationList.value.length > 0 && conversationList.value[0]) {
|
||||
activeConversationId.value = conversationList.value[0].id;
|
||||
// 回调 onConversationClick
|
||||
emits('onConversationClick', conversationList.value[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ getChatConversationList });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElAside
|
||||
class="bg-card relative flex h-full flex-col overflow-hidden border-r border-gray-200"
|
||||
width="280px"
|
||||
class="relative flex h-full flex-col justify-between overflow-hidden p-4"
|
||||
>
|
||||
<Drawer />
|
||||
<!-- 头部 -->
|
||||
<div class="flex flex-col p-4">
|
||||
<div class="mb-4 flex flex-row items-center justify-between">
|
||||
<div class="text-lg font-bold">对话</div>
|
||||
<div class="flex flex-row">
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleConversationCreate"
|
||||
>
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleConversationClear"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleRoleRepositoryOpen"
|
||||
>
|
||||
<IconifyIcon icon="lucide:user" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 左顶部:对话 -->
|
||||
<div class="flex h-full flex-col">
|
||||
<ElButton class="h-9 w-full" type="primary" @click="createConversation">
|
||||
<IconifyIcon icon="lucide:plus" class="mr-1" />
|
||||
新建对话
|
||||
</ElButton>
|
||||
|
||||
<ElInput
|
||||
v-model="searchName"
|
||||
placeholder="搜索对话"
|
||||
@keyup.enter="searchConversation"
|
||||
size="large"
|
||||
class="search-input mt-4"
|
||||
placeholder="搜索历史记录"
|
||||
@keyup="searchConversation"
|
||||
>
|
||||
<template #suffix>
|
||||
<template #prefix>
|
||||
<IconifyIcon icon="lucide:search" />
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
|
||||
<!-- 对话列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-4">
|
||||
<div v-if="loading" class="flex h-full items-center justify-center">
|
||||
<div class="text-sm text-gray-400">加载中...</div>
|
||||
</div>
|
||||
<div v-else-if="Object.keys(conversationMap).length === 0">
|
||||
<ElEmpty description="暂无对话" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="mt-2 flex-1 overflow-auto">
|
||||
<!-- 情况一:加载中 -->
|
||||
<ElEmpty v-if="loading" description="." v-loading="loading" />
|
||||
|
||||
<!-- 情况二:按照 group 分组 -->
|
||||
<div
|
||||
v-for="(conversations, groupName) in conversationMap"
|
||||
:key="groupName"
|
||||
v-for="conversationKey in Object.keys(conversationMap)"
|
||||
:key="conversationKey"
|
||||
>
|
||||
<div
|
||||
v-if="conversations.length > 0"
|
||||
class="mb-2 mt-4 text-xs text-gray-400"
|
||||
v-if="conversationMap[conversationKey].length > 0"
|
||||
class="classify-title pt-2"
|
||||
>
|
||||
{{ groupName }}
|
||||
<p class="mx-1">
|
||||
{{ conversationKey }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="conversation in conversations"
|
||||
v-for="conversation in conversationMap[conversationKey]"
|
||||
:key="conversation.id"
|
||||
class="group relative mb-2 cursor-pointer rounded-lg p-2 transition-all hover:bg-gray-100"
|
||||
:class="{
|
||||
'bg-gray-100': activeConversationId === conversation.id,
|
||||
}"
|
||||
@click="handleConversationClick(conversation.id)"
|
||||
@mouseenter="hoverConversationId = conversation.id"
|
||||
@mouseleave="hoverConversationId = null"
|
||||
@mouseover="hoverConversationId = conversation.id"
|
||||
@mouseout="hoverConversationId = null"
|
||||
class="mt-1"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<ElAvatar
|
||||
v-if="conversation.roleAvatar"
|
||||
:src="conversation.roleAvatar"
|
||||
:size="28"
|
||||
/>
|
||||
<SvgGptIcon v-else class="size-7" />
|
||||
<div class="ml-2 flex-1 overflow-hidden">
|
||||
<div class="truncate text-sm font-medium">
|
||||
<div
|
||||
class="mb-2 flex cursor-pointer flex-row items-center justify-between rounded-lg px-2 leading-10 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="[
|
||||
conversation.id === activeConversationId
|
||||
? 'bg-primary/10 dark:bg-primary/20'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<ElAvatar
|
||||
v-if="conversation.roleAvatar"
|
||||
:src="conversation.roleAvatar"
|
||||
:size="28"
|
||||
/>
|
||||
<SvgGptIcon v-else class="size-6" />
|
||||
<span
|
||||
class="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap p-2 text-sm font-normal"
|
||||
>
|
||||
{{ conversation.title }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hoverConversationId === conversation.id"
|
||||
class="flex flex-row"
|
||||
v-show="hoverConversationId === conversation.id"
|
||||
class="relative right-0.5 flex items-center text-gray-400"
|
||||
>
|
||||
<!-- TODO @AI:三个按钮之间,间隙太大了。 -->
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click.stop="handleConversationPin(conversation)"
|
||||
class="mr-0 px-1"
|
||||
link
|
||||
@click.stop="handleTop(conversation)"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
conversation.pinned ? 'lucide:pin-off' : 'lucide:pin'
|
||||
"
|
||||
v-if="!conversation.pinned"
|
||||
icon="lucide:arrow-up-to-line"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-if="conversation.pinned"
|
||||
icon="lucide:arrow-down-from-line"
|
||||
/>
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click.stop="handleConversationEdit(conversation)"
|
||||
class="mr-0 px-1"
|
||||
link
|
||||
@click.stop="updateConversationTitle(conversation)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click.stop="handleConversationDelete(conversation.id)"
|
||||
class="mr-0 px-1"
|
||||
link
|
||||
@click.stop="deleteChatConversation(conversation)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
<IconifyIcon icon="lucide:trash-2" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<div class="h-12 w-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div
|
||||
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"
|
||||
@click="handleRoleRepository"
|
||||
>
|
||||
<IconifyIcon icon="lucide:user" />
|
||||
<span class="ml-1">角色仓库</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex cursor-pointer items-center text-gray-400"
|
||||
@click="handleClearConversation"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
<span class="ml-1">清空未置顶对话</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElAside>
|
||||
</template>
|
||||
|
||||
@@ -18,11 +18,9 @@ async function handlerPromptClick(prompt: any) {
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative flex h-full w-full flex-row justify-center">
|
||||
<!-- center-container -->
|
||||
<div class="flex flex-col justify-center">
|
||||
<!-- title -->
|
||||
<div class="text-center text-3xl font-bold">芋道 AI</div>
|
||||
|
||||
<!-- role-list -->
|
||||
<div class="mt-5 flex w-96 flex-wrap items-center justify-center">
|
||||
<div
|
||||
|
||||
@@ -37,7 +37,7 @@ const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']);
|
||||
const { copy } = useClipboard(); // 初始化 copy 到粘贴板
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 判断"消息列表"滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
const messageContainer: any = ref(null);
|
||||
const isScrolling = ref(false); // 用于判断用户是否在滚动
|
||||
|
||||
@@ -88,6 +88,7 @@ async function copyContent(content: string) {
|
||||
await copy(content);
|
||||
ElMessage.success('复制成功!');
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(id: number) {
|
||||
// 删除 message
|
||||
@@ -153,7 +154,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row">
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
class="!ml-1 flex items-center bg-transparent !px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="copyContent(item.content)"
|
||||
>
|
||||
@@ -161,7 +162,7 @@ onMounted(async () => {
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="item.id > 0"
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
class="!ml-1 flex items-center bg-transparent !px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleDelete(item.id)"
|
||||
>
|
||||
@@ -196,28 +197,28 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row-reverse">
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
class="!ml-1 flex items-center bg-transparent !px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="copyContent(item.content)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
class="!ml-1 flex items-center bg-transparent !px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleDelete(item.id)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
class="!ml-1 flex items-center bg-transparent !px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleRefresh(item)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:refresh-cw" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
class="!ml-1 flex items-center bg-transparent !px-1.5 hover:bg-gray-100"
|
||||
text
|
||||
@click="handleEdit(item)"
|
||||
>
|
||||
|
||||
@@ -71,12 +71,15 @@ function toggleExpanded() {
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
@apply rounded-sm bg-gray-400/40;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-gray-400/60;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ async function handleCategoryClick(category: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="mx-0 flex flex-wrap items-center">
|
||||
<div
|
||||
class="mr-2 flex flex-row"
|
||||
v-for="category in categoryList"
|
||||
|
||||
@@ -63,59 +63,59 @@ async function handleTabsScroll() {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex h-full flex-wrap content-start items-start overflow-auto px-6 pb-36"
|
||||
class="relative flex h-full flex-wrap content-start items-start overflow-auto pb-36"
|
||||
ref="tabsRef"
|
||||
@scroll="handleTabsScroll"
|
||||
>
|
||||
<div class="mb-5 mr-5 inline-block" v-for="role in roleList" :key="role.id">
|
||||
<div class="mb-3 mr-3 inline-block" v-for="role in roleList" :key="role.id">
|
||||
<ElCard
|
||||
class="relative rounded-lg"
|
||||
body-style="position: relative; display: flex; flex-direction: row; justify-content: flex-start; width: 240px; max-width: 240px; padding: 15px 15px 10px;"
|
||||
body-style="position: relative; display: flex; flex-direction: column; justify-content: flex-start; width: 240px; max-width: 240px; padding: 15px;"
|
||||
>
|
||||
<!-- 更多操作 -->
|
||||
<div v-if="showMore" class="absolute right-2 top-0">
|
||||
<ElDropdown>
|
||||
<ElButton link>
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
<!-- 头部:头像、名称 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex min-w-0 flex-1 items-center">
|
||||
<ElAvatar
|
||||
:src="role.avatar"
|
||||
class="h-8 w-8 flex-shrink-0 overflow-hidden"
|
||||
/>
|
||||
<div class="ml-2 truncate text-base font-medium">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 描述信息 -->
|
||||
<div
|
||||
class="mt-2 line-clamp-2 h-10 overflow-hidden text-sm text-gray-600"
|
||||
>
|
||||
{{ role.description }}
|
||||
</div>
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ElDropdown v-if="showMore">
|
||||
<ElButton size="small">
|
||||
<IconifyIcon icon="lucide:ellipsis" />
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="handleMoreClick('edit', role)">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:edit" color="#787878" />
|
||||
<span class="text-primary">编辑</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="handleMoreClick('delete', role)">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:trash" color="red" />
|
||||
<span class="text-red-500">删除</span>
|
||||
<span class="ml-2 text-red-500">删除</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="handleMoreClick('edit', role)">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:edit" color="#787878" />
|
||||
<span class="text-primary ml-2">编辑</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
|
||||
<!-- 角色信息 -->
|
||||
<div>
|
||||
<ElAvatar :src="role.avatar" class="h-10 w-10 overflow-hidden" />
|
||||
</div>
|
||||
|
||||
<div class="ml-2 w-4/5">
|
||||
<div class="h-20">
|
||||
<div class="max-w-32 text-lg font-bold">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm">
|
||||
{{ role.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-row-reverse">
|
||||
<ElButton type="primary" size="small" @click="handleUseClick(role)">
|
||||
使用
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElButton type="primary" size="small" @click="handleUseClick(role)">
|
||||
使用
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,7 @@ async function handlerCategoryClick(category: string) {
|
||||
async function handlerAddRole() {
|
||||
formModalApi.setData({ formType: 'my-create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑角色 */
|
||||
async function handlerCardEdit(role: any) {
|
||||
formModalApi.setData({ formType: 'my-update', id: role.id }).open();
|
||||
@@ -187,7 +188,7 @@ onMounted(async () => {
|
||||
<FormModal @success="handlerAddRoleSuccess" />
|
||||
|
||||
<ElMain class="relative m-0 flex-1 overflow-hidden p-0">
|
||||
<div class="z-100 absolute right-0 top--1 mr-5 mt-5">
|
||||
<div class="z-100 absolute right-5 top-5 flex items-center">
|
||||
<!-- 搜索输入框 -->
|
||||
<ElInput
|
||||
v-model="search"
|
||||
@@ -196,7 +197,11 @@ onMounted(async () => {
|
||||
@keyup.enter="getActiveTabsRole"
|
||||
>
|
||||
<template #suffix>
|
||||
<IconifyIcon icon="lucide:search" />
|
||||
<IconifyIcon
|
||||
icon="lucide:search"
|
||||
class="cursor-pointer"
|
||||
@click="getActiveTabsRole"
|
||||
/>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton
|
||||
@@ -209,11 +214,10 @@ onMounted(async () => {
|
||||
添加角色
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<ElTabs
|
||||
v-model="activeTab"
|
||||
class="relative h-full p-4"
|
||||
class="relative h-full pb-4 pr-4"
|
||||
@tab-click="handleTabsClick"
|
||||
>
|
||||
<ElTabPane
|
||||
@@ -229,10 +233,8 @@ onMounted(async () => {
|
||||
@on-edit="handlerCardEdit"
|
||||
@on-use="handlerCardUse"
|
||||
@on-page="handlerCardPage('my')"
|
||||
class="mt-5"
|
||||
/>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane
|
||||
name="public-role"
|
||||
class="flex h-full flex-col overflow-y-auto"
|
||||
|
||||
@@ -69,7 +69,7 @@ async function handleGenerateImage() {
|
||||
width: width.value, // 图片宽度
|
||||
height: height.value, // 图片高度
|
||||
options: {},
|
||||
} as unknown as AiImageApi.ImageDrawReq;
|
||||
} as unknown as AiImageApi.ImageDrawReqVO;
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
|
||||
@@ -116,7 +116,7 @@ async function handleGenerateImage() {
|
||||
options: {
|
||||
style: style.value, // 图像生成的风格
|
||||
},
|
||||
} as AiImageApi.ImageDrawReq;
|
||||
} as AiImageApi.ImageDrawReqVO;
|
||||
// 发送请求
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
|
||||
@@ -144,7 +144,7 @@ async function handleImageMidjourneyButtonClick(
|
||||
const data = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId,
|
||||
} as AiImageApi.ImageMidjourneyAction;
|
||||
} as AiImageApi.ImageMidjourneyActionVO;
|
||||
// 2. 发送 action
|
||||
await midjourneyAction(data);
|
||||
// 3. 刷新列表
|
||||
|
||||
@@ -103,7 +103,7 @@ async function handleGenerateImage() {
|
||||
height: imageSize.height,
|
||||
version: selectVersion.value,
|
||||
referImageUrl: referImageUrl.value,
|
||||
} as AiImageApi.ImageMidjourneyImagineReq;
|
||||
} as AiImageApi.ImageMidjourneyImagineReqVO;
|
||||
await midjourneyImagine(req);
|
||||
} finally {
|
||||
// 回调
|
||||
|
||||
@@ -103,7 +103,7 @@ async function handleGenerateImage() {
|
||||
clipGuidancePreset: clipGuidancePreset.value, // 文本提示相匹配的图像 CLIP
|
||||
stylePreset: stylePreset.value, // 风格
|
||||
},
|
||||
} as unknown as AiImageApi.ImageDrawReq;
|
||||
} as unknown as AiImageApi.ImageDrawReqVO;
|
||||
await drawImage(form);
|
||||
} finally {
|
||||
// 回调
|
||||
|
||||
@@ -189,11 +189,17 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'category',
|
||||
label: '角色类别',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色类别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'publicStatus',
|
||||
|
||||
@@ -105,10 +105,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入温度参数',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
min: 0,
|
||||
max: 2,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmCopyTask' });
|
||||
|
||||
/** 任务详情 */
|
||||
function handleDetail(row: BpmProcessInstanceApi.Copy) {
|
||||
function handleDetail(row: BpmProcessInstanceApi.ProcessInstanceCopyRespVO) {
|
||||
const query = {
|
||||
id: row.processInstanceId,
|
||||
...(row.activityId && { activityId: row.activityId }),
|
||||
@@ -52,7 +52,7 @@ const [Grid] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmProcessInstanceApi.Copy>,
|
||||
} as VxeTableGridOptions<BpmProcessInstanceApi.ProcessInstanceCopyRespVO>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const [Grid] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
102
apps/web-ele/src/views/crm/backlog/data.ts
Normal file
102
apps/web-ele/src/views/crm/backlog/data.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
export interface LeftSideItem {
|
||||
name: string;
|
||||
menu: string;
|
||||
count: Ref<number>;
|
||||
}
|
||||
|
||||
/** 跟进状态 */
|
||||
export const FOLLOWUP_STATUS = [
|
||||
{ label: '待跟进', value: false },
|
||||
{ label: '已跟进', value: true },
|
||||
];
|
||||
|
||||
/** 归属范围 */
|
||||
export const SCENE_TYPES = [
|
||||
{ label: '我负责的', value: 1 },
|
||||
{ label: '我参与的', value: 2 },
|
||||
{ label: '下属负责的', value: 3 },
|
||||
];
|
||||
|
||||
/** 联系状态 */
|
||||
export const CONTACT_STATUS = [
|
||||
{ label: '今日需联系', value: 1 },
|
||||
{ label: '已逾期', value: 2 },
|
||||
{ label: '已联系', value: 3 },
|
||||
];
|
||||
|
||||
/** 审批状态 */
|
||||
export const AUDIT_STATUS = [
|
||||
{ label: '待审批', value: 10 },
|
||||
{ label: '审核通过', value: 20 },
|
||||
{ label: '审核不通过', value: 30 },
|
||||
];
|
||||
|
||||
/** 回款提醒类型 */
|
||||
export const RECEIVABLE_REMIND_TYPE = [
|
||||
{ label: '待回款', value: 1 },
|
||||
{ label: '已逾期', value: 2 },
|
||||
{ label: '已回款', value: 3 },
|
||||
];
|
||||
|
||||
/** 合同过期状态 */
|
||||
export const CONTRACT_EXPIRY_TYPE = [
|
||||
{ label: '即将过期', value: 1 },
|
||||
{ label: '已过期', value: 2 },
|
||||
];
|
||||
|
||||
/** 左侧菜单 */
|
||||
export const useLeftSides = (
|
||||
customerTodayContactCount: Ref<number>,
|
||||
clueFollowCount: Ref<number>,
|
||||
customerFollowCount: Ref<number>,
|
||||
customerPutPoolRemindCount: Ref<number>,
|
||||
contractAuditCount: Ref<number>,
|
||||
contractRemindCount: Ref<number>,
|
||||
receivableAuditCount: Ref<number>,
|
||||
receivablePlanRemindCount: Ref<number>,
|
||||
): LeftSideItem[] => {
|
||||
return [
|
||||
{
|
||||
name: '今日需联系客户',
|
||||
menu: 'customerTodayContact',
|
||||
count: customerTodayContactCount,
|
||||
},
|
||||
{
|
||||
name: '分配给我的线索',
|
||||
menu: 'clueFollow',
|
||||
count: clueFollowCount,
|
||||
},
|
||||
{
|
||||
name: '分配给我的客户',
|
||||
menu: 'customerFollow',
|
||||
count: customerFollowCount,
|
||||
},
|
||||
{
|
||||
name: '待进入公海的客户',
|
||||
menu: 'customerPutPoolRemind',
|
||||
count: customerPutPoolRemindCount,
|
||||
},
|
||||
{
|
||||
name: '待审核合同',
|
||||
menu: 'contractAudit',
|
||||
count: contractAuditCount,
|
||||
},
|
||||
{
|
||||
name: '待审核回款',
|
||||
menu: 'receivableAudit',
|
||||
count: receivableAuditCount,
|
||||
},
|
||||
{
|
||||
name: '待回款提醒',
|
||||
menu: 'receivablePlanRemind',
|
||||
count: receivablePlanRemindCount,
|
||||
},
|
||||
{
|
||||
name: '即将到期的合同',
|
||||
menu: 'contractRemind',
|
||||
count: contractRemindCount,
|
||||
},
|
||||
];
|
||||
};
|
||||
115
apps/web-ele/src/views/crm/backlog/index.vue
Normal file
115
apps/web-ele/src/views/crm/backlog/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ElBadge, ElCard } from 'element-plus';
|
||||
|
||||
import { getFollowClueCount } from '#/api/crm/clue';
|
||||
import {
|
||||
getAuditContractCount,
|
||||
getRemindContractCount,
|
||||
} from '#/api/crm/contract';
|
||||
import {
|
||||
getFollowCustomerCount,
|
||||
getPutPoolRemindCustomerCount,
|
||||
getTodayContactCustomerCount,
|
||||
} from '#/api/crm/customer';
|
||||
import { getAuditReceivableCount } from '#/api/crm/receivable';
|
||||
import { getReceivablePlanRemindCount } from '#/api/crm/receivable/plan';
|
||||
|
||||
import { useLeftSides } from './data';
|
||||
import ClueFollowList from './modules/clue-follow-list.vue';
|
||||
import ContractAuditList from './modules/contract-audit-list.vue';
|
||||
import ContractRemindList from './modules/contract-remind-list.vue';
|
||||
import CustomerFollowList from './modules/customer-follow-list.vue';
|
||||
import CustomerPutPoolRemindList from './modules/customer-put-pool-remind-list.vue';
|
||||
import CustomerTodayContactList from './modules/customer-today-contact-list.vue';
|
||||
import ReceivableAuditList from './modules/receivable-audit-list.vue';
|
||||
import ReceivablePlanRemindList from './modules/receivable-plan-remind-list.vue';
|
||||
|
||||
const leftMenu = ref('customerTodayContact');
|
||||
|
||||
const clueFollowCount = ref(0);
|
||||
const customerFollowCount = ref(0);
|
||||
const customerPutPoolRemindCount = ref(0);
|
||||
const customerTodayContactCount = ref(0);
|
||||
const contractAuditCount = ref(0);
|
||||
const contractRemindCount = ref(0);
|
||||
const receivableAuditCount = ref(0);
|
||||
const receivablePlanRemindCount = ref(0);
|
||||
|
||||
const leftSides = useLeftSides(
|
||||
customerTodayContactCount,
|
||||
clueFollowCount,
|
||||
customerFollowCount,
|
||||
customerPutPoolRemindCount,
|
||||
contractAuditCount,
|
||||
contractRemindCount,
|
||||
receivableAuditCount,
|
||||
receivablePlanRemindCount,
|
||||
);
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
const components = {
|
||||
customerTodayContact: CustomerTodayContactList,
|
||||
clueFollow: ClueFollowList,
|
||||
contractAudit: ContractAuditList,
|
||||
receivableAudit: ReceivableAuditList,
|
||||
contractRemind: ContractRemindList,
|
||||
customerFollow: CustomerFollowList,
|
||||
customerPutPoolRemind: CustomerPutPoolRemindList,
|
||||
receivablePlanRemind: ReceivablePlanRemindList,
|
||||
} as const;
|
||||
return components[leftMenu.value as keyof typeof components];
|
||||
});
|
||||
|
||||
/** 侧边点击 */
|
||||
function sideClick(item: { menu: string }) {
|
||||
leftMenu.value = item.menu;
|
||||
}
|
||||
|
||||
/** 获取数量 */
|
||||
async function getCount() {
|
||||
customerTodayContactCount.value = await getTodayContactCustomerCount();
|
||||
customerPutPoolRemindCount.value = await getPutPoolRemindCustomerCount();
|
||||
customerFollowCount.value = await getFollowCustomerCount();
|
||||
clueFollowCount.value = await getFollowClueCount();
|
||||
contractAuditCount.value = await getAuditContractCount();
|
||||
contractRemindCount.value = await getRemindContractCount();
|
||||
receivableAuditCount.value = await getAuditReceivableCount();
|
||||
receivablePlanRemindCount.value = await getReceivablePlanRemindCount();
|
||||
}
|
||||
|
||||
/** 激活时 */
|
||||
onActivated(() => {
|
||||
getCount();
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getCount();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="flex h-full w-full">
|
||||
<ElCard class="w-1/5">
|
||||
<div v-for="item in leftSides" :key="item.menu">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between border-b px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
@click="sideClick(item)"
|
||||
>
|
||||
<div>{{ item.name }}</div>
|
||||
<ElBadge
|
||||
v-if="item.count.value > 0"
|
||||
:value="item.count.value"
|
||||
:type="item.menu === leftMenu ? 'primary' : 'danger'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
<component class="ml-4 w-4/5" :is="currentComponent" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!-- 分配给我的线索 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCluePage } from '#/api/crm/clue';
|
||||
import { useGridColumns } from '#/views/crm/clue/data';
|
||||
|
||||
import { FOLLOWUP_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开线索详情 */
|
||||
function handleDetail(row: CrmClueApi.Clue) {
|
||||
push({ name: 'CrmClueDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'followUpStatus',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: FOLLOWUP_STATUS,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCluePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
transformStatus: false,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmClueApi.Clue>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
查看详情
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!-- 待审核合同 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getContractPage } from '#/api/crm/contract';
|
||||
import { useGridColumns } from '#/views/crm/contract/data';
|
||||
|
||||
import { AUDIT_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 查看审批 */
|
||||
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 打开合同详情 */
|
||||
function handleContractDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
/** 打开客户详情 */
|
||||
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开联系人详情 */
|
||||
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开商机详情 */
|
||||
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'auditStatus',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: AUDIT_STATUS,
|
||||
},
|
||||
defaultValue: AUDIT_STATUS[0]!.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContractPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: 1, // 我负责的
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleContractDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #businessName="{ row }">
|
||||
<ElButton type="primary" link @click="handleBusinessDetail(row)">
|
||||
{{ row.businessName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #contactName="{ row }">
|
||||
<ElButton type="primary" link @click="handleContactDetail(row)">
|
||||
{{ row.contactName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,125 @@
|
||||
<!-- 即将到期的合同 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getContractPage } from '#/api/crm/contract';
|
||||
import { useGridColumns } from '#/views/crm/contract/data';
|
||||
|
||||
import { CONTRACT_EXPIRY_TYPE } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 查看审批 */
|
||||
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 打开合同详情 */
|
||||
function handleContractDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开联系人详情 */
|
||||
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开商机详情 */
|
||||
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'expiryType',
|
||||
label: '到期状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: CONTRACT_EXPIRY_TYPE,
|
||||
},
|
||||
defaultValue: CONTRACT_EXPIRY_TYPE[0]!.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContractPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: 1, // 自己负责的
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleContractDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #businessName="{ row }">
|
||||
<ElButton type="primary" link @click="handleBusinessDetail(row)">
|
||||
{{ row.businessName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #signContactName="{ row }">
|
||||
<ElButton type="primary" link @click="handleContactDetail(row)">
|
||||
{{ row.signContactName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!-- 分配给我的客户 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCustomerPage } from '#/api/crm/customer';
|
||||
import { useGridColumns } from '#/views/crm/customer/data';
|
||||
|
||||
import { FOLLOWUP_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'followUpStatus',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: FOLLOWUP_STATUS,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: 1,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
查看详情
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!-- 待进入公海的客户 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCustomerPage } from '#/api/crm/customer';
|
||||
import { useGridColumns } from '#/views/crm/customer/data';
|
||||
|
||||
import { SCENE_TYPES } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'sceneType',
|
||||
label: '归属',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: SCENE_TYPES,
|
||||
},
|
||||
defaultValue: SCENE_TYPES[0]!.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
pool: true, // 固定 公海参数为 true
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
查看详情
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<!-- 今日需联系客户 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCustomerPage } from '#/api/crm/customer';
|
||||
import { useGridColumns } from '#/views/crm/customer/data';
|
||||
|
||||
import { CONTACT_STATUS, SCENE_TYPES } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'contactStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: CONTACT_STATUS,
|
||||
},
|
||||
defaultValue: CONTACT_STATUS[0]!.value,
|
||||
},
|
||||
{
|
||||
fieldName: 'sceneType',
|
||||
label: '归属',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: SCENE_TYPES,
|
||||
},
|
||||
defaultValue: SCENE_TYPES[0]!.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
pool: null, // 是否公海数据
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
查看详情
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!-- 待审核回款 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getReceivablePage } from '#/api/crm/receivable';
|
||||
import { useGridColumns } from '#/views/crm/receivable/data';
|
||||
|
||||
import { AUDIT_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 查看审批 */
|
||||
function handleProcessDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 打开回款详情 */
|
||||
function handleDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleCustomerDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 打开合同详情 */
|
||||
function handleContractDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'auditStatus',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: AUDIT_STATUS,
|
||||
},
|
||||
defaultValue: AUDIT_STATUS[0]!.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivableApi.Receivable>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #no="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.no }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #contractNo="{ row }">
|
||||
<ElButton type="primary" link @click="handleContractDetail(row)">
|
||||
{{ row.contractNo }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="handleProcessDetail(row)">
|
||||
查看审批
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- 待回款提醒 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivablePlanApi } from '#/api/crm/receivable/plan';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getReceivablePlanPage } from '#/api/crm/receivable/plan';
|
||||
import Form from '#/views/crm/receivable/modules/form.vue';
|
||||
import { useGridColumns } from '#/views/crm/receivable/plan/data';
|
||||
|
||||
import { RECEIVABLE_REMIND_TYPE } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 打开回款详情 */
|
||||
function handleDetail(row: CrmReceivablePlanApi.Plan) {
|
||||
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleCustomerDetail(row: CrmReceivablePlanApi.Plan) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 创建回款 */
|
||||
function handleCreateReceivable(row: CrmReceivablePlanApi.Plan) {
|
||||
formModalApi.setData({ plan: row }).open();
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'remindType',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: RECEIVABLE_REMIND_TYPE,
|
||||
},
|
||||
defaultValue: RECEIVABLE_REMIND_TYPE[0]!.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePlanPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivablePlanApi.Plan>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #period="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.period }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="handleCreateReceivable(row)">
|
||||
创建回款
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
52
apps/web-ele/src/views/crm/business/components/data.ts
Normal file
52
apps/web-ele/src/views/crm/business/components/data.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
/** 商机关联列表列定义 */
|
||||
export function useBusinessDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
formatter: 'formatDate',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
fixed: 'right',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<!-- 商机选择对话框:用于联系人详情中关联已有商机 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBusinessPageByCustomer } from '#/api/crm/business';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useBusinessDetailListColumns } from './data';
|
||||
|
||||
const props = defineProps<{
|
||||
customerId?: number; // 关联联系人与商机时,需要传入 customerId 进行筛选
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const checkedRows = ref<CrmBusinessApi.Business[]>([]);
|
||||
function setCheckedRows({ records }: { records: CrmBusinessApi.Business[] }) {
|
||||
checkedRows.value = records;
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建商机 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ customerId: props.customerId }).open();
|
||||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function handleDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 商机关联弹窗 */
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (checkedRows.value.length === 0) {
|
||||
ElMessage.error('请先选择商机后操作!');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
try {
|
||||
const businessIds = checkedRows.value.map((item) => item.id);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success', businessIds, checkedRows.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 商机选择表格 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商机名称',
|
||||
component: 'Input',
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useBusinessDetailListColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBusinessPageByCustomer({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.customerId,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmBusinessApi.Business>,
|
||||
gridEvents: {
|
||||
checkboxAll: setCheckedRows,
|
||||
checkboxChange: setCheckedRows,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="关联商机" class="w-2/5">
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商机']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:business:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
215
apps/web-ele/src/views/crm/business/components/detail-list.vue
Normal file
215
apps/web-ele/src/views/crm/business/components/detail-list.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<!-- 商机列表:用于【客户】【联系人】详情中,展示其关联的商机列表 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getBusinessPageByContact,
|
||||
getBusinessPageByCustomer,
|
||||
} from '#/api/crm/business';
|
||||
import {
|
||||
createContactBusinessList,
|
||||
deleteContactBusinessList,
|
||||
} from '#/api/crm/contact';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useBusinessDetailListColumns } from './data';
|
||||
import ListModal from './detail-list-modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
bizId: number; // 业务编号
|
||||
bizType: number; // 业务类型
|
||||
contactId?: number; // 特殊:联系人编号;在【联系人】详情中,可以传递联系人编号,默认新建的商机关联到该联系人
|
||||
customerId?: number; // 关联联系人与商机时,需要传入 customerId 进行筛选
|
||||
}>();
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailListModal, detailListModalApi] = useVbenModal({
|
||||
connectedComponent: ListModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const checkedRows = ref<CrmBusinessApi.Business[]>([]);
|
||||
function setCheckedRows({ records }: { records: CrmBusinessApi.Business[] }) {
|
||||
checkedRows.value = records;
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建商机 */
|
||||
function handleCreate() {
|
||||
formModalApi
|
||||
.setData({ customerId: props.customerId, contactId: props.contactId })
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 关联商机 */
|
||||
function handleCreateBusiness() {
|
||||
detailListModalApi.setData({ customerId: props.customerId }).open();
|
||||
}
|
||||
|
||||
/** 解除商机关联 */
|
||||
async function handleDeleteContactBusinessList() {
|
||||
if (checkedRows.value.length === 0) {
|
||||
ElMessage.error('请先选择商机后操作!');
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定要将${checkedRows.value.map((item) => item.name).join(',')}解除关联吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteContactBusinessList({
|
||||
contactId: props.bizId,
|
||||
businessIds: checkedRows.value.map((item) => item.id),
|
||||
});
|
||||
if (res) {
|
||||
// 提示并返回成功
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
handleRefresh();
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function handleDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 创建联系人关联的商机 */
|
||||
async function handleCreateContactBusinessList(businessIds: number[]) {
|
||||
const data = {
|
||||
contactId: props.bizId,
|
||||
businessIds,
|
||||
} as CrmContactApi.ContactBusinessReqVO;
|
||||
await createContactBusinessList(data);
|
||||
handleRefresh();
|
||||
}
|
||||
|
||||
/** 商机关联表格 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useBusinessDetailListColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
return await getBusinessPageByCustomer({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.customerId,
|
||||
...formValues,
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_CONTACT) {
|
||||
return await getBusinessPageByContact({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
contactId: props.contactId,
|
||||
...formValues,
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmBusinessApi.Business>,
|
||||
gridEvents: {
|
||||
checkboxAll: setCheckedRows,
|
||||
checkboxChange: setCheckedRows,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailListModal
|
||||
:customer-id="customerId"
|
||||
@success="handleCreateContactBusinessList"
|
||||
/>
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商机']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:business:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: '关联',
|
||||
icon: ACTION_ICON.ADD,
|
||||
type: 'default',
|
||||
auth: ['crm:contact:create-business'],
|
||||
ifShow: () => !!contactId,
|
||||
onClick: handleCreateBusiness,
|
||||
},
|
||||
{
|
||||
label: '解除关联',
|
||||
icon: ACTION_ICON.ADD,
|
||||
type: 'default',
|
||||
auth: ['crm:contact:create-business'],
|
||||
ifShow: () => !!contactId,
|
||||
onClick: handleDeleteContactBusinessList,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
1
apps/web-ele/src/views/crm/business/components/index.ts
Normal file
1
apps/web-ele/src/views/crm/business/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as BusinessDetailsList } from './detail-list.vue';
|
||||
281
apps/web-ele/src/views/crm/business/data.ts
Normal file
281
apps/web-ele/src/views/crm/business/data.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
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',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商机名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入商机名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择负责人',
|
||||
allowClear: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户名称',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
allowClear: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.customerDefault,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'contactId',
|
||||
label: '合同名称',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'statusTypeId',
|
||||
label: '商机状态组',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getBusinessStatusTypeSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择商机状态组',
|
||||
allowClear: true,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'dealTime',
|
||||
label: '预计成交日期',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择预计成交日期',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'product',
|
||||
label: '产品清单',
|
||||
component: 'Input',
|
||||
formItemClass: 'col-span-3',
|
||||
componentProps: {
|
||||
placeholder: '请输入产品清单',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'totalProductPrice',
|
||||
label: '产品总金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
disabled: true,
|
||||
placeholder: '请输入产品总金额',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'discountPercent',
|
||||
label: '整单折扣(%)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
placeholder: '请输入整单折扣',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: z.number().min(0).max(100).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '折扣后金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
disabled: true,
|
||||
placeholder: '请输入折扣后金额',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['totalProductPrice', 'discountPercent'],
|
||||
trigger(values, form) {
|
||||
const discountPrice =
|
||||
erpPriceMultiply(
|
||||
values.totalProductPrice,
|
||||
values.discountPercent / 100,
|
||||
) ?? 0;
|
||||
form.setFieldValue(
|
||||
'totalPrice',
|
||||
values.totalProductPrice - discountPrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商机名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入商机名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
fixed: 'left',
|
||||
width: 160,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
width: 140,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
formatter: 'formatDate',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
fixed: 'right',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
141
apps/web-ele/src/views/crm/business/detail/data.ts
Normal file
141
apps/web-ele/src/views/crm/business/detail/data.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
DEFAULT_STATUSES,
|
||||
getBusinessStatusSimpleList,
|
||||
} from '#/api/crm/business/status';
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '商机金额(元)',
|
||||
render: (val) => erpPriceInputFormatter(val),
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
label: '商机组',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '商机名称',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '商机金额(元)',
|
||||
render: (val) => erpPriceInputFormatter(val),
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
label: '预计成交日期',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
label: '商机状态组',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
label: '商机阶段',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 商机状态更新表单 */
|
||||
export function useStatusFormSchema(
|
||||
formData: Ref<CrmBusinessApi.Business | undefined>,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'statusId',
|
||||
label: '商机状态',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'endStatus',
|
||||
label: '商机状态',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '商机阶段',
|
||||
component: 'Select',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
async componentProps() {
|
||||
const statusList = await getBusinessStatusSimpleList(
|
||||
formData.value?.statusTypeId ?? 0,
|
||||
);
|
||||
const statusOptions = statusList.map((item) => ({
|
||||
label: `${item.name}(赢单率:${item.percent}%)`,
|
||||
value: item.id,
|
||||
}));
|
||||
const options = DEFAULT_STATUSES.map((item) => ({
|
||||
label: `${item.name}(赢单率:${item.percent}%)`,
|
||||
value: item.endStatus,
|
||||
}));
|
||||
statusOptions.push(...options);
|
||||
return {
|
||||
options: statusOptions,
|
||||
};
|
||||
},
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
191
apps/web-ele/src/views/crm/business/detail/index.vue
Normal file
191
apps/web-ele/src/views/crm/business/detail/index.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { getBusiness } from '#/api/crm/business';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { OperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { $t } from '#/locales';
|
||||
import { ContactDetailsList } from '#/views/crm/contact/components';
|
||||
import { ContractDetailsList } from '#/views/crm/contract/components';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
import { ProductDetailsList } from '#/views/crm/product/components';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailSchema } from './data';
|
||||
import BusinessDetailsInfo from './modules/info.vue';
|
||||
import UpStatusForm from './modules/status-form.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const businessId = ref(0); // 商机编号
|
||||
const business = ref<CrmBusinessApi.Business>({} as CrmBusinessApi.Business); // 商机详情
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]);
|
||||
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: TransferForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [UpStatusModal, upStatusModalApi] = useVbenModal({
|
||||
connectedComponent: UpStatusForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 加载详情 */
|
||||
async function getBusinessDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
business.value = await getBusiness(businessId.value);
|
||||
// 操作日志
|
||||
const res = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_BUSINESS,
|
||||
bizId: businessId.value,
|
||||
});
|
||||
logList.value = res.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'CrmBusiness' });
|
||||
}
|
||||
|
||||
/** 编辑商机 */
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ id: businessId.value }).open();
|
||||
}
|
||||
|
||||
/** 转移商机 */
|
||||
function handleTransfer() {
|
||||
transferModalApi.setData({ bizType: BizTypeEnum.CRM_BUSINESS }).open();
|
||||
}
|
||||
|
||||
/** 更新商机状态操作 */
|
||||
async function handleUpdateStatus() {
|
||||
upStatusModalApi.setData(business.value).open();
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
onMounted(() => {
|
||||
businessId.value = Number(route.params.id);
|
||||
getBusinessDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="business?.name" :loading="loading">
|
||||
<FormModal @success="getBusinessDetail" />
|
||||
<TransferModal @success="getBusinessDetail" />
|
||||
<UpStatusModal @success="getBusinessDetail" />
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '返回',
|
||||
type: 'default',
|
||||
icon: 'lucide:arrow-left',
|
||||
onClick: handleBack,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.edit'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:business:update'],
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
label: '变更商机状态',
|
||||
type: 'primary',
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleUpdateStatus,
|
||||
},
|
||||
{
|
||||
label: '转移',
|
||||
type: 'primary',
|
||||
ifShow: permissionListRef?.validateOwnerUser,
|
||||
onClick: handleTransfer,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<ElCard class="min-h-[10%]">
|
||||
<Descriptions :data="business" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="跟进记录" name="1">
|
||||
<FollowUp :biz-id="businessId" :biz-type="BizTypeEnum.CRM_BUSINESS" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="详细资料" name="2">
|
||||
<BusinessDetailsInfo :business="business" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="联系人" name="3">
|
||||
<ContactDetailsList
|
||||
:biz-id="businessId"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
:business-id="businessId"
|
||||
:customer-id="business.customerId"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="产品" name="4">
|
||||
<ProductDetailsList
|
||||
:biz-id="businessId"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
:business="business"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="合同" name="5">
|
||||
<ContractDetailsList
|
||||
:biz-id="businessId"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="操作日志" name="6">
|
||||
<OperateLog :log-list="logList" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="团队成员" name="7">
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="businessId"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
36
apps/web-ele/src/views/crm/business/detail/modules/info.vue
Normal file
36
apps/web-ele/src/views/crm/business/detail/modules/info.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { ElDivider } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
business: CrmBusinessApi.Business; // 商机信息
|
||||
}>();
|
||||
|
||||
const [BaseDescription] = useDescription({
|
||||
title: '基本信息',
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
|
||||
const [SystemDescription] = useDescription({
|
||||
title: '系统信息',
|
||||
border: false,
|
||||
column: 3,
|
||||
schema: useFollowUpDetailSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseDescription :data="business" />
|
||||
<ElDivider />
|
||||
<SystemDescription :data="business" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { updateBusinessStatus } from '#/api/crm/business';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useStatusFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<CrmBusinessApi.Business>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useStatusFormSchema(formData),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmBusinessApi.Business;
|
||||
try {
|
||||
if (!data.status) {
|
||||
return;
|
||||
}
|
||||
await updateBusinessStatus({
|
||||
id: data.id,
|
||||
statusId: data.status > 0 ? data.status : undefined,
|
||||
endStatus: data.status < 0 ? -data.status : undefined,
|
||||
});
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmBusinessApi.Business>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
data.status = data.endStatus === null ? data.statusId : -data.endStatus;
|
||||
formData.value = data;
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="变更商机状态" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
208
apps/web-ele/src/views/crm/business/index.vue
Normal file
208
apps/web-ele/src/views/crm/business/index.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElLoading,
|
||||
ElMessage,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteBusiness,
|
||||
exportBusiness,
|
||||
getBusinessPage,
|
||||
} from '#/api/crm/business';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理场景类型的切换 */
|
||||
function handleChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const formValues = await gridApi.formApi.getValues();
|
||||
const data = await exportBusiness({
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
downloadFileFromBlobPart({ fileName: '商机.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建商机 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑商机 */
|
||||
function handleEdit(row: CrmBusinessApi.Business) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除商机 */
|
||||
async function handleDelete(row: CrmBusinessApi.Business) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteBusiness(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function handleDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBusinessPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmBusinessApi.Business>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【商机】商机管理、商机状态"
|
||||
url="https://doc.iocoder.cn/crm/business/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
@tab-change="handleChangeSceneType"
|
||||
v-model:model-value="sceneType"
|
||||
>
|
||||
<ElTabPane label="我负责的" name="1" />
|
||||
<ElTabPane label="我参与的" name="2" />
|
||||
<ElTabPane label="下属负责的" name="3" />
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商机']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:business:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:business:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:business:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:business:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
121
apps/web-ele/src/views/crm/business/modules/form.vue
Normal file
121
apps/web-ele/src/views/crm/business/modules/form.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { erpPriceMultiply } from '@vben/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createBusiness,
|
||||
getBusiness,
|
||||
updateBusiness,
|
||||
} from '#/api/crm/business';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
import { ProductEditTable } from '#/views/crm/product/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmBusinessApi.Business>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['商机'])
|
||||
: $t('ui.actionTitle.create', ['商机']);
|
||||
});
|
||||
|
||||
function handleUpdateProducts(products: any) {
|
||||
formData.value = modalApi.getData<CrmBusinessApi.Business>();
|
||||
formData.value!.products = products;
|
||||
if (formData.value) {
|
||||
const totalProductPrice =
|
||||
formData.value.products?.reduce(
|
||||
(prev, curr) => prev + curr.totalPrice,
|
||||
0,
|
||||
) ?? 0;
|
||||
const discountPercent = formData.value.discountPercent;
|
||||
const discountPrice =
|
||||
discountPercent === null
|
||||
? 0
|
||||
: erpPriceMultiply(totalProductPrice, discountPercent / 100);
|
||||
const totalPrice = totalProductPrice - (discountPrice ?? 0);
|
||||
formData.value!.totalProductPrice = totalProductPrice;
|
||||
formData.value!.totalPrice = totalPrice;
|
||||
formApi.setValues(formData.value!);
|
||||
}
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 120,
|
||||
},
|
||||
wrapperClass: 'grid-cols-3',
|
||||
layout: 'vertical',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmBusinessApi.Business;
|
||||
data.products = formData.value?.products;
|
||||
try {
|
||||
await (formData.value?.id ? updateBusiness(data) : createBusiness(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmBusinessApi.Business>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data.id ? await getBusiness(data.id) : data;
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/2">
|
||||
<Form class="mx-4">
|
||||
<template #product="slotProps">
|
||||
<ProductEditTable
|
||||
v-bind="slotProps"
|
||||
class="w-full"
|
||||
:products="formData?.products ?? []"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
@update:products="handleUpdateProducts"
|
||||
/>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
112
apps/web-ele/src/views/crm/business/status/data.ts
Normal file
112
apps/web-ele/src/views/crm/business/status/data.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { getDeptList } from '#/api/system/dept';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '状态组名',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入状态组名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'deptIds',
|
||||
label: '应用部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
multiple: true,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '请选择应用部门',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
help: '不选择部门时,默认全公司生效',
|
||||
},
|
||||
{
|
||||
fieldName: 'statuses',
|
||||
label: '阶段设置',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '状态组名',
|
||||
},
|
||||
{
|
||||
field: 'deptNames',
|
||||
title: '应用部门',
|
||||
formatter: ({ cellValue }) =>
|
||||
cellValue?.length > 0 ? cellValue.join(' ') : '全公司',
|
||||
},
|
||||
{
|
||||
field: 'creator',
|
||||
title: '创建人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 商机状态阶段列表列配置 */
|
||||
export function useFormColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
136
apps/web-ele/src/views/crm/business/status/index.vue
Normal file
136
apps/web-ele/src/views/crm/business/status/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteBusinessStatus,
|
||||
getBusinessStatusPage,
|
||||
} from '#/api/crm/business/status';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建商机状态 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑商机状态 */
|
||||
function handleEdit(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除商机状态 */
|
||||
async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteBusinessStatus(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBusinessStatusPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmBusinessStatusApi.BusinessStatus>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【商机】商机管理、商机状态"
|
||||
url="https://doc.iocoder.cn/crm/business/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="商机状态列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商机状态']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['system:post:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:business-status:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:business-status:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
208
apps/web-ele/src/views/crm/business/status/modules/form.vue
Normal file
208
apps/web-ele/src/views/crm/business/status/modules/form.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElInput, ElInputNumber, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
createBusinessStatus,
|
||||
DEFAULT_STATUSES,
|
||||
getBusinessStatus,
|
||||
updateBusinessStatus,
|
||||
} from '#/api/crm/business/status';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormColumns, useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmBusinessStatusApi.BusinessStatus>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['商机状态'])
|
||||
: $t('ui.actionTitle.create', ['商机状态']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
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));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmBusinessStatusApi.BusinessStatus>();
|
||||
modalApi.lock();
|
||||
try {
|
||||
if (!data || !data.id) {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
deptIds: [],
|
||||
statuses: [],
|
||||
};
|
||||
await handleAddStatus();
|
||||
} else {
|
||||
formData.value = await getBusinessStatus(data.id);
|
||||
if (
|
||||
!formData.value?.statuses?.length ||
|
||||
formData.value?.statuses?.length === 0
|
||||
) {
|
||||
await handleAddStatus();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value as any);
|
||||
await gridApi.grid.reloadData(
|
||||
(formData.value!.statuses =
|
||||
formData.value?.statuses?.concat(DEFAULT_STATUSES)) as any,
|
||||
);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 添加状态 */
|
||||
async function handleAddStatus() {
|
||||
formData.value!.statuses!.splice(-3, 0, {
|
||||
name: '',
|
||||
percent: undefined,
|
||||
} as any);
|
||||
await nextTick();
|
||||
await gridApi.grid.reloadData(formData.value!.statuses as any);
|
||||
}
|
||||
|
||||
/** 删除状态 */
|
||||
async function deleteStatusArea(row: any, rowIndex: number) {
|
||||
await gridApi.grid.remove(row);
|
||||
formData.value!.statuses!.splice(rowIndex, 1);
|
||||
await gridApi.grid.reloadData(formData.value!.statuses as any);
|
||||
}
|
||||
|
||||
/** 表格配置 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
},
|
||||
columns: useFormColumns(),
|
||||
data: formData.value?.statuses?.concat(DEFAULT_STATUSES),
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'row_id',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/2">
|
||||
<Form class="mx-4">
|
||||
<template #statuses>
|
||||
<Grid class="w-full">
|
||||
<template #defaultStatus="{ row, rowIndex }">
|
||||
<span>
|
||||
{{ row.defaultStatus ? '结束' : `阶段${rowIndex + 1}` }}
|
||||
</span>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElInput
|
||||
v-if="!row.endStatus"
|
||||
v-model="row.name"
|
||||
placeholder="请输入状态名"
|
||||
/>
|
||||
<span v-else>{{ row.name }}</span>
|
||||
</template>
|
||||
<template #percent="{ row }">
|
||||
<ElInputNumber
|
||||
v-if="!row.endStatus"
|
||||
v-model="row.percent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
placeholder="请输入赢单率"
|
||||
controls-position="right"
|
||||
class="!w-full"
|
||||
/>
|
||||
<span v-else>{{ row.percent }}</span>
|
||||
</template>
|
||||
<template #actions="{ row, rowIndex }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
ifShow: () => !row.endStatus,
|
||||
onClick: handleAddStatus,
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
ifShow: () => !row.endStatus,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: deleteStatusArea.bind(null, row, rowIndex),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
327
apps/web-ele/src/views/crm/clue/data.ts
Normal file
327
apps/web-ele/src/views/crm/clue/data.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '线索名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入线索名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'source',
|
||||
label: '客户来源',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
|
||||
placeholder: '请选择客户来源',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
clearable: true,
|
||||
placeholder: '请选择负责人',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电话',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'wechat',
|
||||
label: '微信',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入微信',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'qq',
|
||||
label: 'QQ',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 QQ',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'industryId',
|
||||
label: '客户行业',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
|
||||
placeholder: '请选择客户行业',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'level',
|
||||
label: '客户级别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
|
||||
placeholder: '请选择客户级别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'areaId',
|
||||
label: '地址',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getAreaTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '请选择地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'detailAddress',
|
||||
label: '详细地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入详细地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下次联系时间',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '线索名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入线索名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'transformStatus',
|
||||
label: '转化状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '未转化', value: false },
|
||||
{ label: '已转化', value: true },
|
||||
],
|
||||
placeholder: '请选择转化状态',
|
||||
clearable: true,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电话',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
placeholder: ['开始日期', '结束日期'],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '线索名称',
|
||||
fixed: 'left',
|
||||
minWidth: 160,
|
||||
slots: {
|
||||
default: 'name',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
title: '线索来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 135,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'contactLastContent',
|
||||
title: '最后跟进记录',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
111
apps/web-ele/src/views/crm/clue/detail/data.ts
Normal file
111
apps/web-ele/src/views/crm/clue/detail/data.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 详情头部的配置 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'source',
|
||||
label: '线索来源',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情基本信息的配置 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '线索名称',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
label: '客户来源',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
render: (val, data) => {
|
||||
const areaName = val ?? '';
|
||||
const detailAddress = data?.detailAddress ?? '';
|
||||
return [areaName, detailAddress].filter((item) => !!item).join(' ');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
label: '客户行业',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_LEVEL,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
174
apps/web-ele/src/views/crm/clue/detail/index.vue
Normal file
174
apps/web-ele/src/views/crm/clue/detail/index.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { getClue, transformClue } from '#/api/crm/clue';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { OperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailSchema } from './data';
|
||||
import Info from './modules/info.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const clueId = ref(0); // 线索编号
|
||||
const clue = ref<CrmClueApi.Clue>({} as CrmClueApi.Clue); // 线索详情
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志
|
||||
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: TransferForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 加载线索详情 */
|
||||
async function getClueDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
clue.value = await getClue(clueId.value);
|
||||
// 操作日志
|
||||
const res = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_CLUE,
|
||||
bizId: clueId.value,
|
||||
});
|
||||
logList.value = res.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'CrmClue' });
|
||||
}
|
||||
|
||||
/** 编辑线索 */
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ id: clueId.value }).open();
|
||||
}
|
||||
|
||||
/** 转移线索 */
|
||||
function handleTransfer() {
|
||||
transferModalApi.setData({ bizType: BizTypeEnum.CRM_CLUE }).open();
|
||||
}
|
||||
|
||||
/** 转化为客户 */
|
||||
async function handleTransform(): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: '确定将该线索转化为客户吗?',
|
||||
})
|
||||
.then(async () => {
|
||||
// 转化为客户
|
||||
await transformClue(clueId.value);
|
||||
// 提示并返回成功
|
||||
ElMessage.success('转化客户成功');
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
onMounted(() => {
|
||||
clueId.value = Number(route.params.id);
|
||||
getClueDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="clue?.name" :loading="loading">
|
||||
<FormModal @success="getClueDetail" />
|
||||
<TransferModal @success="getClueDetail" />
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '返回',
|
||||
type: 'default',
|
||||
icon: 'lucide:arrow-left',
|
||||
onClick: handleBack,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.edit'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:clue:update'],
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
label: '转移',
|
||||
type: 'primary',
|
||||
ifShow: permissionListRef?.validateOwnerUser,
|
||||
onClick: handleTransfer,
|
||||
},
|
||||
{
|
||||
label: '转化为客户',
|
||||
type: 'primary',
|
||||
ifShow:
|
||||
permissionListRef?.validateOwnerUser && !clue?.transformStatus,
|
||||
onClick: handleTransform,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<ElCard class="min-h-[10%]">
|
||||
<Descriptions :data="clue" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="跟进记录" name="1">
|
||||
<FollowUp :biz-id="clueId" :biz-type="BizTypeEnum.CRM_CLUE" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="基本信息" name="2">
|
||||
<Info :clue="clue" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="团队成员" name="3">
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="clueId"
|
||||
:biz-type="BizTypeEnum.CRM_CLUE"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="操作日志" name="4">
|
||||
<OperateLog :log-list="logList" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
36
apps/web-ele/src/views/crm/clue/detail/modules/info.vue
Normal file
36
apps/web-ele/src/views/crm/clue/detail/modules/info.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
|
||||
import { ElDivider } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
clue: CrmClueApi.Clue;
|
||||
}>();
|
||||
|
||||
const [BaseDescriptions] = useDescription({
|
||||
title: '基本信息',
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
|
||||
const [SystemDescriptions] = useDescription({
|
||||
title: '系统信息',
|
||||
border: false,
|
||||
column: 3,
|
||||
schema: useFollowUpDetailSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseDescriptions :data="clue" />
|
||||
<ElDivider />
|
||||
<SystemDescriptions :data="clue" />
|
||||
</div>
|
||||
</template>
|
||||
193
apps/web-ele/src/views/crm/clue/index.vue
Normal file
193
apps/web-ele/src/views/crm/clue/index.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElLoading,
|
||||
ElMessage,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteClue, exportClue, getCluePage } from '#/api/crm/clue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportClue({
|
||||
sceneType: sceneType.value,
|
||||
...(await gridApi.formApi.getValues()),
|
||||
});
|
||||
downloadFileFromBlobPart({ fileName: '线索.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建线索 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑线索 */
|
||||
function handleEdit(row: CrmClueApi.Clue) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除线索 */
|
||||
async function handleDelete(row: CrmClueApi.Clue) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteClue(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看线索详情 */
|
||||
function handleDetail(row: CrmClueApi.Clue) {
|
||||
push({ name: 'CrmClueDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 处理场景类型的切换 */
|
||||
function handleChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCluePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmClueApi.Clue>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【线索】线索管理"
|
||||
url="https://doc.iocoder.cn/crm/clue/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
@tab-change="handleChangeSceneType"
|
||||
v-model:model-value="sceneType"
|
||||
>
|
||||
<ElTabPane label="我负责的" name="1" />
|
||||
<ElTabPane label="我参与的" name="2" />
|
||||
<ElTabPane label="下属负责的" name="3" />
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['线索']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:clue:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:clue:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:clue:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:clue:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
82
apps/web-ele/src/views/crm/clue/modules/form.vue
Normal file
82
apps/web-ele/src/views/crm/clue/modules/form.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createClue, getClue, updateClue } from '#/api/crm/clue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmClueApi.Clue>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['线索'])
|
||||
: $t('ui.actionTitle.create', ['线索']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmClueApi.Clue;
|
||||
try {
|
||||
await (formData.value?.id ? updateClue(data) : createClue(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmClueApi.Clue>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getClue(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
62
apps/web-ele/src/views/crm/contact/components/data.ts
Normal file
62
apps/web-ele/src/views/crm/contact/components/data.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
|
||||
/** 联系人明细列表列配置 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '姓名',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
title: '职位',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '关键决策人',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<!-- 联系人列表的选择:用于【商机】详情中,选择它要关联的联系人 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getContactPageByCustomer } from '#/api/crm/contact';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailListColumns } from './data';
|
||||
|
||||
const props = defineProps<{
|
||||
customerId?: number; // 关联联系人与商机时,需要传入 customerId 进行筛选
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const checkedRows = ref<CrmContactApi.Contact[]>([]);
|
||||
function setCheckedRows({ records }: { records: CrmContactApi.Contact[] }) {
|
||||
checkedRows.value = records;
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建商机 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ customerId: props.customerId }).open();
|
||||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function handleDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (checkedRows.value.length === 0) {
|
||||
ElMessage.error('请先选择联系人后操作!');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
try {
|
||||
const contactIds = checkedRows.value.map((item) => item.id);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success', contactIds, checkedRows.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '联系人名称',
|
||||
component: 'Input',
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useDetailListColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContactPageByCustomer({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.customerId,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContactApi.Contact>,
|
||||
gridEvents: {
|
||||
checkboxAll: setCheckedRows,
|
||||
checkboxChange: setCheckedRows,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="关联联系人" class="w-2/5">
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['联系人']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:contact:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
210
apps/web-ele/src/views/crm/contact/components/detail-list.vue
Normal file
210
apps/web-ele/src/views/crm/contact/components/detail-list.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<!-- 联系人列表:用于【联系人】【商机】详情中,展示它们关联的联系人列表 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
createBusinessContactList,
|
||||
deleteBusinessContactList,
|
||||
getContactPageByBusiness,
|
||||
getContactPageByCustomer,
|
||||
} from '#/api/crm/contact';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailListColumns } from './data';
|
||||
import ListModal from './detail-list-modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
bizId: number; // 业务编号
|
||||
bizType: number; // 业务类型
|
||||
businessId?: number; // 特殊:商机编号;在【商机】详情中,可以传递商机编号,默认新建的联系人关联到该商机
|
||||
customerId?: number; // 特殊:客户编号;在【商机】详情中,可以传递客户编号,默认新建的联系人关联到该客户
|
||||
}>();
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailListModal, detailListModalApi] = useVbenModal({
|
||||
connectedComponent: ListModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const checkedRows = ref<CrmContactApi.Contact[]>([]);
|
||||
function setCheckedRows({ records }: { records: CrmContactApi.Contact[] }) {
|
||||
checkedRows.value = records;
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建联系人 */
|
||||
function handleCreate() {
|
||||
formModalApi
|
||||
.setData({ customerId: props.customerId, businessId: props.businessId })
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 关联联系人 */
|
||||
function handleCreateContact() {
|
||||
detailListModalApi.setData({ customerId: props.customerId }).open();
|
||||
}
|
||||
|
||||
/** 解除联系人关联 */
|
||||
async function handleDeleteContactBusinessList() {
|
||||
if (checkedRows.value.length === 0) {
|
||||
ElMessage.error('请先选择联系人后操作!');
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定要将${checkedRows.value.map((item) => item.name).join(',')}解除关联吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteBusinessContactList({
|
||||
businessId: props.bizId,
|
||||
contactIds: checkedRows.value.map((item) => item.id),
|
||||
});
|
||||
if (res) {
|
||||
// 提示并返回成功
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
handleRefresh();
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建商机联系人关联 */
|
||||
async function handleCreateBusinessContactList(contactIds: number[]) {
|
||||
const data = {
|
||||
businessId: props.bizId,
|
||||
contactIds,
|
||||
} as CrmContactApi.BusinessContactReqVO;
|
||||
await createBusinessContactList(data);
|
||||
handleRefresh();
|
||||
}
|
||||
|
||||
/** 查看联系人详情 */
|
||||
function handleDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDetailListColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
return await getContactPageByCustomer({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.bizId,
|
||||
...formValues,
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
|
||||
return await getContactPageByBusiness({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
businessId: props.bizId,
|
||||
...formValues,
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContactApi.Contact>,
|
||||
gridEvents: {
|
||||
checkboxAll: setCheckedRows,
|
||||
checkboxChange: setCheckedRows,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailListModal
|
||||
:customer-id="customerId"
|
||||
@success="handleCreateBusinessContactList"
|
||||
/>
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['联系人']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: '关联',
|
||||
icon: ACTION_ICON.ADD,
|
||||
type: 'default',
|
||||
auth: ['crm:contact:create-business'],
|
||||
ifShow: () => !!businessId,
|
||||
onClick: handleCreateContact,
|
||||
},
|
||||
{
|
||||
label: '解除关联',
|
||||
icon: ACTION_ICON.ADD,
|
||||
type: 'default',
|
||||
auth: ['crm:contact:create-business'],
|
||||
ifShow: () => !!businessId,
|
||||
onClick: handleDeleteContactBusinessList,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
1
apps/web-ele/src/views/crm/contact/components/index.ts
Normal file
1
apps/web-ele/src/views/crm/contact/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ContactDetailsList } from './detail-list.vue';
|
||||
366
apps/web-ele/src/views/crm/contact/data.ts
Normal file
366
apps/web-ele/src/views/crm/contact/data.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getSimpleContactList } from '#/api/crm/contact';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '联系人姓名',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入联系人姓名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择负责人',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户名称',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电话',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'wechat',
|
||||
label: '微信',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入微信',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'qq',
|
||||
label: 'QQ',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入QQ',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'post',
|
||||
label: '职位',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入职位',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'master',
|
||||
label: '关键决策人',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
placeholder: '请选择是否关键决策人',
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '直属上级',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleContactList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择直属上级',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'areaId',
|
||||
label: '地址',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getAreaTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '请选择地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'detailAddress',
|
||||
label: '详细地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入详细地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下次联系时间',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '姓名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入联系人姓名',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电话',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'wechat',
|
||||
label: '微信',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入微信',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '电子邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电子邮箱',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '联系人姓名',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
minWidth: 240,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
title: '职位',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
title: '地址',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '详细地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '关键决策人',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'parentId',
|
||||
title: '直属上级',
|
||||
minWidth: 120,
|
||||
slots: { default: 'parentId' },
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
106
apps/web-ele/src/views/crm/contact/detail/data.ts
Normal file
106
apps/web-ele/src/views/crm/contact/detail/data.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
label: '职务',
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
render: (val, data) => {
|
||||
const areaName = val ?? '';
|
||||
const detailAddress = data?.detailAddress ?? '';
|
||||
return [areaName, detailAddress].filter((item) => !!item).join(' ');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
label: '职务',
|
||||
},
|
||||
{
|
||||
field: 'parentName',
|
||||
label: '直属上级',
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
label: '关键决策人',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
render: (val) =>
|
||||
h(DictTag, { type: DICT_TYPE.SYSTEM_USER_SEX, value: val }),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
158
apps/web-ele/src/views/crm/contact/detail/index.vue
Normal file
158
apps/web-ele/src/views/crm/contact/detail/index.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { getContact } from '#/api/crm/contact';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { OperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { $t } from '#/locales';
|
||||
import { BusinessDetailsList } from '#/views/crm/business/components';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailSchema } from './data';
|
||||
import Info from './modules/info.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const contactId = ref(0); // 联系人编号
|
||||
const contact = ref<CrmContactApi.Contact>({} as CrmContactApi.Contact); // 联系人详情
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志
|
||||
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: TransferForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 加载联系人详情 */
|
||||
async function getContactDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
contact.value = await getContact(contactId.value);
|
||||
// 操作日志
|
||||
const res = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_CONTACT,
|
||||
bizId: contactId.value,
|
||||
});
|
||||
logList.value = res.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'CrmContact' });
|
||||
}
|
||||
|
||||
/** 编辑联系人 */
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ id: contactId.value }).open();
|
||||
}
|
||||
|
||||
/** 转移联系人 */
|
||||
function handleTransfer() {
|
||||
transferModalApi.setData({ id: contactId.value }).open();
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
onMounted(() => {
|
||||
contactId.value = Number(route.params.id);
|
||||
getContactDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="contact?.name" :loading="loading">
|
||||
<FormModal @success="getContactDetail" />
|
||||
<TransferModal @success="getContactDetail" />
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '返回',
|
||||
type: 'default',
|
||||
icon: 'lucide:arrow-left',
|
||||
onClick: handleBack,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.edit'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:contact:update'],
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
label: '转移',
|
||||
type: 'primary',
|
||||
ifShow: permissionListRef?.validateOwnerUser,
|
||||
onClick: handleTransfer,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<ElCard class="min-h-[10%]">
|
||||
<Descriptions :data="contact" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="跟进记录" name="1">
|
||||
<FollowUp :biz-id="contactId" :biz-type="BizTypeEnum.CRM_CONTACT" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="详细资料" name="2">
|
||||
<Info :contact="contact" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="操作日志" name="3">
|
||||
<OperateLog :log-list="logList" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="团队成员" name="4">
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="contactId"
|
||||
:biz-type="BizTypeEnum.CRM_CONTACT"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="商机" name="5">
|
||||
<BusinessDetailsList
|
||||
:biz-id="contactId"
|
||||
:biz-type="BizTypeEnum.CRM_CONTACT"
|
||||
:contact-id="contactId"
|
||||
:customer-id="contact.customerId"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
36
apps/web-ele/src/views/crm/contact/detail/modules/info.vue
Normal file
36
apps/web-ele/src/views/crm/contact/detail/modules/info.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { ElDivider } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
contact: CrmContactApi.Contact;
|
||||
}>();
|
||||
|
||||
const [BaseDescriptions] = useDescription({
|
||||
title: '基本信息',
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
|
||||
const [SystemDescriptions] = useDescription({
|
||||
title: '系统信息',
|
||||
border: false,
|
||||
column: 3,
|
||||
schema: useFollowUpDetailSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseDescriptions :data="contact" />
|
||||
<ElDivider />
|
||||
<SystemDescriptions :data="contact" />
|
||||
</div>
|
||||
</template>
|
||||
213
apps/web-ele/src/views/crm/contact/index.vue
Normal file
213
apps/web-ele/src/views/crm/contact/index.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElLoading,
|
||||
ElMessage,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteContact,
|
||||
exportContact,
|
||||
getContactPage,
|
||||
} from '#/api/crm/contact';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理场景类型的切换 */
|
||||
function handleChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const formValues = await gridApi.formApi.getValues();
|
||||
const data = await exportContact({
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
downloadFileFromBlobPart({ fileName: '联系人.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建联系人 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑联系人 */
|
||||
function handleEdit(row: CrmContactApi.Contact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除联系人 */
|
||||
async function handleDelete(row: CrmContactApi.Contact) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteContact(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看联系人详情 */
|
||||
function handleDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContactPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContactApi.Contact>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
@tab-change="handleChangeSceneType"
|
||||
v-model:model-value="sceneType"
|
||||
>
|
||||
<ElTabPane label="我负责的" name="1" />
|
||||
<ElTabPane label="我参与的" name="2" />
|
||||
<ElTabPane label="下属负责的" name="3" />
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['联系人']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:contact:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:contact:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #parentId="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.parentName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:contact:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:contact:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
80
apps/web-ele/src/views/crm/contact/modules/form.vue
Normal file
80
apps/web-ele/src/views/crm/contact/modules/form.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { createContact, getContact, updateContact } from '#/api/crm/contact';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmContactApi.Contact>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['联系人'])
|
||||
: $t('ui.actionTitle.create', ['联系人']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmContactApi.Contact;
|
||||
try {
|
||||
await (formData.value?.id ? updateContact(data) : createContact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmContactApi.Contact>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getContact(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
92
apps/web-ele/src/views/crm/contract/components/data.ts
Normal file
92
apps/web-ele/src/views/crm/contract/components/data.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { erpPriceInputFormatter } from '@vben/utils';
|
||||
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '合同名称',
|
||||
field: 'name',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '合同开始时间',
|
||||
field: 'startTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同结束时间',
|
||||
field: 'endTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '已回款金额(元)',
|
||||
field: 'totalReceivablePrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '未回款金额(元)',
|
||||
field: 'unpaidPrice',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => {
|
||||
return erpPriceInputFormatter(
|
||||
row.totalPrice - row.totalReceivablePrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
field: 'ownerUserDeptName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
field: 'creatorName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同状态',
|
||||
field: 'auditStatus',
|
||||
fixed: 'right',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
133
apps/web-ele/src/views/crm/contract/components/detail-list.vue
Normal file
133
apps/web-ele/src/views/crm/contract/components/detail-list.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<!-- 合同列表:用于【客户】【商机】【联系人】详情中,展示它们关联的合同列表 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getContractPageByBusiness,
|
||||
getContractPageByCustomer,
|
||||
} from '#/api/crm/contract';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailListColumns } from './data';
|
||||
|
||||
const props = defineProps<{
|
||||
bizId: number; // 业务编号
|
||||
bizType: number; // 业务类型
|
||||
}>();
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 已选择的合同 */
|
||||
const checkedRows = ref<CrmContractApi.Contract[]>();
|
||||
function setCheckedRows({ records }: { records: CrmContractApi.Contract[] }) {
|
||||
checkedRows.value = records;
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建合同 */
|
||||
function handleCreate() {
|
||||
formModalApi
|
||||
.setData(
|
||||
props.bizType === BizTypeEnum.CRM_CUSTOMER
|
||||
? {
|
||||
customerId: props.bizId,
|
||||
}
|
||||
: { businessId: props.bizId },
|
||||
)
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 查看合同详情 */
|
||||
function handleDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDetailListColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (props.bizType === BizTypeEnum.CRM_CUSTOMER) {
|
||||
return await getContractPageByCustomer({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
customerId: props.bizId,
|
||||
...formValues,
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_CONTACT) {
|
||||
return await getContractPageByBusiness({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
businessId: props.bizId,
|
||||
...formValues,
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
gridEvents: {
|
||||
checkboxAll: setCheckedRows,
|
||||
checkboxChange: setCheckedRows,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['合同']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:contract:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
1
apps/web-ele/src/views/crm/contract/components/index.ts
Normal file
1
apps/web-ele/src/views/crm/contract/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ContractDetailsList } from './detail-list.vue';
|
||||
40
apps/web-ele/src/views/crm/contract/config/data.ts
Normal file
40
apps/web-ele/src/views/crm/contract/config/data.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
export const schema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'notifyEnabled',
|
||||
label: '提前提醒设置',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '提醒', value: true },
|
||||
{ label: '不提醒', value: false },
|
||||
],
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'notifyDays',
|
||||
componentProps: {
|
||||
placeholder: '请输入天数',
|
||||
class: '!w-full',
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
prepend: () => '提前',
|
||||
append: () => '天提醒',
|
||||
}),
|
||||
rules: z.coerce.number().int().min(0, '天数不能小于 0'),
|
||||
dependencies: {
|
||||
triggerFields: ['notifyEnabled'],
|
||||
show: (values) => values.notifyEnabled,
|
||||
trigger(values) {
|
||||
if (!values.notifyEnabled) {
|
||||
values.notifyDays = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
61
apps/web-ele/src/views/crm/contract/config/index.vue
Normal file
61
apps/web-ele/src/views/crm/contract/config/index.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmContractConfigApi } from '#/api/crm/contract/config';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ElCard, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
getContractConfig,
|
||||
saveContractConfig,
|
||||
} from '#/api/crm/contract/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { schema } from './data';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelClass: 'w-100',
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema,
|
||||
handleSubmit,
|
||||
});
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = (await formApi.getValues()) as CrmContractConfigApi.Config;
|
||||
if (!data.notifyEnabled) {
|
||||
data.notifyDays = undefined;
|
||||
}
|
||||
await saveContractConfig(data);
|
||||
await formApi.setValues(data);
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
}
|
||||
|
||||
/** 获取配置 */
|
||||
async function getConfigInfo() {
|
||||
const res = await getContractConfig();
|
||||
await formApi.setValues(res);
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getConfigInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElCard title="合同配置设置">
|
||||
<Form class="w-1/4" />
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
421
apps/web-ele/src/views/crm/contract/data.ts
Normal file
421
apps/web-ele/src/views/crm/contract/data.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { erpPriceInputFormatter, erpPriceMultiply } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleBusinessList } from '#/api/crm/business';
|
||||
import { getSimpleContactList } from '#/api/crm/contact';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '合同编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '保存时自动生成',
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '合同名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户名称',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'businessId',
|
||||
label: '商机名称',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
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',
|
||||
label: '下单日期',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下单日期',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '合同开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择合同开始时间',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '合同结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择合同结束时间',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'signUserId',
|
||||
label: '公司签约人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
fieldName: 'signContactId',
|
||||
label: '客户签约人',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
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',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'product',
|
||||
label: '产品清单',
|
||||
component: 'Input',
|
||||
formItemClass: 'col-span-3',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalProductPrice',
|
||||
label: '产品总金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
placeholder: '请输入产品总金额',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'discountPercent',
|
||||
label: '整单折扣(%)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
placeholder: '请输入整单折扣',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: z.number().min(0).max(100).optional().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '折扣后金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
disabled: true,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['totalProductPrice', 'discountPercent'],
|
||||
trigger(values, form) {
|
||||
const discountPrice =
|
||||
erpPriceMultiply(
|
||||
values.totalProductPrice,
|
||||
values.discountPercent / 100,
|
||||
) ?? 0;
|
||||
form.setFieldValue(
|
||||
'totalPrice',
|
||||
values.totalProductPrice - discountPrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '合同编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同编号',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '合同名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'no',
|
||||
minWidth: 180,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '合同名称',
|
||||
field: 'name',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 120,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
title: '商机名称',
|
||||
field: 'businessName',
|
||||
minWidth: 130,
|
||||
slots: { default: 'businessName' },
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'totalPrice',
|
||||
minWidth: 140,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '下单时间',
|
||||
field: 'orderDate',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同开始时间',
|
||||
field: 'startTime',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同结束时间',
|
||||
field: 'endTime',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '客户签约人',
|
||||
field: 'signContactName',
|
||||
minWidth: 130,
|
||||
slots: { default: 'signContactName' },
|
||||
},
|
||||
{
|
||||
title: '公司签约人',
|
||||
field: 'signUserName',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '已回款金额(元)',
|
||||
field: 'totalReceivablePrice',
|
||||
minWidth: 140,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '未回款金额(元)',
|
||||
field: 'unReceivablePrice',
|
||||
minWidth: 140,
|
||||
formatter: ({ row }) => {
|
||||
return erpPriceInputFormatter(
|
||||
row.totalPrice - row.totalReceivablePrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后跟进时间',
|
||||
field: 'contactLastTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
field: 'ownerUserDeptName',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
field: 'updateTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
field: 'creatorName',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '合同状态',
|
||||
field: 'auditStatus',
|
||||
fixed: 'right',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
minWidth: 130,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
100
apps/web-ele/src/views/crm/contract/detail/data.ts
Normal file
100
apps/web-ele/src/views/crm/contract/detail/data.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 详情头部的配置 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
render: (val) => erpPriceInputFormatter(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
label: '下单时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'totalReceivablePrice',
|
||||
label: '回款金额(元)',
|
||||
render: (val) => erpPriceInputFormatter(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情基本信息的配置 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '合同编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '合同名称',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'businessName',
|
||||
label: '商机名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
render: (val) => erpPriceInputFormatter(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
label: '下单时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
label: '合同开始时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
label: '合同结束时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'signContactName',
|
||||
label: '客户签约人',
|
||||
},
|
||||
{
|
||||
field: 'signUserName',
|
||||
label: '公司签约人',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
{
|
||||
field: 'auditStatus',
|
||||
label: '合同状态',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_AUDIT_STATUS,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
170
apps/web-ele/src/views/crm/contract/detail/index.vue
Normal file
170
apps/web-ele/src/views/crm/contract/detail/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { getContract } from '#/api/crm/contract';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { OperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { $t } from '#/locales';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
import { ProductDetailsList } from '#/views/crm/product/components';
|
||||
import { ReceivableDetailsList } from '#/views/crm/receivable/components';
|
||||
import { ReceivablePlanDetailsList } from '#/views/crm/receivable/plan/components';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailSchema } from './data';
|
||||
import ContractDetailsInfo from './modules/info.vue';
|
||||
|
||||
const props = defineProps<{ id?: number }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const contractId = ref(0); // 合同编号
|
||||
const contract = ref<CrmContractApi.Contract>({} as CrmContractApi.Contract); // 合同详情
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志
|
||||
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: TransferForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 加载合同详情 */
|
||||
async function loadContractDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
contract.value = await getContract(contractId.value);
|
||||
// 操作日志
|
||||
const res = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_CONTRACT,
|
||||
bizId: contractId.value,
|
||||
});
|
||||
logList.value = res.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'CrmContract' });
|
||||
}
|
||||
|
||||
/** 编辑合同 */
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ id: contractId.value }).open();
|
||||
}
|
||||
|
||||
/** 转移合同 */
|
||||
function handleTransfer() {
|
||||
transferModalApi.setData({ bizType: BizTypeEnum.CRM_CONTRACT }).open();
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
onMounted(() => {
|
||||
contractId.value = Number(props.id || route.params.id);
|
||||
loadContractDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="contract?.name" :loading="loading">
|
||||
<FormModal @success="loadContractDetail" />
|
||||
<TransferModal @success="loadContractDetail" />
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '返回',
|
||||
type: 'default',
|
||||
icon: 'lucide:arrow-left',
|
||||
onClick: handleBack,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.edit'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:contract:update'],
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
label: '转移',
|
||||
type: 'primary',
|
||||
ifShow: permissionListRef?.validateOwnerUser,
|
||||
onClick: handleTransfer,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<ElCard class="min-h-[10%]">
|
||||
<Descriptions :data="contract" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="跟进记录" name="1">
|
||||
<FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="基本信息" name="2">
|
||||
<ContractDetailsInfo :contract="contract" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="产品" name="3">
|
||||
<ProductDetailsList
|
||||
:biz-id="contractId"
|
||||
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="回款" name="4" v-if="contract.customerId">
|
||||
<ReceivablePlanDetailsList
|
||||
:contract-id="contractId"
|
||||
:customer-id="contract.customerId"
|
||||
/>
|
||||
<ReceivableDetailsList
|
||||
:contract-id="contractId"
|
||||
:customer-id="contract.customerId"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="团队成员" name="5">
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="contractId"
|
||||
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="操作日志" name="6">
|
||||
<OperateLog :log-list="logList" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
36
apps/web-ele/src/views/crm/contract/detail/modules/info.vue
Normal file
36
apps/web-ele/src/views/crm/contract/detail/modules/info.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { ElDivider } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
contract: CrmContractApi.Contract; // 合同信息
|
||||
}>();
|
||||
|
||||
const [BaseDescriptions] = useDescription({
|
||||
title: '基本信息',
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
|
||||
const [SystemDescriptions] = useDescription({
|
||||
title: '系统信息',
|
||||
border: false,
|
||||
column: 3,
|
||||
schema: useFollowUpDetailSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseDescriptions :data="contract" />
|
||||
<ElDivider />
|
||||
<SystemDescriptions :data="contract" />
|
||||
</div>
|
||||
</template>
|
||||
267
apps/web-ele/src/views/crm/contract/index.vue
Normal file
267
apps/web-ele/src/views/crm/contract/index.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElLoading,
|
||||
ElMessage,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteContract,
|
||||
exportContract,
|
||||
getContractPage,
|
||||
submitContract,
|
||||
} from '#/api/crm/contract';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理场景类型的切换 */
|
||||
function handleChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const formValues = await gridApi.formApi.getValues();
|
||||
const data = await exportContract({
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
downloadFileFromBlobPart({ fileName: '合同.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建合同 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑合同 */
|
||||
function handleEdit(row: CrmContractApi.Contract) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除合同 */
|
||||
async function handleDelete(row: CrmContractApi.Contract) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteContract(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交审核 */
|
||||
async function handleSubmit(row: CrmContractApi.Contract) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '提交审核中...',
|
||||
});
|
||||
try {
|
||||
await submitContract(row.id!);
|
||||
ElMessage.success('提交审核成功');
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看合同详情 */
|
||||
function handleDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 查看联系人详情 */
|
||||
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.signContactId } });
|
||||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.businessId } });
|
||||
}
|
||||
|
||||
/** 查看审批详情 */
|
||||
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContractPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【合同】合同管理、合同提醒"
|
||||
url="https://doc.iocoder.cn/crm/contract/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
v-model:model-value="sceneType"
|
||||
@tab-change="handleChangeSceneType"
|
||||
>
|
||||
<ElTabPane label="我负责的" name="1" />
|
||||
<ElTabPane label="我参与的" name="2" />
|
||||
<ElTabPane label="下属负责的" name="3" />
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['合同']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:contract:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:contract:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #businessName="{ row }">
|
||||
<ElButton type="primary" link @click="handleBusinessDetail(row)">
|
||||
{{ row.businessName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #signContactName="{ row }">
|
||||
<ElButton type="primary" link @click="handleContactDetail(row)">
|
||||
{{ row.signContactName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
ifShow: row.auditStatus === 0,
|
||||
},
|
||||
{
|
||||
label: '提交审核',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleSubmit.bind(null, row),
|
||||
ifShow: row.auditStatus === 0,
|
||||
},
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
ifShow: row.auditStatus !== 0,
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
auth: ['crm:contract:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
121
apps/web-ele/src/views/crm/contract/modules/form.vue
Normal file
121
apps/web-ele/src/views/crm/contract/modules/form.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
import { erpPriceMultiply } from '@vben/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createContract,
|
||||
getContract,
|
||||
updateContract,
|
||||
} from '#/api/crm/contract';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
import { ProductEditTable } from '#/views/crm/product/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmContractApi.Contract>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['合同'])
|
||||
: $t('ui.actionTitle.create', ['合同']);
|
||||
});
|
||||
|
||||
/** 更新产品列表 */
|
||||
function handleUpdateProducts(products: any) {
|
||||
formData.value = modalApi.getData<CrmContractApi.Contract>();
|
||||
formData.value!.products = products;
|
||||
if (formData.value) {
|
||||
const totalProductPrice =
|
||||
formData.value.products?.reduce(
|
||||
(prev, curr) => prev + curr.totalPrice,
|
||||
0,
|
||||
) ?? 0;
|
||||
const discountPercent = formData.value.discountPercent;
|
||||
const discountPrice =
|
||||
discountPercent === null
|
||||
? 0
|
||||
: erpPriceMultiply(totalProductPrice, discountPercent / 100);
|
||||
const totalPrice = totalProductPrice - (discountPrice ?? 0);
|
||||
formData.value!.totalProductPrice = totalProductPrice;
|
||||
formData.value!.totalPrice = totalPrice;
|
||||
formApi.setValues(formData.value!);
|
||||
}
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 120,
|
||||
},
|
||||
wrapperClass: 'grid-cols-3',
|
||||
layout: 'vertical',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmContractApi.Contract;
|
||||
data.products = formData.value?.products;
|
||||
try {
|
||||
await (formData.value?.id ? updateContract(data) : createContract(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmContractApi.Contract>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getContract(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/2">
|
||||
<Form class="mx-4">
|
||||
<template #product="slotProps">
|
||||
<ProductEditTable
|
||||
v-bind="slotProps"
|
||||
class="w-full"
|
||||
:products="formData?.products ?? []"
|
||||
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
||||
@update:products="handleUpdateProducts"
|
||||
/>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
396
apps/web-ele/src/views/crm/customer/data.ts
Normal file
396
apps/web-ele/src/views/crm/customer/data.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '客户名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'source',
|
||||
label: '客户来源',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
|
||||
placeholder: '请选择客户来源',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择负责人',
|
||||
allowClear: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电话',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'wechat',
|
||||
label: '微信',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入微信',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'qq',
|
||||
label: 'QQ',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入QQ',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'industryId',
|
||||
label: '客户行业',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
|
||||
placeholder: '请选择客户行业',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'level',
|
||||
label: '客户级别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
|
||||
placeholder: '请选择客户级别',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'areaId',
|
||||
label: '地址',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getAreaTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '请选择地址',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'detailAddress',
|
||||
label: '详细地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入详细地址',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下次联系时间',
|
||||
allowClear: true,
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '客户名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入电话',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
placeholder: ['开始日期', '结束日期'],
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 导入客户的表单 */
|
||||
export function useImportFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择负责人',
|
||||
allowClear: true,
|
||||
class: 'w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '客户数据',
|
||||
component: 'Upload',
|
||||
rules: 'required',
|
||||
help: '仅允许导入 xls、xlsx 格式文件',
|
||||
},
|
||||
{
|
||||
fieldName: 'updateSupport',
|
||||
label: '是否覆盖',
|
||||
component: 'Switch',
|
||||
componentProps: {
|
||||
activeValue: true,
|
||||
inactiveValue: false,
|
||||
},
|
||||
rules: z.boolean().default(false),
|
||||
help: '是否更新已经存在的客户数据',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
minWidth: 160,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
title: '客户来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 135,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'lockStatus',
|
||||
title: '锁定状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'dealStatus',
|
||||
title: '成交状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'contactLastContent',
|
||||
title: '最后跟进记录',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'poolDay',
|
||||
title: '距离进入公海天数',
|
||||
minWidth: 140,
|
||||
formatter: ({ cellValue }) =>
|
||||
cellValue === null ? '-' : `${cellValue} 天`,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
130
apps/web-ele/src/views/crm/customer/detail/data.ts
Normal file
130
apps/web-ele/src/views/crm/customer/detail/data.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 分配客户表单 */
|
||||
export function useDistributeFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
render: (val) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: val }),
|
||||
},
|
||||
{
|
||||
field: 'dealStatus',
|
||||
label: '成交状态',
|
||||
render: (val) => (val ? '已成交' : '未成交'),
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
label: '客户来源',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
render: (val, data) => {
|
||||
const areaName = val ?? '';
|
||||
const detailAddress = data?.detailAddress ?? '';
|
||||
return [areaName, detailAddress].filter(Boolean).join(' ');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
label: '客户行业',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
311
apps/web-ele/src/views/crm/customer/detail/index.vue
Normal file
311
apps/web-ele/src/views/crm/customer/detail/index.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
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 { OperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { $t } from '#/locales';
|
||||
import { BusinessDetailsList } from '#/views/crm/business/components';
|
||||
import { ContactDetailsList } from '#/views/crm/contact/components';
|
||||
import { ContractDetailsList } from '#/views/crm/contract/components';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
import { ReceivableDetailsList } from '#/views/crm/receivable/components';
|
||||
import { ReceivablePlanDetailsList } from '#/views/crm/receivable/plan/components';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailSchema } from './data';
|
||||
import DistributeForm from './modules/distribute-form.vue';
|
||||
import Info from './modules/info.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const customerId = ref(0); // 客户编号
|
||||
const customer = ref<CrmCustomerApi.Customer>({} as CrmCustomerApi.Customer); // 客户详情
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志
|
||||
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: TransferForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DistributeModal, distributeModalApi] = useVbenModal({
|
||||
connectedComponent: DistributeForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 加载客户详情 */
|
||||
async function loadCustomerDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
customer.value = await getCustomer(customerId.value);
|
||||
// 操作日志
|
||||
const res = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_CUSTOMER,
|
||||
bizId: customerId.value,
|
||||
});
|
||||
logList.value = res.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'CrmCustomer' });
|
||||
}
|
||||
|
||||
/** 编辑客户 */
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ id: customerId.value }).open();
|
||||
}
|
||||
|
||||
/** 转移线索 */
|
||||
function handleTransfer() {
|
||||
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 () => {
|
||||
// 锁定客户
|
||||
await lockCustomer(customerId.value, lockStatus);
|
||||
// 提示并返回成功
|
||||
ElMessage.success(lockStatus ? '锁定客户成功' : '解锁客户成功');
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 领取客户 */
|
||||
function handleReceive(): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定领取客户【${customer.value.name}】吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
// 领取客户
|
||||
await receiveCustomer([customerId.value]);
|
||||
// 提示并返回成功
|
||||
ElMessage.success('领取客户成功');
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 分配客户 */
|
||||
function handleDistributeForm() {
|
||||
distributeModalApi.setData({ id: customerId.value }).open();
|
||||
}
|
||||
|
||||
/** 客户放入公海 */
|
||||
function handlePutPool(): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm({
|
||||
content: `确定将客户【${customer.value.name}】放入公海吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
// 放入公海
|
||||
await putCustomerPool(customerId.value);
|
||||
// 提示并返回成功
|
||||
ElMessage.success('放入公海成功');
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新成交状态操作 */
|
||||
async function handleUpdateDealStatus(): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dealStatus = !customer.value.dealStatus;
|
||||
confirm({
|
||||
content: `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
// 更新成交状态
|
||||
await updateCustomerDealStatus(customerId.value, dealStatus);
|
||||
// 提示并返回成功
|
||||
ElMessage.success('更新成交状态成功');
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
onMounted(() => {
|
||||
customerId.value = Number(route.params.id);
|
||||
loadCustomerDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="customer?.name" :loading="loading">
|
||||
<FormModal @success="loadCustomerDetail" />
|
||||
<TransferModal @success="loadCustomerDetail" />
|
||||
<DistributeModal @success="loadCustomerDetail" />
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '返回',
|
||||
type: 'default',
|
||||
icon: 'lucide:arrow-left',
|
||||
onClick: handleBack,
|
||||
},
|
||||
{
|
||||
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>
|
||||
<ElCard class="min-h-[10%]">
|
||||
<Descriptions :data="customer" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="跟进记录" name="1">
|
||||
<FollowUp :biz-id="customerId" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="基本信息" name="2">
|
||||
<Info :customer="customer" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="联系人" name="3">
|
||||
<ContactDetailsList
|
||||
:biz-id="customerId"
|
||||
:biz-type="BizTypeEnum.CRM_CUSTOMER"
|
||||
:customer-id="customerId"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="团队成员" name="4">
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="customerId"
|
||||
:biz-type="BizTypeEnum.CRM_CUSTOMER"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="商机" name="5">
|
||||
<BusinessDetailsList
|
||||
:biz-id="customerId"
|
||||
:biz-type="BizTypeEnum.CRM_CUSTOMER"
|
||||
:customer-id="customerId"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="合同" name="6">
|
||||
<ContractDetailsList
|
||||
:biz-id="customerId"
|
||||
:biz-type="BizTypeEnum.CRM_CUSTOMER"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="回款" name="7">
|
||||
<ReceivablePlanDetailsList :customer-id="customerId" />
|
||||
<ReceivableDetailsList :customer-id="customerId" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="操作日志" name="8">
|
||||
<OperateLog :log-list="logList" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { distributeCustomer } from '#/api/crm/customer';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDistributeFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useDistributeFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await distributeCustomer([data.id], data.ownerUserId);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await formApi.setValues(data);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="分配客户" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
36
apps/web-ele/src/views/crm/customer/detail/modules/info.vue
Normal file
36
apps/web-ele/src/views/crm/customer/detail/modules/info.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { ElDivider } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
customer: CrmCustomerApi.Customer; // 客户信息
|
||||
}>();
|
||||
|
||||
const [BaseDescriptions] = useDescription({
|
||||
title: '基本信息',
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
|
||||
const [SystemDescriptions] = useDescription({
|
||||
title: '系统信息',
|
||||
border: false,
|
||||
column: 3,
|
||||
schema: useFollowUpDetailSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseDescriptions :data="customer" />
|
||||
<ElDivider />
|
||||
<SystemDescriptions :data="customer" />
|
||||
</div>
|
||||
</template>
|
||||
217
apps/web-ele/src/views/crm/customer/index.vue
Normal file
217
apps/web-ele/src/views/crm/customer/index.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElLoading,
|
||||
ElMessage,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCustomer,
|
||||
exportCustomer,
|
||||
getCustomerPage,
|
||||
} from '#/api/crm/customer';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
import ImportForm from './modules/import-form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [ImportModal, importModalApi] = useVbenModal({
|
||||
connectedComponent: ImportForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理场景类型的切换 */
|
||||
function handleChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导入客户 */
|
||||
function handleImport() {
|
||||
importModalApi.open();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const formValues = await gridApi.formApi.getValues();
|
||||
const data = await exportCustomer({
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
downloadFileFromBlobPart({ fileName: '客户.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建客户 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑客户 */
|
||||
function handleEdit(row: CrmCustomerApi.Customer) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除客户 */
|
||||
async function handleDelete(row: CrmCustomerApi.Customer) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteCustomer(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【客户】客户管理"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<ImportModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
@tab-change="handleChangeSceneType"
|
||||
v-model:model-value="sceneType"
|
||||
>
|
||||
<ElTabPane label="我负责的" name="1" />
|
||||
<ElTabPane label="我参与的" name="2" />
|
||||
<ElTabPane label="下属负责的" name="3" />
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['客户']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:customer:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.import'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.UPLOAD,
|
||||
auth: ['crm:customer:import'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:customer:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:customer:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:customer:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
156
apps/web-ele/src/views/crm/customer/limitConfig/data.ts
Normal file
156
apps/web-ele/src/views/crm/customer/limitConfig/data.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { LimitConfType } from '#/api/crm/customer/limitConfig';
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userIds',
|
||||
label: '规则适用人群',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
mode: 'multiple',
|
||||
allowClear: true,
|
||||
placeholder: '请选择规则适用人群',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'deptIds',
|
||||
label: '规则适用部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
multiple: true,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择规则适用部门',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'maxCount',
|
||||
label:
|
||||
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT
|
||||
? '拥有客户数上限'
|
||||
: '锁定客户数上限',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: `请输入${
|
||||
LimitConfType.CUSTOMER_QUANTITY_LIMIT === confType
|
||||
? '拥有客户数上限'
|
||||
: '锁定客户数上限'
|
||||
}`,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'dealCountEnabled',
|
||||
label: '成交客户是否占用拥有客户数',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
confType: LimitConfType,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'users',
|
||||
title: '规则适用人群',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue
|
||||
.map((user: any) => {
|
||||
return user.nickname;
|
||||
})
|
||||
.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'depts',
|
||||
title: '规则适用部门',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue
|
||||
.map((dept: any) => {
|
||||
return dept.name;
|
||||
})
|
||||
.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'maxCount',
|
||||
title:
|
||||
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT
|
||||
? '拥有客户数上限'
|
||||
: '锁定客户数上限',
|
||||
},
|
||||
{
|
||||
field: 'dealCountEnabled',
|
||||
title: '成交客户是否占用拥有客户数',
|
||||
visible: confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
172
apps/web-ele/src/views/crm/customer/limitConfig/index.vue
Normal file
172
apps/web-ele/src/views/crm/customer/limitConfig/index.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerLimitConfigApi } from '#/api/crm/customer/limitConfig';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCustomerLimitConfig,
|
||||
getCustomerLimitConfigPage,
|
||||
LimitConfType,
|
||||
} from '#/api/crm/customer/limitConfig';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const configType = ref(LimitConfType.CUSTOMER_QUANTITY_LIMIT);
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理配置类型的切换 */
|
||||
function handleChangeConfigType(key: number | string) {
|
||||
configType.value = key as LimitConfType;
|
||||
gridApi.setGridOptions({
|
||||
columns: useGridColumns(configType.value),
|
||||
});
|
||||
handleRefresh();
|
||||
}
|
||||
|
||||
/** 创建规则 */
|
||||
function handleCreate(type: LimitConfType) {
|
||||
formModalApi.setData({ type }).open();
|
||||
}
|
||||
|
||||
/** 编辑规则 */
|
||||
function handleEdit(
|
||||
row: CrmCustomerLimitConfigApi.CustomerLimitConfig,
|
||||
type: LimitConfType,
|
||||
) {
|
||||
formModalApi.setData({ id: row.id, type }).open();
|
||||
}
|
||||
|
||||
/** 删除规则 */
|
||||
async function handleDelete(
|
||||
row: CrmCustomerLimitConfigApi.CustomerLimitConfig,
|
||||
) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.id]),
|
||||
});
|
||||
try {
|
||||
await deleteCustomerLimitConfig(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(configType.value),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerLimitConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: configType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerLimitConfigApi.CustomerLimitConfig>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
@tab-change="handleChangeConfigType"
|
||||
v-model:model-value="configType"
|
||||
>
|
||||
<ElTabPane
|
||||
label="拥有客户数限制"
|
||||
:name="LimitConfType.CUSTOMER_QUANTITY_LIMIT"
|
||||
/>
|
||||
<ElTabPane
|
||||
label="锁定客户数限制"
|
||||
:name="LimitConfType.CUSTOMER_LOCK_LIMIT"
|
||||
/>
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['规则']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:customer-limit-config:create'],
|
||||
onClick: handleCreate.bind(null, configType),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:customer-limit-config:update'],
|
||||
onClick: handleEdit.bind(null, row, configType),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:customer-limit-config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
100
apps/web-ele/src/views/crm/customer/limitConfig/modules/form.vue
Normal file
100
apps/web-ele/src/views/crm/customer/limitConfig/modules/form.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmCustomerLimitConfigApi } from '#/api/crm/customer/limitConfig';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createCustomerLimitConfig,
|
||||
getCustomerLimitConfig,
|
||||
LimitConfType,
|
||||
updateCustomerLimitConfig,
|
||||
} from '#/api/crm/customer/limitConfig';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmCustomerLimitConfigApi.CustomerLimitConfig>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['规则'])
|
||||
: $t('ui.actionTitle.create', ['规则']);
|
||||
});
|
||||
|
||||
const confType = ref<LimitConfType>(LimitConfType.CUSTOMER_LOCK_LIMIT);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 200,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(confType.value),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as CrmCustomerLimitConfigApi.CustomerLimitConfig;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateCustomerLimitConfig(data)
|
||||
: createCustomerLimitConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data =
|
||||
modalApi.getData<CrmCustomerLimitConfigApi.CustomerLimitConfig>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.type) {
|
||||
confType.value = data.type as LimitConfType;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(confType.value) });
|
||||
modalApi.lock();
|
||||
try {
|
||||
if (data.id) {
|
||||
data = await getCustomerLimitConfig(data.id);
|
||||
}
|
||||
formData.value = data;
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
85
apps/web-ele/src/views/crm/customer/modules/form.vue
Normal file
85
apps/web-ele/src/views/crm/customer/modules/form.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createCustomer,
|
||||
getCustomer,
|
||||
updateCustomer,
|
||||
} from '#/api/crm/customer';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmCustomerApi.Customer>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['客户'])
|
||||
: $t('ui.actionTitle.create', ['客户']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmCustomerApi.Customer;
|
||||
try {
|
||||
await (formData.value?.id ? updateCustomer(data) : createCustomer(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmCustomerApi.Customer>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getCustomer(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
86
apps/web-ele/src/views/crm/customer/modules/import-form.vue
Normal file
86
apps/web-ele/src/views/crm/customer/modules/import-form.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { importCustomer, importCustomerTemplate } from '#/api/crm/customer';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useImportFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await importCustomer({
|
||||
ownerUserId: data.ownerUserId,
|
||||
file: data.file,
|
||||
updateSupport: data.updateSupport,
|
||||
});
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 文件改变时 */
|
||||
function handleChange(file: any) {
|
||||
if (file.raw) {
|
||||
formApi.setFieldValue('file', file.raw);
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载模版 */
|
||||
async function handleDownload() {
|
||||
const data = await importCustomerTemplate();
|
||||
downloadFileFromBlobPart({ fileName: '客户导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="客户导入" class="w-1/3">
|
||||
<Form class="mx-4">
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<ElUpload
|
||||
:limit="1"
|
||||
accept=".xls,.xlsx"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
161
apps/web-ele/src/views/crm/customer/pool/data.ts
Normal file
161
apps/web-ele/src/views/crm/customer/pool/data.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '客户名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'industryId',
|
||||
label: '所属行业',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
|
||||
placeholder: '请选择所属行业',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'level',
|
||||
label: '客户级别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
|
||||
placeholder: '请选择客户级别',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'source',
|
||||
label: '客户来源',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
|
||||
placeholder: '请选择客户来源',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'name',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '客户来源',
|
||||
field: 'source',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '手机',
|
||||
field: 'mobile',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
field: 'telephone',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
field: 'email',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
title: '客户级别',
|
||||
field: 'level',
|
||||
minWidth: 135,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '客户行业',
|
||||
field: 'industryId',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '下次联系时间',
|
||||
field: 'contactNextTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '成交状态',
|
||||
field: 'dealStatus',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后跟进时间',
|
||||
field: 'contactLastTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '最后跟进记录',
|
||||
field: 'contactLastContent',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
field: 'updateTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
field: 'creatorName',
|
||||
minWidth: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
97
apps/web-ele/src/views/crm/customer/pool/index.vue
Normal file
97
apps/web-ele/src/views/crm/customer/pool/index.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { exportCustomer, getCustomerPage } from '#/api/crm/customer';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportCustomer(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '客户公海.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
pool: true,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:customer:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
83
apps/web-ele/src/views/crm/customer/poolConfig/data.ts
Normal file
83
apps/web-ele/src/views/crm/customer/poolConfig/data.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
export const schema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'enabled',
|
||||
label: '客户公海规则设置',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '开启', value: true },
|
||||
{ label: '关闭', value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'contactExpireDays',
|
||||
componentProps: {
|
||||
placeholder: '请输入天数',
|
||||
class: '!w-full',
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
append: () => '天不跟进或',
|
||||
}),
|
||||
rules: z.coerce.number().int().min(0, '天数不能小于 0'),
|
||||
dependencies: {
|
||||
triggerFields: ['enabled'],
|
||||
show: (value) => value.enabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'dealExpireDays',
|
||||
componentProps: {
|
||||
placeholder: '请输入天数',
|
||||
class: '!w-full',
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
prepend: () => '或',
|
||||
append: () => '天未成交',
|
||||
}),
|
||||
rules: z.coerce.number().int().min(0, '天数不能小于 0'),
|
||||
dependencies: {
|
||||
triggerFields: ['enabled'],
|
||||
show: (value) => value.enabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'notifyEnabled',
|
||||
label: '提前提醒设置',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '开启', value: true },
|
||||
{ label: '关闭', value: false },
|
||||
],
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['enabled'],
|
||||
show: (value) => value.enabled,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'notifyDays',
|
||||
componentProps: {
|
||||
placeholder: '请输入天数',
|
||||
class: '!w-full',
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
prepend: () => '提前',
|
||||
append: () => '天提醒',
|
||||
}),
|
||||
rules: z.coerce.number().int().min(0, '天数不能小于 0'),
|
||||
dependencies: {
|
||||
triggerFields: ['notifyEnabled'],
|
||||
show: (value) => value.enabled && value.notifyEnabled,
|
||||
},
|
||||
},
|
||||
];
|
||||
69
apps/web-ele/src/views/crm/customer/poolConfig/index.vue
Normal file
69
apps/web-ele/src/views/crm/customer/poolConfig/index.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmCustomerPoolConfigApi } from '#/api/crm/customer/poolConfig';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ElCard, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
getCustomerPoolConfig,
|
||||
saveCustomerPoolConfig,
|
||||
} from '#/api/crm/customer/poolConfig';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { schema } from './data';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelClass: 'w-100',
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema,
|
||||
handleSubmit,
|
||||
});
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as CrmCustomerPoolConfigApi.CustomerPoolConfig;
|
||||
if (!data.enabled) {
|
||||
data.contactExpireDays = undefined;
|
||||
data.dealExpireDays = undefined;
|
||||
data.notifyEnabled = false;
|
||||
}
|
||||
if (!data.notifyEnabled) {
|
||||
data.notifyDays = undefined;
|
||||
}
|
||||
await saveCustomerPoolConfig(data);
|
||||
// 关闭并提示
|
||||
await formApi.setValues(data);
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
}
|
||||
|
||||
/** 获取配置 */
|
||||
async function getConfigInfo() {
|
||||
const res = await getCustomerPoolConfig();
|
||||
await formApi.setValues(res);
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getConfigInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElCard title="客户公海规则设置">
|
||||
<Form class="w-1/4" />
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -50,6 +50,7 @@ export function useFormSchema(
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下次联系时间',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
|
||||
@@ -31,7 +31,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
} as CrmProductCategoryApi.ProductCategory);
|
||||
return handleTree(data);
|
||||
},
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级分类',
|
||||
showSearch: true,
|
||||
treeDefaultExpandAll: true,
|
||||
@@ -85,7 +87,7 @@ export function useGridColumns(): VxeTableGridOptions<CrmProductCategoryApi.Prod
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
width: 250,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: 'actions',
|
||||
|
||||
@@ -68,6 +68,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
let data = modalApi.getData<CrmProductCategoryApi.ProductCategory>();
|
||||
if (!data || !data.id) {
|
||||
// 设置上级
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
|
||||
@@ -126,10 +126,8 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 产品下拉选项 */
|
||||
const productOptions = ref<CrmProductApi.Product[]>([]);
|
||||
|
||||
/** 初始化 */
|
||||
const productOptions = ref<CrmProductApi.Product[]>([]); // 产品下拉选项
|
||||
onMounted(async () => {
|
||||
productOptions.value = await getProductSimpleList();
|
||||
});
|
||||
|
||||
@@ -27,11 +27,11 @@ const loading = ref(false); // 加载中
|
||||
const productId = ref(0); // 产品编号
|
||||
const product = ref<CrmProductApi.Product>({} as CrmProductApi.Product); // 产品详情
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
border: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ onMounted(() => {
|
||||
<Descriptions :data="product" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs>
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="详细资料" name="1">
|
||||
<Info :product="product" />
|
||||
</ElTabPane>
|
||||
|
||||
@@ -6,20 +6,18 @@ import { useDescription } from '#/components/description';
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
product: CrmProductApi.Product; // 产品信息
|
||||
product: CrmProductApi.Product;
|
||||
}>();
|
||||
|
||||
const [ProductDescriptions] = useDescription({
|
||||
title: '基本信息',
|
||||
bordered: false,
|
||||
border: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<ProductDescriptions :data="product" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
73
apps/web-ele/src/views/crm/receivable/components/data.ts
Normal file
73
apps/web-ele/src/views/crm/receivable/components/data.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '回款编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'contract.no',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款日期',
|
||||
field: 'returnTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '回款金额(元)',
|
||||
field: 'price',
|
||||
minWidth: 150,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '回款方式',
|
||||
field: 'returnType',
|
||||
minWidth: 150,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款状态',
|
||||
field: 'auditStatus',
|
||||
minWidth: 100,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
144
apps/web-ele/src/views/crm/receivable/components/detail-list.vue
Normal file
144
apps/web-ele/src/views/crm/receivable/components/detail-list.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<!-- 回款列表:用于【客户】【合同】详情中,展示它们关联的回款列表 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteReceivable,
|
||||
getReceivablePageByCustomer,
|
||||
} from '#/api/crm/receivable';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Form from '../modules/form.vue';
|
||||
import { useDetailListColumns } from './data';
|
||||
|
||||
const props = defineProps<{
|
||||
contractId?: number; // 合同编号
|
||||
customerId?: number; // 客户编号
|
||||
}>();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建回款 */
|
||||
function handleCreate() {
|
||||
formModalApi
|
||||
.setData({
|
||||
contractId: props.contractId,
|
||||
customerId: props.customerId,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
/** 编辑回款 */
|
||||
function handleEdit(row: CrmReceivableApi.Receivable) {
|
||||
formModalApi.setData({ receivable: row }).open();
|
||||
}
|
||||
|
||||
/** 删除回款 */
|
||||
async function handleDelete(row: CrmReceivableApi.Receivable) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.no]),
|
||||
});
|
||||
try {
|
||||
await deleteReceivable(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDetailListColumns(),
|
||||
height: 500,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
const queryParams: CrmReceivableApi.ReceivablePageParam = {
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
};
|
||||
if (props.customerId && !props.contractId) {
|
||||
queryParams.customerId = props.customerId;
|
||||
} else if (props.customerId && props.contractId) {
|
||||
// 如果是合同的话客户编号也需要带上因为权限基于客户
|
||||
queryParams.customerId = props.customerId;
|
||||
queryParams.contractId = props.contractId;
|
||||
}
|
||||
return await getReceivablePageByCustomer(queryParams);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivableApi.Receivable>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['回款']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:receivable:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:receivable:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
ifShow: row.auditStatus === 0,
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:receivable:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ReceivableDetailsList } from './detail-list.vue';
|
||||
301
apps/web-ele/src/views/crm/receivable/data.ts
Normal file
301
apps/web-ele/src/views/crm/receivable/data.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getContractSimpleList } from '#/api/crm/contract';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import {
|
||||
getReceivablePlan,
|
||||
getReceivablePlanSimpleList,
|
||||
} from '#/api/crm/receivable/plan';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
const userStore = useUserStore();
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '回款编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '保存时自动生成',
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择负责人',
|
||||
allowClear: true,
|
||||
},
|
||||
defaultValue: userStore.userInfo?.id,
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户名称',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => values.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'contractId',
|
||||
label: '合同名称',
|
||||
component: 'Select',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['customerId'],
|
||||
disabled: (values) => !values.customerId || values.id,
|
||||
async componentProps(values) {
|
||||
if (values.customerId) {
|
||||
if (!values.id) {
|
||||
// 特殊:只有在【新增】时,才清空合同编号
|
||||
values.contractId = undefined;
|
||||
}
|
||||
const contracts = await getContractSimpleList(values.customerId);
|
||||
return {
|
||||
options: contracts.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
placeholder: '请选择合同',
|
||||
} as any;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'planId',
|
||||
label: '回款期数',
|
||||
component: 'Select',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['contractId'],
|
||||
disabled: (values) => !values.contractId,
|
||||
async componentProps(values) {
|
||||
if (values.contractId) {
|
||||
values.planId = undefined;
|
||||
const plans = await getReceivablePlanSimpleList(
|
||||
values.customerId,
|
||||
values.contractId,
|
||||
);
|
||||
return {
|
||||
options: plans.map((item) => ({
|
||||
label: item.period,
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'returnType',
|
||||
label: '回款方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE, 'number'),
|
||||
placeholder: '请选择回款方式',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'price',
|
||||
label: '回款金额',
|
||||
component: 'InputNumber',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入回款金额',
|
||||
min: 0,
|
||||
precision: 2,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'returnTime',
|
||||
label: '回款日期',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择回款日期',
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
class: '!w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '回款编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入回款编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '回款编号',
|
||||
field: 'no',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: { default: 'no' },
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'contract',
|
||||
minWidth: 160,
|
||||
slots: { default: 'contractNo' },
|
||||
},
|
||||
{
|
||||
title: '回款日期',
|
||||
field: 'returnTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '回款金额(元)',
|
||||
field: 'price',
|
||||
minWidth: 150,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '回款方式',
|
||||
field: 'returnType',
|
||||
minWidth: 150,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'contract.totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
field: 'ownerUserDeptName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
field: 'updateTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
field: 'creatorName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款状态',
|
||||
field: 'auditStatus',
|
||||
minWidth: 100,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
minWidth: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
105
apps/web-ele/src/views/crm/receivable/detail/data.ts
Normal file
105
apps/web-ele/src/views/crm/receivable/detail/data.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
render: (val, data) =>
|
||||
erpPriceInputFormatter(val ?? data?.contract?.totalPrice ?? 0),
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额(元)',
|
||||
render: (val) => erpPriceInputFormatter(val),
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '回款编号',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'contract',
|
||||
label: '合同编号',
|
||||
render: (val, data) =>
|
||||
val && data?.contract?.no ? data?.contract?.no : '',
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
render: (val) => erpPriceInputFormatter(val),
|
||||
},
|
||||
{
|
||||
field: 'returnType',
|
||||
label: '回款方式',
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 系统信息字段 */
|
||||
export function useDetailSystemSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
label: '创建人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
132
apps/web-ele/src/views/crm/receivable/detail/index.vue
Normal file
132
apps/web-ele/src/views/crm/receivable/detail/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { getReceivable } from '#/api/crm/receivable';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { OperateLog } from '#/components/operate-log';
|
||||
import { ACTION_ICON, TableAction } from '#/components/table-action';
|
||||
import { $t } from '#/locales';
|
||||
import { PermissionList } from '#/views/crm/permission';
|
||||
|
||||
import ReceivableForm from '../modules/form.vue';
|
||||
import { useDetailSchema } from './data';
|
||||
import Info from './modules/info.vue';
|
||||
|
||||
const props = defineProps<{ id?: number }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const receivableId = ref(0); // 回款编号
|
||||
const receivable = ref<CrmReceivableApi.Receivable>(
|
||||
{} as CrmReceivableApi.Receivable,
|
||||
); // 回款详情
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志
|
||||
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队成员列表 Ref
|
||||
const activeTabName = ref('1'); // 选中 Tab 名
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ReceivableForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 加载回款详情 */
|
||||
async function loadReceivableDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
receivable.value = await getReceivable(receivableId.value);
|
||||
// 操作日志
|
||||
const res = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_RECEIVABLE,
|
||||
bizId: receivableId.value,
|
||||
});
|
||||
logList.value = res.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'CrmReceivable' });
|
||||
}
|
||||
|
||||
/** 编辑收款 */
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ receivable: { id: receivableId.value } }).open();
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
onMounted(() => {
|
||||
receivableId.value = Number(props.id || route.params.id);
|
||||
loadReceivableDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height :title="receivable?.no" :loading="loading">
|
||||
<FormModal @success="loadReceivableDetail" />
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '返回',
|
||||
type: 'default',
|
||||
icon: 'lucide:arrow-left',
|
||||
onClick: handleBack,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.edit'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:receivable:update'],
|
||||
ifShow: permissionListRef?.validateWrite,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<ElCard class="min-h-[10%]">
|
||||
<Descriptions :data="receivable" />
|
||||
</ElCard>
|
||||
<ElCard class="mt-4 min-h-[60%]">
|
||||
<ElTabs v-model:model-value="activeTabName">
|
||||
<ElTabPane label="详细资料" name="1" :lazy="false">
|
||||
<Info :receivable="receivable" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="操作日志" name="2" :lazy="false">
|
||||
<OperateLog :log-list="logList" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="团队成员" name="3" :lazy="false">
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="receivableId"
|
||||
:biz-type="BizTypeEnum.CRM_RECEIVABLE"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { ElDivider } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailBaseSchema, useDetailSystemSchema } from '../data';
|
||||
|
||||
defineProps<{
|
||||
receivable: CrmReceivableApi.Receivable; // 收款信息
|
||||
}>();
|
||||
|
||||
const [BaseDescriptions] = useDescription({
|
||||
title: '基本信息',
|
||||
border: false,
|
||||
column: 4,
|
||||
schema: useDetailBaseSchema(),
|
||||
});
|
||||
|
||||
const [SystemDescriptions] = useDescription({
|
||||
title: '系统信息',
|
||||
border: false,
|
||||
column: 3,
|
||||
schema: useDetailSystemSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseDescriptions :data="receivable" />
|
||||
<ElDivider />
|
||||
<SystemDescriptions :data="receivable" />
|
||||
</div>
|
||||
</template>
|
||||
263
apps/web-ele/src/views/crm/receivable/index.vue
Normal file
263
apps/web-ele/src/views/crm/receivable/index.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElLoading,
|
||||
ElMessage,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteReceivable,
|
||||
exportReceivable,
|
||||
getReceivablePage,
|
||||
submitReceivable,
|
||||
} from '#/api/crm/receivable';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理场景类型的切换 */
|
||||
function handleChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const formValues = await gridApi.formApi.getValues();
|
||||
const data = await exportReceivable({
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
downloadFileFromBlobPart({ fileName: '回款.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建回款 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑回款 */
|
||||
function handleEdit(row: CrmReceivableApi.Receivable) {
|
||||
formModalApi.setData({ receivable: row }).open();
|
||||
}
|
||||
|
||||
/** 删除回款 */
|
||||
async function handleDelete(row: CrmReceivableApi.Receivable) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.no]),
|
||||
});
|
||||
try {
|
||||
await deleteReceivable(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交审核 */
|
||||
async function handleSubmit(row: CrmReceivableApi.Receivable) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '提交审核中...',
|
||||
});
|
||||
try {
|
||||
await submitReceivable(row.id!);
|
||||
ElMessage.success('提交审核成功');
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看回款详情 */
|
||||
function handleDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 查看合同详情 */
|
||||
function handleContractDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
|
||||
}
|
||||
|
||||
/** 查看审批详情 */
|
||||
function handleProcessDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivableApi.Receivable>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【回款】回款管理、回款计划"
|
||||
url="https://doc.iocoder.cn/crm/receivable/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<ElTabs
|
||||
class="w-full"
|
||||
@tab-change="handleChangeSceneType"
|
||||
v-model:model-value="sceneType"
|
||||
>
|
||||
<ElTabPane label="我负责的" name="1" />
|
||||
<ElTabPane label="我参与的" name="2" />
|
||||
<ElTabPane label="下属负责的" name="3" />
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['回款']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:receivable:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:receivable:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #no="{ row }">
|
||||
<ElButton type="primary" link @click="handleDetail(row)">
|
||||
{{ row.no }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #contractNo="{ row }">
|
||||
<ElButton
|
||||
v-if="row.contract"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleContractDetail(row)"
|
||||
>
|
||||
{{ row.contract.no }}
|
||||
</ElButton>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:receivable:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '提交审核',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['crm:receivable:update'],
|
||||
onClick: handleSubmit.bind(null, row),
|
||||
ifShow: row.auditStatus === 0,
|
||||
},
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['crm:receivable:update'],
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
ifShow: row.auditStatus !== 0,
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:receivable:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
102
apps/web-ele/src/views/crm/receivable/modules/form.vue
Normal file
102
apps/web-ele/src/views/crm/receivable/modules/form.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createReceivable,
|
||||
getReceivable,
|
||||
updateReceivable,
|
||||
} from '#/api/crm/receivable';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmReceivableApi.Receivable>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['回款'])
|
||||
: $t('ui.actionTitle.create', ['回款']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmReceivableApi.Receivable;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateReceivable(data)
|
||||
: createReceivable(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { receivable, plan } = data;
|
||||
modalApi.lock();
|
||||
try {
|
||||
if (receivable) {
|
||||
formData.value = await getReceivable(receivable.id!);
|
||||
} else if (plan) {
|
||||
formData.value = plan.id
|
||||
? {
|
||||
planId: plan.id,
|
||||
price: plan.price,
|
||||
returnType: plan.returnType,
|
||||
customerId: plan.customerId,
|
||||
contractId: plan.contractId,
|
||||
}
|
||||
: ({
|
||||
customerId: plan.customerId,
|
||||
contractId: plan.contractId,
|
||||
} as any);
|
||||
}
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value as any);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'contractNo',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '期数',
|
||||
field: 'period',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '计划回款(元)',
|
||||
field: 'price',
|
||||
minWidth: 150,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
title: '计划回款日期',
|
||||
field: 'returnTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '提前几天提醒',
|
||||
field: 'remindDays',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '提醒日期',
|
||||
field: 'remindTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user