再次提交一下代码

This commit is contained in:
andy committed 2026-07-17 13:13:47 +07:00
1 parent f4d86248d4
commit 980a3a2535
41 files changed
+1699 -187

No files matched your search

@@ -49,21 +49,27 @@
<ReservationStatusBadge :status="detail.task_status" />
</header>
<section class="task-evidence">
<h2>{{ t('task.evidence') }}</h2>
<dl>
<section class="task-source-email">
<header>
<div>
<dt>{{ t('task.sourceMessageId') }}</dt>
<dd class="th-code">
{{ detail.source_message_id || '-' }}
</dd>
</div>
<div>
<dt>{{ t('task.sourceSubject') }}</dt>
<dd>
{{ detail.source_subject ?? t('task.sourceSummaryPending') }}
</dd>
<p>{{ t('task.sourceEmailPreview') }}</p>
<h2>{{ detail.source_subject ?? t('task.sourceSummaryPending') }}</h2>
</div>
<span
v-if="!detail.source_message_id"
class="task-link task-link--disabled"
>
{{ t('task.sourceEmailFullConversation') }} · {{ t('common.endpointMissing') }}
</span>
<RouterLink
v-else
class="task-link"
:to="`/reservation/source-messages/${detail.source_message_id}/conversation`"
>
{{ t('task.sourceEmailFullConversation') }}
</RouterLink>
</header>
<dl class="task-source-email__meta">
<div>
<dt>{{ t('task.sourceSender') }}</dt>
<dd>{{ detail.source_sender_summary ?? '-' }}</dd>
@@ -72,53 +78,41 @@
<dt>{{ t('task.sourceReceivedAt') }}</dt>
<dd>{{ formatReservationDateTime(detail.source_received_at) }}</dd>
</div>
</dl>
</section>
<section class="task-route-info">
<h2>{{ t('task.routeInfo') }}</h2>
<dl>
<div>
<dt>{{ t('task.systemTaskType') }}</dt>
<dd>
{{ formatReservationTaskCard(t, detail.system_task_type) }}
<small class="th-code">{{ detail.system_task_type }}</small>
</dd>
</div>
<div>
<dt>{{ t('task.taskCardType') }}</dt>
<dd>
{{ formatReservationTaskCardLabel(t, detail) }}
<small class="th-code">{{ detail.task_card_type }}</small>
</dd>
</div>
<div>
<dt>{{ t('task.resultType') }}</dt>
<dd>{{ detail.result_type ? formatReservationResultType(t, detail.result_type) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.aiTaskType') }}</dt>
<dd>{{ detail.ai_task_type || '-' }}</dd>
</div>
<div>
<dt>{{ t('task.taskSubtype') }}</dt>
<dd>{{ detail.task_subtype ? formatReservationRouteCode(t, detail.task_subtype) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.routeCode') }}</dt>
<dt>{{ t('task.sourceMessageId') }}</dt>
<dd class="th-code">
{{ detail.route_code || '-' }}
{{ detail.source_message_id || '-' }}
</dd>
</div>
<div>
<dt>{{ t('task.systemProcessCategory') }}</dt>
<dd>{{ detail.system_process_category ? formatReservationSystemProcessCategory(t, detail.system_process_category) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.reviewStatus') }}</dt>
<dd>{{ detail.review_status || '-' }}</dd>
<dt>{{ t('conversation.messageCount') }}</dt>
<dd>{{ detail.conversation_message_count ?? '-' }}</dd>
</div>
</dl>
<div class="task-source-email__content">
<section>
<h3>{{ t('task.sourceEmailExcerpt') }}</h3>
<p>{{ sourceEmailExcerpt ?? t('task.sourceEmailNoExcerpt') }}</p>
</section>
<section v-if="sourceEmailAttachments.length">
<h3>{{ t('task.sourceEmailAttachments') }}</h3>
<ul>
<li
v-for="item in sourceEmailAttachments"
:key="item.key"
>
<i
class="pi pi-paperclip"
aria-hidden="true"
/>
<span>
{{ item.name }}
<small v-if="item.meta">{{ item.meta }}</small>
</span>
</li>
</ul>
</section>
</div>
</section>
<section
@@ -396,6 +390,52 @@
/>
</aside>
</div>
<details class="task-route-info">
<summary>{{ t('task.routeInfo') }}</summary>
<dl>
<div>
<dt>{{ t('task.systemTaskType') }}</dt>
<dd>
{{ formatReservationTaskCard(t, detail.system_task_type) }}
<small class="th-code">{{ detail.system_task_type }}</small>
</dd>
</div>
<div>
<dt>{{ t('task.taskCardType') }}</dt>
<dd>
{{ formatReservationTaskCardLabel(t, detail) }}
<small class="th-code">{{ detail.task_card_type }}</small>
</dd>
</div>
<div>
<dt>{{ t('task.resultType') }}</dt>
<dd>{{ detail.result_type ? formatReservationResultType(t, detail.result_type) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.aiTaskType') }}</dt>
<dd>{{ detail.ai_task_type || '-' }}</dd>
</div>
<div>
<dt>{{ t('task.taskSubtype') }}</dt>
<dd>{{ detail.task_subtype ? formatReservationRouteCode(t, detail.task_subtype) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.routeCode') }}</dt>
<dd class="th-code">
{{ detail.route_code || '-' }}
</dd>
</div>
<div>
<dt>{{ t('task.systemProcessCategory') }}</dt>
<dd>{{ detail.system_process_category ? formatReservationSystemProcessCategory(t, detail.system_process_category) : '-' }}</dd>
</div>
<div>
<dt>{{ t('task.reviewStatus') }}</dt>
<dd>{{ detail.review_status || '-' }}</dd>
</div>
</dl>
</details>
</template>
</section>
</template>
@@ -430,6 +470,7 @@ import type {
import {
buildEditableFieldValues,
buildInitialFieldValues,
readReservationFieldValue,
validateReservationFieldValues,
type ReservationFieldValidationErrors,
} from '@/utils/reservationFieldRules'
@@ -481,6 +522,8 @@ const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceM
const manualReviewEvidence = computed(() => buildManualReviewEvidence(detail.value, manualReviewRecord.value))
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
const sourceEmailExcerpt = computed(() => readSourceEmailExcerpt(detail.value, fieldValues.value))
const sourceEmailAttachments = computed(() => readSourceEmailAttachments(detail.value, fieldValues.value))
const isFieldRendererReadOnly = computed(
() =>
Boolean(detail.value?.availability.read_only) ||
@@ -731,6 +774,175 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
}
}
interface SourceEmailAttachmentItem {
key: string
name: string
meta: string
}
const sourceEmailAttachmentPathTokens = ['attachment', 'attachments', 'file_reference', 'file_references']
function readSourceEmailExcerpt(
taskDetail: ReservationTaskDetailResult | null,
values: ReservationRecord,
): string | null {
if (!taskDetail) {
return null
}
const fieldExcerpt = stringifySourceEmailText(readFirstEvidenceFieldValue(taskDetail, values, ['relevant_message_excerpt']))
if (fieldExcerpt) {
return fieldExcerpt
}
return null
}
function readSourceEmailAttachments(
taskDetail: ReservationTaskDetailResult | null,
values: ReservationRecord,
): SourceEmailAttachmentItem[] {
if (!taskDetail) {
return []
}
const attachmentValues = taskDetail.fields
.filter((field) => isSourceEmailAttachmentField(field))
.map((field) => readReservationFieldValue(field, values))
const seen = new Set<string>()
return attachmentValues
.flatMap((value) => normalizeAttachmentValue(value))
.map((value, index) => toSourceEmailAttachmentItem(value, index))
.filter((item) => {
const key = `${item.name}-${item.meta}`
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
}
function readFirstEvidenceFieldValue(
taskDetail: ReservationTaskDetailResult,
values: ReservationRecord,
pathTokens: string[],
): unknown {
const field = taskDetail.fields.find((item) => {
const fieldPath = item.field_path.toLowerCase()
const pointer = item.field_pointer?.toLowerCase() ?? ''
return pathTokens.some((token) => fieldPath.includes(token) || pointer.includes(token.split('.').join('/')))
})
return field ? readReservationFieldValue(field, values) : null
}
function isSourceEmailAttachmentField(field: ReservationTaskDetailResult['fields'][number]): boolean {
const fieldPath = field.field_path.toLowerCase()
const pointer = field.field_pointer?.toLowerCase() ?? ''
return sourceEmailAttachmentPathTokens.some((token) => fieldPath.includes(token) || pointer.includes(token))
}
function stringifySourceEmailText(value: unknown): string | null {
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value)
}
return null
}
function normalizeAttachmentValue(value: unknown): unknown[] {
if (value === null || value === undefined || value === '') {
return []
}
if (Array.isArray(value)) {
return value.flatMap((item) => normalizeAttachmentValue(item))
}
if (typeof value === 'string') {
const trimmedValue = value.trim()
if (!trimmedValue) {
return []
}
const parsedValue = parseMaybeJson(trimmedValue)
return parsedValue === trimmedValue ? [trimmedValue] : normalizeAttachmentValue(parsedValue)
}
return [value]
}
function toSourceEmailAttachmentItem(value: unknown, index: number): SourceEmailAttachmentItem {
const record = readRecord(value)
const name = record
? readFirstString(record, ['name', 'file_name', 'filename', 'original_file_name', 'display_name', 'title'])
: stringifySourceEmailText(value)
const rawType = record ? readFirstString(record, ['content_type', 'mime_type', 'media_type', 'file_type', 'type']) : null
const size = record ? readFirstNumber(record, ['size_bytes', 'file_size', 'size']) : null
const typeLabel = formatAttachmentTypeLabel(name, rawType)
const sizeLabel = formatAttachmentSize(size)
const id = record ? readFirstString(record, ['id', 'media_id', 'file_id', 'attachment_id']) : null
const displayName = name || t('conversation.unnamedAttachment')
return {
key: id ? `${id}-${index}` : `${displayName}-${index}`,
name: displayName,
meta: [typeLabel, sizeLabel].filter(Boolean).join(' · '),
}
}
function parseMaybeJson(value: string): unknown {
if (!value.startsWith('{') && !value.startsWith('[')) {
return value
}
try {
return JSON.parse(value)
} catch {
return value
}
}
function readFirstNumber(record: ReservationRecord, keys: string[]): number | null {
for (const key of keys) {
const value = record[key]
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && Number.isFinite(Number(value))) {
return Number(value)
}
}
return null
}
function formatAttachmentTypeLabel(fileName: string | null, rawType: string | null): string {
const source = `${rawType ?? ''} ${fileName ?? ''}`.toLowerCase()
if (source.includes('pdf') || source.endsWith('.pdf')) {
return 'PDF'
}
if (source.includes('spreadsheet') || source.includes('excel') || /\.(xlsx|xls|csv)$/.test(source)) {
return 'Excel'
}
if (source.includes('word') || /\.(docx|doc)$/.test(source)) {
return 'Word'
}
if (source.includes('image') || /\.(png|jpe?g|gif|webp)$/.test(source)) {
return 'Image'
}
if (source.includes('message/rfc822') || source.endsWith('.eml')) {
return 'EML'
}
const extension = fileName?.match(/\.([a-z0-9]{2,8})$/i)?.[1]
return extension ? extension.toUpperCase() : ''
}
function formatAttachmentSize(size: number | null): string {
if (!size || size <= 0) {
return ''
}
if (size < 1024) {
return `${size} B`
}
if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)} KB`
}
return `${(size / 1024 / 1024).toFixed(1)} MB`
}
function formatJson(value: unknown): string {
return JSON.stringify(value ?? {}, null, 2)
}
@@ -921,7 +1133,7 @@ function formatStringList(items: string[]): string {
color: var(--th-color-slate-500);
}
.task-evidence,
.task-source-email,
.task-route-info,
.task-ai-diagnostics {
display: grid;
@@ -931,10 +1143,54 @@ function formatStringList(items: string[]): string {
padding: 16px;
}
.task-source-email {
gap: 16px;
background: var(--th-color-white);
}
.task-source-email header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.task-source-email header p,
.task-source-email header h2,
.task-source-email__content h3,
.task-source-email__content p,
.task-source-email__content ul {
margin: 0;
}
.task-source-email header p {
color: var(--th-color-blue-600);
font-size: 12px;
font-weight: 800;
}
.task-source-email header h2 {
margin-top: 6px;
color: var(--th-color-slate-900);
font-size: 18px;
line-height: 1.35;
}
.task-route-info {
background: var(--th-color-slate-50);
}
.task-route-info summary {
color: var(--th-color-slate-700);
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.task-route-info[open] summary {
margin-bottom: 10px;
}
.task-ai-diagnostics {
background: var(--th-color-white);
}
@@ -961,9 +1217,6 @@ function formatStringList(items: string[]): string {
padding: 16px;
}
.task-evidence h2,
.task-evidence dl,
.task-route-info h2,
.task-route-info dl,
.task-ai-diagnostics h2,
.task-ai-diagnostics pre,
@@ -977,8 +1230,6 @@ function formatStringList(items: string[]): string {
margin: 0;
}
.task-evidence h2,
.task-route-info h2,
.task-ai-diagnostics h2,
.manual-review-evidence h2,
.source-only-result h2 {
@@ -1010,7 +1261,7 @@ function formatStringList(items: string[]): string {
line-height: 1.6;
}
.task-evidence dl,
.task-source-email__meta,
.task-route-info dl,
.manual-review-evidence > dl {
display: grid;
@@ -1024,7 +1275,7 @@ function formatStringList(items: string[]): string {
gap: 12px;
}
.task-evidence dt,
.task-source-email__meta dt,
.task-route-info dt,
.manual-review-evidence > dl dt {
color: var(--th-color-slate-500);
@@ -1032,7 +1283,7 @@ function formatStringList(items: string[]): string {
font-weight: 700;
}
.task-evidence dd,
.task-source-email__meta dd,
.task-route-info dd,
.manual-review-evidence > dl dd,
.source-only-result dd {
@@ -1042,6 +1293,71 @@ function formatStringList(items: string[]): string {
overflow-wrap: anywhere;
}
.task-source-email__content {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
gap: 12px;
}
.task-source-email__content section {
display: grid;
gap: 8px;
border: 1px solid var(--th-color-slate-200);
border-radius: var(--th-radius-sm);
background: var(--th-color-slate-50);
padding: 12px;
}
.task-source-email__content section:only-child {
grid-column: 1 / -1;
}
.task-source-email__content h3 {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 800;
}
.task-source-email__content p {
color: var(--th-color-slate-800);
font-size: 13px;
line-height: 1.65;
white-space: pre-wrap;
}
.task-source-email__content ul {
display: grid;
gap: 8px;
padding: 0;
list-style: none;
}
.task-source-email__content li {
display: flex;
gap: 8px;
color: var(--th-color-slate-900);
font-size: 13px;
line-height: 1.5;
}
.task-source-email__content li i {
flex: 0 0 auto;
margin-top: 2px;
color: var(--th-color-blue-600);
}
.task-source-email__content li span {
min-width: 0;
overflow-wrap: anywhere;
}
.task-source-email__content li small {
display: block;
margin-top: 2px;
color: var(--th-color-slate-500);
font-size: 12px;
}
.manual-review-evidence__candidates {
display: grid;
gap: 10px;
@@ -1213,7 +1529,7 @@ function formatStringList(items: string[]): string {
grid-template-columns: 1fr;
}
.task-evidence dl,
.task-source-email__meta,
.task-route-info dl,
.manual-review-evidence > dl,
.candidate-grid {
@@ -1222,7 +1538,16 @@ function formatStringList(items: string[]): string {
}
@media (max-width: 640px) {
.task-evidence dl,
.task-source-email header,
.task-source-email__content {
grid-template-columns: 1fr;
}
.task-source-email header {
display: grid;
}
.task-source-email__meta,
.task-route-info dl,
.manual-review-evidence > dl,
.candidate-grid,