- PersonalWorkbench.vue: update personal workbench component - useAppShell.js: update app shell composable - useChat.js: update chat composable with new features - AppShellRouteView.vue: update route view - ChatView.vue: update chat view with enhanced UI - TravelReimbursementCreateView.vue: update travel reimbursement form - ChatView.js: update chat view script logic - TravelReimbursementCreateView.js: update travel form script logic
222 lines
8.2 KiB
JavaScript
222 lines
8.2 KiB
JavaScript
import { computed, nextTick, ref, watch } from 'vue'
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
|
import { fetchOntologyParse } from '../../services/ontology.js'
|
|
|
|
export default {
|
|
name: 'ChatView',
|
|
props: {
|
|
documents: { type: Array, required: true },
|
|
docSearch: { type: String, default: '' },
|
|
messages: { type: Array, required: true },
|
|
uploadedFiles: { type: Array, required: true },
|
|
activeCase: { type: Object, default: null },
|
|
quickPrompts: { type: Array, required: true },
|
|
draft: { type: String, default: '' },
|
|
sending: { type: Boolean, default: false },
|
|
messageList: { type: Object, default: null }
|
|
},
|
|
emits: ['send', 'upload', 'draft', 'approveCase', 'rejectCase', 'selectCase'],
|
|
setup(props, { emit }) {
|
|
const { currentUser } = useSystemState()
|
|
const localMessageList = ref(null)
|
|
const promptPage = ref(0)
|
|
const semanticDraft = ref('查一下本周报销超标风险')
|
|
const semanticLoading = ref(false)
|
|
const semanticError = ref('')
|
|
const semanticResult = ref(null)
|
|
|
|
const sessions = [
|
|
{ title: '北京出差,酒店超标报销怎么处理?', time: '10:32', active: true },
|
|
{ title: '发票抬头不一致怎么办', time: '09:48' },
|
|
{ title: '借款冲销流程', time: '昨天' },
|
|
{ title: '预算占用失败处理', time: '昨天' },
|
|
{ title: '招待费报销范围', time: '05-11' },
|
|
{ title: '差旅住宿标准如何匹配城市级别?', time: '05-10' },
|
|
{ title: '电子发票验真失败如何处理?', time: '05-09' },
|
|
{ title: '跨部门项目费用怎么归集?', time: '05-08' },
|
|
{ title: '会议费和招待费如何区分?', time: '05-07' },
|
|
{ title: '超预算申请需要哪些审批节点?', time: '05-06' },
|
|
{ title: '海外差旅汇率按哪天计算?', time: '05-05' },
|
|
{ title: '员工退票手续费是否可报销?', time: '05-04' }
|
|
]
|
|
|
|
const prompts = [
|
|
{ icon: 'mdi mdi-bed-outline', short: '差旅标准', text: '差旅报销特殊标准是什么?' },
|
|
{ icon: 'mdi mdi-receipt-text-outline', short: '发票规范', text: '发票丢失如何处理?' },
|
|
{ icon: 'mdi mdi-cash-refund', short: '借款冲销', text: '借款多久内需要冲销?' },
|
|
{ icon: 'mdi mdi-file-chart-outline', short: '预算冲突', text: '预算不足如何申请?' },
|
|
{ icon: 'mdi mdi-shield-check-outline', short: '审批要求', text: '酒店超标后如何申请例外报销?' },
|
|
{ icon: 'mdi mdi-office-building-marker', short: '住宿标准', text: '差旅住宿标准按什么规则执行?' },
|
|
{ icon: 'mdi mdi-file-question-outline', short: '材料补齐', text: '报销附件缺失怎么补交?' },
|
|
{ icon: 'mdi mdi-alert-circle-outline', short: '风险等级', text: '哪些情况会触发中风险?' }
|
|
]
|
|
|
|
const visiblePrompts = computed(() => prompts.slice((promptPage.value % 2) * 4, (promptPage.value % 2) * 4 + 4))
|
|
|
|
const hotQuestions = [
|
|
'差旅报销标准是什么?',
|
|
'酒店超标后如何申请例外报销?',
|
|
'发票丢失如何处理?',
|
|
'借款多久内需要冲销?',
|
|
'预算超支如何申请?',
|
|
'招待费报销需要哪些凭证?',
|
|
'发票抬头不一致是否允许报销?',
|
|
'报销附件缺失怎么补交?',
|
|
'差旅住宿标准按什么规则执行?',
|
|
'电子发票验真失败如何处理?'
|
|
]
|
|
|
|
const similarQuestions = [
|
|
{ text: '酒店超标后如何申请例外报销?', score: '96%' },
|
|
{ text: '发票抬头不一致是否允许报销?', score: '92%' },
|
|
{ text: '差旅住宿标准按什么规则执行?', score: '89%' },
|
|
{ text: '预算不足时能否先提交报销?', score: '86%' },
|
|
{ text: '电子发票验真失败是否可以先报销?', score: '84%' },
|
|
{ text: '跨部门项目费用如何归集?', score: '81%' },
|
|
{ text: '招待费报销需要哪些凭证?', score: '78%' },
|
|
{ text: '借款冲销逾期会影响报销吗?', score: '76%' }
|
|
]
|
|
|
|
const semanticExamples = [
|
|
'查一下本周报销超标风险',
|
|
'客户 A 这个月还有多少应收',
|
|
'帮我直接付款给供应商B'
|
|
]
|
|
|
|
const semanticConfidenceLabel = computed(() =>
|
|
semanticResult.value ? `${Math.round((semanticResult.value.confidence || 0) * 100)}%` : '未解析'
|
|
)
|
|
const semanticEntitiesText = computed(() => {
|
|
const items = semanticResult.value?.entities || []
|
|
return items.length
|
|
? items.map((item) => `${item.type}:${item.normalized_value}`).join(' / ')
|
|
: '未识别'
|
|
})
|
|
const semanticTimeRangeText = computed(() => {
|
|
const value = semanticResult.value?.time_range
|
|
if (!value?.start_date || !value?.end_date) {
|
|
return '未识别'
|
|
}
|
|
return `${value.start_date} ~ ${value.end_date}${value.granularity ? ` · ${value.granularity}` : ''}`
|
|
})
|
|
const semanticMetricsText = computed(() => {
|
|
const items = semanticResult.value?.metrics || []
|
|
return items.length
|
|
? items
|
|
.map((item) => {
|
|
const suffix = item.top_n ? ` top_${item.top_n}` : ''
|
|
return `${item.name}${suffix}`
|
|
})
|
|
.join(' / ')
|
|
: '未识别'
|
|
})
|
|
const semanticConstraintsText = computed(() => {
|
|
const items = semanticResult.value?.constraints || []
|
|
return items.length
|
|
? items.map((item) => `${item.field}${item.operator}${item.value}`).join(' / ')
|
|
: '未识别'
|
|
})
|
|
const semanticRiskFlagsText = computed(() => {
|
|
const items = semanticResult.value?.risk_flags || []
|
|
return items.length ? items.join(' / ') : '未识别'
|
|
})
|
|
const semanticClarificationText = computed(() => {
|
|
if (!semanticResult.value) {
|
|
return '未解析'
|
|
}
|
|
if (!semanticResult.value.clarification_required) {
|
|
return '无需澄清'
|
|
}
|
|
return semanticResult.value.clarification_question || '需要补充更多上下文'
|
|
})
|
|
const semanticResultJson = computed(() =>
|
|
semanticResult.value ? JSON.stringify(semanticResult.value, null, 2) : ''
|
|
)
|
|
|
|
function rotatePrompts() {
|
|
promptPage.value += 1
|
|
}
|
|
|
|
function applyPrompt(text) {
|
|
emit('draft', text)
|
|
semanticDraft.value = text
|
|
}
|
|
|
|
function applySemanticExample(text) {
|
|
semanticDraft.value = text
|
|
}
|
|
|
|
function useDraftAsSemanticInput() {
|
|
semanticDraft.value = props.draft || semanticDraft.value
|
|
}
|
|
|
|
async function parseSemanticQuery() {
|
|
const query = String(semanticDraft.value || '').trim()
|
|
if (!query) {
|
|
semanticError.value = '请输入要解析的问题。'
|
|
semanticResult.value = null
|
|
return
|
|
}
|
|
|
|
semanticLoading.value = true
|
|
semanticError.value = ''
|
|
|
|
try {
|
|
semanticResult.value = await fetchOntologyParse({
|
|
query,
|
|
user_id: currentUser.value?.username || currentUser.value?.name || 'anonymous',
|
|
context_json: {
|
|
role_codes: currentUser.value?.roleCodes || [],
|
|
is_admin: Boolean(currentUser.value?.isAdmin),
|
|
name: currentUser.value?.name || '',
|
|
role: currentUser.value?.role || ''
|
|
}
|
|
})
|
|
} catch (error) {
|
|
semanticResult.value = null
|
|
semanticError.value = error.message || '语义解析失败,请稍后重试。'
|
|
} finally {
|
|
semanticLoading.value = false
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.messages.length,
|
|
() => {
|
|
nextTick(() => localMessageList.value?.scrollTo({ top: localMessageList.value.scrollHeight, behavior: 'smooth' }))
|
|
}
|
|
)
|
|
|
|
return {
|
|
emit,
|
|
localMessageList,
|
|
promptPage,
|
|
sessions,
|
|
prompts,
|
|
visiblePrompts,
|
|
hotQuestions,
|
|
similarQuestions,
|
|
rotatePrompts,
|
|
applyPrompt,
|
|
semanticDraft,
|
|
semanticLoading,
|
|
semanticError,
|
|
semanticResult,
|
|
semanticExamples,
|
|
semanticConfidenceLabel,
|
|
semanticEntitiesText,
|
|
semanticTimeRangeText,
|
|
semanticMetricsText,
|
|
semanticConstraintsText,
|
|
semanticRiskFlagsText,
|
|
semanticClarificationText,
|
|
semanticResultJson,
|
|
applySemanticExample,
|
|
useDraftAsSemanticInput,
|
|
parseSemanticQuery
|
|
}
|
|
}
|
|
}
|
|
|