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