feat: 完成CRM-ERP客户同步管理模块
主要功能:
- 新增同步管理后台界面,支持全局管理(不限租户)
- 实现租户-ERP公司映射配置,支持多公司绑定
- 实现定时同步任务,可配置Cron表达式
- 实现手动同步功能,从ERP拉取客户数据更新CRM
- 支持新增经销商(ERP客户在CRM不存在时)
- 新增同步日志和预警管理
技术实现:
- 新增 crm_sync_log、crm_sync_alert、sys_tenant_company 表
- 使用 TenantHelper.ignore() 实现全局查询(忽略租户过滤)
- ERP动态API支持 ${param} 字符串替换用于IN条件
- 新增公司列表动态API配置
同步流程:
1. 获取租户-公司映射配置
2. 从ERP获取各公司客户列表
3. 匹配CRM已有经销商并更新
4. 未匹配且ERP未停用的客户新增为经销商
5. 生成差异预警(CRM维护字段)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
hzhub-admin/apps/web-antd/src/api/crm/sync.ts
Normal file
116
hzhub-admin/apps/web-antd/src/api/crm/sync.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
enum Api {
|
||||
config = '/crm/sync/config',
|
||||
start = '/crm/sync/start',
|
||||
stop = '/crm/sync/stop',
|
||||
cron = '/crm/sync/cron',
|
||||
execute = '/crm/sync/execute',
|
||||
logs = '/crm/sync/logs',
|
||||
alerts = '/crm/sync/alerts',
|
||||
stats = '/crm/sync/stats',
|
||||
tenants = '/crm/sync/tenants',
|
||||
tenantCompanies = '/crm/sync/tenant-companies',
|
||||
erpCompanies = '/crm/sync/erp-companies',
|
||||
bindCompany = '/crm/sync/tenant-companies/bind',
|
||||
unbindCompany = '/crm/sync/tenant-companies/unbind',
|
||||
resolveAlert = '/crm/sync/alerts',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步配置
|
||||
*/
|
||||
export function getSyncConfig() {
|
||||
return requestClient.get<{ enabled: boolean; cron: string }>(Api.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时任务
|
||||
*/
|
||||
export function startSyncTask() {
|
||||
return requestClient.post<string>(Api.start, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时任务
|
||||
*/
|
||||
export function stopSyncTask() {
|
||||
return requestClient.post<string>(Api.stop, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Cron表达式
|
||||
*/
|
||||
export function setSyncCron(cron: string) {
|
||||
return requestClient.put<string>(Api.cron, null, { params: { cron } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发同步
|
||||
*/
|
||||
export function executeSync() {
|
||||
return requestClient.post<any>(Api.execute, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询同步日志
|
||||
*/
|
||||
export function getSyncLogs(params: { pageNum: number; pageSize: number }) {
|
||||
return requestClient.get<{ rows: any[]; total: number }>(Api.logs, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询预警列表
|
||||
*/
|
||||
export function getSyncAlerts(params: { pageNum: number; pageSize: number; status?: string }) {
|
||||
return requestClient.get<{ rows: any[]; total: number }>(Api.alerts, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理预警
|
||||
*/
|
||||
export function resolveAlert(alertId: number, action: string, note?: string) {
|
||||
return requestClient.put<string>(`${Api.resolveAlert}/${alertId}`, null, { params: { action, note } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步统计数据
|
||||
*/
|
||||
export function getSyncStats() {
|
||||
return requestClient.get<any>(Api.stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户列表(用于选择器)
|
||||
*/
|
||||
export function getTenants() {
|
||||
return requestClient.get<any[]>(Api.tenants);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有租户的ERP公司映射列表
|
||||
*/
|
||||
export function getTenantCompanies() {
|
||||
return requestClient.get<any[]>(Api.tenantCompanies);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ERP所有公司列表
|
||||
*/
|
||||
export function getErpCompanies() {
|
||||
return requestClient.get<any[]>(Api.erpCompanies);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定ERP公司到租户
|
||||
*/
|
||||
export function bindCompany(tenantId: string, tenantName?: string, erpCompanyId?: string, erpCompanyName?: string) {
|
||||
return requestClient.post<string>(Api.bindCompany, null, { params: { tenantId, tenantName, erpCompanyId, erpCompanyName } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑ERP公司
|
||||
*/
|
||||
export function unbindCompany(id: number) {
|
||||
return requestClient.post<string>(`${Api.unbindCompany}/${id}`, {});
|
||||
}
|
||||
586
hzhub-admin/apps/web-antd/src/views/crm/sync/index.vue
Normal file
586
hzhub-admin/apps/web-antd/src/views/crm/sync/index.vue
Normal file
@@ -0,0 +1,586 @@
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Tabs,
|
||||
message,
|
||||
Popconfirm,
|
||||
Badge,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
} from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined, SyncOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import {
|
||||
getSyncConfig,
|
||||
startSyncTask,
|
||||
stopSyncTask,
|
||||
setSyncCron,
|
||||
executeSync,
|
||||
getSyncLogs,
|
||||
getSyncAlerts,
|
||||
resolveAlert,
|
||||
getSyncStats,
|
||||
getTenants,
|
||||
getTenantCompanies,
|
||||
getErpCompanies,
|
||||
bindCompany,
|
||||
unbindCompany,
|
||||
} from '#/api/crm/sync';
|
||||
|
||||
defineOptions({ name: 'CrmSyncManage' });
|
||||
|
||||
// 同步执行状态
|
||||
const syncing = ref(false);
|
||||
const loading = ref(false);
|
||||
const logs = ref<any[]>([]);
|
||||
const alerts = ref<any[]>([]);
|
||||
const totalLogs = ref(0);
|
||||
const totalAlerts = ref(0);
|
||||
const currentPageLogs = ref(1);
|
||||
const currentPageAlerts = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const alertStatusFilter = ref('PENDING');
|
||||
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 统计数据
|
||||
const stats = ref<any>({
|
||||
lastSyncTime: null,
|
||||
lastSyncDuration: null,
|
||||
lastSyncCount: 0,
|
||||
lastSyncUpdated: 0,
|
||||
lastSyncAlerts: 0,
|
||||
lastSyncErrors: 0,
|
||||
pendingAlerts: 0,
|
||||
boundDealers: 0,
|
||||
});
|
||||
|
||||
// 定时任务状态
|
||||
const taskRunning = ref(false);
|
||||
const currentCron = ref('0 0 2 * * ?');
|
||||
const taskLoading = ref(false);
|
||||
|
||||
// 租户公司映射(全局管理)
|
||||
const tenants = ref<any[]>([]);
|
||||
const tenantCompanies = ref<any[]>([]);
|
||||
const erpCompanies = ref<any[]>([]);
|
||||
const companyLoading = ref(false);
|
||||
const tenantLoading = ref(false);
|
||||
const selectedTenantId = ref<string>('');
|
||||
const selectedTenantName = ref<string>('');
|
||||
const selectedCompanyId = ref<string>('');
|
||||
const selectedCompanyName = ref<string>('');
|
||||
|
||||
// 状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
RUNNING: { text: '同步中', color: 'processing' },
|
||||
COMPLETED: { text: '已完成', color: 'success' },
|
||||
FAILED: { text: '失败', color: 'error' },
|
||||
};
|
||||
|
||||
const syncTypeMap: Record<string, { text: string; color: string }> = {
|
||||
SCHEDULED: { text: '定时同步', color: 'blue' },
|
||||
MANUAL: { text: '手动同步', color: 'orange' },
|
||||
};
|
||||
|
||||
const alertStatusMap: Record<string, { text: string; color: string }> = {
|
||||
PENDING: { text: '待处理', color: 'warning' },
|
||||
ACKNOWLEDGED: { text: '已确认', color: 'default' },
|
||||
RESOLVED: { text: '已处理', color: 'success' },
|
||||
IGNORED: { text: '已忽略', color: 'default' },
|
||||
};
|
||||
|
||||
const alertTypeMap: Record<string, { text: string }> = {
|
||||
CONTACT_DIFF: { text: '联系人差异' },
|
||||
ADDRESS_DIFF: { text: '地址差异' },
|
||||
STATUS_DIFF: { text: '状态差异' },
|
||||
};
|
||||
|
||||
const cronOptions = [
|
||||
{ label: '每 1 小时', value: '0 0 * * * ?' },
|
||||
{ label: '每 2 小时', value: '0 0 0/2 * * ?' },
|
||||
{ label: '每日凌晨 1 点', value: '0 0 1 * * ?' },
|
||||
{ label: '每日凌晨 2 点', value: '0 0 2 * * ?' },
|
||||
{ label: '每日凌晨 3 点', value: '0 0 3 * * ?' },
|
||||
];
|
||||
|
||||
// 同步日志表格列
|
||||
const logColumns = [
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => {
|
||||
const info = statusMap[record.status] || { text: '未知', color: 'default' };
|
||||
return h(Tag, { color: info.color }, () => info.text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '同步类型',
|
||||
key: 'syncType',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => {
|
||||
const info = syncTypeMap[record.syncType] || { text: record.syncType, color: 'default' };
|
||||
return h(Tag, { color: info.color }, () => info.text);
|
||||
},
|
||||
},
|
||||
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 180 },
|
||||
{ title: '结束时间', dataIndex: 'endTime', key: 'endTime', width: 180 },
|
||||
{ title: '耗时(秒)', dataIndex: 'duration', key: 'duration', width: 80 },
|
||||
{ title: '扫描', dataIndex: 'totalCount', key: 'totalCount', width: 80 },
|
||||
{ title: '同步', dataIndex: 'syncedCount', key: 'syncedCount', width: 80 },
|
||||
{ title: '更新', dataIndex: 'updatedCount', key: 'updatedCount', width: 80 },
|
||||
{ title: '预警', dataIndex: 'alertCount', key: 'alertCount', width: 80 },
|
||||
{ title: '错误', dataIndex: 'errorCount', key: 'errorCount', width: 80 },
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 100 },
|
||||
];
|
||||
|
||||
// 预警表格列
|
||||
const alertColumns = [
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => {
|
||||
const info = alertStatusMap[record.status] || { text: record.status, color: 'default' };
|
||||
return h(Tag, { color: info.color }, () => info.text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'alertType',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => {
|
||||
const info = alertTypeMap[record.alertType] || { text: record.alertType };
|
||||
return info.text;
|
||||
},
|
||||
},
|
||||
{ title: '经销商', dataIndex: 'dealerName', key: 'dealerName', width: 150 },
|
||||
{ title: 'ERP编码', dataIndex: 'customerCode', key: 'customerCode', width: 120 },
|
||||
{ title: 'CRM值', dataIndex: 'crmValue', key: 'crmValue', width: 150 },
|
||||
{ title: 'ERP值', dataIndex: 'erpValue', key: 'erpValue', width: 150 },
|
||||
{ title: '描述', dataIndex: 'alertMessage', key: 'alertMessage', width: 200 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
customRender: ({ record }: any) => {
|
||||
if (record.status !== 'PENDING') return null;
|
||||
return h(Space, {}, () => [
|
||||
h(Button, { type: 'link', size: 'small', onClick: () => handleResolveAlert(record.id, 'acknowledge') }, () => '确认'),
|
||||
h(Button, { type: 'link', size: 'small', onClick: () => handleResolveAlert(record.id, 'ignore') }, () => '忽略'),
|
||||
]);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 手动同步
|
||||
async function handleExecuteSync() {
|
||||
Modal.confirm({
|
||||
title: '确认手动同步',
|
||||
icon: () => h(ExclamationCircleOutlined),
|
||||
content: '将同步所有绑定ERP编码的经销商数据,后台异步执行。确定继续吗?',
|
||||
okText: '确定同步',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
syncing.value = true;
|
||||
try {
|
||||
await executeSync();
|
||||
message.success('已启动同步,请查看同步日志');
|
||||
await loadLogs();
|
||||
await loadStats();
|
||||
startPolling();
|
||||
} catch {
|
||||
message.error('启动同步失败');
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 轮询检查同步状态
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollingTimer.value = setInterval(async () => {
|
||||
await loadLogs();
|
||||
const runningLog = logs.value.find((l: any) => l.status === 'RUNNING');
|
||||
if (!runningLog) {
|
||||
stopPolling();
|
||||
await loadStats();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingTimer.value) {
|
||||
clearInterval(pollingTimer.value);
|
||||
pollingTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载同步日志
|
||||
async function loadLogs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getSyncLogs({ pageNum: currentPageLogs.value, pageSize: pageSize.value });
|
||||
logs.value = res?.rows ?? [];
|
||||
totalLogs.value = res?.total ?? 0;
|
||||
} catch (error: any) {
|
||||
message.error('加载同步日志失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载预警列表
|
||||
async function loadAlerts() {
|
||||
try {
|
||||
const res = await getSyncAlerts({ pageNum: currentPageAlerts.value, pageSize: pageSize.value, status: alertStatusFilter.value });
|
||||
alerts.value = res?.rows ?? [];
|
||||
totalAlerts.value = res?.total ?? 0;
|
||||
} catch {
|
||||
message.error('加载预警列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await getSyncStats();
|
||||
stats.value = res ?? {};
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 加载任务状态
|
||||
async function loadTaskStatus() {
|
||||
try {
|
||||
const config = await getSyncConfig();
|
||||
taskRunning.value = config.enabled;
|
||||
currentCron.value = config.cron || '0 0 2 * * ?';
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 加载租户公司映射
|
||||
async function loadTenantCompanies() {
|
||||
try {
|
||||
tenantCompanies.value = await getTenantCompanies() ?? [];
|
||||
} catch {
|
||||
message.error('加载租户公司映射失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载租户列表
|
||||
async function loadTenants() {
|
||||
tenantLoading.value = true;
|
||||
try {
|
||||
tenants.value = await getTenants() ?? [];
|
||||
} catch {
|
||||
message.error('加载租户列表失败');
|
||||
} finally {
|
||||
tenantLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载ERP公司列表
|
||||
async function loadErpCompanies() {
|
||||
companyLoading.value = true;
|
||||
try {
|
||||
erpCompanies.value = await getErpCompanies() ?? [];
|
||||
} catch {
|
||||
message.error('加载ERP公司列表失败');
|
||||
} finally {
|
||||
companyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动/停止定时任务
|
||||
async function handleTaskToggle(checked: boolean) {
|
||||
taskLoading.value = true;
|
||||
try {
|
||||
if (checked) {
|
||||
await startSyncTask();
|
||||
message.success('定时任务已启动');
|
||||
} else {
|
||||
await stopSyncTask();
|
||||
message.success('定时任务已停止');
|
||||
}
|
||||
await loadTaskStatus();
|
||||
} catch {
|
||||
message.error(checked ? '启动失败' : '停止失败');
|
||||
} finally {
|
||||
taskLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置Cron
|
||||
async function handleCronChange(value: string) {
|
||||
try {
|
||||
await setSyncCron(value);
|
||||
message.success('同步频率已更新');
|
||||
currentCron.value = value;
|
||||
} catch {
|
||||
message.error('更新频率失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理预警
|
||||
async function handleResolveAlert(alertId: number, action: string) {
|
||||
try {
|
||||
await resolveAlert(alertId, action);
|
||||
message.success('预警已处理');
|
||||
await loadAlerts();
|
||||
await loadStats();
|
||||
} catch {
|
||||
message.error('处理预警失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定ERP公司
|
||||
async function handleBindCompany() {
|
||||
if (!selectedTenantId.value) {
|
||||
message.warning('请选择租户');
|
||||
return;
|
||||
}
|
||||
if (!selectedCompanyId.value) {
|
||||
message.warning('请选择要绑定的ERP公司');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await bindCompany(selectedTenantId.value, selectedTenantName.value, selectedCompanyId.value, selectedCompanyName.value);
|
||||
message.success('绑定成功');
|
||||
await loadTenantCompanies();
|
||||
selectedTenantId.value = '';
|
||||
selectedTenantName.value = '';
|
||||
selectedCompanyId.value = '';
|
||||
selectedCompanyName.value = '';
|
||||
} catch {
|
||||
message.error('绑定失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 解绑ERP公司
|
||||
async function handleUnbindCompany(id: number) {
|
||||
try {
|
||||
await unbindCompany(id);
|
||||
message.success('已解绑');
|
||||
await loadTenantCompanies();
|
||||
} catch {
|
||||
message.error('解绑失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 选择租户
|
||||
function handleTenantSelect(value: string) {
|
||||
selectedTenantId.value = value;
|
||||
const tenant = tenants.value.find((t: any) => t.tenantId === value);
|
||||
selectedTenantName.value = tenant?.companyName ?? '';
|
||||
}
|
||||
|
||||
// 选择公司
|
||||
function handleCompanySelect(value: string) {
|
||||
selectedCompanyId.value = value;
|
||||
const company = erpCompanies.value.find((c: any) => c.companyCode === value);
|
||||
selectedCompanyName.value = company?.companyName ?? '';
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
function handleLogPageChange(pagination: any) {
|
||||
currentPageLogs.value = pagination.current ?? 1;
|
||||
pageSize.value = pagination.pageSize ?? 10;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
function handleAlertPageChange(pagination: any) {
|
||||
currentPageAlerts.value = pagination.current ?? 1;
|
||||
pageSize.value = pagination.pageSize ?? 10;
|
||||
loadAlerts();
|
||||
}
|
||||
|
||||
// 预警状态筛选
|
||||
function handleAlertStatusChange(value: string) {
|
||||
alertStatusFilter.value = value;
|
||||
currentPageAlerts.value = 1;
|
||||
loadAlerts();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs();
|
||||
loadAlerts();
|
||||
loadStats();
|
||||
loadTaskStatus();
|
||||
loadTenants();
|
||||
loadTenantCompanies();
|
||||
loadErpCompanies();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div style="overflow-y: auto; padding: 16px">
|
||||
<!-- 统计卡片 -->
|
||||
<Card title="同步概览" :bordered="false" style="margin-bottom: 16px">
|
||||
<Row :gutter="16">
|
||||
<Col :span="4">
|
||||
<Statistic title="绑定经销商" :value="stats.boundDealers" />
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Statistic title="待处理预警" :value="stats.pendingAlerts">
|
||||
<template #suffix>
|
||||
<Badge v-if="stats.pendingAlerts > 0" color="warning" />
|
||||
</template>
|
||||
</Statistic>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Statistic title="最近同步" :value="stats.lastSyncCount" suffix="条" />
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Statistic title="更新数量" :value="stats.lastSyncUpdated" suffix="条" />
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Statistic title="新增预警" :value="stats.lastSyncAlerts" suffix="条" />
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Statistic title="耗时" :value="stats.lastSyncDuration" suffix="秒" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<Card title="同步操作" :bordered="false" style="margin-bottom: 16px">
|
||||
<Space>
|
||||
<Button type="primary" :loading="syncing" @click="handleExecuteSync">
|
||||
<template #icon><SyncOutlined /></template>
|
||||
手动同步
|
||||
</Button>
|
||||
<span style="margin-left: 24px">启用定时同步:</span>
|
||||
<Switch
|
||||
v-model:checked="taskRunning"
|
||||
:loading="taskLoading"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
@change="handleTaskToggle"
|
||||
/>
|
||||
<span style="margin-left: 24px">同步频率:</span>
|
||||
<Select
|
||||
:value="currentCron"
|
||||
style="width: 180px"
|
||||
:options="cronOptions"
|
||||
@change="handleCronChange"
|
||||
/>
|
||||
<Tag :color="taskRunning ? 'green' : 'default'">
|
||||
{{ taskRunning ? '运行中' : '已停止' }}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<!-- 租户公司映射 -->
|
||||
<Card title="租户-ERP公司映射" :bordered="false" style="margin-bottom: 16px">
|
||||
<Space style="margin-bottom: 16px">
|
||||
<Select
|
||||
v-model:value="selectedTenantId"
|
||||
placeholder="选择租户"
|
||||
style="width: 250px"
|
||||
show-search
|
||||
:loading="tenantLoading"
|
||||
:options="tenants.map((t: any) => ({ label: `${t.companyName} (${t.tenantId})`, value: t.tenantId }))"
|
||||
@change="handleTenantSelect"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="selectedCompanyId"
|
||||
placeholder="选择ERP公司"
|
||||
style="width: 300px"
|
||||
show-search
|
||||
:loading="companyLoading"
|
||||
:options="erpCompanies.map((c: any) => ({ label: `${c.companyName} (${c.companyCode})`, value: c.companyCode }))"
|
||||
@change="handleCompanySelect"
|
||||
/>
|
||||
<Button type="primary" @click="handleBindCompany">绑定</Button>
|
||||
</Space>
|
||||
<Table
|
||||
:columns="[
|
||||
{ title: '租户编号', dataIndex: 'tenantId', width: 120 },
|
||||
{ title: '租户名称', dataIndex: 'tenantName', width: 150 },
|
||||
{ title: 'ERP公司ID', dataIndex: 'erpCompanyId', width: 150 },
|
||||
{ title: 'ERP公司名称', dataIndex: 'erpCompanyName', width: 200 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100, customRender: ({ record }: any) => h(Tag, { color: record.status === '0' ? 'green' : 'default' }, () => record.status === '0' ? '正常' : '停用') },
|
||||
{ title: '操作', key: 'action', width: 100, customRender: ({ record }: any) => h(Popconfirm, { title: '确定解绑?', onConfirm: () => handleUnbindCompany(record.id) }, () => h(Button, { type: 'link', danger: true, size: 'small' }, () => '解绑')) },
|
||||
]"
|
||||
:data-source="tenantCompanies"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<!-- 日志和预警Tab -->
|
||||
<Card :bordered="false">
|
||||
<Tabs>
|
||||
<Tabs.TabPane key="logs" tab="同步日志">
|
||||
<Table
|
||||
:columns="logColumns"
|
||||
:data-source="logs"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: currentPageLogs,
|
||||
pageSize: pageSize,
|
||||
total: totalLogs,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleLogPageChange"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="alerts" tab="预警管理">
|
||||
<Space style="margin-bottom: 16px">
|
||||
<span>状态筛选:</span>
|
||||
<Select
|
||||
v-model:value="alertStatusFilter"
|
||||
style="width: 150px"
|
||||
:options="[
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '待处理', value: 'PENDING' },
|
||||
{ label: '已确认', value: 'ACKNOWLEDGED' },
|
||||
{ label: '已处理', value: 'RESOLVED' },
|
||||
{ label: '已忽略', value: 'IGNORED' },
|
||||
]"
|
||||
@change="handleAlertStatusChange"
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
:columns="alertColumns"
|
||||
:data-source="alerts"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: currentPageAlerts,
|
||||
pageSize: pageSize,
|
||||
total: totalAlerts,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleAlertPageChange"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
Reference in New Issue
Block a user