---初始化后台管理web页面项目

This commit is contained in:
2025-08-20 14:39:30 +08:00
parent ad49711a7e
commit 87545a8baf
2057 changed files with 282864 additions and 213 deletions

View File

@ -0,0 +1,236 @@
<template>
<PageWrapper dense fixedHeight contentFullHeight>
<CollectionInfo :id="recordId" @return-page="returnPage" v-if="isShowPage" />
<BasicTableErp @register="registerTable" v-else>
<template #toolbar>
<a-button type="primary" @click="handleCreate"> {{ t('新增') }} </a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex == 'overDay'">
<span :style="{ color: `${Number(record.overDay) > 0 ? '#F56C6C' : ''}` }">
{{ record.overDay }}
</span>
</template>
<template v-if="column.dataIndex == 'state'">
<a-tag :color="getState(record.state, false)">
{{ getState(record.state, true) }}
</a-tag>
</template>
<template v-if="column.dataIndex == 'action'">
<a-button type="link" class="actionTxt" @click="showPage(record)">回款记录</a-button>
<a-button type="link" class="actionTxt" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" class="actionTxt" @click="handleDelete(record)">删除</a-button>
</template>
</template>
</BasicTableErp>
<CollectionModal @register="registerModal" @success="reload" />
</PageWrapper>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { useTable, BasicColumn, FormSchema } from '/@/components/Table';
import BasicTableErp from '/@/components/Table/src/BasicTableErp.vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageWrapper } from '/@/components/Page';
import { getCollectionPageList, deleteCollection } from '/@/api/erp/customer/collection';
import { useI18n } from '/@/hooks/web/useI18n';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import CollectionModal from './components/CollectionModal.vue';
import CollectionInfo from './components/CollectionInfo.vue';
const { t } = useI18n();
const columns: BasicColumn[] = [
{
title: '客户名称',
dataIndex: 'name',
width: 100,
},
{
title: '计划回款金额',
dataIndex: 'waitAmount',
width: 100,
},
{
title: '已回款金额',
dataIndex: 'alreadyAmount',
width: 100,
},
{
title: '未回款金额',
dataIndex: 'unpaidAmount',
width: 100,
},
{
title: '计划回款日期',
dataIndex: 'receivedDate',
width: 100,
},
{
title: '最迟回款日期',
dataIndex: 'finallyDate',
width: 100,
},
{
title: '逾期天数',
dataIndex: 'overDay',
width: 80,
},
{
title: '合同标题',
dataIndex: 'title',
width: 100,
},
{
title: '合同负责人',
dataIndex: 'principalNames',
width: 100,
},
{
title: '创建日期',
dataIndex: 'createDate',
width: 100,
},
{
title: '创建人',
dataIndex: 'createUserName',
width: 100,
},
{
title: '状态',
dataIndex: 'state',
width: 80,
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'keyword',
label: '关键字',
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: '请输入关键字',
},
},
{
field: 'receivedDate',
label: '计划回款时间',
component: 'RangePicker',
colProps: { span: 8 },
componentProps: {
format: 'YYYY-MM-DD',
getPopupContainer: () => document.body,
},
},
{
field: 'finallyDate',
label: '最迟回款时间',
component: 'RangePicker',
colProps: { span: 8 },
componentProps: {
format: 'YYYY-MM-DD',
getPopupContainer: () => document.body,
},
},
];
const { notification } = useMessage();
const [registerModal, { openModal }] = useModal();
const recordId = ref('');
const isShowPage = ref<boolean>(false);
const [registerTable, { reload }] = useTable({
title: '回款列表',
api: getCollectionPageList,
rowKey: 'id',
columns,
formConfig: {
rowProps: {
gutter: 16,
},
schemas: searchFormSchema,
fieldMapToTime: [
['receivedDate', ['receivedStartTime', 'receivedEndTime'], 'YYYY-MM-DD', true],
['finallyDate', ['finallyStartTime', 'finallyEndTime'], 'YYYY-MM-DD', true],
],
},
beforeFetch: (params) => {
return { ...params, state: 0 };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
actionColumn: {
width: 150,
title: t('操作'),
dataIndex: 'action',
},
});
const getState = (state, isText) => {
switch (state) {
case -1:
return isText ? '未开始' : 'error';
case 0:
return isText ? '进行中' : 'processing';
case 1:
return isText ? '已完成' : 'success';
}
};
const handleCreate = () => {
openModal(true, {
state: 0,
isUpdate: false,
});
};
const handleEdit = (record) => {
openModal(true, {
state: 0,
id: record.id,
isUpdate: true,
});
};
const handleDelete = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: t('是否确认删除?'),
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await deleteCollection(record.id);
notification.success({
message: t('提示'),
description: t('删除成功'),
});
reload();
},
onCancel() {},
});
};
const showPage = (record) => {
isShowPage.value = true;
recordId.value = record.id;
};
const returnPage = () => {
isShowPage.value = false;
reload();
};
</script>
<style lang="less" scoped>
.actionTxt {
padding: 4px 2px;
&:last-child {
color: #f56c6c;
}
}
</style>

View File

@ -0,0 +1,248 @@
<template>
<PageWrapper dense fixedHeight contentFullHeight>
<CustomerInfo :id="recordId" @return-page="returnPage" :isCommon="true" v-if="isShowPage" />
<BasicTableErp @register="registerTable" v-else>
<template #toolbar>
<a-button type="primary" @click="handleCreate"> {{ t('新增') }} </a-button>
<a-button type="primary" @click="handleImport"> {{ t('导入') }} </a-button>
<a-button type="primary" @click="handleExport"> {{ t('导出') }} </a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex == 'action'">
<a-button type="link" class="actionTxt" @click="handleCustomer(record)">领取</a-button>
<a-button type="link" class="actionTxt" @click="showPage(record)"> 详情 </a-button>
<a-button type="link" class="actionTxt" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" class="actionTxt" @click="handleDelete(record)">删除</a-button>
</template>
</template>
</BasicTableErp>
<CustomerModal @register="registerModal" @success="reload" />
<ImportModal
@register="registerImportModal"
importUrl="/caseErpCustomer/caseErpCustomer/import-common"
@success="reload"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { useTable, BasicColumn, FormSchema } from '/@/components/Table';
import BasicTableErp from '/@/components/Table/src/BasicTableErp.vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageWrapper } from '/@/components/Page';
import { getCustomerPageList, deleteCustomer } from '/@/api/erp/customer/list';
import { getFromCommon, exportInfo, downloadTemplate } from '/@/api/erp/customer/common';
import { useI18n } from '/@/hooks/web/useI18n';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { ImportModal } from '/@/components/Import';
import { downloadByData } from '/@/utils/file/download';
import CustomerModal from './components/CustomerModal.vue';
import CustomerInfo from './components/CustomerInfo.vue';
const { t } = useI18n();
const columns: BasicColumn[] = [
{
title: '客户名称',
dataIndex: 'name',
},
{
title: '客户类型',
dataIndex: 'typeName',
},
{
title: '联系人',
dataIndex: 'defaultName',
},
{
title: '手机号码',
dataIndex: 'defaultPhone',
},
{
title: '所在行业',
dataIndex: 'industry',
},
{
title: '来源',
dataIndex: 'sourceName',
},
{
title: '加入公海日期',
dataIndex: 'inOpenDate',
},
{
title: '创建人',
dataIndex: 'createUserName',
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'typeId',
label: '客户类型',
component: 'XjrSelect',
colProps: { span: 8 },
componentProps: {
datasourceType: 'dic',
params: { itemId: '1679007059387240450' },
labelField: 'name',
valueField: 'id',
placeholder: '请选择客户类型',
getPopupContainer: () => document.body,
},
},
{
field: 'createDate',
label: '加入时间',
component: 'RangePicker',
colProps: { span: 8 },
componentProps: {
format: 'YYYY-MM-DD',
getPopupContainer: () => document.body,
},
},
{
field: 'sourceId',
label: '来源',
component: 'XjrSelect',
colProps: { span: 8 },
componentProps: {
datasourceType: 'dic',
params: { itemId: '1679008505423876097' },
labelField: 'name',
valueField: 'id',
placeholder: '请选择来源',
getPopupContainer: () => document.body,
},
},
{
field: 'name',
label: '客户名称',
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: '请输入客户名称',
},
},
];
const { notification } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerImportModal, { openModal: openImportModal }] = useModal();
const recordId = ref('');
const isShowPage = ref<boolean>(false);
const [registerTable, { reload }] = useTable({
title: '客户公海',
api: getCustomerPageList,
rowKey: 'id',
columns,
formConfig: {
rowProps: {
gutter: 16,
},
schemas: searchFormSchema,
fieldMapToTime: [['createDate', ['startTime', 'endTime'], 'YYYY-MM-DD', true]],
},
beforeFetch: (params) => {
return { ...params, state: 1 };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
actionColumn: {
width: 160,
title: t('操作'),
dataIndex: 'action',
},
});
const handleCreate = () => {
openModal(true, {
state: 1,
isUpdate: false,
});
};
const handleEdit = (record) => {
openModal(true, {
state: 1,
id: record.id,
isUpdate: true,
});
};
const handleCustomer = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: '确认也要领取该客户吗?',
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await getFromCommon(record.id);
notification.success({
message: t('提示'),
description: '领取成功',
});
reload();
},
onCancel() {},
});
};
const handleDelete = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: t('是否确认删除?'),
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await deleteCustomer(record.id);
notification.success({
message: t('提示'),
description: t('删除成功'),
});
reload();
},
onCancel() {},
});
};
const handleImport = () => {
openImportModal(true, {
title: t('快速导入'),
api: downloadTemplate,
templateTitle: '客户公海模板',
});
};
const handleExport = async () => {
const res = await exportInfo();
downloadByData(
res.data,
'客户公海.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
};
const showPage = (record) => {
isShowPage.value = true;
recordId.value = record.id;
};
const returnPage = () => {
isShowPage.value = false;
reload();
};
</script>
<style lang="less" scoped>
.actionTxt {
padding: 4px 2px;
&:last-child {
color: #f56c6c;
}
}
</style>

View File

@ -0,0 +1,318 @@
<template>
<PageWrapper dense fixedHeight contentFullHeight>
<CustomerInfo :id="recordId" @return-page="returnPage" v-if="isShowPage" />
<BasicTableErp @register="registerTable" v-else>
<template #toolbar>
<a-button type="primary" @click="handleCreate"> {{ t('新增') }} </a-button>
<a-button type="primary" @click="handleImport"> {{ t('导入') }} </a-button>
<a-button type="primary" @click="handleExport"> {{ t('导出') }} </a-button>
<a-button type="primary" @click="handleTransfer"> 移入公海 </a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex == 'action'">
<a-button type="link" class="actionTxt" @click="handleFollow(record)">跟进</a-button>
<a-button type="link" class="actionTxt" @click="showPage(record)"> 详情查看 </a-button>
<a-button type="link" class="actionTxt" @click="handleEdit(record)">编辑</a-button>
<SelectUser
:selectedIds="userIds"
:multiple="false"
@change="(ids) => handleTransferUser(ids, record)"
:style="{ display: 'inline' }"
>
<a-button type="link" class="actionTxt" :style="{ color: '#5e95ff' }">转移</a-button>
</SelectUser>
<a-button type="link" class="actionTxt" @click="handleDelete(record)">删除</a-button>
</template>
</template>
</BasicTableErp>
<CustomerModal @register="registerModal" @success="reload" />
<FollowModal @register="registerFollowModal" @success="reload" />
<ImportModal
@register="registerImportModal"
importUrl="/caseErpCustomer/caseErpCustomer/import"
@success="reload"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { useTable, BasicColumn, FormSchema } from '/@/components/Table';
import BasicTableErp from '/@/components/Table/src/BasicTableErp.vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageWrapper } from '/@/components/Page';
import {
getCustomerPageList,
deleteCustomer,
transferCommon,
exportInfo,
downloadTemplate,
transferUser,
} from '/@/api/erp/customer/list';
import { useI18n } from '/@/hooks/web/useI18n';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { ImportModal } from '/@/components/Import';
import { downloadByData } from '/@/utils/file/download';
import { SelectUser } from '/@/components/SelectOrganizational/index';
import CustomerModal from './components/CustomerModal.vue';
import CustomerInfo from './components/CustomerInfo.vue';
import FollowModal from './components/FollowModal.vue';
const { t } = useI18n();
const columns: BasicColumn[] = [
{
title: '客户名称',
dataIndex: 'name',
},
{
title: '客户类型',
dataIndex: 'typeName',
width: 100,
},
{
title: '联系人',
dataIndex: 'defaultName',
width: 100,
},
{
title: '手机号码',
dataIndex: 'defaultPhone',
},
{
title: '所在行业',
dataIndex: 'industry',
width: 100,
},
{
title: '来源',
dataIndex: 'sourceName',
},
{
title: '归属',
dataIndex: 'saleName',
width: 100,
},
{
title: '创建日期',
dataIndex: 'createDate',
},
{
title: '创建人',
dataIndex: 'createUserName',
width: 100,
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'name',
label: '客户名称',
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: '请输入客户名称',
},
},
{
field: 'typeId',
label: '客户类型',
component: 'XjrSelect',
colProps: { span: 8 },
componentProps: {
datasourceType: 'dic',
params: { itemId: '1679007059387240450' },
labelField: 'name',
valueField: 'id',
placeholder: '请选择客户类型',
getPopupContainer: () => document.body,
},
},
{
field: 'sourceId',
label: '来源',
component: 'XjrSelect',
colProps: { span: 8 },
componentProps: {
datasourceType: 'dic',
params: { itemId: '1679008505423876097' },
labelField: 'name',
valueField: 'id',
placeholder: '请选择来源',
getPopupContainer: () => document.body,
},
},
{
field: 'createDate',
label: '创建时间',
component: 'RangePicker',
colProps: { span: 8 },
componentProps: {
format: 'YYYY-MM-DD',
getPopupContainer: () => document.body,
},
},
];
const { notification } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerFollowModal, { openModal: openFollowModal }] = useModal();
const [registerImportModal, { openModal: openImportModal }] = useModal();
const recordId = ref('');
const isShowPage = ref<boolean>(false);
const userIds = ref([]);
const customRow = (record) => {
return {
onClick: () => {
let selectedRowKeys = [...getSelectRowKeys()];
if (selectedRowKeys.indexOf(record.id) >= 0) {
let index = selectedRowKeys.indexOf(record.id);
selectedRowKeys.splice(index, 1);
} else {
selectedRowKeys.push(record.id);
}
setSelectedRowKeys(selectedRowKeys);
},
};
};
const [registerTable, { reload, getSelectRowKeys, setSelectedRowKeys }] = useTable({
title: '客户列表',
api: getCustomerPageList,
rowKey: 'id',
columns,
formConfig: {
rowProps: {
gutter: 16,
},
schemas: searchFormSchema,
fieldMapToTime: [['createDate', ['startTime', 'endTime'], 'YYYY-MM-DD', true]],
},
beforeFetch: (params) => {
return { ...params, state: 0 };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
actionColumn: {
width: 220,
title: t('操作'),
dataIndex: 'action',
},
rowSelection: {
type: 'checkbox',
},
customRow,
});
const handleCreate = () => {
openModal(true, {
state: 0,
isUpdate: false,
});
};
const handleEdit = (record) => {
openModal(true, {
state: 0,
id: record.id,
isUpdate: true,
});
};
const handleFollow = (record) => {
openFollowModal(true, {
record,
});
};
const handleDelete = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: t('是否确认删除?'),
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await deleteCustomer(record.id);
notification.success({
message: t('提示'),
description: t('删除成功'),
});
reload();
},
onCancel() {},
});
};
const handleImport = () => {
openImportModal(true, {
title: t('快速导入'),
api: downloadTemplate,
templateTitle: '客户列表模板',
});
};
const handleExport = async () => {
if (!getSelectRowKeys().length) {
notification.warning({
message: 'Tip',
description: '请选择需要导出的数据',
});
return false;
}
const res = await exportInfo(getSelectRowKeys());
downloadByData(
res.data,
'客户列表.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
};
const handleTransfer = async () => {
if (!getSelectRowKeys().length) {
notification.warning({
message: 'Tip',
description: '请选择需要移入公海的数据',
});
return false;
}
await transferCommon(getSelectRowKeys());
notification.success({
message: t('提示'),
description: '移入公海',
});
reload();
};
const handleTransferUser = async (ids, record) => {
const params = {
id: record.id,
saleIds: ids.length ? ids[0] : '',
};
await transferUser(params);
notification.success({
message: t('提示'),
description: '转移成功',
});
reload();
};
const showPage = (record) => {
isShowPage.value = true;
recordId.value = record.id;
};
const returnPage = () => {
isShowPage.value = false;
reload();
};
</script>
<style lang="less" scoped>
.actionTxt {
padding: 4px 2px;
&:last-child {
color: #f56c6c;
}
}
</style>

View File

@ -0,0 +1,330 @@
<template>
<div style="padding: 10px; box-sizing: border-box">
<div>
<div class="mumber-box">
<div class="mumber" v-for="(it, index) in data.users" :key="index">
<div class="mum" :style="{ color: index > 2 ? '#FF8080' : '#5E95FF' }">{{ it.num }}</div>
<div class="mum-title">{{ it.name }}</div>
</div>
</div>
<a-row :gutter="10">
<a-col :span="6">
<div class="box-bg">
<div class="box-bg-title">
<span class="title-text">客户来源统计</span>
</div>
<div class="item" ref="homeVolChart"></div>
</div>
</a-col>
<a-col :span="12">
<div class="box-bg">
<div class="box-bg-title">
<span class="title-text">回款面积图</span>
<div class="title-chose">
<span
v-for="item in data.priceArray"
:key="item.key"
:class="item.key == data.cur ? 'cur' : ''"
@click="changePrice(item)"
>{{ item.name }}</span
>
</div>
</div>
<div class="item" ref="homeLineChart"></div>
</div>
</a-col>
<a-col :span="6">
<div class="box-bg">
<div class="box-bg-title">
<span class="title-text">客户类型分布统计</span>
</div>
<div class="item" ref="homePieChart"></div>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, reactive, ref, unref, markRaw } from 'vue';
import {
getSourceInfo,
getTypeInfo,
getGatherInfo,
getCustomerInfo,
} from '/@/api/erp/customer/workbench';
import * as echarts from 'echarts';
const formate = (name) => {
let str = name + '';
data.dataOne.forEach((o: any) => {
if (name == o.name) {
str += o.value + '家';
}
});
return str;
};
const formateTwo = (name) => {
let str = name + '';
data.dataTwo.forEach((o: any) => {
if (name == o.name) {
str += o.value + '家';
}
});
return str;
};
const data = reactive({
users: [] as any[],
// 玫瑰饼图
dataOne: [],
pieOption: {
color: ['#7EAAFF', '#6D68F8', '#9E9BFC'],
legend: {
orient: 'horizontal',
x: 'center',
y: 'bottom',
top: '78%',
itemWidth: 10,
itemHeight: 10,
itemGap: 20,
padding: [5, 5, 5, 5],
textStyle: {
color: '#85878e',
},
formatter: formate,
},
series: {
name: '',
type: 'pie',
radius: [0, 85],
center: ['50%', '32%'],
roseType: 'area',
itemStyle: {
borderRadius: 0,
},
label: {
color: '#6e7079',
overflow: 'none',
formatter: (params) => {
let per = params.percent || 0;
return params.name + '\n' + per + '%';
},
},
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
},
//漏斗图
dataTwo: [],
FunnelOption: {
color: ['#9E9BFC', '#6D68F8', '#5887E3', '#7EAAFF', '#8CB1F9'],
legend: {
orient: 'horizontal',
x: 'center',
y: 'bottom',
top: '78%',
itemWidth: 10,
itemHeight: 10,
itemGap: 20,
padding: [5, 5, 5, 5],
textStyle: {
color: '#85878e',
},
formatter: formateTwo,
},
series: {
name: '',
type: 'funnel',
width: '60%',
height: '60%',
left: '5%',
top: 30,
labelLine: {
show: false,
},
label: {
color: '#6e7079',
formatter: '{b} {d}%',
},
data: [],
},
},
// 折线图
priceArray: [] as any[],
cur: 0,
myChart3: null as any,
lineOption: {
tooltip: {
trigger: 'axis',
},
grid: {
left: 50,
right: 40,
},
color: ['#9863c1'],
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
},
yAxis: {
type: 'value',
name: '单位:万元',
},
series: [
{
data: [],
type: 'line',
showSymbol: false,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(140, 125, 252, 0.6)',
},
{
offset: 1,
color: 'rgba(140, 125, 252, 0)',
},
]),
},
smooth: true,
},
],
},
});
const homePieChart = ref<HTMLDivElement>();
const homeLineChart = ref<HTMLDivElement>();
const homeVolChart = ref<HTMLDivElement>();
onMounted(async () => {
let res = await getCustomerInfo();
data.users = res || [];
let res1 = await getSourceInfo();
data.dataTwo = res1 || [];
let res2 = await getGatherInfo();
data.priceArray = res2 || [];
let res3 = await getTypeInfo();
data.dataOne = res3 || [];
data.pieOption.series.data = data.dataOne;
data.FunnelOption.series.data = data.dataTwo;
nextTick(() => {
let myChart = markRaw(echarts.init(unref(homePieChart) as HTMLDivElement));
myChart.setOption(data.pieOption, true);
myChart.resize(); //显示区域大小发生改变更新图表
data.myChart3 = markRaw(echarts.init(unref(homeLineChart) as HTMLDivElement));
changePrice(data.priceArray[0]);
let myChart1 = markRaw(echarts.init(unref(homeVolChart) as HTMLDivElement));
myChart1.setOption(data.FunnelOption, true);
myChart1.resize(); //显示区域大小发生改变更新图表
});
});
const changePrice = (o) => {
data.cur = o.key;
data.lineOption.xAxis.data = o.category;
data.lineOption.series[0].data = o.data;
data.myChart3.setOption(data.lineOption, true);
data.myChart3.resize(); //显示区域大小发生改变更新图表
};
</script>
<style lang="less" scoped>
.title-chose {
float: right;
margin-right: 20px;
span {
text-align: center;
padding: 0 12px;
display: inline-block;
color: #333;
height: 30px;
line-height: 30px;
font-size: 14px;
cursor: pointer;
margin-left: 10px;
&.cur,
&:active {
background-color: #5e95ff;
color: #fff;
}
}
}
.box-bg {
background-color: #fff;
box-sizing: border-box;
border-radius: 5px;
box-shadow: 0 3px 6px 1px rgb(0 0 0 / 10%);
.box-bg-title {
border-bottom: 1px solid #eee;
height: 50px;
line-height: 50px;
font-size: 16px;
box-sizing: border-box;
}
.title-text {
padding: 0 15px;
}
.item {
padding: 10px 0 0 10px;
width: 100%;
height: 370px;
}
}
.mumber-box {
margin: 2px -5px 10px;
display: flex;
justify-content: space-between;
.mumber {
padding: 20px;
box-sizing: border-box;
margin: 0 5px;
flex: 1;
height: 120px;
background: #fff;
box-shadow: 0 3px 6px 1px rgb(0 0 0 / 10%);
border-radius: 20px;
}
.mum-title {
font-size: 14px;
text-align: center;
color: #333;
}
.mum {
font-size: 48px;
font-weight: bolder;
margin: -10px 0 10px;
height: 75%;
display: flex;
align-items: center;
justify-content: center;
}
}
.ant-col {
padding: 0 10px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="附件查看">
<div class="img-box">
<a-image-preview-group v-if="imgList.length">
<a-image v-for="(item, index) in imgList" :key="index" :src="item.fileUrl" />
</a-image-preview-group>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { getFileList } from '/@/api/system/file';
const imgList = ref<any>([]);
defineEmits(['register']);
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
setModalProps({
confirmLoading: false,
destroyOnClose: true,
showOkBtn: false,
showCancelBtn: false,
footer: null,
bodyStyle: {
height: '300px',
},
});
imgList.value = await getFileList({ folderId: data.filePath });
});
</script>
<style lang="less" scoped>
:deep(.ant-image-img) {
width: 100px;
height: 75px;
padding: 5px;
border: 1px solid silver;
border-radius: 4px;
margin-right: 10px;
}
.img-box {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm">
<template #imgUpload="{ model }">
<Upload
v-model:value="model.filePath"
listType="picture"
:maxSize="2"
accept=".bmp,.jpg,.jpeg,.png,.gif,.svg"
/>
</template>
</BasicForm>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { FormSchema } from '/@/components/Table';
import {
addCollectionDetail,
updateCollectionDetail,
getCollectionDetailInfo,
} from '/@/api/erp/customer/collection';
import { useMessage } from '/@/hooks/web/useMessage';
import Upload from '/@/components/Form/src/components/Upload.vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const FormSchema: FormSchema[] = [
{
field: 'amountCollect',
label: '回款金额',
component: 'InputNumber',
required: true,
colProps: { span: 24 },
componentProps: {
placeholder: '请输入回款金额',
},
},
{
field: 'date',
label: '回款时间',
component: 'DatePicker',
required: true,
colProps: { span: 24 },
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择回款时间',
getPopupContainer: () => document.body,
},
},
{
field: 'remark',
label: '备注',
component: 'InputTextArea',
colProps: { span: 24 },
componentProps: {
placeholder: '请输入备注',
},
},
{
field: 'filePath',
label: '附件上传',
component: 'Upload',
slot: 'imgUpload',
colProps: { span: 24 },
},
];
const { notification } = useMessage();
const isUpdate = ref(true);
const gatherId = ref('');
const rowId = ref('');
const emit = defineEmits(['success', 'register']);
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 100,
schemas: FormSchema,
showActionButtonGroup: false,
actionColOptions: {
span: 23,
},
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields();
setModalProps({ confirmLoading: false, destroyOnClose: true });
isUpdate.value = !!data?.isUpdate;
gatherId.value = data.gatherId;
if (unref(isUpdate)) {
rowId.value = data.id;
const record = await getCollectionDetailInfo(data.id);
setFieldsValue({
...record,
});
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增回款记录' : '编辑回款记录'));
const handleSubmit = async () => {
try {
const values = await validate();
values.gatherId = gatherId.value;
setModalProps({ confirmLoading: true });
if (!unref(isUpdate)) {
await addCollectionDetail(values);
notification.success({
message: '新增回款记录详情',
description: t('成功'),
});
} else {
values.id = rowId.value;
await updateCollectionDetail(values);
notification.success({
message: '编辑回款记录详情',
description: t('成功'),
});
}
closeModal();
emit('success');
} catch (error) {
setModalProps({ confirmLoading: false });
}
};
</script>

View File

@ -0,0 +1,209 @@
<template>
<div class="info-box">
<div class="top-box">
<div>
<a-button @click="emit('returnPage')">返回</a-button>
<span class="text-base font-bold pl-5">回款记录</span>
</div>
<a-button type="primary" @click="handleCreate">新增回款记录</a-button>
</div>
<a-divider />
<a-card :bordered="false">
<div class="title-name">{{ baseInfo?.name }}</div>
<div class="title-info">
<span>计划回款金额{{ baseInfo?.waitAmount }}</span>
<span>计划回款日期{{ baseInfo?.receivedDate }}</span>
<span>最迟回款日期{{ baseInfo?.finallyDate }}</span>
<span>合同标题{{ baseInfo?.title }}</span>
<span>合同负责人{{ baseInfo?.principalNames }}</span>
</div>
</a-card>
<BasicTable @register="registerTable">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex == 'action'">
<a-button type="link" class="actionTxt" @click="handleEdit(record)">编辑</a-button>
<a-button
type="link"
class="actionTxt"
@click="handleView(record)"
:disabled="!record.filePath"
>
附件查看
</a-button>
<a-button type="link" class="actionTxt" @click="handleDelete(record)">删除</a-button>
</template>
</template>
</BasicTable>
</div>
<CollectionDetailModal @register="registerModal" @success="reloadTable()" />
<AttachmentModal @register="registerAttachmentModal" />
</template>
<script lang="ts" setup>
import { onMounted, ref, createVNode } from 'vue';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { getDetailInfo, deleteCollectionDetail } from '/@/api/erp/customer/collection';
import { useModal } from '/@/components/Modal';
import CollectionDetailModal from './CollectionDetailModal.vue';
import AttachmentModal from './AttachmentModal.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
const columns: BasicColumn[] = [
{
dataIndex: 'amountCollect',
title: '回款金额',
align: 'center',
},
{
dataIndex: 'remark',
title: '备注',
align: 'center',
},
{
dataIndex: 'date',
title: '回款时间',
align: 'center',
},
{
dataIndex: 'createDate',
title: '创建时间',
align: 'center',
},
{
dataIndex: 'createUserName',
title: '创建人',
align: 'center',
},
];
const { t } = useI18n();
const props = defineProps({
id: String,
});
const emit = defineEmits(['returnPage']);
const dataSource = ref<any>([]);
const baseInfo = ref();
const { notification } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerAttachmentModal, { openModal: openAttachmentModal }] = useModal();
onMounted(() => {
getInfo();
});
const [registerTable, { reload }] = useTable({
dataSource,
rowKey: 'id',
columns,
bordered: true,
pagination: false,
actionColumn: {
width: 150,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
},
});
const getInfo = async () => {
const res = await getDetailInfo(props.id!);
baseInfo.value = res;
dataSource.value = res?.caseErpCustomerGatherDetailVoList;
};
const reloadTable = () => {
getInfo();
reload();
};
const handleCreate = () => {
openModal(true, {
gatherId: props.id,
});
};
const handleEdit = (record) => {
openModal(true, {
gatherId: props.id,
id: record.id,
isUpdate: true,
});
};
const handleView = (record) => {
openAttachmentModal(true, {
filePath: record.filePath,
});
};
const handleDelete = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: t('是否确认删除?'),
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await deleteCollectionDetail(record.id);
notification.success({
message: t('提示'),
description: t('删除成功'),
});
reloadTable();
},
onCancel() {},
});
};
</script>
<style lang="less" scoped>
.info-box {
background-color: #fff;
width: 100%;
height: 100%;
padding: 16px;
margin-right: 8px;
.top-box {
display: flex;
justify-content: space-between;
}
.title-name {
font-size: 18px;
color: #444;
font-weight: 700;
margin-bottom: 25px;
}
.title-info span {
margin-right: 40px;
}
.actionTxt {
padding: 4px 2px;
&:last-child {
color: #f56c6c;
}
}
}
:deep(.ant-divider) {
height: 1px;
background-color: #dcdfe6;
}
:deep(.ant-card-body) {
background-color: #f8f8f8;
margin-bottom: 20px;
}
:deep(.vben-basic-table) {
height: calc(100% - 200px);
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm">
<template #user="{ model }">
<SelectUser
v-model:value="model.principalIds"
suffix="ant-design:setting-outlined"
placeholder="请选择负责人"
/>
</template>
<template #imgUpload="{ model }">
<Upload v-model:value="model.filePath" listType="picture" />
</template>
</BasicForm>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { FormSchema } from '/@/components/Table';
import {
addCollection,
updateCollection,
getCollectionInfo,
} from '/@/api/erp/customer/collection';
import { getCustomerList } from '/@/api/erp/customer/list';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import SelectUser from '/@/components/Form/src/components/SelectUser.vue';
import Upload from '/@/components/Form/src/components/Upload.vue';
const { t } = useI18n();
const FormSchema: FormSchema[] = [
{
field: 'customerId',
label: '客户名称',
component: 'ApiSelect',
required: true,
colProps: { span: 24 },
componentProps: {
placeholder: '请选择客户名称',
api: getCustomerList,
labelField: 'name',
valueField: 'id',
getPopupContainer: () => document.body,
},
},
{
field: 'waitAmount',
label: '计划回款金额',
component: 'InputNumber',
required: true,
colProps: { span: 24 },
componentProps: {
placeholder: '请输入计划回款金额',
},
},
{
field: 'receivedDate',
label: '计划回款日期',
component: 'DatePicker',
required: true,
colProps: { span: 24 },
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择回款日期',
getPopupContainer: () => document.body,
},
},
{
field: 'finallyDate',
label: '最迟回款日期',
component: 'DatePicker',
required: true,
colProps: { span: 24 },
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择最迟回款日期',
getPopupContainer: () => document.body,
},
},
{
field: 'principalIds',
label: '合同负责人',
component: 'Input',
slot: 'user',
required: true,
colProps: { span: 24 },
},
{
field: 'title',
label: '合同标题',
component: 'Input',
colProps: { span: 24 },
componentProps: {
placeholder: '请输入合同标题',
},
},
{
field: 'filePath',
label: '合同附件',
component: 'Upload',
slot: 'imgUpload',
colProps: { span: 24 },
},
];
const { notification } = useMessage();
const isUpdate = ref(true);
const rowId = ref('');
const emit = defineEmits(['success', 'register']);
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 100,
schemas: FormSchema,
showActionButtonGroup: false,
actionColOptions: {
span: 23,
},
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields();
setModalProps({ confirmLoading: false, destroyOnClose: true });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
rowId.value = data.id;
const record = await getCollectionInfo(data.id);
setFieldsValue({
...record,
});
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增回款计划' : '编辑回款计划'));
const handleSubmit = async () => {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
if (!unref(isUpdate)) {
await addCollection(values);
notification.success({
message: '新增回款计划',
description: t('成功'),
});
} else {
values.id = rowId.value;
await updateCollection(values);
notification.success({
message: '编辑回款计划',
description: t('成功'),
});
}
closeModal();
emit('success');
} catch (error) {
setModalProps({ confirmLoading: false });
}
};
</script>

View File

@ -0,0 +1,186 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:width="800"
:body-style="{
height: '400px !important',
}"
:title="getTitle"
@ok="handleSubmit"
>
<div class="config-title">
<span class="font-semibold">联系人</span>
<span class="add-span" @click="addConfig" v-if="!isUpdate">+ 增加</span>
</div>
<a-radio-group v-model:value="defaultIndex" style="width: 100%">
<a-form
ref="formRef"
:model="personList"
name="personList"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
autocomplete="off"
>
<template v-for="(item, index) in personList.list" :key="index">
<a-row style="margin: 0 0 20px 10px">
<a-col :span="20" style="color: #999">联系人{{ index + 1 }}</a-col>
<a-col :span="4" align="right">
<a-radio :value="index">默认</a-radio>
<span @click="deleteConfig(index)" class="delete-span" v-if="index !== 0">删除</span>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item
label="姓名"
:name="['list', index, 'name']"
:rules="{ required: true, message: '请输入姓名', trigger: 'change' }"
>
<a-input v-model:value="item.name" placeholder="请输入姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="电话" :name="['list', index, 'phone']">
<a-input v-model:value="item.phone" placeholder="请输入电话" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="职位" :name="['list', index, 'post']">
<a-input v-model:value="item.post" placeholder="请输入职位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="部门" :name="['list', index, 'dept']">
<a-input v-model:value="item.dept" placeholder="请输入部门" />
</a-form-item>
</a-col>
</a-row>
</template>
</a-form>
</a-radio-group>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import { addContacts, updateContacts, getContactsInfo } from '/@/api/erp/customer/list';
const { t } = useI18n();
const { notification } = useMessage();
const isUpdate = ref(true);
const rowId = ref('');
const customerId = ref('');
const defaultIndex = ref<any>();
const formRef = ref();
const personList = ref<any>({
list: [
{
isDefault: false,
dept: '',
name: '',
phone: '',
post: '',
},
],
});
const emit = defineEmits(['success', 'register']);
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: false, destroyOnClose: true });
isUpdate.value = !!data?.isUpdate;
customerId.value = data.customerId;
defaultIndex.value = null;
if (unref(isUpdate)) {
rowId.value = data.id;
const info = await getContactsInfo(data.id);
personList.value.list[0] = info;
defaultIndex.value = info.isDefault ? 0 : null;
} else {
personList.value = {
list: [
{
isDefault: false,
dept: '',
name: '',
phone: '',
post: '',
},
],
};
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增联系人' : '编辑联系人'));
const addConfig = () => {
personList.value.list.push({ isDefault: false, dept: '', name: '', phone: '', post: '' });
};
const deleteConfig = (index) => {
personList.value.list.splice(index, 1);
};
async function handleSubmit() {
try {
await formRef.value.validate();
personList.value.list.map((x, idx) => {
x.isDefault = idx === defaultIndex.value ? 1 : 0;
});
setModalProps({ confirmLoading: true });
if (!unref(isUpdate)) {
const params = {
customerId: customerId.value,
caseErpCustomerContactsDtoList: [...personList.value.list],
};
await addContacts(params);
notification.success({
message: '新增联系人',
description: t('成功'),
});
} else {
const params = {
id: rowId.value,
customerId: customerId.value,
...personList.value.list[0],
};
await updateContacts(params);
notification.success({
message: '编辑联系人',
description: t('成功'),
});
}
closeModal();
emit('success');
} catch (error) {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
.config-title {
display: flex;
justify-content: space-between;
margin: 18px;
font-weight: 700;
.add-span {
color: #5e95ff;
cursor: pointer;
}
}
.delete-span {
color: #ff8080;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,439 @@
<template>
<div class="info-box">
<div>
<a-button @click="emit('returnPage')">返回</a-button>
<span class="text-base font-bold pl-5">客户详情查看</span>
</div>
<a-divider />
<a-card :bordered="false">
<div class="title-name">{{ baseInfo?.name }}</div>
<div class="title-info">
<span>客户类型{{ baseInfo?.typeName }}</span>
<span>所在行业{{ baseInfo?.industry }}</span>
<span>来源{{ baseInfo?.sourceName }}</span>
<span>规模{{ baseInfo?.scaleName }}</span>
<span>公司性质{{ baseInfo?.natureName }}</span>
<span>地址{{ baseInfo?.address }}</span>
<span v-if="!isCommon">归属{{ baseInfo?.saleName }}</span>
</div>
</a-card>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="跟进记录" v-if="!isCommon">
<BasicTable @register="registerFollowTable">
<template #toolbar>
<a-button type="primary" @click="handleFollowCreate">新增跟进</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex == 'action'">
<TableAction
:actions="[
{
icon: 'clarity:note-edit-line',
onClick: handleFollowEdit.bind(null, record),
},
{
icon: 'ant-design:delete-outlined',
color: 'error',
onClick: handleFollowDelete.bind(null, record),
},
]"
/>
</template>
</template>
</BasicTable>
</a-tab-pane>
<a-tab-pane key="2" tab="客户信息">
<div class="sub-title">客户信息</div>
<a-card :bordered="false">
<div class="title-info flex">
图片资料
<a-image-preview-group v-if="imgList.length">
<a-image v-for="(item, index) in imgList" :key="index" :src="item.fileUrl" />
</a-image-preview-group>
<span v-else>暂无图片</span>
</div>
</a-card>
<div class="sub-title other-info">其他信息</div>
<a-card :bordered="false">
<div class="title-info">
<span>创建日期{{ baseInfo?.createDate }}</span>
<span>创建人{{ baseInfo?.createUserName }}</span>
</div>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" tab="联系人">
<BasicTable @register="registerContactsTable">
<template #toolbar>
<a-button type="primary" @click="handleContactsCreate">新增联系人</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex == 'action'">
<TableAction
:actions="[
{
icon: 'clarity:note-edit-line',
onClick: handleContactsEdit.bind(null, record),
},
{
icon: 'ant-design:delete-outlined',
color: 'error',
onClick: handleContactsDelete.bind(null, record),
},
]"
/>
</template>
</template>
</BasicTable>
</a-tab-pane>
<a-tab-pane key="4" tab="操作记录">
<BasicTable @register="registerLogTable" />
</a-tab-pane>
</a-tabs>
</div>
<FollowModal @register="registerFollowModal" @success="reload('follow')" />
<ContactsModal @register="registerContactsModal" @success="reload('contact')" />
</template>
<script lang="ts" setup>
import { onMounted, ref, createVNode, watch } from 'vue';
import { BasicTable, TableAction, useTable, BasicColumn, FormSchema } from '/@/components/Table';
import { getDetailInfo, deleteFollowCustomer, deleteContacts } from '/@/api/erp/customer/list';
import { getFileList } from '/@/api/system/file';
import { useModal } from '/@/components/Modal';
import FollowModal from './FollowModal.vue';
import ContactsModal from './ContactsModal.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { isNil } from 'lodash-es';
const contactsColumns: BasicColumn[] = [
{
dataIndex: 'name',
title: '联系人',
align: 'center',
},
{
dataIndex: 'phone',
title: '电话',
align: 'center',
},
{
dataIndex: 'post',
title: '职位',
align: 'center',
},
{
dataIndex: 'dept',
title: '部门',
align: 'center',
},
{
dataIndex: 'createDate',
title: '创建时间',
align: 'center',
},
{
dataIndex: 'createUserName',
title: '创建人',
align: 'center',
},
{
dataIndex: 'isDefault',
title: '是否默认',
align: 'center',
customRender: ({ text }) => {
return text ? '是' : '否';
},
},
];
const followColumns: BasicColumn[] = [
{
dataIndex: 'followTime',
title: '跟进时间',
align: 'center',
},
{
dataIndex: 'nextFollowTime',
title: '下次跟进时间',
align: 'center',
},
{
dataIndex: 'followTypeName',
title: '跟进方式',
align: 'center',
},
{
dataIndex: 'content',
title: '说明',
align: 'center',
},
{
dataIndex: 'createUserName',
title: '跟进人',
align: 'center',
},
];
const logColumns: BasicColumn[] = [
{
dataIndex: 'operateUserAccount',
title: '操作人',
align: 'center',
},
{
dataIndex: 'createDate',
title: '操作时间',
align: 'center',
},
{
dataIndex: 'executeResultJson',
title: '操作内容',
align: 'center',
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'keyword',
label: '',
component: 'Input',
colProps: { span: 4 },
componentProps: {
placeholder: '请输入要查询的关键字',
},
},
];
const { t } = useI18n();
const props = defineProps({
id: String,
isCommon: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['returnPage']);
const followInfoDataSource = ref<any>([]);
const contactsInfoDataSource = ref<any>([]);
const logInfoDataSource = ref<any>([]);
const activeKey = ref('1');
const baseInfo = ref();
const imgList = ref<any>([]);
const { notification } = useMessage();
const [registerFollowModal, { openModal: openFollowModal }] = useModal();
const [registerContactsModal, { openModal: openContactsModal }] = useModal();
watch(
() => props.isCommon,
(val) => {
activeKey.value = val ? '2' : '1';
},
{
immediate: true,
},
);
const [registerFollowTable, { reload: reloadFollow }] = useTable({
dataSource: followInfoDataSource,
rowKey: 'id',
columns: followColumns,
bordered: true,
pagination: false,
actionColumn: {
width: 120,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
},
});
const [registerContactsTable, { reload: reloadContact }] = useTable({
dataSource: contactsInfoDataSource,
rowKey: 'id',
columns: contactsColumns,
bordered: true,
pagination: false,
actionColumn: {
width: 120,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
},
});
const [registerLogTable, { setProps }] = useTable({
dataSource: logInfoDataSource,
rowKey: 'id',
columns: logColumns,
handleSearchInfoFn(info) {
const filterData = logInfoDataSource.value.filter((item) => {
for (const key in item) {
if (!isNil(item[key]) && item[key].toString().indexOf(info.keyword) > -1) {
return item;
}
}
});
setProps({ dataSource: filterData });
return info;
},
formConfig: {
schemas: searchFormSchema,
},
useSearchForm: true,
bordered: true,
pagination: false,
});
onMounted(() => {
getInfo();
});
const getInfo = async () => {
const res = await getDetailInfo(props.id!);
baseInfo.value = res?.caseErpCustomerVo;
followInfoDataSource.value = res?.caseErpCustomerFollowInfoVoList;
contactsInfoDataSource.value = res?.caseErpCustomerContactsInfoVoList;
logInfoDataSource.value = res?.caseErpLogList.map((x) => {
return {
operateUserAccount: x.operateUserAccount,
createDate: x.createDate,
executeResultJson: x.executeResultJson,
};
});
if (baseInfo.value.filePath) {
imgList.value = await getFileList({ folderId: baseInfo.value.filePath });
}
};
const reload = (type) => {
getInfo();
type === 'follow' ? reloadFollow() : reloadContact();
};
const handleFollowCreate = () => {
openFollowModal(true, {
record: baseInfo.value,
});
};
const handleFollowEdit = (record) => {
openFollowModal(true, {
record: baseInfo.value,
id: record.id,
isUpdate: true,
});
};
const handleFollowDelete = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: t('是否确认删除?'),
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await deleteFollowCustomer(record.id);
notification.success({
message: t('提示'),
description: t('删除成功'),
});
reload('follow');
},
onCancel() {},
});
};
const handleContactsCreate = () => {
openContactsModal(true, {
customerId: props.id,
isUpdate: false,
});
};
const handleContactsEdit = (record) => {
openContactsModal(true, {
customerId: props.id,
id: record.id,
isUpdate: true,
});
};
const handleContactsDelete = (record) => {
Modal.confirm({
title: t('提示信息'),
icon: createVNode(ExclamationCircleOutlined),
content: t('是否确认删除?'),
okText: t('确认'),
cancelText: t('取消'),
async onOk() {
await deleteContacts(record.id);
notification.success({
message: t('提示'),
description: t('删除成功'),
});
reload('contact');
},
onCancel() {},
});
};
</script>
<style lang="less" scoped>
.info-box {
background-color: #fff;
width: 100%;
height: 100%;
padding: 16px;
margin-right: 8px;
.title-name {
font-size: 18px;
color: #444;
font-weight: 700;
margin-bottom: 25px;
}
.title-info span {
margin-right: 40px;
}
.sub-title {
font-weight: 700;
margin-bottom: 24px;
}
.other-info {
margin-top: 24px;
}
}
:deep(.ant-image) {
width: 100px;
height: 75px;
padding: 5px;
border: 1px solid silver;
border-radius: 4px;
margin-right: 10px;
}
:deep(.ant-image-img) {
height: 100%;
}
:deep(.ant-divider) {
height: 1px;
background-color: #dcdfe6;
}
:deep(.ant-card-body) {
background-color: #f8f8f8;
}
:deep(.ant-tabs) {
height: 70%;
}
:deep(.ant-tabs-content) {
height: 100%;
}
</style>

View File

@ -0,0 +1,305 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:width="800"
:body-style="{
height: '400px !important',
}"
:title="getTitle"
@ok="handleSubmit"
>
<div class="sub-title">基本信息</div>
<BasicForm @register="registerForm">
<template #imgUpload="{ model }">
<Upload v-model:value="model.filePath" listType="picture" />
</template>
</BasicForm>
<div class="config-title">
<span class="font-semibold">联系人</span>
<span class="add-span" @click="addConfig">+ 增加</span>
</div>
<a-radio-group v-model:value="defaultIndex" style="width: 100%">
<a-form
ref="formRef"
:model="personList"
name="personList"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
autocomplete="off"
>
<template v-for="(item, index) in personList.list" :key="index">
<a-row style="margin: 0 0 20px 10px">
<a-col :span="20" style="color: #999">联系人{{ index + 1 }}</a-col>
<a-col :span="4" align="right">
<a-radio :value="index">默认</a-radio>
<span @click="deleteConfig(index)" class="delete-span">删除</span>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item
label="姓名"
:name="['list', index, 'name']"
:rules="{ required: true, message: '请输入姓名', trigger: 'change' }"
>
<a-input v-model:value="item.name" placeholder="请输入姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="电话" :name="['list', index, 'phone']">
<a-input v-model:value="item.phone" placeholder="请输入电话" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="职位" :name="['list', index, 'post']">
<a-input v-model:value="item.post" placeholder="请输入职位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="部门" :name="['list', index, 'dept']">
<a-input v-model:value="item.dept" placeholder="请输入部门" />
</a-form-item>
</a-col>
</a-row>
</template>
</a-form>
</a-radio-group>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { FormSchema } from '/@/components/Table';
import Upload from '/@/components/Form/src/components/Upload.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import { addCustomer, updateCustomer, getCustomerInfo } from '/@/api/erp/customer/list';
const { t } = useI18n();
const FormSchema: FormSchema[] = [
{
field: 'name',
label: '客户名称',
component: 'Input',
required: true,
colProps: { span: 12 },
componentProps: {
placeholder: '请输入客户名称',
},
},
{
field: 'typeId',
label: '客户类型',
component: 'DicSelect',
required: true,
colProps: { span: 12 },
componentProps: {
placeholder: '请选择客户类型',
itemId: '1679007059387240450',
isShowAdd: false,
getPopupContainer: () => document.body,
},
},
{
field: 'industry',
label: '所在行业',
component: 'Input',
required: true,
colProps: { span: 12 },
componentProps: {
placeholder: '请输入所在行业',
},
},
{
field: 'sourceId',
label: '来源',
component: 'DicSelect',
required: true,
colProps: { span: 12 },
componentProps: {
placeholder: '请选择来源',
itemId: '1679008505423876097',
isShowAdd: false,
getPopupContainer: () => document.body,
},
},
{
field: 'scaleId',
label: '规模',
component: 'DicSelect',
colProps: { span: 12 },
componentProps: {
placeholder: '请选择规模',
itemId: '1679009315172012034',
isShowAdd: false,
getPopupContainer: () => document.body,
},
},
{
field: 'natureId',
label: '公司性质',
component: 'DicSelect',
colProps: { span: 12 },
componentProps: {
placeholder: '请选择公司性质',
itemId: '1679010661178691585',
isShowAdd: false,
getPopupContainer: () => document.body,
},
},
{
field: 'address',
label: '地址',
component: 'Input',
required: true,
colProps: { span: 24 },
componentProps: {
placeholder: '请输入地址',
},
},
{
field: 'filePath',
label: '图片上传',
component: 'Upload',
slot: 'imgUpload',
colProps: { span: 24 },
},
];
const { notification } = useMessage();
const isUpdate = ref(true);
const rowId = ref('');
const state = ref();
const defaultIndex = ref(0);
const formRef = ref();
const personList = ref<any>({
list: [
{
isDefault: false,
dept: '',
name: '',
phone: '',
post: '',
},
],
});
const emit = defineEmits(['success', 'register']);
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 100,
schemas: FormSchema,
showActionButtonGroup: false,
actionColOptions: {
span: 23,
},
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: false, destroyOnClose: true });
isUpdate.value = !!data?.isUpdate;
state.value = data?.state;
defaultIndex.value = 0;
if (unref(isUpdate)) {
rowId.value = data.id;
const record = await getCustomerInfo(data.id);
setFieldsValue({ ...record });
personList.value.list = [...record.caseErpCustomerContacts];
personList.value.list.some((x, idx) => {
if (x.isDefault) {
defaultIndex.value = idx;
return true;
}
return false;
});
} else {
personList.value = {
list: [
{
isDefault: false,
dept: '',
name: '',
phone: '',
post: '',
},
],
};
}
});
const getTitle = computed(() => (!unref(isUpdate) ? '新增客户' : '编辑客户'));
const addConfig = () => {
personList.value.list.push({ isDefault: false, dept: '', name: '', phone: '', post: '' });
};
const deleteConfig = (index) => {
personList.value.list.splice(index, 1);
};
async function handleSubmit() {
try {
const values = await validate();
await formRef.value.validate();
if (personList.value.list.length) {
personList.value.list.map((x, idx) => (x.isDefault = idx === defaultIndex.value ? 1 : 0));
}
const info = {
...values,
state: state.value,
addCaseErpCustomerContactsDtoList: personList.value.list,
};
setModalProps({ confirmLoading: true });
if (!unref(isUpdate)) {
await addCustomer(info);
notification.success({
message: '新增客户',
description: t('成功'),
});
} else {
info.id = rowId.value;
await updateCustomer(info);
notification.success({
message: '编辑客户',
description: t('成功'),
});
}
closeModal();
emit('success');
} catch (error) {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
.sub-title {
font-weight: 700;
margin: 0 18px 18px;
}
.config-title {
display: flex;
justify-content: space-between;
margin: 18px;
font-weight: 700;
.add-span {
color: #5e95ff;
cursor: pointer;
}
}
.delete-span {
color: #ff8080;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:width="800"
title="客户跟进"
@ok="handleSubmit"
>
<div class="sub-title">基本信息</div>
<a-card :bordered="false">
<div class="title-info">
<span>客户名称{{ baseInfo?.name }}</span>
<span>客户类型{{ baseInfo?.typeName }}</span>
<span>所在行业{{ baseInfo?.industry }}</span>
<span>来源{{ baseInfo?.sourceName }}</span>
</div>
</a-card>
<div class="sub-title">客户跟进</div>
<BasicForm @register="registerForm">
<template #imgUpload="{ model }">
<Upload v-model:value="model.filePath" listType="picture" />
</template>
</BasicForm>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { FormSchema } from '/@/components/Table';
import Upload from '/@/components/Form/src/components/Upload.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import { addFollowCustomer, updateFollowCustomer, getFollowInfo } from '/@/api/erp/customer/list';
const { t } = useI18n();
const FormSchema: FormSchema[] = [
{
field: 'followTypeId',
label: '跟进方式',
component: 'DicSelect',
required: true,
colProps: { span: 12 },
componentProps: {
placeholder: '请选择跟进方式',
itemId: '1679011374365560833',
isShowAdd: false,
getPopupContainer: () => document.body,
},
},
{
field: 'followTime',
label: '跟进时间',
component: 'DatePicker',
required: true,
colProps: { span: 12 },
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择跟进时间',
},
},
{
field: 'nextFollowTime',
label: '下次跟进',
component: 'DatePicker',
colProps: { span: 24 },
componentProps: {
format: 'YYYY-MM-DD',
placeholder: '请选择下次跟进时间',
},
},
{
field: 'content',
label: '说明',
component: 'Input',
colProps: { span: 24 },
componentProps: {
placeholder: '请输入说明',
},
},
{
field: 'filePath',
label: '图片上传',
component: 'Upload',
slot: 'imgUpload',
colProps: { span: 24 },
},
];
const { notification } = useMessage();
const baseInfo = ref();
const isUpdate = ref(false);
const rowId = ref('');
const emit = defineEmits(['success', 'register']);
const [registerForm, { validate, setFieldsValue }] = useForm({
labelWidth: 100,
schemas: FormSchema,
showActionButtonGroup: false,
actionColOptions: {
span: 23,
},
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: false, destroyOnClose: true });
isUpdate.value = !!data?.isUpdate;
baseInfo.value = data.record;
if (unref(isUpdate)) {
rowId.value = data.id;
const record = await getFollowInfo(data.id);
setFieldsValue({ ...record });
}
});
async function handleSubmit() {
try {
const values = await validate();
values.customerId = baseInfo.value.id;
setModalProps({ confirmLoading: true });
if (!unref(isUpdate)) {
await addFollowCustomer(values);
notification.success({
message: '新增跟进客户',
description: t('成功'),
});
} else {
values.id = rowId.value;
await updateFollowCustomer(values);
notification.success({
message: '编辑跟进客户',
description: t('成功'),
});
}
closeModal();
emit('success');
} catch (error) {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
.sub-title {
font-weight: 700;
margin: 0 18px 18px;
}
.title-info span {
margin-right: 40px;
}
:deep(.ant-card-body) {
background-color: #f8f8f8;
}
</style>