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:
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user