feat: 优化上下游接口明细交互与左侧树层级
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m21s

完善保存后引导发布、顶栏操作与参数区图标化,并区分分组与实体节点样式。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-14 20:18:26 +08:00
parent 33f696bc42
commit a0fea7d001
16 changed files with 825 additions and 211 deletions

View File

@ -141,7 +141,10 @@ axiosInstance.interceptors.response.use(
silentError?: boolean silentError?: boolean
} }
const userStore = useRbacUserStore() const userStore = useRbacUserStore()
const silentError = Boolean(originalRequest?.silentError) // 登录接口由页面内联 errorBox 展示,避免拦截器再弹 Message
const requestUrl = String(originalRequest?.url || '')
const isAuthLoginRequest = /\/rbac\/auth\/login(?:\?|$)/.test(requestUrl)
const silentError = Boolean(originalRequest?.silentError) || isAuthLoginRequest
if (error.response) { if (error.response) {
const status = error.response.status const status = error.response.status

View File

@ -73,8 +73,9 @@ function getLangSupport(lang: string) {
const langSupport = computed(() => getLangSupport(props.language)) const langSupport = computed(() => getLangSupport(props.language))
const scrollExtension = EditorView.theme({ const scrollExtension = EditorView.theme({
'.cm-editor': { height: '100%' }, '&': { height: '100%', maxHeight: '100%' },
'.cm-scroller': { overflow: 'auto', minHeight: '100%' }, '.cm-scroller': { overflow: 'auto' },
'.cm-content': { minHeight: '100%' },
}) })
const themeExtension = computed(() => { const themeExtension = computed(() => {
@ -163,19 +164,24 @@ defineExpose({
.codemirror-container { .codemirror-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto; min-height: 0;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
:deep(.vue-codemirror),
:deep(.cm-editor) { :deep(.cm-editor) {
flex: 1;
width: 100%;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden;
font-size: 14px; font-size: 14px;
} }
:deep(.cm-scroller) { :deep(.cm-scroller) {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
overflow: auto; overflow: auto !important;
} }
} }
</style> </style>

View File

@ -386,9 +386,10 @@
:class="{ :class="{
selected: selectedKeysModel.includes(key), selected: selectedKeysModel.includes(key),
'tree-node--hierarchical': hierarchical, 'tree-node--hierarchical': hierarchical,
'tree-node--branch': hierarchical && getNodeData(key)?.isLeaf !== true, 'tree-node--branch': getNodeData(key)?.isLeaf !== true,
'tree-node--leaf': getNodeData(key)?.isLeaf === true,
'tree-node--with-code': getNodeData(key)?.isLeaf && showNodeCode, 'tree-node--with-code': getNodeData(key)?.isLeaf && showNodeCode,
[`tree-node--depth-${getNodeData(key)?.depth || 1}`]: hierarchical, [`tree-node--depth-${getNodeData(key)?.depth || (getNodeData(key)?.isLeaf ? 2 : 1)}`]: true,
}" }"
@click="hierarchical && selectTreeNode(key)" @click="hierarchical && selectTreeNode(key)"
> >
@ -856,11 +857,35 @@
min-height: var(--clm-node-height); min-height: var(--clm-node-height);
} }
&.tree-node--branch {
height: auto;
min-height: 34px;
max-height: none;
margin: 8px 0 2px;
padding: 6px 8px;
border-color: transparent;
background: transparent;
overflow: visible;
.node-second-row-spacer-cell {
display: none;
}
}
&.tree-node--leaf {
background: var(--color-fill-1);
}
&:hover { &:hover {
background: var(--clm-hover); background: var(--clm-hover);
border-color: rgba(15, 16, 17, 0.1); border-color: rgba(15, 16, 17, 0.1);
} }
&.tree-node--branch:hover {
background: transparent;
border-color: transparent;
}
&.selected { &.selected {
background: var(--clm-selected) !important; background: var(--clm-selected) !important;
border-color: var(--clm-selected-border) !important; border-color: var(--clm-selected-border) !important;
@ -876,16 +901,27 @@
color: var(--clm-muted) !important; color: var(--clm-muted) !important;
} }
&.tree-node--branch .node-title::before { &.tree-node--branch {
background: var(--clm-selected-text); background: transparent !important;
border-color: transparent !important;
.node-title {
color: var(--clm-muted) !important;
font-weight: 600;
}
.node-title::before {
background: var(--app-primary-color);
}
} }
} }
} }
.tree-node--depth-1 .node-title { .tree-node--depth-1.tree-node--branch .node-title {
font-size: 12px; font-size: 12px;
font-weight: 560; font-weight: 600;
letter-spacing: -0.01em; letter-spacing: 0.02em;
color: var(--clm-muted);
} }
.tree-node--depth-2 .node-title { .tree-node--depth-2 .node-title {
@ -906,13 +942,12 @@
.tree-node--branch .node-title::before { .tree-node--branch .node-title::before {
content: ''; content: '';
display: inline-block; display: inline-block;
width: 5px; width: 3px;
height: 5px; height: 12px;
margin-right: 6px; margin-right: 8px;
border-radius: 1.5px; border-radius: 2px;
background: var(--clm-faint); background: color-mix(in srgb, var(--app-primary-color) 55%, var(--clm-faint));
vertical-align: middle; vertical-align: -2px;
transform: translateY(-1px);
} }
.node-subtitle { .node-subtitle {
@ -1062,6 +1097,81 @@
min-height: var(--clm-node-height); min-height: var(--clm-node-height);
} }
/* 分组头:轻量、矮一些,和可点击实体卡片拉开层级 */
&.tree-node--branch {
height: auto;
min-height: 34px;
max-height: none;
margin: 10px 0 4px;
padding: 6px 8px;
border-color: transparent;
background: transparent;
cursor: default;
overflow: visible;
.node-title {
font-size: 12px;
font-weight: 600;
color: var(--clm-muted);
letter-spacing: 0.02em;
}
.node-title::before {
content: '';
display: inline-block;
width: 3px;
height: 12px;
margin-right: 8px;
border-radius: 2px;
background: color-mix(in srgb, var(--app-primary-color) 55%, var(--clm-faint));
vertical-align: -2px;
}
.node-subtitle {
color: var(--clm-faint);
font-size: 11px;
}
.node-second-row-spacer-cell {
display: none;
}
.node-subtitle-cell {
padding-top: 2px;
padding-left: 11px;
}
&:hover {
background: transparent;
border-color: transparent;
}
&.selected {
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
.node-title {
color: var(--clm-muted) !important;
font-weight: 600;
}
.node-subtitle {
color: var(--clm-faint) !important;
}
.node-title::before {
background: var(--app-primary-color);
}
}
}
/* 实体叶子:保持卡片感 */
&.tree-node--leaf {
background: var(--color-fill-1);
border-color: rgba(15, 16, 17, 0.07);
}
&:hover { &:hover {
background: var(--clm-hover); background: var(--clm-hover);
border-color: rgba(15, 16, 17, 0.1); border-color: rgba(15, 16, 17, 0.1);
@ -1073,6 +1183,10 @@
opacity: 1; opacity: 1;
} }
} }
&.tree-node--leaf:hover {
background: var(--clm-hover);
}
} }
.tree-node.selected { .tree-node.selected {

View File

@ -15,6 +15,8 @@ export interface CustomLeftMenuTreeNode {
method?: string; method?: string;
/** 分组等非叶子节点第二行副标题 */ /** 分组等非叶子节点第二行副标题 */
subtitle?: string; subtitle?: string;
/** 树深度:1 分组 / 2+ 实体等 */
depth?: number;
[key: string]: any; [key: string]: any;
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, watch, nextTick, onMounted, onUnmounted, computed } from 'vue'; import { ref, reactive, watch, nextTick, onMounted, onUnmounted, computed } from 'vue';
import { IconDragArrow, IconPlus, IconDelete, IconCopy } from '@arco-design/web-vue/es/icon'; import { IconDragArrow, IconPlus, IconDelete, IconCopy, IconSettings, IconImport, IconEraser } from '@arco-design/web-vue/es/icon';
import FormPro from '@/components/FormPro/index.vue'; import FormPro from '@/components/FormPro/index.vue';
import TablePro from '@/components/TablePro/index.vue'; import TablePro from '@/components/TablePro/index.vue';
import CodeMirror from '@/components/CodeMirror/index.vue'; import CodeMirror from '@/components/CodeMirror/index.vue';
@ -142,7 +142,7 @@
}, },
}); });
const emit = defineEmits(['add-query', 'add-request', 'add-response', 'parse-request', 'parse-response', 'update:basicFormData']); const emit = defineEmits(['add-query', 'add-request', 'add-response', 'parse-request', 'parse-response', 'update:basicFormData', 'return-config-saved']);
const refBasicForm = ref(); const refBasicForm = ref();
const queryTableRef = ref(); const queryTableRef = ref();
@ -198,7 +198,7 @@
title: '操作', title: '操作',
fixed: 'right', fixed: 'right',
align: 'center', align: 'center',
width: 128, width: 140,
slots: { default: 'action' }, slots: { default: 'action' },
}, },
]; ];
@ -540,6 +540,20 @@
responseTreeExpanded.value = next; responseTreeExpanded.value = next;
} }
/** 设置面板:一次展开 / 折叠 Request Body、Mapping Body、Response 参数树 */
function setAllDetailTreesExpand(expanded: string | number | boolean) {
const next = !!expanded;
setBodyTreeExpandAll(next);
setMappingBodyTreeExpandAll(next);
setResponseTreeExpandAll(next);
}
const allDetailTreesExpanded = computed(
() => bodyTreeExpanded.value && mappingBodyTreeExpanded.value && responseTreeExpanded.value,
);
const detailSettingsVisible = ref(false);
watch( watch(
() => props.interfaceId, () => props.interfaceId,
(id, oldId) => { (id, oldId) => {
@ -702,6 +716,21 @@
const responseExample = ref<string>(''); // Demo内容 const responseExample = ref<string>(''); // Demo内容
const responseExampleEditorRef = ref<any>(); // Demo编辑器引用 const responseExampleEditorRef = ref<any>(); // Demo编辑器引用
function relayoutExampleEditors() {
nextTick(() => {
requestExampleEditorRef.value?.getEditor?.()?.requestMeasure?.();
responseExampleEditorRef.value?.getEditor?.()?.requestMeasure?.();
});
}
watch(panelLayout, () => relayoutExampleEditors());
watch(requestTab, (tab) => {
if (tab === 'demo') relayoutExampleEditors();
});
watch(responseTab, (tab) => {
if (tab === 'demo') relayoutExampleEditors();
});
watch([requestExample, responseExample], () => refreshDirtyState()); watch([requestExample, responseExample], () => refreshDirtyState());
// 解析弹窗相关状态 // 解析弹窗相关状态
@ -1288,6 +1317,14 @@
return ['query', 'header', 'fixed', 'body', 'mappingBody', 'demo'].includes(requestTab.value); return ['query', 'header', 'fixed', 'body', 'mappingBody', 'demo'].includes(requestTab.value);
}); });
/** Body 参数树(非 KV)把「清空」放在根节点操作列,不再占顶栏 */
const isBodyTreeClearInRoot = computed(
() =>
requestTab.value === 'body'
&& !isNoneBodyContentType.value
&& !isBodyKvMode.value,
);
const canClearCurrentRequestTab = computed(() => { const canClearCurrentRequestTab = computed(() => {
if (!showRequestTabClearAction.value) return false; if (!showRequestTabClearAction.value) return false;
if (requestTab.value === 'query') return queryTabCount.value > 0; if (requestTab.value === 'query') return queryTabCount.value > 0;
@ -1302,45 +1339,6 @@
return false; return false;
}); });
/** 当前 Request Tab 空态提示(展示在顶部 Tab 行正中) */
const requestHeaderEmptyTip = computed(() => {
if (requestTab.value === 'query' && queryTabCount.value === 0) {
return '暂无 Query 参数,可点击右侧「新增」';
}
if (requestTab.value === 'header' && headerTabCount.value === 0) {
return '暂无 Header 参数,可点击右侧「新增」';
}
if (requestTab.value === 'body' && isNoneBodyContentType.value) {
return '当前为 none,无 HTTP Body';
}
if (requestTab.value === 'body' && isBodyKvMode.value && isBodyKvEmpty.value) {
return '暂无 Body 字段,可点击右侧「新增」';
}
if (
requestTab.value === 'body'
&& !isBodyKvMode.value
&& !isNoneBodyContentType.value
&& isBodyTreeEmpty.value
) {
return '暂无 Body 字段,可「新增」或「导入」';
}
if (requestTab.value === 'mappingBody' && isMappingBodyEmpty.value) {
return '暂无 Mapping Body 字段,可「新增」或「导入」';
}
if (requestTab.value === 'demo') {
return '可复制 cURL,或点顶栏「调试」自动带入';
}
return '';
});
/** 当前 Response Tab 空态提示(展示在顶部 Tab 行正中) */
const responseHeaderEmptyTip = computed(() => {
if (responseTab.value === 'body' && responseBodyTabCount.value === 0) {
return '暂无 Response 字段,可「新增」或「导入」';
}
return '';
});
// 监听 queryParams prop 变化,同步到表格数据 // 监听 queryParams prop 变化,同步到表格数据
watch( watch(
() => props.queryParams, () => props.queryParams,
@ -1539,18 +1537,26 @@
} }
// 保存返回配置 // 保存返回配置;emitSaved 用于按钮保存后引导发布(切 tab 自动保存不引导)
async function saveReturnConfig() { async function saveReturnConfig(options?: { emitSaved?: boolean }) {
if (returnConfigRef.value?.save) { if (returnConfigRef.value?.save) {
try { try {
await returnConfigRef.value.save(); const ok = await returnConfigRef.value.save({
skipSuccessToast: !!options?.emitSaved,
});
if (ok === false) return false;
returnConfigHasChanges.value = false; returnConfigHasChanges.value = false;
// 成功提示由 ReturnConfig 统一发出 if (options?.emitSaved) {
emit('return-config-saved');
}
return true;
} catch (error) { } catch (error) {
// ReturnConfig 内部吞掉错误时可能无提示;仅兜底 // ReturnConfig 内部吞掉错误时可能无提示;仅兜底
$baseMessage('保存失败', 'error'); $baseMessage('保存失败', 'error');
return false;
} }
} }
return false;
} }
// 新增返回配置项 // 新增返回配置项
@ -3333,76 +3339,111 @@
<div class="section-header-right"> <div class="section-header-right">
<div class="section-actions"> <div class="section-actions">
<a-button <a-button
v-if="showRequestTabClearAction" v-if="showRequestTabClearAction && !isBodyTreeClearInRoot"
size="small" size="small"
class="section-icon-btn"
:disabled="!canClearCurrentRequestTab" :disabled="!canClearCurrentRequestTab"
title="清空当前 Tab 内容" title="清空当前 Tab 内容"
@click="confirmClearCurrentRequestTab" @click="confirmClearCurrentRequestTab"
> >
清空 <IconEraser />
</a-button> </a-button>
<template v-if="requestTab === 'query'"> <template v-if="requestTab === 'query'">
<a-button size="small" @click="addNewQueryParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addNewQueryParam">
<IconPlus />
</a-button>
</template> </template>
<template v-else-if="requestTab === 'header'"> <template v-else-if="requestTab === 'header'">
<a-button size="small" @click="addNewHeaderParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addNewHeaderParam">
<IconPlus />
</a-button>
</template> </template>
<template v-else-if="requestTab === 'fixed'"> <template v-else-if="requestTab === 'fixed'">
<a-button size="small" @click="addFixedParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addFixedParam">
<IconPlus />
</a-button>
<a-button size="small" @click="saveFixedParams">保存固定配置</a-button> <a-button size="small" @click="saveFixedParams">保存固定配置</a-button>
</template> </template>
<template v-else-if="requestTab === 'body' && isBodyKvMode"> <template v-else-if="requestTab === 'body' && isBodyKvMode">
<a-button size="small" @click="addNewBodyFormParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addNewBodyFormParam">
<IconPlus />
</a-button>
</template> </template>
<template v-else-if="requestTab === 'body' && !isNoneBodyContentType"> <template v-else-if="requestTab === 'body' && !isNoneBodyContentType">
<a-button size="small" @click="addNewRequestParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addNewRequestParam">
<a-button size="small" @click="drawerClick">导入</a-button> <IconPlus />
</a-button>
<a-button size="small" class="section-icon-btn" title="导入" @click="drawerClick">
<IconImport />
</a-button>
</template> </template>
<template v-else-if="requestTab === 'mappingBody'"> <template v-else-if="requestTab === 'mappingBody'">
<a-button size="small" @click="addNewRequestParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addNewRequestParam">
<a-button size="small" @click="drawerClick">导入</a-button> <IconPlus />
</a-button>
<a-button size="small" class="section-icon-btn" title="导入" @click="drawerClick">
<IconImport />
</a-button>
</template> </template>
<template v-else-if="requestTab === 'demo'"> <template v-else-if="requestTab === 'demo'">
<a-button size="small" @click="copyRequestExampleContent">复制</a-button> <a-button
size="small"
class="section-icon-btn"
title="复制"
@click="copyRequestExampleContent"
>
<IconCopy />
</a-button>
</template> </template>
</div> <a-popover
<div class="header-switches"> v-model:popup-visible="detailSettingsVisible"
<a-switch trigger="click"
v-if="requestTab === 'body' && !isNoneBodyContentType && !isBodyKvMode" position="br"
class="tree-expand-switch" content-class="detail-settings-popover"
:model-value="bodyTreeExpanded" >
checked-text="展开" <a-button
unchecked-text="折叠" size="small"
title="展开 / 折叠全部节点" class="detail-settings-btn"
@change="setBodyTreeExpandAll" title="布局与节点展开设置"
/> >
<a-switch <IconSettings />
v-else-if="requestTab === 'mappingBody'" </a-button>
class="tree-expand-switch" <template #content>
:model-value="mappingBodyTreeExpanded" <div class="detail-settings-panel">
checked-text="展开" <div class="detail-settings-row">
unchecked-text="折叠" <span class="detail-settings-label">请求 / 响应布局</span>
title="展开 / 折叠全部节点" <a-switch
@change="setMappingBodyTreeExpandAll" class="layout-switch"
/> :model-value="panelLayout === 'vertical'"
<a-switch checked-text="上下"
class="layout-switch" unchecked-text="左右"
:model-value="panelLayout === 'vertical'" title="切换请求 / 响应布局"
checked-text="上下" @change="onPanelLayoutSwitch"
unchecked-text="左右" />
title="切换请求 / 响应布局" </div>
@change="onPanelLayoutSwitch" <div class="detail-settings-row">
/> <span class="detail-settings-label">参数树节点</span>
<a-switch
class="tree-expand-switch"
:model-value="allDetailTreesExpanded"
checked-text="展开"
unchecked-text="折叠"
title="展开 / 折叠全部参数树节点(含 Response)"
@change="setAllDetailTreesExpand"
/>
</div>
</div>
</template>
</a-popover>
</div> </div>
</div> </div>
</div> </div>
<div class="section-content" :class="{ 'section-content-fixed-tab': requestTab === 'fixed' }"> <div class="section-content" :class="{ 'section-content-fixed-tab': requestTab === 'fixed' }">
<div <div
v-if="(showContentTypeSelectors && requestTab === 'body') || requestHeaderEmptyTip" v-if="showContentTypeSelectors && requestTab === 'body'"
class="content-type-bar" class="content-type-bar"
> >
<SlidingTabs <SlidingTabs
v-if="showContentTypeSelectors && requestTab === 'body'"
size="mini" size="mini"
class="content-type-tabs" class="content-type-tabs"
:model-value="String(basicFormData?.contentType ?? '')" :model-value="String(basicFormData?.contentType ?? '')"
@ -3410,13 +3451,6 @@
aria-label="Request content type" aria-label="Request content type"
@change="updateContentType" @change="updateContentType"
/> />
<div
v-if="requestHeaderEmptyTip"
class="content-type-empty-tip"
:title="requestHeaderEmptyTip"
>
{{ requestHeaderEmptyTip }}
</div>
</div> </div>
<!-- Query tab 内容 --> <!-- Query tab 内容 -->
<div v-show="requestTab === 'query'" class="query-tab-pane"> <div v-show="requestTab === 'query'" class="query-tab-pane">
@ -3920,12 +3954,24 @@
<template #action="{ row }"> <template #action="{ row }">
<a-space :size="2"> <a-space :size="2">
<a-button type="text" class="row-action-btn" title="新增子节点" @click="addRequestRow(row)"> <a-button
v-if="isBodyRootRow(row)"
type="text"
size="mini"
class="row-action-btn row-clear-btn"
title="清空当前 Tab 内容"
:disabled="!canClearCurrentRequestTab"
@click="confirmClearCurrentRequestTab"
>
<IconEraser />
</a-button>
<a-button type="text" size="mini" class="row-action-btn" title="新增子节点" @click="addRequestRow(row)">
<IconPlus /> <IconPlus />
</a-button> </a-button>
<a-button <a-button
v-if="!isBodyRootRow(row)" v-if="!isBodyRootRow(row)"
type="text" type="text"
size="mini"
class="row-action-btn" class="row-action-btn"
title="复制行" title="复制行"
@click="copyBodyTreeRow(row)" @click="copyBodyTreeRow(row)"
@ -3937,7 +3983,7 @@
content="确定删除该参数?" content="确定删除该参数?"
@ok="deleteRequestRow(row)" @ok="deleteRequestRow(row)"
> >
<a-button type="text" class="row-action-btn" title="删除"> <a-button type="text" size="mini" class="row-action-btn" title="删除">
<IconDelete /> <IconDelete />
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
@ -4076,14 +4122,14 @@
<template #action="{ row }"> <template #action="{ row }">
<a-space :size="2"> <a-space :size="2">
<a-button type="text" class="row-action-btn" title="新增子节点" @click="addRequestRow(row)"> <a-button type="text" size="mini" class="row-action-btn" title="新增子节点" @click="addRequestRow(row)">
<IconPlus /> <IconPlus />
</a-button> </a-button>
<a-button type="text" class="row-action-btn" title="复制行" @click="copyBodyTreeRow(row)"> <a-button type="text" size="mini" class="row-action-btn" title="复制行" @click="copyBodyTreeRow(row)">
<IconCopy /> <IconCopy />
</a-button> </a-button>
<a-popconfirm content="确定删除该参数?" @ok="deleteRequestRow(row)"> <a-popconfirm content="确定删除该参数?" @ok="deleteRequestRow(row)">
<a-button type="text" class="row-action-btn" title="删除"> <a-button type="text" size="mini" class="row-action-btn" title="删除">
<IconDelete /> <IconDelete />
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
@ -4108,7 +4154,7 @@
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
wordWrap: 'on', wordWrap: 'on',
}" }"
:height="600" height="100%"
/> />
</div> </div>
</div> </div>
@ -4128,33 +4174,28 @@
<div class="section-header-right"> <div class="section-header-right">
<div class="section-actions"> <div class="section-actions">
<template v-if="responseTab === 'body'"> <template v-if="responseTab === 'body'">
<a-button size="small" @click="addNewResponseParam">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addNewResponseParam">
<a-button size="small" @click="drawerClick1">导入</a-button> <IconPlus />
</a-button>
<a-button size="small" class="section-icon-btn" title="导入" @click="drawerClick1">
<IconImport />
</a-button>
</template> </template>
<template v-if="responseTab === 'returnConfig'"> <template v-if="responseTab === 'returnConfig'">
<a-button size="small" @click="addReturnConfigItem">新增</a-button> <a-button size="small" class="section-icon-btn" title="新增" @click="addReturnConfigItem">
<a-button size="small" @click="saveReturnConfig">保存返回配置</a-button> <IconPlus />
</a-button>
<a-button size="small" @click="saveReturnConfig({ emitSaved: true })">保存返回配置</a-button>
</template> </template>
</div> </div>
<div v-if="responseTab === 'body'" class="header-switches">
<a-switch
class="tree-expand-switch"
:model-value="responseTreeExpanded"
checked-text="展开"
unchecked-text="折叠"
title="展开 / 折叠全部节点"
@change="setResponseTreeExpandAll"
/>
</div>
</div> </div>
</div> </div>
<div class="section-content"> <div class="section-content">
<div <div
v-if="(showContentTypeSelectors && responseTab === 'body') || responseHeaderEmptyTip" v-if="showContentTypeSelectors && responseTab === 'body'"
class="content-type-bar" class="content-type-bar"
> >
<SlidingTabs <SlidingTabs
v-if="showContentTypeSelectors && responseTab === 'body'"
size="mini" size="mini"
class="content-type-tabs" class="content-type-tabs"
:model-value="String(basicFormData?.responseContentType ?? '')" :model-value="String(basicFormData?.responseContentType ?? '')"
@ -4162,13 +4203,6 @@
aria-label="Response content type" aria-label="Response content type"
@change="updateResponseContentType" @change="updateResponseContentType"
/> />
<div
v-if="responseHeaderEmptyTip"
class="content-type-empty-tip"
:title="responseHeaderEmptyTip"
>
{{ responseHeaderEmptyTip }}
</div>
</div> </div>
<!-- Response body tab 内容 --> <!-- Response body tab 内容 -->
<div v-show="responseTab === 'body'" class="table-content"> <div v-show="responseTab === 'body'" class="table-content">
@ -4318,14 +4352,14 @@
<template #action="{ row }"> <template #action="{ row }">
<a-space :size="2"> <a-space :size="2">
<a-button type="text" class="row-action-btn" title="新增子节点" @click="addResponseRow(row)"> <a-button type="text" size="mini" class="row-action-btn" title="新增子节点" @click="addResponseRow(row)">
<IconPlus /> <IconPlus />
</a-button> </a-button>
<a-button type="text" class="row-action-btn" title="复制行" @click="copyResponseRow(row)"> <a-button type="text" size="mini" class="row-action-btn" title="复制行" @click="copyResponseRow(row)">
<IconCopy /> <IconCopy />
</a-button> </a-button>
<a-popconfirm content="确定删除该参数?" @ok="deleteResponseRow(row)"> <a-popconfirm content="确定删除该参数?" @ok="deleteResponseRow(row)">
<a-button type="text" class="row-action-btn" title="删除"> <a-button type="text" size="mini" class="row-action-btn" title="删除">
<IconDelete /> <IconDelete />
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
@ -4357,7 +4391,7 @@
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
wordWrap: 'on', wordWrap: 'on',
}" }"
:height="600" height="100%"
/> />
</div> </div>
</div> </div>
@ -4694,7 +4728,9 @@
&.layout-vertical { &.layout-vertical {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto minmax(220px, 1.15fr) minmax(180px, 1fr); /* minmax(0, fr):允许面板被压缩,编辑器在内部滚动,避免撑破后被 overflow:hidden 裁切 */
grid-template-rows: auto minmax(0, 1.2fr) minmax(0, 1fr);
overflow: hidden;
} }
} }
@ -4714,6 +4750,24 @@
flex-shrink: 0; flex-shrink: 0;
} }
.detail-settings-btn {
padding: 0 8px !important;
color: var(--color-text-2);
:deep(.arco-icon) {
font-size: 16px;
}
}
.section-icon-btn {
padding: 0 8px !important;
min-width: 32px;
:deep(.arco-icon) {
font-size: 16px;
}
}
.layout-switch, .layout-switch,
.tree-expand-switch { .tree-expand-switch {
flex-shrink: 0; flex-shrink: 0;
@ -4840,24 +4894,6 @@
.content-type-tabs { .content-type-tabs {
flex-shrink: 0; flex-shrink: 0;
} }
.content-type-empty-tip {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -55%);
z-index: 1;
max-width: min(46%, 360px);
padding: 0 8px;
font-size: 12px;
line-height: 20px;
color: var(--color-text-3);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
text-align: center;
}
} }
/* Apifox 式虚拟根节点(仅 Json/Xml 展示,导出剥离) */ /* Apifox 式虚拟根节点(仅 Json/Xml 展示,导出剥离) */
@ -5252,8 +5288,10 @@
.request-example-content { .request-example-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.request-example-container { .request-example-container {
@ -5262,13 +5300,23 @@
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--color-border-2); border: 1px solid var(--color-border-2);
display: flex;
flex-direction: column;
:deep(.codemirror-container) {
flex: 1;
min-height: 0;
height: 100%;
}
} }
.response-example-content { .response-example-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.response-example-container { .response-example-container {
@ -5277,6 +5325,14 @@
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--color-border-2); border: 1px solid var(--color-border-2);
display: flex;
flex-direction: column;
:deep(.codemirror-container) {
flex: 1;
min-height: 0;
height: 100%;
}
} }
.response-return-config-content { .response-return-config-content {
@ -5521,6 +5577,16 @@
:deep(.row-action-btn) { :deep(.row-action-btn) {
color: var(--color-text-3) !important; color: var(--color-text-3) !important;
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
padding: 0 !important;
line-height: 24px !important;
:deep(.arco-icon),
.arco-icon {
font-size: 14px;
}
&:hover, &:hover,
&:focus { &:focus {
@ -5529,6 +5595,39 @@
} }
} }
/* 操作列:避免按钮偏大撑出单元格滚动条(右下角黑点) */
:deep(.vxe-body--column.col--last .vxe-cell),
:deep(.vxe-header--column.col--last .vxe-cell) {
overflow: hidden !important;
scrollbar-width: none;
}
:deep(.vxe-body--column.col--last .vxe-cell::-webkit-scrollbar),
:deep(.vxe-header--column.col--last .vxe-cell::-webkit-scrollbar) {
width: 0 !important;
height: 0 !important;
display: none !important;
}
:deep(.vxe-body--column.col--dirty:before) {
display: none !important;
}
:deep(.row-clear-btn) {
padding: 0 4px !important;
color: var(--color-text-3) !important;
&:hover:not(:disabled),
&:focus:not(:disabled) {
color: #cf1322 !important;
background-color: color-mix(in srgb, #cf1322 8%, transparent) !important;
}
&:disabled {
color: var(--color-text-4) !important;
}
}
.btn-app-outline { .btn-app-outline {
color: var(--app-primary-color) !important; color: var(--app-primary-color) !important;
border-color: var(--app-primary-color) !important; border-color: var(--app-primary-color) !important;
@ -5648,4 +5747,82 @@
} }
} }
} }
.detail-settings-popover {
padding: 10px 12px !important;
.detail-settings-panel {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 220px;
}
.detail-settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.detail-settings-label {
font-size: 13px;
color: var(--color-text-2);
white-space: nowrap;
}
.layout-switch,
.tree-expand-switch {
flex-shrink: 0;
min-width: 58px !important;
width: auto;
height: 28px !important;
line-height: 28px !important;
border-radius: 14px !important;
padding: 0;
background-color: var(--color-fill-3) !important;
&:hover {
background-color: var(--color-fill-4) !important;
}
.arco-switch-handle {
top: 4px;
left: 4px;
width: 20px;
height: 20px;
background-color: #fff;
}
.arco-switch-text-holder {
margin: 0 10px 0 30px;
font-size: 12px;
font-weight: 400;
}
.arco-switch-text {
left: 30px;
font-size: 12px;
font-weight: 400;
color: var(--color-text-2) !important;
}
&.arco-switch-checked {
background-color: var(--color-fill-3) !important;
}
&.arco-switch-checked .arco-switch-handle {
left: calc(100% - 24px);
}
&.arco-switch-checked .arco-switch-text-holder {
margin: 0 30px 0 10px;
}
&.arco-switch-checked .arco-switch-text {
left: 10px;
color: var(--color-text-2) !important;
}
}
}
</style> </style>

View File

@ -4672,9 +4672,14 @@ export default {
'channel.salesProducts.unsaved.discard': 'Discard', 'channel.salesProducts.unsaved.discard': 'Discard',
'channel.salesProducts.unsaved.stay': 'Stay', 'channel.salesProducts.unsaved.stay': 'Stay',
'channel.salesProducts.saveConfirm.title': 'Save confirmation', 'channel.salesProducts.saveConfirm.title': 'Save confirmation',
'channel.salesProducts.saveConfirm.message': 'After saving, the publish preview opens. Publish only after you confirm the diff.', 'channel.salesProducts.saveConfirm.message':
'After saving, the publish preview opens. Publish only after you confirm the diff.\nIf allocation / downstream settings changed: also publish service-channel rules; existing orders need reallocation before forecast uses the new binding.',
'channel.salesProducts.saveConfirm.cancel': 'Cancel', 'channel.salesProducts.saveConfirm.cancel': 'Cancel',
'channel.salesProducts.messages.saveDraftSuccess': 'Draft saved (not published)', 'channel.salesProducts.messages.saveDraftSuccess': 'Draft saved (not published)',
'channel.salesProducts.messages.downstreamSyncTip':
'Tip: allocation/downstream drafts take effect only after rules publish. If channel downstream changed, publish service-channel rules too; reallocate existing orders.',
'channel.salesProducts.publish.downstreamSyncTip':
'After rules publish, new orders use the new snapshot. If downstream changed, ensure related service channels are published; existing orders need reallocation to refresh Downstream fields.',
'channel.salesProducts.messages.saveFailed': 'Save failed', 'channel.salesProducts.messages.saveFailed': 'Save failed',
'channel.salesProducts.messages.toggleDisableTitle': 'Confirm disable', 'channel.salesProducts.messages.toggleDisableTitle': 'Confirm disable',
'channel.salesProducts.messages.toggleDisableConfirm': 'This product will not be available for new orders. Disable "{name}"?', 'channel.salesProducts.messages.toggleDisableConfirm': 'This product will not be available for new orders. Disable "{name}"?',
@ -5059,11 +5064,23 @@ export default {
'channel.serviceChannels.context.app': 'Application', 'channel.serviceChannels.context.app': 'Application',
'channel.serviceChannels.context.apiProvider': 'API provider', 'channel.serviceChannels.context.apiProvider': 'API provider',
'channel.serviceChannels.context.unsaved': 'Unsaved', 'channel.serviceChannels.context.unsaved': 'Unsaved',
'channel.serviceChannels.context.rulesPublishPending': 'Rules pending publish',
'channel.serviceChannels.saveConfirm.title': 'Save confirmation', 'channel.serviceChannels.saveConfirm.title': 'Save confirmation',
'channel.serviceChannels.saveConfirm.message': 'Save only keeps a draft; save & publish pushes channel rules to the live cache.', 'channel.serviceChannels.saveConfirm.message':
'Save only keeps a draft; save & publish pushes channel rules to the live cache.\nIf downstream credentials are affected: also publish related sales-product rules; existing orders need reallocation before forecast uses the new config.',
'channel.serviceChannels.saveConfirm.saveOnly': 'Save only', 'channel.serviceChannels.saveConfirm.saveOnly': 'Save only',
'channel.serviceChannels.saveConfirm.saveAndPublish': 'Save & publish', 'channel.serviceChannels.saveConfirm.saveAndPublish': 'Save & publish',
'channel.serviceChannels.saveConfirm.cancel': 'Cancel', 'channel.serviceChannels.saveConfirm.cancel': 'Cancel',
'channel.serviceChannels.messages.downstreamSyncTip':
'Tip: channel rules must be published to affect forecast credentials. Also publish related sales-product rules; reallocate existing orders.',
'channel.serviceChannels.editProvider.downstreamConfirmTitle': 'Downstream settings changed',
'channel.serviceChannels.editProvider.downstreamConfirmOk': 'Confirm save',
'channel.serviceChannels.editProvider.downstreamConfirmContent':
'App or API provider was changed. After save, please: ① publish rules for each channel under this group; ② publish related sales-product rules; ③ reallocate existing orders. Otherwise forecast may still use old snapshots.',
'channel.serviceChannels.editProvider.downstreamSavedTip':
'Provider downstream settings saved. Next: publish service-channel rules under this group → publish related sales-product rules → reallocate existing orders.',
'channel.serviceChannels.publish.downstreamSyncTip':
'Channel rules publish freezes AppKey/AppSecret/supplierCode. Also publish related sales-product rules; existing orders need reallocation to refresh Downstream bindings.',
'channel.serviceChannels.unsaved.title': 'Unsaved changes', 'channel.serviceChannels.unsaved.title': 'Unsaved changes',
'channel.serviceChannels.unsaved.message': 'Channel config has unsaved changes. Switching will discard them. Continue?', 'channel.serviceChannels.unsaved.message': 'Channel config has unsaved changes. Switching will discard them. Continue?',
'channel.serviceChannels.unsaved.discard': 'Discard', 'channel.serviceChannels.unsaved.discard': 'Discard',

View File

@ -4720,9 +4720,14 @@ export default {
'channel.salesProducts.unsaved.discard': '放弃修改', 'channel.salesProducts.unsaved.discard': '放弃修改',
'channel.salesProducts.unsaved.stay': '留在此页', 'channel.salesProducts.unsaved.stay': '留在此页',
'channel.salesProducts.saveConfirm.title': '保存确认', 'channel.salesProducts.saveConfirm.title': '保存确认',
'channel.salesProducts.saveConfirm.message': '保存后将打开发布预览,确认差异后才会发布到线上缓存。', 'channel.salesProducts.saveConfirm.message':
'保存后将打开发布预览,确认差异后才会发布到线上缓存。\n若排货涉及服务渠道下游对接变更:请同步在「服务渠道」发布规则;已有订单需重排货后,预报才会用上新配置。',
'channel.salesProducts.saveConfirm.cancel': '取消', 'channel.salesProducts.saveConfirm.cancel': '取消',
'channel.salesProducts.messages.saveDraftSuccess': '草稿已保存(未发布)', 'channel.salesProducts.messages.saveDraftSuccess': '草稿已保存(未发布)',
'channel.salesProducts.messages.downstreamSyncTip':
'提醒:排货/下游相关草稿需「规则发布」后生效;若改了服务渠道对接,请同步发布服务渠道规则,存量订单请重排货。',
'channel.salesProducts.publish.downstreamSyncTip':
'规则发布后,新单按新快照排货。若改了下游对接,请确认对应服务渠道也已发布规则;存量订单需重排货才会刷新 Downstream 绑定。',
'channel.salesProducts.messages.saveFailed': '保存失败', 'channel.salesProducts.messages.saveFailed': '保存失败',
'channel.salesProducts.messages.toggleDisableTitle': '确认禁用', 'channel.salesProducts.messages.toggleDisableTitle': '确认禁用',
'channel.salesProducts.messages.toggleDisableConfirm': '禁用后该产品将无法用于新订单,是否确认禁用「{name}」?', 'channel.salesProducts.messages.toggleDisableConfirm': '禁用后该产品将无法用于新订单,是否确认禁用「{name}」?',
@ -5107,11 +5112,23 @@ export default {
'channel.serviceChannels.context.app': '应用', 'channel.serviceChannels.context.app': '应用',
'channel.serviceChannels.context.apiProvider': 'API 服务商', 'channel.serviceChannels.context.apiProvider': 'API 服务商',
'channel.serviceChannels.context.unsaved': '未保存', 'channel.serviceChannels.context.unsaved': '未保存',
'channel.serviceChannels.context.rulesPublishPending': '待发布规则',
'channel.serviceChannels.saveConfirm.title': '保存确认', 'channel.serviceChannels.saveConfirm.title': '保存确认',
'channel.serviceChannels.saveConfirm.message': '仅保存不会发布到线上缓存;保存并发布会立即发布渠道规则。', 'channel.serviceChannels.saveConfirm.message':
'仅保存不会发布到线上缓存;保存并发布会立即发布渠道规则。\n若变更影响下游对接:请同步在「销售产品」发布规则;已有订单需重排货后,预报才会用上新配置。',
'channel.serviceChannels.saveConfirm.saveOnly': '仅保存', 'channel.serviceChannels.saveConfirm.saveOnly': '仅保存',
'channel.serviceChannels.saveConfirm.saveAndPublish': '保存并发布', 'channel.serviceChannels.saveConfirm.saveAndPublish': '保存并发布',
'channel.serviceChannels.saveConfirm.cancel': '取消', 'channel.serviceChannels.saveConfirm.cancel': '取消',
'channel.serviceChannels.messages.downstreamSyncTip':
'提醒:渠道规则需发布后才影响预报凭证;请同步发布相关销售产品规则,存量订单请重排货。',
'channel.serviceChannels.editProvider.downstreamConfirmTitle': '下游对接已变更',
'channel.serviceChannels.editProvider.downstreamConfirmOk': '确认保存',
'channel.serviceChannels.editProvider.downstreamConfirmContent':
'应用或 API 服务商已修改。保存后请按顺序:① 对本分组下各服务渠道「保存并发布规则」;② 发布相关销售产品规则;③ 存量订单重排货。否则预报可能仍用旧快照。',
'channel.serviceChannels.editProvider.downstreamSavedTip':
'服务商下游配置已保存。请尽快:发布本分组下服务渠道规则 → 发布相关销售产品规则 → 存量订单重排货。',
'channel.serviceChannels.publish.downstreamSyncTip':
'渠道规则发布会固化下游 AppKey/AppSecret/supplierCode。请同步发布相关销售产品规则;存量订单需重排货后才会刷新订单上的下游绑定。',
'channel.serviceChannels.unsaved.title': '未保存的更改', 'channel.serviceChannels.unsaved.title': '未保存的更改',
'channel.serviceChannels.unsaved.message': '渠道配置有未保存的修改,切换后将丢失。是否继续?', 'channel.serviceChannels.unsaved.message': '渠道配置有未保存的修改,切换后将丢失。是否继续?',
'channel.serviceChannels.unsaved.discard': '放弃修改', 'channel.serviceChannels.unsaved.discard': '放弃修改',

View File

@ -295,6 +295,7 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
} }
// 登录(OMS 管理端默认 type=User) // 登录(OMS 管理端默认 type=User)
// 失败不弹全局 Message,由登录页内联 errorBox 展示
async function login(account: string, password: string, loginType: OmsLoginType = OmsLoginType.User) { async function login(account: string, password: string, loginType: OmsLoginType = OmsLoginType.User) {
try { try {
const result = await AuthService.postRbacAuthLogin({ const result = await AuthService.postRbacAuthLogin({
@ -307,8 +308,7 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
enableKeepAlive() enableKeepAlive()
await hydrateUserAfterLogin(result) await hydrateUserAfterLogin(result)
return true return true
} catch (error) { } catch {
handleApiError(error)
return false return false
} }
} }

View File

@ -19,7 +19,7 @@
validateInterfaceSavePayload, validateInterfaceSavePayload,
} from '@/utils/interfaceParameterModel'; } from '@/utils/interfaceParameterModel';
const emit = defineEmits(['close', 'saved']); const emit = defineEmits(['close', 'saved', 'return-config-saved']);
const { t } = useI18n(); const { t } = useI18n();
@ -378,8 +378,8 @@
} }
// 保存函数 // 保存函数;skipSuccessToast 用于父级自行弹出「是否去发布」确认框时避免重复提示
async function saveInterface() { async function saveInterface(options?: { skipSuccessToast?: boolean }) {
try { try {
if (!interfaceDetailRef.value) { if (!interfaceDetailRef.value) {
notifySaveError('接口明细组件未初始化'); notifySaveError('接口明细组件未初始化');
@ -436,7 +436,9 @@
} }
await postInterfaceSaveInterface(saveData); await postInterfaceSaveInterface(saveData);
$baseMessage('保存成功,如需生效请去发布', 'success'); if (!options?.skipSuccessToast) {
$baseMessage('保存成功,如需生效请去发布', 'success');
}
interfaceDetailRef.value?.captureDirtyBaseline?.(); interfaceDetailRef.value?.captureDirtyBaseline?.();
emit('saved'); emit('saved');
emit('close'); emit('close');
@ -570,9 +572,10 @@
:interface-id="tableForm.id" :interface-id="tableForm.id"
:service-provider-id="tableForm.serviceProviderId" :service-provider-id="tableForm.serviceProviderId"
@update:basic-form-data="(val) => Object.assign(tableForm, val)" @update:basic-form-data="(val) => Object.assign(tableForm, val)"
@return-config-saved="emit('return-config-saved')"
> >
<template #headerActions> <template #headerActions>
<a-button size="small" @click="openSmartFillModal">智能填充</a-button> <a-button size="small" title="智能填充" @click="openSmartFillModal">AI</a-button>
<slot name="headerActions" /> <slot name="headerActions" />
</template> </template>
</InterfaceDetail> </InterfaceDetail>

View File

@ -104,11 +104,11 @@
return listData.value; return listData.value;
} }
async function addMapClick() { async function addMapClick(options?: { skipSuccessToast?: boolean }) {
handlerCode(); return await handlerCode(options);
} }
async function handlerCode() { async function handlerCode(options?: { skipSuccessToast?: boolean }) {
try { try {
const actionRows = getActionRows(); const actionRows = getActionRows();
@ -147,8 +147,13 @@
items: tableData, items: tableData,
}); });
$baseMessage('保存成功,如需生效请去发布', 'success'); if (!options?.skipSuccessToast) {
} catch {} $baseMessage('保存成功,如需生效请去发布', 'success');
}
return true;
} catch {
return false;
}
} }
async function addClick() { async function addClick() {
@ -258,8 +263,8 @@
return valueList.value.find((item) => item.key === key)?.value ?? key; return valueList.value.find((item) => item.key === key)?.value ?? key;
} }
async function save() { async function save(options?: { skipSuccessToast?: boolean }) {
return await addMapClick(); return await addMapClick(options);
} }
function hasUnsavedChanges() { function hasUnsavedChanges() {

View File

@ -1366,7 +1366,7 @@
interfaceModalVisible.value = false; interfaceModalVisible.value = false;
} }
// 接口保存成功(提示由子组件统一发出,这里只关弹窗并刷新) // 接口保存成功(关弹窗、刷新,并引导发布)
function handleInterfaceSaved() { function handleInterfaceSaved() {
interfaceModalVisible.value = false; interfaceModalVisible.value = false;
loadInterfaceList(); loadInterfaceList();
@ -1384,13 +1384,14 @@
}); });
}); });
} }
askGoPublish(selectedProvider.value);
} }
// ModalPro确定按钮点击 // ModalPro确定按钮点击
async function handleInterfaceSave() { async function handleInterfaceSave() {
if (interfaceModalRef.value) { if (interfaceModalRef.value) {
try { try {
await interfaceModalRef.value.saveInterface(); await interfaceModalRef.value.saveInterface({ skipSuccessToast: true });
return true; // 返回 true 表示成功,允许弹窗关闭 return true; // 返回 true 表示成功,允许弹窗关闭
} catch (error) { } catch (error) {
return false; // 返回 false 表示失败,阻止弹窗关闭 return false; // 返回 false 表示失败,阻止弹窗关闭
@ -1557,9 +1558,9 @@
async function handleInterfaceDetailSave() { async function handleInterfaceDetailSave() {
if (!interfaceDetailModalRef.value) return; if (!interfaceDetailModalRef.value) return;
// 成功提示 / 列表刷新由 saveInterface → @saved 统一处理,避免连弹 2~3 次 // 发布确认已含「保存成功」;刷新 / 引导发布由 saveInterface → @saved 统一处理
try { try {
await interfaceDetailModalRef.value.saveInterface(); await interfaceDetailModalRef.value.saveInterface({ skipSuccessToast: true });
} catch { } catch {
/* 子组件已提示错误 */ /* 子组件已提示错误 */
} }
@ -1731,6 +1732,7 @@
:key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`" :key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`"
ref="interfaceDetailModalRef" ref="interfaceDetailModalRef"
@saved="handleInterfaceSaved" @saved="handleInterfaceSaved"
@return-config-saved="askGoPublish"
> >
<template #headerActions> <template #headerActions>
<a-button <a-button
@ -1738,9 +1740,13 @@
title="将当前 cURL 带入 API 调试" title="将当前 cURL 带入 API 调试"
@click="handleDebugClick" @click="handleDebugClick"
> >
调试 发送
</a-button> </a-button>
<a-button size="small" type="primary" @click="handleInterfaceDetailSave"> <a-button
size="small"
class="app-btn-primary-soft"
@click="handleInterfaceDetailSave"
>
{{ t('system.serviceProviders.detail.actions.save') }} {{ t('system.serviceProviders.detail.actions.save') }}
</a-button> </a-button>
</template> </template>
@ -1799,6 +1805,7 @@
ref="interfaceModalRef" ref="interfaceModalRef"
@close="handleInterfaceModalClose" @close="handleInterfaceModalClose"
@saved="handleInterfaceSaved" @saved="handleInterfaceSaved"
@return-config-saved="askGoPublish"
/> />
</ModalPro> </ModalPro>
<ParameterConfig <ParameterConfig

View File

@ -212,8 +212,8 @@
} }
} }
// 保存函数 // 保存函数;skipSuccessToast 用于父级自行弹出「是否去发布」确认框时避免重复提示
async function saveInterface() { async function saveInterface(options?: { skipSuccessToast?: boolean }) {
try { try {
if (!interfaceDetailRef.value) { if (!interfaceDetailRef.value) {
notifySaveError('接口明细组件未初始化'); notifySaveError('接口明细组件未初始化');
@ -259,7 +259,9 @@
} }
await postInterfaceSaveInterface(saveData as any); await postInterfaceSaveInterface(saveData as any);
$baseMessage('保存成功,如需生效请去发布', 'success'); if (!options?.skipSuccessToast) {
$baseMessage('保存成功,如需生效请去发布', 'success');
}
interfaceDetailRef.value?.captureDirtyBaseline?.(); interfaceDetailRef.value?.captureDirtyBaseline?.();
emit('saved'); emit('saved');
return true; return true;
@ -365,7 +367,7 @@
@update:basic-form-data="(val) => Object.assign(tableForm, val)" @update:basic-form-data="(val) => Object.assign(tableForm, val)"
> >
<template #headerActions> <template #headerActions>
<a-button size="small" @click="openSmartFillModal">智能填充</a-button> <a-button size="small" title="智能填充" @click="openSmartFillModal">AI</a-button>
<slot name="headerActions" /> <slot name="headerActions" />
</template> </template>
</InterfaceDetail> </InterfaceDetail>

View File

@ -986,6 +986,22 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
}); });
} }
/** 保存成功后引导打开发布弹窗(与映射明细保存一致) */
function askPublishAfterSave() {
$baseConfirm({
title: t('system.interface.modal.warningTitle'),
content: t('system.interface.modal.publishConfirm'),
onOk: async () => {
const currentRow = MapServiceProviderROW.value;
if (currentRow) {
(unref(MapServiceProviderRef) as any)?.onRelease?.(currentRow);
return;
}
$baseMessage('请先在中间选择服务商映射后再发布', 'warning');
},
});
}
// 统一的保存函数,根据当前tab调用不同的接口 // 统一的保存函数,根据当前tab调用不同的接口
async function handleTabSave() { async function handleTabSave() {
const currentTab = currentActiveTab.value; const currentTab = currentActiveTab.value;
@ -995,8 +1011,8 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
await saveModelEnter(); await saveModelEnter();
} else if (currentTab === 'detail') { } else if (currentTab === 'detail') {
if (interfaceDetailModalRef.value && typeof interfaceDetailModalRef.value.saveInterface === 'function') { if (interfaceDetailModalRef.value && typeof interfaceDetailModalRef.value.saveInterface === 'function') {
// 成功提示由子组件统一发出;刷新走 @saved / 本地 init // 发布确认框已含「保存成功」,跳过子组件 toast
await interfaceDetailModalRef.value.saveInterface(); await interfaceDetailModalRef.value.saveInterface({ skipSuccessToast: true });
if (selectedInterface.value && interfaceDetailModalRef.value) { if (selectedInterface.value && interfaceDetailModalRef.value) {
await nextTick(); await nextTick();
interfaceDetailModalRef.value.init({ interfaceDetailModalRef.value.init({
@ -1005,6 +1021,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
forceRefresh: true, forceRefresh: true,
}); });
} }
askPublishAfterSave();
} else { } else {
$baseMessage('接口配置组件未初始化', 'warning'); $baseMessage('接口配置组件未初始化', 'warning');
} }
@ -1339,6 +1356,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
key: `item-${item.id}`, key: `item-${item.id}`,
title: item.name, title: item.name,
isLeaf: true, isLeaf: true,
depth: 2,
id: item.id, id: item.id,
name: item.name, name: item.name,
code: item.code || item.name, code: item.code || item.name,
@ -1357,6 +1375,9 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
const categoryNode = { const categoryNode = {
key: `category-${categoryIndex}`, key: `category-${categoryIndex}`,
title: category, title: category,
isLeaf: false,
depth: 1,
subtitle: `${items.length} 个接口`,
children: items children: items
}; };
treeData.push(categoryNode); treeData.push(categoryNode);
@ -1518,9 +1539,13 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
title="将当前 cURL 带入 API 调试" title="将当前 cURL 带入 API 调试"
@click="handleDetailDebugClick" @click="handleDetailDebugClick"
> >
调试 发送
</a-button> </a-button>
<a-button size="small" type="primary" @click="handleTabSave"> <a-button
size="small"
class="app-btn-primary-soft"
@click="handleTabSave"
>
{{ $t('system.interface.actions.save') }} {{ $t('system.interface.actions.save') }}
</a-button> </a-button>
</template> </template>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, nextTick, onMounted, watch, onBeforeUnmount } from 'vue'; import { ref, reactive, computed, nextTick, onMounted, watch, onBeforeUnmount, h } from 'vue';
import { import {
createSalesProduct, createSalesProduct,
createSalesProductPrice, createSalesProductPrice,
@ -1603,7 +1603,12 @@
if (!configFormDirty.value) return; if (!configFormDirty.value) return;
Modal.confirm({ Modal.confirm({
title: t('channel.salesProducts.saveConfirm.title'), title: t('channel.salesProducts.saveConfirm.title'),
content: t('channel.salesProducts.saveConfirm.message'), content: () =>
h(
'div',
{ style: 'white-space: pre-line; line-height: 1.6;' },
t('channel.salesProducts.saveConfirm.message'),
),
okText: t('channel.salesProducts.actions.saveAndPublish'), okText: t('channel.salesProducts.actions.saveAndPublish'),
cancelText: t('channel.salesProducts.saveConfirm.cancel'), cancelText: t('channel.salesProducts.saveConfirm.cancel'),
onOk: async () => { onOk: async () => {
@ -1631,6 +1636,10 @@
try { try {
await persistSalesProductAllocationDraft(pid); await persistSalesProductAllocationDraft(pid);
Message.success(t('channel.salesProducts.messages.saveDraftSuccess')); Message.success(t('channel.salesProducts.messages.saveDraftSuccess'));
Message.warning({
content: t('channel.salesProducts.messages.downstreamSyncTip'),
duration: 8000,
});
} catch (e: any) { } catch (e: any) {
Message.error(e?.message || t('channel.salesProducts.messages.saveFailed')); Message.error(e?.message || t('channel.salesProducts.messages.saveFailed'));
} finally { } finally {
@ -1647,7 +1656,12 @@
if (!rulesFormDirty.value) return; if (!rulesFormDirty.value) return;
Modal.confirm({ Modal.confirm({
title: t('channel.salesProducts.saveConfirm.title'), title: t('channel.salesProducts.saveConfirm.title'),
content: t('channel.salesProducts.saveConfirm.message'), content: () =>
h(
'div',
{ style: 'white-space: pre-line; line-height: 1.6;' },
t('channel.salesProducts.saveConfirm.message'),
),
okText: t('channel.salesProducts.actions.saveAndPublish'), okText: t('channel.salesProducts.actions.saveAndPublish'),
cancelText: t('channel.salesProducts.saveConfirm.cancel'), cancelText: t('channel.salesProducts.saveConfirm.cancel'),
onOk: async () => { onOk: async () => {
@ -1829,6 +1843,12 @@
operatorType === 'price' ? pricePublishAiReport.value.trim() || undefined : undefined, operatorType === 'price' ? pricePublishAiReport.value.trim() || undefined : undefined,
}); });
Message.success(t('channel.salesProducts.publish.success')); Message.success(t('channel.salesProducts.publish.success'));
if (operatorType === 'rules') {
Message.warning({
content: t('channel.salesProducts.publish.downstreamSyncTip'),
duration: 8000,
});
}
publishConfirmVisible.value = false; publishConfirmVisible.value = false;
publishDescription.value = ''; publishDescription.value = '';
publishConfirmDate.value = dayjs(); publishConfirmDate.value = dayjs();
@ -5185,6 +5205,14 @@
@cancel="handleCancelPublishConfirm" @cancel="handleCancelPublishConfirm"
@ok="handleConfirmPublish" @ok="handleConfirmPublish"
> >
<a-alert
v-if="publishOperatorType === 'rules'"
type="warning"
:show-icon="true"
style="margin-bottom: 12px"
>
{{ t('channel.salesProducts.publish.downstreamSyncTip') }}
</a-alert>
<div class="mb-15px">{{ t('channel.salesProducts.publish.effectiveTimeHint') }}</div> <div class="mb-15px">{{ t('channel.salesProducts.publish.effectiveTimeHint') }}</div>
<div class="mb-15px"> <div class="mb-15px">
<a-input <a-input

View File

@ -97,7 +97,7 @@
type="primary" type="primary"
size="small" size="small"
:loading="savingConfig" :loading="savingConfig"
:disabled="!configFormDirty" :disabled="!canSaveAndPublish"
@click="handleSaveAndPublish" @click="handleSaveAndPublish"
> >
{{ t('channel.serviceChannels.actions.saveAndPublish') }} {{ t('channel.serviceChannels.actions.saveAndPublish') }}
@ -131,6 +131,14 @@
<a-tag v-if="configFormDirty && activeTab === 'config'" color="orangered" size="small" class="context-dirty-tag"> <a-tag v-if="configFormDirty && activeTab === 'config'" color="orangered" size="small" class="context-dirty-tag">
{{ t('channel.serviceChannels.context.unsaved') }} {{ t('channel.serviceChannels.context.unsaved') }}
</a-tag> </a-tag>
<a-tag
v-else-if="selectedChannelNeedsRulesPublish && activeTab === 'config'"
color="orangered"
size="small"
class="context-dirty-tag"
>
{{ t('channel.serviceChannels.context.rulesPublishPending') }}
</a-tag>
</div> </div>
<div v-if="!selectedChannel" class="empty-state-panel"> <div v-if="!selectedChannel" class="empty-state-panel">
@ -940,6 +948,14 @@
@cancel="handleCancelPublishConfirm" @cancel="handleCancelPublishConfirm"
@ok="handleConfirmPublish" @ok="handleConfirmPublish"
> >
<a-alert
v-if="publishOperatorType === 'rules'"
type="warning"
:show-icon="true"
style="margin-bottom: 12px"
>
{{ t('channel.serviceChannels.publish.downstreamSyncTip') }}
</a-alert>
<div class="mb-15px">请选择生效的日期时间,不选默认立即生效</div> <div class="mb-15px">请选择生效的日期时间,不选默认立即生效</div>
<div class="mb-15px"> <div class="mb-15px">
<a-input <a-input
@ -1367,7 +1383,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue' import { ref, reactive, computed, watch, nextTick, onMounted, h } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { IconPlus, IconDelete, IconSearch, IconEdit, IconDragArrow, IconQuestionCircleFill, IconQuestionCircle, IconFolder, IconMore } from '@arco-design/web-vue/es/icon' import { IconPlus, IconDelete, IconSearch, IconEdit, IconDragArrow, IconQuestionCircleFill, IconQuestionCircle, IconFolder, IconMore } from '@arco-design/web-vue/es/icon'
import { Message, Modal, Input } from '@arco-design/web-vue' import { Message, Modal, Input } from '@arco-design/web-vue'
@ -2073,6 +2089,79 @@ const configFormDirty = computed(() => {
) )
}) })
/** 编辑服务商后,该分组下渠道需重新发布规则快照(按分组 Id) */
const pendingRulesPublishGroupIds = ref<Set<number>>(new Set())
/** 本轮待发布中,已完成规则发布的渠道(同组其他渠道仍可点) */
const rulesPublishDoneChannelIds = ref<Set<number>>(new Set())
function getSelectedChannelGroupId(): number {
const ch = selectedChannel.value as any
if (!ch) return 0
const fromChannel = Number(ch.groupId ?? ch.GroupId ?? 0)
if (Number.isFinite(fromChannel) && fromChannel > 0) return fromChannel
for (const g of channelGroups.value) {
if ((g.channels || []).some((c: any) => Number(c.id) === Number(ch.id))) {
return Number((g as any).id) || 0
}
}
return 0
}
const selectedChannelNeedsRulesPublish = computed(() => {
const ch = selectedChannel.value as any
const channelId = Number(ch?.id)
const groupId = getSelectedChannelGroupId()
if (!(groupId > 0) || !pendingRulesPublishGroupIds.value.has(groupId)) return false
if (Number.isFinite(channelId) && channelId > 0 && rulesPublishDoneChannelIds.value.has(channelId)) {
return false
}
return true
})
const canSaveAndPublish = computed(
() => configFormDirty.value || selectedChannelNeedsRulesPublish.value,
)
function markGroupRulesPublishPending(groupId: number | string | null | undefined) {
const id = Number(groupId)
if (!Number.isFinite(id) || id <= 0) return
const nextGroups = new Set(pendingRulesPublishGroupIds.value)
nextGroups.add(id)
pendingRulesPublishGroupIds.value = nextGroups
// 该分组重新进入待发布:清掉同组已发布标记
const group = channelGroups.value.find((g) => Number((g as any).id) === id)
const channelIds = (group?.channels || [])
.map((c: any) => Number(c?.id))
.filter((cid: number) => Number.isFinite(cid) && cid > 0)
if (!channelIds.length) return
const nextDone = new Set(rulesPublishDoneChannelIds.value)
channelIds.forEach((cid: number) => nextDone.delete(cid))
rulesPublishDoneChannelIds.value = nextDone
}
function markSelectedChannelRulesPublishDone() {
const ch = selectedChannel.value as any
const channelId = Number(ch?.id)
if (!Number.isFinite(channelId) || channelId <= 0) return
if (rulesPublishDoneChannelIds.value.has(channelId)) return
const next = new Set(rulesPublishDoneChannelIds.value)
next.add(channelId)
rulesPublishDoneChannelIds.value = next
// 同组渠道都发完后,去掉分组待发布标记
const groupId = getSelectedChannelGroupId()
const group = channelGroups.value.find((g) => Number((g as any).id) === groupId)
const channelIds = (group?.channels || [])
.map((c: any) => Number(c?.id))
.filter((cid: number) => Number.isFinite(cid) && cid > 0)
if (channelIds.length && channelIds.every((cid: number) => rulesPublishDoneChannelIds.value.has(cid))) {
const nextGroups = new Set(pendingRulesPublishGroupIds.value)
nextGroups.delete(groupId)
pendingRulesPublishGroupIds.value = nextGroups
}
}
const selectedChannelContext = computed(() => { const selectedChannelContext = computed(() => {
const ch = selectedChannel.value as any const ch = selectedChannel.value as any
if (!ch?.id) return null if (!ch?.id) return null
@ -2189,6 +2278,35 @@ const editProviderForm = reactive({
appId: undefined as number | undefined, appId: undefined as number | undefined,
serviceProviderId: undefined as number | undefined serviceProviderId: undefined as number | undefined
}) })
const editProviderOriginal = reactive({
appId: undefined as number | undefined,
serviceProviderId: undefined as number | undefined,
})
function isEditProviderDownstreamChanged() {
return (
Number(editProviderForm.appId) !== Number(editProviderOriginal.appId) ||
Number(editProviderForm.serviceProviderId) !== Number(editProviderOriginal.serviceProviderId)
)
}
function confirmEditProviderDownstreamChange(): Promise<boolean> {
return new Promise((resolve) => {
Modal.confirm({
title: t('channel.serviceChannels.editProvider.downstreamConfirmTitle'),
content: () =>
h(
'div',
{ style: 'white-space: pre-line; line-height: 1.6;' },
t('channel.serviceChannels.editProvider.downstreamConfirmContent'),
),
okText: t('channel.serviceChannels.editProvider.downstreamConfirmOk'),
cancelText: t('channel.serviceChannels.saveConfirm.cancel'),
onOk: () => resolve(true),
onCancel: () => resolve(false),
})
})
}
// 取消编辑服务商 // 取消编辑服务商
const handleCancelEditProvider = () => { const handleCancelEditProvider = () => {
@ -3324,6 +3442,8 @@ const handleEditGroup = (group: any) => {
editProviderForm.appId = (group as any).appId editProviderForm.appId = (group as any).appId
editProviderForm.serviceProviderId = editProviderForm.serviceProviderId =
(group as any).apiServiceProviderId ?? (group as any).serviceProviderId (group as any).apiServiceProviderId ?? (group as any).serviceProviderId
editProviderOriginal.appId = editProviderForm.appId
editProviderOriginal.serviceProviderId = editProviderForm.serviceProviderId
editProviderModalVisible.value = true editProviderModalVisible.value = true
} }
@ -3496,6 +3616,13 @@ async function executeChannelConfigSave(
} }
await loadChannelGroups() await loadChannelGroups()
Message.success(t('channel.serviceChannels.messages.saveAndPublishSuccess')) Message.success(t('channel.serviceChannels.messages.saveAndPublishSuccess'))
if (options.publishAfter === 'rules') {
markSelectedChannelRulesPublishDone()
Message.warning({
content: t('channel.serviceChannels.messages.downstreamSyncTip'),
duration: 8000,
})
}
} catch (e: any) { } catch (e: any) {
Message.error( Message.error(
t('channel.serviceChannels.messages.publishFailedAfterSave', { t('channel.serviceChannels.messages.publishFailedAfterSave', {
@ -3506,6 +3633,10 @@ async function executeChannelConfigSave(
} }
} else { } else {
Message.success(successMessage) Message.success(successMessage)
Message.warning({
content: t('channel.serviceChannels.messages.downstreamSyncTip'),
duration: 8000,
})
} }
return true return true
} catch (e: any) { } catch (e: any) {
@ -3525,21 +3656,66 @@ const handleSaveDraft = async () => {
await executeChannelConfigSave(t('channel.serviceChannels.messages.saveDraftSuccess')) await executeChannelConfigSave(t('channel.serviceChannels.messages.saveDraftSuccess'))
} }
/** 仅因服务商下游变更触发:不改渠道表单,直接发布规则快照 */
async function publishChannelRulesAfterProviderChange() {
if (!selectedChannel.value || !(selectedChannel.value as any).id) {
Message.warning('请先选择渠道')
return false
}
const savedId = (selectedChannel.value as any).id
savingConfig.value = true
try {
await publishServiceChannel({
serviceChannelId: savedId,
operatorType: 'rules',
description: '服务商下游配置变更后发布渠道规则',
})
markSelectedChannelRulesPublishDone()
await loadServiceChannelPublishRecords('rules')
await loadChannelGroups()
Message.success(t('channel.serviceChannels.messages.saveAndPublishSuccess'))
Message.warning({
content: t('channel.serviceChannels.messages.downstreamSyncTip'),
duration: 8000,
})
return true
} catch (e: any) {
Message.error(
t('channel.serviceChannels.messages.publishFailedAfterSave', {
msg: e?.message || '未知错误',
}),
)
return false
} finally {
savingConfig.value = false
}
}
const handleSaveAndPublish = async () => { const handleSaveAndPublish = async () => {
if (!selectedChannel.value || !(selectedChannel.value as any).id) { if (!selectedChannel.value || !(selectedChannel.value as any).id) {
Message.warning('请先选择渠道') Message.warning('请先选择渠道')
return return
} }
if (!configFormDirty.value) return if (!canSaveAndPublish.value) return
const formDirty = configFormDirty.value
Modal.confirm({ Modal.confirm({
title: t('channel.serviceChannels.saveConfirm.title'), title: t('channel.serviceChannels.saveConfirm.title'),
content: t('channel.serviceChannels.saveConfirm.message'), content: () =>
h(
'div',
{ style: 'white-space: pre-line; line-height: 1.6;' },
t('channel.serviceChannels.saveConfirm.message'),
),
okText: t('channel.serviceChannels.saveConfirm.saveAndPublish'), okText: t('channel.serviceChannels.saveConfirm.saveAndPublish'),
cancelText: t('channel.serviceChannels.saveConfirm.cancel'), cancelText: t('channel.serviceChannels.saveConfirm.cancel'),
onOk: async () => { onOk: async () => {
await executeChannelConfigSave(t('channel.serviceChannels.messages.saveAndPublishSuccess'), { if (formDirty) {
publishAfter: 'rules', await executeChannelConfigSave(t('channel.serviceChannels.messages.saveAndPublishSuccess'), {
}) publishAfter: 'rules',
})
} else {
await publishChannelRulesAfterProviderChange()
}
}, },
}) })
} }
@ -4012,16 +4188,24 @@ const handleConfirmAddProvider = async () => {
const handleConfirmEditProvider = async () => { const handleConfirmEditProvider = async () => {
if (!editProviderForm.providerName || !editProviderForm.appId || !editProviderForm.serviceProviderId) { if (!editProviderForm.providerName || !editProviderForm.appId || !editProviderForm.serviceProviderId) {
Message.warning('请填写完整信息') Message.warning('请填写完整信息')
return return Promise.reject()
}
const downstreamChanged = isEditProviderDownstreamChanged()
if (downstreamChanged) {
const confirmed = await confirmEditProviderDownstreamChange()
if (!confirmed) {
return Promise.reject()
}
} }
try { try {
const editedGroupId = Number(editingProviderGroupId.value)
await updateServiceChannelGroup({ await updateServiceChannelGroup({
id: Number(editingProviderGroupId.value), id: editedGroupId,
name: editProviderForm.providerName, name: editProviderForm.providerName,
appId: Number(editProviderForm.appId), appId: Number(editProviderForm.appId),
apiServiceProviderId: Number(editProviderForm.serviceProviderId), apiServiceProviderId: Number(editProviderForm.serviceProviderId),
}) })
const group = channelGroups.value.find(g => Number((g as any).id) === editingProviderGroupId.value) const group = channelGroups.value.find(g => Number((g as any).id) === editedGroupId)
if (group) { if (group) {
;(group as any).name = editProviderForm.providerName ;(group as any).name = editProviderForm.providerName
;(group as any).appId = editProviderForm.appId ;(group as any).appId = editProviderForm.appId
@ -4032,9 +4216,26 @@ const handleConfirmEditProvider = async () => {
editingProviderGroupId.value = null editingProviderGroupId.value = null
await loadServiceProviderList() await loadServiceProviderList()
await loadChannelGroups() await loadChannelGroups()
// 服务商分组一保存,该分组下渠道即可「保存并发布」以刷新规则快照
markGroupRulesPublishPending(editedGroupId)
if (activeTab.value !== 'config') {
activeTab.value = 'config'
}
if (downstreamChanged) {
Message.warning({
content: t('channel.serviceChannels.editProvider.downstreamSavedTip'),
duration: 8000,
})
} else {
Message.info({
content: t('channel.serviceChannels.context.rulesPublishPending'),
duration: 5000,
})
}
} catch (error) { } catch (error) {
console.error('编辑服务商失败:', error) console.error('编辑服务商失败:', error)
Message.error('编辑失败') Message.error('编辑失败')
return Promise.reject(error)
} }
} }
@ -4261,6 +4462,13 @@ const handleConfirmPublish = async () => {
description: publishDescription.value || undefined, description: publishDescription.value || undefined,
}) })
Message.success('发布成功') Message.success('发布成功')
if (operatorType === 'rules') {
markSelectedChannelRulesPublishDone()
Message.warning({
content: t('channel.serviceChannels.publish.downstreamSyncTip'),
duration: 8000,
})
}
publishConfirmVisible.value = false publishConfirmVisible.value = false
publishDescription.value = '' publishDescription.value = ''
publishConfirmDate.value = null publishConfirmDate.value = null

View File

@ -390,10 +390,10 @@ async function handleSubmit() {
}); });
} else { } else {
errorBox.value = t('login.form.login.failed'); errorBox.value = t('login.form.login.failed');
Message.error(t('login.form.login.failed'));
loginCharacters.value?.onLoginFailed(); loginCharacters.value?.onLoginFailed();
} }
} catch { } catch {
errorBox.value = t('login.form.login.failed');
loginCharacters.value?.onLoginFailed(); loginCharacters.value?.onLoginFailed();
} finally { } finally {
loading.value = false; loading.value = false;