[CMS-VISUAL] 站点管理改版: 表格→站点工作台(主站宽卡+院系子站卡片网格)+ 页头承载站点身份 + v3.1 视觉修订(182 测试绿)
build / build (push) Successful in 2m20s

- SiteManage.vue: 8 列表格改为站点工作台,按 schId 分组「主站宽卡 + 院系子站网格」,
  保留卡片⇄表格视图切换与搜索过滤;scope=all 全局视图支持多校分组,搜索时主站卡始终保留(层级锚点)
- 新增 views/cms/components/SiteCard.vue、SiteMainCard.vue
- 删除 components/SiteContextCard.vue: 站点身份改由页头「学校名.站点名」承担(SchoolLayout),
  侧栏不再铺第二份站点列表,切站入口从 3 处收敛到 2 处(面包屑 + 站点工作台)
- v3.1 视觉修订: 色带语义统一为「当前站」(全页同时只有一张卡带选中态,不再一页两处蓝);
  删除面包屑 band-sub 橙色接缝条(身份已收回页头);返回按钮 ios-undo 18×18 → ios-arrow-back 26×26 可点方块;
  删除 SiteManage .zone-tip 设计说明文字(误当 UI 渲染)
- 测试: siteLayer.spec.ts 站点卡 4 用例重写为页头反向/正向断言,siteManage.spec.ts 补卡片视图断言;182 用例绿
- 方案: docs/22-站点管理修订方案.md(含 §9 v3.1 视觉修订与像素对照表)
This commit is contained in:
chenxing
2026-09-28 18:32:13 +08:00
parent b6b022e4ca
commit cff4325947
8 changed files with 902 additions and 309 deletions
+261
View File
@@ -0,0 +1,261 @@
<script setup lang="ts">
import { computed } from "vue";
import { assetUrl } from "@/api/module/cms";
import { portalUrlOf } from "@/utils/sitePath";
import { SiteVo, ThemeVo } from "@/types/cms";
/**
* 院系子站卡(docs/22):站点工作台网格单元。
* 信息按「身份 → 规模 → 皮肤 → 地址 → 动作」纵向分层,不再与表格争列宽。
* 当前站 = 蓝描边 + 「当前」绿徽章;停用 = 降透明 + 灰图标 + 「进入管理」禁用。
*/
const props = defineProps<{
site: SiteVo;
isCurrent: boolean;
theme?: ThemeVo;
}>();
const emit = defineEmits<{
(e: "enter", site: SiteVo): void;
(e: "setting", site: SiteVo): void;
(e: "portal", site: SiteVo): void;
}>();
const disabled = computed(() => props.site.status !== 1);
/** 当前站无需「进入管理」(已在站内),与停用同视为不可达 */
const enterDisabled = computed(() => disabled.value || props.isCurrent);
/**
* 规模统计:SiteVo 暂未提供聚合字段(docs/22 Q1),故当前渲染为空、该行自动隐藏;
* 后端在 /api/site/list 补齐 categoryCount / articleCount / updateTime 后自动出现,模板不动。
*/
const stats = computed(() => {
const s = props.site as any;
const list: { num: string | number; lbl: string }[] = [];
if (s.categoryCount != null) list.push({ num: s.categoryCount, lbl: "栏目" });
if (s.articleCount != null) list.push({ num: s.articleCount, lbl: "文章" });
if (s.updateTime) list.push({ num: String(s.updateTime).slice(5, 10), lbl: "更新" });
return list;
});
/** 皮肤缩略图(走 assetUrl 补资源前缀,与表格视图同源) */
const themeThumb = computed(() => (props.theme?.screenshotUrl ? assetUrl(props.theme.screenshotUrl) : ""));
const themeTitle = computed(() => props.theme?.title || props.site.theme || "default");
</script>
<template>
<div class="site-card" :class="{ 'is-cur': isCurrent, disabled }">
<div class="sc-head">
<span class="sc-ico" :class="disabled ? 'off' : 'sub'">
<Icon type="ios-cog" />
</span>
<div class="sc-id">
<div class="sc-name">
<h4 :title="site.name">{{ site.name }}</h4>
<span class="sc-badge sub">院系子站</span>
<span v-if="isCurrent" class="sc-badge cur">当前</span>
<span v-else-if="disabled" class="sc-badge off">停用</span>
</div>
<p class="sc-code">{{ site.path || site.code }}</p>
</div>
</div>
<div v-if="stats.length" class="sc-meta">
<span v-for="st in stats" :key="st.lbl">{{ st.lbl }} <b>{{ st.num }}</b></span>
</div>
<div class="sc-theme">
<span class="thumb" :style="themeThumb ? { backgroundImage: `url(${themeThumb})` } : null"></span>
<span class="tname" :title="themeTitle">
{{ themeTitle }}
<span v-if="site.theme" class="tspec">{{ site.theme }}</span>
</span>
</div>
<p class="sc-domain">
<Icon type="ios-globe-outline" />
<code :title="portalUrlOf(site)">{{ portalUrlOf(site) }}</code>
</p>
<div class="sc-actions">
<Button type="primary" size="small" :disabled="enterDisabled" @click="emit('enter', site)">
进入管理
</Button>
<Button size="small" @click="emit('setting', site)">站点设置</Button>
<Button size="small" @click="emit('portal', site)">门户</Button>
</div>
</div>
</template>
<style scoped lang="scss">
/* 与超管 SchoolWorkbench 的卡片语法同构:白底 8px 圆角 + 1px 边 + hover 抬升 2px */
.site-card {
position: relative;
overflow: hidden;
background: #fff;
border: 1px solid #e8eaec;
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.2s, transform 0.2s, border-color 0.2s;
&:hover {
box-shadow: 0 4px 16px rgba(31, 42, 58, 0.12);
transform: translateY(-2px);
border-color: #c9d6fb;
}
/* 当前操作站点:4px 左色带 + 蓝描边 + 淡蓝光环。
与 SiteMainCard 同一套语义 —— 全页同时只有一张卡带「选中」,不再出现第二处蓝色标识。 */
&.is-cur {
border-color: #9db4f7;
box-shadow: 0 0 0 2px rgba(87, 122, 251, 0.12);
}
&.is-cur::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: #577afb;
}
&.disabled {
opacity: 0.62;
background: #fafbfc;
&:hover {
transform: none;
box-shadow: 0 1px 2px rgba(31, 42, 58, 0.06);
}
}
}
.sc-head {
display: flex;
align-items: flex-start;
gap: 10px;
}
/* 身份色:主站 #577afb / 子站 #ff9900(与 SiteSwitcher 定稿身份色一致) */
.sc-ico {
flex-shrink: 0;
width: 34px;
height: 34px;
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
background: #577afb;
&.sub {
background: #ff9900;
}
&.off {
background: #c0c4cc;
}
}
.sc-id {
min-width: 0;
flex: 1;
}
.sc-name {
display: flex;
align-items: center;
gap: 6px;
h4 {
margin: 0;
font-size: 15px;
color: #17233d;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.sc-badge {
flex-shrink: 0;
font-size: 10px;
line-height: 16px;
padding: 0 5px;
border-radius: 3px;
color: #fff;
&.sub {
background: #ff9900;
}
&.cur {
background: #2ba471;
}
&.off {
background: #c0c4cc;
}
}
.sc-code {
margin: 3px 0 0;
font-size: 12px;
color: #808695;
font-family: Consolas, Monaco, monospace;
}
.sc-meta {
display: flex;
gap: 14px;
margin: 11px 0 0;
font-size: 12px;
color: #515a6e;
b {
color: #17233d;
}
}
.sc-theme {
display: flex;
align-items: center;
gap: 8px;
margin: 10px 0 0;
padding-top: 10px;
border-top: 1px dashed #eceff3;
font-size: 12px;
color: #656667;
.thumb {
width: 52px;
height: 32px;
border-radius: 4px;
border: 1px solid #e8eaec;
flex-shrink: 0;
background: #f0f2f5 center/cover no-repeat;
}
.tname {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tspec {
color: #a0a6b0;
font-family: Consolas, Monaco, monospace;
font-size: 11px;
}
}
.sc-domain {
margin: 8px 0 0;
font-size: 12px;
color: #808695;
display: flex;
align-items: center;
gap: 5px;
code {
font-family: Consolas, Monaco, monospace;
color: #656667;
background: #f5f7fa;
border-radius: 3px;
padding: 1px 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.sc-actions {
display: flex;
gap: 8px;
margin: 14px 0 0;
/* 三个动作等宽铺满(iview Button 是子组件根节点,scoped 属性会下发到根 DOM) */
.ivu-btn {
flex: 1;
padding-left: 0;
padding-right: 0;
}
}
</style>