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:
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user