From 07d414829f4260bb177f383faec9937d613d3c10 Mon Sep 17 00:00:00 2001 From: default Date: Wed, 28 Oct 2026 11:58:04 +0000 Subject: [PATCH] feat: add system management and scheduled task modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move 审计日志 after 系统管理; add 定时任务 submenu and /scheduled-tasks route - Add ScheduledTasksView with cron editing, manual run and run history drawer - Add StyleConfigView and SysConfigView under system management - Add line feedback dialog and branding composable - Remove explanatory subtitles and mock-mode notices from pages - Allow workspace proxy hosts in Vite dev server (allowedHosts, HMR port) Co-Authored-By: Claude Code --- src/api/index.js | 40 +- src/components/LineFeedbackDialog.vue | 520 ++++++++++++++++++++++++++ src/components/StatusTag.vue | 2 + src/composables/useBranding.js | 41 ++ src/layouts/AppLayout.vue | 96 ++++- src/router/index.js | 6 +- src/styles.css | 12 +- src/views/AuditLogsView.vue | 22 +- src/views/DashboardView.vue | 2 - src/views/LoginView.vue | 18 +- src/views/MasterDataView.vue | 10 - src/views/PushTasksView.vue | 17 +- src/views/ScheduledTasksView.vue | 439 ++++++++++++++++++++++ src/views/SourceDocsView.vue | 16 +- src/views/StyleConfigView.vue | 221 +++++++++++ src/views/SysConfigView.vue | 345 +++++++++++++++++ src/views/VoucherDetailView.vue | 254 +++++++++++-- src/views/VouchersView.vue | 8 +- vite.config.js | 9 + 19 files changed, 1975 insertions(+), 103 deletions(-) create mode 100644 src/components/LineFeedbackDialog.vue create mode 100644 src/composables/useBranding.js create mode 100644 src/views/ScheduledTasksView.vue create mode 100644 src/views/StyleConfigView.vue create mode 100644 src/views/SysConfigView.vue diff --git a/src/api/index.js b/src/api/index.js index f8f6103..da99984 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,5 +1,11 @@ import http from './http' +// Voucher generation may call an external LLM synchronously (up to two +// 20-second attempts plus retry backoff), so these endpoints need a much +// longer budget than the global 30-second request timeout. Aborting earlier +// would stop the UI spinner while the backend keeps processing. +const GENERATION_TIMEOUT = 180000 + export const authApi = { login: (data) => http.post('/auth/login', data), me: () => http.get('/auth/me'), @@ -10,11 +16,32 @@ export const dashboardApi = { get: () => http.get('/dashboard') } +export const systemConfigApi = { + get: () => http.get('/system-config'), + update: (data) => http.put('/system-config', data) +} + +export const sysConfigApi = { + list: () => http.get('/sys-config'), + create: (data) => http.post('/sys-config', data), + update: (id, data) => http.put(`/sys-config/${id}`, data), + remove: (id) => http.delete(`/sys-config/${id}`) +} + +export const scheduledJobApi = { + list: () => http.get('/scheduled-jobs'), + update: (id, data) => http.put(`/scheduled-jobs/${id}`, data), + run: (id) => http.post(`/scheduled-jobs/${id}/run`), + runs: (id, params) => http.get(`/scheduled-jobs/${id}/runs`, { params }) +} + export const sourceDocApi = { list: (params) => http.get('/source-docs', { params }), detail: (id) => http.get(`/source-docs/${id}`), - reprocess: (id) => http.post(`/source-docs/${id}/reprocess`), - manualVoucher: (id, data) => http.post(`/source-docs/${id}/manual-voucher`, data) + reprocess: (id) => + http.post(`/source-docs/${id}/reprocess`, null, { timeout: GENERATION_TIMEOUT }), + manualVoucher: (id, data) => + http.post(`/source-docs/${id}/manual-voucher`, data, { timeout: GENERATION_TIMEOUT }) } export const voucherApi = { @@ -23,7 +50,14 @@ export const voucherApi = { updateLines: (id, data) => http.put(`/vouchers/${id}/lines`, data), approve: (id) => http.post(`/vouchers/${id}/approve`), reject: (id, data) => http.post(`/vouchers/${id}/reject`, data), - regenerate: (id) => http.post(`/vouchers/${id}/regenerate`) + regenerate: (id) => + http.post(`/vouchers/${id}/regenerate`, null, { timeout: GENERATION_TIMEOUT }), + feedbackContext: (id, lineNo, params) => + http.get(`/vouchers/${id}/lines/${lineNo}/feedback/context`, { params }), + feedbackTest: (id, lineNo, data) => + http.post(`/vouchers/${id}/lines/${lineNo}/feedback/test`, data), + feedbackApply: (id, lineNo, data) => + http.post(`/vouchers/${id}/lines/${lineNo}/feedback/apply`, data) } export const pushTaskApi = { diff --git a/src/components/LineFeedbackDialog.vue b/src/components/LineFeedbackDialog.vue new file mode 100644 index 0000000..bdac0e5 --- /dev/null +++ b/src/components/LineFeedbackDialog.vue @@ -0,0 +1,520 @@ + + + + + diff --git a/src/components/StatusTag.vue b/src/components/StatusTag.vue index a67e100..baf76bb 100644 --- a/src/components/StatusTag.vue +++ b/src/components/StatusTag.vue @@ -23,6 +23,7 @@ const LABELS = { SUCCESS: '成功', SUCCESS_WITH_EXISTING: '重复已确认', FAILED: '失败', + RUNNING: '运行中', ACTIVE: '启用中', INACTIVE: '已停用', DISABLED: '已停用', @@ -53,6 +54,7 @@ const TYPES = { SUCCESS: 'success', SUCCESS_WITH_EXISTING: 'success', FAILED: 'danger', + RUNNING: 'primary', ACTIVE: 'success', INACTIVE: 'info', DISABLED: 'info', diff --git a/src/composables/useBranding.js b/src/composables/useBranding.js new file mode 100644 index 0000000..555aa94 --- /dev/null +++ b/src/composables/useBranding.js @@ -0,0 +1,41 @@ +import { reactive } from 'vue' +import { systemConfigApi } from '../api' + +const state = reactive({ + platformName: 'AutoVoucher', + logo: null, + loaded: false +}) + +let pending = null + +export function useBranding() { + async function load(force = false) { + if (state.loaded && !force) { + return state + } + if (pending) { + return pending + } + pending = systemConfigApi + .get() + .then((data) => { + apply(data || {}) + state.loaded = true + return state + }) + .catch(() => state) + .finally(() => { + pending = null + }) + return pending + } + + function apply(data) { + state.platformName = data.platformName || 'AutoVoucher' + state.logo = data.logo || null + state.loaded = true + } + + return { branding: state, load, apply } +} diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue index 1f89bbb..22ffe39 100644 --- a/src/layouts/AppLayout.vue +++ b/src/layouts/AppLayout.vue @@ -1,11 +1,15 @@ + + + + diff --git a/src/views/SourceDocsView.vue b/src/views/SourceDocsView.vue index 6d48bcd..294ff8d 100644 --- a/src/views/SourceDocsView.vue +++ b/src/views/SourceDocsView.vue @@ -120,10 +120,12 @@ async function submitManual() { accountCode: manualForm.accountCode, reason: manualForm.reason }) - manualDialog.value = false ElMessage.success(`已按手工科目生成凭证 ${voucher.voucherNo}`) + // Keep the dialog (and its loading state) visible until the refreshed data + // is ready, then close it and navigate to the generated voucher. await reloadDetail() await load() + manualDialog.value = false router.push(`/vouchers/${voucher.id}`) } finally { manualLoading.value = false @@ -164,9 +166,6 @@ watch( @@ -268,6 +267,9 @@ watch( 重新生成 + + 正在调用大模型判定科目,请稍候… + diff --git a/src/views/StyleConfigView.vue b/src/views/StyleConfigView.vue new file mode 100644 index 0000000..9ea6136 --- /dev/null +++ b/src/views/StyleConfigView.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/src/views/SysConfigView.vue b/src/views/SysConfigView.vue new file mode 100644 index 0000000..58ca7ed --- /dev/null +++ b/src/views/SysConfigView.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/src/views/VoucherDetailView.vue b/src/views/VoucherDetailView.vue index 6665297..329a309 100644 --- a/src/views/VoucherDetailView.vue +++ b/src/views/VoucherDetailView.vue @@ -5,6 +5,7 @@ import { masterDataApi, voucherApi } from '../api' import StatusTag from '../components/StatusTag.vue' import JsonBlock from '../components/JsonBlock.vue' import OaViewDialog from '../components/OaViewDialog.vue' +import LineFeedbackDialog from '../components/LineFeedbackDialog.vue' import { formatAmount, formatDateTime } from '../utils/format' import { ElMessage, ElMessageBox } from 'element-plus' @@ -35,6 +36,33 @@ const auxIndex = ref(-1) const auxRows = ref([]) const oaVisible = ref(false) +const previewVisible = ref(false) + +const feedbackVisible = ref(false) +const feedbackLine = ref(null) +const feedbackTab = ref('feedback') + +const SCOPE_LABELS = { LINE_FEEDBACK: '行反馈规则' } + +function scopeLabel(scope) { + return SCOPE_LABELS[scope] || scope || '-' +} + +function canFeedback(row) { + return ( + row.lineNo != null && + row.direction === 'DEBIT' && + row.accountGroup !== 'INPUT_VAT' && + canRegenerate.value && + voucher.value?.currentFlag === 1 + ) +} + +function openFeedback(row, tab) { + feedbackLine.value = row + feedbackTab.value = tab + feedbackVisible.value = true +} const isPending = computed(() => voucher.value?.status === 'PENDING_REVIEW') const canRegenerate = computed( @@ -93,6 +121,7 @@ function auxCount(value) { function toEditLine(line) { return { _key: ++rowSeq, + lineNo: line.lineNo, direction: line.direction, accountCode: line.accountCode, amount: Number(line.amount ?? 0), @@ -101,6 +130,7 @@ function toEditLine(line) { taxAmount: line.taxAmount === null || line.taxAmount === undefined ? null : Number(line.taxAmount), aux: parseAux(line.auxJson), summary: line.summary || '', + sourceLineNo: line.sourceLineNo ?? null, evidenceJson: line.evidenceJson, accountGroup: line.accountGroup, decisionSource: line.decisionSource, @@ -152,16 +182,48 @@ const totals = computed(() => { return { debit, credit, diff: Math.round((debit - credit) * 100) / 100 } }) +// Preview follows the current edit state, so finance can check the voucher shape +// before saving changes. +const previewLines = computed(() => { + const lines = isPending.value ? editLines.value : voucher.value?.lines || [] + return lines.map((line) => ({ + lineNo: line.lineNo, + summary: line.summary || '', + accountCode: line.accountCode, + accountName: line.accountName || accountByCode(line.accountCode)?.name || '', + aux: auxText(line.auxJson ?? line.aux), + debit: line.direction === 'DEBIT' ? Number(line.amount) || 0 : 0, + credit: line.direction === 'CREDIT' ? Number(line.amount) || 0 : 0 + })) +}) + +function previewSummary({ columns }) { + return columns.map((column, index) => { + if (index === 0) { + return '合计' + } + if (column.label === '借方金额') { + return formatAmount(totals.value.debit, '') + } + if (column.label === '贷方金额') { + return formatAmount(totals.value.credit, '') + } + return '' + }) +} + function addLine() { editLines.value.push({ _key: ++rowSeq, + lineNo: null, direction: 'DEBIT', accountCode: '', amount: 0, taxRatePercent: null, taxAmount: null, aux: {}, - summary: '' + summary: '', + sourceLineNo: null }) } @@ -285,7 +347,8 @@ async function doSave() { taxAmount: line.taxAmount === null || line.taxAmount === undefined ? null : Number(line.taxAmount), aux: line.aux || {}, - summary: line.summary || '' + summary: line.summary || '', + sourceLineNo: line.sourceLineNo ?? null })) } const detail = await voucherApi.updateLines(voucher.value.id, payload) @@ -389,6 +452,7 @@ onMounted(load) 历史版本
+ {{ voucher.voucherTypeCode }} · {{ voucher.period }} · {{ voucher.companyId }}/{{ voucher.bookId }} · 版本 v{{ voucher.version }} @@ -397,9 +461,13 @@ onMounted(load)
返回列表 OA 查看凭证 + 凭证预览 重新生成 + + 正在调用大模型判定科目,请稍候… + - + - - - - + @@ -635,7 +694,9 @@ onMounted(load) - + + + @@ -838,6 +964,50 @@ onMounted(load) margin-bottom: 10px; } +.lines-summary { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 22px; +} + +.summary-item { + font-size: 13px; +} + +.summary-item .amount { + font-size: 15px; + font-variant-numeric: tabular-nums; +} + +.voucher-preview { + padding: 4px 6px; +} + +.preview-title { + text-align: center; + font-size: 22px; + font-weight: 600; + letter-spacing: 8px; + margin-bottom: 16px; +} + +.preview-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 28px; + font-size: 13px; + margin-bottom: 12px; +} + +.preview-foot { + display: flex; + justify-content: space-between; + gap: 24px; + margin-top: 12px; + font-size: 13px; +} + .line-extra { padding: 8px 16px; display: flex; @@ -865,4 +1035,10 @@ onMounted(load) align-items: center; margin-bottom: 8px; } + +.generating-hint { + align-self: center; + font-size: 12px; + white-space: nowrap; +} diff --git a/src/views/VouchersView.vue b/src/views/VouchersView.vue index e6a4c91..b98b0ab 100644 --- a/src/views/VouchersView.vue +++ b/src/views/VouchersView.vue @@ -46,9 +46,6 @@ onMounted(load) @@ -66,7 +63,7 @@ onMounted(load) + + + diff --git a/vite.config.js b/vite.config.js index 015a961..0cecc5a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,12 +2,21 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' const backend = process.env.VITE_BACKEND_URL || 'http://localhost:8080' +// When the workspace is served through the HTTPS proxy, HMR must connect to +// the proxy port instead of the Vite port. +const hmrClientPort = process.env.VITE_HMR_CLIENT_PORT + ? Number(process.env.VITE_HMR_CLIENT_PORT) + : undefined export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 5173, + // The workspace is reachable through a per-user proxy domain such as + // 5174--main--zjx-code--zhangjianxue.code.connectconcur.com. + allowedHosts: ['.code.connectconcur.com'], + hmr: hmrClientPort ? { clientPort: hmrClientPort } : undefined, proxy: { '/api': { target: backend, changeOrigin: true }, '/open': { target: backend, changeOrigin: true }