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 视觉修订与像素对照表)
262 lines
7.3 KiB
Vue
262 lines
7.3 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):站点工作台网格单元。
|
||
* 信息按「身份 → 规模 → 皮肤 → 地址 → 动作」纵向分层,不再与表格争列宽。
|
||
* 当前站 = 蓝描边 + 「当前」绿徽章;停用 = 降透明 + 灰图标 + 「进入管理」禁用。
|
||
*/
|
||
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>
|