feat: introduce i18n system and class invitation codes
Add complete i18n infrastructure using next-intl (cookie-driven, without i18n routing) with zh-CN/en dictionary files, locale switcher, and NextIntlClientProvider in root layout. Add class invitation code system with new class_invitation_codes table, data-access layer (generate/validate/consume/revoke), server actions with permission checks, rate limiting, and audit logging. Add class-invitation-manager UI component. Refactor onboarding stepper to use i18n translations and accept new invitation code format (6-char alphanumeric) with backward compatibility for legacy 6-digit codes.
This commit is contained in:
75
src/shared/components/locale-switcher.tsx
Normal file
75
src/shared/components/locale-switcher.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTransition } from "react"
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import { Check, Globe } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/components/ui/dropdown-menu"
|
||||
import { LOCALES, type Locale } from "@/shared/i18n/locale"
|
||||
import { setLocaleAction } from "@/i18n/actions"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
/**
|
||||
* 语言切换组件(v3 i18n 体系)。
|
||||
*
|
||||
* 设计:
|
||||
* - 不使用 URL 路由段,通过 cookie 持久化用户偏好
|
||||
* - 切换时调用 setLocaleAction 写入 cookie + revalidatePath
|
||||
* - 用 useTransition 保证切换过程不阻塞 UI
|
||||
*/
|
||||
export function LocaleSwitcher({ compact = false }: { compact?: boolean }) {
|
||||
const locale = useLocale()
|
||||
const t = useTranslations("common.locale")
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
function handleSelect(next: Locale) {
|
||||
if (next === locale) return
|
||||
startTransition(async () => {
|
||||
const result = await setLocaleAction(next)
|
||||
if (result.success) {
|
||||
toast.success(t("switch"))
|
||||
} else {
|
||||
toast.error(t("switch"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={compact ? "icon" : "sm"}
|
||||
disabled={isPending}
|
||||
aria-label={t("switch")}
|
||||
className={cn(compact && "h-9 w-9")}
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
{!compact ? (
|
||||
<span className="ml-1.5 text-sm">{t(locale)}</span>
|
||||
) : null}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{LOCALES.map((l) => (
|
||||
<DropdownMenuItem
|
||||
key={l}
|
||||
onClick={() => handleSelect(l)}
|
||||
className="flex items-center justify-between gap-2"
|
||||
>
|
||||
<span>{t(l)}</span>
|
||||
{l === locale ? <Check className="h-4 w-4" /> : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
43
src/shared/i18n/locale.ts
Normal file
43
src/shared/i18n/locale.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 项目 i18n 配置(without i18n routing 模式)。
|
||||
*
|
||||
* 设计决策(v3 引入完整 i18n 体系):
|
||||
* - 采用 next-intl 4.x,官方推荐的 Next.js App Router 方案
|
||||
* - 不使用 `[locale]` 路由段,避免破坏现有 (auth)/(dashboard)/(onboarding) 路由组结构
|
||||
* - locale 通过 cookie 持久化,SSR 时从 cookie 读取
|
||||
* - 字典放在 shared/i18n/messages/,符合三层架构约束
|
||||
*
|
||||
* 支持的 locale:
|
||||
* - zh-CN(默认):简体中文
|
||||
* - en:英文
|
||||
*/
|
||||
|
||||
export const LOCALES = ["zh-CN", "en"] as const;
|
||||
export type Locale = (typeof LOCALES)[number];
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = "zh-CN";
|
||||
|
||||
/** Cookie 名称,与 proxy.ts / request.ts 保持一致 */
|
||||
export const LOCALE_COOKIE = "NEXT_LOCALE";
|
||||
|
||||
/** Cookie 属性:1 年有效期,全站可读 */
|
||||
export const LOCALE_COOKIE_OPTIONS = {
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
path: "/",
|
||||
sameSite: "lax" as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验字符串是否为受支持的 locale。
|
||||
* 用于从 cookie/header 读取后的容错处理。
|
||||
*/
|
||||
export function isLocale(value: string | null | undefined): value is Locale {
|
||||
return typeof value === "string" && (LOCALES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任意字符串归一化为合法 locale,非法值回退到默认。
|
||||
*/
|
||||
export function normalizeLocale(value: string | null | undefined): Locale {
|
||||
return isLocale(value) ? value : DEFAULT_LOCALE;
|
||||
}
|
||||
31
src/shared/i18n/messages/en/auth.json
Normal file
31
src/shared/i18n/messages/en/auth.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign In",
|
||||
"subtitle": "Sign in to your Next_Edu account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"rememberMe": "Remember me",
|
||||
"signIn": "Sign In",
|
||||
"signingIn": "Signing in...",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"noAccount": "Don't have an account?",
|
||||
"register": "Register"
|
||||
},
|
||||
"register": {
|
||||
"title": "Register",
|
||||
"subtitle": "Create your Next_Edu account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm password",
|
||||
"name": "Name",
|
||||
"signUp": "Sign Up",
|
||||
"signingUp": "Signing up...",
|
||||
"hasAccount": "Already have an account?",
|
||||
"signIn": "Sign In"
|
||||
},
|
||||
"errors": {
|
||||
"invalidCredentials": "Invalid email or password",
|
||||
"emailExists": "This email is already registered",
|
||||
"passwordTooWeak": "Password is too weak"
|
||||
}
|
||||
}
|
||||
55
src/shared/i18n/messages/en/classes.json
Normal file
55
src/shared/i18n/messages/en/classes.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"invitation": {
|
||||
"title": "Class Invitation Code",
|
||||
"generate": "Generate Code",
|
||||
"regenerate": "Regenerate",
|
||||
"revoke": "Revoke",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"code": "Code",
|
||||
"status": "Status",
|
||||
"expiresAt": "Expires At",
|
||||
"usedCount": "Used Count",
|
||||
"maxUses": "Max Uses",
|
||||
"createdAt": "Created At",
|
||||
"note": "Note",
|
||||
"neverExpires": "Never expires",
|
||||
"unlimited": "Unlimited",
|
||||
"active": "Active",
|
||||
"disabled": "Disabled",
|
||||
"expired": "Expired",
|
||||
"exhausted": "Exhausted",
|
||||
"list": "Invitation Codes",
|
||||
"empty": "No invitation codes",
|
||||
"join": "Join Class",
|
||||
"joinPlaceholder": "Enter 6-character code",
|
||||
"joinSuccess": "Joined class successfully",
|
||||
"joinFailed": "Failed to join class",
|
||||
"invalidCode": "Invalid or expired invitation code",
|
||||
"rateLimited": "Too many attempts, please try again later",
|
||||
"alreadyInClass": "You are already in this class",
|
||||
"subjectConflict": "This subject already has a teacher for this class",
|
||||
"generateSuccess": "Invitation code generated",
|
||||
"generateFailed": "Failed to generate invitation code",
|
||||
"revokeSuccess": "Invitation code revoked",
|
||||
"revokeFailed": "Failed to revoke invitation code",
|
||||
"regenerateSuccess": "Invitation code regenerated",
|
||||
"regenerateConfirm": "The old code will be invalidated immediately after regeneration. Continue?",
|
||||
"revokeConfirm": "The code will be invalidated immediately after revocation. Continue?",
|
||||
"expiresInHours": "Validity (hours)",
|
||||
"maxUsesLabel": "Max uses",
|
||||
"customNote": "Note (optional)",
|
||||
"customNotePlaceholder": "e.g., Back-to-school temporary code",
|
||||
"generateWithCustom": "Generate custom code",
|
||||
"defaultDuration": "Never expires",
|
||||
"defaultMaxUses": "Unlimited"
|
||||
},
|
||||
"class": {
|
||||
"title": "Class",
|
||||
"name": "Class Name",
|
||||
"grade": "Grade",
|
||||
"homeroomTeacher": "Homeroom Teacher",
|
||||
"studentCount": "Student Count",
|
||||
"actions": "Actions"
|
||||
}
|
||||
}
|
||||
53
src/shared/i18n/messages/en/common.json
Normal file
53
src/shared/i18n/messages/en/common.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Next_Edu - K12 Smart Education System",
|
||||
"description": "Enterprise Grade K12 Education Management System"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"confirm": "Confirm",
|
||||
"search": "Search",
|
||||
"reset": "Reset",
|
||||
"submit": "Submit",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"finish": "Finish",
|
||||
"skip": "Skip",
|
||||
"retry": "Retry",
|
||||
"close": "Close",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"edit": "Edit",
|
||||
"view": "View",
|
||||
"loading": "Loading...",
|
||||
"saving": "Saving...",
|
||||
"submitting": "Submitting..."
|
||||
},
|
||||
"status": {
|
||||
"success": "Success",
|
||||
"failure": "Failure",
|
||||
"pending": "Pending",
|
||||
"active": "Active",
|
||||
"disabled": "Disabled",
|
||||
"expired": "Expired",
|
||||
"exhausted": "Exhausted"
|
||||
},
|
||||
"locale": {
|
||||
"switch": "Switch language",
|
||||
"zh-CN": "中文",
|
||||
"en": "English"
|
||||
},
|
||||
"empty": {
|
||||
"default": "No data",
|
||||
"noResults": "No matching results found"
|
||||
},
|
||||
"pagination": {
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"page": "Page {page}",
|
||||
"total": "{total} items"
|
||||
}
|
||||
}
|
||||
23
src/shared/i18n/messages/en/errors.json
Normal file
23
src/shared/i18n/messages/en/errors.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"auth": {
|
||||
"required": "Please sign in first",
|
||||
"forbidden": "Access denied",
|
||||
"sessionExpired": "Session expired, please sign in again"
|
||||
},
|
||||
"network": {
|
||||
"requestFailed": "Request failed, please try again later",
|
||||
"timeout": "Request timed out",
|
||||
"serverError": "Server error"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"invalidFormat": "Invalid format",
|
||||
"tooShort": "Too short",
|
||||
"tooLong": "Too long"
|
||||
},
|
||||
"db": {
|
||||
"notFound": "Record not found",
|
||||
"duplicate": "Record already exists",
|
||||
"constraintViolation": "Data constraint violation"
|
||||
}
|
||||
}
|
||||
78
src/shared/i18n/messages/en/onboarding.json
Normal file
78
src/shared/i18n/messages/en/onboarding.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"title": "First Login Onboarding",
|
||||
"description": "Complete your first login setup",
|
||||
"steps": {
|
||||
"roleConfirm": "Role Confirmation",
|
||||
"basicInfo": "Basic Information",
|
||||
"roleInfo": "Role Information",
|
||||
"complete": "Complete"
|
||||
},
|
||||
"role": {
|
||||
"yourRole": "Your Role",
|
||||
"allRoles": "All roles: {roles}",
|
||||
"adminAssigned": "Roles are pre-assigned by administrators. Contact an administrator to change.",
|
||||
"admin": "Administrator",
|
||||
"teacher": "Teacher",
|
||||
"student": "Student",
|
||||
"parent": "Parent",
|
||||
"grade_head": "Grade Head",
|
||||
"teaching_head": "Teaching Head"
|
||||
},
|
||||
"form": {
|
||||
"name": "Name",
|
||||
"nameRequired": "Please enter your name",
|
||||
"nameMax": "Name cannot exceed 50 characters",
|
||||
"phone": "Phone",
|
||||
"phoneRequired": "Please enter your phone number",
|
||||
"phoneInvalid": "Please enter a valid 11-digit phone number starting with 1",
|
||||
"address": "Address",
|
||||
"addressMax": "Address cannot exceed 200 characters"
|
||||
},
|
||||
"teacher": {
|
||||
"classCodes": "Class Invitation Codes",
|
||||
"classCodesOptional": "Class invitation codes (optional, multiple allowed)",
|
||||
"classCodesPlaceholder": "One per line or comma-separated, 6 alphanumeric characters",
|
||||
"classCodesHint": "The server validates the code and checks whether the subject is already assigned to another teacher.",
|
||||
"subjects": "Teaching Subjects",
|
||||
"subjectsOptional": "Teaching subjects (optional, multiple selection)",
|
||||
"subjectsHint": "Multiple subjects can be selected. The server binds the teacher to each subject for each class. If a subject already has a teacher, the binding will be rejected."
|
||||
},
|
||||
"student": {
|
||||
"classCodesOptional": "Class invitation codes (optional)",
|
||||
"classCodesPlaceholder": "One per line or comma-separated, 6 alphanumeric characters",
|
||||
"classCodesHint": "Skip this if an administrator has pre-assigned your class."
|
||||
},
|
||||
"parent": {
|
||||
"bindHint": "Bind children via child email + child birthday + last 4 digits of child phone (at least one complete entry required, multiple allowed).",
|
||||
"childN": "Child {index}",
|
||||
"childEmail": "Child Email",
|
||||
"childEmailRequired": "Please enter the child's email",
|
||||
"childEmailInvalid": "Invalid child email format",
|
||||
"childBirthDate": "Child Birthday",
|
||||
"childBirthDateRequired": "Please enter the child's birthday",
|
||||
"childBirthDateInvalid": "Invalid child birthday format (YYYY-MM-DD)",
|
||||
"childPhoneSuffix": "Child Phone Last 4 Digits",
|
||||
"childPhoneSuffixRequired": "Please enter the last 4 digits of the child's phone",
|
||||
"childPhoneSuffixInvalid": "Invalid last 4 digits format",
|
||||
"childRelation": "Relationship",
|
||||
"childRelationPlaceholder": "Father / Mother / Other",
|
||||
"addChild": "Add Child"
|
||||
},
|
||||
"complete": {
|
||||
"ready": "Ready to Finish",
|
||||
"readyHint": "Click finish to enter the system."
|
||||
},
|
||||
"validation": {
|
||||
"needNamePhone": "Please enter your name and phone",
|
||||
"needOneChild": "Please complete at least one child's binding information"
|
||||
},
|
||||
"progress": {
|
||||
"label": "Onboarding progress"
|
||||
},
|
||||
"toast": {
|
||||
"completeSuccess": "Setup complete",
|
||||
"partialFailure": "Setup complete, but {count} binding(s) failed",
|
||||
"submitFailed": "Submission failed",
|
||||
"inputInvalid": "Input validation failed"
|
||||
}
|
||||
}
|
||||
31
src/shared/i18n/messages/zh-CN/auth.json
Normal file
31
src/shared/i18n/messages/zh-CN/auth.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "登录",
|
||||
"subtitle": "使用您的账号登录 Next_Edu",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"rememberMe": "记住我",
|
||||
"signIn": "登录",
|
||||
"signingIn": "登录中...",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"noAccount": "还没有账号?",
|
||||
"register": "注册"
|
||||
},
|
||||
"register": {
|
||||
"title": "注册",
|
||||
"subtitle": "创建 Next_Edu 账号",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"name": "姓名",
|
||||
"signUp": "注册",
|
||||
"signingUp": "注册中...",
|
||||
"hasAccount": "已有账号?",
|
||||
"signIn": "登录"
|
||||
},
|
||||
"errors": {
|
||||
"invalidCredentials": "邮箱或密码错误",
|
||||
"emailExists": "该邮箱已被注册",
|
||||
"passwordTooWeak": "密码强度不足"
|
||||
}
|
||||
}
|
||||
55
src/shared/i18n/messages/zh-CN/classes.json
Normal file
55
src/shared/i18n/messages/zh-CN/classes.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"invitation": {
|
||||
"title": "班级邀请码",
|
||||
"generate": "生成邀请码",
|
||||
"regenerate": "重新生成",
|
||||
"revoke": "撤销",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"code": "邀请码",
|
||||
"status": "状态",
|
||||
"expiresAt": "过期时间",
|
||||
"usedCount": "已用次数",
|
||||
"maxUses": "最大次数",
|
||||
"createdAt": "创建时间",
|
||||
"note": "备注",
|
||||
"neverExpires": "永久有效",
|
||||
"unlimited": "不限",
|
||||
"active": "有效",
|
||||
"disabled": "已禁用",
|
||||
"expired": "已过期",
|
||||
"exhausted": "已用尽",
|
||||
"list": "邀请码列表",
|
||||
"empty": "暂无邀请码",
|
||||
"join": "加入班级",
|
||||
"joinPlaceholder": "请输入 6 位邀请码",
|
||||
"joinSuccess": "加入班级成功",
|
||||
"joinFailed": "加入班级失败",
|
||||
"invalidCode": "邀请码无效或已失效",
|
||||
"rateLimited": "尝试过于频繁,请稍后再试",
|
||||
"alreadyInClass": "你已在该班级中",
|
||||
"subjectConflict": "该班级此科目已有任课教师",
|
||||
"generateSuccess": "邀请码生成成功",
|
||||
"generateFailed": "邀请码生成失败",
|
||||
"revokeSuccess": "邀请码已撤销",
|
||||
"revokeFailed": "邀请码撤销失败",
|
||||
"regenerateSuccess": "邀请码已重新生成",
|
||||
"regenerateConfirm": "重新生成后旧邀请码将立即失效,确定继续吗?",
|
||||
"revokeConfirm": "撤销后此邀请码将立即失效,确定继续吗?",
|
||||
"expiresInHours": "有效期(小时)",
|
||||
"maxUsesLabel": "最大使用次数",
|
||||
"customNote": "备注(可选)",
|
||||
"customNotePlaceholder": "如:开学季临时码",
|
||||
"generateWithCustom": "生成自定义邀请码",
|
||||
"defaultDuration": "永久有效",
|
||||
"defaultMaxUses": "不限次数"
|
||||
},
|
||||
"class": {
|
||||
"title": "班级",
|
||||
"name": "班级名称",
|
||||
"grade": "年级",
|
||||
"homeroomTeacher": "班主任",
|
||||
"studentCount": "学生人数",
|
||||
"actions": "操作"
|
||||
}
|
||||
}
|
||||
53
src/shared/i18n/messages/zh-CN/common.json
Normal file
53
src/shared/i18n/messages/zh-CN/common.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Next_Edu - K12 智慧教务系统",
|
||||
"description": "企业级 K12 教务管理系统"
|
||||
},
|
||||
"actions": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"confirm": "确认",
|
||||
"search": "搜索",
|
||||
"reset": "重置",
|
||||
"submit": "提交",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"finish": "完成",
|
||||
"skip": "跳过",
|
||||
"retry": "重试",
|
||||
"close": "关闭",
|
||||
"add": "添加",
|
||||
"remove": "移除",
|
||||
"edit": "编辑",
|
||||
"view": "查看",
|
||||
"loading": "加载中...",
|
||||
"saving": "保存中...",
|
||||
"submitting": "提交中..."
|
||||
},
|
||||
"status": {
|
||||
"success": "成功",
|
||||
"failure": "失败",
|
||||
"pending": "待处理",
|
||||
"active": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"expired": "已过期",
|
||||
"exhausted": "已用尽"
|
||||
},
|
||||
"locale": {
|
||||
"switch": "切换语言",
|
||||
"zh-CN": "中文",
|
||||
"en": "English"
|
||||
},
|
||||
"empty": {
|
||||
"default": "暂无数据",
|
||||
"noResults": "未找到匹配结果"
|
||||
},
|
||||
"pagination": {
|
||||
"previous": "上一页",
|
||||
"next": "下一页",
|
||||
"page": "第 {page} 页",
|
||||
"total": "共 {total} 条"
|
||||
}
|
||||
}
|
||||
23
src/shared/i18n/messages/zh-CN/errors.json
Normal file
23
src/shared/i18n/messages/zh-CN/errors.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"auth": {
|
||||
"required": "请先登录",
|
||||
"forbidden": "无权限访问",
|
||||
"sessionExpired": "会话已过期,请重新登录"
|
||||
},
|
||||
"network": {
|
||||
"requestFailed": "请求失败,请稍后重试",
|
||||
"timeout": "请求超时",
|
||||
"serverError": "服务器错误"
|
||||
},
|
||||
"validation": {
|
||||
"required": "此字段为必填",
|
||||
"invalidFormat": "格式错误",
|
||||
"tooShort": "长度过短",
|
||||
"tooLong": "长度过长"
|
||||
},
|
||||
"db": {
|
||||
"notFound": "记录不存在",
|
||||
"duplicate": "记录已存在",
|
||||
"constraintViolation": "数据约束冲突"
|
||||
}
|
||||
}
|
||||
78
src/shared/i18n/messages/zh-CN/onboarding.json
Normal file
78
src/shared/i18n/messages/zh-CN/onboarding.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"title": "首次登录引导",
|
||||
"description": "完成首次登录信息配置",
|
||||
"steps": {
|
||||
"roleConfirm": "角色确认",
|
||||
"basicInfo": "基础信息",
|
||||
"roleInfo": "角色信息",
|
||||
"complete": "完成"
|
||||
},
|
||||
"role": {
|
||||
"yourRole": "你的角色",
|
||||
"allRoles": "全部角色:{roles}",
|
||||
"adminAssigned": "角色由管理员预分配,如需变更请联系管理员。",
|
||||
"admin": "管理员",
|
||||
"teacher": "教师",
|
||||
"student": "学生",
|
||||
"parent": "家长",
|
||||
"grade_head": "年级主任",
|
||||
"teaching_head": "教研组长"
|
||||
},
|
||||
"form": {
|
||||
"name": "姓名",
|
||||
"nameRequired": "请填写姓名",
|
||||
"nameMax": "姓名长度不能超过 50 个字符",
|
||||
"phone": "电话",
|
||||
"phoneRequired": "请填写电话",
|
||||
"phoneInvalid": "请输入有效的手机号(11 位,以 1 开头)",
|
||||
"address": "住址",
|
||||
"addressMax": "住址长度不能超过 200 个字符"
|
||||
},
|
||||
"teacher": {
|
||||
"classCodes": "班级邀请码",
|
||||
"classCodesOptional": "班级邀请码(可选,可多个)",
|
||||
"classCodesPlaceholder": "每行一个或用逗号分隔,6 位字母数字",
|
||||
"classCodesHint": "服务端会校验邀请码有效性及科目是否已被其他教师占用。",
|
||||
"subjects": "教学科目",
|
||||
"subjectsOptional": "教学科目(可选,可多选)",
|
||||
"subjectsHint": "可选多个科目,服务端会为每个班级的每个所选科目绑定任课教师。若班级该科目已有任课教师,绑定将被拒绝。"
|
||||
},
|
||||
"student": {
|
||||
"classCodesOptional": "班级邀请码(可选)",
|
||||
"classCodesPlaceholder": "每行一个或用逗号分隔,6 位字母数字",
|
||||
"classCodesHint": "若管理员已预分配班级,可跳过此项。"
|
||||
},
|
||||
"parent": {
|
||||
"bindHint": "通过子女邮箱 + 子女生日 + 子女手机号后 4 位绑定子女(至少完整填写一个,可添加多个)。",
|
||||
"childN": "子女 {index}",
|
||||
"childEmail": "子女邮箱",
|
||||
"childEmailRequired": "请填写子女邮箱",
|
||||
"childEmailInvalid": "子女邮箱格式错误",
|
||||
"childBirthDate": "子女生日",
|
||||
"childBirthDateRequired": "请填写子女生日",
|
||||
"childBirthDateInvalid": "子女生日格式错误(YYYY-MM-DD)",
|
||||
"childPhoneSuffix": "子女手机号后 4 位",
|
||||
"childPhoneSuffixRequired": "请填写子女手机号后 4 位",
|
||||
"childPhoneSuffixInvalid": "子女手机号后 4 位格式错误",
|
||||
"childRelation": "关系",
|
||||
"childRelationPlaceholder": "父亲 / 母亲 / 其他",
|
||||
"addChild": "添加子女"
|
||||
},
|
||||
"complete": {
|
||||
"ready": "已准备完成",
|
||||
"readyHint": "点击完成后进入系统。"
|
||||
},
|
||||
"validation": {
|
||||
"needNamePhone": "请填写姓名与电话",
|
||||
"needOneChild": "请至少完整填写一个子女的绑定信息"
|
||||
},
|
||||
"progress": {
|
||||
"label": "Onboarding 进度"
|
||||
},
|
||||
"toast": {
|
||||
"completeSuccess": "配置完成",
|
||||
"partialFailure": "配置完成,但 {count} 项绑定失败",
|
||||
"submitFailed": "提交失败",
|
||||
"inputInvalid": "输入校验失败"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user