All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。 Co-authored-by: Cursor <cursoragent@cursor.com>
470 lines
15 KiB
Vue
470 lines
15 KiB
Vue
<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://...' \ --header 'User-Agent: ...' \ --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>
|