feat: 添加风险规则及 agent assets 功能增强
This commit is contained in:
@@ -1,310 +1,310 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useNavigation, navItems } from './useNavigation.js'
|
||||
import { useRequests } from './useRequests.js'
|
||||
import { useSystemState } from './useSystemState.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||
|
||||
const SESSION_TYPE_EXPENSE = 'expense'
|
||||
|
||||
function isPlaceholderValue(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return true
|
||||
}
|
||||
|
||||
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
|
||||
}
|
||||
|
||||
function hasMissingAttachment(request) {
|
||||
const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : []
|
||||
|
||||
if (expenseItems.length) {
|
||||
return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim())
|
||||
}
|
||||
|
||||
const attachmentSummary = String(request?.attachmentSummary || '').trim()
|
||||
const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim()
|
||||
return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue)
|
||||
}
|
||||
|
||||
function hasPendingInfo(request) {
|
||||
if (!request) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return [
|
||||
request.profileDepartment,
|
||||
request.profilePosition,
|
||||
request.profileGrade,
|
||||
request.profileManager,
|
||||
request.reason,
|
||||
request.occurredDisplay
|
||||
].some(isPlaceholderValue)
|
||||
}
|
||||
|
||||
function resolveDetailAlertTone(request) {
|
||||
if (request?.approvalKey === 'completed') return 'success'
|
||||
if (request?.approvalKey === 'rejected') return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function buildDetailAlerts(request) {
|
||||
if (!request) {
|
||||
return []
|
||||
}
|
||||
|
||||
const alerts = []
|
||||
const nodeLabel = String(request.node || request.approval || '').trim()
|
||||
|
||||
if (nodeLabel) {
|
||||
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
|
||||
}
|
||||
|
||||
if (hasMissingAttachment(request)) {
|
||||
alerts.push({ label: '缺少票据', tone: 'warning' })
|
||||
}
|
||||
|
||||
if (hasPendingInfo(request)) {
|
||||
alerts.push({ label: '待补信息', tone: 'warning' })
|
||||
}
|
||||
|
||||
return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3)
|
||||
}
|
||||
|
||||
export function useAppShell() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const smartEntryOpen = ref(false)
|
||||
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
|
||||
const smartEntrySessionId = ref(0)
|
||||
|
||||
const { activeView, currentView, setView } = useNavigation()
|
||||
const {
|
||||
requests,
|
||||
loading: requestsLoading,
|
||||
error: requestsError,
|
||||
search,
|
||||
filters,
|
||||
ranges,
|
||||
activeRange,
|
||||
filteredRequests,
|
||||
approveRequest,
|
||||
rejectRequest,
|
||||
reload: reloadRequests
|
||||
} = useRequests()
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||
|
||||
const selectedRequest = computed(() => {
|
||||
const requestId = String(route.params.requestId || '')
|
||||
|
||||
if (!requestId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rawRequest = requests.value.find(
|
||||
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
||||
)
|
||||
return normalizeRequestForUi(rawRequest)
|
||||
})
|
||||
|
||||
const detailMode = computed(() => route.name === 'app-request-detail')
|
||||
const logDetailMode = computed(() => route.name === 'app-log-detail')
|
||||
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
||||
|
||||
const topBarView = computed(() => {
|
||||
if (detailMode.value) {
|
||||
return {
|
||||
title: '报销单详情',
|
||||
desc: '查看报销明细、票据材料、审批进度与风险提示。'
|
||||
}
|
||||
}
|
||||
|
||||
if (logDetailMode.value) {
|
||||
return {
|
||||
title: '日志详情',
|
||||
desc: '查看单条日志的解析结果、上下文信息与原始记录。'
|
||||
}
|
||||
}
|
||||
|
||||
return currentView.value
|
||||
})
|
||||
|
||||
const requestSummary = computed(() =>
|
||||
filteredRequests.value.reduce(
|
||||
(summary, item) => {
|
||||
const request = normalizeRequestForUi(item)
|
||||
if (!request) {
|
||||
return summary
|
||||
}
|
||||
|
||||
summary.total += 1
|
||||
|
||||
if (request.approvalKey === 'draft') {
|
||||
summary.draft += 1
|
||||
} else if (request.approvalKey === 'in_progress') {
|
||||
summary.inProgress += 1
|
||||
} else if (request.approvalKey === 'supplement') {
|
||||
summary.supplement += 1
|
||||
} else if (request.approvalKey === 'completed') {
|
||||
summary.completed += 1
|
||||
}
|
||||
|
||||
return summary
|
||||
},
|
||||
{ total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 }
|
||||
)
|
||||
)
|
||||
|
||||
function handleApprove(request) {
|
||||
const message = approveRequest(request)
|
||||
toast(message)
|
||||
}
|
||||
|
||||
function handleReject(request) {
|
||||
const message = rejectRequest(request)
|
||||
toast(message)
|
||||
}
|
||||
|
||||
function handleNavigate(view) {
|
||||
smartEntryOpen.value = false
|
||||
setView(view)
|
||||
}
|
||||
|
||||
function openTravelCreate() {
|
||||
smartEntryOpen.value = true
|
||||
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
smartEntryContext.value = {
|
||||
prompt: payload.prompt ?? '',
|
||||
source: payload.source ?? 'workbench',
|
||||
request: payload.request ?? selectedRequest.value,
|
||||
files: Array.isArray(payload.files) ? payload.files : [],
|
||||
conversation
|
||||
}
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
|
||||
function closeSmartEntry() {
|
||||
smartEntryOpen.value = false
|
||||
}
|
||||
|
||||
async function handleDraftSaved(payload = {}) {
|
||||
const claimNo = String(payload.claimNo || payload.claim_no || '').trim()
|
||||
const status = String(payload.status || payload.claimStatus || '').trim()
|
||||
const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim()
|
||||
smartEntryOpen.value = false
|
||||
await reloadRequests()
|
||||
if (status === 'submitted') {
|
||||
toast(`${claimNo || '该'}单据已完成 AI验审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`)
|
||||
} else {
|
||||
toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`)
|
||||
}
|
||||
router.push({ name: 'app-requests' })
|
||||
}
|
||||
|
||||
function openRequestDetail(request) {
|
||||
router.push({
|
||||
name: 'app-request-detail',
|
||||
params: { requestId: request.claimId || request.id }
|
||||
})
|
||||
}
|
||||
|
||||
function closeRequestDetail() {
|
||||
router.push({ name: 'app-requests' })
|
||||
}
|
||||
|
||||
async function handleRequestUpdated() {
|
||||
await reloadRequests()
|
||||
}
|
||||
|
||||
async function handleRequestDeleted() {
|
||||
await reloadRequests()
|
||||
router.push({ name: 'app-requests' })
|
||||
}
|
||||
|
||||
return {
|
||||
activeRange,
|
||||
activeView,
|
||||
closeRequestDetail,
|
||||
closeSmartEntry,
|
||||
currentView,
|
||||
customRange,
|
||||
detailMode,
|
||||
logDetailMode,
|
||||
filteredRequests,
|
||||
filters,
|
||||
handleApprove,
|
||||
handleDraftSaved,
|
||||
handleNavigate,
|
||||
handleReject,
|
||||
handleRequestDeleted,
|
||||
handleRequestUpdated,
|
||||
navItems,
|
||||
openRequestDetail,
|
||||
openSmartEntry,
|
||||
openTravelCreate,
|
||||
ranges,
|
||||
requestSummary,
|
||||
requestsError,
|
||||
requestsLoading,
|
||||
reloadRequests,
|
||||
requests,
|
||||
search,
|
||||
selectedRequest,
|
||||
setView,
|
||||
smartEntryContext,
|
||||
smartEntryOpen,
|
||||
smartEntrySessionId,
|
||||
detailAlerts,
|
||||
toast,
|
||||
topBarView
|
||||
}
|
||||
}
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useNavigation, navItems } from './useNavigation.js'
|
||||
import { useRequests } from './useRequests.js'
|
||||
import { useSystemState } from './useSystemState.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||
|
||||
const SESSION_TYPE_EXPENSE = 'expense'
|
||||
|
||||
function isPlaceholderValue(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return true
|
||||
}
|
||||
|
||||
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
|
||||
}
|
||||
|
||||
function hasMissingAttachment(request) {
|
||||
const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : []
|
||||
|
||||
if (expenseItems.length) {
|
||||
return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim())
|
||||
}
|
||||
|
||||
const attachmentSummary = String(request?.attachmentSummary || '').trim()
|
||||
const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim()
|
||||
return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue)
|
||||
}
|
||||
|
||||
function hasPendingInfo(request) {
|
||||
if (!request) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return [
|
||||
request.profileDepartment,
|
||||
request.profilePosition,
|
||||
request.profileGrade,
|
||||
request.profileManager,
|
||||
request.reason,
|
||||
request.occurredDisplay
|
||||
].some(isPlaceholderValue)
|
||||
}
|
||||
|
||||
function resolveDetailAlertTone(request) {
|
||||
if (request?.approvalKey === 'completed') return 'success'
|
||||
if (request?.approvalKey === 'rejected') return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function buildDetailAlerts(request) {
|
||||
if (!request) {
|
||||
return []
|
||||
}
|
||||
|
||||
const alerts = []
|
||||
const nodeLabel = String(request.node || request.approval || '').trim()
|
||||
|
||||
if (nodeLabel) {
|
||||
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
|
||||
}
|
||||
|
||||
if (hasMissingAttachment(request)) {
|
||||
alerts.push({ label: '缺少票据', tone: 'warning' })
|
||||
}
|
||||
|
||||
if (hasPendingInfo(request)) {
|
||||
alerts.push({ label: '待补信息', tone: 'warning' })
|
||||
}
|
||||
|
||||
return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3)
|
||||
}
|
||||
|
||||
export function useAppShell() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const smartEntryOpen = ref(false)
|
||||
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
|
||||
const smartEntrySessionId = ref(0)
|
||||
|
||||
const { activeView, currentView, setView } = useNavigation()
|
||||
const {
|
||||
requests,
|
||||
loading: requestsLoading,
|
||||
error: requestsError,
|
||||
search,
|
||||
filters,
|
||||
ranges,
|
||||
activeRange,
|
||||
filteredRequests,
|
||||
approveRequest,
|
||||
rejectRequest,
|
||||
reload: reloadRequests
|
||||
} = useRequests()
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||
|
||||
const selectedRequest = computed(() => {
|
||||
const requestId = String(route.params.requestId || '')
|
||||
|
||||
if (!requestId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rawRequest = requests.value.find(
|
||||
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
||||
)
|
||||
return normalizeRequestForUi(rawRequest)
|
||||
})
|
||||
|
||||
const detailMode = computed(() => route.name === 'app-request-detail')
|
||||
const logDetailMode = computed(() => route.name === 'app-log-detail')
|
||||
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
||||
|
||||
const topBarView = computed(() => {
|
||||
if (detailMode.value) {
|
||||
return {
|
||||
title: '报销单详情',
|
||||
desc: '查看报销明细、票据材料、审批进度与风险提示。'
|
||||
}
|
||||
}
|
||||
|
||||
if (logDetailMode.value) {
|
||||
return {
|
||||
title: '日志详情',
|
||||
desc: '查看单条日志的解析结果、上下文信息与原始记录。'
|
||||
}
|
||||
}
|
||||
|
||||
return currentView.value
|
||||
})
|
||||
|
||||
const requestSummary = computed(() =>
|
||||
filteredRequests.value.reduce(
|
||||
(summary, item) => {
|
||||
const request = normalizeRequestForUi(item)
|
||||
if (!request) {
|
||||
return summary
|
||||
}
|
||||
|
||||
summary.total += 1
|
||||
|
||||
if (request.approvalKey === 'draft') {
|
||||
summary.draft += 1
|
||||
} else if (request.approvalKey === 'in_progress') {
|
||||
summary.inProgress += 1
|
||||
} else if (request.approvalKey === 'supplement') {
|
||||
summary.supplement += 1
|
||||
} else if (request.approvalKey === 'completed') {
|
||||
summary.completed += 1
|
||||
}
|
||||
|
||||
return summary
|
||||
},
|
||||
{ total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 }
|
||||
)
|
||||
)
|
||||
|
||||
function handleApprove(request) {
|
||||
const message = approveRequest(request)
|
||||
toast(message)
|
||||
}
|
||||
|
||||
function handleReject(request) {
|
||||
const message = rejectRequest(request)
|
||||
toast(message)
|
||||
}
|
||||
|
||||
function handleNavigate(view) {
|
||||
smartEntryOpen.value = false
|
||||
setView(view)
|
||||
}
|
||||
|
||||
function openTravelCreate() {
|
||||
smartEntryOpen.value = true
|
||||
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
smartEntryContext.value = {
|
||||
prompt: payload.prompt ?? '',
|
||||
source: payload.source ?? 'workbench',
|
||||
request: payload.request ?? selectedRequest.value,
|
||||
files: Array.isArray(payload.files) ? payload.files : [],
|
||||
conversation
|
||||
}
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
|
||||
function closeSmartEntry() {
|
||||
smartEntryOpen.value = false
|
||||
}
|
||||
|
||||
async function handleDraftSaved(payload = {}) {
|
||||
const claimNo = String(payload.claimNo || payload.claim_no || '').trim()
|
||||
const status = String(payload.status || payload.claimStatus || '').trim()
|
||||
const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim()
|
||||
smartEntryOpen.value = false
|
||||
await reloadRequests()
|
||||
if (status === 'submitted') {
|
||||
toast(`${claimNo || '该'}单据已完成 AI验审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`)
|
||||
} else {
|
||||
toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`)
|
||||
}
|
||||
router.push({ name: 'app-requests' })
|
||||
}
|
||||
|
||||
function openRequestDetail(request) {
|
||||
router.push({
|
||||
name: 'app-request-detail',
|
||||
params: { requestId: request.claimId || request.id }
|
||||
})
|
||||
}
|
||||
|
||||
function closeRequestDetail() {
|
||||
router.push({ name: 'app-requests' })
|
||||
}
|
||||
|
||||
async function handleRequestUpdated() {
|
||||
await reloadRequests()
|
||||
}
|
||||
|
||||
async function handleRequestDeleted() {
|
||||
await reloadRequests()
|
||||
router.push({ name: 'app-requests' })
|
||||
}
|
||||
|
||||
return {
|
||||
activeRange,
|
||||
activeView,
|
||||
closeRequestDetail,
|
||||
closeSmartEntry,
|
||||
currentView,
|
||||
customRange,
|
||||
detailMode,
|
||||
logDetailMode,
|
||||
filteredRequests,
|
||||
filters,
|
||||
handleApprove,
|
||||
handleDraftSaved,
|
||||
handleNavigate,
|
||||
handleReject,
|
||||
handleRequestDeleted,
|
||||
handleRequestUpdated,
|
||||
navItems,
|
||||
openRequestDetail,
|
||||
openSmartEntry,
|
||||
openTravelCreate,
|
||||
ranges,
|
||||
requestSummary,
|
||||
requestsError,
|
||||
requestsLoading,
|
||||
reloadRequests,
|
||||
requests,
|
||||
search,
|
||||
selectedRequest,
|
||||
setView,
|
||||
smartEntryContext,
|
||||
smartEntryOpen,
|
||||
smartEntrySessionId,
|
||||
detailAlerts,
|
||||
toast,
|
||||
topBarView
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user