app端优化

This commit is contained in:
biss committed 2026-08-12 22:16:43 +08:00
1 parent 7994ed1ed3
commit 1e45ede5be
14 files changed
+357 -37

No files matched your search

+37 -1
View File
@@ -13,6 +13,9 @@ interface WidgetPayload {
scheduleDate: string
schedule: WidgetItem[]
exams: WidgetItem[]
attendance: WidgetItem[]
notifications: WidgetItem[]
unreadNotificationCount: number
updatedAt: string
}
@@ -128,6 +131,26 @@ function examItems(exams: any[], now: Date): WidgetItem[] {
}))
}
function attendanceItems(activities: any[]): WidgetItem[] {
return activities.slice(0, 3).map((activity: any) => ({
title: String(activity.courseName ?? '待签到课程'),
subtitle: [activity.courseCode, activity.sheetName].filter(Boolean).join(' · '),
time: activity.checkInAt ? '已签到' : activity.checkInEndsAt
? `截止 ${new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(activity.checkInEndsAt))}`
: '签到进行中',
}))
}
function notificationItems(notifications: any[]): WidgetItem[] {
return notifications.filter((item: any) => !item.isRead).slice(0, 3).map((item: any) => ({
title: String(item.title ?? '教务通知'),
subtitle: String(item.senderName ?? '教务系统'),
time: item.createdAt ? new Intl.DateTimeFormat('zh-CN', {
month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false,
}).format(new Date(item.createdAt)) : '未读',
}))
}
export async function clearNativeWidgets() {
if (!native) return
try {
@@ -154,9 +177,13 @@ export async function syncNativeWidgets(force = false) {
return
}
const [timetableResult, examResult] = await Promise.allSettled([
const [timetableResult, examResult, notificationResult, attendanceResult] = await Promise.allSettled([
http.get('/timetables/mine'),
http.get('/exams/my-schedule'),
http.get('/notifications', { params: { page: 1, pageSize: 3, unreadOnly: true } }),
roles.includes('Student')
? http.get('/attendance/open-check-ins')
: Promise.resolve({ data: [] }),
])
const now = new Date()
const payload: WidgetPayload = {
@@ -168,6 +195,15 @@ export async function syncNativeWidgets(force = false) {
exams: examResult.status === 'fulfilled'
? examItems(examResult.value.data, now)
: [],
attendance: attendanceResult.status === 'fulfilled'
? attendanceItems(attendanceResult.value.data)
: [],
notifications: notificationResult.status === 'fulfilled'
? notificationItems(notificationResult.value.data.items ?? [])
: [],
unreadNotificationCount: notificationResult.status === 'fulfilled'
? Number(notificationResult.value.data.unreadCount ?? 0)
: 0,
updatedAt: now.toISOString(),
}
await NativeHome.updateWidgets({ payload })
+3 -2
View File
@@ -16,8 +16,9 @@
--el-color-primary: #233876;
--el-color-success: #087f73;
--el-border-radius-base: 7px;
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
/* Capacitor 8 injects these Android values; env() alone stays at zero in WebView. */
--safe-area-top: max(env(safe-area-inset-top, 0px), var(--safe-area-inset-top, 0px));
--safe-area-bottom: max(env(safe-area-inset-bottom, 0px), var(--safe-area-inset-bottom, 0px));
}
* { box-sizing: border-box; }
+60 -1
View File
@@ -917,7 +917,7 @@ onMounted(async () => {
</template>
</el-table-column>
</el-table>
<el-table v-else :data="group.projects" class="project-table student-project-table" size="small">
<el-table v-else :data="group.projects" class="project-table student-project-table desktop-project-table" size="small">
<el-table-column type="expand" width="44">
<template #default="{ row }">
<div class="student-project-detail">
@@ -988,6 +988,43 @@ onMounted(async () => {
</template>
</el-table-column>
</el-table>
<div v-if="isStudent" class="mobile-project-cards">
<article v-for="row in group.projects" :key="row.id" class="mobile-project-card">
<header>
<div>
<span>{{ row.code }}</span>
<h4>{{ row.name }}</h4>
</div>
<el-tag :type="row.arrangementMode === 'Centralized' ? 'primary' : 'success'" size="small" effect="plain">
{{ modeMeta[row.arrangementMode].label }}
</el-tag>
</header>
<p>{{ row.description || modeMeta[row.arrangementMode].note }}</p>
<div class="mobile-project-arrangement">
<template v-if="row.arrangementMode === 'Centralized' && row.scheduleEntry">
<b>统一安排</b>
<span>第 {{ row.scheduleEntry.projectWeek ?? `${row.scheduleEntry.startWeek}–${row.scheduleEntry.endWeek}` }} 周 · 星期 {{ row.scheduleEntry.dayOfWeek }} · 第 {{ row.scheduleEntry.startPeriod }}–{{ row.scheduleEntry.startPeriod + row.scheduleEntry.periodCount - 1 }} 节</span>
<small>{{ row.scheduleEntry.campusName }} · {{ row.scheduleEntry.buildingName }} {{ row.scheduleEntry.classroomName }}</small>
</template>
<template v-else-if="row.arrangementMode === 'SelfScheduled'">
<b>{{ row.myBooking ? '已预约场次' : '选择实验场次' }}</b>
<div v-if="row.sessions.length" class="mobile-session-list">
<article v-for="session in row.sessions" :key="session.id" :class="{ 'is-selected': row.myBooking?.experimentSessionId === session.id }">
<div>
<strong>{{ formatSessionTime(session) }}</strong>
<small>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</small>
</div>
<el-tag :type="sessionTagType(session)" size="small" effect="plain">余 {{ session.remainingCount }} / {{ session.capacity }}</el-tag>
<el-button v-if="row.myBooking?.experimentSessionId === session.id" size="small" type="success" disabled>已预约</el-button>
<el-button v-else size="small" type="primary" plain :disabled="row.status !== 'Published' || !!row.myBooking || session.remainingCount <= 0" @click="book(row, session)">预约</el-button>
</article>
</div>
<span v-else class="table-muted">暂无有效实验场次</span>
<el-button v-if="row.myBooking && row.status === 'Published'" class="mobile-cancel-booking" size="small" text type="danger" @click="cancelBooking(row)">取消预约</el-button>
</template>
</div>
</article>
</div>
</div>
</section>
@@ -1593,6 +1630,7 @@ onMounted(async () => {
.batch-project-cell b, .batch-project-cell small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.batch-project-cell b { color: var(--lab-ink); font-size: 12px; }
.batch-project-cell small { color: var(--muted); font-size: 10px; }
.mobile-project-cards { display: none; }
@media (prefers-reduced-motion: reduce) {
.session-ticket { transition: none; }
}
@@ -1611,5 +1649,26 @@ onMounted(async () => {
.student-project-detail { grid-template-columns: 1fr; }
.mode-choice { grid-template-columns: 1fr; }
.intro-actions { flex-wrap: wrap; justify-content: flex-end; }
.desktop-project-table { display: none; }
.mobile-project-cards { display: grid; gap: 1px; background: #dce5ea; }
.mobile-project-card { padding: 15px 14px; background: #fff; }
.mobile-project-card > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.mobile-project-card > header > div { min-width: 0; }
.mobile-project-card > header span { color: #68808d; font: 700 10px/1.2 Consolas, monospace; letter-spacing: .06em; }
.mobile-project-card h4 { margin: 4px 0 0; color: var(--lab-ink); font-size: 15px; line-height: 1.45; }
.mobile-project-card > p { margin: 9px 0 0; color: #667f8d; font-size: 12px; line-height: 1.6; }
.mobile-project-arrangement { display: grid; gap: 5px; margin-top: 13px; padding: 11px 12px; border-left: 3px solid #a6bbc6; background: #f6fafb; }
.mobile-project-arrangement > b { color: #365364; font-size: 12px; }
.mobile-project-arrangement > span { color: #405d6c; font-size: 12px; line-height: 1.5; }
.mobile-project-arrangement > small { color: #718792; font-size: 11px; line-height: 1.45; }
.mobile-session-list { display: grid; gap: 7px; margin-top: 3px; }
.mobile-session-list > article { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 9px; border: 1px solid #dce6e9; background: #fff; }
.mobile-session-list > article.is-selected { border-color: #69a99d; box-shadow: inset 3px 0 0 var(--lab-teal); }
.mobile-session-list > article > div { display: grid; gap: 3px; min-width: 0; }
.mobile-session-list strong { color: #304d5d; font-size: 12px; }
.mobile-session-list small { overflow: hidden; color: #718792; font-size: 10px; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; }
.mobile-session-list .el-button { grid-column: 2; grid-row: 1 / span 2; align-self: center; min-height: 34px; }
.mobile-session-list .el-tag { grid-column: 1; width: fit-content; }
.mobile-cancel-booking { justify-self: start; margin-top: 2px; }
}
</style>
Loaded 3 of 14 files, more files were not shown because too many files have changed in this diff. Show more