feat(admin): 站点设置页三处体验修复 + 主题参数横排色卡 + 新皮肤 mock
build / build (push) Successful in 2m19s

用户验收反馈三处:
1. 左侧锚点导航不固定——根因 .base-wrap-main .main-container 的 legacy
   overflow:hidden 使 sticky 相对不滚动的隐藏滚动盒定位失效(实测滚动 600px
   导航 top=-387);本页运行时改写为 visible、离开还原,修复后滚动 800px
   导航稳定停在 top:72px。窄屏(≤1080)导航转横排胶囊。
2. 皮肤颜色参数竖排长蛇(真实主题 12 个色参数 = 12 行)——ThemeParamForm
   重排:color 参数转 150px 起步 auto-fill 色卡网格(宽屏一行 4~5 张,
   色卡含取色器+名称+HEX),select/image/text 转两列行式;非法值红框提示。
3. 「换肤/改参数保存后门户立即生效」提示框样式突兀且与按钮拥挤——移除
   皮肤区内联 form-hint,并入卡片 footer 的 card-hint(与基本信息卡一致)。
整页配套:分区卡 10px 圆角+柔和阴影+品牌竖条标题、锚点导航品牌指示条、
footer hint 自适应换行。
mock THEMES 补 poly-blue/agri-forest/jade-normal 三套新皮肤条目。
vitest 147/147 绿;vite build 通过。
This commit is contained in:
chenxing
2026-09-20 02:42:07 +08:00
parent e5a9506228
commit abccca4397
3 changed files with 307 additions and 73 deletions
+171 -52
View File
@@ -5,8 +5,10 @@ import { effectiveValue, invalidParamKeys, FormModelValue } from "@/utils/themeP
/**
* 主题参数微调表单(方案 §6.3):按 theme.json params schema 动态生成。
* color→ColorPicker、select→Select、image→Input(手填 /cms/upload/ 路径,走附件上传后回填)、
* text→Input。父级 v-model 绑定 FormModelValue(声明过的 key,值已兜底为默认)。
* 2026-09-20 布局重排(用户反馈"颜色设置竖着太长"):
* - color 参数 → 色卡网格(横排自动换行,auto-fill 150px;真实主题 12 个色参数不再拉出 12 行长蛇);
* - select/image/text → 两列行式(label + 控件同行);
* - 非法值红框 + 行内提示;父级 v-model 绑定 FormModelValue(声明过的 key,值已兜底为默认)。
*/
const props = defineProps<{
theme?: ThemeVo;
@@ -19,6 +21,8 @@ const emit = defineEmits<{
const themeName = computed(() => props.theme?.title || props.theme?.name || "");
const params = computed(() => props.theme?.params || []);
const colorParams = computed(() => params.value.filter(p => p.type === "color"));
const otherParams = computed(() => params.value.filter(p => p.type !== "color"));
const badKeys = computed(() =>
props.theme ? invalidParamKeys(props.theme, props.modelValue) : []
);
@@ -38,71 +42,186 @@ function isBad(key: string): boolean {
<template>
<div v-if="!theme" class="empty-tip">请先选择皮肤包</div>
<Form v-else :label-width="90" class="theme-param-form">
<div v-else class="theme-param-form">
<div class="param-head">
参数微调 · {{ themeName }}
<span class="param-head-title">参数微调 · {{ themeName }}</span>
<span v-if="badKeys.length" class="param-warn">
有 {{ badKeys.length }} 个非法值未生效(将回退皮肤默认)
{{ badKeys.length }} 个非法值未生效(保存时将回退皮肤默认)
</span>
<span class="param-head-tip">点击色卡可修改;保存后门户立即生效</span>
</div>
<FormItem v-for="p in params" :key="p.key" :label="p.label">
<!-- color:色板 -->
<ColorPicker
v-if="p.type === 'color'"
:model-value="displayValue(p.key)"
recommend
@on-change="(c: string) => setValue(p.key, c)"
/>
<!-- select:下拉 -->
<Select
v-else-if="p.type === 'select'"
:model-value="displayValue(p.key)"
style="width: 260px"
@on-change="(v: string) => setValue(p.key, v)"
<!-- color:色卡网格(横排) -->
<div v-if="colorParams.length" class="param-grid">
<div
v-for="p in colorParams"
:key="p.key"
class="param-chip"
:class="{ bad: isBad(p.key) }"
:title="p.key"
>
<Option v-for="o in p.options" :key="o.value" :value="o.value">{{ o.label }}</Option>
</Select>
<!-- image:附件路径(上传后回填 /cms/upload/**) -->
<Input
v-else-if="p.type === 'image'"
:model-value="displayValue(p.key)"
placeholder="/cms/upload/cms/... (仅接受 /cms/upload/** 路径)"
style="width: 320px"
@on-change="(v: string) => setValue(p.key, v)"
/>
<!-- text:短文本 -->
<Input
v-else
:model-value="displayValue(p.key)"
:maxlength="256"
style="width: 320px"
@on-change="(v: string) => setValue(p.key, v)"
/>
<span v-if="isBad(p.key)" class="bad-mark">当前值非法({{ p.type }} 类型校验失败)</span>
</FormItem>
</Form>
<ColorPicker
:model-value="displayValue(p.key)"
recommend
@on-change="(c: string) => setValue(p.key, c)"
/>
<span class="chip-name">{{ p.label }}</span>
<span class="chip-hex">{{ displayValue(p.key) }}</span>
</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>
<div class="row-control">
<Select
v-if="p.type === 'select'"
:model-value="displayValue(p.key)"
size="small"
transfer
@on-change="(v: string) => setValue(p.key, v)"
>
<Option v-for="o in p.options" :key="o.value" :value="o.value">{{ o.label }}</Option>
</Select>
<Input
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)"
/>
<Input
v-else
:model-value="displayValue(p.key)"
size="small"
:maxlength="256"
@on-change="(v: string) => setValue(p.key, v)"
/>
<span v-if="isBad(p.key)" class="bad-mark">非法值({{ p.type }} 校验失败)</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.empty-tip {
color: #999;
color: #8a93a6;
font-size: 13px;
padding: 12px 0;
}
.param-head {
font-size: 13px;
font-weight: 600;
color: #666;
margin-bottom: 4px;
.theme-param-form {
margin-top: 10px;
}
.param-warn {
color: #ed4014;
font-weight: 400;
margin-left: 8px;
.param-head {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 10px;
.param-head-title {
font-size: 13px;
font-weight: 600;
color: #1e2635;
}
.param-head-tip {
color: #8a93a6;
font-size: 12px;
}
.param-warn {
color: #e54d42;
font-size: 12px;
}
}
/* color 色卡网格:横排自动换行(150px 起步,宽屏一行 4~5 张) */
.param-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.param-chip {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 10px;
border: 1px solid #e9edf5;
border-radius: 8px;
background: #fff;
transition: border-color 0.2s, box-shadow 0.2s;
&:hover {
border-color: #b9c8f7;
box-shadow: 0 4px 12px -8px rgba(87, 122, 251, 0.45);
}
&.bad {
border-color: #e54d42;
}
.chip-name {
font-size: 12px;
color: #1e2635;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chip-hex {
font-size: 11px;
font-family: Consolas, Menlo, monospace;
color: #8a93a6;
}
/* ColorPicker 触发器整条拉宽成色条 */
:deep(.ivu-color-picker) {
width: 100%;
display: block;
}
:deep(.ivu-color-picker .ivu-input-wrapper) {
display: block;
}
:deep(.ivu-color-picker .ivu-input) {
width: 100%;
height: 22px;
border-radius: 6px;
cursor: pointer;
}
}
/* select/image/text:两列行式 */
.param-rows {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 8px 18px;
margin-top: 12px;
}
.param-row {
display: flex;
align-items: center;
gap: 10px;
&.bad .row-label {
color: #e54d42;
}
.row-label {
flex: none;
width: 76px;
font-size: 13px;
color: #1e2635;
text-align: right;
}
.row-control {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
.ivu-select,
.ivu-input-wrapper {
flex: 1;
min-width: 0;
}
}
}
.bad-mark {
color: #ed4014;
color: #e54d42;
font-size: 12px;
margin-left: 8px;
white-space: nowrap;
}
</style>