diff --git a/web/native/android/app/src/main/AndroidManifest.xml b/web/native/android/app/src/main/AndroidManifest.xml index 5156edb..8dd2642 100644 --- a/web/native/android/app/src/main/AndroidManifest.xml +++ b/web/native/android/app/src/main/AndroidManifest.xml @@ -37,7 +37,8 @@ @@ -49,7 +50,8 @@ @@ -59,6 +61,32 @@ android:resource="@xml/exam_widget_info" /> + + + + + + + + + + + + + + 0 + ? unreadCount + " 条未读通知,打开 App 查看" + : "当前没有未读通知"; + render( + context, + manager, + appWidgetIds, + payload, + payload.optJSONArray("notifications"), + "教务通知", + emptyText, + "notifications", + 1401); + } + private static void render( Context context, AppWidgetManager manager, diff --git a/web/native/android/app/src/main/res/layout/widget_academic.xml b/web/native/android/app/src/main/res/layout/widget_academic.xml index 915f3d4..c2b0493 100644 --- a/web/native/android/app/src/main/res/layout/widget_academic.xml +++ b/web/native/android/app/src/main/res/layout/widget_academic.xml @@ -3,7 +3,7 @@ android:id="@+id/widget_root" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/widget_background" + android:background="#F8FBFF" android:clickable="true" android:focusable="true" android:orientation="vertical" @@ -15,12 +15,6 @@ android:gravity="center_vertical" android:orientation="horizontal"> - - @@ -45,8 +39,8 @@ + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:orientation="horizontal"> - + android:layout_width="76dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#2F8F89" + android:textSize="11sp" + android:textStyle="bold" /> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#152848" + android:textSize="13sp" + android:textStyle="bold" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#74839E" + android:textSize="10sp" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:orientation="horizontal"> - + android:layout_width="76dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#2F8F89" + android:textSize="11sp" + android:textStyle="bold" /> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#152848" + android:textSize="13sp" + android:textStyle="bold" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#74839E" + android:textSize="10sp" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:orientation="horizontal"> - + android:layout_width="76dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#2F8F89" + android:textSize="11sp" + android:textStyle="bold" /> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#152848" + android:textSize="13sp" + android:textStyle="bold" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="#74839E" + android:textSize="10sp" /> diff --git a/web/native/android/app/src/main/res/values/strings.xml b/web/native/android/app/src/main/res/values/strings.xml index 4d3c5d9..2a84d9b 100644 --- a/web/native/android/app/src/main/res/values/strings.xml +++ b/web/native/android/app/src/main/res/values/strings.xml @@ -14,4 +14,6 @@ 查看教务通知和待办 明序今日课表 明序近期考试 + 明序课堂签到 + 明序教务通知 diff --git a/web/native/android/app/src/main/res/xml/attendance_widget_info.xml b/web/native/android/app/src/main/res/xml/attendance_widget_info.xml new file mode 100644 index 0000000..6ad4607 --- /dev/null +++ b/web/native/android/app/src/main/res/xml/attendance_widget_info.xml @@ -0,0 +1,15 @@ + + diff --git a/web/native/android/app/src/main/res/xml/exam_widget_info.xml b/web/native/android/app/src/main/res/xml/exam_widget_info.xml index cefc9a0..c7957e7 100644 --- a/web/native/android/app/src/main/res/xml/exam_widget_info.xml +++ b/web/native/android/app/src/main/res/xml/exam_widget_info.xml @@ -2,9 +2,14 @@ diff --git a/web/native/android/app/src/main/res/xml/notifications_widget_info.xml b/web/native/android/app/src/main/res/xml/notifications_widget_info.xml new file mode 100644 index 0000000..32c922a --- /dev/null +++ b/web/native/android/app/src/main/res/xml/notifications_widget_info.xml @@ -0,0 +1,15 @@ + + diff --git a/web/native/android/app/src/main/res/xml/schedule_widget_info.xml b/web/native/android/app/src/main/res/xml/schedule_widget_info.xml index bed285e..8c581e4 100644 --- a/web/native/android/app/src/main/res/xml/schedule_widget_info.xml +++ b/web/native/android/app/src/main/res/xml/schedule_widget_info.xml @@ -2,9 +2,14 @@ diff --git a/web/src/services/nativeHome.ts b/web/src/services/nativeHome.ts index 0c6ceed..79a36f9 100644 --- a/web/src/services/nativeHome.ts +++ b/web/src/services/nativeHome.ts @@ -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 }) diff --git a/web/src/style.css b/web/src/style.css index 9e65cd0..42162bf 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -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; } diff --git a/web/src/views/ExperimentsView.vue b/web/src/views/ExperimentsView.vue index 32c684b..3b5e93f 100644 --- a/web/src/views/ExperimentsView.vue +++ b/web/src/views/ExperimentsView.vue @@ -917,7 +917,7 @@ onMounted(async () => { - + +
+
+
+
+ {{ row.code }} +

{{ row.name }}

+
+ + {{ modeMeta[row.arrangementMode].label }} + +
+

{{ row.description || modeMeta[row.arrangementMode].note }}

+
+ + +
+
+
@@ -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; } } diff --git a/web/src/views/FreeClassroomsView.vue b/web/src/views/FreeClassroomsView.vue index 8cec236..e8c35bb 100644 --- a/web/src/views/FreeClassroomsView.vue +++ b/web/src/views/FreeClassroomsView.vue @@ -535,5 +535,23 @@ h2 { .result-panel { padding: 16px; } + + .building-groups { + gap: 14px; + margin-top: 16px; + } + + .building-groups article > header { + padding: 12px; + } + + .room-grid { + grid-template-columns: 1fr; + } + + .room-item { + min-height: 76px; + padding: 13px 14px; + } }