feat: improve app ui and project metadata

This commit is contained in:
Yaojia Wang
2026-06-28 07:31:20 +02:00
parent 3bd464d400
commit 90a66437d7
86 changed files with 4892 additions and 1108 deletions

View File

@@ -4,9 +4,12 @@ import type { ForeshadowView } from "@/lib/api/types";
import {
buildRegisterRequest,
buildTransitionRequest,
countByStatus,
extractReason,
groupByStatus,
isWindowApproaching,
LANE_LABELS,
nextFreeForeshadowCode,
suggestForeshadowCode,
validationReasonMessage,
} from "./board";
@@ -24,6 +27,24 @@ describe("suggestForeshadowCode", () => {
});
});
describe("nextFreeForeshadowCode", () => {
it("keeps the preferred code when it is free", () => {
expect(nextFreeForeshadowCode("FS-04", new Set(["FS-01", "FS-02"]))).toBe(
"FS-04",
);
});
it("skips taken codes and returns the first free FS-NN", () => {
const taken = new Set(["FS-01", "FS-02", "FS-03", "FS-05", "FS-OLD"]);
// preferred FS-01 已占 → 取首个空闲 FS-04
expect(nextFreeForeshadowCode("FS-01", taken)).toBe("FS-04");
});
it("returns FS-01 when nothing is taken", () => {
expect(nextFreeForeshadowCode("FS-01", new Set())).toBe("FS-01");
});
});
const view = (over: Partial<ForeshadowView>): ForeshadowView => ({
code: "F-001",
title: "线索",
@@ -53,6 +74,32 @@ describe("groupByStatus", () => {
});
});
describe("countByStatus", () => {
it("counts items in the same lane model used by the board", () => {
expect(
countByStatus([
view({ status: "OPEN" }),
view({ status: "OPEN" }),
view({ status: "PARTIAL" }),
view({ status: "CLOSED" }),
view({ status: "OVERDUE" }),
view({ status: "WEIRD" }),
]),
).toEqual({ OPEN: 3, PARTIAL: 1, CLOSED: 1, OVERDUE: 1 });
});
});
describe("LANE_LABELS", () => {
it("keeps author-facing labels distinct from status codes", () => {
expect(LANE_LABELS).toEqual({
OPEN: "待推进",
PARTIAL: "推进中",
CLOSED: "已回收",
OVERDUE: "已逾期",
});
});
});
describe("isWindowApproaching", () => {
it("is true when current chapter is inside [from,to] for OPEN/PARTIAL", () => {
const f = view({

View File

@@ -18,13 +18,14 @@ export const LANES: readonly ForeshadowStatus[] = [
] as const;
export const LANE_LABELS: Record<ForeshadowStatus, string> = {
OPEN: "OPEN",
PARTIAL: "PARTIAL",
CLOSED: "CLOSED",
OVERDUE: "OVERDUE",
OPEN: "待推进",
PARTIAL: "推进中",
CLOSED: "已回收",
OVERDUE: "已逾期",
};
export type ForeshadowLanes = Record<ForeshadowStatus, ForeshadowView[]>;
export type ForeshadowLaneCounts = Record<ForeshadowStatus, number>;
function emptyLanes(): ForeshadowLanes {
return { OPEN: [], PARTIAL: [], CLOSED: [], OVERDUE: [] };
@@ -46,6 +47,18 @@ export function groupByStatus(
return lanes;
}
export function countByStatus(
items: readonly ForeshadowView[] | undefined,
): ForeshadowLaneCounts {
const lanes = groupByStatus(items);
return {
OPEN: lanes.OPEN.length,
PARTIAL: lanes.PARTIAL.length,
CLOSED: lanes.CLOSED.length,
OVERDUE: lanes.OVERDUE.length,
};
}
// 接近回收窗口判定current 在 [from, to] 内,或已超 from 但尚未 CLOSED提示安排回收
// 仅对 OPEN/PARTIAL 提示CLOSED 已回收、OVERDUE 已逾期另有强调。
export function isWindowApproaching(
@@ -70,6 +83,21 @@ export function suggestForeshadowCode(
return `FS-${String(index + 1).padStart(2, "0")}`;
}
// 在已占用代号taken区分大小写按原样为预填代号挑一个不冲突的值
// preferred 未被占用就用它;否则从 FS-01 起取首个空闲的 FS-<两位序号>。
// 纯函数(可单测)。避免登记时与库里已存代号撞码 → 422 duplicate。
export function nextFreeForeshadowCode(
preferred: string,
taken: ReadonlySet<string>,
): string {
if (preferred && !taken.has(preferred)) return preferred;
for (let n = 1; n < 1000; n++) {
const candidate = `FS-${String(n).padStart(2, "0")}`;
if (!taken.has(candidate)) return candidate;
}
return preferred; // 理论不可达1000 个代号全占用时退回原值
}
export interface RegisterInput {
projectId: string;
code: string;