feat(web): 用户端骨架——登录、工作台、新建项目、解析进度、读标清单
- packages/api-client:从 openapi.yaml 生成类型;cookie + CSRF / Bearer 两种鉴权、 分片上传、fetch 读事件流(API2) - apps/web:Vue 3 + Vite + TanStack Query,样式只来自 tokens.css / components.css(AR10); 扣点说明放在「开始解析」旁边(红线 9);接口地址、路由方式运行时可配(架构 §5.2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
134
apps/web/src/lib/labels.ts
Normal file
134
apps/web/src/lib/labels.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/** 枚举的中文显示名由前端维护(api-design §1);不认识的枚举值原样显示,不报错。 */
|
||||
import type { Schemas } from "@bid-doctor/api-client";
|
||||
|
||||
type Dict<K extends string> = Readonly<Record<K, string>>;
|
||||
const pick = <K extends string>(d: Dict<K>, k: string | null | undefined): string => (k ? (d as Record<string, string>)[k] ?? k : "");
|
||||
|
||||
export const PROJECT_STATUS: Dict<Schemas["ProjectStatus"]> = {
|
||||
tracking: "跟进中",
|
||||
drafting: "编制中",
|
||||
checked: "已检查",
|
||||
submitted: "已递交",
|
||||
archived: "已归档",
|
||||
};
|
||||
export const projectStatus = (s: string) => pick(PROJECT_STATUS, s);
|
||||
|
||||
export const LEGAL_REGIME: Dict<Schemas["LegalRegime"]> = {
|
||||
gov_procurement: "政府采购",
|
||||
tendering: "招标投标",
|
||||
unknown: "法律体系待识别",
|
||||
};
|
||||
export const legalRegime = (s: string) => pick(LEGAL_REGIME, s);
|
||||
|
||||
export const CATEGORY_ORDER: readonly Schemas["Category"][] = [
|
||||
"qualification",
|
||||
"invalidity",
|
||||
"scoring",
|
||||
"schedule",
|
||||
"tech_param",
|
||||
"format_doc",
|
||||
"price",
|
||||
];
|
||||
export const CATEGORY: Dict<Schemas["Category"]> = {
|
||||
qualification: "资格条件",
|
||||
invalidity: "无效条款",
|
||||
scoring: "评分办法",
|
||||
schedule: "日期与递交",
|
||||
tech_param: "技术参数",
|
||||
format_doc: "格式文件",
|
||||
price: "报价",
|
||||
};
|
||||
export const category = (s: string) => pick(CATEGORY, s);
|
||||
|
||||
/** 界面显示为 ★ / ▲ / 一般(一般不显示符号) */
|
||||
export const IMPORTANCE_MARK: Dict<Schemas["Importance"]> = { substantive: "★", important: "▲", normal: "" };
|
||||
export const IMPORTANCE_NAME: Dict<Schemas["Importance"]> = { substantive: "实质性要求", important: "重要", normal: "一般" };
|
||||
|
||||
export const PENDING_REASON: Dict<Schemas["PendingReason"]> = {
|
||||
no_citation: "没找到可靠出处",
|
||||
low_confidence: "把握不大",
|
||||
verify_failed: "出处核对没通过",
|
||||
marker_uncovered: "标注句没被提取覆盖,由规则补出",
|
||||
absent_in_rerun: "重新解析时没再出现",
|
||||
};
|
||||
export const pendingReason = (s: string | null | undefined) => pick(PENDING_REASON, s);
|
||||
|
||||
export const MARKER: Dict<Schemas["MarkingRule"]["marker"]> = {
|
||||
star: "★",
|
||||
asterisk: "*",
|
||||
triangle: "▲",
|
||||
italic_underline: "斜体加下划线",
|
||||
bold: "加粗",
|
||||
underline: "下划线",
|
||||
keyword: "关键词",
|
||||
};
|
||||
|
||||
export const QUOTA_UNIT: Dict<Schemas["QuotaUnit"]> = { parse: "解析", check: "检查", generate: "生成", dedupe: "查重" };
|
||||
export const quotaUnit = (s: string) => pick(QUOTA_UNIT, s);
|
||||
|
||||
export const PLAN_KEY: Dict<Schemas["PlanCode"]> = { free: "free", pro: "pro", enterprise: "ent" };
|
||||
|
||||
/** 工作台「下一步」。target 为空表示骨架阶段还没有对应页面,只显示文字。 */
|
||||
export const NEXT_ACTION: Dict<Schemas["NextAction"]> = {
|
||||
upload_tender: "上传招标文件",
|
||||
wait_parse: "查看解析进度",
|
||||
select_lots: "选择所投的包",
|
||||
confirm_checklist: "确认清单",
|
||||
upload_bid: "上传投标文件",
|
||||
run_check: "发起检查",
|
||||
handle_fatal: "处理致命问题",
|
||||
export_report: "导出检查报告",
|
||||
archive: "归档项目",
|
||||
none: "查看项目",
|
||||
};
|
||||
export const nextAction = (s: string) => pick(NEXT_ACTION, s);
|
||||
|
||||
/** 解析阶段(§7.11):服务端的 8 个 stage 合成界面上的 5 步 */
|
||||
export const PARSE_STEPS: readonly { label: string; stages: readonly string[] }[] = [
|
||||
{ label: "识别文件格式", stages: ["detect", "convert"] },
|
||||
{ label: "提取文字", stages: ["text_layer", "ocr"] },
|
||||
{ label: "识别章节结构", stages: ["structure"] },
|
||||
{ label: "提取清单", stages: ["extract"] },
|
||||
{ label: "核对出处", stages: ["verify", "merge"] },
|
||||
];
|
||||
export const parseStepIndex = (stage: string | null | undefined) =>
|
||||
stage ? PARSE_STEPS.findIndex((s) => s.stages.includes(stage)) : -1;
|
||||
|
||||
export const READY_TIP = "只表示致命问题都已处理,不代表投标文件没有问题。";
|
||||
|
||||
/* ---------- 时间 ---------- */
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
export const fmtDate = (d: Date) => `${d.getMonth() + 1}月${d.getDate()}日`;
|
||||
export const fmtTime = (d: Date) => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
export const fmtDateTime = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
return `${fmtDate(d)} ${fmtTime(d)}`;
|
||||
};
|
||||
export const fmtDuration = (ms: number) => {
|
||||
const s = Math.max(0, Math.round(ms / 1000));
|
||||
return { min: Math.floor(s / 60), sec: pad(s % 60) };
|
||||
};
|
||||
|
||||
/** 投标截止的两行显示和颜色类(components.css 的 .due-*) */
|
||||
export function deadline(iso: string | null | undefined, now = new Date()): { main: string; sub: string; cls: string } | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
const ms = d.getTime() - now.getTime();
|
||||
const when = `${fmtDate(d)} ${fmtTime(d)}`;
|
||||
if (ms < 0) return { main: "已截止", sub: when, cls: "due-past" };
|
||||
const hours = Math.floor(ms / 3_600_000);
|
||||
const days = Math.floor(hours / 24);
|
||||
if (d.toDateString() === now.toDateString()) return { main: `今天 ${fmtTime(d)} 截止`, sub: `剩 ${hours} 小时`, cls: "due-today" };
|
||||
if (hours <= 72) return { main: `剩 ${days} 天 ${hours % 24} 小时`, sub: when, cls: "due-soon" };
|
||||
return { main: `剩 ${days} 天`, sub: when, cls: "" };
|
||||
}
|
||||
|
||||
export function extOf(name: string): string {
|
||||
const m = /\.([a-z0-9]{1,5})$/i.exec(name);
|
||||
return m?.[1] ? m[1].toUpperCase() : "文件";
|
||||
}
|
||||
|
||||
export function fmtSize(bytes: number): string {
|
||||
if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
Reference in New Issue
Block a user