front/src/views/api/upstreamInterface/components/UpstreamInterfaceModal.vue
zzh b9702cd0bc
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
fix: 订单详情编辑体验、接口参数与价格表批量填价
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 16:27:22 +08:00

470 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { cloneDeep } from 'lodash';
import { Message } from '@arco-design/web-vue';
import axios from 'axios';
import { rewriteBrowserApiPath } from '@/utils/rewriteBrowserApiPath';
import { postInterfaceSaveInterface } from '@/api/InterfaceServices/interface';
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
import { $baseMessage } from '@/utils';
import { useI18n } from 'vue-i18n';
import InterfaceDetail from '@/components/InterfaceDetail/index.vue';
import { parseCurlCommand } from '@/utils/parseCurl';
import { formatSmartFillMessage } from '@/utils/curlToInterfaceFill';
import { formItems, formRules } from '../config/form-config';
import {
buildInterfaceSavePayload,
hydrateDetailParamsFromInterface,
validateInterfaceSavePayload,
} from '@/utils/interfaceParameterModel';
const emit = defineEmits(['close', 'saved']);
const { t } = useI18n();
defineOptions({
name: 'UpstreamInterfaceModal',
});
// 组件引用
const interfaceDetailRef = ref<any>(); // InterfaceDetail组件引用
// 参数数据
const queryParams = ref<any[]>([]); // Query参数列表
const requestParams = ref<any[]>([]); // Request参数列表
const responseParams = ref<any[]>([]); // Response参数列表
// 编辑模式标识
const isEditMode = ref<boolean>(false);
// 添加类型注解的响应式对象
let tableForm = reactive<any>({
optionType: 1,
id: 0,
name: '',
abbreviation: '',
code: '',
requestAddress: '',
description: '',
isBatch: true,
category: 1, // 接口分组默认值为1(订单接口)
requestProtocol: undefined,
requestMethod: 2, // 默认 POST
contentType: undefined,
responseContentType: undefined,
qps: 1,
mappedParamType: undefined,
inParameterList: [],
outParameterList: [],
queryParameterList: [],
inParams: '', // 请求示例
outParams: '', // 返回示例
isUpStream: true, // 上游接口默认设置为 true
});
// 控制表单渲染的标志,只有在 init 被调用后才渲染表单,避免 SelectPro 自动请求字典数据
const shouldRenderForm = ref(false);
// 防止重复初始化
const isInitializing = ref(false);
const lastInitId = ref<number | null>(null);
// 上游接口的表单配置(使用 form-config.ts 中的配置)
const upstreamFormItems = formItems.map(item => {
// 确保 label 字段被保留
const mappedItem: any = {
...item,
label: item.label, // 明确保留 label
};
// 如果 compProps 是函数,需要特殊处理
if (typeof item.compProps === 'function') {
mappedItem.compProps = (params: any) => {
const originalProps = (item.compProps as any)(params);
return {
...originalProps,
style: originalProps?.style || { width: '100%' },
};
};
} else {
// compProps 是对象
mappedItem.compProps = {
...item.compProps,
style: item.compProps?.style || { width: '100%' },
};
}
return mappedItem;
});
// 上游接口的验证规则(使用 form-config.ts 中的规则)
const upstreamFormRules = formRules;
// 初始化函数;forceRefresh 用于保存后强制重拉,切 tab 回来不重复 init
async function init(data?: {
id?: number | string;
interfaceId?: number | string;
forceRefresh?: boolean;
}) {
// 优先使用传入的参数
let idRaw: any = null;
if (data?.id || data?.interfaceId) {
idRaw = data.id || data.interfaceId;
}
if (!idRaw) {
// 新增模式
shouldRenderForm.value = true;
tableForm.id = 0;
tableForm.optionType = 1;
tableForm.category = 1; // 接口分组默认值为1(订单接口)
tableForm.isUpStream = true; // 上游接口默认设置为 true
tableForm.requestMethod = 2; // 默认 POST
tableForm.qps = 1;
lastInitId.value = null;
await nextTick();
if (interfaceDetailRef.value) {
interfaceDetailRef.value.setBasicFormValues({ ...tableForm });
interfaceDetailRef.value.captureOriginalParamIds?.();
}
return;
}
const id = Number(idRaw);
const forceRefresh = !!data?.forceRefresh;
// 正在加载同一接口:忽略重复调用
if (isInitializing.value && lastInitId.value === id) {
return;
}
// 已加载同一接口且非强制刷新(例如明细 ↔ 调试来回切):跳过重拉,避免误报未保存
if (!forceRefresh && lastInitId.value === id && shouldRenderForm.value) {
return;
}
// 标记正在初始化和当前ID
isInitializing.value = true;
lastInitId.value = id;
isEditMode.value = true;
try {
// 标记可以渲染表单了
shouldRenderForm.value = true;
// 直接使用axios调用,避免request.ts的拦截器问题
const token = localStorage.getItem('token');
const response = await axios.get(
rewriteBrowserApiPath(`/api/Interface/GetInterfaceById?id=${id}`),
{
headers: {
'Authorization': token ? `Bearer ${token}` : '',
'Content-Type': 'application/json;charset=utf-8',
'Accept-Language': 'zh-CN'
},
timeout: 30000, // 30秒超时
});
// 处理 API 返回的数据格式
const apiData = response.data;
// 确保 tableForm 先被赋值
Object.assign(tableForm, {
...apiData,
optionType: apiData.optionType || 3,
id: apiData.id || 0,
category: apiData.category || 1, // 接口分组默认值为1(订单接口)
inParameterList: apiData.inParameterList || [],
outParameterList: apiData.outParameterList || [],
queryParameterList: apiData.formParameterList || [],
isUpStream: apiData.isUpStream !== undefined ? apiData.isUpStream : true, // 上游接口默认设置为 true
});
const hydrated = hydrateDetailParamsFromInterface(apiData);
queryParams.value = hydrated.queryParams;
requestParams.value = hydrated.requestParams.length
? hydrated.requestParams
: (apiData.inParameterList || []);
responseParams.value = hydrated.responseParams;
// 设置请求示例和返回示例
if (apiData.inParams) {
// 这个会在 InterfaceDetail 组件中处理
}
if (apiData.outParams) {
// 这个会在 InterfaceDetail 组件中处理
}
// 等待组件渲染完成
await nextTick();
await nextTick();
await new Promise(resolve => setTimeout(resolve, 200));
// 再次检查组件是否还存在(可能在等待过程中组件被卸载)
if (interfaceDetailRef.value && interfaceDetailRef.value.setBasicFormValues) {
try {
interfaceDetailRef.value.setBasicFormValues({ ...tableForm });
interfaceDetailRef.value.captureOriginalParamIds?.({
queryParams: queryParams.value,
requestParams: requestParams.value,
responseParams: responseParams.value,
});
} catch (error) {
}
}
// 重置初始化标志(成功时)
isInitializing.value = false;
} catch (error: any) {
// 重置初始化标志(失败时)
isInitializing.value = false;
$baseMessage('获取接口数据失败', 'error');
}
}
// 保存函数;skipSuccessToast 用于父级自行弹出「是否去发布」确认框时避免重复提示
async function saveInterface(options?: { skipSuccessToast?: boolean }) {
try {
if (!interfaceDetailRef.value) {
notifySaveError('接口明细组件未初始化');
}
const isValid = await interfaceDetailRef.value.validateBasicForm();
if (isValid === false) {
notifySaveError('请填写所有必填项');
}
const formData = cloneDeep(interfaceDetailRef.value.getBasicFormValues());
if (!formData) {
notifySaveError('请填写所有必填项');
}
Object.keys(formData).forEach((key) => {
if (formData[key] === undefined) formData[key] = '';
});
await interfaceDetailRef.value.getRequestTableRef()?.clearActived?.();
await interfaceDetailRef.value.getResponseTableRef()?.clearActived?.();
const detailValidateMsg = interfaceDetailRef.value.validateDetailParamsBeforeSave?.();
if (detailValidateMsg) {
notifySaveError(detailValidateMsg);
}
const saveData = buildInterfaceSavePayload({
formData,
mode: 'upstream',
isEditMode: isEditMode.value,
snapshot: interfaceDetailRef.value.getSaveSnapshot(),
extraFields: {
id: isEditMode.value ? (formData.id || tableForm.id) : undefined,
isUpStream: true,
outParams: interfaceDetailRef.value.getResponseExample() || '',
},
});
const validateMsg = validateInterfaceSavePayload(saveData, 'upstream');
if (validateMsg) {
notifySaveError(validateMsg);
}
await postInterfaceSaveInterface(saveData as any);
if (!options?.skipSuccessToast) {
$baseMessage('保存成功,如需生效请去发布', 'success');
}
interfaceDetailRef.value?.captureDirtyBaseline?.();
emit('saved');
return true;
} catch (error: any) {
if (!error?.alreadyNotified) {
$baseMessage('保存失败', 'error');
}
throw error;
}
}
function notifySaveError(message: string): never {
$baseMessage(message, 'error');
const err: any = new Error(message);
err.alreadyNotified = true;
throw err;
}
const smartFillVisible = ref(false);
const smartFillCurl = ref('');
const smartFillLoading = ref(false);
function openSmartFillModal() {
const existing = interfaceDetailRef.value?.getRequestExample?.();
smartFillCurl.value = typeof existing === 'string' && existing.trim() ? existing.trim() : '';
smartFillVisible.value = true;
}
async function handleSmartFillConfirm() {
const curlText = smartFillCurl.value.trim();
if (!curlText) {
Message.warning('请粘贴 cURL 命令');
return;
}
try {
parseCurlCommand(curlText);
} catch (err: unknown) {
Message.error(err instanceof Error ? err.message : 'cURL 格式不正确');
return;
}
if (!interfaceDetailRef.value?.applyCurlSmartFill) {
Message.error('接口明细组件未就绪');
return;
}
smartFillLoading.value = true;
try {
const result = await interfaceDetailRef.value.applyCurlSmartFill(curlText);
await nextTick();
if (result.requestParamsSnapshot?.length) {
requestParams.value = result.requestParamsSnapshot;
}
const detail = interfaceDetailRef.value;
if (detail) {
const formValues = detail.getBasicFormValues?.() || {};
Object.assign(tableForm, formValues);
queryParams.value = detail.getQueryParams?.() || [];
}
await nextTick();
interfaceDetailRef.value.setBasicFormValues?.({ ...tableForm });
interfaceDetailRef.value.markDirty?.();
Message.success(formatSmartFillMessage(result));
smartFillVisible.value = false;
} catch (err: unknown) {
Message.error(err instanceof Error ? err.message : '智能填充失败');
} finally {
smartFillLoading.value = false;
}
}
// 暴露方法给父组件
defineExpose({
init,
submit: saveInterface,
saveInterface,
getRequestExample: () => interfaceDetailRef.value?.getRequestExample?.() || '',
hasUnsavedChanges: () => interfaceDetailRef.value?.hasUnsavedChanges?.() || false,
confirmLeaveUnsaved: (options?: { content?: string }) =>
interfaceDetailRef.value?.confirmLeaveUnsaved?.(options) ?? Promise.resolve(true),
captureDirtyBaseline: () => interfaceDetailRef.value?.captureDirtyBaseline?.(),
});
</script>
<template>
<div class="upstream-interface-modal">
<!-- 接口明细 -->
<InterfaceDetail
ref="interfaceDetailRef"
parameter-mode="upstream"
:use-form-edit="true"
:show-content-type-selectors="true"
:basic-params="tableForm"
:basic-form-items="upstreamFormItems"
:basic-form-data="tableForm"
:basic-form-rules="upstreamFormRules"
:query-params="queryParams"
:request-params="requestParams"
:response-params="responseParams"
:interface-id="tableForm.id"
:show-query="false"
:show-return-config="false"
@update:basic-form-data="(val) => Object.assign(tableForm, val)"
>
<template #headerActions>
<a-button size="small" title="智能填充" @click="openSmartFillModal">AI</a-button>
<slot name="headerActions" />
</template>
</InterfaceDetail>
<a-modal
v-model:visible="smartFillVisible"
title="智能填充"
:width="800"
:mask-closable="false"
unmount-on-close
class="smart-fill-modal"
@cancel="smartFillVisible = false"
>
<a-spin :loading="smartFillLoading" tip="正在解析并填充...">
<div class="smart-fill-panel">
<p class="smart-fill-tip">
粘贴 cURL 后自动填充 Query / Body / Response 相关结构。签名类参数值建议留空;curl 示例值可写入「描述」供对照。
</p>
<div class="smart-fill-textarea-wrap">
<a-textarea
v-model="smartFillCurl"
class="smart-fill-textarea"
placeholder="curl --location --request POST 'https://...' \&#10;--header 'User-Agent: ...' \&#10;--data-raw '{...}'"
allow-clear
/>
</div>
</div>
</a-spin>
<template #footer>
<a-button @click="smartFillVisible = false">取消</a-button>
<a-button type="primary" :loading="smartFillLoading" @click="handleSmartFillConfirm">
填充到表单
</a-button>
</template>
</a-modal>
</div>
</template>
<style lang="less" scoped>
.upstream-interface-modal {
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
background-color: var(--color-bg-1);
display: flex;
flex-direction: column;
}
.smart-fill-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.smart-fill-tip {
margin: 0;
font-size: 13px;
color: var(--color-text-2);
line-height: 1.6;
flex-shrink: 0;
}
.smart-fill-textarea-wrap {
width: 100%;
aspect-ratio: 1 / 1;
}
.smart-fill-textarea {
width: 100%;
height: 100%;
:deep(.arco-textarea-wrapper) {
width: 100% !important;
height: 100% !important;
min-height: 100% !important;
}
:deep(textarea.arco-textarea) {
width: 100% !important;
height: 100% !important;
min-height: 100% !important;
resize: none;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
line-height: 1.5;
}
}
</style>