feat(课表小组件): SyllabusWidget改版直吃AcaSyllabu oapi结构——班级下拉切换器Teleport进卡头(通用header插槽能力),行=slotNo中文数字、列=周一~周五(周末有课才追加)、单元格=班级/科目;空态换原版no-data-box.svg插画;卡头钉死#f7f8f9+overflow visible供浮层溢出
build / build (push) Successful in 1m38s
build / build (push) Successful in 1m38s
This commit is contained in:
@@ -8,80 +8,124 @@
|
||||
:paragraph="{ rows: 4 }"
|
||||
animated
|
||||
/>
|
||||
<div v-else-if="grid.length > 0" class="table-wrap">
|
||||
<table class="my-table">
|
||||
<tr>
|
||||
<th>节</th>
|
||||
<th v-for="week in weekCols" :key="week.Weeks">{{ week.weekName }}</th>
|
||||
</tr>
|
||||
<tr v-for="row in grid" :key="row.label">
|
||||
<td class="sec">{{ row.label }}</td>
|
||||
<td v-for="(cell, index) in row.cells" :key="index">
|
||||
<template v-if="cell">
|
||||
<div class="cla">{{ cell.ClassName }}</div>
|
||||
<div class="sub">{{ cell.SubName }}</div>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="no-data">暂无数据</div>
|
||||
|
||||
<template v-else-if="classes.length > 0">
|
||||
<!-- 班级切换器:挂进卡片 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">{{ currentClass.className }}</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)"
|
||||
>
|
||||
{{ cls.className }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<div v-if="slots.length > 0" class="table-wrap">
|
||||
<table class="my-table">
|
||||
<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">
|
||||
<template v-if="subjectOf(slot, day.index)">
|
||||
<div class="cla">{{ currentClass.className }}</div>
|
||||
<div class="sub">{{ subjectOf(slot, day.index) }}</div>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="no-data-box"></div>
|
||||
</template>
|
||||
|
||||
<div v-else class="no-data-box"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
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);
|
||||
const list = ref<any[]>([]);
|
||||
/** oapi data[]:每项一个班级(classId/className/timetable.slots…) */
|
||||
const classes = ref<any[]>([]);
|
||||
const clsIdx = ref(0);
|
||||
const ddOpen = ref(false);
|
||||
let observer: ResizeObserver | null = null;
|
||||
|
||||
/** 表头星期(按 Weeks 去重升序),口径同旧 syllabus_index.js weekArr */
|
||||
const weekCols = computed(() => {
|
||||
const nameByKey: Record<number, string> = {};
|
||||
list.value.forEach(x => {
|
||||
if (nameByKey[x.Weeks] === undefined) nameByKey[x.Weeks] = x.weekName;
|
||||
});
|
||||
return Object.keys(nameByKey)
|
||||
.map(key => ({ Weeks: Number(key), weekName: nameByKey[Number(key)] }))
|
||||
.sort((a, b) => a.Weeks - b.Weeks);
|
||||
const currentClass = computed<any>(() => classes.value[clsIdx.value] || {});
|
||||
|
||||
/** 节次行:按 slotNo 升序(R2:按接口实际返回渲染) */
|
||||
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));
|
||||
});
|
||||
|
||||
/** 行节次(按 CourseTimes 去重升序),口径同旧 courseArr */
|
||||
const courseRows = computed(() => {
|
||||
const nameByKey: Record<number, string> = {};
|
||||
list.value.forEach(x => {
|
||||
if (nameByKey[x.CourseTimes] === undefined) nameByKey[x.CourseTimes] = x.CourseName;
|
||||
});
|
||||
return Object.keys(nameByKey)
|
||||
.map(key => ({ CourseTimes: Number(key), CourseName: nameByKey[Number(key)] }))
|
||||
.sort((a, b) => a.CourseTimes - b.CourseTimes);
|
||||
/** 星期列:恒 周一~周五;周六/周日有课才追加(R3) */
|
||||
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] }));
|
||||
});
|
||||
|
||||
const grid = computed(() => {
|
||||
const cellByKey: Record<string, any> = {};
|
||||
list.value.forEach(x => {
|
||||
cellByKey[`${x.CourseTimes}-${x.Weeks}`] = x;
|
||||
});
|
||||
return courseRows.value.map(course => ({
|
||||
label: (course.CourseName || "").replace("第", "").replace("节", ""),
|
||||
cells: weekCols.value.map(week => cellByKey[`${course.CourseTimes}-${week.Weeks}`]),
|
||||
}));
|
||||
});
|
||||
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", "");
|
||||
list.value = Array.isArray(data) ? data : [];
|
||||
classes.value = Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
console.warn("[widget] syllabus 取数失败", e);
|
||||
}
|
||||
@@ -89,7 +133,8 @@ onMounted(async () => {
|
||||
reportHeight();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("click", closeDropdown);
|
||||
if (observer) observer.disconnect();
|
||||
});
|
||||
|
||||
@@ -100,8 +145,73 @@ function reportHeight() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wg-syllabus {
|
||||
padding: 4px 0 10px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 课表 ---------- */
|
||||
.table-wrap {
|
||||
padding: 0 15px;
|
||||
}
|
||||
@@ -109,18 +219,19 @@ function reportHeight() {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
th {
|
||||
line-height: 20px;
|
||||
padding: 8px 0;
|
||||
height: 44px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: #6e7c87;
|
||||
text-align: center;
|
||||
}
|
||||
td {
|
||||
border-top: 1px solid #f7f7f7;
|
||||
line-height: 15px;
|
||||
padding: 8px 0;
|
||||
height: 52px;
|
||||
border-top: 1px solid #f2f4f6;
|
||||
padding: 4px 2px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td.sec {
|
||||
color: #6e7c87;
|
||||
@@ -128,17 +239,19 @@ function reportHeight() {
|
||||
}
|
||||
.cla {
|
||||
color: #344449;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.sub {
|
||||
color: #98a2b0;
|
||||
line-height: 1.35;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.no-data {
|
||||
min-height: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #b9c2cc;
|
||||
font-size: 13px;
|
||||
/* 空态:沿用原版 no-data-box.svg(插画 + 转曲「暂无数据」文案)。
|
||||
原版规则为 background-size:100px 100px(会压扁),此处取 100px auto 保比例。 */
|
||||
.no-data-box {
|
||||
min-height: 130px;
|
||||
background: url("../../assets/image/no-data-box.svg") no-repeat 50% 50%;
|
||||
background-size: 100px auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user