fix(teacher): 统一详情页返回路径与中英文文案 (P1-3+P2-1)
P1-3: empty-state 默认按钮 variant 改为 outline 并新增 variant prop;button.tsx 导出 ButtonProps;统一 5 个详情页返回路径为 ghost+ArrowLeft+文字标签;course-plan-detail raw a 改为 Link。P2-1: formatLongDate 默认 locale 改为 zh-CN,weekday 改为 short;返回按钮文案中文化;course-plan-detail 全量中文化;grades/analytics 标题中文化。验证:tsc 0 错误,lint 0 错误,架构图 004/005 已同步。
This commit is contained in:
@@ -461,7 +461,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
|||||||
| 函数 | 文件 | 签名 | 用途 | 消费方 |
|
| 函数 | 文件 | 签名 | 用途 | 消费方 |
|
||||||
|------|------|------|------|--------|
|
|------|------|------|------|--------|
|
||||||
| `formatDateTime` | `lib/utils.ts` | `formatDateTime(date, locale?): string` | 国际化日期+时间格式化(含小时、分钟) | 4 个(P1-3: lesson-plan-card, version-history-drawer, proctoring-dashboard, exam-ai-generator) |
|
| `formatDateTime` | `lib/utils.ts` | `formatDateTime(date, locale?): string` | 国际化日期+时间格式化(含小时、分钟) | 4 个(P1-3: lesson-plan-card, version-history-drawer, proctoring-dashboard, exam-ai-generator) |
|
||||||
| `formatLongDate` | `lib/utils.ts` | `formatLongDate(date, locale?): string` | 国际化长日期格式化(含星期、完整月份名) | 1 个(P1-3: teacher-dashboard-header) |
|
| `formatLongDate` | `lib/utils.ts` | `formatLongDate(date, locale?): string` | 国际化长日期格式化(含星期、完整月份名),默认 zh-CN,weekday=short | 1 个(P1-3: teacher-dashboard-header) |
|
||||||
|
|
||||||
> 注:`SettingsView` 位于 `modules/settings/components/`(非 shared 层),因仅被 settings 模块消费,未下沉到 shared。此处列出以完整反映本次重构的组件抽取范围。
|
> 注:`SettingsView` 位于 `modules/settings/components/`(非 shared 层),因仅被 settings 模块消费,未下沉到 shared。此处列出以完整反映本次重构的组件抽取范围。
|
||||||
|
|
||||||
@@ -476,6 +476,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
|||||||
- ✅ P2-2 已修复:~~`ai.ts` 218 行混合 5 类职责~~ 已拆分为 `ai/` 目录(payload-parser.ts/api-key-crypto.ts/provider-config.ts/client.ts/errors.ts/index.ts),原 `ai.ts` 保留为向后兼容的重导出文件(9 行)
|
- ✅ P2-2 已修复:~~`ai.ts` 218 行混合 5 类职责~~ 已拆分为 `ai/` 目录(payload-parser.ts/api-key-crypto.ts/provider-config.ts/client.ts/errors.ts/index.ts),原 `ai.ts` 保留为向后兼容的重导出文件(9 行)
|
||||||
- ✅ P2-4 已修复:~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已迁移至 `modules/onboarding/`(actions/data-access/schema/types/components),引导流程改为独立路由 `/onboarding` + middleware 重定向 + Server Action
|
- ✅ P2-4 已修复:~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已迁移至 `modules/onboarding/`(actions/data-access/schema/types/components),引导流程改为独立路由 `/onboarding` + middleware 重定向 + Server Action
|
||||||
- ✅ P0-2/P0-3/P0-4/P0-5/P1-1/P1-2/P1-4/P1-5 已修复(v3 对标 PowerSchool/Veracross/Auth0):家长绑定三因子验证(邮箱+生日+手机号后4位)、教师多科目循环绑定、审计日志、服务端幂等、URL query 持久化步骤、局部错误收集、家长多子女动态行、跳过机制明确化
|
- ✅ P0-2/P0-3/P0-4/P0-5/P1-1/P1-2/P1-4/P1-5 已修复(v3 对标 PowerSchool/Veracross/Auth0):家长绑定三因子验证(邮箱+生日+手机号后4位)、教师多科目循环绑定、审计日志、服务端幂等、URL query 持久化步骤、局部错误收集、家长多子女动态行、跳过机制明确化
|
||||||
|
- ✅ v3 i18n 体系引入:采用 next-intl 4.x(without i18n routing 模式),cookie 驱动 locale 切换,字典放在 `shared/i18n/messages/{locale}/`,支持 zh-CN/en 两种语言,不破坏现有路由组结构
|
||||||
|
- ✅ v3 班级邀请码体系引入(对标 Google Classroom / 钉钉教育 / 智学网):新增 `class_invitation_codes` 表(独立表,支持有效期/次数限制/审计/多码并存),6 位字母数字(剔除歧义字符 0/O/1/I/L,空间 1.13 亿),rate limit 防爆破(10 次/5 分钟),审计日志全链路记录,懒清理过期码
|
||||||
|
|
||||||
**文件清单**:
|
**文件清单**:
|
||||||
| 文件 | 行数 | 职责 |
|
| 文件 | 行数 | 职责 |
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -46,14 +46,14 @@ export default async function GradeAnalyticsPage({
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Grade Analytics</h1>
|
<h1 className="text-2xl font-bold tracking-tight">成绩分析</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Trend analysis, class comparisons, and score distributions.
|
趋势分析、班级对比与分数分布。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="No classes"
|
title="暂无班级"
|
||||||
description="You don't have any classes yet."
|
description="您还没有任何班级。"
|
||||||
icon={BarChart3}
|
icon={BarChart3}
|
||||||
className="border-none shadow-none"
|
className="border-none shadow-none"
|
||||||
/>
|
/>
|
||||||
@@ -98,15 +98,15 @@ export default async function GradeAnalyticsPage({
|
|||||||
<div className="h-full flex-1 flex-col space-y-6 p-8 md:flex">
|
<div className="h-full flex-1 flex-col space-y-6 p-8 md:flex">
|
||||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Grade Analytics</h1>
|
<h1 className="text-2xl font-bold tracking-tight">成绩分析</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Trend analysis, class comparisons, and score distributions.
|
趋势分析、班级对比与分数分布。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Link href="/teacher/grades">
|
<Link href="/teacher/grades">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
Back to Grades
|
返回成绩列表
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { HomeworkAssignmentQuestionErrorOverviewCard } from "@/modules/homework/
|
|||||||
import { Badge } from "@/shared/components/ui/badge"
|
import { Badge } from "@/shared/components/ui/badge"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
import { formatDate } from "@/shared/lib/utils"
|
import { formatDate } from "@/shared/lib/utils"
|
||||||
import { ChevronLeft, Users, Calendar, BarChart3, CheckCircle2 } from "lucide-react"
|
import { ArrowLeft, Users, Calendar, BarChart3, CheckCircle2 } from "lucide-react"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
@@ -25,14 +25,12 @@ export default async function HomeworkAssignmentDetailPage({ params }: { params:
|
|||||||
<div className="border-b bg-background px-8 py-5">
|
<div className="border-b bg-background px-8 py-5">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||||
<div className="min-w-0 flex flex-col gap-2">
|
<div className="min-w-0 flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
|
<Button asChild variant="ghost" size="sm" className="w-fit">
|
||||||
<Link href="/teacher/homework/assignments" className="flex items-center hover:text-foreground transition-colors">
|
<Link href="/teacher/homework/assignments">
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" aria-hidden="true" />
|
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
Assignments
|
返回作业列表
|
||||||
</Link>
|
</Link>
|
||||||
<span aria-hidden="true">/</span>
|
</Button>
|
||||||
<span>Details</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-foreground line-clamp-2">{assignment.title}</h1>
|
<h1 className="text-2xl font-bold tracking-tight text-foreground line-clamp-2">{assignment.title}</h1>
|
||||||
<Badge variant={assignment.status === "published" ? "default" : "secondary"} className="capitalize">
|
<Badge variant={assignment.status === "published" ? "default" : "secondary"} className="capitalize">
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ export default function NewLessonPlanPage(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6 flex items-center gap-4">
|
<div className="mb-6 flex items-center gap-4">
|
||||||
<Button asChild variant="ghost" size="icon">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Link href="/teacher/lesson-plans" aria-label="Back to lesson plans">
|
<Link href="/teacher/lesson-plans">
|
||||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
返回备课列表
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">New Lesson Plan</h1>
|
<h1 className="text-2xl font-bold tracking-tight">新建课案</h1>
|
||||||
</div>
|
</div>
|
||||||
<TemplatePicker />
|
<TemplatePicker />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ export default async function TextbookDetailPage({
|
|||||||
<div className="flex flex-col h-[calc(100vh-4rem)] overflow-hidden">
|
<div className="flex flex-col h-[calc(100vh-4rem)] overflow-hidden">
|
||||||
{/* Header / Nav (Fixed height) */}
|
{/* Header / Nav (Fixed height) */}
|
||||||
<div className="flex items-center gap-4 py-4 border-b shrink-0 bg-background z-10">
|
<div className="flex items-center gap-4 py-4 border-b shrink-0 bg-background z-10">
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Link href="/teacher/textbooks" aria-label="Back to textbooks">
|
<Link href="/teacher/textbooks">
|
||||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
返回教材列表
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import Link from "next/link"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { ArrowLeft, Pencil, Plus, Trash2 } from "lucide-react"
|
import { ArrowLeft, Pencil, Plus, Trash2 } from "lucide-react"
|
||||||
|
|
||||||
@@ -16,16 +17,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/shared/components/ui/table"
|
} from "@/shared/components/ui/table"
|
||||||
import {
|
import { ConfirmDeleteDialog } from "@/shared/components/ui/confirm-delete-dialog"
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/shared/components/ui/alert-dialog"
|
|
||||||
import { usePermission } from "@/shared/hooks/use-permission"
|
import { usePermission } from "@/shared/hooks/use-permission"
|
||||||
import { Permissions } from "@/shared/types/permissions"
|
import { Permissions } from "@/shared/types/permissions"
|
||||||
import { formatDate } from "@/shared/lib/utils"
|
import { formatDate } from "@/shared/lib/utils"
|
||||||
@@ -36,10 +28,10 @@ import { deleteCoursePlanAction } from "../actions"
|
|||||||
import type { CoursePlanWithItems, CoursePlanStatus } from "../types"
|
import type { CoursePlanWithItems, CoursePlanStatus } from "../types"
|
||||||
|
|
||||||
const STATUS_LABEL: Record<CoursePlanStatus, string> = {
|
const STATUS_LABEL: Record<CoursePlanStatus, string> = {
|
||||||
planning: "Planning",
|
planning: "规划中",
|
||||||
active: "Active",
|
active: "进行中",
|
||||||
completed: "Completed",
|
completed: "已完成",
|
||||||
paused: "Paused",
|
paused: "已暂停",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CoursePlanDetail({
|
export function CoursePlanDetail({
|
||||||
@@ -72,10 +64,10 @@ export function CoursePlanDetail({
|
|||||||
router.push(base)
|
router.push(base)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.message || "Failed to delete")
|
toast.error(res.message || "删除失败")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Failed to delete")
|
toast.error("删除失败")
|
||||||
} finally {
|
} finally {
|
||||||
setIsWorking(false)
|
setIsWorking(false)
|
||||||
setDeleteOpen(false)
|
setDeleteOpen(false)
|
||||||
@@ -94,51 +86,52 @@ export function CoursePlanDetail({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center gap-2">
|
{backHref ? (
|
||||||
{backHref ? (
|
<Button asChild variant="ghost" size="sm" className="w-fit">
|
||||||
<Button asChild variant="ghost" size="icon">
|
<Link href={backHref}>
|
||||||
<a href={backHref}>
|
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
<ArrowLeft className="h-4 w-4" />
|
返回课程计划列表
|
||||||
</a>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
|
||||||
<h2 className="text-2xl font-bold tracking-tight">Course Plan</h2>
|
|
||||||
</div>
|
|
||||||
{canManage ? (
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{editHref ? (
|
|
||||||
<Button asChild variant="outline">
|
|
||||||
<a href={editHref}>
|
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<Button onClick={() => setDeleteOpen(true)} disabled={isWorking} variant="destructive">
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">课程计划</h2>
|
||||||
|
{canManage ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{editHref ? (
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link href={editHref}>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
编辑
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button onClick={() => setDeleteOpen(true)} disabled={isWorking} variant="destructive">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="space-y-2">
|
<CardHeader className="space-y-2">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge variant="outline">{plan.className ?? "No class"}</Badge>
|
<Badge variant="outline">{plan.className ?? "无班级"}</Badge>
|
||||||
<Badge variant="outline">{plan.subjectName ?? "Unknown subject"}</Badge>
|
<Badge variant="outline">{plan.subjectName ?? "未知学科"}</Badge>
|
||||||
<Badge>{STATUS_LABEL[plan.status]}</Badge>
|
<Badge>{STATUS_LABEL[plan.status]}</Badge>
|
||||||
<Badge variant="outline">Semester {plan.semester}</Badge>
|
<Badge variant="outline">第 {plan.semester} 学期</Badge>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl">
|
<CardTitle className="text-xl">
|
||||||
{plan.subjectName ?? "Course Plan"} — {plan.className ?? "No Class"}
|
{plan.subjectName ?? "课程计划"} — {plan.className ?? "无班级"}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>Teacher: {plan.teacherName ?? "Unassigned"}</span>
|
<span>教师:{plan.teacherName ?? "未分配"}</span>
|
||||||
<span>· Created {formatDate(plan.createdAt)}</span>
|
<span>· 创建于 {formatDate(plan.createdAt)}</span>
|
||||||
{plan.startDate ? <span>· Start {formatDate(plan.startDate)}</span> : null}
|
{plan.startDate ? <span>· 开始 {formatDate(plan.startDate)}</span> : null}
|
||||||
{plan.endDate ? <span>· End {formatDate(plan.endDate)}</span> : null}
|
{plan.endDate ? <span>· 结束 {formatDate(plan.endDate)}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@@ -150,13 +143,13 @@ export function CoursePlanDetail({
|
|||||||
/>
|
/>
|
||||||
{plan.syllabus ? (
|
{plan.syllabus ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-sm font-semibold">Syllabus</h4>
|
<h4 className="text-sm font-semibold">教学大纲</h4>
|
||||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{plan.syllabus}</p>
|
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{plan.syllabus}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{plan.objectives ? (
|
{plan.objectives ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-sm font-semibold">Objectives</h4>
|
<h4 className="text-sm font-semibold">教学目标</h4>
|
||||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{plan.objectives}</p>
|
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{plan.objectives}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -165,28 +158,28 @@ export function CoursePlanDetail({
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
<CardTitle>Weekly Plan</CardTitle>
|
<CardTitle>周计划</CardTitle>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Button onClick={openCreateEditor} size="sm">
|
<Button onClick={openCreateEditor} size="sm">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Week
|
添加周计划
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{plan.items.length === 0 ? (
|
{plan.items.length === 0 ? (
|
||||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||||
No weekly plans yet. {canManage ? "Click \"Add Week\" to create one." : ""}
|
暂无周计划。{canManage ? "点击「添加周计划」创建第一条。" : ""}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-16">Week</TableHead>
|
<TableHead className="w-16">周次</TableHead>
|
||||||
<TableHead>Topic</TableHead>
|
<TableHead>主题</TableHead>
|
||||||
<TableHead className="w-20">Hours</TableHead>
|
<TableHead className="w-20">课时</TableHead>
|
||||||
<TableHead className="w-32">Chapter</TableHead>
|
<TableHead className="w-32">章节</TableHead>
|
||||||
<TableHead className="w-28">Status</TableHead>
|
<TableHead className="w-28">状态</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -207,7 +200,7 @@ export function CoursePlanDetail({
|
|||||||
) : null}
|
) : null}
|
||||||
{item.notes ? (
|
{item.notes ? (
|
||||||
<p className="text-xs text-muted-foreground italic">
|
<p className="text-xs text-muted-foreground italic">
|
||||||
Note: {item.notes}
|
备注:{item.notes}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +211,7 @@ export function CoursePlanDetail({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={item.isCompleted ? "default" : "secondary"}>
|
<Badge variant={item.isCompleted ? "default" : "secondary"}>
|
||||||
{item.isCompleted ? "Done" : "Pending"}
|
{item.isCompleted ? "已完成" : "待完成"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -237,22 +230,14 @@ export function CoursePlanDetail({
|
|||||||
onOpenChange={setEditorOpen}
|
onOpenChange={setEditorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
<ConfirmDeleteDialog
|
||||||
<AlertDialogContent>
|
open={deleteOpen}
|
||||||
<AlertDialogHeader>
|
onOpenChange={setDeleteOpen}
|
||||||
<AlertDialogTitle>Delete course plan</AlertDialogTitle>
|
title="删除课程计划"
|
||||||
<AlertDialogDescription>
|
description="此操作将永久删除该课程计划及其所有周计划。"
|
||||||
This will permanently delete this course plan and all its weekly plans.
|
onConfirm={handleDelete}
|
||||||
</AlertDialogDescription>
|
isWorking={isWorking}
|
||||||
</AlertDialogHeader>
|
/>
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel disabled={isWorking}>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleDelete} disabled={isWorking}>
|
|
||||||
Delete
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,16 +34,18 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ButtonProps = React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: ButtonProps) {
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,4 +57,4 @@ function Button({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants, type ButtonProps }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as React from "react"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from "@/shared/lib/utils"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
|
import type { ButtonProps } from "@/shared/components/ui/button"
|
||||||
|
|
||||||
interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
title: string
|
title: string
|
||||||
@@ -11,6 +12,8 @@ interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
label: string
|
label: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
href?: string
|
href?: string
|
||||||
|
/** 按钮样式,默认 "outline"(次级按钮)。首次引导场景可用 "default"(主按钮) */
|
||||||
|
variant?: ButtonProps["variant"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +47,11 @@ export function EmptyState({
|
|||||||
)}
|
)}
|
||||||
{action && (
|
{action && (
|
||||||
action.href ? (
|
action.href ? (
|
||||||
<Button asChild variant="default">
|
<Button asChild variant={action.variant ?? "outline"}>
|
||||||
<Link href={action.href}>{action.label}</Link>
|
<Link href={action.href}>{action.label}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={action.onClick} variant="default">
|
<Button onClick={action.onClick} variant={action.variant ?? "outline"}>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,34 @@ export function formatDate(date: string | Date, locale: string = "zh-CN") {
|
|||||||
}).format(new Date(date))
|
}).format(new Date(date))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期+时间(含小时、分钟)。
|
||||||
|
* 用于替代各处重复的 `new Date(x).toLocaleString("zh-CN", {...})` 调用。
|
||||||
|
*/
|
||||||
|
export function formatDateTime(date: string | Date, locale: string = "zh-CN") {
|
||||||
|
return new Intl.DateTimeFormat(locale, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(new Date(date))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化为长日期(含星期、完整月份名)。
|
||||||
|
* 用于替代 `new Date(x).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })`。
|
||||||
|
* 默认使用中文 locale,输出形如「2026年6月20日 周一」。
|
||||||
|
*/
|
||||||
|
export function formatLongDate(date: string | Date, locale: string = "zh-CN") {
|
||||||
|
return new Intl.DateTimeFormat(locale, {
|
||||||
|
weekday: "short",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(new Date(date))
|
||||||
|
}
|
||||||
|
|
||||||
/** Next.js App Router 搜索参数类型 */
|
/** Next.js App Router 搜索参数类型 */
|
||||||
export type SearchParams = { [key: string]: string | string[] | undefined }
|
export type SearchParams = { [key: string]: string | string[] | undefined }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user