feat(admin): 品牌卡套视觉基线(Batch 1 收口)

- 品牌表单 ElCard → ConfigCard(图标/副标题/信息条/footer/danger 行);
  表单双列 grid(标题|主色、WhatsApp|邮箱 同排;头像/欢迎语/话题/占位/隐私 span-2);
  保存备注移 footer-right、重置出厂移 #danger(既有 resetting/onResetBrand 逻辑不动)
- 预览卡头加副标题「本地简化预览(真机效果以嵌入站点为准)」(cc-subtitle 观感)
- CredsPane 挂载处补 @navigate=onGroupSelect 接线;email 卡 info 去「带 • 的」前缀
  (push/ai 卡核实无该串);头注释同步收口
- 剪除 pane 条件链尾部 v-else 占位死分支(activeGroupLabel/ALL_NAV_ITEMS/
  .placeholder-card/.save-bar 等配套同清)
- form-grid 样式与 CredsPane 同款规则就地复用(不建共用文件)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 18:34:09 +08:00
parent 20ab561859
commit 680c4bade6
2 changed files with 151 additions and 163 deletions
+2 -2
View File
@@ -19,7 +19,7 @@
前缀被其他租户占用的 400 detail 内联展示 前缀被其他租户占用的 400 detail 内联展示
- Task 5 视觉基线:三卡套 ConfigCard(图标/副标题/信息条/footer/danger - Task 5 视觉基线:三卡套 ConfigCard(图标/副标题/信息条/footer/danger
行);footer-left「出问题?回滚 →」emit navigate('versions')(父层 行);footer-left「出问题?回滚 →」emit navigate('versions')(父层
接线在后续任务);表单区双列 grid index.vue 已接线);表单区双列 grid
--> -->
<template> <template>
<div class="creds-pane"> <div class="creds-pane">
@@ -28,7 +28,7 @@
icon="solar:letter-linear" icon="solar:letter-linear"
title="邮件通知" title="邮件通知"
subtitle="客户留下线索后,系统用这组配置发通知邮件" subtitle="客户留下线索后,系统用这组配置发通知邮件"
info="整组保存 · 立即生效 · 产生新版本可回滚;带 • 的字段留空 = 沿用平台默认" info="整组保存 · 立即生效 · 产生新版本可回滚;字段留空 = 沿用平台默认"
danger-label="恢复出厂:把邮件配置重置为初始档案值" danger-label="恢复出厂:把邮件配置重置为初始档案值"
> >
<ElAlert <ElAlert
+149 -161
View File
@@ -22,10 +22,14 @@
详情抽屉整份 JSON(密钥 '__SET__' 掩码);勾两版并排对比(spec §7.3 详情抽屉整份 JSON(密钥 '__SET__' 掩码);勾两版并排对比(spec §7.3
最低形态,范围可聚焦单组);回滚确认(明示产生新版本/历史不变)→ 最低形态,范围可聚焦单组);回滚确认(明示产生新版本/历史不变)→
POST rollback → 响应内新快照直接回写本页配置态 POST rollback → 响应内新快照直接回写本页配置态
- 组级重置出厂(Task 7d):各编辑卡头部危险色小按钮(品牌在本页; - 组级重置出厂(Task 7d):各编辑卡危险行小按钮(品牌在本页;
提示词/词表/凭据三组在各 pane)→ 确认框明示组名与「恢复出厂档案值」 提示词/词表/凭据三组在各 pane)→ 确认框明示组名与「恢复出厂档案值」
→ POST config/{group}/reset → 该组表单/版本刷新(合法组清单 = → POST config/{group}/reset → 该组表单/版本刷新(合法组清单 =
后端 CONFIG_GROUPS 九组,reset.ts 的 RESET_GROUP_LABELS 镜像) 后端 CONFIG_GROUPS 九组,reset.ts 的 RESET_GROUP_LABELS 镜像)
- Task 6 视觉基线(feature/config-ux Batch 1 收口):品牌表单卡套
ConfigCard(表单双列 grid + footer 回滚/备注/保存 + danger 重置行);
预览卡头加副标题;CredsPane @navigate 接线 onGroupSelect;
8 组全实装后 v-else 占位死分支剪除(activeGroupLabel/ALL_NAV_ITEMS 同清)
- 挂件实时预览:**本地简化预览组件**(标题/颜色气泡/头像/话题/占位的 - 挂件实时预览:**本地简化预览组件**(标题/颜色气泡/头像/话题/占位的
静态模拟,零后端依赖、即时反映表单值;真机预览留给嵌入代码上真站) 静态模拟,零后端依赖、即时反映表单值;真机预览留给嵌入代码上真站)
- 嵌入代码:域名取租户详情 sites[].domains(超管可枚举全租户,租户管理 - 嵌入代码:域名取租户详情 sites[].domains(超管可枚举全租户,租户管理
@@ -127,141 +131,150 @@
</ElAlert> </ElAlert>
<template v-else> <template v-else>
<!-- 品牌表单 --> <!-- 品牌表单(Task 6:套 ConfigCard 视觉基线——表单双列 grid + footer/danger 行) -->
<ElCard shadow="never" class="form-card"> <ConfigCard
<template #header> class="form-card"
<div class="card-header"> icon="solar:palette-round-line-duotone"
<span class="card-title">品牌(brand 组 · 整组替换语义)</span> title="品牌与外观"
<div class="header-side"> subtitle="客户看到的聊天窗口长什么样"
<span class="card-sub">保存即写新版本并热生效</span> info="整组保存 · 立即生效;图片与链接留空 = 不展示"
<ElButton danger-label="恢复出厂:把品牌配置重置为初始档案值"
size="small" >
type="danger"
plain
:loading="resetting"
@click="onResetBrand"
>
重置出厂
</ElButton>
</div>
</div>
</template>
<ElForm label-width="110px" @submit.prevent> <ElForm label-width="110px" @submit.prevent>
<ElFormItem label="标题 title"> <div class="form-grid">
<ElInput <ElFormItem label="标题 title">
v-model="form.title"
maxlength="128"
placeholder="挂件标题栏文案(如 HESUPIPE Support)"
/>
</ElFormItem>
<ElFormItem label="主题色 color">
<div class="color-row">
<ElColorPicker v-model="form.color" />
<ElInput <ElInput
v-model="form.color" v-model="form.title"
placeholder="#066fc0(3/6 位十六进制,留空取挂件默认)" maxlength="128"
class="color-input" placeholder="挂件标题栏文案(如 HESUPIPE Support)"
/> />
</div> </ElFormItem>
</ElFormItem> <ElFormItem label="主题色 color">
<ElFormItem label="头像 avatar"> <div class="color-row">
<div class="avatar-row"> <ElColorPicker v-model="form.color" />
<div class="avatar-box"> <ElInput
<img v-model="form.color"
v-if="avatarUrl && !avatarImgFailed" placeholder="#066fc0(3/6 位十六进制,留空取挂件默认)"
:src="avatarUrl" class="color-input"
alt="品牌头像"
@error="avatarImgFailed = true"
/> />
<span
v-else
class="avatar-letter"
:style="{ background: previewColor }"
>
{{ avatarLetter }}
</span>
</div> </div>
<div class="avatar-actions"> </ElFormItem>
<ElButton size="small" :loading="uploading" @click="pickAvatar"> <ElFormItem label="头像 avatar" class="span-2">
上传新头像 <div class="avatar-row">
</ElButton> <div class="avatar-box">
<p class="avatar-tip">png / jpg / webp,≤2MB(后端按文件内容校验)</p> <img
<p v-if="form.avatar" class="avatar-value mono">{{ form.avatar }}</p> v-if="avatarUrl && !avatarImgFailed"
:src="avatarUrl"
alt="品牌头像"
@error="avatarImgFailed = true"
/>
<span
v-else
class="avatar-letter"
:style="{ background: previewColor }"
>
{{ avatarLetter }}
</span>
</div>
<div class="avatar-actions">
<ElButton size="small" :loading="uploading" @click="pickAvatar">
上传新头像
</ElButton>
<p class="avatar-tip">png / jpg / webp,≤2MB(后端按文件内容校验)</p>
<p v-if="form.avatar" class="avatar-value mono">{{ form.avatar }}</p>
</div>
</div> </div>
</div> <input
<input ref="avatarInput"
ref="avatarInput" type="file"
type="file" accept=".png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp"
accept=".png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp" hidden
hidden @change="onAvatarChange"
@change="onAvatarChange" />
/> </ElFormItem>
</ElFormItem> <ElFormItem label="欢迎语 welcome" class="span-2">
<ElFormItem label="欢迎语 welcome"> <ElInput
<ElInput v-model="form.welcome"
v-model="form.welcome" type="textarea"
type="textarea" :rows="2"
:rows="2" maxlength="500"
maxlength="500" placeholder="挂件打开时的第一条消息"
placeholder="挂件打开时的第一条消息" />
/> </ElFormItem>
</ElFormItem> <ElFormItem label="快捷话题 topics" class="span-2">
<ElFormItem label="快捷话题 topics"> <ElSelect
<ElSelect v-model="form.topics"
v-model="form.topics" multiple
multiple filterable
filterable allow-create
allow-create default-first-option
default-first-option :reserve-keyword="false"
:reserve-keyword="false" placeholder="输入后回车添加(字符串数组,随品牌下发)"
placeholder="输入后回车添加(字符串数组,随品牌下发)" class="topics-select"
class="topics-select" />
/> </ElFormItem>
</ElFormItem> <ElFormItem label="输入框占位" class="span-2">
<ElFormItem label="输入框占位"> <ElInput
<ElInput v-model="form.placeholder"
v-model="form.placeholder" maxlength="200"
maxlength="200" placeholder="聊天输入框 placeholder 文案"
placeholder="聊天输入框 placeholder 文案" />
/> </ElFormItem>
</ElFormItem> <ElFormItem label="WhatsApp">
<ElFormItem label="WhatsApp"> <ElInput v-model="form.whatsapp" placeholder="+8613900000000 或 wa.me 链接" />
<ElInput v-model="form.whatsapp" placeholder="+8613900000000 或 wa.me 链接" /> </ElFormItem>
</ElFormItem> <ElFormItem label="邮箱 email">
<ElFormItem label="邮箱 email"> <ElInput v-model="form.email" placeholder="页脚展示邮箱" />
<ElInput v-model="form.email" placeholder="页脚展示邮箱" /> </ElFormItem>
</ElFormItem> <ElFormItem label="隐私政策 URL" class="span-2">
<ElFormItem label="隐私政策 URL"> <ElInput v-model="form.privacy_url" placeholder="https://example.com/privacy" />
<ElInput v-model="form.privacy_url" placeholder="https://example.com/privacy" /> </ElFormItem>
</ElFormItem> </div>
<ElFormItem label="保存备注">
<ElInput
v-model="saveComment"
maxlength="200"
placeholder="可选;写入版本历史 comment"
/>
</ElFormItem>
</ElForm> </ElForm>
<div class="save-bar"> <template #footer-left>
<ElButton link type="primary" size="small" @click="onGroupSelect('versions')">
出问题?回滚 →
</ElButton>
</template>
<template #footer-right>
<ElInput
v-model="saveComment"
maxlength="200"
placeholder="备注(选填)"
class="comment-input"
/>
<ElButton <ElButton
type="primary" type="primary"
:loading="saving" :loading="saving"
:disabled="!configLoaded" :disabled="!configLoaded"
@click="onSave" @click="onSave"
> >
保存品牌组(整组提交,热生效) 保存
</ElButton> </ElButton>
</div> </template>
</ElCard> <template #danger>
<ElButton
size="small"
type="danger"
plain
:loading="resetting"
@click="onResetBrand"
>
重置出厂…
</ElButton>
</template>
</ConfigCard>
<!-- 右列:本地预览 + 嵌入代码 --> <!-- 右列:本地预览 + 嵌入代码 -->
<div class="side-col"> <div class="side-col">
<ElCard shadow="never" class="preview-card"> <ElCard shadow="never" class="preview-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title">挂件预览</span> <div class="card-heading">
<span class="card-title">挂件预览</span>
<p class="card-subtitle">本地简化预览(真机效果以嵌入站点为准)</p>
</div>
<span class="card-sub">本地简化模拟,随表单即时变化</span> <span class="card-sub">本地简化模拟,随表单即时变化</span>
</div> </div>
</template> </template>
@@ -457,6 +470,7 @@
:push-group="snapshot?.push" :push-group="snapshot?.push"
:ai-group="snapshot?.ai" :ai-group="snapshot?.ai"
@saved="onPaneSaved" @saved="onPaneSaved"
@navigate="onGroupSelect"
/> />
</div> </div>
</template> </template>
@@ -494,11 +508,6 @@
/> />
</div> </div>
</template> </template>
<!-- 其余组占位 -->
<ElCard v-else shadow="never" class="placeholder-card">
<ElEmpty :description="`${activeGroupLabel}——后续任务实装`" />
</ElCard>
</div> </div>
</ElCard> </ElCard>
</div> </div>
@@ -515,6 +524,7 @@
uploadBrandAvatar, uploadBrandAvatar,
type BrandGroupValue type BrandGroupValue
} from '@/api/config' } from '@/api/config'
import ConfigCard from './ConfigCard.vue'
import PromptsPane from './PromptsPane.vue' import PromptsPane from './PromptsPane.vue'
import WallPane from './WallPane.vue' import WallPane from './WallPane.vue'
import CredsPane from './CredsPane.vue' import CredsPane from './CredsPane.vue'
@@ -575,12 +585,6 @@
sub: '改坏了?这里一键回滚' sub: '改坏了?这里一键回滚'
} }
/** 全部导航项扁平表(activeGroupLabel / 查找用) */
const ALL_NAV_ITEMS: NavItem[] = [
...NAV_SECTIONS.flatMap((s) => s.items),
VERSIONS_NAV
]
/** 通用 JSON 组(终审 I-1:contact/engine/misc 走 JsonGroupPane 一份实现) */ /** 通用 JSON 组(终审 I-1:contact/engine/misc 走 JsonGroupPane 一份实现) */
const JSON_GROUPS = new Set<GroupKey>(['contact', 'engine', 'misc']) const JSON_GROUPS = new Set<GroupKey>(['contact', 'engine', 'misc'])
@@ -597,10 +601,6 @@
activeGroup.value = index as GroupKey activeGroup.value = index as GroupKey
} }
const activeGroupLabel = computed(
() => ALL_NAV_ITEMS.find((i) => i.key === activeGroup.value)?.label ?? ''
)
// ---- 租户上下文(超管可切换;租户管理员固定自己——useTenantOptions 收拢) ---------------- // ---- 租户上下文(超管可切换;租户管理员固定自己——useTenantOptions 收拢) ----------------
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -1056,32 +1056,23 @@
.form-card { .form-card {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
}
.card-header { // 表单区双列 grid(span-2 = 独占整行:头像/多选/长文本)——与 CredsPane 同款规则
display: flex; .form-grid {
align-items: baseline; display: grid;
justify-content: space-between; grid-template-columns: 1fr 1fr;
gap: 12px; column-gap: 16px;
.card-title { .span-2 {
font-weight: 600; grid-column: span 2;
}
.card-sub {
font-size: 12px;
color: var(--art-gray-400);
}
// 头部右列:副标题 + 重置出厂小按钮(Task 7d)
.header-side {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
} }
} }
.comment-input {
width: 220px;
}
.color-row { .color-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -1152,12 +1143,6 @@
font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
} }
.save-bar {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
// ---- 右列:预览 + 嵌入 -------------------------------------------------------------- // ---- 右列:预览 + 嵌入 --------------------------------------------------------------
.side-col { .side-col {
@@ -1179,6 +1164,13 @@
font-weight: 600; font-weight: 600;
} }
// 预览卡标题下方副标题(Task 6——观感同 ConfigCard 的 cc-subtitle)
.card-subtitle {
margin: 2px 0 0;
font-size: 12px;
color: var(--art-gray-500, var(--el-text-color-secondary));
}
.card-sub { .card-sub {
font-size: 12px; font-size: 12px;
color: var(--art-gray-400); color: var(--art-gray-400);
@@ -1373,10 +1365,6 @@
color: var(--art-gray-400); color: var(--art-gray-400);
} }
.placeholder-card {
flex: 1;
}
// ---- 提示词/词表 pane 容器(Task 7b) ------------------------------------------------ // ---- 提示词/词表 pane 容器(Task 7b) ------------------------------------------------
.group-pane { .group-pane {