feat(teacher-portal): 完整实现 teacher-portal 微前端
包含 settings/students/api、graphql、mocks、ui-tokens 设计令牌等
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
// 认证工具:token 存储 + 路由保护
|
||||
/**
|
||||
* 认证工具:token 存储 + 路由保护
|
||||
*
|
||||
* F12 裁决:P2 用 localStorage(accessToken + userInfo),P6 迁移 httpOnly cookie
|
||||
* 维护者:ai13(teacher-portal)
|
||||
* 关联:02-architecture-design.md §2.1 会话状态
|
||||
*/
|
||||
|
||||
const TOKEN_KEY = "edu_access_token";
|
||||
const USER_KEY = "edu_user_info";
|
||||
@@ -48,12 +54,17 @@ export function isAuthenticated(): boolean {
|
||||
return getToken() !== null;
|
||||
}
|
||||
|
||||
// 调用 Gateway 登录接口
|
||||
/**
|
||||
* 调用 Gateway 登录接口
|
||||
*
|
||||
* 非 GraphQL(认证前提,MF 依赖认证,F12 裁决)
|
||||
* 路径:POST /api/auth/login(经 next.config.js rewrites 代理到 api-gateway)
|
||||
*/
|
||||
export async function login(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<{ user: UserInfo; token: string }> {
|
||||
const res = await fetch("/api/v1/iam/login", {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
|
||||
302
apps/teacher-portal/src/lib/graphql.ts
Normal file
302
apps/teacher-portal/src/lib/graphql.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* GraphQL client + queries/mutations(ARB-001 P2 第一版 schema)
|
||||
*
|
||||
* 维护者:ai13(teacher-portal)
|
||||
* 关联:coord.md §1 ARB-001、02-architecture-design.md §4
|
||||
*
|
||||
* schema 来源:packages/shared-ts/contracts/graphql/teacher-bff.graphql(ai03 维护)
|
||||
* P2 第一版 Query 范围(ARB-001 §1.2):dashboard / viewports / me / classes / class(5 个)
|
||||
* P2 不包含 Mutation(P3+ 扩展)
|
||||
*
|
||||
* 错误处理(ARB-001 §1.3):
|
||||
* - BFF 始终返回 ActionState 信封(success/errors/data)
|
||||
* - GraphQL errors[].extensions.code = BFF_TEACHER_*(G14 裁决)
|
||||
* - 前端按前缀路由 i18n key(F11: error.teacher_bff.<code_snake>)
|
||||
*/
|
||||
|
||||
import { createClient, cacheExchange, fetchExchange } from "urql";
|
||||
import { gql } from "urql";
|
||||
import type { Client } from "urql";
|
||||
|
||||
// ============ Types(对齐 ARB-001 §1.2 schema) ============
|
||||
|
||||
export type DataScope =
|
||||
"SELF" | "CLASS" | "GRADE" | "SCHOOL" | "DISTRICT" | "ALL";
|
||||
|
||||
export type ExamStatus =
|
||||
"DRAFT" | "PUBLISHED" | "IN_PROGRESS" | "GRADING" | "SCORED" | "ARCHIVED";
|
||||
|
||||
export type SubmissionStatus = "NOT_SUBMITTED" | "SUBMITTED" | "GRADED";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
dataScope: DataScope;
|
||||
}
|
||||
|
||||
export interface ViewportItem {
|
||||
key: string;
|
||||
label: string;
|
||||
route: string;
|
||||
icon: string | null;
|
||||
sortOrder: string;
|
||||
requiredPermission: string | null;
|
||||
}
|
||||
|
||||
export interface Class {
|
||||
id: string;
|
||||
name: string;
|
||||
gradeId: string;
|
||||
studentCount: number;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
totalExams: number;
|
||||
pendingGrading: number;
|
||||
todayHomework: number;
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
user: User;
|
||||
classes: Class[];
|
||||
viewports: ViewportItem[];
|
||||
stats: DashboardStats;
|
||||
}
|
||||
|
||||
// ============ urql client 单例(总裁 §2.17 方案 A) ============
|
||||
|
||||
/**
|
||||
* 创建 urql client 单例
|
||||
*
|
||||
* Shell(teacher-portal)调用此函数创建 client,通过 GraphQLProvider 注入给所有 Remote。
|
||||
* token 从 localStorage 读取(F12: P2 用 localStorage,P6 迁移 httpOnly cookie)。
|
||||
*/
|
||||
export function createGraphQLClient(): Client {
|
||||
const url =
|
||||
process.env.NEXT_PUBLIC_TEACHER_BFF_GRAPHQL_URL ||
|
||||
"/api/v1/teacher/graphql";
|
||||
|
||||
return createClient({
|
||||
url,
|
||||
fetchOptions: () => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// F12: P2 token 存 localStorage
|
||||
if (typeof window !== "undefined") {
|
||||
const token = window.localStorage.getItem("edu_access_token");
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
return { headers };
|
||||
},
|
||||
exchanges: [cacheExchange, fetchExchange],
|
||||
});
|
||||
}
|
||||
|
||||
// ============ P2 Query(ARB-001 §1.2,5 个 Must Have) ============
|
||||
|
||||
/** 仪表盘聚合查询(并行:iam.me + iam.viewports + iam.permissions + classes.byTeacher) */
|
||||
export const DashboardQuery = gql`
|
||||
query Dashboard {
|
||||
dashboard {
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
roles
|
||||
dataScope
|
||||
}
|
||||
classes {
|
||||
id
|
||||
name
|
||||
gradeId
|
||||
studentCount
|
||||
}
|
||||
viewports {
|
||||
key
|
||||
label
|
||||
route
|
||||
icon
|
||||
sortOrder
|
||||
requiredPermission
|
||||
}
|
||||
stats {
|
||||
totalExams
|
||||
pendingGrading
|
||||
todayHomework
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 视口配置查询(导航) */
|
||||
export const ViewportsQuery = gql`
|
||||
query Viewports {
|
||||
viewports {
|
||||
key
|
||||
label
|
||||
route
|
||||
icon
|
||||
sortOrder
|
||||
requiredPermission
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 当前用户信息查询 */
|
||||
export const MeQuery = gql`
|
||||
query Me {
|
||||
me {
|
||||
id
|
||||
email
|
||||
name
|
||||
roles
|
||||
dataScope
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 班级列表查询(按教师 dataScope 过滤) */
|
||||
export const ClassesQuery = gql`
|
||||
query Classes {
|
||||
classes {
|
||||
id
|
||||
name
|
||||
gradeId
|
||||
studentCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 班级详情查询 */
|
||||
export const ClassQuery = gql`
|
||||
query Class($id: ID!) {
|
||||
class(id: $id) {
|
||||
id
|
||||
name
|
||||
gradeId
|
||||
studentCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============ P3 Query/Mutation(MSW mock,待 teacher-bff P3+ 实现) ============
|
||||
// 以下查询对应 workline.md §3 P3-P5 交付物,schema 尚未在 ARB-001 P2 第一版中定义。
|
||||
// 开发期通过 MSW 拦截返回 mock 响应;上游就绪后由 ai03 在 teacher-bff.graphql 扩展。
|
||||
|
||||
/** 学生信息(iam 数据,经 teacher-bff 聚合) */
|
||||
export interface Student {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
/** 班级学生名单查询(P3 扩展,MSW mock) */
|
||||
export const ClassStudentsQuery = gql`
|
||||
query ClassStudents($classId: ID!) {
|
||||
classStudents(classId: $classId) {
|
||||
id
|
||||
name
|
||||
email
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 考试项(core-edu 域) */
|
||||
export interface ExamItem {
|
||||
id: string;
|
||||
classId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
examDate: string;
|
||||
duration: number;
|
||||
totalScore: number;
|
||||
status: ExamStatus;
|
||||
}
|
||||
|
||||
/** 班级考试列表查询(P3 扩展,MSW mock) */
|
||||
export const ClassExamsQuery = gql`
|
||||
query ClassExams($classId: ID!) {
|
||||
classExams(classId: $classId) {
|
||||
id
|
||||
classId
|
||||
title
|
||||
description
|
||||
examDate
|
||||
duration
|
||||
totalScore
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 作业项(core-edu 域) */
|
||||
export interface HomeworkItem {
|
||||
id: string;
|
||||
classId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
dueDate: string;
|
||||
status: SubmissionStatus;
|
||||
}
|
||||
|
||||
/** 班级作业列表查询(P3 扩展,MSW mock) */
|
||||
export const ClassHomeworkQuery = gql`
|
||||
query ClassHomework($classId: ID!) {
|
||||
classHomework(classId: $classId) {
|
||||
id
|
||||
classId
|
||||
title
|
||||
description
|
||||
dueDate
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 成绩项(core-edu 域) */
|
||||
export interface GradeItem {
|
||||
id: string;
|
||||
studentId: string;
|
||||
studentName: string;
|
||||
examId: string | null;
|
||||
homeworkId: string | null;
|
||||
score: number;
|
||||
feedback: string | null;
|
||||
}
|
||||
|
||||
/** 考试成绩列表查询(P3 扩展,MSW mock) */
|
||||
export const ExamGradesQuery = gql`
|
||||
query ExamGrades($examId: ID!) {
|
||||
examGrades(examId: $examId) {
|
||||
id
|
||||
studentId
|
||||
studentName
|
||||
examId
|
||||
homeworkId
|
||||
score
|
||||
feedback
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** 更新用户信息 Mutation(P3 扩展,MSW mock) */
|
||||
export const UpdateUserMutation = gql`
|
||||
mutation UpdateUser($input: UpdateUserInput!) {
|
||||
updateUser(input: $input) {
|
||||
id
|
||||
email
|
||||
name
|
||||
roles
|
||||
dataScope
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user