style(web): 更新差旅报销创建页面样式和业务脚本,增强前端交互和状态管理
This commit is contained in:
@@ -3102,6 +3102,25 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review-upload-decision-modal {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-upload-decision-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-upload-decision-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-upload-decision-actions .primary-dialog-btn,
|
||||||
|
.review-upload-decision-actions .secondary-dialog-btn {
|
||||||
|
flex: 1 1 168px;
|
||||||
|
}
|
||||||
|
|
||||||
.review-edit-modal {
|
.review-edit-modal {
|
||||||
max-height: min(860px, calc(100vh - 48px));
|
max-height: min(860px, calc(100vh - 48px));
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -3503,6 +3522,10 @@
|
|||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review-upload-decision-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.primary-dialog-btn,
|
.primary-dialog-btn,
|
||||||
.secondary-dialog-btn,
|
.secondary-dialog-btn,
|
||||||
.danger-dialog-btn {
|
.danger-dialog-btn {
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { useNavigation, navItems } from './useNavigation.js'
|
import { useNavigation, navItems } from './useNavigation.js'
|
||||||
import { useRequests } from './useRequests.js'
|
import { useRequests } from './useRequests.js'
|
||||||
|
import { useSystemState } from './useSystemState.js'
|
||||||
import { useToast } from './useToast.js'
|
import { useToast } from './useToast.js'
|
||||||
|
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||||
|
|
||||||
|
const SESSION_TYPE_EXPENSE = 'expense'
|
||||||
|
|
||||||
function isPlaceholderValue(value) {
|
function isPlaceholderValue(value) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -101,6 +105,7 @@ export function useAppShell() {
|
|||||||
rejectRequest,
|
rejectRequest,
|
||||||
reload: reloadRequests
|
reload: reloadRequests
|
||||||
} = useRequests()
|
} = useRequests()
|
||||||
|
const { currentUser } = useSystemState()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||||
@@ -179,7 +184,34 @@ export function useAppShell() {
|
|||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSmartEntry(payload = {}) {
|
function resolveCurrentUserId() {
|
||||||
|
const user = currentUser.value || {}
|
||||||
|
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSmartEntryConversation(payload = {}) {
|
||||||
|
if (payload.conversation) {
|
||||||
|
return payload.conversation
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.restoreLatestConversation) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
||||||
|
preferRecoverable: true
|
||||||
|
})
|
||||||
|
return latestPayload?.found ? latestPayload.conversation || null : null
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore latest expense conversation for smart entry:', error)
|
||||||
|
toast(error?.message || '恢复最近报销会话失败,请稍后重试。')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSmartEntry(payload = {}) {
|
||||||
|
const conversation = await resolveSmartEntryConversation(payload)
|
||||||
smartEntryOpen.value = true
|
smartEntryOpen.value = true
|
||||||
|
|
||||||
smartEntryContext.value = {
|
smartEntryContext.value = {
|
||||||
@@ -187,7 +219,7 @@ export function useAppShell() {
|
|||||||
source: payload.source ?? 'workbench',
|
source: payload.source ?? 'workbench',
|
||||||
request: payload.request ?? selectedRequest.value,
|
request: payload.request ?? selectedRequest.value,
|
||||||
files: Array.isArray(payload.files) ? payload.files : [],
|
files: Array.isArray(payload.files) ? payload.files : [],
|
||||||
conversation: payload.conversation ?? null
|
conversation
|
||||||
}
|
}
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,16 +264,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div v-if="resolveReviewPrimaryAction(message.reviewPayload) || message.draftPayload?.claim_no" class="review-footer-actions">
|
<div v-if="resolveReviewSubmitActions(message.reviewPayload).length || message.draftPayload?.claim_no" class="review-footer-actions">
|
||||||
<div class="review-footer-btn-row">
|
<div class="review-footer-btn-row">
|
||||||
<button
|
<button
|
||||||
v-if="resolveReviewPrimaryAction(message.reviewPayload)"
|
v-for="action in resolveReviewSubmitActions(message.reviewPayload)"
|
||||||
|
:key="`${message.id}-${action.action_type}`"
|
||||||
type="button"
|
type="button"
|
||||||
class="review-footer-btn primary"
|
:class="['review-footer-btn', action.emphasis === 'primary' ? 'primary' : '']"
|
||||||
:disabled="reviewActionBusy"
|
:disabled="reviewActionBusy"
|
||||||
@click="handleReviewAction(message, resolveReviewPrimaryAction(message.reviewPayload))"
|
@click="handleReviewAction(message, action)"
|
||||||
>
|
>
|
||||||
{{ buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
|
{{ action.label || buildReviewPrimaryButtonLabel(message.reviewPayload, message.draftPayload) }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -290,7 +291,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="review-footer-btn"
|
class="review-footer-btn"
|
||||||
:disabled="submitting || reviewActionBusy"
|
:disabled="submitting || reviewActionBusy"
|
||||||
@click="triggerFileUpload"
|
@click="triggerFileUpload(message.reviewPayload.document_cards?.length ? 'composer-continue' : 'composer')"
|
||||||
>
|
>
|
||||||
{{ message.reviewPayload.document_cards?.length ? '继续上传票据' : '上传票据' }}
|
{{ message.reviewPayload.document_cards?.length ? '继续上传票据' : '上传票据' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -599,6 +600,18 @@
|
|||||||
>
|
>
|
||||||
{{ scene }}
|
{{ scene }}
|
||||||
</button>
|
</button>
|
||||||
|
<input
|
||||||
|
v-if="reviewInlineForm.scene_label === REVIEW_SCENE_OTHER_OPTION"
|
||||||
|
v-model="reviewInlineForm.reason_value"
|
||||||
|
class="review-inline-input review-inline-select-custom"
|
||||||
|
:class="{ invalid: Boolean(reviewInlineErrors[item.key]) }"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入具体事由"
|
||||||
|
@click.stop
|
||||||
|
@input="clearInlineReviewFieldError(item.key)"
|
||||||
|
@blur="commitInlineReviewEditor"
|
||||||
|
@keydown.enter.prevent="commitInlineReviewEditor"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<strong v-else :title="item.value">{{ item.value }}</strong>
|
<strong v-else :title="item.value">{{ item.value }}</strong>
|
||||||
@@ -905,6 +918,30 @@
|
|||||||
@confirm="confirmCancelReview"
|
@confirm="confirmCancelReview"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Transition name="assistant-modal">
|
||||||
|
<div v-if="uploadDecisionDialogOpen" class="assistant-overlay review-overlay">
|
||||||
|
<section class="review-confirm-modal review-upload-decision-modal">
|
||||||
|
<div class="review-upload-decision-copy">
|
||||||
|
<span class="assistant-badge">上传票据</span>
|
||||||
|
<h3>检测到你已有单据事件</h3>
|
||||||
|
<p>这次新上传的附件需要先确认处理方式。你可以继续归集到上一笔单据,也可以重新开启一张新单据。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="review-confirm-actions review-upload-decision-actions">
|
||||||
|
<button type="button" class="primary-dialog-btn" :disabled="submitting || reviewActionBusy" @click="continueExistingUpload">
|
||||||
|
继续
|
||||||
|
</button>
|
||||||
|
<button type="button" class="secondary-dialog-btn" :disabled="submitting || reviewActionBusy" @click="createNewUploadDocument">
|
||||||
|
新单据
|
||||||
|
</button>
|
||||||
|
<button type="button" class="secondary-dialog-btn" :disabled="submitting || reviewActionBusy" @click="closeUploadDecisionDialog">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<Transition name="assistant-modal">
|
<Transition name="assistant-modal">
|
||||||
<div v-if="documentPreviewDialog.open" class="assistant-overlay review-overlay">
|
<div v-if="documentPreviewDialog.open" class="assistant-overlay review-overlay">
|
||||||
<section class="review-preview-modal">
|
<section class="review-preview-modal">
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import { useSystemState } from '../../composables/useSystemState.js'
|
|||||||
import { useToast } from '../../composables/useToast.js'
|
import { useToast } from '../../composables/useToast.js'
|
||||||
import { recognizeOcrFiles } from '../../services/ocr.js'
|
import { recognizeOcrFiles } from '../../services/ocr.js'
|
||||||
import { clearUserConversations, deleteConversation, fetchLatestConversation, runOrchestrator } from '../../services/orchestrator.js'
|
import { clearUserConversations, deleteConversation, fetchLatestConversation, runOrchestrator } from '../../services/orchestrator.js'
|
||||||
|
import {
|
||||||
|
fetchExpenseClaimAttachmentAsset,
|
||||||
|
fetchExpenseClaimDetail,
|
||||||
|
fetchExpenseClaimItemAttachmentMeta,
|
||||||
|
uploadExpenseClaimItemAttachment
|
||||||
|
} from '../../services/reimbursements.js'
|
||||||
|
|
||||||
const aiAvatar = '/assets/header.png'
|
const aiAvatar = '/assets/header.png'
|
||||||
const userAvatar = '/assets/person.png'
|
const userAvatar = '/assets/person.png'
|
||||||
@@ -155,7 +161,8 @@ const REVIEW_OTHER_CATEGORY_OPTIONS = [
|
|||||||
{ key: 'other', label: '其他费用' }
|
{ key: 'other', label: '其他费用' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const REVIEW_SCENE_OPTIONS = ['请客户吃饭', '出差行程', '住宿报销', '交通出行', '会务活动', '其他场景']
|
const REVIEW_SCENE_OTHER_OPTION = '其他场景'
|
||||||
|
const REVIEW_SCENE_OPTIONS = ['请客户吃饭', '出差行程', '住宿报销', '交通出行', '会务活动', REVIEW_SCENE_OTHER_OPTION]
|
||||||
const DATE_INPUT_FORMAT = 'YYYY-MM-DD'
|
const DATE_INPUT_FORMAT = 'YYYY-MM-DD'
|
||||||
const MAX_ATTACHMENTS = 10
|
const MAX_ATTACHMENTS = 10
|
||||||
const MAX_OCR_DOCUMENTS = 10
|
const MAX_OCR_DOCUMENTS = 10
|
||||||
@@ -332,6 +339,9 @@ function normalizeOcrDocuments(payload) {
|
|||||||
document_type_label: String(item.document_type_label || '').trim(),
|
document_type_label: String(item.document_type_label || '').trim(),
|
||||||
scene_code: String(item.scene_code || 'other').trim() || 'other',
|
scene_code: String(item.scene_code || 'other').trim() || 'other',
|
||||||
scene_label: String(item.scene_label || '').trim(),
|
scene_label: String(item.scene_label || '').trim(),
|
||||||
|
preview_kind: String(item.preview_kind || '').trim(),
|
||||||
|
preview_data_url: String(item.preview_data_url || '').trim(),
|
||||||
|
preview_url: String(item.preview_url || '').trim(),
|
||||||
document_fields: Array.isArray(item.document_fields)
|
document_fields: Array.isArray(item.document_fields)
|
||||||
? item.document_fields
|
? item.document_fields
|
||||||
.map((field) => ({
|
.map((field) => ({
|
||||||
@@ -346,11 +356,129 @@ function normalizeOcrDocuments(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildOcrSummary(payload) {
|
function buildOcrSummary(payload) {
|
||||||
const parts = normalizeOcrDocuments(payload)
|
return buildOcrSummaryFromDocuments(normalizeOcrDocuments(payload))
|
||||||
.map((item) => `${item.filename}:${item.summary || item.text}`)
|
}
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
return parts.join(';')
|
function buildOcrSummaryFromDocuments(documents) {
|
||||||
|
return (Array.isArray(documents) ? documents : [])
|
||||||
|
.slice(0, MAX_OCR_DOCUMENTS)
|
||||||
|
.map((item) => {
|
||||||
|
const filename = String(item?.filename || '').trim()
|
||||||
|
const summary = String(item?.summary || item?.text || '').trim()
|
||||||
|
if (filename && summary) {
|
||||||
|
return `${filename}:${summary}`
|
||||||
|
}
|
||||||
|
return filename || summary
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReviewDocumentFieldKey(label) {
|
||||||
|
const compact = String(label || '').replace(/\s+/g, '').toLowerCase()
|
||||||
|
if (!compact) return ''
|
||||||
|
if (
|
||||||
|
['金额', '价税合计', '合计', '总额', '总计', '票价', '支付金额', '实付金额', '实收金额'].some((token) =>
|
||||||
|
compact.includes(token.toLowerCase())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 'amount'
|
||||||
|
}
|
||||||
|
if (['日期', '时间', '开票日期', '发生时间'].some((token) => compact.includes(token.toLowerCase()))) {
|
||||||
|
return 'date'
|
||||||
|
}
|
||||||
|
if (['商户', '酒店', '销售方', '开票方', '收款方'].some((token) => compact.includes(token.toLowerCase()))) {
|
||||||
|
return 'merchant_name'
|
||||||
|
}
|
||||||
|
if (['票据号码', '发票号码', '票号', '单号', '订单号'].some((token) => compact.includes(token.toLowerCase()))) {
|
||||||
|
return 'invoice_number'
|
||||||
|
}
|
||||||
|
if (compact.includes('发票代码')) {
|
||||||
|
return 'invoice_code'
|
||||||
|
}
|
||||||
|
if (compact.includes('车次') || compact.includes('航班')) {
|
||||||
|
return 'trip_no'
|
||||||
|
}
|
||||||
|
if (compact.includes('行程') || compact.includes('路线')) {
|
||||||
|
return 'route'
|
||||||
|
}
|
||||||
|
return compact
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOcrDocumentsFromReviewPayload(reviewPayload) {
|
||||||
|
const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : []
|
||||||
|
return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => {
|
||||||
|
const fields = Array.isArray(item?.fields)
|
||||||
|
? item.fields
|
||||||
|
.map((field) => {
|
||||||
|
const label = String(field?.label || '').trim()
|
||||||
|
const value = String(field?.value || '').trim()
|
||||||
|
if (!label || !value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: normalizeReviewDocumentFieldKey(label),
|
||||||
|
label,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: String(item?.filename || '').trim(),
|
||||||
|
summary: String(item?.summary || '').trim(),
|
||||||
|
text: [
|
||||||
|
String(item?.scene_label || '').trim(),
|
||||||
|
String(item?.summary || '').trim(),
|
||||||
|
...fields.map((field) => `${field.label}:${field.value}`)
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
.slice(0, 240),
|
||||||
|
avg_score: Number(item?.avg_score || 0),
|
||||||
|
document_type: String(item?.document_type || 'other').trim() || 'other',
|
||||||
|
document_type_label: resolveDocumentTypeLabel(item?.document_type),
|
||||||
|
scene_code: resolveExpenseTypeCode(item?.suggested_expense_type),
|
||||||
|
scene_label: String(item?.scene_label || '').trim(),
|
||||||
|
document_fields: fields,
|
||||||
|
warnings: Array.isArray(item?.warnings) ? item.warnings : []
|
||||||
|
}
|
||||||
|
}).filter((item) => item.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeUploadAttachmentNames(existingNames, incomingNames) {
|
||||||
|
const merged = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
for (const value of [...(existingNames || []), ...(incomingNames || [])]) {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
if (!normalized || seen.has(normalized)) continue
|
||||||
|
seen.add(normalized)
|
||||||
|
merged.push(normalized)
|
||||||
|
if (merged.length >= MAX_ATTACHMENTS) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeUploadOcrDocuments(existingDocuments, incomingDocuments) {
|
||||||
|
const merged = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
for (const item of [...(existingDocuments || []), ...(incomingDocuments || [])]) {
|
||||||
|
const filename = String(item?.filename || '').trim()
|
||||||
|
if (!filename || seen.has(filename)) continue
|
||||||
|
seen.add(filename)
|
||||||
|
merged.push(item)
|
||||||
|
if (merged.length >= MAX_OCR_DOCUMENTS) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferPreviewKind(file) {
|
function inferPreviewKind(file) {
|
||||||
@@ -457,11 +585,46 @@ function buildOcrFilePreviews(payload) {
|
|||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
filename: String(item?.filename || '').trim(),
|
filename: String(item?.filename || '').trim(),
|
||||||
kind: String(item?.preview_kind || '').trim(),
|
kind: String(item?.preview_kind || '').trim(),
|
||||||
url: String(item?.preview_data_url || '').trim()
|
url: String(item?.preview_url || item?.preview_data_url || '').trim()
|
||||||
}))
|
}))
|
||||||
.filter((item) => item.filename && item.kind === 'image' && item.url)
|
.filter((item) => item.filename && item.kind === 'image' && item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildReviewFilePreviewsFromReviewPayload(reviewPayload) {
|
||||||
|
const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : []
|
||||||
|
return documents
|
||||||
|
.map((item) => ({
|
||||||
|
filename: String(item?.filename || '').trim(),
|
||||||
|
kind: String(item?.preview_kind || '').trim(),
|
||||||
|
url: String(item?.preview_url || item?.preview_data_url || '').trim()
|
||||||
|
}))
|
||||||
|
.filter((item) => item.filename && item.kind === 'image' && item.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReviewFilePreviewsFromMessages(messages) {
|
||||||
|
const previews = []
|
||||||
|
for (const message of Array.isArray(messages) ? messages : []) {
|
||||||
|
previews.push(...buildReviewFilePreviewsFromReviewPayload(message?.reviewPayload))
|
||||||
|
}
|
||||||
|
return mergeFilePreviews([], previews)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAttachmentPreviewKind(metadata) {
|
||||||
|
const explicitKind = String(metadata?.preview_kind || '').trim()
|
||||||
|
if (explicitKind) {
|
||||||
|
return explicitKind
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaType = String(metadata?.media_type || '').trim().toLowerCase()
|
||||||
|
if (mediaType.startsWith('image/')) {
|
||||||
|
return 'image'
|
||||||
|
}
|
||||||
|
if (mediaType === 'application/pdf') {
|
||||||
|
return 'pdf'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function extractReviewAttachmentNames(reviewPayload) {
|
function extractReviewAttachmentNames(reviewPayload) {
|
||||||
const documentNames = Array.isArray(reviewPayload?.document_cards)
|
const documentNames = Array.isArray(reviewPayload?.document_cards)
|
||||||
? reviewPayload.document_cards.map((item) => String(item?.filename || '').trim()).filter(Boolean)
|
? reviewPayload.document_cards.map((item) => String(item?.filename || '').trim()).filter(Boolean)
|
||||||
@@ -504,6 +667,8 @@ function buildReviewDocumentDrafts(reviewPayload) {
|
|||||||
confidenceLabel: String(item.confidenceLabel || '').trim(),
|
confidenceLabel: String(item.confidenceLabel || '').trim(),
|
||||||
documentTypeLabel: String(item.documentTypeLabel || '').trim(),
|
documentTypeLabel: String(item.documentTypeLabel || '').trim(),
|
||||||
expenseTypeLabel: String(item.expenseTypeLabel || '').trim(),
|
expenseTypeLabel: String(item.expenseTypeLabel || '').trim(),
|
||||||
|
preview_kind: String(item.preview_kind || '').trim(),
|
||||||
|
preview_data_url: String(item.preview_data_url || '').trim(),
|
||||||
warnings: Array.isArray(item.warnings) ? [...item.warnings] : [],
|
warnings: Array.isArray(item.warnings) ? [...item.warnings] : [],
|
||||||
fields: Array.isArray(item.fields)
|
fields: Array.isArray(item.fields)
|
||||||
? item.fields.map((field) => ({
|
? item.fields.map((field) => ({
|
||||||
@@ -648,7 +813,11 @@ function buildInitialInsightFromConversation(conversation) {
|
|||||||
const attachmentNames = Array.isArray(messageJson?.attachment_names)
|
const attachmentNames = Array.isArray(messageJson?.attachment_names)
|
||||||
? messageJson.attachment_names.filter(Boolean)
|
? messageJson.attachment_names.filter(Boolean)
|
||||||
: []
|
: []
|
||||||
return buildAgentInsight(orchestratorPayload, attachmentNames, [])
|
return buildAgentInsight(
|
||||||
|
orchestratorPayload,
|
||||||
|
attachmentNames,
|
||||||
|
buildReviewFilePreviewsFromReviewPayload(orchestratorPayload?.result?.review_payload)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1322,6 +1491,13 @@ function resolveReviewPrimaryAction(reviewPayload) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveReviewSubmitActions(reviewPayload) {
|
||||||
|
return (Array.isArray(reviewPayload?.confirmation_actions) ? reviewPayload.confirmation_actions : []).filter((item) => {
|
||||||
|
const actionType = String(item?.action_type || '').trim()
|
||||||
|
return actionType && !['cancel_review', 'edit_review'].includes(actionType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function resolveReviewEditAction(reviewPayload) {
|
function resolveReviewEditAction(reviewPayload) {
|
||||||
return (
|
return (
|
||||||
(Array.isArray(reviewPayload?.confirmation_actions) ? reviewPayload.confirmation_actions : []).find(
|
(Array.isArray(reviewPayload?.confirmation_actions) ? reviewPayload.confirmation_actions : []).find(
|
||||||
@@ -1339,6 +1515,12 @@ function buildReviewPrimaryButtonLabel(reviewPayload, draftPayload) {
|
|||||||
if (action.action_type === 'next_step') {
|
if (action.action_type === 'next_step') {
|
||||||
return '继续下一步'
|
return '继续下一步'
|
||||||
}
|
}
|
||||||
|
if (action.action_type === 'link_to_existing_draft') {
|
||||||
|
return action.label || '关联到现有草稿'
|
||||||
|
}
|
||||||
|
if (action.action_type === 'create_new_claim_from_documents') {
|
||||||
|
return action.label || '单独建立报销单'
|
||||||
|
}
|
||||||
return action.label || '确认'
|
return action.label || '确认'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1471,7 +1653,7 @@ function buildReviewFactCards(reviewPayload, inlineState = createEmptyInlineRevi
|
|||||||
{
|
{
|
||||||
key: 'scene',
|
key: 'scene',
|
||||||
label: '场景 / 事由',
|
label: '场景 / 事由',
|
||||||
value: String(inlineState.scene_label || '').trim() || '待补充',
|
value: String(inlineState.reason_value || inlineState.scene_label || '').trim() || '待补充',
|
||||||
icon: 'mdi mdi-silverware-fork-knife',
|
icon: 'mdi mdi-silverware-fork-knife',
|
||||||
editor: 'select',
|
editor: 'select',
|
||||||
modelKey: 'scene_label',
|
modelKey: 'scene_label',
|
||||||
@@ -2003,6 +2185,7 @@ export default {
|
|||||||
const conversationId = ref(initialSessionState.conversationId)
|
const conversationId = ref(initialSessionState.conversationId)
|
||||||
const draftClaimId = ref(initialSessionState.draftClaimId)
|
const draftClaimId = ref(initialSessionState.draftClaimId)
|
||||||
const previewRegistry = []
|
const previewRegistry = []
|
||||||
|
const restoredDraftPreviewClaims = new Set()
|
||||||
const reviewFilePreviews = ref(initialSessionState.reviewFilePreviews)
|
const reviewFilePreviews = ref(initialSessionState.reviewFilePreviews)
|
||||||
const sessionSnapshots = ref({
|
const sessionSnapshots = ref({
|
||||||
[SESSION_TYPE_EXPENSE]: null,
|
[SESSION_TYPE_EXPENSE]: null,
|
||||||
@@ -2012,6 +2195,7 @@ export default {
|
|||||||
const currentInsight = ref(initialSessionState.currentInsight)
|
const currentInsight = ref(initialSessionState.currentInsight)
|
||||||
const reviewCancelDialogOpen = ref(false)
|
const reviewCancelDialogOpen = ref(false)
|
||||||
const reviewEditDialogOpen = ref(false)
|
const reviewEditDialogOpen = ref(false)
|
||||||
|
const uploadDecisionDialogOpen = ref(false)
|
||||||
const deleteSessionDialogOpen = ref(false)
|
const deleteSessionDialogOpen = ref(false)
|
||||||
const reviewActionBusy = ref(false)
|
const reviewActionBusy = ref(false)
|
||||||
const deleteSessionBusy = ref(false)
|
const deleteSessionBusy = ref(false)
|
||||||
@@ -2024,6 +2208,7 @@ export default {
|
|||||||
const reviewInlineEditorKey = ref('')
|
const reviewInlineEditorKey = ref('')
|
||||||
const reviewInlineErrors = ref({})
|
const reviewInlineErrors = ref({})
|
||||||
const reviewOtherCategoryOpen = ref(false)
|
const reviewOtherCategoryOpen = ref(false)
|
||||||
|
const composerUploadIntent = ref(String(initialSessionState.composerUploadIntent || '').trim())
|
||||||
const reviewDocumentDrafts = ref([])
|
const reviewDocumentDrafts = ref([])
|
||||||
const reviewDocumentBaseDrafts = ref([])
|
const reviewDocumentBaseDrafts = ref([])
|
||||||
const activeReviewDocumentIndex = ref(0)
|
const activeReviewDocumentIndex = ref(0)
|
||||||
@@ -2149,7 +2334,18 @@ export default {
|
|||||||
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
|
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
|
||||||
const activeReviewDocumentPreview = computed(() =>
|
const activeReviewDocumentPreview = computed(() =>
|
||||||
activeReviewDocument.value
|
activeReviewDocument.value
|
||||||
? resolveDocumentPreview(activeReviewFilePreviews.value, activeReviewDocument.value.filename)
|
? (
|
||||||
|
resolveDocumentPreview(activeReviewFilePreviews.value, activeReviewDocument.value.filename)
|
||||||
|
|| (
|
||||||
|
activeReviewDocument.value.preview_kind === 'image' && activeReviewDocument.value.preview_data_url
|
||||||
|
? {
|
||||||
|
filename: activeReviewDocument.value.filename,
|
||||||
|
kind: activeReviewDocument.value.preview_kind,
|
||||||
|
url: activeReviewDocument.value.preview_data_url
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
)
|
||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
const canPreviewActiveReviewDocument = computed(() => Boolean(activeReviewDocumentPreview.value?.url))
|
const canPreviewActiveReviewDocument = computed(() => Boolean(activeReviewDocumentPreview.value?.url))
|
||||||
@@ -2174,6 +2370,7 @@ export default {
|
|||||||
const sessionType = resolveInitialSessionType(conversation) || fallbackSessionType
|
const sessionType = resolveInitialSessionType(conversation) || fallbackSessionType
|
||||||
const restoredMessages = normalizeInitialConversationMessages(conversation)
|
const restoredMessages = normalizeInitialConversationMessages(conversation)
|
||||||
const initialInsight = buildInitialInsightFromConversation(conversation)
|
const initialInsight = buildInitialInsightFromConversation(conversation)
|
||||||
|
const restoredReviewFilePreviews = buildReviewFilePreviewsFromMessages(restoredMessages)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionType,
|
sessionType,
|
||||||
@@ -2183,10 +2380,11 @@ export default {
|
|||||||
conversationId: resolveInitialConversationId(conversation),
|
conversationId: resolveInitialConversationId(conversation),
|
||||||
draftClaimId: resolveInitialDraftClaimId(conversation),
|
draftClaimId: resolveInitialDraftClaimId(conversation),
|
||||||
currentInsight: initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType),
|
currentInsight: initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType),
|
||||||
reviewFilePreviews: [],
|
reviewFilePreviews: restoredReviewFilePreviews,
|
||||||
composerDraft: '',
|
composerDraft: '',
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
composerFilesExpanded: false,
|
composerFilesExpanded: false,
|
||||||
|
composerUploadIntent: '',
|
||||||
insightPanelCollapsed: false
|
insightPanelCollapsed: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2202,6 +2400,7 @@ export default {
|
|||||||
composerDraft: '',
|
composerDraft: '',
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
composerFilesExpanded: false,
|
composerFilesExpanded: false,
|
||||||
|
composerUploadIntent: '',
|
||||||
insightPanelCollapsed: false
|
insightPanelCollapsed: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2222,6 +2421,7 @@ export default {
|
|||||||
composerDraft: composerDraft.value,
|
composerDraft: composerDraft.value,
|
||||||
attachedFiles: attachedFiles.value,
|
attachedFiles: attachedFiles.value,
|
||||||
composerFilesExpanded: composerFilesExpanded.value,
|
composerFilesExpanded: composerFilesExpanded.value,
|
||||||
|
composerUploadIntent: composerUploadIntent.value,
|
||||||
insightPanelCollapsed: insightPanelCollapsed.value
|
insightPanelCollapsed: insightPanelCollapsed.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2239,7 +2439,9 @@ export default {
|
|||||||
composerDraft.value = String(nextState.composerDraft || '')
|
composerDraft.value = String(nextState.composerDraft || '')
|
||||||
attachedFiles.value = Array.isArray(nextState.attachedFiles) ? nextState.attachedFiles : []
|
attachedFiles.value = Array.isArray(nextState.attachedFiles) ? nextState.attachedFiles : []
|
||||||
composerFilesExpanded.value = Boolean(nextState.composerFilesExpanded)
|
composerFilesExpanded.value = Boolean(nextState.composerFilesExpanded)
|
||||||
|
composerUploadIntent.value = String(nextState.composerUploadIntent || '').trim()
|
||||||
insightPanelCollapsed.value = Boolean(nextState.insightPanelCollapsed)
|
insightPanelCollapsed.value = Boolean(nextState.insightPanelCollapsed)
|
||||||
|
uploadDecisionDialogOpen.value = false
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
adjustComposerTextareaHeight()
|
adjustComposerTextareaHeight()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
@@ -2247,7 +2449,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadLatestSessionState(targetSessionType) {
|
async function loadLatestSessionState(targetSessionType) {
|
||||||
const payload = await fetchLatestConversation(resolveCurrentUserId(), targetSessionType)
|
const payload = await fetchLatestConversation(resolveCurrentUserId(), targetSessionType, {
|
||||||
|
preferRecoverable: targetSessionType === SESSION_TYPE_EXPENSE
|
||||||
|
})
|
||||||
if (payload?.found && payload.conversation) {
|
if (payload?.found && payload.conversation) {
|
||||||
return buildConversationSessionState(payload.conversation, targetSessionType)
|
return buildConversationSessionState(payload.conversation, targetSessionType)
|
||||||
}
|
}
|
||||||
@@ -2307,6 +2511,7 @@ export default {
|
|||||||
watch(
|
watch(
|
||||||
() => activeReviewPayload.value,
|
() => activeReviewPayload.value,
|
||||||
(payload) => {
|
(payload) => {
|
||||||
|
rememberFilePreviews(buildReviewFilePreviewsFromReviewPayload(payload))
|
||||||
const nextInlineState = buildInlineReviewState(payload)
|
const nextInlineState = buildInlineReviewState(payload)
|
||||||
reviewInlineForm.value = { ...nextInlineState }
|
reviewInlineForm.value = { ...nextInlineState }
|
||||||
reviewInlineBaseForm.value = { ...nextInlineState }
|
reviewInlineBaseForm.value = { ...nextInlineState }
|
||||||
@@ -2360,6 +2565,17 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [activeSessionType.value, resolveActiveClaimId()],
|
||||||
|
([sessionType, claimId]) => {
|
||||||
|
if (sessionType !== SESSION_TYPE_EXPENSE || !claimId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void restorePersistedDraftAttachmentPreviews(claimId)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void clearKnowledgeSessionOnEntry()
|
void clearKnowledgeSessionOnEntry()
|
||||||
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
|
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
|
||||||
@@ -2420,6 +2636,128 @@ export default {
|
|||||||
reviewFilePreviews.value = mergeFilePreviews(reviewFilePreviews.value, filePreviews)
|
reviewFilePreviews.value = mergeFilePreviews(reviewFilePreviews.value, filePreviews)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackPreviewObjectUrl(url) {
|
||||||
|
if (!url || !String(url).startsWith('blob:')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
previewRegistry.push(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveActiveClaimId() {
|
||||||
|
return String(draftClaimId.value || linkedRequest.value?.claimId || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildPersistedAttachmentPreview(metadata) {
|
||||||
|
const filename = String(metadata?.file_name || '').trim()
|
||||||
|
const kind = resolveAttachmentPreviewKind(metadata)
|
||||||
|
const previewPath = String(metadata?.preview_url || '').trim()
|
||||||
|
if (!filename || !kind || !previewPath) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await fetchExpenseClaimAttachmentAsset(previewPath)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
trackPreviewObjectUrl(url)
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
kind,
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restorePersistedDraftAttachmentPreviews(claimId, options = {}) {
|
||||||
|
const normalizedClaimId = String(claimId || '').trim()
|
||||||
|
if (!normalizedClaimId || isKnowledgeSession.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const force = Boolean(options.force)
|
||||||
|
if (!force && restoredDraftPreviewClaims.has(normalizedClaimId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const claim = await fetchExpenseClaimDetail(normalizedClaimId)
|
||||||
|
const items = Array.isArray(claim?.items) ? claim.items : []
|
||||||
|
const previews = []
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemId = String(item?.id || '').trim()
|
||||||
|
if (!itemId) continue
|
||||||
|
|
||||||
|
let metadata = null
|
||||||
|
try {
|
||||||
|
metadata = await fetchExpenseClaimItemAttachmentMeta(normalizedClaimId, itemId)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = String(metadata?.file_name || '').trim()
|
||||||
|
if (!metadata?.previewable || !filename || resolveDocumentPreview(reviewFilePreviews.value, filename)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const preview = await buildPersistedAttachmentPreview(metadata)
|
||||||
|
if (preview) {
|
||||||
|
previews.push(preview)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load persisted attachment preview:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previews.length) {
|
||||||
|
rememberFilePreviews(previews)
|
||||||
|
}
|
||||||
|
restoredDraftPreviewClaims.add(normalizedClaimId)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore persisted draft attachment previews:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncComposerFilesToDraft(claimId, files) {
|
||||||
|
const normalizedClaimId = String(claimId || '').trim()
|
||||||
|
if (!normalizedClaimId || !Array.isArray(files) || !files.length || isKnowledgeSession.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const claim = await fetchExpenseClaimDetail(normalizedClaimId)
|
||||||
|
const items = Array.isArray(claim?.items) ? claim.items : []
|
||||||
|
const exactMatchBuckets = new Map()
|
||||||
|
const placeholderQueue = []
|
||||||
|
const usedItemIds = new Set()
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemId = String(item?.id || '').trim()
|
||||||
|
const invoiceId = String(item?.invoiceId || item?.invoice_id || '').trim()
|
||||||
|
if (!itemId) continue
|
||||||
|
if (invoiceId && !invoiceId.includes('/')) {
|
||||||
|
placeholderQueue.push(item)
|
||||||
|
}
|
||||||
|
if (!invoiceId) continue
|
||||||
|
const bucket = exactMatchBuckets.get(invoiceId) || []
|
||||||
|
bucket.push(item)
|
||||||
|
exactMatchBuckets.set(invoiceId, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const exactBucket = exactMatchBuckets.get(file.name) || []
|
||||||
|
const nextExactMatch = exactBucket.find((item) => !usedItemIds.has(String(item?.id || '').trim()))
|
||||||
|
const fallbackMatch = placeholderQueue.find((item) => !usedItemIds.has(String(item?.id || '').trim()))
|
||||||
|
const targetItem = nextExactMatch || fallbackMatch
|
||||||
|
const targetItemId = String(targetItem?.id || '').trim()
|
||||||
|
if (!targetItemId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
usedItemIds.add(targetItemId)
|
||||||
|
await uploadExpenseClaimItemAttachment(normalizedClaimId, targetItemId, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
await restorePersistedDraftAttachmentPreviews(normalizedClaimId, { force: true })
|
||||||
|
}
|
||||||
|
|
||||||
function replaceMessage(messageId, nextMessage) {
|
function replaceMessage(messageId, nextMessage) {
|
||||||
const index = messages.value.findIndex((item) => item.id === messageId)
|
const index = messages.value.findIndex((item) => item.id === messageId)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
@@ -2471,6 +2809,9 @@ export default {
|
|||||||
|
|
||||||
const mergeResult = mergeFilesWithLimit(attachedFiles.value, files, MAX_ATTACHMENTS)
|
const mergeResult = mergeFilesWithLimit(attachedFiles.value, files, MAX_ATTACHMENTS)
|
||||||
attachedFiles.value = mergeResult.files
|
attachedFiles.value = mergeResult.files
|
||||||
|
if (fileInputMode.value === 'composer-continue' && files.length) {
|
||||||
|
composerUploadIntent.value = 'continue_existing'
|
||||||
|
}
|
||||||
if (mergeResult.overflowCount > 0) {
|
if (mergeResult.overflowCount > 0) {
|
||||||
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
||||||
}
|
}
|
||||||
@@ -2495,16 +2836,45 @@ export default {
|
|||||||
if (attachedFiles.value.length <= VISIBLE_ATTACHMENT_CHIPS) {
|
if (attachedFiles.value.length <= VISIBLE_ATTACHMENT_CHIPS) {
|
||||||
composerFilesExpanded.value = false
|
composerFilesExpanded.value = false
|
||||||
}
|
}
|
||||||
|
if (!attachedFiles.value.length) {
|
||||||
|
composerUploadIntent.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAttachedFiles() {
|
function clearAttachedFiles() {
|
||||||
attachedFiles.value = []
|
attachedFiles.value = []
|
||||||
composerFilesExpanded.value = false
|
composerFilesExpanded.value = false
|
||||||
|
composerUploadIntent.value = ''
|
||||||
if (fileInputRef.value) {
|
if (fileInputRef.value) {
|
||||||
fileInputRef.value.value = ''
|
fileInputRef.value.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeUploadDecisionDialog() {
|
||||||
|
if (submitting.value || reviewActionBusy.value) return
|
||||||
|
uploadDecisionDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function continueExistingUpload() {
|
||||||
|
if (submitting.value || reviewActionBusy.value) return
|
||||||
|
uploadDecisionDialogOpen.value = false
|
||||||
|
composerUploadIntent.value = 'continue_existing'
|
||||||
|
await submitComposer({
|
||||||
|
uploadDisposition: 'continue_existing',
|
||||||
|
skipUploadDecisionPrompt: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNewUploadDocument() {
|
||||||
|
if (submitting.value || reviewActionBusy.value) return
|
||||||
|
uploadDecisionDialogOpen.value = false
|
||||||
|
composerUploadIntent.value = ''
|
||||||
|
await submitComposer({
|
||||||
|
uploadDisposition: 'new_document',
|
||||||
|
skipUploadDecisionPrompt: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function runShortcut(shortcut) {
|
async function runShortcut(shortcut) {
|
||||||
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
|
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
|
||||||
await switchSessionType(shortcut.targetSessionType)
|
await switchSessionType(shortcut.targetSessionType)
|
||||||
@@ -2611,6 +2981,20 @@ export default {
|
|||||||
expense_type: String(reviewInlineForm.value.expense_type || '').trim()
|
expense_type: String(reviewInlineForm.value.expense_type || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
activeEditorKey === 'scene' &&
|
||||||
|
nextForm.scene_label === REVIEW_SCENE_OTHER_OPTION
|
||||||
|
) {
|
||||||
|
nextForm.reason_value = String(reviewInlineForm.value.reason_value || '').trim()
|
||||||
|
if (!nextForm.reason_value) {
|
||||||
|
setInlineReviewFieldError('scene', '请选择“其他场景”后,请补充具体事由')
|
||||||
|
reviewInlineForm.value = nextForm
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (activeEditorKey === 'scene') {
|
||||||
|
nextForm.reason_value = nextForm.scene_label
|
||||||
|
}
|
||||||
|
|
||||||
if (activeEditorKey === 'occurred_date' && nextForm.occurred_date && !isValidIsoDateString(nextForm.occurred_date)) {
|
if (activeEditorKey === 'occurred_date' && nextForm.occurred_date && !isValidIsoDateString(nextForm.occurred_date)) {
|
||||||
setInlineReviewFieldError('occurred_date', `请输入正确的时间格式:${DATE_INPUT_FORMAT}`)
|
setInlineReviewFieldError('occurred_date', `请输入正确的时间格式:${DATE_INPUT_FORMAT}`)
|
||||||
return false
|
return false
|
||||||
@@ -2635,13 +3019,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectInlineScene(scene) {
|
function selectInlineScene(scene) {
|
||||||
|
const normalizedScene = String(scene || '').trim()
|
||||||
reviewInlineForm.value = {
|
reviewInlineForm.value = {
|
||||||
...reviewInlineForm.value,
|
...reviewInlineForm.value,
|
||||||
scene_label: String(scene || '').trim(),
|
scene_label: normalizedScene,
|
||||||
reason_value: String(scene || '').trim()
|
reason_value:
|
||||||
|
normalizedScene === REVIEW_SCENE_OTHER_OPTION
|
||||||
|
? ''
|
||||||
|
: normalizedScene
|
||||||
}
|
}
|
||||||
|
clearInlineReviewFieldError('scene')
|
||||||
|
if (normalizedScene !== REVIEW_SCENE_OTHER_OPTION) {
|
||||||
reviewInlineEditorKey.value = ''
|
reviewInlineEditorKey.value = ''
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectReviewCategory(option) {
|
function selectReviewCategory(option) {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
@@ -2671,7 +3062,8 @@ export default {
|
|||||||
if (!normalized || submitting.value || reviewActionBusy.value) return
|
if (!normalized || submitting.value || reviewActionBusy.value) return
|
||||||
submitComposer({
|
submitComposer({
|
||||||
rawText: `查看报销草稿 ${normalized} 的当前信息`,
|
rawText: `查看报销草稿 ${normalized} 的当前信息`,
|
||||||
userText: `查看草稿 ${normalized}`
|
userText: `查看草稿 ${normalized}`,
|
||||||
|
systemGenerated: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2679,7 +3071,8 @@ export default {
|
|||||||
if (!activeReviewPayload.value || submitting.value || reviewActionBusy.value) return
|
if (!activeReviewPayload.value || submitting.value || reviewActionBusy.value) return
|
||||||
submitComposer({
|
submitComposer({
|
||||||
rawText: '请解释一下当前这笔报销的合规风险和待补充项。',
|
rawText: '请解释一下当前这笔报销的合规风险和待补充项。',
|
||||||
userText: '查看全部风险项'
|
userText: '查看全部风险项',
|
||||||
|
systemGenerated: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2702,10 +3095,8 @@ export default {
|
|||||||
|
|
||||||
function closeDocumentPreview() {
|
function closeDocumentPreview() {
|
||||||
documentPreviewDialog.value = {
|
documentPreviewDialog.value = {
|
||||||
open: false,
|
...documentPreviewDialog.value,
|
||||||
filename: '',
|
open: false
|
||||||
kind: 'file',
|
|
||||||
url: ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2804,6 +3195,7 @@ export default {
|
|||||||
),
|
),
|
||||||
pendingText: '正在保存修改并刷新右侧核对信息...',
|
pendingText: '正在保存修改并刷新右侧核对信息...',
|
||||||
files: reviewInlinePendingFiles.value,
|
files: reviewInlinePendingFiles.value,
|
||||||
|
systemGenerated: true,
|
||||||
extraContext: {
|
extraContext: {
|
||||||
review_action: 'edit_review',
|
review_action: 'edit_review',
|
||||||
review_form_values: buildReviewFormValues(fields),
|
review_form_values: buildReviewFormValues(fields),
|
||||||
@@ -2861,6 +3253,10 @@ export default {
|
|||||||
if (sessionSwitchBusy.value) return null
|
if (sessionSwitchBusy.value) return null
|
||||||
|
|
||||||
const rawText = String(options.rawText ?? composerDraft.value).trim()
|
const rawText = String(options.rawText ?? composerDraft.value).trim()
|
||||||
|
const systemGenerated = Boolean(options.systemGenerated)
|
||||||
|
const resolvedUploadDisposition =
|
||||||
|
String(options.uploadDisposition || '').trim() ||
|
||||||
|
(composerUploadIntent.value === 'continue_existing' ? 'continue_existing' : '')
|
||||||
const normalizedFiles = isKnowledgeSession.value ? [] : Array.from(options.files ?? attachedFiles.value)
|
const normalizedFiles = isKnowledgeSession.value ? [] : Array.from(options.files ?? attachedFiles.value)
|
||||||
const fileMergeResult = mergeFilesWithLimit([], normalizedFiles, MAX_ATTACHMENTS)
|
const fileMergeResult = mergeFilesWithLimit([], normalizedFiles, MAX_ATTACHMENTS)
|
||||||
const files = fileMergeResult.files
|
const files = fileMergeResult.files
|
||||||
@@ -2869,6 +3265,25 @@ export default {
|
|||||||
}
|
}
|
||||||
if (!rawText && !files.length) return
|
if (!rawText && !files.length) return
|
||||||
|
|
||||||
|
const extraContext = options.extraContext && typeof options.extraContext === 'object'
|
||||||
|
? { ...options.extraContext }
|
||||||
|
: {}
|
||||||
|
const reviewAttachmentNames = extractReviewAttachmentNames(activeReviewPayload.value)
|
||||||
|
const hasExistingDocumentEvent =
|
||||||
|
Boolean(String(draftClaimId.value || '').trim()) || reviewAttachmentNames.length > 0
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isKnowledgeSession.value &&
|
||||||
|
files.length &&
|
||||||
|
hasExistingDocumentEvent &&
|
||||||
|
!resolvedUploadDisposition &&
|
||||||
|
!options.skipUploadDecisionPrompt &&
|
||||||
|
!String(extraContext.review_action || '').trim()
|
||||||
|
) {
|
||||||
|
uploadDecisionDialogOpen.value = true
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const fileNames = files.map((file) => file.name)
|
const fileNames = files.map((file) => file.name)
|
||||||
const filePreviews = buildFilePreviews(files, previewRegistry)
|
const filePreviews = buildFilePreviews(files, previewRegistry)
|
||||||
rememberFilePreviews(filePreviews)
|
rememberFilePreviews(filePreviews)
|
||||||
@@ -2877,10 +3292,11 @@ export default {
|
|||||||
rawText ||
|
rawText ||
|
||||||
(isKnowledgeSession.value
|
(isKnowledgeSession.value
|
||||||
? `我上传了 ${fileNames.length} 份附件,请帮我回答相关财务问题。`
|
? `我上传了 ${fileNames.length} 份附件,请帮我回答相关财务问题。`
|
||||||
|
: resolvedUploadDisposition === 'continue_existing'
|
||||||
|
? `继续上传 ${fileNames.length} 份票据,并归集到当前单据。`
|
||||||
|
: resolvedUploadDisposition === 'new_document'
|
||||||
|
? `新上传 ${fileNames.length} 份票据,请单独建立报销单。`
|
||||||
: `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`)
|
: `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`)
|
||||||
const extraContext = options.extraContext && typeof options.extraContext === 'object'
|
|
||||||
? options.extraContext
|
|
||||||
: {}
|
|
||||||
|
|
||||||
// 只有在非静默模式下才添加用户消息
|
// 只有在非静默模式下才添加用户消息
|
||||||
if (!options.skipUserMessage) {
|
if (!options.skipUserMessage) {
|
||||||
@@ -2928,7 +3344,23 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendMessage = buildBackendMessage(rawText, fileNames, ocrSummary)
|
let effectiveFileNames = [...fileNames]
|
||||||
|
let effectiveOcrDocuments = [...ocrDocuments]
|
||||||
|
let effectiveOcrSummary = ocrSummary
|
||||||
|
|
||||||
|
if (resolvedUploadDisposition === 'continue_existing') {
|
||||||
|
extraContext.review_action = 'link_to_existing_draft'
|
||||||
|
effectiveFileNames = mergeUploadAttachmentNames(reviewAttachmentNames, fileNames)
|
||||||
|
effectiveOcrDocuments = mergeUploadOcrDocuments(
|
||||||
|
buildOcrDocumentsFromReviewPayload(activeReviewPayload.value),
|
||||||
|
ocrDocuments
|
||||||
|
)
|
||||||
|
effectiveOcrSummary = buildOcrSummaryFromDocuments(effectiveOcrDocuments)
|
||||||
|
} else if (resolvedUploadDisposition === 'new_document') {
|
||||||
|
extraContext.review_action = 'create_new_claim_from_documents'
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendMessage = buildBackendMessage(rawText, effectiveFileNames, effectiveOcrSummary)
|
||||||
const payload = await runOrchestrator({
|
const payload = await runOrchestrator({
|
||||||
source: 'user_message',
|
source: 'user_message',
|
||||||
user_id: user.username || user.name || 'anonymous',
|
user_id: user.username || user.name || 'anonymous',
|
||||||
@@ -2942,11 +3374,12 @@ export default {
|
|||||||
...buildClientTimeContext(),
|
...buildClientTimeContext(),
|
||||||
session_type: activeSessionType.value,
|
session_type: activeSessionType.value,
|
||||||
entry_source: props.entrySource,
|
entry_source: props.entrySource,
|
||||||
attachment_names: fileNames,
|
user_input_text: systemGenerated ? '' : rawText,
|
||||||
attachment_count: fileNames.length,
|
attachment_names: effectiveFileNames,
|
||||||
|
attachment_count: effectiveFileNames.length,
|
||||||
draft_claim_id: isKnowledgeSession.value ? undefined : draftClaimId.value || undefined,
|
draft_claim_id: isKnowledgeSession.value ? undefined : draftClaimId.value || undefined,
|
||||||
ocr_summary: ocrSummary,
|
ocr_summary: effectiveOcrSummary,
|
||||||
ocr_documents: ocrDocuments,
|
ocr_documents: effectiveOcrDocuments,
|
||||||
...(linkedRequest.value && !isKnowledgeSession.value ? { request_context: linkedRequest.value } : {}),
|
...(linkedRequest.value && !isKnowledgeSession.value ? { request_context: linkedRequest.value } : {}),
|
||||||
...extraContext
|
...extraContext
|
||||||
}
|
}
|
||||||
@@ -2959,10 +3392,20 @@ export default {
|
|||||||
? ''
|
? ''
|
||||||
: String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
|
: String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
|
||||||
|
|
||||||
|
const resolvedDraftClaimId = String(payload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
|
||||||
|
if (!isKnowledgeSession.value && resolvedDraftClaimId && files.length) {
|
||||||
|
try {
|
||||||
|
await syncComposerFilesToDraft(resolvedDraftClaimId, files)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to persist composer attachments to draft claim:', error)
|
||||||
|
toast(error?.message || '票据已识别,但附件持久化失败,请重试上传。')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
replaceMessage(
|
replaceMessage(
|
||||||
pendingMessage.id,
|
pendingMessage.id,
|
||||||
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
|
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
|
||||||
meta: buildMessageMeta(payload, fileNames),
|
meta: buildMessageMeta(payload, effectiveFileNames),
|
||||||
citations: Array.isArray(payload?.result?.citations) ? payload.result.citations : [],
|
citations: Array.isArray(payload?.result?.citations) ? payload.result.citations : [],
|
||||||
suggestedActions: Array.isArray(payload?.result?.suggested_actions)
|
suggestedActions: Array.isArray(payload?.result?.suggested_actions)
|
||||||
? payload.result.suggested_actions
|
? payload.result.suggested_actions
|
||||||
@@ -2975,7 +3418,7 @@ export default {
|
|||||||
)
|
)
|
||||||
currentInsight.value = buildAgentInsight(
|
currentInsight.value = buildAgentInsight(
|
||||||
payload,
|
payload,
|
||||||
fileNames,
|
effectiveFileNames,
|
||||||
mergeFilePreviews(filePreviews, ocrFilePreviews)
|
mergeFilePreviews(filePreviews, ocrFilePreviews)
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2993,6 +3436,7 @@ export default {
|
|||||||
currentInsight.value = buildErrorInsight(error, fileNames)
|
currentInsight.value = buildErrorInsight(error, fileNames)
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
|
composerUploadIntent.value = ''
|
||||||
nextTick(scrollToBottom)
|
nextTick(scrollToBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3039,6 +3483,7 @@ export default {
|
|||||||
rawText: buildReviewCorrectionMessage(fields),
|
rawText: buildReviewCorrectionMessage(fields),
|
||||||
userText: '我已修改识别信息,请按最新内容更新。',
|
userText: '我已修改识别信息,请按最新内容更新。',
|
||||||
pendingText: '正在根据修改内容重新识别...',
|
pendingText: '正在根据修改内容重新识别...',
|
||||||
|
systemGenerated: true,
|
||||||
extraContext: {
|
extraContext: {
|
||||||
review_action: 'edit_review',
|
review_action: 'edit_review',
|
||||||
review_form_values: buildReviewFormValues(fields)
|
review_form_values: buildReviewFormValues(fields)
|
||||||
@@ -3064,7 +3509,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['save_draft', 'next_step'].includes(actionType)) {
|
if (!['save_draft', 'next_step', 'link_to_existing_draft', 'create_new_claim_from_documents'].includes(actionType)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3072,13 +3517,11 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存草稿直接处理,不显示对话
|
if (['save_draft', 'link_to_existing_draft', 'create_new_claim_from_documents'].includes(actionType)) {
|
||||||
if (actionType === 'save_draft') {
|
await handleSaveDraftDirectly(message, actionType)
|
||||||
await handleSaveDraftDirectly(message)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下一步继续使用对话流程
|
|
||||||
reviewActionBusy.value = true
|
reviewActionBusy.value = true
|
||||||
try {
|
try {
|
||||||
const baseFields = reviewInlineBaseFields.value.length
|
const baseFields = reviewInlineBaseFields.value.length
|
||||||
@@ -3109,6 +3552,7 @@ export default {
|
|||||||
userText: reviewChangedUserText || '我确认当前识别结果,继续下一步。',
|
userText: reviewChangedUserText || '我确认当前识别结果,继续下一步。',
|
||||||
files: reviewInlinePendingFiles.value,
|
files: reviewInlinePendingFiles.value,
|
||||||
pendingText: '正在进入下一步...',
|
pendingText: '正在进入下一步...',
|
||||||
|
systemGenerated: true,
|
||||||
extraContext: {
|
extraContext: {
|
||||||
review_action: actionType,
|
review_action: actionType,
|
||||||
review_form_values: buildReviewFormValues(fields),
|
review_form_values: buildReviewFormValues(fields),
|
||||||
@@ -3133,12 +3577,48 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:直接保存草稿的函数,不显示对话
|
async function handleSaveDraftDirectly(message, actionType = 'save_draft') {
|
||||||
async function handleSaveDraftDirectly(message) {
|
|
||||||
reviewActionBusy.value = true
|
reviewActionBusy.value = true
|
||||||
|
let savingMessage = null
|
||||||
|
|
||||||
// 记录当前消息数量,用于后续移除 submitComposer 添加的消息
|
const actionConfig = {
|
||||||
const messageCountBefore = messages.value.length
|
save_draft: {
|
||||||
|
rawText: '请按当前已识别信息先保存草稿,缺失字段后续再补。',
|
||||||
|
pendingText: '正在保存当前草稿...',
|
||||||
|
helperText: '正在保存草稿...',
|
||||||
|
successMeta: '草稿已保存',
|
||||||
|
successMessage: (payload) => {
|
||||||
|
const claimNo = String(payload?.result?.draft_payload?.claim_no || '').trim()
|
||||||
|
return claimNo ? `草稿已保存,单号:${claimNo}` : '草稿保存完成'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
link_to_existing_draft: {
|
||||||
|
rawText: '请把当前上传的票据合并到现有报销草稿中。',
|
||||||
|
pendingText: '正在关联到现有草稿...',
|
||||||
|
helperText: '正在关联现有草稿...',
|
||||||
|
successMeta: '已关联草稿',
|
||||||
|
successMessage: (payload) => {
|
||||||
|
const claimNo = String(payload?.result?.draft_payload?.claim_no || '').trim()
|
||||||
|
return claimNo ? `已关联到草稿 ${claimNo}` : '已关联到现有草稿'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create_new_claim_from_documents: {
|
||||||
|
rawText: '请基于当前上传的多张票据,单独建立一张新的报销草稿。',
|
||||||
|
pendingText: '正在建立新的报销草稿...',
|
||||||
|
helperText: '正在建立新报销草稿...',
|
||||||
|
successMeta: '新草稿已建立',
|
||||||
|
successMessage: (payload) => {
|
||||||
|
const claimNo = String(payload?.result?.draft_payload?.claim_no || '').trim()
|
||||||
|
return claimNo ? `已建立新草稿 ${claimNo}` : '已建立新的报销草稿'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[actionType] || {
|
||||||
|
rawText: '请按当前已识别信息先保存草稿,缺失字段后续再补。',
|
||||||
|
pendingText: '正在保存当前草稿...',
|
||||||
|
helperText: '正在保存草稿...',
|
||||||
|
successMeta: '草稿已保存',
|
||||||
|
successMessage: () => '草稿保存完成'
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseFields = reviewInlineBaseFields.value.length
|
const baseFields = reviewInlineBaseFields.value.length
|
||||||
@@ -3146,36 +3626,33 @@ export default {
|
|||||||
: cloneReviewEditFields(message?.reviewPayload?.edit_fields)
|
: cloneReviewEditFields(message?.reviewPayload?.edit_fields)
|
||||||
const fields = mergeInlineReviewFields(baseFields, reviewInlineForm.value)
|
const fields = mergeInlineReviewFields(baseFields, reviewInlineForm.value)
|
||||||
|
|
||||||
// 先显示一个临时的"正在保存"消息
|
savingMessage = createMessage('assistant', actionConfig.helperText, [], { meta: ['处理中'] })
|
||||||
const savingMessage = createMessage('assistant', '正在保存草稿...', [], { meta: ['处理中'] })
|
|
||||||
messages.value.push(savingMessage)
|
messages.value.push(savingMessage)
|
||||||
nextTick(scrollToBottom)
|
nextTick(scrollToBottom)
|
||||||
|
|
||||||
// 调用保存逻辑,不通过对话
|
|
||||||
const payload = await submitComposer({
|
const payload = await submitComposer({
|
||||||
rawText: '请按当前已识别信息先保存草稿,缺失字段后续再补。',
|
rawText: actionConfig.rawText,
|
||||||
userText: '', // 不显示用户消息
|
userText: '',
|
||||||
skipUserMessage: true, // 跳过添加用户消息
|
skipUserMessage: true,
|
||||||
files: reviewInlinePendingFiles.value,
|
files: reviewInlinePendingFiles.value,
|
||||||
pendingText: '正在保存当前草稿...',
|
pendingText: actionConfig.pendingText,
|
||||||
|
systemGenerated: true,
|
||||||
extraContext: {
|
extraContext: {
|
||||||
review_action: 'save_draft',
|
review_action: actionType,
|
||||||
review_form_values: buildReviewFormValues(fields),
|
review_form_values: buildReviewFormValues(fields),
|
||||||
review_document_form_values: buildReviewDocumentCorrectionContext(reviewDocumentDrafts.value)
|
review_document_form_values: buildReviewDocumentCorrectionContext(reviewDocumentDrafts.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 移除临时消息
|
|
||||||
const tempIndex = messages.value.findIndex((msg) => msg === savingMessage)
|
const tempIndex = messages.value.findIndex((msg) => msg === savingMessage)
|
||||||
if (tempIndex !== -1) {
|
if (tempIndex !== -1) {
|
||||||
messages.value.splice(tempIndex, 1)
|
messages.value.splice(tempIndex, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload?.result?.draft_payload?.claim_no) {
|
if (payload?.result?.draft_payload?.claim_no) {
|
||||||
// 显示保存成功的消息
|
|
||||||
messages.value.push(
|
messages.value.push(
|
||||||
createMessage('assistant', `✅ 草稿已保存,单号:${payload.result.draft_payload.claim_no}`, [], {
|
createMessage('assistant', actionConfig.successMessage(payload), [], {
|
||||||
meta: ['草稿已保存']
|
meta: [actionConfig.successMeta]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3190,19 +3667,18 @@ export default {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 没有返回草稿信息,可能保存失败
|
messages.value.push(createMessage('assistant', actionConfig.successMessage(payload), [], { meta: [actionConfig.successMeta] }))
|
||||||
messages.value.push(createMessage('assistant', '草稿保存完成', [], { meta: ['草稿已保存'] }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(scrollToBottom)
|
nextTick(scrollToBottom)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 移除临时消息
|
if (savingMessage) {
|
||||||
const tempIndex = messages.value.findIndex((msg) => msg === savingMessage)
|
const tempIndex = messages.value.findIndex((msg) => msg === savingMessage)
|
||||||
if (tempIndex !== -1) {
|
if (tempIndex !== -1) {
|
||||||
messages.value.splice(tempIndex, 1)
|
messages.value.splice(tempIndex, 1)
|
||||||
}
|
}
|
||||||
// 显示错误消息
|
}
|
||||||
messages.value.push(createMessage('assistant', '❌ 保存草稿失败,请稍后重试。', [], { meta: ['错误'] }))
|
messages.value.push(createMessage('assistant', '保存失败,请稍后重试。', [], { meta: ['错误'] }))
|
||||||
nextTick(scrollToBottom)
|
nextTick(scrollToBottom)
|
||||||
} finally {
|
} finally {
|
||||||
reviewActionBusy.value = false
|
reviewActionBusy.value = false
|
||||||
@@ -3265,6 +3741,7 @@ export default {
|
|||||||
reviewOtherCategoryOpen,
|
reviewOtherCategoryOpen,
|
||||||
reviewInlinePendingFiles,
|
reviewInlinePendingFiles,
|
||||||
DATE_INPUT_FORMAT,
|
DATE_INPUT_FORMAT,
|
||||||
|
REVIEW_SCENE_OTHER_OPTION,
|
||||||
REVIEW_SCENE_OPTIONS,
|
REVIEW_SCENE_OPTIONS,
|
||||||
REVIEW_OTHER_CATEGORY_OPTIONS,
|
REVIEW_OTHER_CATEGORY_OPTIONS,
|
||||||
reviewPanelConfidence,
|
reviewPanelConfidence,
|
||||||
@@ -3281,6 +3758,7 @@ export default {
|
|||||||
reviewHasUnsavedChanges,
|
reviewHasUnsavedChanges,
|
||||||
reviewCancelDialogOpen,
|
reviewCancelDialogOpen,
|
||||||
reviewEditDialogOpen,
|
reviewEditDialogOpen,
|
||||||
|
uploadDecisionDialogOpen,
|
||||||
deleteSessionDialogOpen,
|
deleteSessionDialogOpen,
|
||||||
reviewActionBusy,
|
reviewActionBusy,
|
||||||
deleteSessionBusy,
|
deleteSessionBusy,
|
||||||
@@ -3300,6 +3778,7 @@ export default {
|
|||||||
buildReviewTodoSectionMeta,
|
buildReviewTodoSectionMeta,
|
||||||
buildReviewAlertChips,
|
buildReviewAlertChips,
|
||||||
buildReviewTodoItems,
|
buildReviewTodoItems,
|
||||||
|
resolveReviewSubmitActions,
|
||||||
resolveReviewPrimaryAction,
|
resolveReviewPrimaryAction,
|
||||||
resolveReviewEditAction,
|
resolveReviewEditAction,
|
||||||
buildReviewPrimaryButtonLabel,
|
buildReviewPrimaryButtonLabel,
|
||||||
@@ -3334,6 +3813,9 @@ export default {
|
|||||||
openDeleteSessionDialog,
|
openDeleteSessionDialog,
|
||||||
closeDeleteSessionDialog,
|
closeDeleteSessionDialog,
|
||||||
confirmDeleteCurrentSession,
|
confirmDeleteCurrentSession,
|
||||||
|
closeUploadDecisionDialog,
|
||||||
|
continueExistingUpload,
|
||||||
|
createNewUploadDocument,
|
||||||
openInlineReviewEditor,
|
openInlineReviewEditor,
|
||||||
closeInlineReviewEditor,
|
closeInlineReviewEditor,
|
||||||
commitInlineReviewEditor,
|
commitInlineReviewEditor,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
deleteExpenseClaimItem,
|
deleteExpenseClaimItem,
|
||||||
deleteExpenseClaimItemAttachment,
|
deleteExpenseClaimItemAttachment,
|
||||||
deleteExpenseClaim,
|
deleteExpenseClaim,
|
||||||
fetchExpenseClaimItemAttachment,
|
|
||||||
fetchExpenseClaimItemAttachmentMeta,
|
fetchExpenseClaimItemAttachmentMeta,
|
||||||
|
fetchExpenseClaimItemAttachmentPreview,
|
||||||
submitExpenseClaim,
|
submitExpenseClaim,
|
||||||
uploadExpenseClaimItemAttachment,
|
uploadExpenseClaimItemAttachment,
|
||||||
updateExpenseClaimItem
|
updateExpenseClaimItem
|
||||||
@@ -894,10 +894,14 @@ export default {
|
|||||||
attachmentPreviewOpen.value = true
|
attachmentPreviewOpen.value = true
|
||||||
attachmentPreviewLoading.value = true
|
attachmentPreviewLoading.value = true
|
||||||
attachmentPreviewName.value = resolveAttachmentDisplayName(item)
|
attachmentPreviewName.value = resolveAttachmentDisplayName(item)
|
||||||
attachmentPreviewMediaType.value = String(resolveAttachmentMeta(item)?.media_type || '').trim()
|
const metadata = resolveAttachmentMeta(item)
|
||||||
|
attachmentPreviewMediaType.value =
|
||||||
|
String(metadata?.preview_kind || '').trim() === 'image'
|
||||||
|
? 'image/png'
|
||||||
|
: String(metadata?.media_type || '').trim()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await fetchExpenseClaimItemAttachment(request.value.claimId, item.id)
|
const blob = await fetchExpenseClaimItemAttachmentPreview(request.value.claimId, item.id)
|
||||||
revokeAttachmentPreviewUrl()
|
revokeAttachmentPreviewUrl()
|
||||||
attachmentPreviewUrl.value = URL.createObjectURL(blob)
|
attachmentPreviewUrl.value = URL.createObjectURL(blob)
|
||||||
attachmentPreviewMediaType.value = blob.type || attachmentPreviewMediaType.value
|
attachmentPreviewMediaType.value = blob.type || attachmentPreviewMediaType.value
|
||||||
@@ -1139,7 +1143,8 @@ export default {
|
|||||||
emit('openAssistant', {
|
emit('openAssistant', {
|
||||||
source: 'detail',
|
source: 'detail',
|
||||||
prompt: '',
|
prompt: '',
|
||||||
request: request.value
|
request: request.value,
|
||||||
|
restoreLatestConversation: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user