官网开发 0811

This commit is contained in:
eafonyang
2026-08-11 18:42:11 +08:00
parent 65281a734a
commit e40d9d6b27
12 changed files with 280 additions and 172 deletions
@@ -90,19 +90,27 @@ const editorConfig = {
padding: 0 12px;
}
/* 深色主题适配(wangEditor 默认浅色) */
:global(html.dark) .rich-editor,
:global(html.dark) .rich-editor :deep(.w-e-text-container),
:global(html.dark) .rich-editor :deep(.w-e-toolbar) {
background-color: transparent;
color: rgba(255, 255, 255, 0.82);
}
:global(html.dark) .rich-editor :deep(.w-e-bar-item button) {
color: rgba(255, 255, 255, 0.72);
}
:global(html.dark) .rich-editor :deep(.w-e-text-container [data-slate-editor]) {
color: rgba(255, 255, 255, 0.82);
/*
* 深色主题适配:wangEditor 通过 CSS 变量(--w-e-*)控制全部内部元素颜色,
* 覆盖变量即可一次性适配工具栏、编辑区、下拉菜单、模态框等所有子组件,
* 无需逐元素覆写 background-color / color(旧方案漏掉了下拉菜单等内部元素)。
*/
:global(html.dark) .rich-editor {
--w-e-textarea-bg-color: #18181c;
--w-e-textarea-color: rgba(255, 255, 255, 0.82);
--w-e-textarea-border-color: rgba(255, 255, 255, 0.12);
--w-e-textarea-slight-border-color: rgba(255, 255, 255, 0.08);
--w-e-textarea-slight-color: rgba(255, 255, 255, 0.4);
--w-e-textarea-slight-bg-color: rgba(255, 255, 255, 0.06);
--w-e-textarea-selected-border-color: #1668dc;
--w-e-textarea-handler-bg-color: #1668dc;
--w-e-toolbar-color: rgba(255, 255, 255, 0.72);
--w-e-toolbar-bg-color: #18181c;
--w-e-toolbar-active-color: rgba(255, 255, 255, 0.95);
--w-e-toolbar-active-bg-color: rgba(255, 255, 255, 0.1);
--w-e-toolbar-disabled-color: rgba(255, 255, 255, 0.3);
--w-e-toolbar-border-color: rgba(255, 255, 255, 0.1);
--w-e-modal-button-bg-color: rgba(255, 255, 255, 0.08);
--w-e-modal-button-border-color: rgba(255, 255, 255, 0.15);
}
</style>
@@ -47,11 +47,9 @@ const layoutOptions: LayoutOption[] = [
{ label: '图文分栏', value: 'split_content', desc: '图片与文案左右并排介绍' },
{ label: '图片画廊', value: 'gallery', desc: '多张图片网格展示' },
{ label: '数据统计', value: 'stats', desc: '大数字展示关键指标数据' },
{ label: '视频展示', value: 'video_showcase', desc: '以视频为主的视觉区块' },
{ label: '标题+媒体', value: 'title_media', desc: '顶部标题文案 + 下方图片/视频,对齐方向可配置' },
{ label: '马赛克拼贴', value: 'mosaic', desc: '一大带多小的不规则网格,每个槽位支持图片/视频、中英双素材' },
{ label: '滚动叙事', value: 'scroll_narrative', desc: '随滚动逐步展开的叙事内容' },
{ label: '引言评价', value: 'quote', desc: '引用语 / 用户评价展示' },
{ label: '跑马灯', value: 'marquee', desc: '图片/视频轮播,可自定义切换间隔,最多 5 个素材' },
{ label: '参数规格表', value: 'spec_table', desc: '产品技术参数规格表格' }
];
@@ -111,7 +109,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, 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>
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), marquee_items: emptyMarqueeItems(1), marquee_interval: 3, min_height: null } as Record<string, unknown>
});
// 背景图/视频显示模式选项(bg_type 为图片/视频时生效)
@@ -200,6 +198,43 @@ function handleMosaicPatternChange(value: string) {
const mosaicSlotItems = computed<MosaicItemForm[]>(() => (formData.config.mosaic_items as MosaicItemForm[]) || []);
const mosaicSlotLabelList = computed(() => mosaicSlotLabels(String(formData.config.mosaic_pattern ?? 'big_3')));
// ===== 跑马灯(layout=marquee):图片/视频轮播,存 config.marquee_items / config.marquee_interval =====
interface MarqueeItemForm {
type: 'image' | 'video';
media_zh: string;
media_en: string;
/** 可选跳转链接 */
link: string;
}
const MARQUEE_MAX = 5;
function emptyMarqueeItem(): MarqueeItemForm {
return { type: 'image', media_zh: '', media_en: '', link: '' };
}
function emptyMarqueeItems(count: number): MarqueeItemForm[] {
return Array.from({ length: count }, () => emptyMarqueeItem());
}
// 归一化回填:类型/字段容错,数量限制 1~5(空则给 1 个空槽位)
function normalizeMarqueeItems(raw: unknown): MarqueeItemForm[] {
const arr = Array.isArray(raw) ? raw : [];
const list = arr.slice(0, MARQUEE_MAX).map((it): MarqueeItemForm => {
const o = (it ?? {}) as Partial<MarqueeItemForm>;
return { type: o.type === 'video' ? 'video' : 'image', media_zh: o.media_zh || '', media_en: o.media_en || '', link: o.link || '' };
});
return list.length ? list : emptyMarqueeItems(1);
}
function handleAddMarqueeItem() {
const items = (formData.config.marquee_items as MarqueeItemForm[]) || [];
if (items.length >= MARQUEE_MAX) return;
formData.config.marquee_items = [...items, emptyMarqueeItem()];
}
function handleRemoveMarqueeItem(idx: number) {
const items = (formData.config.marquee_items as MarqueeItemForm[]) || [];
const next = items.filter((_, i) => i !== idx);
formData.config.marquee_items = next.length ? next : emptyMarqueeItems(1);
}
const marqueeItems = computed<MarqueeItemForm[]>(() => (formData.config.marquee_items as MarqueeItemForm[]) || []);
// 九宫格文案位置(hero_split 专用)
const textPositionOptions = [
{ label: '左上', value: 'top_left' },
@@ -334,7 +369,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, 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 };
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), marquee_items: emptyMarqueeItems(1), marquee_interval: 3, min_height: null };
dialogVisible.value = true;
}
@@ -381,9 +416,11 @@ function fillFormFromRow(row: Api.Www.SectionBlock) {
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'
mosaic_pattern: ['big_3', 'half_2', 'big_4', 'big_2'].includes(String(cfg.mosaic_pattern)) ? String(cfg.mosaic_pattern) : 'big_3',
marquee_interval: typeof cfg.marquee_interval === 'number' && cfg.marquee_interval >= 1 ? Math.min(cfg.marquee_interval, 60) : 3
};
formData.config.mosaic_items = normalizeMosaicItems(cfg.mosaic_items, String(formData.config.mosaic_pattern));
formData.config.marquee_items = normalizeMarqueeItems(cfg.marquee_items);
}
function handleEdit(row: Api.Www.SectionBlock) {
@@ -581,6 +618,26 @@ onMounted(loadData);
<span class="text-12px opacity-60">桌面端标题字号移动端自动按约 62.5% 缩小如填 48 移动端 30px留空 = 兜底 36px引言布局无标题字段不受影响</span>
</div>
</NFormItem>
<NFormItem v-if="formData.layout === 'hero_split' || formData.layout === 'mosaic'" :label="formData.layout === 'mosaic' ? '网格高度' : '最小高度'">
<div class="w-full flex flex-col gap-8px">
<NInputNumber
:value="(formData.config.min_height as number | null) ?? null"
:min="0"
:max="1200"
:step="20"
class="w-full"
clearable
:placeholder="formData.layout === 'mosaic' ? '留空 = 默认 16:9 宽高比' : '留空 = 首屏高度(默认)'"
@update:value="v => (formData.config.min_height = v)"
>
<template #suffix>px</template>
</NInputNumber>
<span class="text-12px opacity-60">
<template v-if="formData.layout === 'mosaic'">仅桌面端生效留空 = 默认 16:9 宽高比填数值 = 网格固定高度px素材自动裁剪填充可调低区块高度移动端保持单列堆叠不变</template>
<template v-else>仅主视觉分屏布局生效留空 = 沿用首屏高度至少 680px 约一屏0 = 完全自适应内容去上下内边距区块高度严格等于内容高度填数值 = 自定义最小高度</template>
</span>
</div>
</NFormItem>
<NDivider>文案颜色留空=跟随主题默认色</NDivider>
<NFormItem label="Overline 颜色">
<ColorInput v-model:value="formData.overline_color" />
@@ -607,26 +664,6 @@ onMounted(loadData);
<NRadio v-for="opt in bgFitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
</NRadioGroup>
</NFormItem>
<NFormItem v-if="formData.layout === 'hero_split' || formData.layout === 'mosaic'" :label="formData.layout === 'mosaic' ? '网格高度' : '最小高度'">
<div class="w-full flex flex-col gap-8px">
<NInputNumber
:value="(formData.config.min_height as number | null) ?? null"
:min="0"
:max="1200"
:step="20"
class="w-full"
clearable
:placeholder="formData.layout === 'mosaic' ? '留空 = 默认 16:9 宽高比' : '留空 = 首屏高度(默认)'"
@update:value="v => (formData.config.min_height = v)"
>
<template #suffix>px</template>
</NInputNumber>
<span class="text-12px opacity-60">
<template v-if="formData.layout === 'mosaic'">仅桌面端生效留空 = 默认 16:9 宽高比填数值 = 网格固定高度px素材自动裁剪填充可调低区块高度移动端保持单列堆叠不变</template>
<template v-else>仅主视觉分屏布局生效留空 = 沿用首屏高度至少 680px 约一屏0 = 完全自适应内容去上下内边距区块高度严格等于内容高度填数值 = 自定义最小高度</template>
</span>
</div>
</NFormItem>
<NFormItem v-if="formData.layout === 'hero_split' || formData.layout === 'title_media'" label="文案位置">
<div class="w-full flex flex-col gap-8px">
<div class="grid grid-cols-3 gap-4px">
@@ -739,6 +776,40 @@ onMounted(loadData);
</div>
</NFormItem>
</template>
<template v-else-if="formData.layout === 'marquee'">
<NFormItem label="切换间隔">
<div class="w-full flex flex-col gap-8px">
<NInputNumber
:value="(formData.config.marquee_interval as number) ?? 3"
:min="1"
:max="60"
:step="1"
class="w-full"
@update:value="v => (formData.config.marquee_interval = v ?? 3)"
>
<template #suffix></template>
</NInputNumber>
<span class="text-12px opacity-60">相邻素材自动切换的时间间隔1~60 默认 3 </span>
</div>
</NFormItem>
<NFormItem v-for="(item, idx) in marqueeItems" :key="idx" :label="`素材 ${idx + 1}`">
<div class="w-full flex flex-col gap-8px">
<div class="flex items-center justify-between">
<NRadioGroup v-model:value="item.type">
<NRadio value="image">图片</NRadio>
<NRadio value="video">视频</NRadio>
</NRadioGroup>
<NButton v-if="marqueeItems.length > 1" text type="error" size="tiny" @click="handleRemoveMarqueeItem(idx)">删除</NButton>
</div>
<MediaSelectInput v-model:value="item.media_zh" :file-type="item.type === 'video' ? 'video' : 'image'" :maxlength="500" placeholder="素材(中)" />
<MediaSelectInput v-model:value="item.media_en" :file-type="item.type === 'video' ? 'video' : 'image'" :maxlength="500" placeholder="素材(英,留空回退中文素材)" />
<NInput v-model:value="item.link" :maxlength="500" placeholder="跳转链接(可选,点击素材后新窗口打开)" />
</div>
</NFormItem>
<NFormItem label="操作">
<NButton :disabled="marqueeItems.length >= 5" @click="handleAddMarqueeItem">+ 添加素材{{ marqueeItems.length }}/5</NButton>
</NFormItem>
</template>
<template v-else>
<NFormItem label="图片(中)">
<MediaSelectInput v-model:value="formData.media_url_zh" file-type="image" :maxlength="500" placeholder="素材路径(支持 /uploads/... 相对路径)" />