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 视觉修订与像素对照表)
237 lines
6.6 KiB
Vue
237 lines
6.6 KiB
Vue
<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):站点工作台的层级锚点。
|
|
* 主站唯一、子站都挂在它下面 —— 横向宽卡独占一行(左侧 4px 主色带),
|
|
* 与子站网格在版式上分区;层级不靠「所属主站」列的文字表达(那是旧表格的做法)。
|
|
*/
|
|
const props = defineProps<{
|
|
site: SiteVo;
|
|
isCurrent: boolean;
|
|
theme?: ThemeVo;
|
|
/** 本校院系子站总数(由父级按全量行统计,搜索态下不随过滤变化) */
|
|
subCount: number;
|
|
}>();
|
|
|
|
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);
|
|
|
|
/** 统计:子站数本地可算(必显);栏目/文章同 SiteCard —— 后端补齐字段后自动出现 */
|
|
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: "文章" });
|
|
list.push({ num: props.subCount, lbl: "子站" });
|
|
return list;
|
|
});
|
|
|
|
const themeThumb = computed(() => (props.theme?.screenshotUrl ? assetUrl(props.theme.screenshotUrl) : ""));
|
|
const themeTitle = computed(() => props.theme?.title || props.site.theme || "default");
|
|
</script>
|
|
|
|
<template>
|
|
<div class="main-card" :class="{ 'is-cur': isCurrent, disabled }">
|
|
<div class="mc-body">
|
|
<div class="mc-left">
|
|
<span class="sc-ico" :class="disabled ? 'off' : ''">
|
|
<Icon type="ios-school" />
|
|
</span>
|
|
<div class="sc-id">
|
|
<div class="sc-name">
|
|
<h4 :title="site.name">{{ site.name }}</h4>
|
|
<span class="sc-badge main">主站</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.code }} · {{ portalUrlOf(site) }}</p>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mc-stats">
|
|
<div class="st" v-for="st in stats" :key="st.lbl">
|
|
<div class="num">{{ st.num }}</div>
|
|
<div class="lbl">{{ st.lbl }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mc-actions">
|
|
<Button type="primary" :disabled="enterDisabled" @click="emit('enter', site)">进入管理</Button>
|
|
<Button @click="emit('setting', site)">站点设置</Button>
|
|
<Button @click="emit('portal', site)">门户预览</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
/* 主站宽卡:与子站卡共用同一套「当前站」语义 —— 4px 左色带 + 蓝描边 + 淡蓝光环,
|
|
全页同时只有一张卡带选中样式(原色带常驻表达「根节点」,会被误读成选中态)。
|
|
层级改由版式表达:宽卡独占一行 + 「主站」徽章 + 「N 子站」统计。
|
|
色带走 ::before 绝对定位而非 border-left —— 用边框会在切换态时把内容挤偏 3px。 */
|
|
.main-card {
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: #fff;
|
|
border: 1px solid #e8eaec;
|
|
border-radius: 8px;
|
|
margin-bottom: 26px;
|
|
&.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;
|
|
}
|
|
}
|
|
.mc-body {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
padding: 18px 20px;
|
|
}
|
|
.mc-left {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.sc-ico {
|
|
flex-shrink: 0;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 7px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 19px;
|
|
color: #fff;
|
|
background: #577afb;
|
|
&.off {
|
|
background: #c0c4cc;
|
|
}
|
|
}
|
|
.sc-id {
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.sc-name {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
h4 {
|
|
margin: 0;
|
|
font-size: 17px;
|
|
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;
|
|
&.main {
|
|
background: #577afb;
|
|
}
|
|
&.cur {
|
|
background: #2ba471;
|
|
}
|
|
&.off {
|
|
background: #c0c4cc;
|
|
}
|
|
}
|
|
.sc-code {
|
|
margin: 3px 0 0;
|
|
font-size: 12px;
|
|
color: #808695;
|
|
font-family: Consolas, Monaco, monospace;
|
|
}
|
|
.sc-theme {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin: 6px 0 0;
|
|
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;
|
|
}
|
|
}
|
|
.mc-stats {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
.st {
|
|
padding: 0 18px;
|
|
border-left: 1px solid #eef0f3;
|
|
text-align: center;
|
|
&:first-child {
|
|
border-left: none;
|
|
}
|
|
}
|
|
.num {
|
|
font-size: 19px;
|
|
font-weight: bold;
|
|
color: #17233d;
|
|
line-height: 1.2;
|
|
}
|
|
.lbl {
|
|
font-size: 11px;
|
|
color: #a0a6b0;
|
|
margin-top: 2px;
|
|
}
|
|
}
|
|
.mc-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|