import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import AuditAssetList from '../../components/audit/AuditAssetList.vue' import AuditJsonRiskRuleDetail from '../../components/audit/AuditJsonRiskRuleDetail.vue' import AuditRuleDialogs from '../../components/audit/AuditRuleDialogs.vue' import AuditSpreadsheetChangeDrawer from '../../components/audit/AuditSpreadsheetChangeDrawer.vue' import AuditSpreadsheetRuleDetail from '../../components/audit/AuditSpreadsheetRuleDetail.vue' import AuditVersionTimelineDrawer from '../../components/audit/AuditVersionTimelineDrawer.vue' import TableLoadingState from '../../components/shared/TableLoadingState.vue' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { isFinanceUser, isManagerUser, isPlatformAdminUser } from '../../utils/accessControl.js' import { useAuditAssetData } from './useAuditAssetData.js' import { buildAuditDetailTopBar } from './auditViewDetailTopBar.js' import { useAuditListFilters } from './auditViewListFilters.js' import { useAuditRuleReviewFlow } from './useAuditRuleReviewFlow.js' import { useAuditRuleVersionActions } from './useAuditRuleVersionActions.js' import { useAuditRiskRuleActions } from './useAuditRiskRuleActions.js' import { useAuditRiskRuleCreateFlow } from './useAuditRiskRuleCreateFlow.js' import { useAuditRiskRuleJsonEditor } from './useAuditRiskRuleJsonEditor.js' import { useAuditSpreadsheetEditor } from './useAuditSpreadsheetEditor.js' import { useAuditVersionTimeline } from './useAuditVersionTimeline.js' import { TAB_META, STATUS_OPTIONS, ENABLED_STATE_OPTIONS, ONLINE_STATE_OPTIONS, RISK_SCENARIO_OPTIONS, normalizeText, readConfigJson, buildPreviewRuleDetail, } from './auditViewModel.js' import { RISK_RULE_BUSINESS_STAGE_OPTIONS, RISK_RULE_EXPENSE_CATEGORY_OPTIONS, } from './auditViewRiskRuleModel.js' export default { name: 'AuditView', components: { AuditAssetList, AuditJsonRiskRuleDetail, AuditRuleDialogs, AuditSpreadsheetChangeDrawer, AuditSpreadsheetRuleDetail, AuditVersionTimelineDrawer, TableLoadingState }, emits: ['detail-open-change', 'detail-topbar-change'], setup(_, { emit }) { const { toast } = useToast() const { currentUser } = useSystemState() const tabs = Object.entries(TAB_META).map(([id, meta]) => ({ id, label: meta.label })) const activeType = ref('financialRules') const selectedSkill = ref(null) const keyword = ref('') const activeFilterPopover = ref('') const selectedDomain = ref('') const selectedOwner = ref('') const selectedRiskLevel = ref('') const selectedStatus = ref('') const selectedRiskScenario = ref('') const selectedOnlineState = ref('') const selectedEnabledState = ref('') const actionState = ref('') const riskRuleAttachmentOptions = [ { label: '是', value: true }, { label: '否', value: false } ] const isAdmin = computed(() => isPlatformAdminUser(currentUser.value)) const isRuleManager = computed(() => isManagerUser(currentUser.value)) const isFinance = computed(() => isFinanceUser(currentUser.value)) const activeMeta = computed(() => TAB_META[activeType.value]) const activeTabLabel = computed(() => activeMeta.value.label) const searchPlaceholder = computed(() => activeMeta.value.searchPlaceholder) const createButtonLabel = computed(() => activeMeta.value.createButtonLabel) const hintText = computed(() => activeMeta.value.hintText) const tableColumns = computed(() => activeMeta.value.tableColumns) const showRuntimeColumn = computed(() => activeMeta.value.showRuntimeColumn !== false) const showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false) const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false) const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false) const showOnlineColumn = computed(() => false) const showEnabledColumn = computed(() => activeMeta.value.showEnabledColumn === true) const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules') const selectedSkillUsesSpreadsheet = computed( () => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule) ) const selectedSkillUsesJsonRisk = computed( () => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule) ) const canManageSelected = computed( () => isRuleManager.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock ) const canAdminOperateSelected = computed( () => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock ) const canEditSelected = computed( () => Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock && (isAdmin.value || isFinance.value) ) const latestRiskRuleTestSummary = computed(() => selectedSkill.value?.latestTestSummary || null) const riskRuleTestPassed = computed(() => Boolean(latestRiskRuleTestSummary.value?.test_passed)) const riskRuleInReview = computed( () => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'review' ) const riskRuleGenerationBusy = computed( () => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'generating' ) const riskRuleGenerationFailed = computed( () => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'failed' ) const canOpenRiskRuleTest = computed( () => selectedSkillUsesJsonRisk.value && canAdminOperateSelected.value && Boolean(selectedSkill.value?.id) && !riskRuleGenerationBusy.value && !riskRuleGenerationFailed.value ) const canDeleteRiskRule = computed( () => selectedSkillUsesJsonRisk.value && canAdminOperateSelected.value && Boolean(selectedSkill.value?.id) && !normalizeText(selectedSkill.value?.publishedVersion).replace('-', '') ) const canOpenRiskRuleReviewSubmit = computed(() => false) const canSubmitRiskRuleReview = computed( () => canOpenRiskRuleReviewSubmit.value && riskRuleTestPassed.value ) const canReturnRiskRule = computed(() => false) const riskRuleHasPublishableRevision = computed(() => { const revision = selectedSkill.value?.configJson?.revision_draft return selectedSkillUsesJsonRisk.value && revision && revision.generation_status === 'completed' && normalizeText(selectedSkill.value?.workingVersion).replace('-', '') && selectedSkill.value?.workingVersion !== selectedSkill.value?.publishedVersion }) const canPublishRiskRule = computed( () => Boolean(riskRuleHasPublishableRevision.value) && canManageSelected.value && riskRuleTestPassed.value && !detailBusy.value ) const canToggleRiskRuleEnabled = computed( () => selectedSkillUsesJsonRisk.value && canManageSelected.value ) const canEditRiskRuleDraft = computed( () => selectedSkillUsesJsonRisk.value && (canEditSelected.value || canManageSelected.value) && !detailBusy.value && !riskRuleGenerationBusy.value && !normalizeText(selectedSkill.value?.publishedVersion).replace('-', '') ) const canCreateRiskRuleRevision = computed( () => selectedSkillUsesJsonRisk.value && (canEditSelected.value || canManageSelected.value) && !detailBusy.value && !riskRuleGenerationBusy.value && !riskRuleGenerationFailed.value && Boolean(normalizeText(selectedSkill.value?.publishedVersion).replace('-', '')) ) const canEditMarkdown = computed(() => selectedSkillIsRule.value && canEditSelected.value) const isDisplayingWorkingVersion = computed( () => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion ) const canUploadSpreadsheet = computed( () => canEditSelected.value && selectedSkillUsesSpreadsheet.value && !detailBusy.value ) const canDownloadSpreadsheet = computed( () => selectedSkillUsesSpreadsheet.value && Boolean(selectedSkill.value?.id) && !detailBusy.value ) const canEditSpreadsheetInline = computed( () => selectedSkillUsesSpreadsheet.value && (selectedSkill.value?.isPreviewMock || canEditSelected.value) ) const selectedSpreadsheetFileName = computed( () => normalizeText(selectedSkill.value?.ruleDocument?.file_name) || '未上传规则表' ) const { versionSwitchTarget, versionTimelineOpen, versionTimelineLoading, versionTimelineError, selectedVersionTimelineItems, loadVersionTimeline: loadVersionTimelineInState, openVersionTimeline: openVersionTimelineInState, closeVersionTimeline: closeVersionTimelineInState, clearVersionTimelineState, openVersionSwitch: openVersionSwitchInState, cancelVersionSwitch: cancelVersionSwitchInState, confirmVersionSwitch: confirmVersionSwitchInState } = useAuditVersionTimeline({ selectedSkill, toast }) const { loadRiskRuleJson, saveRiskRuleJson, formatRiskRuleJson, downloadRiskRuleJson } = useAuditRiskRuleJsonEditor({ selectedSkill, canEditMarkdown, actionState, toast }) const detailBusy = computed(() => Boolean(actionState.value)) const { loading, errorMessage, detailLoading, detailError, assetBuckets, currentAssets, loadRuns, loadAssets, refreshCurrentAssets, loadSelectedAssetDetail, mergeSelectedRuleLifecycle } = useAuditAssetData({ activeType, activeMeta, selectedSkill, loadVersionTimeline: loadVersionTimelineInState, loadSpreadsheetChangeRecords: (...args) => loadSpreadsheetChangeRecords(...args), loadRiskRuleJson, toast }) const { spreadsheetUploadInput, spreadsheetOnlyOfficeLoading, spreadsheetOnlyOfficeError, spreadsheetOnlyOfficeReady, spreadsheetOnlyOfficeHostId, spreadsheetChangeDetailOpen, selectedSpreadsheetChangeRecord, selectedSpreadsheetChangeRecords, selectedSpreadsheetChangeSheetRows, selectedSpreadsheetChangeCellRows, destroySpreadsheetOnlyOfficeEditor, mountSpreadsheetOnlyOfficeEditor, triggerSpreadsheetUpload, downloadSpreadsheetFile, handleSpreadsheetFileInput, loadSpreadsheetChangeRecords, openSpreadsheetChangeDetail, closeSpreadsheetChangeDetail } = useAuditSpreadsheetEditor({ selectedSkill, selectedSkillUsesSpreadsheet, canEditSpreadsheetInline, canUploadSpreadsheet, canDownloadSpreadsheet, selectedSpreadsheetFileName, actionState, refreshCurrentAssets, loadSelectedAssetDetail, resolveActor, toast }) const { canCreateRiskRule, riskRuleCreateOpen, riskRuleCreateForm, riskRuleCreateBusy, openRiskRuleCreateDialog, closeRiskRuleCreateDialog, submitRiskRuleCreate, stopAllRiskRuleGenerationPolls } = useAuditRiskRuleCreateFlow({ activeType, isRuleManager, detailBusy, actionState, assetBuckets, refreshCurrentAssets, resolveActor, toast }) const { saveRuleMarkdown, saveRuleRuntimeJson, activateSelectedRule, restoreSelectedVersion } = useAuditRuleVersionActions({ selectedSkill, selectedSkillIsRule, canEditMarkdown, canManageSelected, actionState, detailBusy, refreshCurrentAssets, loadSelectedAssetDetail, resolveActor, toast }) const { reviewSubmitOpen, reviewSubmitVersion, reviewSubmitReviewer, reviewSubmitReviewerLoading, reviewSubmitReviewerOptions, canSubmitReview, hasReviewSubmitReviewers, canReviewSelected, reviewSelectedRule, openSubmitReviewDialog, closeSubmitReviewDialog, submitSelectedRuleForReview } = useAuditRuleReviewFlow({ selectedSkill, selectedSkillIsRule, selectedSkillUsesJsonRisk, canEditSelected, canManageSelected, isDisplayingWorkingVersion, canOpenRiskRuleReviewSubmit, riskRuleTestPassed, detailBusy, actionState, refreshCurrentAssets, loadSelectedAssetDetail, resolveActor, toast }) const { riskRuleTestOpen, riskRuleDeleteOpen, riskRuleReturnOpen, riskRulePublishOpen, riskRuleReturnNote, riskRuleEditOpen, riskRuleEditMode, riskRuleEditForm, resetRiskRuleActionDialogs, openRiskRuleTestDialog, closeRiskRuleTestDialog, handleRiskRuleReportSaved, openRiskRuleEditDialog, closeRiskRuleEditDialog, submitRiskRuleEdit, openDeleteRiskRuleDialog, closeDeleteRiskRuleDialog, deleteSelectedRiskRule, openReturnRiskRuleDialog, closeReturnRiskRuleDialog, returnSelectedRiskRule, openPublishRiskRuleDialog, closePublishRiskRuleDialog, publishSelectedRiskRule, toggleSelectedRiskRuleEnabled } = useAuditRiskRuleActions({ selectedSkill, detailBusy, actionState, canOpenRiskRuleTest, canDeleteRiskRule, canReturnRiskRule, canPublishRiskRule, riskRuleHasPublishableRevision, canToggleRiskRuleEnabled, canEditRiskRuleDraft, canCreateRiskRuleRevision, riskRuleTestPassed, refreshCurrentAssets, loadSelectedAssetDetail, mergeSelectedRuleLifecycle, closeDetail, resolveActor, toast }) const showReviewNote = computed( () => selectedSkillIsRule.value && (selectedSkill.value?.reviewNote || selectedSkill.value?.reviewTimeLabel) ) const { activeFilterTokens, auditEmptyState, domainOptions, ownerOptions, riskLevelOptions, selectedDomainLabel, selectedEnabledStateLabel, selectedOnlineStateLabel, selectedOwnerLabel, selectedRiskLevelLabel, selectedRiskScenarioLabel, selectedStatusLabel, showEnabledFilter, showOnlineFilter, showOwnerFilter, showRiskLevelFilter, showRiskScenarioFilter, showStatusFilter, visibleSkills } = useAuditListFilters({ activeType, activeTabLabel, currentAssets, keyword, selectedDomain, selectedEnabledState, selectedOnlineState, selectedOwner, selectedRiskLevel, selectedRiskScenario, selectedStatus }) const canActivateSelected = computed(() => { if (!selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) { return false } return ( isDisplayingWorkingVersion.value && selectedSkill.value?.reviewStatusValue === 'approved' && selectedSkill.value?.workingVersion !== selectedSkill.value?.publishedVersion ) }) const activateBlockedReason = computed(() => { if (!selectedSkillIsRule.value) { return '' } if (selectedSkill.value?.isPreviewMock) { return '当前为页面预览态,暂不执行真实审核和上线。' } if (!canManageSelected.value) { return '仅高级财务人员可执行审核和上线。' } if (!isDisplayingWorkingVersion.value) { return '请先切回当前工作版本,再执行审核或上线。' } if (selectedSkill.value?.workingVersion === selectedSkill.value?.publishedVersion) { return '当前工作版本已经是线上版本。' } if (selectedSkill.value?.reviewStatusValue !== 'approved') { return '当前规则版本未审核通过,不能上线。' } return '' }) const auditDetailTopBar = computed(() => buildAuditDetailTopBar({ skill: selectedSkill.value, usesJsonRiskRule: selectedSkillUsesJsonRisk.value }) ) watch( selectedSkill, (value) => { emit('detail-open-change', Boolean(value)) }, { immediate: true } ) watch( auditDetailTopBar, (value) => { emit('detail-topbar-change', value) }, { immediate: true, deep: true } ) watch( () => [ selectedSkill.value?.id || '', selectedSkill.value?.loading ? '1' : '0', selectedSkill.value?.usesSpreadsheetRule ? '1' : '0' ], async () => { if (!selectedSkillUsesSpreadsheet.value || selectedSkill.value?.loading) { destroySpreadsheetOnlyOfficeEditor() spreadsheetOnlyOfficeError.value = '' spreadsheetOnlyOfficeLoading.value = false return } await mountSpreadsheetOnlyOfficeEditor() } ) watch(activeType, () => { destroySpreadsheetOnlyOfficeEditor() selectedSkill.value = null clearVersionTimelineState() resetFilters() loadAssets({ force: true }).catch((error) => { errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。' }) }) function resetFilters() { keyword.value = '' selectedDomain.value = '' selectedOwner.value = '' selectedRiskLevel.value = '' selectedStatus.value = '' selectedRiskScenario.value = '' selectedOnlineState.value = '' selectedEnabledState.value = '' activeFilterPopover.value = '' } function handleAuditEmptyAction() { if (!currentAssets.value.length || !activeFilterTokens.value.length) { loadAssets({ force: true }).catch(() => {}) return } resetFilters() } function toggleFilterPopover(name) { activeFilterPopover.value = activeFilterPopover.value === name ? '' : name } function closeFilterPopover() { activeFilterPopover.value = '' } function selectFilter(name, value) { if (name === 'domain') { selectedDomain.value = value } if (name === 'owner') { selectedOwner.value = value } if (name === 'riskLevel') { selectedRiskLevel.value = value } if (name === 'status') { selectedStatus.value = value } if (name === 'riskScenario') { selectedRiskScenario.value = value } if (name === 'online') { selectedOnlineState.value = value } if (name === 'enabled') { selectedEnabledState.value = value } closeFilterPopover() } function handleDocumentClick(event) { const target = event.target if (!(target instanceof Element)) { closeFilterPopover() return } if (!target.closest('.picker-filter')) { closeFilterPopover() } } function resolveActor() { return currentUser.value?.name || currentUser.value?.username || 'system' } function openAssetDetail(asset) { if (asset?.usesJsonRiskRule && asset.statusValue === 'generating') { toast('规则仍在后台生成中,生成完成后才能进入详情。') return } destroySpreadsheetOnlyOfficeEditor() spreadsheetOnlyOfficeError.value = '' spreadsheetOnlyOfficeLoading.value = false if (asset?.isPreviewMock) { selectedSkill.value = buildPreviewRuleDetail() detailError.value = '' detailLoading.value = false clearVersionTimelineState() return } const opensSpreadsheetRule = Boolean(asset?.usesSpreadsheetRule) selectedSkill.value = { ...asset, configJson: {}, isPreviewMock: false, usesSpreadsheetRule: opensSpreadsheetRule, usesJsonRiskRule: Boolean(asset?.usesJsonRiskRule), riskRuleJsonText: '{}', riskRuleSummary: null, riskRuleDescription: '', riskRuleSourceRef: '', ruleDocument: asset?.ruleDocument || null, scenarioList: [], fields: [], promptSections: [], outputRules: [], tests: [], triggers: [], tools: [], history: [], markdownContent: '', runtimeRuleText: '', ruleTemplateKey: '', ruleTemplateLabel: '', runtimeKind: 'policy_rule_draft', displayVersion: asset.version, displayVersionChangeNote: '无版本说明', loading: !opensSpreadsheetRule, reviewStatusLabel: opensSpreadsheetRule ? '' : '加载中', reviewStatusTone: 'draft' } clearVersionTimelineState() if (opensSpreadsheetRule) { loadSpreadsheetChangeRecords(asset.id).catch(() => {}) } loadSelectedAssetDetail(asset.id).catch(() => {}) } function closeDetail() { destroySpreadsheetOnlyOfficeEditor() spreadsheetOnlyOfficeError.value = '' spreadsheetOnlyOfficeLoading.value = false selectedSkill.value = null detailError.value = '' detailLoading.value = false clearVersionTimelineState() resetRiskRuleActionDialogs() } onMounted(() => { document.addEventListener('click', handleDocumentClick) loadAssets({ force: true }).catch(() => {}) loadRuns().catch(() => {}) }) onBeforeUnmount(() => { destroySpreadsheetOnlyOfficeEditor() stopAllRiskRuleGenerationPolls() document.removeEventListener('click', handleDocumentClick) }) return { tabs, activeType, activeTabLabel, selectedSkill, versionSwitchTarget, keyword, createButtonLabel, hintText, searchPlaceholder, tableColumns, showRuntimeColumn, showVersionColumn, showMetricColumn, showStatusColumn, showOnlineColumn, showEnabledColumn, visibleSkills, auditEmptyState, loading, errorMessage, detailLoading, detailError, selectedDomain, selectedOwner, selectedRiskLevel, selectedStatus, selectedRiskScenario, selectedOnlineState, selectedEnabledState, selectedDomainLabel, selectedOwnerLabel, selectedRiskLevelLabel, selectedStatusLabel, selectedRiskScenarioLabel, selectedOnlineStateLabel, selectedEnabledStateLabel, showRiskScenarioFilter, showOwnerFilter, showRiskLevelFilter, showStatusFilter, showOnlineFilter, showEnabledFilter, domainOptions, ownerOptions, riskLevelOptions, statusOptions: STATUS_OPTIONS, riskScenarioOptions: RISK_SCENARIO_OPTIONS, onlineStateOptions: ONLINE_STATE_OPTIONS, enabledStateOptions: ENABLED_STATE_OPTIONS, activeFilterPopover, activeFilterTokens, canManageSelected, canEditSelected, canCreateRiskRule, canOpenRiskRuleTest, canDeleteRiskRule, canOpenRiskRuleReviewSubmit, canSubmitRiskRuleReview, canReturnRiskRule, canPublishRiskRule, canToggleRiskRuleEnabled, riskRuleTestPassed, riskRuleInReview, canSubmitReview, hasReviewSubmitReviewers, canReviewSelected, canEditMarkdown, canUploadSpreadsheet, canDownloadSpreadsheet, canEditSpreadsheetInline, canActivateSelected, activateBlockedReason, selectedSkillIsRule, selectedSkillUsesSpreadsheet, selectedSkillUsesJsonRisk, selectedSpreadsheetFileName, selectedVersionTimelineItems, selectedSpreadsheetChangeRecords, detailBusy, actionState, reviewSubmitOpen, reviewSubmitVersion, reviewSubmitReviewer, reviewSubmitReviewerLoading, reviewSubmitReviewerOptions, riskRuleAttachmentOptions, riskRuleCreateOpen, riskRuleCreateForm, riskRuleCreateBusy, riskRuleTestOpen, riskRuleDeleteOpen, riskRuleReturnOpen, riskRulePublishOpen, riskRuleReturnNote, riskRuleEditOpen, riskRuleEditMode, riskRuleEditForm, riskRuleBusinessStageOptions: RISK_RULE_BUSINESS_STAGE_OPTIONS, riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS, showReviewNote, spreadsheetUploadInput, spreadsheetOnlyOfficeLoading, spreadsheetOnlyOfficeError, spreadsheetOnlyOfficeReady, spreadsheetOnlyOfficeHostId, versionTimelineOpen, versionTimelineLoading, versionTimelineError, spreadsheetChangeDetailOpen, selectedSpreadsheetChangeRecord, selectedSpreadsheetChangeSheetRows, selectedSpreadsheetChangeCellRows, openAssetDetail, closeDetail, resetFilters, handleAuditEmptyAction, toggleFilterPopover, selectFilter, closeFilterPopover, openRiskRuleCreateDialog, closeRiskRuleCreateDialog, submitRiskRuleCreate, openVersionSwitch: openVersionSwitchInState, cancelVersionSwitch: cancelVersionSwitchInState, confirmVersionSwitch: confirmVersionSwitchInState, saveRuleMarkdown, saveRuleRuntimeJson, saveRiskRuleJson, formatRiskRuleJson, downloadRiskRuleJson, triggerSpreadsheetUpload, downloadSpreadsheetFile, handleSpreadsheetFileInput, reviewSelectedRule, openSubmitReviewDialog, closeSubmitReviewDialog, submitSelectedRuleForReview, openRiskRuleTestDialog, closeRiskRuleTestDialog, handleRiskRuleReportSaved, openRiskRuleEditDialog, closeRiskRuleEditDialog, submitRiskRuleEdit, openDeleteRiskRuleDialog, closeDeleteRiskRuleDialog, deleteSelectedRiskRule, openReturnRiskRuleDialog, closeReturnRiskRuleDialog, returnSelectedRiskRule, openPublishRiskRuleDialog, closePublishRiskRuleDialog, publishSelectedRiskRule, toggleSelectedRiskRuleEnabled, activateSelectedRule, restoreSelectedVersion, openVersionTimeline: openVersionTimelineInState, closeVersionTimeline: closeVersionTimelineInState, openSpreadsheetChangeDetail, closeSpreadsheetChangeDetail, loadAssets } } }