feat(web,desktop,server): 灵动岛收起后可再次打开 · 下线作业截止时间 · Web 端圆角设计
灵动岛"收起后部分情况无法再次打开"(用户反馈): - 收起不再等于消失:只要有未处理通知(当前展示的或队列里的),超时后保持胶囊常驻, 只有"知道了 / 标为已读"、队列清空或进入上课时段才彻底隐藏。 - 收起态点击兜底:active 为空但队列还有未看通知时,点击胶囊会把它取出并展开; 上课时段仍严格不显示(该兜底放在上课守卫之后,不影响"上课不打扰")。 - 命中测试自愈:渲染进程记住最近指针位置,状态/外观变化与形变每一帧都按新几何重算命中; 岛可见时主进程也会重新下发可交互,避免"窗口还处于穿透 → 点了没反应"。 - 新增 3 条回归断言:收起后胶囊常驻、收起态点击可再次展开(命中恢复)、空闲细缝也可点开。 作业"截止时间"功能下线(用户要求): - 接口/类型:HomeworkDto 与创建请求去掉 dueAt,homeworks schema/service 不再读写; 仪表盘去掉"即将截止的作业"卡片与待截止统计口径(改为"未完成/待完成")。 - Web:作业发布表单移除截止时间选择、列表与详情移除截止时间列; 学生端作业列表/详情移除截止时间与"已逾期"状态(只保留完成状态)。 - 灵动岛:作业卡不再显示"截止时间:…"副标题,改为只展示作业要求。 - DB 中的 Homework.dueAt 列**保留**(已在 schema 注明为遗留字段),避免对已安装库做破坏性迁移; 种子数据不再写入该列。 Web 管理端圆角设计(用户要求): - 新增设计令牌 --ch-radius-xl/lg/md/sm/pill 与两级柔和投影,并让 Element Plus 的 --el-border-radius-* 对齐同一套令牌;卡片、弹窗、抽屉、按钮、输入、标签、分页、 侧栏菜单项、表格表头统一改造,卡片去硬边框改投影 + 悬停微浮起,顶栏/侧栏做圆角收口。 - 未改组件源码,全部为 CSS 变量 + 类选择器覆盖;手机端媒体查询保持生效(截图已更新)。 验证:verify:desktop 62/62(含 3 条收起态回归)与离线复跑 56/56、打包便携版 62/62(packaged=true)、 verify:e2e 136/136(新增"作业接口不再返回 dueAt")、verify:web 20/20、typecheck/lint/format 全绿; 服务端与 Web 已重新打包并部署到本机安装目录。
This commit is contained in:
1 parent
0690546307
commit
b69f708aa2
23 files changed
+534
-222
No files matched your search
@@ -299,16 +299,19 @@ async function submitPassword(): Promise<void> {
|
||||
|
||||
.layout-menu :deep(.el-menu-item.is-active) {
|
||||
background: #409eff;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--ch-radius-sm);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.layout-menu :deep(.el-menu-item) {
|
||||
margin: 2px 8px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
/* 顶栏跟随圆角设计:底部圆角 + 轻投影,与内容区一体化 */
|
||||
border-radius: 0 0 var(--ch-radius-lg) var(--ch-radius-lg);
|
||||
box-shadow: var(--ch-shadow-sm);
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--ch-border);
|
||||
display: flex;
|
||||
@@ -326,7 +329,7 @@ async function submitPassword(): Promise<void> {
|
||||
height: 32px;
|
||||
margin-right: 2px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--ch-radius-sm);
|
||||
background: #f2f3f5;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
@@ -392,6 +395,9 @@ async function submitPassword(): Promise<void> {
|
||||
<style>
|
||||
/* 侧边栏配色放在全局:桌面 asider 与移动端抽屉复用同一套样式(抽屉内容在 teleport 中,scoped 样式不生效) */
|
||||
.ch-sidebar {
|
||||
/* 圆角设计:深色侧栏右侧做圆角,与内容区形成"卡片浮起"观感 */
|
||||
border-radius: 0 var(--ch-radius-xl) var(--ch-radius-xl) 0;
|
||||
overflow: hidden;
|
||||
background: #1f2d3d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,239 @@
|
||||
:root {
|
||||
--ch-bg: #f5f7fa;
|
||||
--ch-card-radius: 10px;
|
||||
--ch-border: #e4e7ed;
|
||||
--ch-bg: #f2f4f8;
|
||||
/* 圆角设计令牌:卡片 > 面板 > 控件 > 小元素,统一走这四个档位 */
|
||||
--ch-radius-xl: 22px;
|
||||
--ch-radius-lg: 18px;
|
||||
--ch-radius-md: 14px;
|
||||
--ch-radius-sm: 10px;
|
||||
--ch-radius-pill: 999px;
|
||||
--ch-card-radius: var(--ch-radius-lg);
|
||||
--ch-border: #e6e8ef;
|
||||
--ch-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04);
|
||||
--ch-shadow-md: 0 6px 24px rgba(15, 23, 42, 0.08);
|
||||
/* 1Panel 风格:固定顶栏 + 深色侧栏 */
|
||||
--ch-header-height: 56px;
|
||||
--ch-aside-width: 210px;
|
||||
|
||||
/* Element Plus 圆角令牌对齐(组件库内部都引用这些变量,改这里即可全局生效) */
|
||||
--el-border-radius-base: var(--ch-radius-sm);
|
||||
--el-border-radius-small: 8px;
|
||||
--el-border-radius-round: var(--ch-radius-pill);
|
||||
--el-box-shadow-light: var(--ch-shadow-sm);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- 圆角设计:
|
||||
|
||||
统一"大圆角 + 轻阴影 + 无硬边框"的观感:
|
||||
- 卡片 / 面板 / 弹窗 / 抽屉:18~22px 圆角 + 柔和投影(去掉 1px 灰边)
|
||||
- 按钮 / 输入框 / 下拉 / 标签:10px 圆角,胶囊类(标签页、分页、单选组)走 999px
|
||||
- 表格:外层由卡片包裹,表头首末单元格跟随卡片圆角
|
||||
|
||||
全部通过 Element Plus 的 CSS 变量 + 少量类选择器覆盖,不改组件源码。 */
|
||||
|
||||
.el-card {
|
||||
border: none;
|
||||
border-radius: var(--ch-radius-lg);
|
||||
box-shadow: var(--ch-shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
border-bottom: 1px solid var(--ch-border);
|
||||
padding: 14px 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-button.is-round,
|
||||
.el-button--circle {
|
||||
border-radius: var(--ch-radius-pill);
|
||||
}
|
||||
|
||||
.el-input__wrapper,
|
||||
.el-textarea__inner,
|
||||
.el-select__wrapper,
|
||||
.el-input-number,
|
||||
.el-date-editor.el-input__wrapper {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper {
|
||||
box-shadow: 0 0 0 1px var(--ch-border) inset;
|
||||
}
|
||||
|
||||
.el-input__wrapper.is-focus,
|
||||
.el-select__wrapper.is-focused {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
box-shadow: 0 0 0 1px var(--ch-border) inset;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: var(--ch-radius-xl);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--ch-shadow-md);
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
margin-right: 0;
|
||||
padding: 18px 22px 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 10px 22px 6px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 8px 22px 18px;
|
||||
}
|
||||
|
||||
.el-drawer {
|
||||
border-radius: var(--ch-radius-xl) 0 0 var(--ch-radius-xl);
|
||||
}
|
||||
|
||||
.el-drawer.rtl {
|
||||
border-radius: var(--ch-radius-xl) 0 0 var(--ch-radius-xl);
|
||||
}
|
||||
|
||||
.el-drawer.ltr {
|
||||
border-radius: 0 var(--ch-radius-xl) var(--ch-radius-xl) 0;
|
||||
}
|
||||
|
||||
.el-drawer.ttb,
|
||||
.el-drawer.btt {
|
||||
border-radius: var(--ch-radius-xl);
|
||||
}
|
||||
|
||||
.el-message,
|
||||
.el-message-box,
|
||||
.el-notification,
|
||||
.el-popper.is-light,
|
||||
.el-dropdown-menu,
|
||||
.el-select-dropdown,
|
||||
.el-picker-panel {
|
||||
border-radius: var(--ch-radius-md);
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
border-radius: var(--ch-radius-lg);
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.el-popper.is-light {
|
||||
border: 1px solid var(--ch-border);
|
||||
box-shadow: var(--ch-shadow-sm);
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item,
|
||||
.el-select-dropdown__item {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
margin: 2px 6px;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
border-radius: var(--ch-radius-pill);
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.el-alert {
|
||||
border-radius: var(--ch-radius-md);
|
||||
}
|
||||
|
||||
.el-descriptions__body {
|
||||
border-radius: var(--ch-radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-descriptions__label.el-descriptions__cell.is-bordered-label {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.el-radio-group,
|
||||
.el-radio-button:first-child .el-radio-button__inner {
|
||||
border-radius: var(--ch-radius-sm) 0 0 var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
.el-radio-button:last-child .el-radio-button__inner {
|
||||
border-radius: 0 var(--ch-radius-sm) var(--ch-radius-sm) 0;
|
||||
}
|
||||
|
||||
.el-radio-button__inner {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.el-checkbox__inner,
|
||||
.el-switch__core,
|
||||
.el-progress-bar__outer,
|
||||
.el-progress-bar__inner {
|
||||
border-radius: var(--ch-radius-pill);
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
border-radius: var(--ch-radius-sm) var(--ch-radius-sm) 0 0;
|
||||
}
|
||||
|
||||
.el-pagination button,
|
||||
.el-pager li,
|
||||
.el-pagination .el-input__wrapper {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
.el-pagination.is-background .el-pager li {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: var(--ch-radius-md);
|
||||
--el-table-border-color: var(--ch-border);
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.el-table__inner-wrapper::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table--border .el-table__inner-wrapper,
|
||||
.el-table--border {
|
||||
border-radius: var(--ch-radius-md);
|
||||
}
|
||||
|
||||
.el-empty__image {
|
||||
border-radius: var(--ch-radius-md);
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-upload-dragger,
|
||||
.el-upload-list__item {
|
||||
border-radius: var(--ch-radius-md);
|
||||
}
|
||||
|
||||
.el-step__icon,
|
||||
.el-collapse,
|
||||
.el-collapse-item__header {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
border-radius: var(--ch-radius-sm);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -28,7 +257,7 @@ body {
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 16px 20px 24px;
|
||||
padding: 18px 22px 28px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@@ -67,9 +296,18 @@ body {
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: var(--ch-card-radius);
|
||||
padding: 16px;
|
||||
border: 1px solid var(--ch-border);
|
||||
border-radius: var(--ch-radius-lg);
|
||||
padding: 18px;
|
||||
border: none;
|
||||
box-shadow: var(--ch-shadow-sm);
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--ch-shadow-md);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
PRIORITY_LABELS,
|
||||
PRIORITY_TAG_TYPES,
|
||||
formatDate,
|
||||
formatDeadline,
|
||||
relativeTime,
|
||||
type DashboardSummary,
|
||||
} from '@classhelper/shared';
|
||||
@@ -32,7 +31,7 @@ const statCards = computed(() => {
|
||||
{
|
||||
label: '作业总数',
|
||||
value: data?.homeworkCount ?? 0,
|
||||
hint: `待截止/待完成 ${data?.pendingHomeworkCount ?? 0}`,
|
||||
hint: `未完成/待完成 ${data?.pendingHomeworkCount ?? 0}`,
|
||||
},
|
||||
{ label: '通知总数', value: data?.notificationCount ?? 0, hint: `未读 ${unreadCount.value}` },
|
||||
{ label: '成绩记录', value: data?.gradeCount ?? 0, hint: '已发布成绩' },
|
||||
@@ -102,7 +101,7 @@ function openNotification(item: NotificationDto): void {
|
||||
</div>
|
||||
|
||||
<el-row :gutter="12" class="mt-16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-col :xs="24" :md="24">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
@@ -127,25 +126,6 @@ function openNotification(item: NotificationDto): void {
|
||||
</ul>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>即将截止的作业</span>
|
||||
<el-button link type="primary" @click="go('homeworks')">去发布</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-if="(summary?.upcomingDeadlines ?? []).length === 0" description="暂无待截止作业" />
|
||||
<ul v-else class="list">
|
||||
<li v-for="item in summary?.upcomingDeadlines" :key="item.id" class="list-item">
|
||||
<el-tag size="small" effect="plain">{{ item.course?.name ?? '未关联课程' }}</el-tag>
|
||||
<span class="list-title">{{ item.title }}</span>
|
||||
<span class="text-muted">{{ formatDeadline(item.dueAt) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="mt-16 table-card">
|
||||
@@ -165,9 +145,6 @@ function openNotification(item: NotificationDto): void {
|
||||
<span>{{ row.completedCount ?? 0 }} 人已完成</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="截止时间" width="180">
|
||||
<template #default="{ row }">{{ formatDate(row.dueAt, true) || '不限' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" width="180">
|
||||
<template #default="{ row }">{{ formatDate(row.createdAt, true) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||
import {
|
||||
SOCKET_EVENTS,
|
||||
formatDate,
|
||||
formatDeadline,
|
||||
isOverdue,
|
||||
truncate,
|
||||
type ClassDto,
|
||||
type CourseDto,
|
||||
@@ -76,7 +74,6 @@ const form = reactive({
|
||||
title: '',
|
||||
content: '',
|
||||
attachmentUrl: '',
|
||||
dueAt: '',
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
@@ -92,7 +89,6 @@ function openCreate(): void {
|
||||
form.title = '';
|
||||
form.content = '';
|
||||
form.attachmentUrl = '';
|
||||
form.dueAt = '';
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -103,7 +99,6 @@ function openEdit(row: HomeworkDto): void {
|
||||
form.title = row.title;
|
||||
form.content = row.content;
|
||||
form.attachmentUrl = row.attachmentUrl ?? '';
|
||||
form.dueAt = row.dueAt ? row.dueAt.slice(0, 16) : '';
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -117,7 +112,6 @@ async function submitForm(): Promise<void> {
|
||||
title: form.title.trim(),
|
||||
content: form.content,
|
||||
attachmentUrl: form.attachmentUrl.trim() || null,
|
||||
dueAt: form.dueAt ? new Date(form.dueAt).toISOString() : null,
|
||||
};
|
||||
|
||||
if (editingId.value) {
|
||||
@@ -207,16 +201,6 @@ onUnmounted(() => {
|
||||
<el-table-column label="课程" width="110">
|
||||
<template #default="{ row }">{{ row.course?.name ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="截止时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-danger': isOverdue(row.dueAt) }">
|
||||
{{ formatDate(row.dueAt, true) || '不限' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余" width="130">
|
||||
<template #default="{ row }">{{ formatDeadline(row.dueAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已完成" width="100">
|
||||
<template #default="{ row }">{{ row.completedCount ?? 0 }} 人</template>
|
||||
</el-table-column>
|
||||
@@ -254,15 +238,6 @@ onUnmounted(() => {
|
||||
<el-form-item label="附件链接">
|
||||
<el-input v-model="form.attachmentUrl" placeholder="https://..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="截止时间">
|
||||
<el-date-picker
|
||||
v-model="form.dueAt"
|
||||
type="datetime"
|
||||
placeholder="选择截止时间"
|
||||
value-format="YYYY-MM-DDTHH:mm"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formVisible = false">取消</el-button>
|
||||
@@ -275,9 +250,6 @@ onUnmounted(() => {
|
||||
<el-descriptions :column="detailColumns" border size="small">
|
||||
<el-descriptions-item label="课程">{{ current.course?.name ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发布人">{{ current.creator?.name ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="截止时间">
|
||||
{{ formatDate(current.dueAt, true) || '不限' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发布时间">
|
||||
{{ formatDate(current.createdAt, true) }}
|
||||
</el-descriptions-item>
|
||||
|
||||
Reference in new issue
Block a user