feat: 配置中心-品牌(表单/本地预览/嵌入代码/头像上传)+ 站点覆盖层编辑入口
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<!--
|
||||
站点门面覆盖层表单字段(Task 7a 从 tenants/list 的「新建站点」弹窗提取共用)
|
||||
- 消费方:新建站点弹窗 / 编辑门面弹窗(同一字段清单与留空=继承语义)
|
||||
- props.form 为父组件持有的 reactive 表单对象,本组件就地双向绑定其字段
|
||||
(不整体替换引用——父组件保留 slug/name 等自有字段的所有权)
|
||||
- 留空 = 不写入覆盖 = 继承租户默认品牌(brand 组);收集逻辑见 model.ts
|
||||
-->
|
||||
<template>
|
||||
<div class="overlay-fields">
|
||||
<div class="overlay-header">
|
||||
<span class="overlay-title">门面覆盖层</span>
|
||||
<span class="overlay-sub">以下字段留空 = 继承租户默认品牌(brand 组)</span>
|
||||
</div>
|
||||
<div class="overlay-grid">
|
||||
<ElFormItem v-for="field in OVERLAY_FIELDS" :key="field.key" :label="field.label">
|
||||
<ElInput
|
||||
v-model="form[field.key]"
|
||||
:placeholder="field.key === 'color' ? '#066fc0(留空继承)' : '留空=继承租户默认'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
<ElFormItem label="快捷话题 topics">
|
||||
<ElSelect
|
||||
v-model="form.topics"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:reserve-keyword="false"
|
||||
placeholder="输入后回车添加;留空 = 继承租户默认"
|
||||
class="overlay-topics"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OVERLAY_FIELDS, type OverlayFormState } from './model'
|
||||
|
||||
defineOptions({ name: 'SiteOverlayFields' })
|
||||
|
||||
defineProps<{ form: OverlayFormState }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.overlay-fields {
|
||||
.overlay-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin: 4px 0 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--art-card-border);
|
||||
|
||||
.overlay-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.overlay-sub {
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.overlay-topics {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 站点门面覆盖层字段模型(Task 6 建立;Task 7a 提取共用)
|
||||
*
|
||||
* 消费方:站点管理页(tenants/list)的「新建站点」与「编辑门面」弹窗——
|
||||
* 两处共用同一字段清单 / 空表单 / 提交收集逻辑。
|
||||
*
|
||||
* 语义(与后端 tenant_routes.patch_site 的整字典赋值对齐):
|
||||
* - buildOverlay 只收集非空字段——留空 = 剔除该键的覆盖 = 继承租户默认
|
||||
* 品牌(brand 组);旧键不残留(整字典替换,M-2a)。
|
||||
* - overlay 值是 brand 组子集(站点级覆盖,public.py 的 site.brand 深合并)。
|
||||
*/
|
||||
|
||||
/** 门面覆盖层字符串字段(brand 组子集) */
|
||||
export const OVERLAY_FIELDS = [
|
||||
{ key: 'title', label: '标题 title' },
|
||||
{ key: 'color', label: '主题色 color' },
|
||||
{ key: 'avatar', label: '头像路径 avatar' },
|
||||
{ key: 'welcome', label: '欢迎语 welcome' },
|
||||
{ key: 'placeholder', label: '输入框占位 placeholder' },
|
||||
{ key: 'whatsapp', label: 'WhatsApp' },
|
||||
{ key: 'email', label: '邮箱 email' },
|
||||
{ key: 'privacy_url', label: '隐私政策 URL' }
|
||||
] as const
|
||||
|
||||
/** 覆盖层字段 key 联合类型 */
|
||||
export type OverlayFieldKey = (typeof OVERLAY_FIELDS)[number]['key']
|
||||
|
||||
/** 覆盖层表单状态(字符串字段全空串语义 + topics 数组) */
|
||||
export interface OverlayFormState extends Record<OverlayFieldKey, string> {
|
||||
topics: string[]
|
||||
}
|
||||
|
||||
/** 覆盖层字段中文标签(站点表 tooltip 用;topics 单列) */
|
||||
export const OVERLAY_LABELS: Record<string, string> = Object.fromEntries(
|
||||
OVERLAY_FIELDS.map((f) => [f.key, f.label] as [string, string]).concat([
|
||||
['topics', '快捷话题 topics']
|
||||
])
|
||||
)
|
||||
|
||||
/** 空表单(全留空 = 完全继承租户默认) */
|
||||
export function blankOverlayForm(): OverlayFormState {
|
||||
return {
|
||||
title: '',
|
||||
color: '',
|
||||
avatar: '',
|
||||
welcome: '',
|
||||
placeholder: '',
|
||||
whatsapp: '',
|
||||
email: '',
|
||||
privacy_url: '',
|
||||
topics: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单 → brand_overlay 提交字典:只收集非空字段(留空 = 不覆盖)。
|
||||
* 全空表单产出 {} = 清空覆盖层(整字典赋值语义下回到完全继承)。
|
||||
*/
|
||||
export function buildOverlay(form: OverlayFormState): Record<string, unknown> {
|
||||
const overlay: Record<string, unknown> = {}
|
||||
for (const field of OVERLAY_FIELDS) {
|
||||
const value = form[field.key].trim()
|
||||
if (value) overlay[field.key] = value
|
||||
}
|
||||
const topics = form.topics.map((t) => t.trim()).filter(Boolean)
|
||||
if (topics.length > 0) overlay.topics = topics
|
||||
return overlay
|
||||
}
|
||||
|
||||
/**
|
||||
* overlay 落库值 → 表单预填(编辑门面用):只认已知键的字符串值;
|
||||
* 返回表单之外未能识别的键(保存时会被整字典替换丢弃,调用方提示用户)。
|
||||
*/
|
||||
export function overlayToForm(
|
||||
overlay: Record<string, unknown> | null | undefined
|
||||
): { form: OverlayFormState; unknownKeys: string[] } {
|
||||
const form = blankOverlayForm()
|
||||
const source = overlay ?? {}
|
||||
for (const field of OVERLAY_FIELDS) {
|
||||
const value = source[field.key]
|
||||
if (typeof value === 'string') form[field.key] = value
|
||||
}
|
||||
if (Array.isArray(source.topics)) {
|
||||
form.topics = source.topics.filter((t): t is string => typeof t === 'string')
|
||||
}
|
||||
const known = new Set<string>([...OVERLAY_FIELDS.map((f) => f.key), 'topics'])
|
||||
const unknownKeys = Object.keys(source).filter((k) => !known.has(k))
|
||||
return { form, unknownKeys }
|
||||
}
|
||||
Reference in New Issue
Block a user