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:
SpecialX
2026-07-13 17:21:39 +08:00
parent 8b729a710f
commit 96a936e154
14 changed files with 453 additions and 55 deletions

View File

@@ -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 Semanticlight===== */
--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%,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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 = (