官网开发 0810

This commit is contained in:
eafonyang
2026-08-10 14:56:44 +08:00
parent c9a1d9c576
commit 48510739a7
20 changed files with 636 additions and 53 deletions
@@ -4,6 +4,7 @@ import { NButton, NSpace, NSwitch, NTag, type DataTableColumns, type FormInst, t
import ColorInput from '../page/home/components/ColorInput.vue';
import MediaSelectInput from './MediaSelectInput.vue';
import RichTextEditor from './RichTextEditor.vue';
import SpecManager from './SpecManager.vue';
import {
fetchCreateSection,
fetchDeleteSection,
@@ -110,7 +111,7 @@ const formData = reactive({
media_url_en: '',
video_url: '',
is_visible: true,
config: { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null } as Record<string, unknown>
config: { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, margin_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', split_ratio: 50, mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null } as Record<string, unknown>
});
// 背景图/视频显示模式选项(bg_type 为图片/视频时生效)
@@ -128,6 +129,20 @@ const containerWidthOptions = [
{ label: '全宽', value: 'full' }
];
// 图文分栏比例选项(值为素材栏宽占比);标签顺序随「媒体位置」联动:
// 素材在左 → 「素材 : 文案」,素材在右 → 「文案 : 素材」(与视觉左右顺序一致,底层存值不变)
const splitRatioOptions = computed(() => {
const mediaRight = formData.config.media_position === 'right';
const label = (media: number) => (mediaRight ? `${100 - media} : ${media}` : `${media} : ${100 - media}`);
return [
{ label: `${label(30)}`, value: 30 },
{ label: `${label(40)}`, value: 40 },
{ label: `${label(50)}(默认)`, value: 50 },
{ label: `${label(60)}`, value: 60 },
{ label: `${label(70)}`, value: 70 }
];
});
// ===== 马赛克拼贴(layout=mosaic):固定版式 + 槽位素材,存 config.mosaic_pattern / config.mosaic_items =====
interface MosaicItemForm {
type: 'image' | 'video';
@@ -247,11 +262,15 @@ const columns = computed((): DataTableColumns<Api.Www.SectionBlock> => [
{
title: '操作',
key: 'actions',
width: 240,
width: 290,
fixed: 'right',
render(row) {
return h('div', { class: 'flex-y-center gap-8px' }, [
h(NButton, { size: 'small', type: 'primary', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
// spec_table 布局:管理参数分组与参数项
row.layout === 'spec_table'
? h(NButton, { size: 'small', type: 'info', onClick: () => handleSpecManage(row) }, { default: () => '参数' })
: null,
h(NButton, { size: 'small', onClick: () => handleCopy(row) }, { default: () => '复制' }),
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row) }, { default: () => '删除' })
]);
@@ -315,7 +334,7 @@ function handleAdd() {
formData.media_url_en = '';
formData.video_url = '';
formData.is_visible = true;
formData.config = { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null };
formData.config = { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, margin_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', split_ratio: 50, mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null };
dialogVisible.value = true;
}
@@ -356,9 +375,11 @@ function fillFormFromRow(row: Api.Www.SectionBlock) {
container_width: ['1400', '1600', 'full'].includes(String(cfg.container_width)) ? String(cfg.container_width) : '1200',
border_radius: typeof cfg.border_radius === 'number' && cfg.border_radius > 0 ? cfg.border_radius : 0,
padding_y: typeof cfg.padding_y === 'number' && cfg.padding_y >= 0 ? cfg.padding_y : null,
margin_y: typeof cfg.margin_y === 'number' && cfg.margin_y >= 0 ? cfg.margin_y : null,
title_font_size: typeof cfg.title_font_size === 'number' && cfg.title_font_size > 0 ? cfg.title_font_size : null,
bg_fit: cfg.bg_fit === 'contain' || cfg.bg_fit === 'auto' ? cfg.bg_fit : 'cover',
media_position: cfg.media_position === 'right' ? 'right' : 'left',
split_ratio: [30, 40, 50, 60, 70].includes(Number(cfg.split_ratio)) ? Number(cfg.split_ratio) : 50,
min_height: typeof cfg.min_height === 'number' && cfg.min_height >= 0 ? cfg.min_height : null,
mosaic_pattern: ['big_3', 'half_2', 'big_4', 'big_2'].includes(String(cfg.mosaic_pattern)) ? String(cfg.mosaic_pattern) : 'big_3'
};
@@ -404,7 +425,14 @@ function handleDelete(row: Api.Www.SectionBlock) {
});
}
async function handleSubmit() {
// 参数管理(spec_table 布局):唤起 SpecManager
const specManagerRef = ref<InstanceType<typeof SpecManager> | null>(null);
function handleSpecManage(row: Api.Www.SectionBlock) {
specManagerRef.value?.open(row);
}
async function handleSubmit(closeAfter = true) {
submitLoading.value = true;
const { id, ...payload } = formData;
const reqData = {
@@ -412,13 +440,15 @@ async function handleSubmit() {
page_type: props.pageType,
product_id: props.productId || null
};
const { error } = id
const { data, error } = id
? await fetchUpdateSection(id, reqData)
: await fetchCreateSection(reqData);
submitLoading.value = false;
if (!error) {
// 「应用」新建后回填新 id,后续保存走更新接口,避免重复创建
if (!id && data?.id) formData.id = data.id;
window.$message?.success(id ? '更新成功' : '创建成功');
dialogVisible.value = false;
if (closeAfter) dialogVisible.value = false;
loadData();
}
}
@@ -447,6 +477,8 @@ onMounted(loadData);
:title="dialogTitle"
class="w-1000px"
:mask-closable="false"
:segmented="{ content: true, footer: 'soft' }"
content-style="max-height: calc(85vh - 130px); overflow-y: auto;"
>
<NForm ref="formRef" :model="formData" label-placement="left" label-width="120">
<NDivider>基础配置</NDivider>
@@ -515,6 +547,23 @@ onMounted(loadData);
<span class="text-12px opacity-60">内容区与区块上下边缘的间距留空 = 布局默认多数 96px / 全幅横幅 120px / 主视觉分屏 64px0 = 完全贴边</span>
</div>
</NFormItem>
<NFormItem label="上下外边距">
<div class="w-full flex flex-col gap-8px">
<NInputNumber
:value="(formData.config.margin_y as number | null) ?? null"
:min="0"
:max="240"
:step="8"
class="w-full"
clearable
placeholder="留空 = 紧贴相邻区块"
@update:value="v => (formData.config.margin_y = v)"
>
<template #suffix>px</template>
</NInputNumber>
<span class="text-12px opacity-60">区块与上下相邻区块之间的间隙露出页面背景色适合区块宽度 &lt; 100 + 圆角的卡片式区块留空/0 = 紧贴默认</span>
</div>
</NFormItem>
<NFormItem label="标题字号">
<div class="w-full flex flex-col gap-8px">
<NInputNumber
@@ -596,10 +645,20 @@ onMounted(loadData);
</NFormItem>
<NFormItem v-if="formData.layout === 'split_content'" label="媒体位置">
<NRadioGroup :value="(formData.config.media_position as string) ?? 'left'" @update:value="v => (formData.config.media_position = v)">
<NRadio value="left">图片/视频在左</NRadio>
<NRadio value="right">图片/视频在右</NRadio>
<NRadio value="left">素材文案</NRadio>
<NRadio value="right">文案素材</NRadio>
</NRadioGroup>
</NFormItem>
<NFormItem v-if="formData.layout === 'split_content'" label="分栏比例">
<div class="w-full flex flex-col gap-8px">
<NSelect
:value="(formData.config.split_ratio as number) ?? 50"
:options="splitRatioOptions"
@update:value="v => (formData.config.split_ratio = v)"
/>
<span class="text-12px opacity-60">按页面视觉左右顺序标注{{ formData.config.media_position === 'right' ? '文案 : 素材' : '素材 : 文案' }}默认五五等分移动端小屏始终单列堆叠不受影响</span>
</div>
</NFormItem>
<NFormItem v-if="formData.layout === 'mosaic'" label="拼贴版式">
<div class="w-full flex flex-col gap-8px">
<NRadioGroup :value="(formData.config.mosaic_pattern as string) ?? 'big_3'" @update:value="handleMosaicPatternChange">
@@ -700,10 +759,14 @@ onMounted(loadData);
<template #footer>
<NSpace justify="end">
<NButton @click="dialogVisible = false">取消</NButton>
<NButton type="primary" :loading="submitLoading" @click="handleSubmit">确定</NButton>
<NButton type="success" secondary :loading="submitLoading" @click="handleSubmit(false)">应用</NButton>
<NButton type="primary" :loading="submitLoading" @click="handleSubmit()">确定</NButton>
</NSpace>
</template>
</NModal>
<!-- 参数规格管理spec_table 布局专用 -->
<SpecManager ref="specManagerRef" />
</div>
</template>