2026-05-06 11:00:38 +08:00
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TravelRequestDetailView',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
request: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({})
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
emits: ['backToRequests', 'openAssistant'] ,
|
|
|
|
|
|
setup(props, { emit }) {
|
|
|
|
|
|
const expandedExpenseId = ref(null)
|
|
|
|
|
|
const aiEntryOpen = ref(false)
|
|
|
|
|
|
const aiDraft = ref('')
|
|
|
|
|
|
const aiFileInput = ref(null)
|
|
|
|
|
|
const aiEntrySeed = ref(2)
|
|
|
|
|
|
const pendingAiExpense = ref(null)
|
|
|
|
|
|
const uploadedAiFiles = ref([])
|
|
|
|
|
|
const expenseItems = ref([
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'exp-1',
|
|
|
|
|
|
time: '07-08',
|
|
|
|
|
|
dayLabel: '第 1 天',
|
|
|
|
|
|
name: '高铁票',
|
|
|
|
|
|
category: '交通',
|
|
|
|
|
|
desc: '上海虹桥 -> 杭州东',
|
|
|
|
|
|
detail: '客户方案汇报前往现场',
|
|
|
|
|
|
amount: '¥236.00',
|
|
|
|
|
|
status: '规则通过',
|
|
|
|
|
|
tone: 'ok',
|
|
|
|
|
|
attachmentStatus: '2 份附件',
|
|
|
|
|
|
attachmentHint: '车票 + 行程单',
|
|
|
|
|
|
attachmentTone: 'ok',
|
|
|
|
|
|
attachments: ['高铁票.pdf', '行程单.pdf'],
|
|
|
|
|
|
riskLabel: '规则通过',
|
|
|
|
|
|
riskText: '票据与行程匹配',
|
|
|
|
|
|
riskTone: 'low'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'exp-2',
|
|
|
|
|
|
time: '07-09',
|
|
|
|
|
|
dayLabel: '第 2 天',
|
|
|
|
|
|
name: '酒店住宿',
|
|
|
|
|
|
category: '住宿',
|
|
|
|
|
|
desc: '杭州西湖商务酒店',
|
|
|
|
|
|
detail: '1 晚住宿,含早餐',
|
|
|
|
|
|
amount: '¥1,180.00',
|
|
|
|
|
|
status: '待补材料',
|
|
|
|
|
|
tone: 'bad',
|
|
|
|
|
|
attachmentStatus: '缺 1 份',
|
|
|
|
|
|
attachmentHint: '缺少入住清单',
|
|
|
|
|
|
attachmentTone: 'partial',
|
|
|
|
|
|
attachments: ['酒店发票.jpg'],
|
|
|
|
|
|
riskLabel: '待补材料',
|
|
|
|
|
|
riskText: '需补酒店入住清单',
|
|
|
|
|
|
riskTone: 'medium'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'exp-3',
|
|
|
|
|
|
time: '07-10',
|
|
|
|
|
|
dayLabel: '第 3 天',
|
|
|
|
|
|
name: '出租车',
|
|
|
|
|
|
category: '市内交通',
|
|
|
|
|
|
desc: '客户公司往返酒店',
|
|
|
|
|
|
detail: '含夜间打车 2 次',
|
|
|
|
|
|
amount: '¥128.00',
|
|
|
|
|
|
status: '需说明',
|
|
|
|
|
|
tone: 'bad',
|
|
|
|
|
|
attachmentStatus: '3 份附件',
|
|
|
|
|
|
attachmentHint: '发票已上传',
|
|
|
|
|
|
attachmentTone: 'ok',
|
|
|
|
|
|
attachments: ['出租车发票1.jpg', '出租车发票2.jpg', '打车订单.png'],
|
|
|
|
|
|
riskLabel: '超标说明',
|
|
|
|
|
|
riskText: '1 笔夜间交通需补充说明',
|
|
|
|
|
|
riskTone: 'medium'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'exp-4',
|
|
|
|
|
|
time: '07-11',
|
|
|
|
|
|
dayLabel: '第 4 天',
|
|
|
|
|
|
name: '餐补',
|
|
|
|
|
|
category: '补贴',
|
|
|
|
|
|
desc: '差旅餐补',
|
|
|
|
|
|
detail: '按 4 天标准自动计算',
|
|
|
|
|
|
amount: '¥320.00',
|
|
|
|
|
|
status: '规则通过',
|
|
|
|
|
|
tone: 'ok',
|
|
|
|
|
|
attachmentStatus: '系统生成',
|
|
|
|
|
|
attachmentHint: '无需上传附件',
|
|
|
|
|
|
attachmentTone: 'neutral',
|
|
|
|
|
|
attachments: [],
|
|
|
|
|
|
riskLabel: '规则通过',
|
|
|
|
|
|
riskText: '补贴标准校验通过',
|
|
|
|
|
|
riskTone: 'low'
|
|
|
|
|
|
}
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const request = computed(() => ({
|
|
|
|
|
|
id: props.request?.id ?? 'BR240712001',
|
|
|
|
|
|
reason: props.request?.reason ?? '客户方案汇报',
|
|
|
|
|
|
city: props.request?.city ?? '上海',
|
|
|
|
|
|
period: props.request?.period ?? '07-08~07-11 (4天)',
|
|
|
|
|
|
applyTime: props.request?.applyTime ?? '2024-07-07',
|
|
|
|
|
|
amount: props.request?.amount ?? '¥3,680.00',
|
|
|
|
|
|
node: props.request?.node ?? '财务审核',
|
|
|
|
|
|
approval: props.request?.approval ?? '审批中',
|
|
|
|
|
|
approvalTone: props.request?.approvalTone ?? 'info',
|
|
|
|
|
|
travel: props.request?.travel ?? '已订酒店/机票',
|
|
|
|
|
|
travelTone: props.request?.travelTone ?? 'low'
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const profile = {
|
|
|
|
|
|
name: '张晓明',
|
|
|
|
|
|
department: '财务管理员',
|
|
|
|
|
|
avatar: '张'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const summaryItems = [
|
|
|
|
|
|
{ label: '出差城市', value: request.value.city, icon: 'mdi mdi-map-marker-path' },
|
|
|
|
|
|
{ label: '出差区间', value: request.value.period, icon: 'mdi mdi-clock-outline' },
|
|
|
|
|
|
{ label: '票据关联', value: '6 条明细 / 5 份材料', icon: 'mdi mdi-file-document-multiple-outline' },
|
|
|
|
|
|
{ label: '商旅状态', value: request.value.travel, icon: 'mdi mdi-airplane' },
|
|
|
|
|
|
{ label: '出差事由', value: request.value.reason, icon: 'mdi mdi-briefcase-outline' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const heroSummaryItems = computed(() => [
|
|
|
|
|
|
{ label: '单号', value: request.value.id, icon: 'mdi mdi-pound-box-outline' },
|
|
|
|
|
|
{ label: '申请类型', value: '差旅费申请/报销', icon: 'mdi mdi-tag-multiple' },
|
|
|
|
|
|
...summaryItems
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const currentProgressRingMotion = {
|
|
|
|
|
|
initial: {
|
|
|
|
|
|
scale: 1,
|
|
|
|
|
|
opacity: 0.34,
|
|
|
|
|
|
},
|
|
|
|
|
|
enter: {
|
|
|
|
|
|
scale: [1, 1.42, 1.78],
|
|
|
|
|
|
opacity: [0.34, 0.16, 0],
|
|
|
|
|
|
transition: {
|
|
|
|
|
|
duration: 3.2,
|
|
|
|
|
|
repeat: Infinity,
|
|
|
|
|
|
repeatType: 'loop',
|
|
|
|
|
|
repeatDelay: 0.85,
|
|
|
|
|
|
ease: 'easeOut',
|
|
|
|
|
|
times: [0, 0.5, 1],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const progressSteps = computed(() => {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ index: 1, label: '提交申请', time: '07-11 08:46', done: true, active: true },
|
|
|
|
|
|
{ index: 2, label: '票据识别', time: '07-11 08:48', done: true, active: true },
|
|
|
|
|
|
{ index: 3, label: '费用归类', time: '07-11 08:49', done: true, active: true },
|
|
|
|
|
|
{ index: 4, label: '部门负责人审批', time: '07-11 11:28', done: true, active: true },
|
|
|
|
|
|
{ index: 5, label: '财务审批', time: '进行中', active: true, current: true },
|
|
|
|
|
|
{ index: 6, label: '归档入账', time: '待处理' }
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const expenseTotal = computed(() => {
|
|
|
|
|
|
const total = expenseItems.value.reduce((sum, item) => sum + parseCurrency(item.amount), 0)
|
|
|
|
|
|
return formatCurrency(total)
|
|
|
|
|
|
})
|
|
|
|
|
|
const uploadedExpenseCount = computed(() => expenseItems.value.filter((item) => item.attachments.length).length)
|
|
|
|
|
|
const canSendAiEntry = computed(() => Boolean(aiDraft.value.trim() || uploadedAiFiles.value.length))
|
|
|
|
|
|
|
|
|
|
|
|
const detailNote = '本次出差用于客户方案汇报与现场沟通,需覆盖往返交通、住宿及市内交通费用。已完成主要票据上传,待补酒店入住清单后即可进入完整审批流程。'
|
|
|
|
|
|
|
|
|
|
|
|
function toggleExpenseAttachments(id) {
|
|
|
|
|
|
expandedExpenseId.value = expandedExpenseId.value === id ? null : id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showExpenseRisk(item) {
|
|
|
|
|
|
return Boolean(item.riskText)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openAiEntry() {
|
|
|
|
|
|
aiEntryOpen.value = false
|
|
|
|
|
|
emit('openAssistant', {
|
|
|
|
|
|
source: 'detail',
|
|
|
|
|
|
prompt: '',
|
|
|
|
|
|
request: request.value
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAiEntry() {
|
|
|
|
|
|
aiEntryOpen.value = false
|
|
|
|
|
|
aiDraft.value = ''
|
|
|
|
|
|
pendingAiExpense.value = null
|
|
|
|
|
|
uploadedAiFiles.value = []
|
|
|
|
|
|
if (aiFileInput.value) {
|
|
|
|
|
|
aiFileInput.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseCurrency(value) {
|
|
|
|
|
|
return Number.parseFloat(String(value).replace(/[^\d.]/g, '')) || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatCurrency(value) {
|
|
|
|
|
|
return `¥${value.toFixed(2)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildNextExpenseId() {
|
|
|
|
|
|
aiEntrySeed.value += 1
|
|
|
|
|
|
return `exp-ai-${aiEntrySeed.value}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferExpenseCategory(text) {
|
|
|
|
|
|
if (/高铁|火车|机票|航班|打车|出租车|地铁|公交|交通/.test(text)) return '交通'
|
|
|
|
|
|
if (/酒店|住宿|房费/.test(text)) return '住宿'
|
|
|
|
|
|
if (/餐|午饭|晚饭|早餐|餐补/.test(text)) return '餐饮'
|
|
|
|
|
|
return '其他'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferExpenseName(text, category) {
|
|
|
|
|
|
if (/高铁/.test(text)) return '高铁票'
|
|
|
|
|
|
if (/机票|航班/.test(text)) return '机票'
|
|
|
|
|
|
if (/出租车|打车/.test(text)) return '出租车'
|
|
|
|
|
|
if (/酒店|住宿/.test(text)) return '酒店住宿'
|
|
|
|
|
|
if (/餐补/.test(text)) return '餐补'
|
|
|
|
|
|
if (/餐|午饭|晚饭|早餐/.test(text)) return '餐饮'
|
|
|
|
|
|
return `${category}费用`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferAttachments(text, uploadedFiles = []) {
|
|
|
|
|
|
if (uploadedFiles.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: `${uploadedFiles.length} 份附件`,
|
|
|
|
|
|
hint: uploadedFiles.map((file) => file.name).join(' + '),
|
|
|
|
|
|
tone: 'ok',
|
|
|
|
|
|
files: uploadedFiles.map((file) => file.name),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (/无需|免附件|系统生成/.test(text)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: '系统生成',
|
|
|
|
|
|
hint: '无需上传附件',
|
|
|
|
|
|
tone: 'neutral',
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uploaded = /已上传|上传了|附上|附件/.test(text)
|
|
|
|
|
|
const receipt = /发票/.test(text)
|
|
|
|
|
|
const itinerary = /行程单/.test(text)
|
|
|
|
|
|
const ticket = /车票|机票/.test(text)
|
|
|
|
|
|
const hotelList = /入住清单/.test(text)
|
|
|
|
|
|
const files = []
|
|
|
|
|
|
|
|
|
|
|
|
if (receipt) files.push('发票.jpg')
|
|
|
|
|
|
if (itinerary) files.push('行程单.pdf')
|
|
|
|
|
|
if (ticket && !files.includes('票据.pdf')) files.push('票据.pdf')
|
|
|
|
|
|
if (hotelList) files.push('入住清单.pdf')
|
|
|
|
|
|
|
|
|
|
|
|
if (uploaded || files.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: `${Math.max(files.length, 1)} 份附件`,
|
|
|
|
|
|
hint: files.length ? files.join(' + ') : '已上传附件待识别',
|
|
|
|
|
|
tone: 'ok',
|
|
|
|
|
|
files: files.length ? files : ['附件1.jpg'],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: '缺 1 份',
|
|
|
|
|
|
hint: '待补上传票据原件',
|
|
|
|
|
|
tone: 'missing',
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferRisk(text, attachmentTone) {
|
|
|
|
|
|
if (/夜间|超标|说明/.test(text)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: '需说明',
|
|
|
|
|
|
tone: 'bad',
|
|
|
|
|
|
riskLabel: '超标说明',
|
|
|
|
|
|
riskText: '识别到特殊场景,建议补充费用说明',
|
|
|
|
|
|
riskTone: 'medium',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (attachmentTone === 'missing' || attachmentTone === 'partial') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: '待补材料',
|
|
|
|
|
|
tone: 'bad',
|
|
|
|
|
|
riskLabel: '待补材料',
|
|
|
|
|
|
riskText: '附件不完整,需补齐后再提交审批',
|
|
|
|
|
|
riskTone: 'medium',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: '规则通过',
|
|
|
|
|
|
tone: 'ok',
|
|
|
|
|
|
riskLabel: '规则通过',
|
|
|
|
|
|
riskText: 'AI 识别通过,字段已结构化',
|
|
|
|
|
|
riskTone: 'low',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractDateLabel(text) {
|
|
|
|
|
|
const match = text.match(/(\d{1,2})月(\d{1,2})日|(\d{1,2})[-/.](\d{1,2})/)
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
return { time: '07-12', dayLabel: `第 ${expenseItems.value.length + 1} 天` }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const month = String(match[1] || match[3] || '07').padStart(2, '0')
|
|
|
|
|
|
const day = String(match[2] || match[4] || '12').padStart(2, '0')
|
|
|
|
|
|
return { time: `${month}-${day}`, dayLabel: `第 ${expenseItems.value.length + 1} 天` }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractAmount(text) {
|
|
|
|
|
|
const match = text.match(/(\d+(?:\.\d{1,2})?)\s*元/)
|
|
|
|
|
|
return formatCurrency(Number.parseFloat(match?.[1] || '0'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildAiExpense(text) {
|
|
|
|
|
|
const category = inferExpenseCategory(text)
|
|
|
|
|
|
const name = inferExpenseName(text, category)
|
|
|
|
|
|
const dateInfo = extractDateLabel(text)
|
|
|
|
|
|
const attachments = inferAttachments(text, uploadedAiFiles.value)
|
|
|
|
|
|
const risk = inferRisk(text, attachments.tone)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: buildNextExpenseId(),
|
|
|
|
|
|
time: dateInfo.time,
|
|
|
|
|
|
dayLabel: dateInfo.dayLabel,
|
|
|
|
|
|
name,
|
|
|
|
|
|
category,
|
|
|
|
|
|
desc: text.slice(0, 24),
|
|
|
|
|
|
detail: text,
|
|
|
|
|
|
amount: extractAmount(text),
|
|
|
|
|
|
status: risk.status,
|
|
|
|
|
|
tone: risk.tone,
|
|
|
|
|
|
attachmentStatus: attachments.status,
|
|
|
|
|
|
attachmentHint: attachments.hint,
|
|
|
|
|
|
attachmentTone: attachments.tone,
|
|
|
|
|
|
attachments: attachments.files,
|
|
|
|
|
|
riskLabel: risk.riskLabel,
|
|
|
|
|
|
riskText: risk.riskText,
|
|
|
|
|
|
riskTone: risk.riskTone,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const aiMessages = ref([
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'ai-msg-1',
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
text: '请直接描述费用场景、日期、金额和是否已上传票据,我会整理成费用明细。',
|
|
|
|
|
|
},
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
function sendAiEntry() {
|
|
|
|
|
|
const text = aiDraft.value.trim() || `已上传 ${uploadedAiFiles.value.length} 份单据,请根据附件识别费用。`
|
|
|
|
|
|
if (!text && !uploadedAiFiles.value.length) return
|
|
|
|
|
|
|
|
|
|
|
|
aiMessages.value.push({
|
|
|
|
|
|
id: `ai-msg-user-${Date.now()}`,
|
|
|
|
|
|
role: 'user',
|
|
|
|
|
|
text: uploadedAiFiles.value.length ? `${text}\n附件:${uploadedAiFiles.value.map((file) => file.name).join('、')}` : text,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
pendingAiExpense.value = buildAiExpense(text)
|
|
|
|
|
|
|
|
|
|
|
|
aiMessages.value.push({
|
|
|
|
|
|
id: `ai-msg-assistant-${Date.now()}`,
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
text: `已识别为 ${pendingAiExpense.value.name},金额 ${pendingAiExpense.value.amount},可直接加入费用明细。`,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
aiDraft.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function regenerateAiEntry() {
|
|
|
|
|
|
if (!pendingAiExpense.value) return
|
|
|
|
|
|
const sourceText = pendingAiExpense.value.detail
|
|
|
|
|
|
pendingAiExpense.value = buildAiExpense(sourceText.replace('待补上传票据原件', '已上传发票'))
|
|
|
|
|
|
aiMessages.value.push({
|
|
|
|
|
|
id: `ai-msg-regenerate-${Date.now()}`,
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
text: '已重新整理识别结果,你可以继续确认后加入费用明细。',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyAiExpense() {
|
|
|
|
|
|
if (!pendingAiExpense.value) return
|
|
|
|
|
|
expenseItems.value.push({ ...pendingAiExpense.value })
|
|
|
|
|
|
expandedExpenseId.value = pendingAiExpense.value.id
|
|
|
|
|
|
aiMessages.value.push({
|
|
|
|
|
|
id: `ai-msg-apply-${Date.now()}`,
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
text: '该费用条目已加入下方费用明细表。',
|
|
|
|
|
|
})
|
|
|
|
|
|
pendingAiExpense.value = null
|
|
|
|
|
|
aiDraft.value = ''
|
|
|
|
|
|
uploadedAiFiles.value = []
|
|
|
|
|
|
if (aiFileInput.value) {
|
|
|
|
|
|
aiFileInput.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
aiEntryOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function triggerAiUpload() {
|
|
|
|
|
|
aiFileInput.value?.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleAiFilesChange(event) {
|
|
|
|
|
|
const files = Array.from(event.target.files ?? [])
|
|
|
|
|
|
uploadedAiFiles.value = files
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
emit,
|
|
|
|
|
|
expandedExpenseId,
|
|
|
|
|
|
aiEntryOpen,
|
|
|
|
|
|
aiDraft,
|
|
|
|
|
|
aiFileInput,
|
|
|
|
|
|
aiEntrySeed,
|
|
|
|
|
|
pendingAiExpense,
|
|
|
|
|
|
uploadedAiFiles,
|
|
|
|
|
|
expenseItems,
|
|
|
|
|
|
request,
|
|
|
|
|
|
profile,
|
|
|
|
|
|
summaryItems,
|
|
|
|
|
|
heroSummaryItems,
|
|
|
|
|
|
currentProgressRingMotion,
|
|
|
|
|
|
progressSteps,
|
|
|
|
|
|
expenseTotal,
|
|
|
|
|
|
uploadedExpenseCount,
|
|
|
|
|
|
canSendAiEntry,
|
|
|
|
|
|
detailNote,
|
|
|
|
|
|
toggleExpenseAttachments,
|
|
|
|
|
|
showExpenseRisk,
|
|
|
|
|
|
openAiEntry,
|
|
|
|
|
|
closeAiEntry,
|
|
|
|
|
|
aiMessages,
|
|
|
|
|
|
sendAiEntry,
|
|
|
|
|
|
regenerateAiEntry,
|
|
|
|
|
|
applyAiExpense,
|
|
|
|
|
|
triggerAiUpload,
|
|
|
|
|
|
handleAiFilesChange
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-09 03:04:09 +00:00
|
|
|
|
|