Files
UserhomeFront/src/components/widgets/SyllabusWidget.vue
T
chenxing 3a7502f106
build / build (push) Successful in 46s
style(课表小组件): 表头垂直居中、左右留白 8px、行高 44px、科目 2 行省略号兜底
- th 补 vertical-align: middle —— base/_reset.scss(meyerweb) 给 th/td 统一设了 baseline,
  scoped 只给 td 写了 middle 导致表头文字按基线对齐、偏上,视觉上像"贴住卡片 header"
- .table-wrap padding 2px 15px 12px → 8px 8px 10px(小组件窄,收敛左右留白)
- td height 52 → 44px
- 科目包 <span class="sub" :title>,-webkit-line-clamp:2:超长科目最多 2 行、省略号收尾、悬停看全文
2026-10-14 08:44:27 +08:00

306 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="wg-syllabus" ref="refRoot">
<Skeleton
v-if="loading"
style="padding: 15px"
:loading="true"
:title="false"
:paragraph="{ rows: 4 }"
animated
/>
<template v-else>
<!-- 班级切换器:挂进卡片 header(index.vue 埋的 wg-hdr-<id> 锚点);多班(教师任教/班主任、校领导全校)才显示 -->
<Teleport v-if="classes.length > 1" :to="'#wg-hdr-' + widgetId">
<div class="cls-dd" :class="{ open: ddOpen }">
<button type="button" class="cls-trigger" @click.stop="ddOpen = !ddOpen">
<span class="cls-name">{{ classLabel(currentClass) }}</span>
<svg class="caret" viewBox="0 0 1024 1024" width="10" height="10" aria-hidden="true">
<path fill="currentColor" d="M512 704 128 320l64-64 320 320 320-320 64 64z" />
</svg>
</button>
<div v-show="ddOpen" class="cls-panel">
<div
v-for="(cls, index) in classes"
:key="cls.classId"
class="cls-item"
:class="{ on: index === clsIdx }"
@click="pickClass(index)"
>
{{ classLabel(cls) }}
</div>
</div>
</div>
</Teleport>
<div v-if="slots.length > 0" class="table-wrap">
<table class="tt">
<thead>
<tr>
<th>节</th>
<th v-for="day in dayCols" :key="day.index">{{ day.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="slot in slots" :key="slot.slotNo">
<td class="sec">{{ cnNum(slot.slotNo) }}</td>
<td v-for="day in dayCols" :key="day.index">
<span class="sub" :title="subjectOf(slot, day.index)">{{ subjectOf(slot, day.index) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 空态:模板内联 SVG,不依赖外部图片 / CSS 背景 -->
<div v-else class="wg-empty">
<svg width="112" height="112" viewBox="0 0 210 242" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect opacity="0.01" x="0.5" y="0.5" width="209" height="209" stroke="#DCDEE0" stroke-dasharray="5" />
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M21 69.5625H47.25V171.938H24.9375H21H2.625V95.8125H21V69.5625Z" fill="#F2F3F5" />
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M105 36.5217C104.368 36.671 103.709 36.75 103.031 36.75C98.3196 36.75 94.5 32.9304 94.5 28.2188C94.5 23.721 97.9807 20.0361 102.395 19.7108C102.382 19.486 102.375 19.2594 102.375 19.0312C102.375 12.8698 107.37 7.875 113.531 7.875C118.555 7.875 122.803 11.1953 124.201 15.7611C124.362 15.7537 124.524 15.75 124.688 15.75C130.486 15.75 135.188 20.451 135.188 26.25C135.188 32.049 130.486 36.75 124.688 36.75C124.243 36.75 123.805 36.7224 123.375 36.6688V36.75H105V36.5217Z" fill="#F2F3F5" />
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M20.8304 43.8601C20.5247 43.9314 20.2058 43.969 19.878 43.969C17.5982 43.969 15.75 42.1461 15.75 39.8973C15.75 37.7506 17.4342 35.9919 19.5704 35.8367C19.5638 35.7293 19.5605 35.6211 19.5605 35.5121C19.5605 32.5714 21.9774 30.1875 24.9587 30.1875C27.3894 30.1875 29.4449 31.7722 30.1212 33.9512C30.1994 33.9477 30.2779 33.946 30.3569 33.946C33.1629 33.946 35.4376 36.1896 35.4376 38.9573C35.4376 41.725 33.1629 43.9687 30.3569 43.9687C30.1417 43.9687 29.9297 43.9555 29.7215 43.9299V43.9688H20.8304V43.8601Z" fill="#F2F3F5" />
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M190.616 19.6875V38.0625H202.125V139.609H161.438V38.0625V19.6875H190.616Z" fill="#F2F3F5" />
<rect y="182.438" width="210" height="27.5625" fill="#F7F8FA" />
<rect opacity="0.6" x="107.625" y="82.6875" width="47.25" height="69.5625" rx="2" fill="#DCDCDC" />
<rect x="57.75" y="65.625" width="84" height="86.625" rx="2" fill="#F2F3F5" />
<rect x="65.625" y="73.5" width="68.25" height="72.1875" rx="1" fill="#fff" />
<rect x="77.4375" y="87.9375" width="44.625" height="7.875" rx="1" fill="#EAEDF1" />
<rect x="77.4375" y="106.312" width="44.625" height="7.875" fill="#EAEDF1" />
<rect x="77.4375" y="124.688" width="44.625" height="7.875" rx="1" fill="#EAEDF1" />
<rect x="47.25" y="145.688" width="115.5" height="36.75" rx="1" fill="#EAEDF1" />
<rect x="85.3125" y="160.125" width="39.375" height="7.875" rx="1" fill="#F7F8FA" />
</svg>
<span class="txt">暂无课表数据</span>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import UserSettingApi from "@/api/UserSettingApi";
defineProps<{ widgetId: number }>();
const emit = defineEmits(["loaded"]);
/** 节次中文数字(下标 0 = 第 1 节) */
const CN_NUM = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
/** 星期名(下标 0~6 = 周一~周日,对齐 slots[].subjects 下标口径) */
const WEEK_NAMES = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const refRoot = ref<HTMLElement | null>(null);
const loading = ref(true);
/** oapi data[]:每项一个班级(classId/className/gradeName/timetable.slots…) */
const classes = ref<any[]>([]);
const clsIdx = ref(0);
const ddOpen = ref(false);
let observer: ResizeObserver | null = null;
const currentClass = computed<any>(() => classes.value[clsIdx.value] || {});
/** 节次行:按 slotNo 升序(按接口实际返回渲染) */
const slots = computed<any[]>(() => {
const list = currentClass.value && currentClass.value.timetable && currentClass.value.timetable.slots;
if (!Array.isArray(list)) return [];
return [...list].sort((a, b) => ((a && a.slotNo) || 0) - ((b && b.slotNo) || 0));
});
/** 星期列:恒 周一~周五;周六/周日有课才追加 */
const dayCols = computed(() => {
const cols = [0, 1, 2, 3, 4];
if (slots.value.some(slot => subjectOf(slot, 5))) cols.push(5);
if (slots.value.some(slot => subjectOf(slot, 6))) cols.push(6);
return cols.map(index => ({ index, name: WEEK_NAMES[index] }));
});
/** 班级展示名 = 年级 + 班级(如「高一 1班」);无年级时退化为班级名 */
function classLabel(cls: any): string {
if (!cls) return "";
const grade = cls.gradeName ? String(cls.gradeName) : "";
const name = cls.className ? String(cls.className) : "";
return grade ? `${grade} ${name}` : name;
}
function subjectOf(slot: any, index: number): string {
const arr = slot && slot.subjects;
if (!Array.isArray(arr) || !arr[index]) return "";
return String(arr[index]);
}
function cnNum(slotNo: number): string {
return CN_NUM[slotNo - 1] || String(slotNo);
}
function pickClass(index: number) {
clsIdx.value = index;
ddOpen.value = false;
}
function closeDropdown() {
ddOpen.value = false;
}
onMounted(async () => {
document.addEventListener("click", closeDropdown);
if (window.ResizeObserver && refRoot.value) {
observer = new ResizeObserver(reportHeight);
observer.observe(refRoot.value);
}
try {
const data: any = await UserSettingApi.getWidgetData("syllabus", "");
classes.value = Array.isArray(data) ? data : [];
} catch (e) {
console.warn("[widget] syllabus 取数失败", e);
}
loading.value = false;
reportHeight();
});
onBeforeUnmount(() => {
document.removeEventListener("click", closeDropdown);
if (observer) observer.disconnect();
});
function reportHeight() {
if (refRoot.value) emit("loaded", refRoot.value.offsetHeight);
}
</script>
<style lang="scss" scoped>
.wg-syllabus {
padding: 0 0 10px;
}
/* ---------- 班级切换器(下拉;渲染在卡片 header 内,见 index.vue 的 wg-hdr-<id> 锚点) ---------- */
.cls-dd {
position: relative;
}
.cls-trigger {
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
background: transparent;
cursor: pointer;
font-family: inherit;
// header 是 700 粗体,切换器属次级信息 → 复位常规字重
font-weight: 400;
font-size: 13px;
color: #344449;
padding: 3px 8px;
border-radius: 6px;
line-height: 1.5;
&:hover {
background: #eef1f5;
}
.caret {
color: #98a2b0;
transition: transform 0.18s;
}
}
.cls-dd.open .caret {
transform: rotate(180deg);
}
.cls-panel {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 30;
min-width: 132px;
max-height: 264px;
overflow-y: auto;
background: #fff;
border: 1px solid #e7eaee;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.14);
padding: 4px;
}
.cls-item {
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
color: #344449;
cursor: pointer;
white-space: nowrap;
&:hover {
background: #f5f8fb;
}
&.on {
color: #2d8cf0;
background: #f0f7ff;
font-weight: 600;
}
}
/* ---------- 课表 ---------- */
/* ⚠️ 表格类名必须用私有名(tt):用 my-table 会命中全局 componets/_my_table.scss,
带进 #c7c7c7 竖线与 th 灰底 ⇒「竖线明显 / 表头与卡头糊成一片 / 横线被压得看不见」 */
.table-wrap {
padding: 8px 8px 10px;
}
.tt {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
th {
height: 44px;
font-size: 13px;
font-weight: 700;
color: #344449;
text-align: center;
// reset(meyerweb) 给 th/td 统一设了 vertical-align: baseline ⇒ 必须显式复位,
// 否则表头文字按基线对齐、偏上,视觉上像"贴住卡片 header"
vertical-align: middle;
border: 0;
border-bottom: 1px solid #e9edf1;
}
th:first-child,
td:first-child {
width: 46px;
}
td {
height: 44px;
border-bottom: 1px solid #eef1f4;
padding: 4px 2px;
text-align: center;
font-size: 13px;
vertical-align: middle;
}
tr:last-child td {
border-bottom: 0;
}
td.sec {
color: #6e7c87;
font-weight: 700;
}
}
/* 科目文本:最多 2 行,超出以省略号收尾,title 悬停可看全文 */
.sub {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
line-height: 1.35;
word-break: break-word;
}
/* ---------- 空态(模板内联 SVG,不依赖外部图片 / CSS 背景) ---------- */
.wg-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 132px;
padding: 16px 0 20px;
.txt {
font-size: 12px;
color: #98a2b0;
}
}
</style>