fix: merge 解决冲突

This commit is contained in:
jason
2025-06-04 13:22:33 +08:00
62 changed files with 1737 additions and 433 deletions

View File

@@ -42,4 +42,3 @@ defineExpose({ validateConfig });
/>
</ContentWrap>
</template>
<style lang="scss" scoped></style>

View File

@@ -1389,4 +1389,3 @@ defineExpose({ loadTodoTask });
<!-- 签名弹窗 -->
<Signature ref="signRef" @success="handleSignFinish" />
</template>
<style lang="scss" scoped></style>

View File

@@ -1,7 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>
<p>待完成</p>
</div>
<div>businessInfo</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>businessList</div>
</template>

View File

@@ -5,16 +5,18 @@ import { defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { ArrowLeft } from '@vben/icons';
import { Button, Card, Modal, Tabs } from 'ant-design-vue';
import { getClue, transformClue } from '#/api/crm/clue';
import { BizTypeEnum } from '#/api/crm/permission';
import { useDescription } from '#/components/description';
import { PermissionList, TransferForm } from '#/views/crm/permission';
import { useDetailSchema } from '../data';
import ClueForm from './form.vue';
import TransferForm from './transfer.vue';
const ClueDetailsInfo = defineAsyncComponent(() => import('./detail-info.vue'));
@@ -22,6 +24,7 @@ const loading = ref(false);
const route = useRoute();
const router = useRouter();
const tabs = useTabs();
const clueId = ref(0);
@@ -58,6 +61,7 @@ async function loadClueDetail() {
/** 返回列表页 */
function handleBack() {
tabs.closeCurrentTab();
router.push('/crm/clue');
}
@@ -68,7 +72,7 @@ function handleEdit() {
/** 转移线索 */
function handleTransfer() {
transferModalApi.setData({ id: clueId }).open();
transferModalApi.setData({ bizType: BizTypeEnum.CRM_CLUE }).open();
}
/** 转化为客户 */
@@ -141,7 +145,13 @@ onMounted(async () => {
<ClueDetailsInfo :clue="clue" />
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="3">
<div>团队成员</div>
<PermissionList
ref="permissionListRef"
:biz-id="clue.id!"
:biz-type="BizTypeEnum.CRM_CLUE"
:show-action="true"
@quit-team="handleBack"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="4">
<div>操作日志</div>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>contactInfo</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>contactList</div>
</template>

View File

@@ -146,73 +146,73 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '合同编号',
field: 'no',
width: 150,
minWidth: 150,
fixed: 'left',
},
{
title: '合同名称',
field: 'name',
width: 150,
minWidth: 150,
fixed: 'left',
slots: { default: 'name' },
},
{
title: '客户名称',
field: 'customerName',
width: 150,
minWidth: 150,
slots: { default: 'customerName' },
},
{
title: '商机名称',
field: 'businessName',
width: 150,
minWidth: 150,
slots: { default: 'businessName' },
},
{
title: '合同金额(元)',
field: 'totalPrice',
width: 150,
minWidth: 150,
formatter: 'formatNumber',
},
{
title: '下单时间',
field: 'orderDate',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '合同开始时间',
field: 'startTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '合同结束时间',
field: 'endTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '客户签约人',
field: 'signContactName',
width: 150,
minWidth: 150,
slots: { default: 'signContactName' },
},
{
title: '公司签约人',
field: 'signUserName',
width: 150,
minWidth: 150,
},
{
title: '已回款金额(元)',
field: 'totalReceivablePrice',
width: 150,
minWidth: 150,
formatter: 'formatNumber',
},
{
title: '未回款金额(元)',
field: 'unpaidPrice',
width: 150,
minWidth: 150,
formatter: ({ row }) => {
return floatToFixed2(row.totalPrice - row.totalReceivablePrice);
},
@@ -220,46 +220,46 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '最后跟进时间',
field: 'contactLastTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '负责人',
field: 'ownerUserName',
width: 150,
minWidth: 150,
},
{
title: '所属部门',
field: 'ownerUserDeptName',
width: 150,
minWidth: 150,
},
{
title: '更新时间',
field: 'updateTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '创建时间',
field: 'createTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '创建人',
field: 'creatorName',
width: 150,
minWidth: 150,
},
{
title: '备注',
field: 'remark',
width: 150,
minWidth: 150,
},
{
title: '合同状态',
field: 'auditStatus',
fixed: 'right',
width: 100,
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>contractInfo</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>contractList</div>
</template>

View File

@@ -1,7 +1,5 @@
<script lang="ts" setup></script>
<template>
<div>
<p>待完成</p>
</div>
<div>detail-info</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>customerList</div>
</template>

View File

@@ -1,7 +1,209 @@
<script lang="ts" setup></script>
<script setup lang="ts">
import type { CrmCustomerApi } from '#/api/crm/customer';
import type { SystemOperateLogApi } from '#/api/system/operate-log';
import { defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { Button, Card, Modal, Tabs } from 'ant-design-vue';
import { getCustomer, 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 { useDetailSchema } from '../data';
const CustomerDetailsInfo = defineAsyncComponent(
() => import('./detail-info.vue'),
);
const loading = ref(false);
const route = useRoute();
const router = useRouter();
const customerId = ref(0);
const customer = ref<CrmCustomerApi.Customer>({} as CrmCustomerApi.Customer);
const permissionListRef = ref(); // 团队成员列表 Ref
const [Description] = useDescription({
componentProps: {
bordered: false,
column: 4,
class: 'mx-4',
},
schema: useDetailSchema(),
});
/** 加载详情 */
async function loadCustomerDetail() {
loading.value = true;
customerId.value = Number(route.params.id);
const data = await getCustomer(customerId.value);
await getOperateLog();
customer.value = data;
loading.value = false;
}
/** 编辑 */
function handleEdit() {
// formModalApi.setData({ id: clueId }).open();
}
/** 转移线索 */
function handleTransfer() {
// transferModalApi.setData({ id: clueId }).open();
}
/** 锁定客户 */
function handleLock() {
// transferModalApi.setData({ id: clueId }).open();
}
/** 解锁客户 */
function handleUnlock() {
// transferModalApi.setData({ id: clueId }).open();
}
/** 领取客户 */
function handleReceive() {
// transferModalApi.setData({ id: clueId }).open();
}
/** 分配客户 */
function handleDistributeForm() {
// transferModalApi.setData({ id: clueId }).open();
}
/** 客户放入公海 */
function handlePutPool() {
// transferModalApi.setData({ id: clueId }).open();
}
/** 更新成交状态操作 */
async function handleUpdateDealStatus() {
const dealStatus = !customer.value.dealStatus;
try {
await Modal.confirm({
title: '提示',
content: `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`,
});
await updateCustomerDealStatus(customerId.value, dealStatus);
Modal.success({
title: '成功',
content: '更新成交状态成功',
});
await loadCustomerDetail();
} catch {
// 用户取消操作
}
}
/** 获取操作日志 */
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志列表
async function getOperateLog() {
if (!customerId.value) {
return;
}
const data = await getOperateLogPage({
bizType: BizTypeEnum.CRM_CUSTOMER,
bizId: customerId.value,
});
logList.value = data.list;
}
// 加载数据
onMounted(async () => {
await loadCustomerDetail();
});
</script>
<template>
<div>
<p>待完成</p>
</div>
<Page auto-content-height :title="customer?.name" :loading="loading">
<template #extra>
<div class="flex items-center gap-2">
<Button
v-if="permissionListRef?.validateWrite"
type="primary"
@click="handleEdit"
v-access:code="['crm:customer:update']"
>
{{ $t('ui.actionTitle.edit') }}
</Button>
<Button
v-if="permissionListRef?.validateOwnerUser"
type="primary"
@click="handleTransfer"
>
转移
</Button>
<Button
v-if="permissionListRef?.validateWrite"
@click="handleUpdateDealStatus"
>
更改成交状态
</Button>
<Button
v-if="customer.lockStatus && permissionListRef?.validateOwnerUser"
@click="handleUnlock"
>
解锁
</Button>
<Button
v-if="!customer.lockStatus && permissionListRef?.validateOwnerUser"
@click="handleLock"
>
锁定
</Button>
<Button v-if="!customer.ownerUserId" @click="handleReceive">
领取
</Button>
<Button v-if="!customer.ownerUserId" @click="handleDistributeForm">
分配
</Button>
<Button
v-if="customer.ownerUserId && permissionListRef?.validateOwnerUser"
@click="handlePutPool"
>
放入公海
</Button>
</div>
</template>
<Card>
<Description :data="customer" />
</Card>
<Card class="mt-4">
<Tabs>
<Tabs.TabPane tab="跟进记录" key="1">
<div>跟进记录</div>
</Tabs.TabPane>
<Tabs.TabPane tab="基本信息" key="2">
<CustomerDetailsInfo />
</Tabs.TabPane>
<Tabs.TabPane tab="联系人" key="3">
<div>联系人</div>
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="4">
<div>团队成员</div>
</Tabs.TabPane>
<Tabs.TabPane tab="商机" key="5">
<div>商机</div>
</Tabs.TabPane>
<Tabs.TabPane tab="合同" key="6">
<div>合同</div>
</Tabs.TabPane>
<Tabs.TabPane tab="回款" key="7">
<div>回款</div>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="8">
<OperateLog :log-list="logList" />
</Tabs.TabPane>
</Tabs>
</Card>
</Page>
</template>

View File

@@ -57,14 +57,14 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '客户名称',
field: 'name',
width: 160,
minWidth: 160,
fixed: 'left',
slots: { default: 'name' },
},
{
title: '客户来源',
field: 'source',
width: 100,
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
@@ -73,22 +73,22 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '手机',
field: 'mobile',
width: 120,
minWidth: 120,
},
{
title: '电话',
field: 'telephone',
width: 120,
minWidth: 120,
},
{
title: '邮箱',
field: 'email',
width: 140,
minWidth: 140,
},
{
title: '客户级别',
field: 'level',
width: 135,
minWidth: 135,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
@@ -97,7 +97,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '客户行业',
field: 'industryId',
width: 100,
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
@@ -106,18 +106,18 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '下次联系时间',
field: 'contactNextTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '备注',
field: 'remark',
width: 200,
minWidth: 200,
},
{
title: '成交状态',
field: 'dealStatus',
width: 80,
minWidth: 80,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
@@ -126,30 +126,30 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '最后跟进时间',
field: 'contactLastTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '最后跟进记录',
field: 'contactLastContent',
width: 200,
minWidth: 200,
},
{
title: '更新时间',
field: 'updateTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '创建时间',
field: 'createTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '创建人',
field: 'creatorName',
width: 100,
minWidth: 100,
},
];
}

View File

@@ -0,0 +1,205 @@
<script lang="ts" setup>
import type { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { BizTypeEnum } from '@/api/crm/permission';
import { DICT_TYPE } from '@/utils/dict';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import FollowUpRecordForm from './modules/form.vue';
/** 跟进记录列表 */
defineOptions({ name: 'FollowUpRecord' });
const props = defineProps<{
bizId: number;
bizType: number;
}>();
const { push } = useRouter();
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 添加跟进记录 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 删除跟进记录 */
async function handleDelete(row: FollowUpRecordVO) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
});
try {
await FollowUpRecordApi.deleteFollowUpRecord(row.id);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 打开联系人详情 */
function openContactDetail(id: number) {
push({ name: 'CrmContactDetail', params: { id } });
}
/** 打开商机详情 */
function openBusinessDetail(id: number) {
push({ name: 'CrmBusinessDetail', params: { id } });
}
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FollowUpRecordForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{
field: 'createTime',
title: '创建时间',
width: 180,
formatter: 'formatDateTime',
},
{ field: 'creatorName', title: '跟进人' },
{
field: 'type',
title: '跟进类型',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_FOLLOW_UP_TYPE },
},
},
{ field: 'content', title: '跟进内容' },
{
field: 'nextTime',
title: '下次联系时间',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'contacts',
title: '关联联系人',
visible: props.bizType === BizTypeEnum.CRM_CUSTOMER,
slots: {
default: ({ row }) =>
row.contacts?.map((contact) =>
h(
Button,
{
type: 'link',
onClick: () => openContactDetail(contact.id),
},
() => contact.name,
),
),
},
},
{
field: 'businesses',
title: '关联商机',
visible: props.bizType === BizTypeEnum.CRM_CUSTOMER,
slots: {
default: ({ row }) =>
row.businesses?.map((business) =>
h(
Button,
{
type: 'link',
onClick: () => openBusinessDetail(business.id),
},
() => business.name,
),
),
},
},
{
field: 'actions',
title: '操作',
width: 100,
slots: { default: 'actions' },
},
],
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
return await FollowUpRecordApi.getFollowUpRecordPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
bizType: props.bizType,
bizId: props.bizId,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
},
} as VxeTableGridOptions<FollowUpRecordVO>,
});
watch(
() => props.bizId,
() => {
gridApi.query();
},
);
</script>
<template>
<Page auto-content-height>
<Grid table-title="跟进记录列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: '写跟进',
type: 'primary',
icon: ACTION_ICON.EDIT,
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
<FormModal @success="onRefresh" />
</Page>
</template>

View File

@@ -0,0 +1,87 @@
<script lang="ts" setup>
import type { CrmFollowUpRecordApi } from '#/api/crm/followup';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createFollowUpRecord,
getFollowUpRecord,
updateFollowUpRecord,
} from '#/api/crm/followup';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const formData = ref<CrmFollowUpRecordApi.FollowUpRecord>();
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: 120,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as CrmFollowUpRecordApi.FollowUpRecord;
try {
await (formData.value?.id
? updateFollowUpRecord(data)
: createFollowUpRecord(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<CrmFollowUpRecordApi.FollowUpRecord>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getFollowUpRecord(data.id as number);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-[40%]">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -1,15 +0,0 @@
<script lang="ts" setup>
defineOptions({ name: 'CrmPermissionList' });
defineProps<{
bizId: number | undefined; // 模块数据编号
bizType: number; // 模块类型
showAction: boolean; // 是否展示操作按钮
}>();
</script>
<template>
<div>
<p>待完成</p>
</div>
</template>

View File

@@ -0,0 +1,2 @@
export { default as PermissionList } from './modules/permission-list.vue';
export { default as TransferForm } from './modules/transfer-form.vue';

View File

@@ -0,0 +1,163 @@
<script lang="ts" setup>
import type { CrmPermissionApi } from '#/api/crm/permission';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
BizTypeEnum,
createPermission,
PermissionLevelEnum,
updatePermission,
} from '#/api/crm/permission';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions } from '#/utils';
const emit = defineEmits(['success']);
const formData = ref<CrmPermissionApi.Permission>();
const getTitle = computed(() => {
return formData.value?.ids
? $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: [
{
fieldName: 'ids',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'userId',
label: '选择人员',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
},
dependencies: {
triggerFields: ['ids'],
show: (values) => {
return values.ids === undefined;
},
},
},
{
fieldName: 'level',
label: '权限级别',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(
DICT_TYPE.CRM_PERMISSION_LEVEL,
'number',
).filter((dict) => dict.value !== PermissionLevelEnum.OWNER),
},
rules: 'required',
},
{
fieldName: 'toBizTypes',
label: '同时添加至',
component: 'CheckboxGroup',
componentProps: {
options: [
{
label: '联系人',
value: BizTypeEnum.CRM_CONTACT,
},
{
label: '商机',
value: BizTypeEnum.CRM_BUSINESS,
},
{
label: '合同',
value: BizTypeEnum.CRM_CONTRACT,
},
],
},
dependencies: {
triggerFields: ['ids', 'bizType'],
show: (values) => {
return (
values.ids === undefined &&
formData.value?.bizType === BizTypeEnum.CRM_CUSTOMER
);
},
},
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
let data = (await formApi.getValues()) as CrmPermissionApi.Permission;
data = Object.assign(data, formData.value);
try {
await (formData.value?.ids
? updatePermission(data)
: createPermission(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData();
if (!data || !data.bizType || !data.bizId) {
return;
}
modalApi.lock();
try {
formData.value = {
ids: data.ids || [data.id] || undefined,
userId: undefined,
bizType: data.bizType,
bizId: data.bizId,
level: data.level,
};
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,288 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmPermissionApi } from '#/api/crm/permission';
import { ref, watch } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deletePermissionBatch,
deleteSelfPermission,
getPermissionList,
PermissionLevelEnum,
} from '#/api/crm/permission';
import { $t } from '#/locales';
import { DICT_TYPE } from '#/utils';
import Form from './permission-form.vue';
defineOptions({ name: 'CrmPermissionList' });
const props = defineProps<{
bizId: number; // 模块数据编号
bizType: number; // 模块类型
showAction: boolean; // 是否展示操作按钮
}>();
const emits = defineEmits<{
(e: 'quitTeam'): void;
}>();
const gridData = ref<CrmPermissionApi.Permission[]>([]);
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
// 校验负责人权限和编辑权限
const validateOwnerUser = ref(false);
const validateWrite = ref(false);
const isPool = ref(false);
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
const checkedIds = ref<CrmPermissionApi.Permission[]>([]);
function setCheckedIds({
records,
}: {
records: CrmPermissionApi.Permission[];
}) {
checkedIds.value = records;
}
function handleCreate() {
formModalApi
.setData({
bizType: props.bizType,
bizId: props.bizId,
})
.open();
}
function handleEdit() {
if (checkedIds.value.length === 0) {
message.error('请先选择团队成员后操作!');
return;
}
if (checkedIds.value.length > 1) {
message.error('只能选择一个团队成员进行编辑!');
return;
}
formModalApi
.setData({
bizType: props.bizType,
bizId: props.bizId,
id: checkedIds.value[0]?.id,
level: checkedIds.value[0]?.level,
})
.open();
}
function handleDelete() {
if (checkedIds.value.length === 0) {
message.error('请先选择团队成员后操作!');
return;
}
return new Promise((resolve, reject) => {
confirm({
content: `你要将${checkedIds.value.map((item) => item.nickname).join(',')}移出团队吗?`,
})
.then(async () => {
// 更新用户状态
const res = await deletePermissionBatch(
checkedIds.value.map((item) => item.id as number),
);
if (res) {
// 提示并返回成功
message.success($t('ui.actionMessage.operationSuccess'));
resolve(true);
} else {
reject(new Error('移出失败'));
}
})
.catch(() => {
reject(new Error('取消操作'));
});
});
}
const userStore = useUserStore();
async function handleQuit() {
const permission = gridApi.grid
.getData()
.find(
(item) =>
item.id === userStore.userInfo?.id &&
item.level === PermissionLevelEnum.OWNER,
);
if (permission) {
message.warning('负责人不能退出团队!');
return;
}
const userPermission = gridApi.grid
.getData()
.find((item) => item.id === userStore.userInfo?.id);
if (!userPermission) {
message.warning('你不是团队成员!');
return;
}
await deleteSelfPermission(userPermission.id);
message.success('退出团队成员成功!');
emits('quitTeam');
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{
type: 'checkbox',
width: 50,
},
{
field: 'nickname',
title: '姓名',
},
{
field: 'deptName',
title: '部门',
},
{
field: 'postNames',
title: '岗位',
},
{
field: 'level',
title: '权限级别',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_PERMISSION_LEVEL },
},
},
{
field: 'createTime',
title: '加入时间',
formatter: 'formatDateTime',
},
],
height: 'auto',
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_params) => {
const res = await getPermissionList({
bizId: props.bizId,
bizType: props.bizType,
});
gridData.value = res;
return res;
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<CrmPermissionApi.Permission>,
gridEvents: {
checkboxAll: setCheckedIds,
checkboxChange: setCheckedIds,
},
});
defineExpose({
openForm: handleCreate,
validateOwnerUser,
validateWrite,
isPool,
});
watch(
() => gridData.value,
(data) => {
isPool.value = false;
if (data.length > 0) {
isPool.value = data.some(
(item) => item.level === PermissionLevelEnum.OWNER,
);
validateOwnerUser.value = false;
validateWrite.value = false;
const userId = userStore.userInfo?.id;
gridData.value
.filter((item) => item.userId === userId)
.forEach((item) => {
if (item.level === PermissionLevelEnum.OWNER) {
validateOwnerUser.value = true;
validateWrite.value = true;
} else if (item.level === PermissionLevelEnum.WRITE) {
validateWrite.value = true;
}
});
} else {
isPool.value = true;
}
},
{
immediate: true,
},
);
</script>
<template>
<div>
<FormModal @success="onRefresh" />
<Grid>
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('common.create'),
type: 'primary',
icon: ACTION_ICON.ADD,
ifShow: validateOwnerUser,
onClick: handleCreate,
},
{
label: $t('common.edit'),
type: 'primary',
icon: ACTION_ICON.EDIT,
ifShow: validateOwnerUser,
onClick: handleEdit,
},
{
label: $t('common.delete'),
type: 'primary',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: validateOwnerUser,
onClick: handleDelete,
},
{
label: '退出团队',
type: 'primary',
danger: true,
ifShow: !validateOwnerUser,
onClick: handleQuit,
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,202 @@
<script lang="ts" setup>
import type { CrmPermissionApi } from '#/api/crm/permission';
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { transferBusiness } from '#/api/crm/business';
import { transferClue } from '#/api/crm/clue';
import { transferContact } from '#/api/crm/contact';
import { transferContract } from '#/api/crm/contract';
import { transferCustomer } from '#/api/crm/customer';
import { BizTypeEnum, PermissionLevelEnum } from '#/api/crm/permission';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions } from '#/utils';
defineOptions({ name: 'CrmTransferForm' });
const emit = defineEmits(['success']);
const bizType = defineModel<number>('bizType');
const getTitle = computed(() => {
switch (bizType.value) {
case BizTypeEnum.CRM_BUSINESS: {
return '商机转移';
}
case BizTypeEnum.CRM_CLUE: {
return '线索转移';
}
case BizTypeEnum.CRM_CONTACT: {
return '联系人转移';
}
case BizTypeEnum.CRM_CONTRACT: {
return '合同转移';
}
case BizTypeEnum.CRM_CUSTOMER: {
return '客户转移';
}
default: {
return '转移';
}
}
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'ownerUserId',
label: '选择新负责人',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
},
rules: 'required',
},
{
fieldName: 'oldOwnerHandler',
label: '老负责人',
component: 'RadioGroup',
componentProps: {
options: [
{
label: '加入团队',
value: true,
},
{
label: '移除',
value: false,
},
],
},
rules: 'required',
},
{
fieldName: 'oldOwnerPermissionLevel',
label: '老负责人权限级别',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(
DICT_TYPE.CRM_PERMISSION_LEVEL,
'number',
).filter((dict) => dict.value !== PermissionLevelEnum.OWNER),
},
dependencies: {
triggerFields: ['oldOwnerHandler'],
show: (values) => values.oldOwnerHandler,
trigger(values) {
if (!values.oldOwnerHandler) {
formApi.setFieldValue('oldOwnerPermissionLevel', undefined);
}
},
},
rules: 'required',
},
{
fieldName: 'toBizTypes',
label: '同时转移',
component: 'CheckboxGroup',
componentProps: {
options: [
{
label: '联系人',
value: BizTypeEnum.CRM_CONTACT,
},
{
label: '商机',
value: BizTypeEnum.CRM_BUSINESS,
},
{
label: '合同',
value: BizTypeEnum.CRM_CONTRACT,
},
],
},
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as CrmPermissionApi.TransferReq;
try {
switch (bizType.value) {
case BizTypeEnum.CRM_BUSINESS: {
return await transferBusiness(data);
}
case BizTypeEnum.CRM_CLUE: {
return await transferClue(data);
}
case BizTypeEnum.CRM_CONTACT: {
return await transferContact(data);
}
case BizTypeEnum.CRM_CONTRACT: {
return await transferContract(data);
}
case BizTypeEnum.CRM_CUSTOMER: {
return await transferCustomer(data);
}
default: {
message.error('【转移失败】没有转移接口');
throw new Error('【转移失败】没有转移接口');
}
}
} finally {
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formApi.resetForm();
return;
}
// 加载数据
const data = modalApi.getData<{ bizType: number }>();
if (!data || !data.bizType) {
return;
}
bizType.value = data.bizType;
formApi.setFieldValue('id', data.bizType);
},
});
</script>
<template>
<Modal :title="getTitle" class="w-[40%]">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>productInfo</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>productList</div>
</template>

View File

@@ -173,38 +173,38 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '回款编号',
field: 'no',
width: 150,
minWidth: 150,
fixed: 'left',
slots: { default: 'no' },
},
{
title: '客户名称',
field: 'customerName',
width: 150,
minWidth: 150,
slots: { default: 'customerName' },
},
{
title: '合同编号',
field: 'contract',
width: 150,
minWidth: 150,
slots: { default: 'contractNo' },
},
{
title: '回款日期',
field: 'returnTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '回款金额(元)',
field: 'price',
width: 150,
minWidth: 150,
formatter: 'formatNumber',
},
{
title: '回款方式',
field: 'returnType',
width: 150,
minWidth: 150,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
@@ -213,45 +213,45 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '备注',
field: 'remark',
width: 150,
minWidth: 150,
},
{
title: '合同金额(元)',
field: 'contract.totalPrice',
width: 150,
minWidth: 150,
formatter: 'formatNumber',
},
{
title: '负责人',
field: 'ownerUserName',
width: 150,
minWidth: 150,
},
{
title: '所属部门',
field: 'ownerUserDeptName',
width: 150,
minWidth: 150,
},
{
title: '更新时间',
field: 'updateTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '创建时间',
field: 'createTime',
width: 150,
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '创建人',
field: 'creatorName',
width: 150,
minWidth: 150,
},
{
title: '回款状态',
field: 'auditStatus',
width: 100,
minWidth: 100,
fixed: 'right',
cellRender: {
name: 'CellDict',

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>receivableInfo</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>receivableList</div>
</template>

View File

@@ -122,48 +122,48 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '客户名称',
field: 'customerName',
width: 150,
minWidth: 150,
fixed: 'left',
slots: { default: 'customerName' },
},
{
title: '合同编号',
field: 'contractNo',
width: 200,
minWidth: 200,
},
{
title: '期数',
field: 'period',
width: 150,
minWidth: 150,
slots: { default: 'period' },
},
{
title: '计划回款金额(元)',
field: 'price',
width: 160,
minWidth: 160,
formatter: 'formatNumber',
},
{
title: '计划回款日期',
field: 'returnTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '提前几天提醒',
field: 'remindDays',
width: 150,
minWidth: 150,
},
{
title: '提醒日期',
field: 'remindTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '回款方式',
field: 'returnType',
width: 130,
minWidth: 130,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
@@ -172,28 +172,29 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '备注',
field: 'remark',
minWidth: 120,
},
{
title: '负责人',
field: 'ownerUserName',
width: 120,
minWidth: 120,
},
{
title: '实际回款金额(元)',
field: 'receivable.price',
width: 160,
minWidth: 160,
formatter: 'formatNumber',
},
{
title: '实际回款日期',
field: 'receivable.returnTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '未回款金额(元)',
field: 'unpaidPrice',
width: 160,
minWidth: 160,
formatter: ({ row }) => {
if (row.receivable) {
return row.price - row.receivable.price;
@@ -204,19 +205,19 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
title: '更新时间',
field: 'updateTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '创建时间',
field: 'createTime',
width: 180,
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '创建人',
field: 'creatorName',
width: 100,
minWidth: 100,
},
{
title: '操作',

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>receivablePlanInfo</div>
</template>

View File

@@ -0,0 +1,4 @@
<script lang="ts" setup></script>
<template>
<div>receivablePlanList</div>
</template>