fix(island,homework,schedule,web): 16 项反馈修复(联合通知点不开 / 设置不生效 / 红点位置 / 看板当天且铺满 / 单双周与周数可调 / Web 手机端与批量通知等)

一、灵动岛
- ①「紧急通知后来了新作业,收起再点开不了新作业」根因有二,都已修:
  (a) active 仍是紧急那条,点击只会反复打开它 —— 现在点击胶囊优先打开**最新**那条未处理消息(队列里更晚的);
  (b) 上课时段 syncWindow 会把普通通知立刻隐藏(表现为点了没反应)—— 新增 explicitShowId,
      "用户主动点开"的那条守卫放行(岛完全隐藏时仍严格不打扰)。
  冒烟新增「紧急通知后来了新作业:收起再点击打开的是新作业(联合通知回归)」。
- ② 设置实时预览/保存:补「设置页真实链路」断言(window.desktop.islandSetAppearance → IPC → 主进程 →
  渲染进程 CSS 变量 + 窗口尺寸),并断言"岛显示中改尺寸立即生效"(实测均通过)。
- ③ 未读红点移到「通知」图标右上角(此前在文字后面),并加 DOM 几何断言(dx>0 且 dy<0,
  没有未读时自动造一条未读通知再断言、断言后删除)。
- 冒烟稳定性:新增 island.setBlurCollapseEnabled()(默认开),测试期间关掉"失焦收起",
  避免系统抢焦点导致展开态被收回的随机失败;专门验证失焦的用例仍临时打开。

二、作业看板(学生端)
- ⑥ 只看当天:新增「只看今天」开关(默认开),按 createdAt 的本地日期过滤,昨天的过期作业不再混入。
- ⑧ 完整显示:卡片同时渲染标题 + 作业要求全文(列表模式也加「作业要求」列)。
- ⑨ 铺满不滚动:看板外层高度固定 + overflow hidden,按内容/可用高度算缩放系数用 transform: scale 适配
  (工具栏显示当前缩放比例),页面本身不出现滚动条。
- ⑦ 全屏看板标题栏用 #header 插槽:标题左对齐、当天日期时间绝对居中(受「显示时间」开关控制,每分钟刷新)。

三、课表与周数
- ⑤ 学期周数可调:新增 Class.termWeeks(默认 20,迁移 + 维护脚本),班主任在班级管理里改;
  GET /api/dashboard/term?classId= 返回该班 maxWeek,课表周次下拉与新建课表的默认 weekEnd 都按它来
  (不再固定 30 周)。
- ⑩ 课表管理只保留「导入 ClassIsland 课程表」入口(时间表由课程表 JSON 的 TimeLayouts 一并带入),
  「导入时间配置」按钮下线;ui-smoke 第 7.7 段改为断言新弹窗(解析 4 行 + 单双周标签 + 旧入口已下线)。

四、以班级为单位(⑬)与 Web 端
- 个人学生账号登录停用:POST /api/auth/login 对 role=STUDENT 返回 403(改用班级码 + 班级密码);
  学生行仍作为个人数据记录键由班级会话代全班读写;e2e 全面切换到班级会话并新增该断言。
- ④ 作业科目过少:新增共享 SUBJECT_CATALOG(16 个常见科目)+ 种子课程扩到全科目;
  Web 作业发布弹窗可选「其他科目(新建)」,提交前自动建课(同名复用)。
- ⑪ 通知支持多班批量发布(多选 + 逐班发布 + 汇总结果 + 二次确认覆盖所有选中班);
  ⑫ 单班只显示已读情况、跨班才显示已读率;⑭ 手机端课表可横向滚动看全 7 天;
  ⑮ 手机端已连接不再显示孤立绿点(仅未连接时显示文字标签);⑯ 手机端「叫人/发布通知」按钮同宽左对齐;
  ⑬ Web 学生管理去掉「用户名」列(新建自动生成学号式用户名,导入仍支持显式指定)。

验证:verify:e2e 149/149、verify:web 20/20、verify:desktop 75/75(复跑 68/68,打包便携版 75/75 packaged=true)、
typecheck / lint / format 全绿;服务端与 Web 已重新打包并部署到本机安装目录。
This commit is contained in:
LoveChengke committed 2026-09-13 08:39:11 +08:00
1 parent b8ef5d8dbb
commit 11008928a1
34 files changed
+1421 -340

No files matched your search

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { SOCKET_EVENTS, formatDate, type HomeworkDto, type HomeworkSubmissionDto } from '@classhelper/shared';
import { homeworkApi } from '../api/index.js';
@@ -13,7 +13,12 @@ const auth = useAuthStore();
const realtime = useRealtimeStore();
/** 看板/列表模式与看板外观(持久化到客户端配置,重启仍生效) */
const DEFAULT_BOARD = { mode: 'board' as 'board' | 'list', showTime: false, fontSize: 15 };
const DEFAULT_BOARD = {
mode: 'board' as 'board' | 'list',
showTime: false,
fontSize: 15,
todayOnly: true,
};
const loading = ref(false);
const homeworks = ref<HomeworkDto[]>([]);
@@ -27,20 +32,51 @@ const submitting = ref(false);
const viewMode = ref<'board' | 'list'>(DEFAULT_BOARD.mode);
const boardShowTime = ref(DEFAULT_BOARD.showTime);
const boardFontSize = ref(DEFAULT_BOARD.fontSize);
const todayOnly = ref(DEFAULT_BOARD.todayOnly);
const savingBoard = ref(false);
/** 全屏放大的科目(null = 未打开) */
const fullscreenCourse = ref<string | null>(null);
const fullscreenBoard = ref(false);
/* ------------------------------------------------------------ 当天的判定 */
/** 本地"今天"的日期串(YYYY-MM-DD) */
function localDayKey(value: string | Date): string {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/** 每分钟刷新一次"现在",用于标题栏时钟与"今天"的判定 */
const now = ref(new Date());
let clockTimer: ReturnType<typeof setInterval> | null = null;
const todayKey = computed(() => localDayKey(now.value));
const todayText = computed(
() =>
`${now.value.getFullYear()} 年 ${now.value.getMonth() + 1} 月 ${now.value.getDate()} 日 ` +
`${['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.value.getDay()]} ` +
`${String(now.value.getHours()).padStart(2, '0')}:${String(now.value.getMinutes()).padStart(2, '0')}`,
);
/** 按"今天"与完成状态过滤 */
const filtered = computed(() => {
if (filter.value === 'pending') return homeworks.value.filter((item) => item.completed !== true);
if (filter.value === 'done') return homeworks.value.filter((item) => item.completed === true);
return homeworks.value;
let list = homeworks.value;
if (todayOnly.value) list = list.filter((item) => localDayKey(item.createdAt) === todayKey.value);
if (filter.value === 'pending') return list.filter((item) => item.completed !== true);
if (filter.value === 'done') return list.filter((item) => item.completed === true);
return list;
});
const pendingCount = computed(() => homeworks.value.filter((item) => item.completed !== true).length);
const doneCount = computed(() => homeworks.value.filter((item) => item.completed === true).length);
const todayCount = computed(
() => homeworks.value.filter((item) => localDayKey(item.createdAt) === todayKey.value).length,
);
const pendingCount = computed(() => filtered.value.filter((item) => item.completed !== true).length);
const doneCount = computed(() => filtered.value.filter((item) => item.completed === true).length);
/**
* 看板分组:按科目聚合成卡片(未关联课程的归到「其他」),
@@ -60,6 +96,7 @@ const COURSE_ORDER = [
'音乐',
'美术',
'信息技术',
'通用技术',
];
interface BoardColumn {
@@ -98,6 +135,39 @@ const fullscreenItems = computed(() =>
: [],
);
/* ------------------------------------------------------------ 看板自适应缩放
*
* 需求:作业必须**全部完整显示**(标题 + 作业要求),且**不需要上下滚动**。
* 做法:先把卡片按视口宽度铺成网格,再按"内容高度 / 可用高度"算一个缩放系数,
* 用 transform: scale() 整体缩到刚好铺满(字号仍由用户滑块决定,缩放只是兜底适配)。
*/
const boardHostRef = ref<HTMLElement | null>(null);
const boardInnerRef = ref<HTMLElement | null>(null);
const boardScale = ref(1);
const fullscreenHostRef = ref<HTMLElement | null>(null);
const fullscreenInnerRef = ref<HTMLElement | null>(null);
const fullscreenScale = ref(1);
function computeScale(host: HTMLElement | null, inner: HTMLElement | null): number {
if (!host || !inner) return 1;
const available = host.clientHeight;
// 先按 1 倍量一次内容高度(transform 不影响 offsetHeight)
const content = inner.offsetHeight;
if (available <= 0 || content <= 0) return 1;
const scale = Math.min(1, available / content);
return Math.max(0.45, Math.round(scale * 1000) / 1000);
}
function fitBoards(): void {
void nextTick(() => {
boardScale.value = computeScale(boardHostRef.value, boardInnerRef.value);
fullscreenScale.value = computeScale(fullscreenHostRef.value, fullscreenInnerRef.value);
});
}
/* ------------------------------------------------------------ 数据加载 */
async function loadHomework(): Promise<void> {
loading.value = true;
try {
@@ -115,6 +185,7 @@ async function loadHomework(): Promise<void> {
if (current.value) {
current.value = homeworks.value.find((item) => item.id === current.value?.id) ?? current.value;
}
fitBoards();
} finally {
loading.value = false;
}
@@ -127,6 +198,7 @@ async function loadBoardSettings(): Promise<void> {
if (!saved) return;
if (saved.mode === 'board' || saved.mode === 'list') viewMode.value = saved.mode;
if (typeof saved.showTime === 'boolean') boardShowTime.value = saved.showTime;
if (typeof saved.todayOnly === 'boolean') todayOnly.value = saved.todayOnly;
if (typeof saved.fontSize === 'number' && Number.isFinite(saved.fontSize)) {
boardFontSize.value = Math.min(28, Math.max(11, Math.round(saved.fontSize)));
}
@@ -141,8 +213,10 @@ async function saveBoardSettings(): Promise<void> {
mode: viewMode.value,
showTime: boardShowTime.value,
fontSize: boardFontSize.value,
todayOnly: todayOnly.value,
},
});
fitBoards();
} finally {
savingBoard.value = false;
}
@@ -155,12 +229,14 @@ function openDetail(item: HomeworkDto): void {
function openCourseFullscreen(column: BoardColumn): void {
fullscreenCourse.value = column.course;
fitBoards();
}
/** 覆盖全屏:整块看板放大(点击标题栏「全屏」按钮) */
function openBoardFullscreen(): void {
fullscreenCourse.value = null;
fullscreenBoard.value = true;
fitBoards();
}
async function toggleComplete(item: HomeworkDto | null): Promise<void> {
@@ -266,15 +342,28 @@ function onRecovered(): void {
void loadHomework();
}
let resizeObserver: ResizeObserver | null = null;
onMounted(async () => {
await loadBoardSettings();
await loadHomework();
clockTimer = setInterval(() => {
now.value = new Date();
}, 30_000);
// 视口变化后重新适配(保证"全部显示在屏幕上"始终成立)
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(() => fitBoards());
if (boardHostRef.value) resizeObserver.observe(boardHostRef.value);
}
fitBoards();
realtime.on(SOCKET_EVENTS.homeworkNew, onHomeworkEvent);
realtime.on(SOCKET_EVENTS.homeworkUpdated, onHomeworkEvent);
appStore.onServerRecovered(onRecovered);
});
onUnmounted(() => {
if (clockTimer) clearInterval(clockTimer);
resizeObserver?.disconnect();
realtime.off(SOCKET_EVENTS.homeworkNew, onHomeworkEvent);
realtime.off(SOCKET_EVENTS.homeworkUpdated, onHomeworkEvent);
appStore.offServerRecovered(onRecovered);
@@ -283,16 +372,22 @@ onUnmounted(() => {
</script>
<template>
<div class="page">
<div class="page homework-page">
<div class="page-header">
<div>
<h2 class="page-title">我的作业</h2>
<p class="page-subtitle">
共 {{ homeworks.length }} 份 · 待完成 {{ pendingCount }} · 已完成 {{ doneCount }}
今天 {{ todayCount }} 份 · 待完成 {{ pendingCount }} · 已完成 {{ doneCount }}
<el-tag v-if="fromCache" size="small" type="warning" effect="plain">离线缓存</el-tag>
</p>
</div>
<div class="toolbar">
<el-switch
v-model="todayOnly"
active-text="只看今天"
inactive-text="全部日期"
@change="saveBoardSettings"
/>
<el-radio-group v-model="filter">
<el-radio-button value="all">全部</el-radio-button>
<el-radio-button value="pending">未完成</el-radio-button>
@@ -322,50 +417,57 @@ onUnmounted(() => {
:min="11"
:max="28"
:step="1"
style="width: 200px"
style="width: 180px"
@change="saveBoardSettings"
/>
<span class="text-muted">{{ boardFontSize }}px</span>
<el-button :icon="'FullScreen'" @click="openBoardFullscreen">全屏看板</el-button>
<span class="text-muted">自适应缩放 {{ Math.round(boardScale * 100) }}%</span>
<span v-if="savingBoard" class="text-muted">保存中…</span>
</div>
</el-card>
<el-card v-loading="loading" shadow="never">
<el-empty v-if="filtered.length === 0" description="没有符合条件的作业" />
<el-card v-loading="loading" shadow="never" class="homework-body">
<el-empty v-if="filtered.length === 0" description="今天没有作业" />
<!-- 看板模式:按科目分卡片,点击卡片放大全屏、点击条目看详情 -->
<div v-else-if="viewMode === 'board'" class="board" :style="boardStyle">
<section
v-for="column in boardColumns"
:key="column.course"
class="board-card"
:class="{ 'has-pending': column.pending > 0 }"
@click="openCourseFullscreen(column)"
>
<header class="board-card-head">
<span class="board-course">{{ column.course }}</span>
<span v-if="column.pending > 0" class="board-badge">{{ column.pending }}</span>
</header>
<ol class="board-list">
<li
v-for="(item, index) in column.items"
:key="item.id"
class="board-item"
:class="{ done: item.completed }"
@click.stop="openDetail(item)"
>
<span class="board-index">{{ index + 1 }}.</span>
<span class="board-title">{{ item.title }}</span>
<span v-if="boardShowTime" class="board-time">{{ formatDate(item.createdAt, true) }}</span>
<span v-if="item.completed" class="board-done-tag">已完成</span>
</li>
</ol>
<footer class="board-card-foot">
<span class="text-muted">共 {{ column.items.length }} 条</span>
<span class="board-zoom">点击放大</span>
</footer>
</section>
<div v-else-if="viewMode === 'board'" ref="boardHostRef" class="board-host">
<div ref="boardInnerRef" class="board" :style="{ ...boardStyle, '--board-scale': boardScale }">
<section
v-for="column in boardColumns"
:key="column.course"
class="board-card"
:class="{ 'has-pending': column.pending > 0 }"
@click="openCourseFullscreen(column)"
>
<header class="board-card-head">
<span class="board-course">{{ column.course }}</span>
<span v-if="column.pending > 0" class="board-badge">{{ column.pending }}</span>
</header>
<ol class="board-list">
<li
v-for="(item, index) in column.items"
:key="item.id"
class="board-item"
:class="{ done: item.completed }"
@click.stop="openDetail(item)"
>
<span class="board-index">{{ index + 1 }}.</span>
<span class="board-text">
<span class="board-title">{{ item.title }}</span>
<!-- 需求:作业要完整显示(标题 + 作业要求),不能只显示标题 -->
<span v-if="item.content.trim()" class="board-content">{{ item.content }}</span>
</span>
<span v-if="boardShowTime" class="board-time">{{ formatDate(item.createdAt, true) }}</span>
<span v-if="item.completed" class="board-done-tag">已完成</span>
</li>
</ol>
<footer class="board-card-foot">
<span class="text-muted">共 {{ column.items.length }} 条</span>
<span class="board-zoom">点击放大</span>
</footer>
</section>
</div>
</div>
<!-- 列表模式(默认表格视图) -->
@@ -377,7 +479,8 @@ onUnmounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="作业标题" min-width="220" show-overflow-tooltip />
<el-table-column prop="title" label="作业标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="content" label="作业要求" min-width="240" show-overflow-tooltip />
<el-table-column label="课程" width="110">
<template #default="{ row }">{{ row.course?.name ?? '-' }}</template>
</el-table-column>
@@ -410,45 +513,76 @@ onUnmounted(() => {
:title="`${fullscreenCourse ?? ''} · 作业`"
class="board-fullscreen"
>
<ol class="board-list board-list-lg" :style="fullscreenStyle">
<li
v-for="(item, index) in fullscreenItems"
:key="item.id"
class="board-item"
:class="{ done: item.completed }"
@click="openDetail(item)"
<template #header="{ titleId, titleClass }">
<div class="board-dialog-head">
<span :id="titleId" :class="titleClass">{{ fullscreenCourse ?? '' }} · 作业</span>
<!-- 需求:当天时间显示在标题栏正中间(受"显示时间"开关控制) -->
<span v-if="boardShowTime" class="board-dialog-clock">{{ todayText }}</span>
</div>
</template>
<div ref="fullscreenHostRef" class="board-host board-host-lg">
<ol
ref="fullscreenInnerRef"
class="board-list board-list-lg"
:style="{ ...fullscreenStyle, '--board-scale': fullscreenScale }"
>
<span class="board-index">{{ index + 1 }}.</span>
<span class="board-title">{{ item.title }}</span>
<span v-if="boardShowTime" class="board-time">{{ formatDate(item.createdAt, true) }}</span>
<span v-if="item.completed" class="board-done-tag">已完成</span>
</li>
</ol>
<el-empty v-if="fullscreenItems.length === 0" description="该科目暂无作业" />
<li
v-for="(item, index) in fullscreenItems"
:key="item.id"
class="board-item"
:class="{ done: item.completed }"
@click="openDetail(item)"
>
<span class="board-index">{{ index + 1 }}.</span>
<span class="board-text">
<span class="board-title">{{ item.title }}</span>
<span v-if="item.content.trim()" class="board-content">{{ item.content }}</span>
</span>
<span v-if="boardShowTime" class="board-time">{{ formatDate(item.createdAt, true) }}</span>
<span v-if="item.completed" class="board-done-tag">已完成</span>
</li>
</ol>
</div>
<el-empty v-if="fullscreenItems.length === 0" description="该科目今天没有作业" />
</el-dialog>
<!-- 全屏:整块看板放大 -->
<el-dialog v-model="fullscreenBoard" fullscreen title="今日作业看板" class="board-fullscreen">
<div class="board board-lg" :style="fullscreenStyle">
<section v-for="column in boardColumns" :key="column.course" class="board-card">
<header class="board-card-head">
<span class="board-course">{{ column.course }}</span>
<span v-if="column.pending > 0" class="board-badge">{{ column.pending }}</span>
</header>
<ol class="board-list">
<li
v-for="(item, index) in column.items"
:key="item.id"
class="board-item"
:class="{ done: item.completed }"
@click="openDetail(item)"
>
<span class="board-index">{{ index + 1 }}.</span>
<span class="board-title">{{ item.title }}</span>
<span v-if="boardShowTime" class="board-time">{{ formatDate(item.createdAt, true) }}</span>
</li>
</ol>
</section>
<el-dialog v-model="fullscreenBoard" fullscreen class="board-fullscreen">
<template #header="{ titleId, titleClass }">
<div class="board-dialog-head">
<span :id="titleId" :class="titleClass">今日作业看板</span>
<span v-if="boardShowTime" class="board-dialog-clock">{{ todayText }}</span>
</div>
</template>
<div ref="fullscreenHostRef" class="board-host board-host-lg">
<div
ref="fullscreenInnerRef"
class="board board-lg"
:style="{ ...fullscreenStyle, '--board-scale': fullscreenScale }"
>
<section v-for="column in boardColumns" :key="column.course" class="board-card">
<header class="board-card-head">
<span class="board-course">{{ column.course }}</span>
<span v-if="column.pending > 0" class="board-badge">{{ column.pending }}</span>
</header>
<ol class="board-list">
<li
v-for="(item, index) in column.items"
:key="item.id"
class="board-item"
:class="{ done: item.completed }"
@click="openDetail(item)"
>
<span class="board-index">{{ index + 1 }}.</span>
<span class="board-text">
<span class="board-title">{{ item.title }}</span>
<span v-if="item.content.trim()" class="board-content">{{ item.content }}</span>
</span>
<span v-if="boardShowTime" class="board-time">{{ formatDate(item.createdAt, true) }}</span>
</li>
</ol>
</section>
</div>
</div>
</el-dialog>
@@ -468,7 +602,7 @@ onUnmounted(() => {
<el-empty v-if="submissions.length === 0 && !submissionsLoading" description="该班还没有学生账号" />
<el-checkbox-group v-model="notSubmittedIds">
<el-checkbox v-for="student in submissions" :key="student.userId" :value="student.userId">
{{ student.name }}({{ student.username }})
{{ student.name }}
</el-checkbox>
</el-checkbox-group>
</div>
@@ -543,6 +677,27 @@ onUnmounted(() => {
margin: 0;
}
/* 作业页整体不滚动:看板靠自适应缩放铺满,避免"要上下翻页才能看全" */
.homework-page {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
box-sizing: border-box;
}
.homework-body {
flex: 1 1 auto;
min-height: 0;
}
.homework-body :deep(.el-card__body) {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
}
/* ---------------------------------------------------------------- 看板 */
.board-config {
@@ -560,15 +715,32 @@ onUnmounted(() => {
font-weight: 600;
}
/* 承载区:高度固定,里面靠 scale 适配,绝不出现滚动条 */
.board-host {
flex: 1 1 auto;
min-height: 240px;
overflow: hidden;
display: flex;
align-items: flex-start;
justify-content: center;
}
.board-host-lg {
height: calc(100vh - 150px);
}
.board {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(calc(var(--board-font, 15px) * 15), 1fr));
gap: calc(var(--board-font, 15px) * 0.8);
width: 100%;
transform: scale(var(--board-scale, 1));
transform-origin: top center;
}
.board-lg {
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(calc(var(--board-font, 15px) * 16), 1fr));
gap: calc(var(--board-font, 15px) * 0.9);
}
.board-card {
@@ -577,8 +749,7 @@ onUnmounted(() => {
background: #1c1c1e;
color: #f5f5f7;
border-radius: 18px;
padding: calc(var(--board-font, 15px) * 0.9) calc(var(--board-font, 15px) * 1.1);
min-height: 120px;
padding: calc(var(--board-font, 15px) * 0.8) calc(var(--board-font, 15px) * 0.95);
cursor: zoom-in;
transition:
transform 0.16s ease,
@@ -596,7 +767,7 @@ onUnmounted(() => {
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: calc(var(--board-font, 15px) * 0.5);
margin-bottom: calc(var(--board-font, 15px) * 0.45);
}
.board-course {
@@ -625,13 +796,13 @@ onUnmounted(() => {
list-style: none;
display: flex;
flex-direction: column;
gap: calc(var(--board-font, 15px) * 0.35);
gap: calc(var(--board-font, 15px) * 0.4);
flex: 1;
}
.board-item {
display: flex;
align-items: baseline;
align-items: flex-start;
gap: 8px;
font-size: var(--board-font, 15px);
line-height: 1.5;
@@ -649,12 +820,29 @@ onUnmounted(() => {
flex: 0 0 auto;
}
.board-title {
.board-text {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.board-title {
font-weight: 600;
word-break: break-word;
}
.board-item.done .board-title {
/* 作业要求:完整展示(不截断),字号略小以保持层级 */
.board-content {
font-size: calc(var(--board-font, 15px) * 0.82);
color: #c7c7cc;
white-space: pre-wrap;
word-break: break-word;
}
.board-item.done .board-title,
.board-item.done .board-content {
text-decoration: line-through;
color: #8e8e93;
}
@@ -675,7 +863,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: calc(var(--board-font, 15px) * 0.5);
margin-top: calc(var(--board-font, 15px) * 0.45);
font-size: calc(var(--board-font, 15px) * 0.7);
}
@@ -691,6 +879,31 @@ onUnmounted(() => {
padding: 6px 8px;
}
/* 全屏弹窗标题栏:标题 + 当天时间(时间绝对居中对齐) */
.board-dialog-head {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
padding-right: 48px;
}
.board-dialog-clock {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: 500;
color: #6b7280;
white-space: nowrap;
}
.board-fullscreen :deep(.el-dialog__body) {
padding-top: 6px;
height: calc(100vh - 84px);
overflow: hidden;
}
.submission-toolbar {
display: flex;
align-items: center;