feat(ui): 查标第 6 步前端补齐
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled

- 读标页:每条清单项写最近一次检查的结论,点投标侧出处看投标原文;
  「已响应」也能反馈判断有误(漏报);证书要求写资质库里的情况
  (新接口 GET /projects/{id}/qualification-matches,资质库空时不写)
- 概览:检查记录卡(历次检查、各级问题数、报告下载)、文件卡
- 工作台:「致命」列、统计可点筛选、待处理的致命问题卡(?f= 定位到问题)
- 日期输入加日历弹层(DateInput,Reka UI Calendar)
- 查标页:? 键显示快捷键说明;反馈对话框抽成 FeedbackDialog 共用
- 扫描件提示去掉多余空格

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-05 13:39:19 +08:00
parent 808259da87
commit 54cbb556d0
30 changed files with 956 additions and 143 deletions

View File

@@ -0,0 +1,112 @@
<script setup lang="ts">
/**
* 日期输入(设计规范 §7.22、§9.3):可以直接键入(接受 2026/11/20、2026年11月20日 等,离开时整理成 2026-11-20),
* 右侧按钮打开日期选择器。选择器周一开始;今天 1px 边框,选中墨底白字;非本月的格子留空;不可选的日期只变灰。
* 交互基础用 Reka UI 的 Popover + Calendar(键盘方向键选日期、Esc 关闭、焦点回到按钮),外观来自 app.css 的 .dt / .cal。
*/
import { getLocalTimeZone, parseDate, today, type DateValue } from "@internationalized/date";
import { Calendar, ChevronLeft, ChevronRight } from "@lucide/vue";
import {
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNext,
CalendarPrev,
CalendarRoot,
PopoverAnchor,
PopoverContent,
PopoverPortal,
PopoverRoot,
PopoverTrigger,
} from "reka-ui";
import { computed, ref } from "vue";
import { parseDay } from "@/lib/labels";
const props = defineProps<{ id?: string; placeholder?: string; disabled?: boolean; label?: string }>();
const text = defineModel<string>({ required: true });
const emit = defineEmits<{ blur: [] }>();
const open = ref(false);
const input = ref<HTMLInputElement | null>(null);
defineExpose({ select: () => input.value?.select(), focus: () => input.value?.focus() });
/** 输入框里能认出的日期,选择器打开时停在这个月 */
const value = computed<DateValue | undefined>(() => {
const d = parseDay(text.value);
return d ? parseDate(d) : undefined;
});
function pick(v: DateValue | undefined) {
if (!v) return;
text.value = v.toString();
open.value = false;
}
function onBlur() {
const d = parseDay(text.value);
if (d) text.value = d;
emit("blur");
}
</script>
<template>
<PopoverRoot v-model:open="open">
<PopoverAnchor as-child>
<div class="dt">
<input
:id="id"
ref="input"
v-model="text"
class="input"
:placeholder="placeholder ?? '例如:2026-11-20'"
:disabled="disabled"
autocomplete="off"
@blur="onBlur"
/>
<PopoverTrigger as-child>
<button type="button" class="icon-btn sm" :aria-label="`打开日期选择器${label ? `:${label}` : ''}`" :disabled="disabled">
<Calendar class="ic" :stroke-width="1.5" />
</button>
</PopoverTrigger>
</div>
</PopoverAnchor>
<PopoverPortal>
<PopoverContent class="cal" align="start" :side-offset="4" :aria-label="label ?? '选择日期'">
<CalendarRoot
v-slot="{ grid, weekDays }"
:model-value="value"
:default-placeholder="value ?? today(getLocalTimeZone())"
locale="zh-CN"
:week-starts-on="1"
weekday-format="narrow"
initial-focus
@update:model-value="pick"
>
<CalendarHeader class="cal-h">
<CalendarPrev class="icon-btn sm" aria-label="上个月"><ChevronLeft class="ic" :stroke-width="1.5" /></CalendarPrev>
<CalendarHeading v-slot="{ headingValue }"><b>{{ headingValue }}</b></CalendarHeading>
<CalendarNext class="icon-btn sm" aria-label="下个月"><ChevronRight class="ic" :stroke-width="1.5" /></CalendarNext>
</CalendarHeader>
<CalendarGrid v-for="month in grid" :key="month.value.toString()" class="cal-grid">
<CalendarGridHead>
<CalendarGridRow>
<CalendarHeadCell v-for="d in weekDays" :key="d" class="wd">{{ d }}</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow v-for="(week, i) in month.rows" :key="i">
<CalendarCell v-for="d in week" :key="d.toString()" :date="d">
<CalendarCellTrigger :day="d" :month="month.value" class="day" />
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</CalendarRoot>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>