feat(app,styles): update dashboard pages and global styles

- Update parent: children/[studentId]/page, elective/page, leave/page

- Update student: error-book/student-error-book-list-client

- Update teacher: classes/schedule/schedule-filters, classes/students/students-filters

- Update src/app/globals.css
This commit is contained in:
SpecialX
2026-07-07 16:22:27 +08:00
parent ebaf03107d
commit fc150e1e14
7 changed files with 34 additions and 27 deletions

View File

@@ -3,10 +3,10 @@ import { notFound } from "next/navigation"
import { requireAuth } from "@/shared/lib/auth-guard" import { requireAuth } from "@/shared/lib/auth-guard"
import { getSearchParam } from "@/shared/lib/utils" import { getSearchParam } from "@/shared/lib/utils"
import { import {
verifyParentChildRelation, verifyParentChildRelationAction,
getChildDashboardData, getChildDashboardDataAction,
getChildNameList, getChildNameListAction,
} from "@/modules/parent/data-access" } from "@/modules/parent/actions"
import { ChildDetailHeader } from "@/modules/parent/components/child-detail-header" import { ChildDetailHeader } from "@/modules/parent/components/child-detail-header"
import { import {
ChildDetailPanel, ChildDetailPanel,
@@ -31,7 +31,8 @@ export default async function ChildDetailPage({
const t = await getTranslations("common") const t = await getTranslations("common")
// 校验当前家长与该子女存在关系(同时按 parentId + studentId 过滤,防止跨家庭信息泄露) // 校验当前家长与该子女存在关系(同时按 parentId + studentId 过滤,防止跨家庭信息泄露)
const relation = await verifyParentChildRelation(studentId, ctx.userId) // 审计 v1 P0 修复G4-002改调 Server Action统一权限校验入口
const relation = await verifyParentChildRelationAction(studentId)
// dataScope 二次校验admin/其他角色可能通过 requireAuth但需确认 dataScope 包含该子女 // dataScope 二次校验admin/其他角色可能通过 requireAuth但需确认 dataScope 包含该子女
const isInScope = const isInScope =
@@ -52,8 +53,8 @@ export default async function ChildDetailPage({
} }
const [child, siblings] = await Promise.all([ const [child, siblings] = await Promise.all([
getChildDashboardData(studentId, relation), getChildDashboardDataAction(studentId),
getChildNameList(ctx.userId), getChildNameListAction(),
]) ])
if (!child) { if (!child) {
notFound() notFound()

View File

@@ -4,9 +4,9 @@ import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard" import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions" import { Permissions } from "@/shared/types/permissions"
import { import {
getChildren, getChildrenAction,
getChildBasicInfo, getChildBasicInfoAction,
} from "@/modules/parent/data-access" } from "@/modules/parent/actions"
import { getStudentSelections } from "@/modules/elective/data-access-selections" import { getStudentSelections } from "@/modules/elective/data-access-selections"
import { import {
ParentChildrenDataPage, ParentChildrenDataPage,
@@ -25,7 +25,6 @@ interface ChildSelectionData {
export default async function ParentElectivePage() { export default async function ParentElectivePage() {
const t = await getTranslations("elective") const t = await getTranslations("elective")
const ctx = await requirePermission(Permissions.ELECTIVE_READ) const ctx = await requirePermission(Permissions.ELECTIVE_READ)
const parentId = ctx.userId
// dataScope 校验:家长必须有关联子女才能查看选课信息 // dataScope 校验:家长必须有关联子女才能查看选课信息
if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) { if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) {
@@ -41,11 +40,12 @@ export default async function ParentElectivePage() {
} }
// 并行拉取每个子女的选课记录,使用 allSettled 容错 // 并行拉取每个子女的选课记录,使用 allSettled 容错
const relations = await getChildren(parentId) // 审计 v1 P0 修复G4-002改调 Server Action统一权限校验入口
const relations = await getChildrenAction()
const results = await Promise.allSettled( const results = await Promise.allSettled(
relations.map(async (r): Promise<ChildSelectionData> => { relations.map(async (r): Promise<ChildSelectionData> => {
// 双重校验parentId + studentId 必须存在关联关系,防止跨家庭信息泄露 // 双重校验parentId + studentId 必须存在关联关系,防止跨家庭信息泄露
const basicInfo = await getChildBasicInfo(r.studentId, r.relation) const basicInfo = await getChildBasicInfoAction(r.studentId, r.relation)
const selections = await getStudentSelections(r.studentId) const selections = await getStudentSelections(r.studentId)
return { return {
studentId: r.studentId, studentId: r.studentId,

View File

@@ -5,7 +5,7 @@ import { ArrowLeft, CalendarDays } from "lucide-react"
import { getAuthContext } from "@/shared/lib/auth-guard" import { getAuthContext } from "@/shared/lib/auth-guard"
import { Button } from "@/shared/components/ui/button" import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { getChildren, getChildBasicInfo } from "@/modules/parent/data-access" import { getChildrenAction, getChildBasicInfoAction } from "@/modules/parent/actions"
import { getLeaveRequests } from "@/modules/leave-requests/data-access" import { getLeaveRequests } from "@/modules/leave-requests/data-access"
import { import {
LeaveRequestForm, LeaveRequestForm,
@@ -46,8 +46,9 @@ export default async function ParentLeavePage() {
} }
// 并行:子女关系列表 + 已有请假申请列表 // 并行:子女关系列表 + 已有请假申请列表
// 审计 v1 P0 修复G4-002改调 Server Action统一权限校验入口
const [relations, leaveResult] = await Promise.all([ const [relations, leaveResult] = await Promise.all([
getChildren(ctx.userId), getChildrenAction(),
getLeaveRequests({ getLeaveRequests({
scope: ctx.dataScope, scope: ctx.dataScope,
currentUserId: ctx.userId, currentUserId: ctx.userId,
@@ -59,7 +60,7 @@ export default async function ParentLeavePage() {
// 构造子女选项(含 activeClass供表单下拉使用 // 构造子女选项(含 activeClass供表单下拉使用
const childOptions: ChildOption[] = [] const childOptions: ChildOption[] = []
for (const r of relations) { for (const r of relations) {
const basic = await getChildBasicInfo(r.studentId, r.relation) const basic = await getChildBasicInfoAction(r.studentId, r.relation)
if (basic && basic.classId && basic.className) { if (basic && basic.classId && basic.className) {
childOptions.push({ childOptions.push({
id: basic.id, id: basic.id,

View File

@@ -3,7 +3,7 @@
import { useTransition } from "react" import { useTransition } from "react"
import type { JSX } from "react" import type { JSX } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { toast } from "sonner" import { notify } from "@/shared/lib/notify"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { ErrorBookList } from "@/modules/error-book/components/error-book-list" import { ErrorBookList } from "@/modules/error-book/components/error-book-list"
@@ -102,10 +102,10 @@ export function StudentErrorBookListClient({
) )
const res = await createPracticeSessionAction(undefined, formData) const res = await createPracticeSessionAction(undefined, formData)
if (res.success && res.data) { if (res.success && res.data) {
toast.success(res.message ?? tPractice("starter.title")) notify.success(res.message ?? tPractice("starter.title"))
router.push(`/student/practice/${res.data.sessionId}`) router.push(`/student/practice/${res.data.sessionId}`)
} else { } else {
toast.error(res.message ?? t("messages.saveFailed")) notify.error(res.message ?? t("messages.saveFailed"))
} }
}) })
} }

View File

@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { useQueryState, parseAsString } from "nuqs" import { useQueryState, parseAsString } from "nuqs"
import { Plus } from "lucide-react" import { Plus } from "lucide-react"
import { toast } from "sonner" import { notify } from "@/shared/lib/notify"
import { Button } from "@/shared/components/ui/button" import { Button } from "@/shared/components/ui/button"
import { import {
@@ -57,14 +57,14 @@ export function ScheduleFilters({ classes }: { classes: TeacherClass[] }) {
formData.set("classId", createClassId) formData.set("classId", createClassId)
const res = await createClassScheduleItemAction(null, formData) const res = await createClassScheduleItemAction(null, formData)
if (res.success) { if (res.success) {
toast.success(res.message) notify.success(res.message)
setOpen(false) setOpen(false)
router.refresh() router.refresh()
} else { } else {
toast.error(res.message || t("list.failedCreate")) notify.error(res.message || t("list.failedCreate"))
} }
} catch { } catch {
toast.error(t("list.failedCreate")) notify.error(t("list.failedCreate"))
} finally { } finally {
setIsWorking(false) setIsWorking(false)
} }

View File

@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { useQueryState, parseAsString } from "nuqs" import { useQueryState, parseAsString } from "nuqs"
import { UserPlus, ChevronDown, Check } from "lucide-react" import { UserPlus, ChevronDown, Check } from "lucide-react"
import { toast } from "sonner" import { notify } from "@/shared/lib/notify"
import { Input } from "@/shared/components/ui/input" import { Input } from "@/shared/components/ui/input"
import { Button } from "@/shared/components/ui/button" import { Button } from "@/shared/components/ui/button"
@@ -65,14 +65,14 @@ export function StudentsFilters({ classes, defaultClassId }: { classes: TeacherC
try { try {
const res = await enrollStudentByEmailAction(enrollClassId, null, formData) const res = await enrollStudentByEmailAction(enrollClassId, null, formData)
if (res.success) { if (res.success) {
toast.success(res.message) notify.success(res.message)
setOpen(false) setOpen(false)
router.refresh() router.refresh()
} else { } else {
toast.error(res.message || t("list.failedCreate")) notify.error(res.message || t("list.failedCreate"))
} }
} catch { } catch {
toast.error(t("list.failedCreate")) notify.error(t("list.failedCreate"))
} finally { } finally {
setIsWorking(false) setIsWorking(false)
} }

View File

@@ -4,6 +4,11 @@
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
/* 排除非源码目录,防止文档中的 Tailwind 任意值语法字符串被误识别为类名 */
@source not "../../docs";
@source not "../../scripts";
@source not "../../tests";
/* Reduced Motion */ /* Reduced Motion */
@layer base { @layer base {
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {