5 changed files with 883 additions and 0 deletions
@ -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 } |
|||
}); |
|||
}; |
|||
@ -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 }); |
|||
}; |
|||
@ -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> |
|||
@ -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…
Reference in new issue