feat(theme): S5 学院蓝主题改造(管理端)——homeLayout编辑器/主题参数表单/站点设置/类型 + 单测
build / build (push) Successful in 2m19s
build / build (push) Successful in 2m19s
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ThemeVo } from "@/types/cms";
|
||||
import ImageAssetField from "@/components/ImageAssetField.vue";
|
||||
import { effectiveValue, invalidParamKeys, FormModelValue } from "@/utils/themeParamForm";
|
||||
|
||||
/**
|
||||
* 主题参数微调表单(方案 §6.3):按 theme.json params schema 动态生成。
|
||||
* 主题参数微调表单(方案 §6.3 + 工作项 T):按 theme.json params schema 动态生成,
|
||||
* 值语义为「皮肤包级参数值」(`ThemeVo.paramValues`,落 themes/.meta/{name}.json)——
|
||||
* 一包一份值,引用同包的站点共享;需要差异走「复制皮肤包」。
|
||||
* 2026-09-20 布局重排(用户反馈"颜色设置竖着太长"):
|
||||
* - color 参数 → 色卡网格(横排自动换行,auto-fill 150px;真实主题 12 个色参数不再拉出 12 行长蛇);
|
||||
* - select/image/text → 两列行式(label + 控件同行);
|
||||
* - 非法值红框 + 行内提示;父级 v-model 绑定 FormModelValue(声明过的 key,值已兜底为默认)。
|
||||
* - select/image/text → 两列行式(label + 控件同行);image 走 ImageAssetField(上传 + 手填双通道);
|
||||
* - 非法值红框 + 行内提示;「已修改」标记 + 单项恢复默认 + 整体重置(对照 params[].defaultValue)。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
theme?: ThemeVo;
|
||||
modelValue: FormModelValue;
|
||||
/** image 参数上传归属站点(皮肤包管理借用内容源站点;缺省则仅可手填路径) */
|
||||
siteId?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -26,6 +31,7 @@ const otherParams = computed(() => params.value.filter(p => p.type !== "color"))
|
||||
const badKeys = computed(() =>
|
||||
props.theme ? invalidParamKeys(props.theme, props.modelValue) : []
|
||||
);
|
||||
const modifiedCount = computed(() => params.value.filter(p => isModified(p.key)).length);
|
||||
|
||||
function setValue(key: string, value: string) {
|
||||
emit("update:modelValue", { ...props.modelValue, [key]: value });
|
||||
@@ -35,6 +41,25 @@ function displayValue(key: string): string {
|
||||
return effectiveValue(props.theme?.params.find(p => p.key === key), props.modelValue);
|
||||
}
|
||||
|
||||
/** 是否偏离包内声明的默认值(= 会被写进 .meta paramValues 的那些项) */
|
||||
function isModified(key: string): boolean {
|
||||
const p = params.value.find(x => x.key === key);
|
||||
if (!p) return false;
|
||||
return displayValue(key) !== (p.defaultValue ?? "");
|
||||
}
|
||||
|
||||
function resetOne(key: string) {
|
||||
const p = params.value.find(x => x.key === key);
|
||||
setValue(key, p?.defaultValue ?? "");
|
||||
}
|
||||
|
||||
/** 整体重置 = 提交全默认值模型(父级据此给后端空 Map,恢复 theme.json 默认) */
|
||||
function resetAll() {
|
||||
const next: FormModelValue = {};
|
||||
params.value.forEach(p => (next[p.key] = p.defaultValue ?? ""));
|
||||
emit("update:modelValue", next);
|
||||
}
|
||||
|
||||
function isBad(key: string): boolean {
|
||||
return badKeys.value.includes(key);
|
||||
}
|
||||
@@ -48,7 +73,11 @@ function isBad(key: string): boolean {
|
||||
<span v-if="badKeys.length" class="param-warn">
|
||||
{{ badKeys.length }} 个非法值未生效(保存时将回退皮肤默认)
|
||||
</span>
|
||||
<span class="param-head-tip">点击色卡可修改;保存后门户立即生效</span>
|
||||
<span class="param-head-tip">
|
||||
{{ modifiedCount ? `已修改 ${modifiedCount} 项` : "未修改(全部为包内默认值)" }}
|
||||
· 保存后引用该包的站点门户立即生效
|
||||
</span>
|
||||
<Button v-if="modifiedCount" size="small" ghost class="reset-all" @click="resetAll">整体恢复默认</Button>
|
||||
</div>
|
||||
|
||||
<!-- color:色卡网格(横排) -->
|
||||
@@ -57,7 +86,7 @@ function isBad(key: string): boolean {
|
||||
v-for="p in colorParams"
|
||||
:key="p.key"
|
||||
class="param-chip"
|
||||
:class="{ bad: isBad(p.key) }"
|
||||
:class="{ bad: isBad(p.key), modified: isModified(p.key) }"
|
||||
:title="p.key"
|
||||
>
|
||||
<ColorPicker
|
||||
@@ -65,15 +94,30 @@ function isBad(key: string): boolean {
|
||||
recommend
|
||||
@on-change="(c: string) => setValue(p.key, c)"
|
||||
/>
|
||||
<span class="chip-name">{{ p.label }}</span>
|
||||
<span class="chip-name">
|
||||
{{ p.label }}
|
||||
<span v-if="isModified(p.key)" class="chip-flag">已修改</span>
|
||||
</span>
|
||||
<span class="chip-hex">{{ displayValue(p.key) }}</span>
|
||||
<Button
|
||||
v-if="isModified(p.key)"
|
||||
size="small"
|
||||
ghost
|
||||
class="chip-reset"
|
||||
@click="resetOne(p.key)"
|
||||
>
|
||||
恢复默认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- select / image / text:两列行式 -->
|
||||
<div v-if="otherParams.length" class="param-rows">
|
||||
<div v-for="p in otherParams" :key="p.key" class="param-row" :class="{ bad: isBad(p.key) }">
|
||||
<label class="row-label">{{ p.label }}</label>
|
||||
<label class="row-label">
|
||||
{{ p.label }}
|
||||
<span v-if="isModified(p.key)" class="chip-flag">已修改</span>
|
||||
</label>
|
||||
<div class="row-control">
|
||||
<Select
|
||||
v-if="p.type === 'select'"
|
||||
@@ -84,12 +128,12 @@ function isBad(key: string): boolean {
|
||||
>
|
||||
<Option v-for="o in p.options" :key="o.value" :value="o.value">{{ o.label }}</Option>
|
||||
</Select>
|
||||
<Input
|
||||
<ImageAssetField
|
||||
v-else-if="p.type === 'image'"
|
||||
:model-value="displayValue(p.key)"
|
||||
size="small"
|
||||
placeholder="/cms/upload/cms/... (仅接受 /cms/upload/** 路径)"
|
||||
@on-change="(v: string) => setValue(p.key, v)"
|
||||
:site-id="siteId"
|
||||
placeholder="/cms/upload/cms/... (上传或粘贴路径,仅接受 /cms/upload/** 与 /yschcms/upload/**)"
|
||||
@update:model-value="(v: string) => setValue(p.key, v)"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
@@ -98,6 +142,7 @@ function isBad(key: string): boolean {
|
||||
:maxlength="256"
|
||||
@on-change="(v: string) => setValue(p.key, v)"
|
||||
/>
|
||||
<Button v-if="isModified(p.key)" size="small" ghost @click="resetOne(p.key)">恢复默认</Button>
|
||||
<span v-if="isBad(p.key)" class="bad-mark">非法值({{ p.type }} 校验失败)</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +178,9 @@ function isBad(key: string): boolean {
|
||||
color: #e54d42;
|
||||
font-size: 12px;
|
||||
}
|
||||
.reset-all {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* color 色卡网格:横排自动换行(150px 起步,宽屏一行 4~5 张) */
|
||||
@@ -157,6 +205,10 @@ function isBad(key: string): boolean {
|
||||
&.bad {
|
||||
border-color: #e54d42;
|
||||
}
|
||||
&.modified {
|
||||
border-color: #ffd591;
|
||||
background: #fffdf7;
|
||||
}
|
||||
.chip-name {
|
||||
font-size: 12px;
|
||||
color: #1e2635;
|
||||
@@ -169,6 +221,13 @@ function isBad(key: string): boolean {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
color: #8a93a6;
|
||||
}
|
||||
.chip-reset {
|
||||
align-self: flex-start;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
/* ColorPicker 触发器整条拉宽成色条 */
|
||||
:deep(.ivu-color-picker) {
|
||||
width: 100%;
|
||||
@@ -185,6 +244,18 @@ function isBad(key: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/* 「已修改」小徽标:色卡与行式 label 共用 */
|
||||
.chip-flag {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
padding: 0 5px;
|
||||
border-radius: 7px;
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
/* select/image/text:两列行式 */
|
||||
.param-rows {
|
||||
display: grid;
|
||||
@@ -217,6 +288,10 @@ function isBad(key: string): boolean {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
:deep(.image-asset-field) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bad-mark {
|
||||
|
||||
Reference in New Issue
Block a user