feat: 配置中心-品牌(表单/本地预览/嵌入代码/头像上传)+ 站点覆盖层编辑入口
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
||||
全局唯一 → 400 引导改 blank 或先改占用方)→ ④管理员账号 → 创建
|
||||
(201 后列表刷新 + 提示"租户已创建,管理员首登需改密")
|
||||
- 站点管理(详情抽屉内):站点列表/新建(门面覆盖层表单,留空=继承租户
|
||||
默认)/停用复启用/域名绑定解绑(撞车 400 的归属信息内联展示)
|
||||
默认)/编辑门面(Task 7a:PATCH brand_overlay 整字典,留空剔除,共用
|
||||
SiteOverlayFields 组件)/停用复启用/域名绑定解绑(撞车 400 的归属信息
|
||||
内联展示)
|
||||
- 停用后果文案(Phase 2 语义):租户停用=官网挂件 503 服务暂停页;
|
||||
站点停用=该站点域名 404(不是服务暂停)——确认框写明
|
||||
- slug 不可变(后端 PATCH 带 slug 键 400)——界面不提供改 slug 入口
|
||||
@@ -203,8 +205,11 @@
|
||||
<span v-else class="muted">继承租户默认</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right">
|
||||
<ElTableColumn label="操作" width="216" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="primary" link size="small" @click="openOverlayEdit(row)">
|
||||
编辑门面
|
||||
</ElButton>
|
||||
<ElButton
|
||||
:type="row.status === 'active' ? 'danger' : 'success'"
|
||||
link
|
||||
@@ -395,33 +400,10 @@
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<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="siteForm[field.key]"
|
||||
:placeholder="field.key === 'color' ? '#066fc0(留空继承)' : '留空=继承租户默认'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
<ElFormItem label="快捷话题 topics">
|
||||
<ElSelect
|
||||
v-model="siteForm.topics"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:reserve-keyword="false"
|
||||
placeholder="输入后回车添加;留空 = 继承租户默认"
|
||||
class="overlay-topics"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<SiteOverlayFields :form="siteForm" />
|
||||
</ElForm>
|
||||
<p class="dialog-tip">
|
||||
站点创建后,域名经站点列表的「绑定域名」挂载;覆盖层可随时整组更新。
|
||||
站点创建后,域名经站点列表的「绑定域名」挂载;覆盖层可随时经「编辑门面」整组更新。
|
||||
</p>
|
||||
<template #footer>
|
||||
<ElButton @click="siteDialogVisible = false">取消</ElButton>
|
||||
@@ -429,6 +411,35 @@
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- ==================== 编辑门面覆盖层弹窗(Task 7a 顺手项) ==================== -->
|
||||
<ElDialog
|
||||
v-model="overlayEditVisible"
|
||||
:title="`编辑门面覆盖层(站点 ${overlayEditSiteSlug})`"
|
||||
width="620px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm label-position="top" @submit.prevent>
|
||||
<SiteOverlayFields :form="overlayEditForm" />
|
||||
</ElForm>
|
||||
<ElAlert
|
||||
v-if="overlayUnknownKeys.length > 0"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="overlay-edit-alert"
|
||||
:title="`覆盖层含未支持编辑的键:${overlayUnknownKeys.join('、')}——保存将整组替换,未识别键会被移除`"
|
||||
/>
|
||||
<p class="dialog-tip">
|
||||
保存按整组替换提交:留空字段即剔除覆盖、恢复继承租户默认品牌(与新建站点同一语义)。
|
||||
</p>
|
||||
<template #footer>
|
||||
<ElButton @click="overlayEditVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="overlayEditSaving" @click="onSaveOverlay">
|
||||
保存
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- ==================== 绑定域名弹窗 ==================== -->
|
||||
<ElDialog
|
||||
v-model="domainDialogVisible"
|
||||
@@ -466,6 +477,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import SiteOverlayFields from '@/components/business/SiteOverlayFields/index.vue'
|
||||
import {
|
||||
OVERLAY_LABELS,
|
||||
blankOverlayForm,
|
||||
buildOverlay,
|
||||
overlayToForm,
|
||||
type OverlayFormState
|
||||
} from '@/components/business/SiteOverlayFields/model'
|
||||
import {
|
||||
addSiteDomain,
|
||||
createSite,
|
||||
@@ -505,25 +524,6 @@
|
||||
}
|
||||
] as const
|
||||
|
||||
/** 门面覆盖层字符串字段(brand 组子集;留空=不写入 overlay=继承租户默认) */
|
||||
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
|
||||
|
||||
/** 覆盖层字段中文标签(站点表 tooltip 用;topics 单列) */
|
||||
const OVERLAY_LABELS: Record<string, string> = Object.fromEntries(
|
||||
OVERLAY_FIELDS.map((f) => [f.key, f.label] as [string, string]).concat([
|
||||
['topics', '快捷话题 topics']
|
||||
])
|
||||
)
|
||||
|
||||
function overlayKeyLabel(key: string): string {
|
||||
return OVERLAY_LABELS[key] ?? key
|
||||
}
|
||||
@@ -771,20 +771,8 @@
|
||||
const siteDialogVisible = ref(false)
|
||||
const siteCreating = ref(false)
|
||||
|
||||
function blankSiteForm() {
|
||||
return {
|
||||
slug: '',
|
||||
name: '',
|
||||
title: '',
|
||||
color: '',
|
||||
avatar: '',
|
||||
welcome: '',
|
||||
placeholder: '',
|
||||
whatsapp: '',
|
||||
email: '',
|
||||
privacy_url: '',
|
||||
topics: [] as string[]
|
||||
}
|
||||
function blankSiteForm(): OverlayFormState & { slug: string; name: string } {
|
||||
return { slug: '', name: '', ...blankOverlayForm() }
|
||||
}
|
||||
|
||||
const siteForm = reactive(blankSiteForm())
|
||||
@@ -794,18 +782,6 @@
|
||||
siteDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 只收集非空字段进 brand_overlay——留空=不覆盖=继承租户默认 */
|
||||
function buildOverlay(): Record<string, unknown> {
|
||||
const overlay: Record<string, unknown> = {}
|
||||
for (const field of OVERLAY_FIELDS) {
|
||||
const value = siteForm[field.key].trim()
|
||||
if (value) overlay[field.key] = value
|
||||
}
|
||||
const topics = siteForm.topics.map((t) => t.trim()).filter(Boolean)
|
||||
if (topics.length > 0) overlay.topics = topics
|
||||
return overlay
|
||||
}
|
||||
|
||||
async function onCreateSite(): Promise<void> {
|
||||
const slug = siteForm.slug.trim()
|
||||
if (!SLUG_RE.test(slug)) {
|
||||
@@ -817,7 +793,7 @@
|
||||
await createSite(detailSlug.value, {
|
||||
slug,
|
||||
name: siteForm.name.trim() || undefined,
|
||||
brand_overlay: buildOverlay()
|
||||
brand_overlay: buildOverlay(siteForm)
|
||||
})
|
||||
ElMessage.success('站点已创建')
|
||||
siteDialogVisible.value = false
|
||||
@@ -829,6 +805,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 编辑门面覆盖层(Task 7a 顺手项:PATCH sites 的 brand_overlay 整字典) ----------
|
||||
|
||||
const overlayEditVisible = ref(false)
|
||||
const overlayEditSaving = ref(false)
|
||||
const overlayEditSiteSlug = ref('')
|
||||
const overlayEditForm = reactive(blankOverlayForm())
|
||||
/** 落库 overlay 中未支持编辑的键(保存整组替换会丢弃——弹窗内警示) */
|
||||
const overlayUnknownKeys = ref<string[]>([])
|
||||
|
||||
function openOverlayEdit(site: SiteSummary): void {
|
||||
overlayEditSiteSlug.value = site.slug
|
||||
const { form, unknownKeys } = overlayToForm(site.brand_overlay)
|
||||
Object.assign(overlayEditForm, form)
|
||||
overlayUnknownKeys.value = unknownKeys
|
||||
overlayEditVisible.value = true
|
||||
}
|
||||
|
||||
async function onSaveOverlay(): Promise<void> {
|
||||
overlayEditSaving.value = true
|
||||
try {
|
||||
await patchSite(detailSlug.value, overlayEditSiteSlug.value, {
|
||||
brand_overlay: buildOverlay(overlayEditForm)
|
||||
})
|
||||
ElMessage.success('门面覆盖层已更新(热生效)')
|
||||
overlayEditVisible.value = false
|
||||
refreshAfterMutation()
|
||||
} catch {
|
||||
// 全局拦截器已提示
|
||||
} finally {
|
||||
overlayEditSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 创建向导(四步) -----------------------------------------------------------------
|
||||
|
||||
const wizardVisible = ref(false)
|
||||
@@ -1110,32 +1119,9 @@
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.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%;
|
||||
.overlay-edit-alert {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user