feat(parent-portal): docker 构建修复 + 禁用 mock + 记录下游待办
Dockerfile 改用 standalone 模式 next.config.js 添加 output standalone 修复 cssnano-simple 构建失败:globals.css 注释含 */ 被误解析 tailwind.config.js 覆盖 boxShadow 为 rgba 格式 + 禁用 ringWidth 组件修复:替换 / 语法为 inline style 或自定义类 otel.ts 添加 webpackIgnore 跳过 optionalDependencies 静态分析 .env.example 默认禁用 mock 新增 .dockerignore 和 docs/nextstep.md 删除 postcss.config.js 验证:Docker 镜像构建成功,容器 healthy,typecheck + lint 零错误
This commit is contained in:
92
.dockerignore
Normal file
92
.dockerignore
Normal file
@@ -0,0 +1,92 @@
|
||||
# 依赖目录(构建时重新安装)
|
||||
**/node_modules
|
||||
.pnpm-store
|
||||
|
||||
# 构建产物
|
||||
**/.next
|
||||
**/out
|
||||
**/dist
|
||||
**/build
|
||||
**/coverage
|
||||
**/target
|
||||
**/bin
|
||||
**/obj
|
||||
|
||||
# Go 构建产物
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Python 缓存
|
||||
**/__pycache__
|
||||
**/*.py[cod]
|
||||
**/*$py.class
|
||||
**/.venv
|
||||
**/venv
|
||||
**/*.egg-info
|
||||
|
||||
# 环境变量与本地配置
|
||||
.env*.local
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Secrets / Keys
|
||||
keys/
|
||||
*.pem
|
||||
*.key
|
||||
secrets/
|
||||
|
||||
# IDE 与编辑器
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# 平台与临时文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# TypeScript 增量构建缓存
|
||||
*.tsbuildinfo
|
||||
|
||||
# 测试覆盖率报告
|
||||
.nyc_output
|
||||
**/coverage
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# 数据库文件
|
||||
*.db
|
||||
*.sqlite
|
||||
arch.db
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
docker-compose.minimal.override.yml
|
||||
|
||||
# 临时目录
|
||||
tmp/
|
||||
temp/
|
||||
.turbo
|
||||
.cache
|
||||
|
||||
# 文档(不需要打包到镜像)
|
||||
docs/
|
||||
**/docs/
|
||||
*.md
|
||||
!README.md
|
||||
47
apps/parent-portal/.dockerignore
Normal file
47
apps/parent-portal/.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
||||
# 依赖目录(构建时重新安装)
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# 构建产物
|
||||
.next
|
||||
out
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
|
||||
# 测试与开发工具
|
||||
.turbo
|
||||
.cache
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 环境变量与本地配置
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# IDE 与编辑器
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# 平台与临时文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# TypeScript 增量构建缓存
|
||||
*.tsbuildinfo
|
||||
|
||||
# 测试覆盖率报告
|
||||
.nyc_output
|
||||
coverage
|
||||
@@ -19,7 +19,8 @@ NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/v1/parent/v1/graphql
|
||||
# ===== Mock =====
|
||||
# MSW mock 开关:enabled 启用 mock,disabled 调真实后端
|
||||
# 依据:workline.md §4.3 全并行 Mock 策略
|
||||
NEXT_PUBLIC_API_MOCKING=enabled
|
||||
# 默认禁用:parent-portal 已切换至真实 API 集成(详见 docs/nextstep.md)
|
||||
NEXT_PUBLIC_API_MOCKING=disabled
|
||||
|
||||
# ===== 推送(P5)=====
|
||||
# push-gateway WebSocket 地址
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 多阶段构建:Next.js 生产镜像(parent-portal MF Remote)
|
||||
# 多阶段构建:Next.js 生产镜像(parent-portal,standalone 模式)
|
||||
# 用法:docker build -t edu/parent-portal:latest -f apps/parent-portal/Dockerfile .
|
||||
# 依据:02-architecture-design.md §16 部署、G1 裁决(首次即多阶段)
|
||||
# 端口:4002(4 端 portal 强制 4000-4003)
|
||||
@@ -12,6 +12,7 @@ RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
|
||||
# 先拷依赖清单,利用缓存
|
||||
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
|
||||
COPY tsconfig.base.json ./tsconfig.base.json
|
||||
COPY apps/parent-portal/package.json ./apps/parent-portal/
|
||||
# 安装依赖(含 devDependencies,构建需要)
|
||||
RUN pnpm install --filter @edu/parent-portal... --frozen-lockfile || pnpm install --filter @edu/parent-portal...
|
||||
@@ -19,9 +20,8 @@ RUN pnpm install --filter @edu/parent-portal... --frozen-lockfile || pnpm instal
|
||||
# 拷源码
|
||||
COPY apps/parent-portal ./apps/parent-portal
|
||||
|
||||
# 构建(禁用 telemetry,生产模式)
|
||||
# 构建(禁用 telemetry,生产模式,standalone 输出)
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter @edu/parent-portal run build
|
||||
|
||||
# ============ Runtime ============
|
||||
@@ -31,22 +31,25 @@ WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=4002
|
||||
# standalone server.js 默认绑定 HOSTNAME(Docker 中为容器 ID),导致 localhost 不可达
|
||||
# 强制绑定 0.0.0.0 让健康检查 wget localhost 能连通
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# 非 root 用户运行
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
|
||||
|
||||
# 拷构建产物与必要清单
|
||||
COPY --from=builder /app/apps/parent-portal/package.json ./package.json
|
||||
COPY --from=builder /app/apps/parent-portal/.next ./.next
|
||||
COPY --from=builder /app/apps/parent-portal/public ./public
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/apps/parent-portal/next.config.js ./next.config.js
|
||||
# 拷 standalone 产物(已含 node_modules 和 server.js,自包含)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/parent-portal/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/parent-portal/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/parent-portal/public ./public
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 4002
|
||||
|
||||
# 健康检查
|
||||
# 健康检查(使用 127.0.0.1 而非 localhost,避免容器内 hosts 解析问题)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD wget --quiet --spider http://localhost:4002/api/health || exit 1
|
||||
CMD wget --quiet --spider http://127.0.0.1:4002/api/health || exit 1
|
||||
|
||||
CMD ["node_modules/.bin/next", "start", "-p", "4002"]
|
||||
# standalone 模式下直接用 node server.js 启动(已自包含所有依赖)
|
||||
WORKDIR /app/apps/parent-portal
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
210
apps/parent-portal/docs/nextstep.md
Normal file
210
apps/parent-portal/docs/nextstep.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# parent-portal 下游待办事项(Next Steps)
|
||||
|
||||
> 版本:v1
|
||||
> 日期:2026-07-13
|
||||
> 用途:记录 parent-portal 切换至真实 API 集成所需的下游模块工作
|
||||
> 背景:parent-portal 已完成 Docker Desktop 验证(容器健康、25 路由生成、/login 200、/api/health 200),现需下游服务就绪才能完成端到端真实 API 联调
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前状态摘要
|
||||
|
||||
| 项目 | 状态 | 说明 |
|
||||
| -------------------------- | ----------------- | ---------------------------------------------------------------------- |
|
||||
| parent-portal Docker 镜像 | ✅ 已就绪 | `edu-parent-portal:test`,standalone 模式,25 路由 |
|
||||
| parent-portal 容器健康检查 | ✅ 通过 | `/api/health` 200,`/login` 200,`/parent/dashboard` 307 auth redirect |
|
||||
| Mock 数据 | ✅ 已禁用 | `NEXT_PUBLIC_API_MOCKING=disabled` |
|
||||
| GraphQL 端点配置 | ✅ 已配置 | `NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/v1/parent/v1/graphql` |
|
||||
| parent-bff 服务 | ❌ 未就绪 | 端点路径错配 + 27 query / 5 mutation 缺失 |
|
||||
| api-gateway 路由 | ⚠️ 已配置但未验证 | `/api/v1/parent/*` → parent-bff:3010,但 parent-bff 未运行 |
|
||||
| iam GetChildrenByParent | ✅ 已就绪 | gRPC + HTTP 均已实现 |
|
||||
| push-gateway /ws | ✅ 已就绪 | 完整 WebSocket 实现 |
|
||||
|
||||
---
|
||||
|
||||
## 2. P0 阻塞项:parent-bff 端点路径错配
|
||||
|
||||
### 问题描述
|
||||
|
||||
前端调用 GraphQL 端点:`/api/v1/parent/v1/graphql`(ARB-022 §24.4 ISSUE-003 方案 A 双 /v1 前缀)
|
||||
|
||||
api-gateway 代理路径转换:
|
||||
|
||||
- 入站:`/api/v1/parent/v1/graphql`
|
||||
- 剥离 `/api` 后转发:`/v1/parent/v1/graphql` → parent-bff:3010
|
||||
|
||||
但 parent-bff 当前 controller 仅注册 `/graphql`,导致 `/v1/parent/v1/graphql` 在 parent-bff 找不到匹配的 handler,返回 404。
|
||||
|
||||
### 修复位置(parent-bff 模块)
|
||||
|
||||
| 文件 | 当前 | 应改为 |
|
||||
| ----------------------------------------------------- | ----------------------------- | -------------------------------------------------------------- |
|
||||
| `services/parent-bff/src/entry/graphql.controller.ts` | `@Controller("graphql")` | `@Controller("v1/parent/v1/graphql")` 或使用 `setGlobalPrefix` |
|
||||
| `services/parent-bff/src/graphql/yoga.ts` | `graphqlEndpoint: "/graphql"` | `graphqlEndpoint: "/v1/parent/v1/graphql"` |
|
||||
| `services/parent-bff/src/graphql/graphql.module.ts` | `forRoutes("graphql")` | `forRoutes("v1/parent/v1/graphql")` |
|
||||
|
||||
### 验证步骤
|
||||
|
||||
1. 修改后构建 parent-bff 镜像
|
||||
2. 启动 parent-bff 容器,验证 `http://parent-bff:3010/v1/parent/v1/graphql` 返回 200
|
||||
3. 经 api-gateway 代理访问 `http://api-gateway:8080/api/v1/parent/v1/graphql` 返回 200
|
||||
|
||||
---
|
||||
|
||||
## 3. P0 阻塞项:parent-bff 缺失 pnpm-lock.yaml
|
||||
|
||||
### 问题描述
|
||||
|
||||
`services/parent-bff/Dockerfile` 第 6 行 `RUN pnpm install --frozen-lockfile` 失败,因为 `services/parent-bff/` 目录下没有 `pnpm-lock.yaml` 文件。
|
||||
|
||||
### 修复方案
|
||||
|
||||
在 `services/parent-bff/` 目录执行 `pnpm install` 生成 lockfile,并提交到仓库。
|
||||
|
||||
```bash
|
||||
cd services/parent-bff
|
||||
pnpm install
|
||||
git add pnpm-lock.yaml
|
||||
git commit -m "chore(parent-bff): add pnpm-lock.yaml for Docker build"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. P0 阻塞项:parent-bff GraphQL Schema 严重不对齐
|
||||
|
||||
### 问题描述
|
||||
|
||||
| 类别 | 前端定义 | 后端实现 | 差距 |
|
||||
| -------- | -------- | -------- | --------------------------------------------------------------- |
|
||||
| Query | 32 个 | 11 个 | 21 个缺失(含 6 个命名不一致 + 15 个完全缺失 + 3 个签名不一致) |
|
||||
| Mutation | 6 个 | 3 个 | 3 个缺失(含 2 个命名不一致 + 1 个完全缺失 + 1 个签名不一致) |
|
||||
|
||||
### 4.1 命名不一致的 Query(6 个)
|
||||
|
||||
| 前端 operation | 后端 operation | 修复方式 |
|
||||
| --------------------------- | ------------------------- | ---------------------------------------------------------------- |
|
||||
| `currentUser` | `me` | 二选一统一命名(建议后端改 `me` → `currentUser`) |
|
||||
| `myChildren` | `children` | 二选一统一命名(建议后端改 `children` → `myChildren`) |
|
||||
| `myNotifications` | `notifications` | 二选一统一命名(建议后端改 `notifications` → `myNotifications`) |
|
||||
| `myNotificationPreferences` | `notificationPreferences` | 二选一统一命名 |
|
||||
| `childSummary` | `child` | 字段不同,需明确语义 |
|
||||
| `childAnalytics` | (前端未使用) | 后端已有但前端未对接,保留 |
|
||||
|
||||
### 4.2 完全缺失的 Query(15 个,后端需新增)
|
||||
|
||||
| 前端 operation | 涉及下游服务 |
|
||||
| ------------------------ | ------------ |
|
||||
| `childAttendance` | core-edu |
|
||||
| `childWeakness` | data-ana |
|
||||
| `childTrend` | data-ana |
|
||||
| `childExamResult` | core-edu |
|
||||
| `childClasses` | classes |
|
||||
| `childLearningPath` | data-ana |
|
||||
| `childLeaveRequests` | core-edu |
|
||||
| `academicYears` | core-edu |
|
||||
| `childReportCard` | core-edu |
|
||||
| `childErrorBookStats` | data-ana |
|
||||
| `childTopWrongQuestions` | data-ana |
|
||||
| `childWeakKps` | data-ana |
|
||||
| `childMasterySummary` | data-ana |
|
||||
| `childDiagnosticReports` | data-ana |
|
||||
| `childPracticeStats` | data-ana |
|
||||
| `childPracticeSessions` | data-ana |
|
||||
| `childCoursePlans` | core-edu |
|
||||
| `childCoursePlanDetail` | content |
|
||||
| `childLessonPlans` | core-edu |
|
||||
| `childLessonPlanDetail` | content |
|
||||
| `childElective` | content |
|
||||
| `childDetail` | data-ana |
|
||||
| `childGrowthArchive` | data-ana |
|
||||
|
||||
### 4.3 签名不一致的 Query(3 个)
|
||||
|
||||
| 前端 operation | 不一致点 |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `childGrades` | 前端 `(childId, subject, limit)` vs 后端 `(childId, page, pageSize, subject)`;返回字段不同(前端 `examName/examDate/studentScore/classMax/classMin/gradeLevel` vs 后端 `examId/examTitle/score/rank/gradedAt`) |
|
||||
| `childHomework` | 前端有 `status` 过滤参数,后端无;字段差异大(前端 `subject/className/assignedDate/score/maxScore/feedback` vs 后端 `title/classId/dueDate/status`) |
|
||||
| `childExams` | 字段差异大(前端 `name/subject/startsAt/expiresAt/durationSeconds/questionCount/totalScore` vs 后端 `title/examDate/publishedAt`) |
|
||||
|
||||
### 4.4 缺失的 Mutation(3 个)
|
||||
|
||||
| 前端 operation | 涉及下游服务 |
|
||||
| -------------------- | ---------------------------------------------------------- |
|
||||
| `markAsRead` | parent-bff(重命名 `markNotificationRead` → `markAsRead`) |
|
||||
| `markAllAsRead` | parent-bff(新增) |
|
||||
| `switchChild` | parent-bff(重命名 `selectChild` → `switchChild`) |
|
||||
| `createLeaveRequest` | core-edu(新增) |
|
||||
| `exportChildGrades` | core-edu(新增) |
|
||||
|
||||
### 4.5 签名不一致的 Mutation(1 个)
|
||||
|
||||
| 前端 operation | 不一致点 |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `updateNotificationPreferences` | 前端 `(parentId, preferences: JSON, defaults: JSON)` vs 后端 `(input: UpdateNotificationPreferencesInput)` |
|
||||
|
||||
### 修复步骤
|
||||
|
||||
1. **契约先行**:更新 `packages/shared-ts/contracts/graphql/parent-bff.graphql`,扩充至前端 32 query + 6 mutation
|
||||
2. **resolver 实现**:在 `services/parent-bff/src/graphql/resolvers/` 新增对应 resolver
|
||||
3. **下游 gRPC client**:在 `services/parent-bff/src/clients/` 新增/对接 core-edu / data-ana / content / classes 的 gRPC client
|
||||
4. **测试**:parent-bff 单测 + parent-portal 端到端联调
|
||||
|
||||
---
|
||||
|
||||
## 5. P1 阻塞项:api-gateway 镜像构建问题
|
||||
|
||||
### 问题描述
|
||||
|
||||
`services/api-gateway/Dockerfile` 使用 `golang:1.22-alpine` 和 `alpine:3.20` 基础镜像,但本地 Docker Desktop 未拉取这两个镜像,且 daocloud 镜像源返回 401。
|
||||
|
||||
### 修复方案
|
||||
|
||||
1. 提前在服务器执行:`docker pull golang:1.22-alpine && docker pull alpine:3.20`
|
||||
2. 或在 Dockerfile 中改用本地已有的镜像源
|
||||
|
||||
---
|
||||
|
||||
## 6. 已就绪的下游服务(无需等待)
|
||||
|
||||
| 服务 | 状态 | 验证位置 |
|
||||
| ----------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| iam.GetChildrenByParent | ✅ gRPC + HTTP 均已实现 | `services/iam/src/iam/iam.grpc.controller.ts:116`、`services/iam/src/iam/iam.controller.ts:116` |
|
||||
| push-gateway /ws | ✅ 完整实现 | `services/push-gateway/internal/ws/handler.go` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 验证 Checklist(下游就绪后执行)
|
||||
|
||||
- [ ] parent-bff 添加 pnpm-lock.yaml
|
||||
- [ ] parent-bff GraphQL 端点路径改为 `/v1/parent/v1/graphql`
|
||||
- [ ] parent-bff schema 扩充至 32 query + 6 mutation
|
||||
- [ ] parent-bff 所有 resolver 实现完成
|
||||
- [ ] api-gateway 镜像可构建
|
||||
- [ ] 在 Docker Desktop 启动完整服务栈:api-gateway + parent-bff + iam + core-edu + data-ana + content + classes + push-gateway
|
||||
- [ ] parent-portal 端到端联调:登录 → /parent/dashboard → 各功能页面
|
||||
- [ ] 验证 WebSocket 推送:通知功能
|
||||
- [ ] 验证 25 个路由全部可访问(无 500 错误)
|
||||
|
||||
---
|
||||
|
||||
## 8. 关键文件路径
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `apps/parent-portal/Dockerfile` | parent-portal Docker 构建(已修复,standalone 模式) |
|
||||
| `apps/parent-portal/next.config.js` | Next.js 配置(已添加 `output: "standalone"`) |
|
||||
| `apps/parent-portal/src/lib/graphql-client.ts` | GraphQL 客户端配置 |
|
||||
| `apps/parent-portal/src/lib/graphql/operations.ts` | 前端所有 GraphQL operations 定义 |
|
||||
| `apps/parent-portal/.env.example` | 环境变量模板(`NEXT_PUBLIC_API_MOCKING=disabled`) |
|
||||
| `services/parent-bff/Dockerfile` | parent-bff Docker 构建(待修复:缺 pnpm-lock.yaml) |
|
||||
| `services/parent-bff/src/entry/graphql.controller.ts` | GraphQL 端点路径(待修复) |
|
||||
| `services/parent-bff/src/graphql/yoga.ts` | GraphQL Yoga 配置(待修复) |
|
||||
| `services/parent-bff/src/graphql/graphql.module.ts` | GraphQL 模块路由(待修复) |
|
||||
| `services/parent-bff/src/graphql/resolvers/index.ts` | resolver 实现(待扩充) |
|
||||
| `packages/shared-ts/contracts/graphql/parent-bff.graphql` | GraphQL schema 契约(待扩充) |
|
||||
| `services/api-gateway/main.go:86` | parent 代理注册 |
|
||||
| `services/api-gateway/Dockerfile` | api-gateway Docker 构建(待修复:基础镜像缺失) |
|
||||
|
||||
---
|
||||
|
||||
**总结**:parent-portal 前端已完全就绪(Docker 镜像构建成功、容器健康、25 路由可用、mock 已禁用),等待下游 parent-bff 完成端点路径修复 + schema 扩充 + resolver 实现后,即可完成端到端真实 API 联调。
|
||||
@@ -26,6 +26,8 @@ const remotes = (isServer) => ({
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
// standalone 输出模式:自包含 server.js + 精简 node_modules(适合 Docker 部署)
|
||||
output: "standalone",
|
||||
// ESM 兼容(MF 2.0 需要)
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* Layer 1 Primitive:原始色板/字号/间距/阴影(业务代码不直接引用)
|
||||
* Layer 2 Semantic:语义令牌(light/dark),业务代码唯一引用入口
|
||||
* Layer 3 Tailwind Theme:@theme inline 暴露为 bg-*/text-*/font-* 类
|
||||
* Layer 3 Tailwind Theme:@theme inline 暴露为 bg, text, font 等类
|
||||
*/
|
||||
|
||||
:root {
|
||||
@@ -59,10 +59,10 @@
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
/* 阴影(使用 rgba 旧语法,避免 cssnano-simple 无法解析 rgb(/ 语法)*/
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* ===== Layer 2 Semantic(light)===== */
|
||||
--paper: var(--color-paper-50);
|
||||
@@ -168,6 +168,11 @@ h6 {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* 安全的 col-span-2(替代 Tailwind col-span-2,避免 cssnano-simple 无法解析 grid-column: span 2 / span 2 语法)*/
|
||||
.col-span-2-safe {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
/* 骨架屏动画 */
|
||||
@keyframes skeleton-pulse {
|
||||
0%,
|
||||
|
||||
@@ -62,25 +62,25 @@ export default function SettingsPage(): JSX.Element {
|
||||
<dl className="space-y-3">
|
||||
<div className="grid grid-cols-3 items-baseline gap-4">
|
||||
<dt className="text-xs text-ink-muted">用户 ID</dt>
|
||||
<dd className="col-span-2 font-mono text-sm">{user.id}</dd>
|
||||
<dd className="col-span-2-safe font-mono text-sm">{user.id}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-baseline gap-4">
|
||||
<dt className="text-xs text-ink-muted">姓名</dt>
|
||||
<dd className="col-span-2 text-sm">{user.name}</dd>
|
||||
<dd className="col-span-2-safe text-sm">{user.name}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-baseline gap-4">
|
||||
<dt className="text-xs text-ink-muted">邮箱</dt>
|
||||
<dd className="col-span-2 text-sm">{user.email}</dd>
|
||||
<dd className="col-span-2-safe text-sm">{user.email}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-baseline gap-4">
|
||||
<dt className="text-xs text-ink-muted">角色</dt>
|
||||
<dd className="col-span-2 text-sm">
|
||||
<dd className="col-span-2-safe text-sm">
|
||||
{user.roles.join(", ") || "无角色"}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-baseline gap-4">
|
||||
<dt className="text-xs text-ink-muted">数据范围</dt>
|
||||
<dd className="col-span-2 text-sm">{user.dataScope}</dd>
|
||||
<dd className="col-span-2-safe text-sm">{user.dataScope}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -57,9 +57,16 @@ export function AttendanceCalendar({
|
||||
month,
|
||||
onMonthChange,
|
||||
}: AttendanceCalendarProps) {
|
||||
const { year, month: monthLabel, days, recordMap } = useMemo(() => {
|
||||
const {
|
||||
year,
|
||||
month: monthLabel,
|
||||
days,
|
||||
recordMap,
|
||||
} = useMemo(() => {
|
||||
const now = new Date();
|
||||
const target = month ? parseMonth(month) : { year: now.getFullYear(), month: now.getMonth() };
|
||||
const target = month
|
||||
? parseMonth(month)
|
||||
: { year: now.getFullYear(), month: now.getMonth() };
|
||||
const y = target.year;
|
||||
const m = target.month;
|
||||
const firstDay = new Date(y, m, 1);
|
||||
@@ -96,7 +103,9 @@ export function AttendanceCalendar({
|
||||
{onMonthChange && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMonthChange(shiftMonth(formatMonth(year, monthLabel), -1))}
|
||||
onClick={() =>
|
||||
onMonthChange(shiftMonth(formatMonth(year, monthLabel), -1))
|
||||
}
|
||||
className="rounded px-2 py-1 text-sm text-ink-muted hover:text-ink"
|
||||
aria-label="上一月"
|
||||
>
|
||||
@@ -109,7 +118,9 @@ export function AttendanceCalendar({
|
||||
{onMonthChange && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMonthChange(shiftMonth(formatMonth(year, monthLabel), 1))}
|
||||
onClick={() =>
|
||||
onMonthChange(shiftMonth(formatMonth(year, monthLabel), 1))
|
||||
}
|
||||
className="rounded px-2 py-1 text-sm text-ink-muted hover:text-ink"
|
||||
aria-label="下一月"
|
||||
>
|
||||
@@ -146,14 +157,15 @@ export function AttendanceCalendar({
|
||||
<div className="mt-1 grid grid-cols-7 gap-1">
|
||||
{days.map((day, idx) => {
|
||||
if (day === null) {
|
||||
return <div key={idx} className="aspect-square" />;
|
||||
return <div key={idx} style={{ aspectRatio: "1 / 1" }} />;
|
||||
}
|
||||
const dateStr = `${year}-${String(monthLabel + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
const record = recordMap.get(dateStr);
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex aspect-square flex-col items-center justify-center rounded text-sm"
|
||||
className="flex flex-col items-center justify-center rounded text-sm"
|
||||
style={{ aspectRatio: "1 / 1" }}
|
||||
title={record ? STATUS_LABELS[record.status] : undefined}
|
||||
>
|
||||
<span className="text-xs">{day}</span>
|
||||
|
||||
@@ -84,10 +84,13 @@ export function AttendanceWarningBanner({
|
||||
role="alert"
|
||||
className={cn(
|
||||
"rounded border p-4",
|
||||
hasDanger
|
||||
? "border-danger bg-danger/10"
|
||||
: "border-warning bg-warning/10",
|
||||
hasDanger ? "border-danger" : "border-warning",
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: hasDanger
|
||||
? "hsl(var(--danger) / 0.1)"
|
||||
: "hsl(var(--warning) / 0.1)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 space-y-1">
|
||||
|
||||
@@ -66,10 +66,13 @@ export function ParentAttentionBanner({
|
||||
role="alert"
|
||||
className={cn(
|
||||
"rounded border p-4",
|
||||
hasDanger
|
||||
? "border-danger bg-danger/10"
|
||||
: "border-warning bg-warning/10",
|
||||
hasDanger ? "border-danger" : "border-warning",
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: hasDanger
|
||||
? "hsl(var(--danger) / 0.1)"
|
||||
: "hsl(var(--warning) / 0.1)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 space-y-1">
|
||||
|
||||
@@ -60,20 +60,29 @@ export async function initOTel(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
// 动态加载 OTel 各包(运行时按需加载)
|
||||
const instrumentation =
|
||||
(await import("@opentelemetry/instrumentation")) as unknown as OTelAutoInstrumentationType;
|
||||
const fetchInstrumentationMod =
|
||||
await import("@opentelemetry/instrumentation-fetch");
|
||||
const xhrInstrumentationMod =
|
||||
await import("@opentelemetry/instrumentation-xml-http-request");
|
||||
const documentLoadMod =
|
||||
await import("@opentelemetry/instrumentation-document-load");
|
||||
const webTracerMod =
|
||||
(await import("@opentelemetry/sdk-trace-web")) as unknown as OTelExporterType;
|
||||
const exporterMod =
|
||||
(await import("@opentelemetry/exporter-trace-otlp-http")) as unknown as OTelExporterType;
|
||||
const contextManagerMod =
|
||||
(await import("@opentelemetry/context-zone")) as unknown as ZoneContextManagerType;
|
||||
// webpackIgnore: true 让 webpack 跳过这些模块的静态分析,
|
||||
// 这样即使 @opentelemetry/* 未安装(optionalDependencies)也能构建成功
|
||||
const instrumentation = (await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/instrumentation"
|
||||
)) as unknown as OTelAutoInstrumentationType;
|
||||
const fetchInstrumentationMod = await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/instrumentation-fetch"
|
||||
);
|
||||
const xhrInstrumentationMod = await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/instrumentation-xml-http-request"
|
||||
);
|
||||
const documentLoadMod = await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/instrumentation-document-load"
|
||||
);
|
||||
const webTracerMod = (await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/sdk-trace-web"
|
||||
)) as unknown as OTelExporterType;
|
||||
const exporterMod = (await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/exporter-trace-otlp-http"
|
||||
)) as unknown as OTelExporterType;
|
||||
const contextManagerMod = (await import(
|
||||
/* webpackIgnore: true */ "@opentelemetry/context-zone"
|
||||
)) as unknown as ZoneContextManagerType;
|
||||
|
||||
// 构造 FetchInstrumentation
|
||||
const FetchInstrumentation = (
|
||||
|
||||
@@ -56,7 +56,24 @@ module.exports = {
|
||||
lg: "var(--radius-lg)",
|
||||
full: "9999px",
|
||||
},
|
||||
// 覆盖阴影值使用 rgba() 格式,避免 cssnano-simple 无法解析 rgb(0 0 0 / 0.1) 现代语法
|
||||
boxShadow: {
|
||||
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||
DEFAULT: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
|
||||
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
|
||||
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
||||
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
|
||||
"2xl": "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
|
||||
inner: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)",
|
||||
none: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
// 禁用 ringWidth 核心插件(parent-portal 不使用 ring-* 类)
|
||||
// 原因:ringWidth 在 preflight 中生成 --tw-ring-color: rgb(59 130 246 / 0.5),
|
||||
// 旧版 cssnano-simple 无法解析此现代 CSS 语法导致构建失败
|
||||
corePlugins: {
|
||||
ringWidth: false,
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user