优化项目结构
This commit is contained in:
-4
@@ -83,8 +83,6 @@ declare namespace Api {
|
||||
body_color: string;
|
||||
bg_type: string;
|
||||
bg_value: string;
|
||||
overlay_enabled: boolean;
|
||||
overlay_opacity: number;
|
||||
overline_zh: string;
|
||||
overline_en: string;
|
||||
title_zh: string;
|
||||
@@ -119,8 +117,6 @@ declare namespace Api {
|
||||
body_color?: string;
|
||||
bg_type?: string;
|
||||
bg_value?: string;
|
||||
overlay_enabled?: boolean;
|
||||
overlay_opacity?: number;
|
||||
overline_zh?: string;
|
||||
overline_en?: string;
|
||||
title_zh?: string;
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { NButton, NColorPicker, NInputNumber, NRadioButton, NRadioGroup, NSlider } from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'GradientInput' });
|
||||
|
||||
/**
|
||||
* 渐变背景编辑器
|
||||
*
|
||||
* 对外仍是单条 CSS 渐变字符串(v-model:value,写入 section.bg_value),
|
||||
* 内部以「类型 + 角度 + 色标」结构化编辑,并提供精选预设一键套用。
|
||||
* 所有编辑仅在本地预览,点击「确认」后才写回 bg_value,
|
||||
* 避免切换背景类型时误覆盖原有值(如视频/图片路径)。
|
||||
* 传入值无法解析为受支持格式时,以默认渐变作为本地编辑起点。
|
||||
*/
|
||||
const props = defineProps<{ value: string }>();
|
||||
const emit = defineEmits<{ 'update:value': [value: string] }>();
|
||||
|
||||
interface GradientStop {
|
||||
color: string;
|
||||
position: number;
|
||||
}
|
||||
interface GradientState {
|
||||
type: 'linear' | 'radial';
|
||||
angle: number;
|
||||
stops: GradientStop[];
|
||||
}
|
||||
interface Preset extends GradientState {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const MAX_STOPS = 6;
|
||||
const MIN_STOPS = 2;
|
||||
|
||||
function defaultGradient(): GradientState {
|
||||
return {
|
||||
type: 'linear',
|
||||
angle: 135,
|
||||
stops: [
|
||||
{ color: '#667eea', position: 0 },
|
||||
{ color: '#764ba2', position: 100 }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 精选预设(点击即用)
|
||||
const PRESETS: Preset[] = [
|
||||
{ name: '梦幻紫', type: 'linear', angle: 135, stops: [{ color: '#667eea', position: 0 }, { color: '#764ba2', position: 100 }] },
|
||||
{ name: '蔚蓝', type: 'linear', angle: 135, stops: [{ color: '#4facfe', position: 0 }, { color: '#00f2fe', position: 100 }] },
|
||||
{ name: '翠绿', type: 'linear', angle: 135, stops: [{ color: '#43e97b', position: 0 }, { color: '#38f9d7', position: 100 }] },
|
||||
{ name: '樱粉', type: 'linear', angle: 135, stops: [{ color: '#f5576c', position: 0 }, { color: '#f093fb', position: 100 }] },
|
||||
{ name: '暖阳', type: 'linear', angle: 135, stops: [{ color: '#f6d365', position: 0 }, { color: '#fda085', position: 100 }] },
|
||||
{ name: '深海', type: 'linear', angle: 135, stops: [{ color: '#30cfd0', position: 0 }, { color: '#330867', position: 100 }] },
|
||||
{ name: '马卡龙', type: 'linear', angle: 135, stops: [{ color: '#a8edea', position: 0 }, { color: '#fed6e3', position: 100 }] },
|
||||
{ name: '石墨', type: 'linear', angle: 135, stops: [{ color: '#232526', position: 0 }, { color: '#414345', position: 100 }] },
|
||||
{ name: '暗夜', type: 'linear', angle: 200, stops: [{ color: '#0f2027', position: 0 }, { color: '#203a43', position: 50 }, { color: '#2c5364', position: 100 }] }
|
||||
];
|
||||
|
||||
const state = reactive<GradientState>(defaultGradient());
|
||||
|
||||
function buildCssFrom(s: GradientState): string {
|
||||
const stops = [...s.stops].sort((a, b) => a.position - b.position);
|
||||
const stopsStr = stops.map(st => `${st.color} ${st.position}%`).join(', ');
|
||||
return s.type === 'radial' ? `radial-gradient(circle, ${stopsStr})` : `linear-gradient(${s.angle}deg, ${stopsStr})`;
|
||||
}
|
||||
|
||||
const previewCss = computed(() => buildCssFrom(state));
|
||||
|
||||
function commit() {
|
||||
emit('update:value', buildCssFrom(state));
|
||||
window.$message?.success('已应用到背景值');
|
||||
}
|
||||
|
||||
function applyState(s: GradientState) {
|
||||
state.type = s.type;
|
||||
state.angle = s.angle;
|
||||
state.stops = s.stops.map(st => ({ ...st }));
|
||||
}
|
||||
|
||||
// 解析 CSS 渐变字符串;仅支持本组件生成的受控格式(hex/rgb 色 + 百分比位置)
|
||||
function parseGradient(v: string): GradientState | null {
|
||||
const linear = v.match(/^linear-gradient\(\s*(\d+(?:\.\d+)?)deg\s*,\s*(.+)\)\s*$/i);
|
||||
const radial = v.match(/^radial-gradient\(\s*circle\s*,\s*(.+)\)\s*$/i);
|
||||
const stopsStr = linear ? linear[2] : radial ? radial[1] : null;
|
||||
if (!stopsStr) return null;
|
||||
const stopRe = /(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|[a-zA-Z]+)\s+(\d+(?:\.\d+)?)%/g;
|
||||
const stops: GradientStop[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = stopRe.exec(stopsStr)) !== null) {
|
||||
stops.push({ color: m[1], position: Number.parseFloat(m[2]) });
|
||||
}
|
||||
if (stops.length < MIN_STOPS) return null;
|
||||
return { type: linear ? 'linear' : 'radial', angle: linear ? Number.parseFloat(linear[1]) : 135, stops };
|
||||
}
|
||||
|
||||
function init() {
|
||||
const v = (props.value || '').trim();
|
||||
const parsed = parseGradient(v);
|
||||
// 能解析则回填现有渐变;否则以默认渐变作为本地编辑起点,
|
||||
// 不写回 bg_value,避免切换类型时误覆盖原值(如视频/图片路径)
|
||||
applyState(parsed || defaultGradient());
|
||||
}
|
||||
|
||||
function applyPreset(p: Preset) {
|
||||
applyState(p);
|
||||
}
|
||||
|
||||
function addStop() {
|
||||
if (state.stops.length >= MAX_STOPS) return;
|
||||
const last = state.stops[state.stops.length - 1];
|
||||
const position = Math.min(Math.round((last.position + 100) / 2), 100);
|
||||
state.stops.push({ color: last.color, position });
|
||||
}
|
||||
|
||||
function removeStop(idx: number) {
|
||||
if (state.stops.length <= MIN_STOPS) return;
|
||||
state.stops.splice(idx, 1);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col gap-10px">
|
||||
<!-- 实时预览 -->
|
||||
<div class="h-44px w-full rounded-6px border border-solid border-[var(--n-border-color)]" :style="{ background: previewCss }" />
|
||||
|
||||
<!-- 可视化编辑 -->
|
||||
<!-- 预设渐变 -->
|
||||
<div>
|
||||
<p class="mb-4px text-12px opacity-60">预设渐变</p>
|
||||
<div class="grid grid-cols-9 gap-6px">
|
||||
<button
|
||||
v-for="p in PRESETS"
|
||||
:key="p.name"
|
||||
type="button"
|
||||
class="h-30px cursor-pointer rounded-4px border border-solid border-transparent transition-all hover:border-primary"
|
||||
:title="p.name"
|
||||
:style="{ background: buildCssFrom(p) }"
|
||||
@click="applyPreset(p)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 类型 + 角度 -->
|
||||
<div class="flex items-center gap-8px">
|
||||
<NRadioGroup v-model:value="state.type" size="small">
|
||||
<NRadioButton value="linear">线性</NRadioButton>
|
||||
<NRadioButton value="radial">径向</NRadioButton>
|
||||
</NRadioGroup>
|
||||
<template v-if="state.type === 'linear'">
|
||||
<NSlider v-model:value="state.angle" :min="0" :max="360" :step="1" class="flex-1" />
|
||||
<NInputNumber v-model:value="state.angle" :min="0" :max="360" size="small" class="w-84px">
|
||||
<template #suffix>°</template>
|
||||
</NInputNumber>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 色标列表 -->
|
||||
<div class="flex flex-col gap-6px">
|
||||
<div v-for="(s, i) in state.stops" :key="i" class="flex items-center gap-6px">
|
||||
<NColorPicker v-model:value="s.color" :show-alpha="false" :modes="['hex']" size="small" class="w-120px" />
|
||||
<NInputNumber v-model:value="s.position" :min="0" :max="100" size="small" class="w-96px">
|
||||
<template #suffix>%</template>
|
||||
</NInputNumber>
|
||||
<NButton v-if="state.stops.length > MIN_STOPS" size="tiny" quaternary type="error" @click="removeStop(i)">删除</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<NButton size="small" :disabled="state.stops.length >= MAX_STOPS" @click="addStop">+ 添加色标({{ state.stops.length }}/{{ MAX_STOPS }})</NButton>
|
||||
<NButton size="small" type="primary" @click="commit">确认应用到背景值</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { NButton, NSpace, NSwitch, NTag, type DataTableColumns, type FormInst, type SelectOption, type SelectRenderOption } from 'naive-ui';
|
||||
import ColorInput from '../page/home/components/ColorInput.vue';
|
||||
import GradientInput from './GradientInput.vue';
|
||||
import MediaSelectInput from './MediaSelectInput.vue';
|
||||
import RichTextEditor from './RichTextEditor.vue';
|
||||
import SpecManager from './SpecManager.vue';
|
||||
@@ -89,8 +90,6 @@ const formData = reactive({
|
||||
body_color: '',
|
||||
bg_type: 'color',
|
||||
bg_value: '',
|
||||
overlay_enabled: false,
|
||||
overlay_opacity: 0,
|
||||
overline_zh: '',
|
||||
overline_en: '',
|
||||
title_zh: '',
|
||||
@@ -349,8 +348,6 @@ function handleAdd() {
|
||||
formData.body_color = '';
|
||||
formData.bg_type = 'color';
|
||||
formData.bg_value = '';
|
||||
formData.overlay_enabled = false;
|
||||
formData.overlay_opacity = 0;
|
||||
formData.overline_zh = '';
|
||||
formData.overline_en = '';
|
||||
formData.title_zh = '';
|
||||
@@ -382,8 +379,6 @@ function fillFormFromRow(row: Api.Www.SectionBlock) {
|
||||
formData.body_color = row.body_color || '';
|
||||
formData.bg_type = row.bg_type;
|
||||
formData.bg_value = row.bg_value;
|
||||
formData.overlay_enabled = Boolean(row.overlay_enabled);
|
||||
formData.overlay_opacity = Number(row.overlay_opacity);
|
||||
formData.overline_zh = row.overline_zh;
|
||||
formData.overline_en = row.overline_en;
|
||||
formData.title_zh = row.title_zh;
|
||||
@@ -519,146 +514,170 @@ onMounted(loadData);
|
||||
>
|
||||
<NForm ref="formRef" :model="formData" label-placement="left" label-width="120">
|
||||
<NDivider>基础配置</NDivider>
|
||||
<NFormItem label="布局类型">
|
||||
<NSelect v-model:value="formData.layout" :options="layoutOptions" :render-label="renderLayoutLabel" :render-option="renderLayoutOption" />
|
||||
</NFormItem>
|
||||
<NFormItem label="主题">
|
||||
<NRadioGroup v-model:value="formData.theme">
|
||||
<NRadio v-for="opt in themeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem label="区块宽度">
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<NInputNumber
|
||||
:value="(formData.config.width_percent as number) ?? 100"
|
||||
:min="30"
|
||||
:max="100"
|
||||
:step="5"
|
||||
class="w-full"
|
||||
@update:value="v => (formData.config.width_percent = v ?? 100)"
|
||||
>
|
||||
<template #suffix>%</template>
|
||||
</NInputNumber>
|
||||
<span class="text-12px opacity-60">占视口宽度百分比,居中显示;100 = 全宽。移动端(小屏)始终全宽</span>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem label="内容区宽度">
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<NSelect
|
||||
:value="(formData.config.container_width as string) ?? '1200'"
|
||||
:options="containerWidthOptions"
|
||||
@update:value="v => (formData.config.container_width = v)"
|
||||
/>
|
||||
<span class="text-12px opacity-60">区块内容容器的最大宽度,居中于区块内;全宽 = 内容占满整个区块</span>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem label="区块圆角">
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<NInputNumber
|
||||
:value="(formData.config.border_radius as number) ?? 0"
|
||||
:min="0"
|
||||
:max="60"
|
||||
:step="4"
|
||||
class="w-full"
|
||||
@update:value="v => (formData.config.border_radius = v ?? 0)"
|
||||
>
|
||||
<template #suffix>px</template>
|
||||
</NInputNumber>
|
||||
<span class="text-12px opacity-60">区块四角圆角大小;0 = 直角(默认)。建议与「区块宽度」小于 100 搭配使用,全宽时圆角不可见</span>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem label="上下内边距">
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<NInputNumber
|
||||
:value="(formData.config.padding_y as number | null) ?? null"
|
||||
:min="0"
|
||||
:max="240"
|
||||
:step="8"
|
||||
class="w-full"
|
||||
clearable
|
||||
placeholder="留空 = 布局默认值"
|
||||
@update:value="v => (formData.config.padding_y = v)"
|
||||
>
|
||||
<template #suffix>px</template>
|
||||
</NInputNumber>
|
||||
<span class="text-12px opacity-60">内容区与区块上下边缘的间距。留空 = 布局默认(多数 96px / 全幅横幅 120px / 主视觉分屏 64px);0 = 完全贴边</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">区块与上下相邻区块之间的间隙,露出页面背景色;适合「区块宽度 < 100 + 圆角」的卡片式区块。留空/0 = 紧贴(默认)</span>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem label="标题字号">
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<NInputNumber
|
||||
:value="(formData.config.title_font_size as number | null) ?? null"
|
||||
:min="16"
|
||||
:max="96"
|
||||
:step="2"
|
||||
class="w-full"
|
||||
clearable
|
||||
placeholder="留空 = 36px(兜底)"
|
||||
@update:value="v => (formData.config.title_font_size = v)"
|
||||
>
|
||||
<template #suffix>px</template>
|
||||
</NInputNumber>
|
||||
<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">
|
||||
<div class="grid grid-cols-2 gap-x-16px">
|
||||
<NFormItem label="布局类型" label-placement="top">
|
||||
<NSelect v-model:value="formData.layout" :options="layoutOptions" :render-label="renderLayoutLabel" :render-option="renderLayoutOption" />
|
||||
</NFormItem>
|
||||
<NFormItem label="主题" label-placement="top">
|
||||
<NRadioGroup v-model:value="formData.theme">
|
||||
<NRadio v-for="opt in themeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-x-16px">
|
||||
<NFormItem label="区块宽度" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<NInputNumber
|
||||
:value="(formData.config.width_percent as number) ?? 100"
|
||||
:min="30"
|
||||
:max="100"
|
||||
:step="5"
|
||||
class="w-full"
|
||||
@update:value="v => (formData.config.width_percent = v ?? 100)"
|
||||
>
|
||||
<template #suffix>%</template>
|
||||
</NInputNumber>
|
||||
</template>
|
||||
占视口宽度百分比,居中显示;100 = 全宽。移动端(小屏)始终全宽
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem label="内容区宽度" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<NSelect
|
||||
:value="(formData.config.container_width as string) ?? '1200'"
|
||||
:options="containerWidthOptions"
|
||||
@update:value="v => (formData.config.container_width = v)"
|
||||
/>
|
||||
</template>
|
||||
区块内容容器的最大宽度,居中于区块内;全宽 = 内容占满整个区块
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem label="区块圆角" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<NInputNumber
|
||||
:value="(formData.config.border_radius as number) ?? 0"
|
||||
:min="0"
|
||||
:max="60"
|
||||
:step="4"
|
||||
class="w-full"
|
||||
@update:value="v => (formData.config.border_radius = v ?? 0)"
|
||||
>
|
||||
<template #suffix>px</template>
|
||||
</NInputNumber>
|
||||
</template>
|
||||
区块四角圆角大小;0 = 直角(默认)。建议与「区块宽度」小于 100 搭配使用,全宽时圆角不可见
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-x-16px">
|
||||
<NFormItem label="上下内边距" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<NInputNumber
|
||||
:value="(formData.config.padding_y as number | null) ?? null"
|
||||
:min="0"
|
||||
:max="240"
|
||||
:step="8"
|
||||
class="w-full"
|
||||
clearable
|
||||
placeholder="留空 = 布局默认值"
|
||||
@update:value="v => (formData.config.padding_y = v)"
|
||||
>
|
||||
<template #suffix>px</template>
|
||||
</NInputNumber>
|
||||
</template>
|
||||
内容区与区块上下边缘的间距。留空 = 布局默认(多数 96px / 全幅横幅 120px / 主视觉分屏 64px);0 = 完全贴边
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem label="上下外边距" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<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>
|
||||
</template>
|
||||
区块与上下相邻区块之间的间隙,露出页面背景色;适合「区块宽度 < 100 + 圆角」的卡片式区块。留空/0 = 紧贴(默认)
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="formData.layout === 'hero_split' || formData.layout === 'mosaic'" :label="formData.layout === 'mosaic' ? '网格高度' : '最小高度'" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<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>
|
||||
</template>
|
||||
<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" />
|
||||
</NFormItem>
|
||||
<NFormItem label="标题颜色">
|
||||
<ColorInput v-model:value="formData.title_color" />
|
||||
</NFormItem>
|
||||
<NFormItem label="副标题/正文颜色">
|
||||
<ColorInput v-model:value="formData.body_color" />
|
||||
</NFormItem>
|
||||
<NFormItem label="背景类型">
|
||||
<NRadioGroup v-model:value="formData.bg_type">
|
||||
<NRadioButton value="color">纯色</NRadioButton>
|
||||
<NRadioButton value="image">图片</NRadioButton>
|
||||
<NRadioButton value="gradient">渐变</NRadioButton>
|
||||
<NRadioButton value="video">视频</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem label="背景值">
|
||||
<MediaSelectInput v-model:value="formData.bg_value" placeholder="颜色值或图片/视频路径(支持 /uploads/... 相对路径)" />
|
||||
</NFormItem>
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<div class="flex items-start gap-x-16px">
|
||||
<NFormItem label="背景类型" label-placement="top" class="shrink-0">
|
||||
<NRadioGroup v-model:value="formData.bg_type">
|
||||
<NRadioButton value="color">纯色</NRadioButton>
|
||||
<NRadioButton value="gradient">渐变</NRadioButton>
|
||||
<NRadioButton value="image">图片</NRadioButton>
|
||||
<NRadioButton value="video">视频</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem label="背景值" label-placement="top" class="min-w-0 flex-1">
|
||||
<ColorInput v-if="formData.bg_type === 'color'" v-model:value="formData.bg_value" placeholder="留空则无背景色" />
|
||||
<GradientInput v-else-if="formData.bg_type === 'gradient'" v-model:value="formData.bg_value" />
|
||||
<MediaSelectInput v-else v-model:value="formData.bg_value" placeholder="图片/视频路径(支持 /uploads/... 相对路径)" />
|
||||
</NFormItem>
|
||||
</div>
|
||||
<NDivider>文案样式</NDivider>
|
||||
<div class="grid grid-cols-4 gap-x-16px">
|
||||
<NFormItem label="标题字号" label-placement="top">
|
||||
<NTooltip trigger="focus" placement="top">
|
||||
<template #trigger>
|
||||
<NInputNumber
|
||||
:value="(formData.config.title_font_size as number | null) ?? null"
|
||||
:min="16"
|
||||
:max="96"
|
||||
:step="2"
|
||||
class="w-full"
|
||||
clearable
|
||||
placeholder="留空 = 36px(兜底)"
|
||||
@update:value="v => (formData.config.title_font_size = v)"
|
||||
>
|
||||
<template #suffix>px</template>
|
||||
</NInputNumber>
|
||||
</template>
|
||||
桌面端标题字号;移动端自动按约 62.5% 缩小(如填 48 → 移动端 30px)。留空 = 兜底 36px;引言布局无标题字段不受影响
|
||||
</NTooltip>
|
||||
</NFormItem>
|
||||
<NFormItem label="Overline 颜色" label-placement="top">
|
||||
<ColorInput v-model:value="formData.overline_color" />
|
||||
</NFormItem>
|
||||
<NFormItem label="标题颜色" label-placement="top">
|
||||
<ColorInput v-model:value="formData.title_color" />
|
||||
</NFormItem>
|
||||
<NFormItem label="副标题/正文颜色" label-placement="top">
|
||||
<ColorInput v-model:value="formData.body_color" />
|
||||
</NFormItem>
|
||||
</div>
|
||||
<NFormItem v-if="formData.bg_type === 'image' || formData.bg_type === 'video'" label="背景显示模式">
|
||||
<NRadioGroup :value="(formData.config.bg_fit as string) ?? 'cover'" @update:value="v => (formData.config.bg_fit = v)">
|
||||
<NRadio v-for="opt in bgFitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
|
||||
@@ -704,14 +723,6 @@ onMounted(loadData);
|
||||
<span class="text-12px opacity-60">固定版式;切换版式会同步下方槽位数量(已填素材按顺序保留)</span>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem label="遮罩">
|
||||
<NSpace align="center">
|
||||
<NSwitch v-model:value="formData.overlay_enabled" />
|
||||
<span>透明度</span>
|
||||
<NSlider v-model:value="formData.overlay_opacity" :min="0" :max="1" :step="0.05" class="w-200px" />
|
||||
<span>{{ formData.overlay_opacity.toFixed(2) }}</span>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
|
||||
<NDivider>文案内容</NDivider>
|
||||
<NFormItem label="Overline(中)">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NColorPicker, NInput } from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'ColorInput' });
|
||||
|
||||
const props = defineProps<{ value: string }>();
|
||||
const props = withDefaults(defineProps<{ value: string; placeholder?: string }>(), { placeholder: '留空则跟随主题默认色' });
|
||||
const emit = defineEmits<{ (e: 'update:value', value: string): void }>();
|
||||
|
||||
// 空值 = 跟随主题默认色
|
||||
@@ -19,23 +19,24 @@ const showPicker = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<div class="relative w-full">
|
||||
<NInput
|
||||
v-model:value="model"
|
||||
placeholder="留空则跟随主题默认色"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
@focus="showPicker = true"
|
||||
/>
|
||||
<!-- 空值时取色器内部以白色兜底展示,选中任意颜色即写回 -->
|
||||
<NColorPicker
|
||||
v-if="showPicker"
|
||||
:value="model || '#FFFFFF'"
|
||||
:show-alpha="false"
|
||||
:modes="['hex']"
|
||||
default-show
|
||||
class="w-full"
|
||||
@update:value="v => (model = v)"
|
||||
@update:show="v => !v && (showPicker = false)"
|
||||
/>
|
||||
<!-- 取色器用普通 div 绝对定位悬浮在输入框下方,不占文档流,避免展开时擑高布局导致同行字段位移 -->
|
||||
<div v-if="showPicker" class="absolute left-0 top-full z-30 mt-4px w-full">
|
||||
<NColorPicker
|
||||
:value="model || '#FFFFFF'"
|
||||
:show-alpha="false"
|
||||
:modes="['hex']"
|
||||
default-show
|
||||
class="w-full"
|
||||
@update:value="v => (model = v)"
|
||||
@update:show="v => !v && (showPicker = false)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user