feat(admin): 未保存离开保护(Batch 3 收口)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 20:42:18 +08:00
parent 250ffdfa53
commit edf58ce0a0
7 changed files with 162 additions and 16 deletions
@@ -98,6 +98,8 @@
): void ): void
/** footer 回滚入口:请求父层切到版本历史 */ /** footer 回滚入口:请求父层切到版本历史 */
(e: 'navigate', group: string): void (e: 'navigate', group: string): void
/** 未保存修改上报(Task 14:父层三处离开拦截消费) */
(e: 'dirty', value: boolean): void
}>() }>()
// ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置表单) ---------------------- // ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置表单) ----------------------
@@ -107,17 +109,25 @@
const saving = ref(false) const saving = ref(false)
const resetting = ref(false) const resetting = ref(false)
const actionError = ref('') const actionError = ref('')
/** dirty 基线(与表单重建同 watch 刷新——保存/重置回写经父层快照组
* 引用变化走同一条链,无需手动清) */
const baseline = ref(JSON.stringify(contactToForm(undefined)))
watch( watch(
() => props.group, () => props.group,
(group) => { (group) => {
Object.assign(form, contactToForm(group)) const next = contactToForm(group)
Object.assign(form, next)
baseline.value = JSON.stringify(next)
saveComment.value = '' saveComment.value = ''
actionError.value = '' actionError.value = ''
}, },
{ immediate: true } { immediate: true }
) )
const dirty = computed(() => JSON.stringify(form) !== baseline.value)
watch(dirty, (v) => emit('dirty', v), { immediate: true })
// ---- 保存(contactToPayload → PUT 整组;类型校验后端兜底) ---------------------------- // ---- 保存(contactToPayload → PUT 整组;类型校验后端兜底) ----------------------------
async function onSave(): Promise<void> { async function onSave(): Promise<void> {
@@ -407,6 +407,8 @@
): void ): void
/** footer 回滚入口:请求父层切到对应组('versions' = 版本历史) */ /** footer 回滚入口:请求父层切到对应组('versions' = 版本历史) */
(e: 'navigate', group: string): void (e: 'navigate', group: string): void
/** 未保存修改上报(Task 14:三卡合一,父层三处离开拦截消费) */
(e: 'dirty', value: boolean): void
}>() }>()
// ---- 编辑态(组引用变化 = 保存回写/租户切换/重载 → 重置表单与密钥态) ---------------- // ---- 编辑态(组引用变化 = 保存回写/租户切换/重载 → 重置表单与密钥态) ----------------
@@ -432,6 +434,12 @@
const emailError = ref('') const emailError = ref('')
const pushError = ref('') const pushError = ref('')
const aiError = ref('') const aiError = ref('')
/** dirty 基线(三表单各自 normalize*Group().form 的 JSON 快照——与
* resetAll 同 watch 刷新;密钥输入不进基线:掩码 GET 本就无原值可比,
* 由 dirty 计算里的三态判定补足) */
const emailBaseline = ref(JSON.stringify(normalizeEmailCredsGroup(undefined).form))
const pushBaseline = ref(JSON.stringify(normalizePushCredsGroup(undefined).form))
const aiBaseline = ref(JSON.stringify(normalizeAiCredsGroup(undefined).form))
// 测试态(声明先于下方 immediate watch 的 resetAll——避免 TDZ 引用) // 测试态(声明先于下方 immediate watch 的 resetAll——避免 TDZ 引用)
const emailTestTo = ref('') const emailTestTo = ref('')
@@ -454,14 +462,17 @@
Object.assign(emailForm, es.form) Object.assign(emailForm, es.form)
emailPasswordMasked.value = es.passwordMasked emailPasswordMasked.value = es.passwordMasked
emailPasswordCleared.value = false emailPasswordCleared.value = false
emailBaseline.value = JSON.stringify(es.form)
const ps = normalizePushCredsGroup(pushGroup) const ps = normalizePushCredsGroup(pushGroup)
Object.assign(pushForm, ps.form) Object.assign(pushForm, ps.form)
pushApiKeyMasked.value = ps.apiKeyMasked pushApiKeyMasked.value = ps.apiKeyMasked
pushApiKeyCleared.value = false pushApiKeyCleared.value = false
pushBaseline.value = JSON.stringify(ps.form)
const as = normalizeAiCredsGroup(aiGroup) const as = normalizeAiCredsGroup(aiGroup)
Object.assign(aiForm, as.form) Object.assign(aiForm, as.form)
aiApiKeyMasked.value = as.apiKeyMasked aiApiKeyMasked.value = as.apiKeyMasked
aiApiKeyCleared.value = false aiApiKeyCleared.value = false
aiBaseline.value = JSON.stringify(as.form)
emailComment.value = '' emailComment.value = ''
pushComment.value = '' pushComment.value = ''
aiComment.value = '' aiComment.value = ''
@@ -481,6 +492,22 @@
{ immediate: true } { immediate: true }
) )
/** 未保存判定:三表单任一偏离基线,或密钥三态(输入非空 = 覆盖新值 /
* cleared = 显式清除)——密钥输入框基线恒空串,只能独立判定 */
const dirty = computed(
() =>
JSON.stringify(emailForm) !== emailBaseline.value ||
emailForm.smtpPassword !== '' ||
emailPasswordCleared.value ||
JSON.stringify(pushForm) !== pushBaseline.value ||
pushForm.apiKey !== '' ||
pushApiKeyCleared.value ||
JSON.stringify(aiForm) !== aiBaseline.value ||
aiForm.apiKey !== '' ||
aiApiKeyCleared.value
)
watch(dirty, (v) => emit('dirty', v), { immediate: true })
// ---- 保存(前端预检 → 整组 PUT;密钥不动回哨兵保持库值) ------------------------------ // ---- 保存(前端预检 → 整组 PUT;密钥不动回哨兵保持库值) ------------------------------
async function onSaveEmail(): Promise<void> { async function onSaveEmail(): Promise<void> {
@@ -42,7 +42,7 @@
</div> </div>
</ElForm> </ElForm>
<ElCollapse class="seed-collapse"> <ElCollapse v-model="openSections" class="seed-collapse">
<ElCollapseItem name="seed" title="会话上下文映射(context_seed_keys · 高级)"> <ElCollapseItem name="seed" title="会话上下文映射(context_seed_keys · 高级)">
<p class="section-note"> <p class="section-note">
产品字段 → 会话上下文种子键的映射;空行保存时自动剔除,清空全部行 = 产品字段 → 会话上下文种子键的映射;空行保存时自动剔除,清空全部行 =
@@ -86,7 +86,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
RESET_GROUP_LABELS,
engineToForm, engineToForm,
engineToPayload, engineToPayload,
validateEngineForm, validateEngineForm,
@@ -116,6 +115,8 @@
): void ): void
/** footer 回滚入口:请求父层切到版本历史 */ /** footer 回滚入口:请求父层切到版本历史 */
(e: 'navigate', group: string): void (e: 'navigate', group: string): void
/** 未保存修改上报(Task 14:父层三处离开拦截消费) */
(e: 'dirty', value: boolean): void
}>() }>()
// ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置表单) ---------------------- // ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置表单) ----------------------
@@ -125,19 +126,27 @@
const saving = ref(false) const saving = ref(false)
const resetting = ref(false) const resetting = ref(false)
const actionError = ref('') const actionError = ref('')
/** 高级折叠区默认收起(引擎级参数——按需展开) */ /** 高级折叠区展开项(默认收起——引擎级参数按需展开) */
const openSections = ref<string[]>([]) const openSections = ref<string[]>([])
/** dirty 基线(与表单重建同 watch 刷新——KeyValueEditor 行变化经
* form.seedRows 引用/行内变更自然涵盖) */
const baseline = ref(JSON.stringify(engineToForm(undefined)))
watch( watch(
() => props.group, () => props.group,
(group) => { (group) => {
Object.assign(form, engineToForm(group)) const next = engineToForm(group)
Object.assign(form, next)
baseline.value = JSON.stringify(next)
saveComment.value = '' saveComment.value = ''
actionError.value = '' actionError.value = ''
}, },
{ immediate: true } { immediate: true }
) )
const dirty = computed(() => JSON.stringify(form) !== baseline.value)
watch(dirty, (v) => emit('dirty', v), { immediate: true })
// ---- 保存(顺序纪律:两段校验合并 → payload → PUT 整组) ----------------------------- // ---- 保存(顺序纪律:两段校验合并 → payload → PUT 整组) -----------------------------
async function onSave(): Promise<void> { async function onSave(): Promise<void> {
@@ -180,9 +189,9 @@
try { try {
const res = await confirmAndResetGroup(props.slug, 'engine') const res = await confirmAndResetGroup(props.slug, 'engine')
if (!res) return // 用户取消确认 if (!res) return // 用户取消确认
ElMessage.success( // 重置成功 toast 用卡头标题(「聊天限流」)——RESET_GROUP_LABELS 的
`${RESET_GROUP_LABELS.engine}已重置出厂(版本 v${res.version},热生效)` // 「引擎」是后端组名镜像,与用户视角的卡头标题错位(Task 14 折入)
) ElMessage.success(`聊天限流已重置出厂(版本 v${res.version},热生效)`)
emit('saved', { version: res.version, group: 'engine', config: res.config }) emit('saved', { version: res.version, group: 'engine', config: res.config })
} catch (error) { } catch (error) {
actionError.value = error instanceof Error ? error.message : '重置失败' actionError.value = error instanceof Error ? error.message : '重置失败'
+14 -5
View File
@@ -59,7 +59,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
RESET_GROUP_LABELS,
miscToForm, miscToForm,
miscToPayload, miscToPayload,
validateKeyValueRows, validateKeyValueRows,
@@ -88,6 +87,8 @@
): void ): void
/** footer 回滚入口:请求父层切到版本历史 */ /** footer 回滚入口:请求父层切到版本历史 */
(e: 'navigate', group: string): void (e: 'navigate', group: string): void
/** 未保存修改上报(Task 14:父层三处离开拦截消费) */
(e: 'dirty', value: boolean): void
}>() }>()
// ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置表单) ---------------------- // ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置表单) ----------------------
@@ -97,17 +98,25 @@
const saving = ref(false) const saving = ref(false)
const resetting = ref(false) const resetting = ref(false)
const actionError = ref('') const actionError = ref('')
/** dirty 基线(与表单重建同 watch 刷新——KeyValueEditor 行变化经
* form.extrasRows 引用/行内变更自然涵盖) */
const baseline = ref(JSON.stringify(miscToForm(undefined)))
watch( watch(
() => props.group, () => props.group,
(group) => { (group) => {
Object.assign(form, miscToForm(group)) const next = miscToForm(group)
Object.assign(form, next)
baseline.value = JSON.stringify(next)
saveComment.value = '' saveComment.value = ''
actionError.value = '' actionError.value = ''
}, },
{ immediate: true } { immediate: true }
) )
const dirty = computed(() => JSON.stringify(form) !== baseline.value)
watch(dirty, (v) => emit('dirty', v), { immediate: true })
// ---- 保存(validateKeyValueRows → payload → PUT 整组) ------------------------------- // ---- 保存(validateKeyValueRows → payload → PUT 整组) -------------------------------
async function onSave(): Promise<void> { async function onSave(): Promise<void> {
@@ -147,9 +156,9 @@
try { try {
const res = await confirmAndResetGroup(props.slug, 'misc') const res = await confirmAndResetGroup(props.slug, 'misc')
if (!res) return // 用户取消确认 if (!res) return // 用户取消确认
ElMessage.success( // 重置成功 toast 用卡头标题(「国家区号映射」)——RESET_GROUP_LABELS 的
`${RESET_GROUP_LABELS.misc}已重置出厂(版本 v${res.version},热生效)` // 「杂项」是后端组名镜像,与用户视角的卡头标题错位(Task 14 折入)
) ElMessage.success(`国家区号映射已重置出厂(版本 v${res.version},热生效)`)
emit('saved', { version: res.version, group: 'misc', config: res.config }) emit('saved', { version: res.version, group: 'misc', config: res.config })
} catch (error) { } catch (error) {
actionError.value = error instanceof Error ? error.message : '重置失败' actionError.value = error instanceof Error ? error.message : '重置失败'
+11 -1
View File
@@ -183,6 +183,8 @@
(e: 'saved', payload: { version: number; group: 'prompts'; config: Record<string, unknown> }): void (e: 'saved', payload: { version: number; group: 'prompts'; config: Record<string, unknown> }): void
/** footer 回滚链接:请求父层切到版本历史组 */ /** footer 回滚链接:请求父层切到版本历史组 */
(e: 'navigate', group: string): void (e: 'navigate', group: string): void
/** 未保存修改上报(Task 14:父层三处离开拦截消费) */
(e: 'dirty', value: boolean): void
}>() }>()
// ---- 编辑态(group 引用变化 = 租户切换/重载 → 重置表单) ---------------------------- // ---- 编辑态(group 引用变化 = 租户切换/重载 → 重置表单) ----------------------------
@@ -191,17 +193,25 @@
const saveComment = ref('') const saveComment = ref('')
const saving = ref(false) const saving = ref(false)
const actionError = ref('') const actionError = ref('')
/** dirty 基线(与表单重建同 watch 刷新——保存/重置回写经父层快照组
* 引用变化走同一条链,无需手动清) */
const baseline = ref(JSON.stringify(normalizePromptsGroup(undefined)))
watch( watch(
() => props.group, () => props.group,
(group) => { (group) => {
Object.assign(draft, normalizePromptsGroup(group)) const next = normalizePromptsGroup(group)
Object.assign(draft, next)
baseline.value = JSON.stringify(next)
saveComment.value = '' saveComment.value = ''
actionError.value = '' actionError.value = ''
}, },
{ immediate: true } { immediate: true }
) )
const dirty = computed(() => JSON.stringify(draft) !== baseline.value)
watch(dirty, (v) => emit('dirty', v), { immediate: true })
// ---- 字节实时提示 ------------------------------------------------------------------- // ---- 字节实时提示 -------------------------------------------------------------------
const byteInfos = computed(() => { const byteInfos = computed(() => {
@@ -225,6 +225,8 @@
(e: 'saved', payload: { version: number; group: 'wall'; config: Record<string, unknown> }): void (e: 'saved', payload: { version: number; group: 'wall'; config: Record<string, unknown> }): void
/** footer 回滚链接:请求父层切到版本历史组 */ /** footer 回滚链接:请求父层切到版本历史组 */
(e: 'navigate', group: string): void (e: 'navigate', group: string): void
/** 未保存修改上报(Task 14:父层三处离开拦截消费) */
(e: 'dirty', value: boolean): void
}>() }>()
// ---- 编辑态(group 引用变化 = 租户切换/重载 → 重建模型) ---------------------------- // ---- 编辑态(group 引用变化 = 租户切换/重载 → 重建模型) ----------------------------
@@ -237,11 +239,17 @@
const testing = ref(false) const testing = ref(false)
const testResults = ref<WallTestResponse['results'] | null>(null) const testResults = ref<WallTestResponse['results'] | null>(null)
const lastWallSource = ref<WallTestResponse['wall_source'] | ''>('') const lastWallSource = ref<WallTestResponse['wall_source'] | ''>('')
/** dirty 对比基线:编辑模型的深克隆(JSON 往返脱离响应式;与模型重建
* 同 watch 刷新——保存/重置回写经父层快照组引用变化走同一条链) */
const baseline = ref<WallEditModel | null>(null)
watch( watch(
() => props.group, () => props.group,
(group) => { (group) => {
model.value = initWallEdit(group) model.value = initWallEdit(group)
baseline.value = model.value
? (JSON.parse(JSON.stringify(model.value)) as WallEditModel)
: null
saveComment.value = '' saveComment.value = ''
actionError.value = '' actionError.value = ''
testResults.value = null testResults.value = null
@@ -249,6 +257,17 @@
{ immediate: true } { immediate: true }
) )
/** 未保存判定:序列化等价物对比——buildWallPayload 产物不含 showComment
* 等瞬态 UI 字段(展开备注再收起不会误报 dirty) */
const dirty = computed(() => {
if (!model.value || !baseline.value) return false
return (
JSON.stringify(buildWallPayload(model.value)) !==
JSON.stringify(buildWallPayload(baseline.value))
)
})
watch(dirty, (v) => emit('dirty', v), { immediate: true })
// ---- 主面小卡白话标签 ------------------------------------------------------------------ // ---- 主面小卡白话标签 ------------------------------------------------------------------
const NO_CONTACT_LABELS: Record<(typeof NO_CONTACT_KEYS)[number], string> = { const NO_CONTACT_LABELS: Record<(typeof NO_CONTACT_KEYS)[number], string> = {
+64 -2
View File
@@ -34,6 +34,11 @@
退役通用 JSON 整组编辑(旧 JsonGroup 组件删除),换 ContactFormPane / 退役通用 JSON 整组编辑(旧 JsonGroup 组件删除),换 ContactFormPane /
EngineFormPane / MiscFormPane 三表单卡(ConfigCard 基线 + KeyValueEditor EngineFormPane / MiscFormPane 三表单卡(ConfigCard 基线 + KeyValueEditor
行编辑——engine context_seed_keys / misc name_to_region_extras 共用) 行编辑——engine context_seed_keys / misc name_to_region_extras 共用)
- Task 14 未保存离开保护(Batch 3 收口):七编辑面 dirty 上报(brand
内联基线快照 + 六 pane emit dirty)汇聚 paneDirty;三处拦截——切导航组
(onGroupSelect)/ 切租户(ElSelect 改 :model-value + change 前置确认,
取消时显示与草稿均还原)/ 路由离开(onBeforeRouteLeave)——
ElMessageBox「丢弃修改 / 留下」确认后才放行
- 挂件实时预览:**本地简化预览组件**(标题/颜色气泡/头像/话题/占位的 - 挂件实时预览:**本地简化预览组件**(标题/颜色气泡/头像/话题/占位的
静态模拟,零后端依赖、即时反映表单值;真机预览留给嵌入代码上真站) 静态模拟,零后端依赖、即时反映表单值;真机预览留给嵌入代码上真站)
- 嵌入代码:域名取租户详情 sites[].domains(超管可枚举全租户,租户管理 - 嵌入代码:域名取租户详情 sites[].domains(超管可枚举全租户,租户管理
@@ -57,10 +62,11 @@
<ElTag v-if="version !== null" type="info" size="large">当前版本 v{{ version }}</ElTag> <ElTag v-if="version !== null" type="info" size="large">当前版本 v{{ version }}</ElTag>
<ElSelect <ElSelect
v-if="isSuper" v-if="isSuper"
v-model="activeSlug" :model-value="activeSlug"
:loading="tenantsLoading" :loading="tenantsLoading"
placeholder="选择租户" placeholder="选择租户"
class="tenant-switcher" class="tenant-switcher"
@change="onTenantSwitch"
> >
<ElOption <ElOption
v-for="tenant in tenants" v-for="tenant in tenants"
@@ -431,6 +437,7 @@
:group="snapshot?.prompts" :group="snapshot?.prompts"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect" @navigate="onGroupSelect"
@dirty="childPaneDirty = $event"
/> />
</div> </div>
</template> </template>
@@ -453,6 +460,7 @@
:group="snapshot?.wall" :group="snapshot?.wall"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect" @navigate="onGroupSelect"
@dirty="childPaneDirty = $event"
/> />
</div> </div>
</template> </template>
@@ -477,6 +485,7 @@
:ai-group="snapshot?.ai" :ai-group="snapshot?.ai"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect" @navigate="onGroupSelect"
@dirty="childPaneDirty = $event"
/> />
</div> </div>
</template> </template>
@@ -499,6 +508,7 @@
:group="snapshot?.contact" :group="snapshot?.contact"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect" @navigate="onGroupSelect"
@dirty="childPaneDirty = $event"
/> />
</div> </div>
</template> </template>
@@ -521,6 +531,7 @@
:group="snapshot?.engine" :group="snapshot?.engine"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect" @navigate="onGroupSelect"
@dirty="childPaneDirty = $event"
/> />
</div> </div>
</template> </template>
@@ -543,6 +554,7 @@
:group="snapshot?.misc" :group="snapshot?.misc"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect" @navigate="onGroupSelect"
@dirty="childPaneDirty = $event"
/> />
</div> </div>
</template> </template>
@@ -564,6 +576,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteLeave } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { useAuthStore } from '@/store/modules/auth' import { useAuthStore } from '@/store/modules/auth'
import { fetchTenantDetail, type TenantDetailResponse } from '@/api/tenants' import { fetchTenantDetail, type TenantDetailResponse } from '@/api/tenants'
import { useTenantOptions } from '@/composables/useTenantOptions' import { useTenantOptions } from '@/composables/useTenantOptions'
@@ -638,7 +652,14 @@
const activeGroup = ref<GroupKey>('brand') const activeGroup = ref<GroupKey>('brand')
function onGroupSelect(index: string): void { /** 六 pane 的 dirty 上报值(pane 卸载不触发 emit——切组/切租户放行时
* 手动清残留;brand 内联表单不入此列,直接并入 paneDirty) */
const childPaneDirty = ref(false)
async function onGroupSelect(index: string): Promise<void> {
if (index === activeGroup.value) return
if (!(await confirmDiscard())) return
childPaneDirty.value = false
activeGroup.value = index as GroupKey activeGroup.value = index as GroupKey
} }
@@ -685,6 +706,32 @@
const saveComment = ref('') const saveComment = ref('')
const saving = ref(false) const saving = ref(false)
// ---- 未保存离开保护(Task 14:dirty 汇聚 + 三处拦截消费) ----------------------------
/** 品牌表单基线快照(加载/保存回写/重置/回滚/切租户清空后刷新) */
const brandBaseline = ref(JSON.stringify(form))
const brandDirty = computed(() => JSON.stringify(form) !== brandBaseline.value)
/** 全页未保存态:brand 内联表单 + 当前挂载 pane(切导航组/切租户/路由离开消费) */
const paneDirty = computed(() => brandDirty.value || childPaneDirty.value)
/** 离开确认:有未保存修改时弹「丢弃修改 / 留下」;取消(含 ESC/遮罩)= 留下 */
async function confirmDiscard(): Promise<boolean> {
if (!paneDirty.value) return true
try {
await ElMessageBox.confirm('有未保存的修改,离开将丢失。确定离开?', '未保存的修改', {
type: 'warning',
confirmButtonText: '丢弃修改',
cancelButtonText: '留下'
})
return true
} catch {
return false
}
}
// 路由离开拦截(切导航组/切租户另在 onGroupSelect/onTenantSwitch 内前置拦截)
onBeforeRouteLeave(async () => await confirmDiscard())
async function loadConfig(epoch: number): Promise<void> { async function loadConfig(epoch: number): Promise<void> {
const slug = activeSlug.value const slug = activeSlug.value
if (!slug) return if (!slug) return
@@ -696,6 +743,7 @@
version.value = res.version version.value = res.version
snapshot.value = res.config snapshot.value = res.config
Object.assign(form, normalizeBrandGroup(res.config.brand)) Object.assign(form, normalizeBrandGroup(res.config.brand))
brandBaseline.value = JSON.stringify(form)
configLoaded.value = true configLoaded.value = true
avatarImgFailed.value = false avatarImgFailed.value = false
} catch (error) { } catch (error) {
@@ -731,6 +779,7 @@
version.value = payload.version version.value = payload.version
snapshot.value = payload.config snapshot.value = payload.config
Object.assign(form, normalizeBrandGroup(payload.config.brand)) Object.assign(form, normalizeBrandGroup(payload.config.brand))
brandBaseline.value = JSON.stringify(form)
avatarImgFailed.value = false avatarImgFailed.value = false
} }
@@ -751,6 +800,7 @@
snapshot.value = { ...snapshot.value, brand: res.config } snapshot.value = { ...snapshot.value, brand: res.config }
} }
Object.assign(form, normalizeBrandGroup(res.config)) Object.assign(form, normalizeBrandGroup(res.config))
brandBaseline.value = JSON.stringify(form)
avatarImgFailed.value = false avatarImgFailed.value = false
} }
ElMessage.success(`品牌已重置出厂(版本 v${res.version},热生效)`) ElMessage.success(`品牌已重置出厂(版本 v${res.version},热生效)`)
@@ -798,6 +848,7 @@
version.value = res.version version.value = res.version
// 响应即该组掩码视图(品牌组无密钥字段)——回写表单保持与库一致 // 响应即该组掩码视图(品牌组无密钥字段)——回写表单保持与库一致
Object.assign(form, normalizeBrandGroup(res.config)) Object.assign(form, normalizeBrandGroup(res.config))
brandBaseline.value = JSON.stringify(form)
avatarImgFailed.value = false avatarImgFailed.value = false
ElMessage.success(`品牌已保存(版本 v${res.version},热生效)`) ElMessage.success(`品牌已保存(版本 v${res.version},热生效)`)
} catch { } catch {
@@ -955,9 +1006,20 @@
// ---- 装配(租户切换:重置表单/版本/嵌入选择,配置+详情双载) ------------------------------ // ---- 装配(租户切换:重置表单/版本/嵌入选择,配置+详情双载) ------------------------------
/** 切租户前置确认(Task 14):ElSelect 单向 :model-value——取消时 prop
* 未变,选择器显示自动还原旧租户且草稿保留;确认后才写 activeSlug 触发
* 下方 watch 重载(草稿随重载丢弃) */
async function onTenantSwitch(slug: string): Promise<void> {
if (!slug || slug === activeSlug.value) return
if (!(await confirmDiscard())) return
childPaneDirty.value = false
activeSlug.value = slug
}
watch(activeSlug, () => { watch(activeSlug, () => {
if (!activeSlug.value) return if (!activeSlug.value) return
Object.assign(form, blankForm()) Object.assign(form, blankForm())
brandBaseline.value = JSON.stringify(form)
version.value = null version.value = null
configLoaded.value = false configLoaded.value = false
configError.value = '' configError.value = ''