Browse Source

业务数据同步

dev
zhuibobo 2 months ago
parent
commit
07a5abeb89
  1. 3
      script/sql/zfjd-refactoring-v1/005-2-重新整理二期菜单结构.sql
  2. 75
      zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/biz/index.ts
  3. 54
      zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/interface/index.ts
  4. 365
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/biz/index.vue
  5. 386
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/interface/index.vue

3
script/sql/zfjd-refactoring-v1/005-2-重新整理二期菜单结构.sql

@ -17,6 +17,7 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component,
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (40, '系统工具', 0, 4, 'tool', null, '', 1, 0, '#', '', 'M', '0', '0', '', 'tool', 103, 1, '2026-03-31 20:14:46', null, null, '系统工具目录', 'authService', null);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (2, '系统监控', 0, 5, 'monitor', null, '', 1, 0, '#', '', 'M', '0', '0', '', 'monitor', 103, 1, '2026-03-31 20:14:46', null, null, '系统监控目录', 'authService', null);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (60, '系统对接', 0, 6, 'integration', null, '', 1, 0, '#', '', 'M', '0', '0', '', 'international', 103, 1, '2026-03-31 20:14:46', null, null, '系统对接目录', 'authService', null);
-- INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (70, '系统集成模拟', 0, 7, 'system-integration-mock', null, '', 1, 0, '#', '', 'M', '0', '0', '', 'redis', 103, 1, '2026-03-31 20:14:46', null, null, '系统集成模拟目录', 'authService', null);
-- =============================================
@ -61,6 +62,8 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component,
-- =============================================
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (4001, '代码生成', 40, 1, 'gen', 'tool/gen/index', '', 1, 0, '#', '', 'C', '0', '0', 'tool:gen:list', 'code', 103, 1, '2026-03-31 20:14:46', null, null, '代码生成菜单', 'authService', null);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (4021, '业务数据同步', 40, 3, 'biz', 'efcode/biz/index', '', 1, 0, '#', '', 'C', '0', '0', 'tool:biz:list', 'code', 103, 1, '2026-03-31 20:14:46', null, null, '调用任务', 'authService', null);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (4031, '调用接口', 40, 4, 'interface', 'efcode/interface/index', '', 1, 0, '#', '', 'C', '0', '0', 'tool:biz:list', 'code', 103, 1, '2026-03-31 20:14:46', null, null, '调用接口', 'authService', null);
-- 系统工具 辅助隐藏页面
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, url, target, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark, system_code, type) VALUES (4010, '修改生成配置', 40, 2, 'gen-edit/index/:tableId', 'tool/gen/editTable', '', 1, 1, '#', '', 'C', '1', '0', 'tool:gen:edit', '#', 103, 1, '2026-03-31 20:14:46', null, null, '/tool/gen', 'authService', null);

75
zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/biz/index.ts

@ -0,0 +1,75 @@
import request from '@/utils/request';
/**
*
*/
export const triggerSync = (configKey: string) => {
return request({
url: '/web/biz-sync/trigger/' + configKey,
method: 'post'
});
};
/**
*
*/
export const triggerAllSync = () => {
return request({
url: '/web/biz-sync/trigger-all',
method: 'post'
});
};
/**
* PK
*/
export const triggerForceSync = () => {
return request({
url: '/web/biz-sync/trigger-force',
method: 'post'
});
};
/**
*
*/
export const getTaskList = (params: { configKey?: string; pageNum?: number; pageSize?: number }) => {
return request({
url: '/web/biz-sync/task/list',
method: 'get',
params
});
};
/**
*
*/
export const getLogList = (params: { taskId?: string; configKey?: string; pageNum?: number; pageSize?: number }) => {
return request({
url: '/web/biz-sync/log/list',
method: 'get',
params
});
};
/**
*
*/
export const getRecordList = (params: { configKey?: string; status?: string; pageNum?: number; pageSize?: number }) => {
return request({
url: '/web/biz-sync/record/list',
method: 'get',
params
});
};
/**
*
*/
export const retryRecord = (recordId: number) => {
return request({
url: '/web/biz-sync/record/retry',
method: 'post',
params: { recordId }
});
};

54
zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/interface/index.ts

@ -0,0 +1,54 @@
import request from '@/utils/request';
/** HTTP 方法类型 */
export type InterfaceMethod = 'get' | 'post' | 'put' | 'delete';
/** 接口调用配置(与页面中的 JSON 定义一致) */
export interface InterfaceItem {
/** 显示名称 */
name: string;
/** 分组(通常填 Controller 名称) */
group: string;
/** 请求路径,支持 {占位符} 形式的路径参数,例如 /reserve/check/{targetPlanId} */
url: string;
/** 请求方法 */
method: InterfaceMethod;
/** 描述说明 */
description?: string;
/** 默认参数(get 拼 query,post/put 作 body) */
params?: Record<string, any>;
/** 路径参数默认值,key 与 url 中的 {xxx} 对应 */
pathParams?: Record<string, any>;
/** 是否需要二次确认 */
confirm?: boolean;
}
/** 将 URL 中的 {xxx} 占位符替换为实际值 */
function fillPathParams(url: string, pathParams?: Record<string, any>): string {
if (!pathParams) return url;
return url.replace(/\{(\w+)\}/g, (_, key) => {
const v = pathParams[key];
return v == null ? '' : encodeURIComponent(String(v));
});
}
/**
*
* @param item
* @param overrideParams ( item.params)
* @param overridePathParams ( item.pathParams)
*/
export const invokeInterface = (
item: InterfaceItem,
overrideParams?: Record<string, any>,
overridePathParams?: Record<string, any>
) => {
const method = (item.method || 'get').toLowerCase() as InterfaceMethod;
const url = fillPathParams(item.url, overridePathParams ?? item.pathParams);
const payload = overrideParams ?? item.params ?? {};
if (method === 'get' || method === 'delete') {
return request({ url, method, params: payload });
}
return request({ url, method, data: payload });
};

365
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/biz/index.vue

@ -0,0 +1,365 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
triggerSync,
triggerAllSync,
triggerForceSync,
getTaskList,
getLogList
} from '@/api/efcode/biz';
/**
* 同步配置项列表
* 对应后端 resources/businessdatasyn/*.json 中的每一个配置文件
* 后续新增同步表时,只需在此数组中追加一条即可
*/
const syncConfigs = ref([
{
configKey: 'enforcement_registration',
description: '执法登记记录表双向同步',
v1Table: 'enforcement_registration',
v2Table: 'enforcement_registration',
syncDirection: '双向',
enabled: true
},
{
configKey: 'enterprise_information',
description: '企业信息表双向同步',
v1Table: 'enterprise_information',
v2Table: 'enterprise_information',
syncDirection: '双向',
enabled: false
}
]);
const loading = ref(false);
// ============ ============
const taskDialogVisible = ref(false);
const taskLoading = ref(false);
const currentConfigKey = ref('');
const taskList = ref<any[]>([]);
const taskTotal = ref(0);
const taskQuery = reactive({
configKey: '',
pageNum: 1,
pageSize: 10
});
// ============ ============
const logDialogVisible = ref(false);
const logLoading = ref(false);
const currentTaskId = ref('');
const logList = ref<any[]>([]);
const logTotal = ref(0);
const logQuery = reactive({
taskId: '',
configKey: '',
pageNum: 1,
pageSize: 10
});
/** 触发单个表增量同步 */
const handleTrigger = async (row: any) => {
try {
await ElMessageBox.confirm(
`确认触发【${row.description}】增量同步?`,
'确认',
{ type: 'warning' }
);
loading.value = true;
const res: any = await triggerSync(row.configKey);
ElMessage.success(res.msg || '触发成功');
} catch (e: any) {
if (e !== 'cancel' && e?.action !== 'cancel') {
ElMessage.error(e?.msg || '触发失败');
}
} finally {
loading.value = false;
}
};
/** 触发全部同步 */
const handleTriggerAll = async () => {
try {
await ElMessageBox.confirm('确认触发全部表的增量同步?', '确认', { type: 'warning' });
loading.value = true;
const res: any = await triggerAllSync();
ElMessage.success(res.msg || '全部触发成功');
} catch (e: any) {
if (e !== 'cancel' && e?.action !== 'cancel') {
ElMessage.error(e?.msg || '触发失败');
}
} finally {
loading.value = false;
}
};
/** 强制全量同步 */
const handleForceSync = async () => {
try {
await ElMessageBox.confirm(
'强制全量同步将扫描全部数据,耗时较长,确认执行?',
'警告',
{ type: 'error', confirmButtonText: '确定执行', cancelButtonText: '取消' }
);
loading.value = true;
const res: any = await triggerForceSync();
ElMessage.success(res.msg || '强制全量触发成功');
} catch (e: any) {
if (e !== 'cancel' && e?.action !== 'cancel') {
ElMessage.error(e?.msg || '触发失败');
}
} finally {
loading.value = false;
}
};
/** 查看任务记录 */
const handleViewTask = (row: any) => {
currentConfigKey.value = row.configKey;
taskQuery.configKey = row.configKey;
taskQuery.pageNum = 1;
taskDialogVisible.value = true;
loadTaskList();
};
/** 加载任务列表 */
const loadTaskList = async () => {
taskLoading.value = true;
try {
const res: any = await getTaskList(taskQuery);
const data = res.data;
taskList.value = data?.records || [];
taskTotal.value = data?.total || 0;
} catch (e) {
console.error('加载任务列表失败', e);
} finally {
taskLoading.value = false;
}
};
/** 查看某个任务的执行日志明细 */
const handleViewLog = (task: any) => {
currentTaskId.value = task.taskId;
logQuery.taskId = task.taskId;
logQuery.configKey = '';
logQuery.pageNum = 1;
logDialogVisible.value = true;
loadLogList();
};
/** 加载日志列表 */
const loadLogList = async () => {
logLoading.value = true;
try {
const res: any = await getLogList(logQuery);
const data = res.data;
logList.value = data?.records || [];
logTotal.value = data?.total || 0;
} catch (e) {
console.error('加载日志列表失败', e);
} finally {
logLoading.value = false;
}
};
/** 任务状态标签类型 */
const taskStatusType = (status: string) => {
switch (status) {
case 'SUCCESS': return 'success';
case 'RUNNING': return 'warning';
case 'PARTIAL_FAIL': return 'danger';
case 'FAIL': return 'danger';
case 'ABORTED': return 'info';
default: return 'info';
}
};
/** action 标签类型 */
const actionTagType = (action: string) => {
if (!action) return 'info';
if (action.startsWith('WRITE_')) return 'success';
if (action.startsWith('DELETE_')) return 'danger';
if (action.startsWith('CONFLICT_')) return 'warning';
return 'info';
};
/** 格式化时间 */
const formatTime = (val: string) => {
if (!val) return '-';
return val.replace('T', ' ').substring(0, 19);
};
onMounted(() => {});
</script>
<template>
<div class="app-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<div class="header-title">
<span>业务数据同步管理</span>
<el-tag type="info" size="small" style="margin-left: 8px">
{{ syncConfigs.length }} 个配置
</el-tag>
</div>
<div class="header-actions">
<el-button type="primary" :loading="loading" @click="handleTriggerAll">
全部增量同步
</el-button>
<el-button type="danger" :loading="loading" @click="handleForceSync">
强制全量同步
</el-button>
</div>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
<template #title>
<span>
单个同步配置说明:每条记录对应后端 resources/businessdatasyn/*.json 中的一个 JSON 配置文件,
实际的字段映射/主键/哈希列在 JSON 中定义此页仅提供触发入口
</span>
</template>
</el-alert>
<el-table :data="syncConfigs" border stripe style="width: 100%">
<el-table-column label="配置Key" prop="configKey" width="240" />
<el-table-column label="描述" prop="description" min-width="200" />
<el-table-column label="V1表名" prop="v1Table" width="200" />
<el-table-column label="V2表名" prop="v2Table" width="200" />
<el-table-column label="同步方向" prop="syncDirection" width="90" align="center" />
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
{{ row.enabled ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
link
size="small"
:disabled="!row.enabled"
@click="handleTrigger(row)"
>
增量同步
</el-button>
<el-button type="info" link size="small" @click="handleViewTask(row)">
任务记录
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 任务记录弹窗 -->
<el-dialog
v-model="taskDialogVisible"
:title="`同步任务记录 - ${currentConfigKey}`"
width="1000px"
append-to-body
>
<el-table v-loading="taskLoading" :data="taskList" border stripe size="small">
<el-table-column label="任务ID" prop="taskId" width="220" show-overflow-tooltip />
<el-table-column label="开始时间" width="160">
<template #default="{ row }">{{ formatTime(row.startTime) }}</template>
</el-table-column>
<el-table-column label="结束时间" width="160">
<template #default="{ row }">{{ formatTime(row.endTime) }}</template>
</el-table-column>
<el-table-column label="候选" prop="totalCandidates" width="70" align="center" />
<el-table-column label="成功" prop="successCount" width="70" align="center" />
<el-table-column label="冲突" prop="conflictCount" width="70" align="center" />
<el-table-column label="失败" prop="failCount" width="70" align="center" />
<el-table-column label="状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="taskStatusType(row.status)" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="触发方式" prop="triggerType" width="90" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewLog(row)">
日志
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="taskQuery.pageNum"
v-model:page-size="taskQuery.pageSize"
:total="taskTotal"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
class="mt-4"
@size-change="loadTaskList"
@current-change="loadTaskList"
/>
</el-dialog>
<!-- 日志明细弹窗 -->
<el-dialog
v-model="logDialogVisible"
:title="`同步日志明细 - ${currentTaskId}`"
width="1100px"
append-to-body
>
<el-table v-loading="logLoading" :data="logList" border stripe size="small">
<el-table-column label="主键值" prop="pkValue" width="200" show-overflow-tooltip />
<el-table-column label="动作" width="140" align="center">
<template #default="{ row }">
<el-tag :type="actionTagType(row.action)" size="small">{{ row.action }}</el-tag>
</template>
</el-table-column>
<el-table-column label="归属方" prop="ownerSide" width="80" align="center" />
<el-table-column label="成功" width="70" align="center">
<template #default="{ row }">
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
{{ row.success ? '✓' : '✗' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="错误信息" prop="errorMsg" min-width="200" show-overflow-tooltip />
<el-table-column label="耗时(ms)" prop="costMs" width="90" align="center" />
<el-table-column label="时间" width="160">
<template #default="{ row }">{{ formatTime(row.createTime) }}</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="logQuery.pageNum"
v-model:page-size="logQuery.pageSize"
:total="logTotal"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
class="mt-4"
@size-change="loadLogList"
@current-change="loadLogList"
/>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.app-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-title {
display: flex;
align-items: center;
font-weight: bold;
}
}
.mt-4 {
margin-top: 16px;
}
</style>

386
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/interface/index.vue

@ -0,0 +1,386 @@
<script setup lang="ts" name="EfcodeInterfaceInvoker">
import { ref, computed, reactive } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { invokeInterface, type InterfaceItem } from '@/api/efcode/interface';
/**
* 后端接口调用配置列表
*
* 每一项对应一个可手动触发的后端接口新增接口时,在此数组追加一条即可
* 字段说明见 @/api/efcode/interface 中的 InterfaceItem 定义
*
* 当前示例:AppEnfCpPlanController(/app-enf/cpPlan)
*/
const interfaceConfigs = ref<InterfaceItem[]>([
{
name: '我的计划列表',
group: 'AppEnfCpPlanController',
url: '/app-enf/cpPlan/list',
method: 'get',
description: '查询当前登录用户创建的计划(分页)',
params: { pageNum: 1, pageSize: 10 }
},
{
name: '其他计划列表',
group: 'AppEnfCpPlanController',
url: '/app-enf/cpPlan/otherList',
method: 'get',
description: '本单位全部 + 其他单位公开(仅待预约)',
params: { pageNum: 1, pageSize: 10 }
},
{
name: '预约前置检查',
group: 'AppEnfCpPlanController',
url: '/app-enf/cpPlan/reserve/check/{targetPlanId}',
method: 'get',
description: '预约前置检查,需填写 targetPlanId',
pathParams: { targetPlanId: '' }
},
{
name: '执行预约',
group: 'AppEnfCpPlanController',
url: '/app-enf/cpPlan/reserve/do',
method: 'post',
description: '执行预约,提交 targetPlanId、opMessage',
params: { targetPlanId: '', opMessage: '' },
confirm: true
},
{
name: '计划自动匹配预约',
group: 'AppEnfCpPlanController',
url: '/app-enf/cpPlan/autoMatchReserve',
method: 'get',
description: '手动触发,强制匹配并落库',
confirm: true
}
]);
// ============ ============
const searchKeyword = ref('');
const filteredConfigs = computed(() => {
const kw = searchKeyword.value.trim().toLowerCase();
if (!kw) return interfaceConfigs.value;
return interfaceConfigs.value.filter(
(i) =>
i.name.toLowerCase().includes(kw) ||
i.group.toLowerCase().includes(kw) ||
i.url.toLowerCase().includes(kw) ||
(i.description ?? '').toLowerCase().includes(kw)
);
});
// ============ ============
const invokeDialogVisible = ref(false);
const invokeLoading = ref(false);
const currentItem = ref<InterfaceItem | null>(null);
const currentParams = ref('');
const currentPathParams = ref('');
const currentResponse = ref('');
const currentResponseStatus = ref<'success' | 'error' | ''>('');
/** 请求方法对应的标签颜色 */
const methodTagType = (method: string) => {
switch ((method || '').toLowerCase()) {
case 'get': return 'success';
case 'post': return 'primary';
case 'put': return 'warning';
case 'delete': return 'danger';
default: return 'info';
}
};
/** 打开调用弹窗(可编辑参数后再触发) */
const openInvoke = (row: InterfaceItem) => {
currentItem.value = row;
currentParams.value = row.params ? JSON.stringify(row.params, null, 2) : '';
currentPathParams.value = row.pathParams ? JSON.stringify(row.pathParams, null, 2) : '';
currentResponse.value = '';
currentResponseStatus.value = '';
invokeDialogVisible.value = true;
};
/** 安全解析 JSON 字符串(空串返回 undefined) */
const parseJsonSafe = (text: string, label: string): Record<string, any> | undefined => {
const s = (text || '').trim();
if (!s) return undefined;
try {
const obj = JSON.parse(s);
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error('必须是 JSON 对象');
}
return obj;
} catch (e: any) {
throw new Error(`${label} JSON 解析失败:${e?.message || e}`);
}
};
/** 执行调用 */
const doInvoke = async () => {
if (!currentItem.value) return;
const item = currentItem.value;
let params: Record<string, any> | undefined;
let pathParams: Record<string, any> | undefined;
try {
params = parseJsonSafe(currentParams.value, '请求参数');
pathParams = parseJsonSafe(currentPathParams.value, '路径参数');
} catch (e: any) {
ElMessage.error(e.message);
return;
}
if (item.confirm) {
try {
await ElMessageBox.confirm(`确认调用【${item.name}】?`, '确认', { type: 'warning' });
} catch {
return;
}
}
invokeLoading.value = true;
currentResponse.value = '';
currentResponseStatus.value = '';
try {
const res: any = await invokeInterface(item, params, pathParams);
currentResponse.value = JSON.stringify(res, null, 2);
currentResponseStatus.value = 'success';
ElMessage.success(res?.msg || '调用成功');
} catch (e: any) {
currentResponse.value =
typeof e === 'string' ? e : JSON.stringify({ message: e?.message, ...e }, null, 2);
currentResponseStatus.value = 'error';
} finally {
invokeLoading.value = false;
}
};
// ============ / JSON ============
const rawDialogVisible = ref(false);
const rawJsonText = ref('');
const openRawDialog = () => {
rawJsonText.value = JSON.stringify(interfaceConfigs.value, null, 2);
rawDialogVisible.value = true;
};
/** 保存(仅当前会话生效,不做持久化) */
const applyRawJson = () => {
try {
const arr = JSON.parse(rawJsonText.value);
if (!Array.isArray(arr)) throw new Error('配置必须是数组');
interfaceConfigs.value = arr;
ElMessage.success('已应用(仅当前会话生效)');
rawDialogVisible.value = false;
} catch (e: any) {
ElMessage.error('JSON 解析失败:' + (e?.message || e));
}
};
const copyRawJson = async () => {
try {
await navigator.clipboard.writeText(rawJsonText.value);
ElMessage.success('已复制到剪贴板');
} catch {
ElMessage.warning('复制失败,请手动复制');
}
};
/** 完整请求 URL 预览(仅展示相对路径) */
const previewUrl = computed(() => {
if (!currentItem.value) return '';
let url = currentItem.value.url;
try {
const pp = parseJsonSafe(currentPathParams.value, '路径参数');
if (pp) {
url = url.replace(/\{(\w+)\}/g, (_, k) => (pp[k] == null ? `{${k}}` : String(pp[k])));
}
} catch {
// ,
}
return url;
});
const responseAlertType = computed(() =>
currentResponseStatus.value === 'error' ? 'error' : 'success'
);
</script>
<template>
<div class="app-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<div class="header-title">
<span>后端接口调用</span>
<el-tag type="info" size="small" style="margin-left: 8px">
{{ interfaceConfigs.length }} 个接口
</el-tag>
</div>
<div class="header-actions">
<el-input
v-model="searchKeyword"
placeholder="按名称/分组/URL 过滤"
clearable
style="width: 260px; margin-right: 8px"
/>
<el-button type="primary" @click="openRawDialog">查看/编辑 JSON 配置</el-button>
</div>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
<template #title>
<span>
本页用于手动调用后端接口所有接口在 <code>interfaceConfigs</code> 数组中以 JSON 形式定义,
新增接口时追加一条即可字段:name/group/url/method/params/pathParams/confirm
</span>
</template>
</el-alert>
<el-table :data="filteredConfigs" border stripe style="width: 100%">
<el-table-column label="名称" prop="name" min-width="160" show-overflow-tooltip />
<el-table-column label="分组" prop="group" width="220" show-overflow-tooltip />
<el-table-column label="方法" width="80" align="center">
<template #default="{ row }">
<el-tag :type="methodTagType(row.method)" size="small">
{{ (row.method || 'get').toUpperCase() }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="URL" prop="url" min-width="280" show-overflow-tooltip />
<el-table-column label="描述" prop="description" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openInvoke(row)">
调用
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 调用弹窗 -->
<el-dialog
v-model="invokeDialogVisible"
:title="currentItem ? `调用接口 - ${currentItem.name}` : '调用接口'"
width="820px"
append-to-body
:close-on-click-modal="false"
>
<template v-if="currentItem">
<el-descriptions :column="1" border size="small" style="margin-bottom: 12px">
<el-descriptions-item label="分组">{{ currentItem.group }}</el-descriptions-item>
<el-descriptions-item label="方法">
<el-tag :type="methodTagType(currentItem.method)" size="small">
{{ (currentItem.method || 'get').toUpperCase() }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="URL">
<span style="font-family: Consolas, monospace">{{ previewUrl }}</span>
</el-descriptions-item>
<el-descriptions-item v-if="currentItem.description" label="描述">
{{ currentItem.description }}
</el-descriptions-item>
</el-descriptions>
<el-form label-width="100px" label-position="top">
<el-form-item v-if="currentItem.pathParams" label="路径参数 (JSON)">
<el-input
v-model="currentPathParams"
type="textarea"
:autosize="{ minRows: 3, maxRows: 8 }"
placeholder='例如: { "targetPlanId": "xxx" }'
spellcheck="false"
/>
</el-form-item>
<el-form-item
:label="
(currentItem.method || 'get').toLowerCase() === 'get'
? '查询参数 (JSON)'
: '请求体 (JSON)'
"
>
<el-input
v-model="currentParams"
type="textarea"
:autosize="{ minRows: 4, maxRows: 12 }"
placeholder='例如: { "pageNum": 1, "pageSize": 10 }'
spellcheck="false"
/>
</el-form-item>
<el-form-item v-if="currentResponse" label="响应结果">
<el-alert
:type="responseAlertType"
:closable="false"
:title="currentResponseStatus === 'error' ? '调用失败' : '调用成功'"
style="margin-bottom: 8px"
/>
<el-input
:model-value="currentResponse"
type="textarea"
:autosize="{ minRows: 6, maxRows: 20 }"
readonly
spellcheck="false"
/>
</el-form-item>
</el-form>
</template>
<template #footer>
<el-button @click="invokeDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="invokeLoading" @click="doInvoke">
发起调用
</el-button>
</template>
</el-dialog>
<!-- 原始 JSON 配置弹窗 -->
<el-dialog
v-model="rawDialogVisible"
title="接口配置 JSON"
width="820px"
append-to-body
:close-on-click-modal="false"
>
<el-alert type="warning" :closable="false" style="margin-bottom: 8px">
<template #title>
此处修改仅在当前会话生效,刷新后恢复要长期新增/修改,请编辑
<code>src/views/efcode/interface/index.vue</code> 中的 <code>interfaceConfigs</code>
</template>
</el-alert>
<el-input
v-model="rawJsonText"
type="textarea"
:autosize="{ minRows: 16, maxRows: 30 }"
spellcheck="false"
/>
<template #footer>
<el-button @click="copyRawJson">复制</el-button>
<el-button @click="rawDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="applyRawJson">应用</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.app-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-title {
display: flex;
align-items: center;
font-weight: bold;
}
.header-actions {
display: flex;
align-items: center;
}
}
</style>
Loading…
Cancel
Save