feat(theme+规范): 管理端接入 yh-theme 主题层并按 bsch 应用页规范升级页面骨架

- 主题层: scripts/gen-yh-theme.mjs 生成 assets/styles/yh-theme.css(构建期静态化 color-mix),
  5 主题档×侧栏深浅×渐变×side-plain 挂 html 档位类名; main.ts 于 icons.css 后引入并 initTheme() 消首帧闪色
- utils/theme.ts(+8 用例): localStorage(yh-theme) 持久化 + <html> 档位类名幂等切换
- ThemeSwitcher.vue(+6 用例): 顶栏配色胶囊(5 色网格+3 开关), 定稿 07-seam-pure 同款皮肤
- SchoolLayout: 顶栏 logo 徽标+配色切换器挂载; 站点身份色带退役, 子站橙由 yh-theme .band-sub::before 融合
- useTableHeight 响应化(+5 用例): ResizeObserver(body) 重算修复窗口缩放冻结, 常量 56/56 档不动
- bsch 规范骨架: 操作区 fix mb-12/次按钮 fr, Table 补 link-table table-bottom-box,
  文章关键字 200px 即输即搜(300ms 防抖去 enter-button), 附件分页 customPageCount 记忆+page-box fr,
  轮播展示位移入搜索区, 友链分组改 tabs-wrapper+Tabs card, 五表单校验失败统一"请填写红色星号内容!"
- 142 用例全绿, vite build 通过
This commit is contained in:
chenxing
2026-09-19 16:48:16 +08:00
parent 6f1f666479
commit f29c193ea7
19 changed files with 1299 additions and 72 deletions
+4 -1
View File
@@ -139,7 +139,10 @@ function buildRequest() {
async function save(): Promise<number | undefined> {
const valid = await formRef.value?.validate();
if (!valid) return undefined;
if (!valid) {
Message.warning("请填写红色星号内容!"); // bsch §4 统一提示
return undefined;
}
saving.value = true;
try {
return await articleApi.save(buildRequest()).send();
+14 -6
View File
@@ -116,6 +116,13 @@ function uploadDone(res: any) {
}
}
function pageSizeUpdate(size: number) {
pageSize.value = size;
localStorage.setItem("customPageCount", String(size));
pageNum.value = 1;
run();
}
onMounted(run);
// 站点上下文切换即时重载(docs/09 §3.3)
@@ -128,7 +135,7 @@ watch(() => store.siteId, () => {
<template>
<div class="page-wrapper">
<div class="main-container">
<div class="fix">
<div class="fix mb-12">
<Upload
class="fl"
:action="uploadUrl() + '?siteId=' + store.siteId"
@@ -140,14 +147,15 @@ watch(() => store.siteId, () => {
>
<Button type="primary" size="large" icon="ios-cloud-upload-outline">上传附件</Button>
</Upload>
<Button class="fl" style="margin-left: 8px" size="large" @click="cleanOrphans">
<Button class="fr" size="large" @click="cleanOrphans">
未使用附件({{ unusedCount }})
</Button>
</div>
<div class="search-wrap">
<Checkbox v-model="filter.orphanOnly" @on-change="pageNum = 1; run()">仅看未使用附件</Checkbox>
<div class="search-wrap fix">
<Checkbox class="fl" v-model="filter.orphanOnly" @on-change="pageNum = 1; run()">仅看未使用附件</Checkbox>
</div>
<Table
class="link-table table-bottom-box"
:columns="columns"
:data="rows"
:height="tableHeight"
@@ -156,12 +164,12 @@ watch(() => store.siteId, () => {
/>
<div class="page-panel">
<Page
class="fr"
class="page-box fr"
:total="total"
:current="pageNum"
:page-size="pageSize"
:page-size-opts="[10, 20, 30, 40]"
@on-page-size-change="(s: number) => { pageSize = s; pageNum = 1; run(); }"
@on-page-size-change="pageSizeUpdate"
@on-change="(p: number) => { pageNum = p; run(); }"
placement="top"
show-total
+20 -10
View File
@@ -148,7 +148,11 @@ function bannerUploaded(res: any) {
async function save() {
const valid = await formRef.value?.validate();
if (!valid) return;
if (!valid) {
// bsch 规范 §4:校验失败统一提示,不逐字段弹
Message.warning("请填写红色星号内容!");
return;
}
if (!form.imageUrl) {
Message.warning("请上传轮播图片");
return;
@@ -200,17 +204,27 @@ watch(position, () => run());
<template>
<div class="page-wrapper">
<div class="main-container">
<div class="fix">
<RadioGroup v-model="position" class="fl position-switch" type="button">
<Radio v-for="p in positionDict" :key="p.value" :label="p.value">{{ p.label }}</Radio>
</RadioGroup>
<div class="fix mb-12">
<Button class="fl" type="primary" size="large" @click="openCreate">
<Icon type="ios-add" />
新增轮播
</Button>
<span class="size-tip fr">{{ positionTip }}</span>
</div>
<Table :columns="columns" :data="rows" :height="tableHeight" :loading="loading" no-data-text="暂无轮播" />
<!-- 展示位是筛选维度不是操作,按 bsch 规范归搜索区(左侧下拉) -->
<div class="search-wrap fix">
<Select v-model="position" class="search-wrap_sel">
<Option v-for="p in positionDict" :key="p.value" :value="p.value">{{ p.label }}</Option>
</Select>
</div>
<Table
class="link-table table-bottom-box"
:columns="columns"
:data="rows"
:height="tableHeight"
:loading="loading"
no-data-text="暂无轮播"
/>
<Modal v-model="modalVisible" :title="form.id ? '编辑轮播' : '新增轮播'" :mask-closable="false" width="560px">
<Form ref="formRef" :model="form" :rules="rules" :label-width="100">
@@ -276,10 +290,6 @@ watch(position, () => run());
</template>
<style scoped lang="scss">
.position-switch {
margin-right: 12px;
margin-top: 6px;
}
.size-tip {
color: #999;
line-height: 40px;
+5 -1
View File
@@ -239,7 +239,11 @@ function findCycleParent(): string {
async function save() {
const valid = await refFormData.value?.validate();
if (!valid) return;
if (!valid) {
// bsch 规范 §4:校验失败统一提示
Message.warning("请填写红色星号内容!");
return;
}
// 成环校验(下拉已不再置灰,这里给出可读原因;后端 CategoryService.validateParent 同口径兜底)
const cycleHit = findCycleParent();
if (cycleHit) {
+13 -6
View File
@@ -127,7 +127,11 @@ function logoUploaded(res: any) {
async function save() {
const valid = await formRef.value?.validate();
if (!valid) return;
if (!valid) {
// bsch 规范 §4:校验失败统一提示
Message.warning("请填写红色星号内容!");
return;
}
saving.value = true;
try {
await linkApi
@@ -169,18 +173,21 @@ watch(() => store.siteId, () => run());
</script>
<template>
<div class="page-wrapper">
<div class="tabs-wrapper">
<!-- 分组即 tab(bsch 规范 §1:tabs-wrapper + Tabs type=card,与 svc-hall ServiceDictManage 同范式) -->
<Tabs v-if="groups.length" type="card" :value="activeGroup || '全部'" @on-click="(g: string) => (activeGroup = g === '全部' ? '' : g)">
<TabPane name="全部" label="全部" />
<TabPane v-for="g in groups" :key="g" :name="g" :label="`${g}(${rows.filter(r => r.groupName === g).length})`" />
</Tabs>
<div class="main-container">
<div class="fix">
<div class="fix mb-12">
<Button class="fl" type="primary" size="large" @click="openCreate">
<Icon type="ios-add" />
新增友链
</Button>
<Tabs v-if="groups.length" class="fr" :value="activeGroup" @on-click="(g: string) => (activeGroup = g)">
<TabPane v-for="g in groups" :key="g" :name="g" :label="`${g}(${rows.filter(r => r.groupName === g).length})`" />
</Tabs>
</div>
<Table
class="link-table table-bottom-box"
:columns="columns"
:data="visibleRows"
:height="tableHeight"
+2 -2
View File
@@ -193,13 +193,13 @@ watch(() => store.currentSchool?.schId, () => run());
<a href="javascript:void(0);" @click="goBackWorkbench">返回学校工作台</a>
</Alert>
</div>
<div class="fix" v-if="canCreateSite">
<div class="fix mb-12" v-if="canCreateSite">
<Button class="fl" type="primary" size="large" @click="openCreate">
<Icon type="ios-add" />
{{ isSchoolAdmin ? "新建院系子站" : "新建站点" }}
</Button>
</div>
<Table :columns="columns" :data="rows" :height="tableHeight" :loading="loading" no-data-text="暂无站点" />
<Table class="link-table table-bottom-box" :columns="columns" :data="rows" :height="tableHeight" :loading="loading" no-data-text="暂无站点" />
</div>
</div>
</template>
+20 -5
View File
@@ -251,7 +251,10 @@ async function submitSection(key: string, patch: { configJson?: string }) {
async function saveBasic() {
const valid = await basicRef.value?.validate();
if (!valid) return;
if (!valid) {
Message.warning("请填写红色星号内容!"); // bsch §4 统一提示
return;
}
await submitSection("basic", {});
}
@@ -263,22 +266,34 @@ async function saveTheme() {
}
async function saveDisplay() {
if (!(await displayFormRef.value?.validate())) return;
if (!(await displayFormRef.value?.validate())) {
Message.warning("请填写红色星号内容!"); // bsch §4
return;
}
await submitSection("display", {});
}
async function saveFooter() {
if (!(await footerFormRef.value?.validate())) return;
if (!(await footerFormRef.value?.validate())) {
Message.warning("请填写红色星号内容!"); // bsch §4
return;
}
await submitSection("footer", {});
}
async function saveSeo() {
if (!(await seoFormRef.value?.validate())) return;
if (!(await seoFormRef.value?.validate())) {
Message.warning("请填写红色星号内容!"); // bsch §4
return;
}
await submitSection("seo", {});
}
async function saveExtras() {
if (!(await extrasFormRef.value?.validate())) return;
if (!(await extrasFormRef.value?.validate())) {
Message.warning("请填写红色星号内容!"); // bsch §4
return;
}
await submitSection("extras", {});
}