feat(cma): treasury stage-1 MVP - migration, models, endpoints, rules, 2 pages
G1 review fixes P0-1/P0-2/P1-3/P1-4/P1-5/P2-6/P2-7/P2-8 applied: - up() filters comment lines then asserts missing_tables == [] (CREATE hits 6/6) - created counter = new-object diff via information_schema (idempotent second run = 0) - down() drops columns per-column, dumps via write-path creds + fail-closed - seeds expanded to 8 rules + 6 limits; data_status DEFAULT estimated - no physical FK (app-layer checks), amount FLOAT wan-yuan, round(x,4) Scope: 6 treasury_* tables + cash_plans 4 added columns; cash.py untouched. Not pushed: deploy is approval-gated (de851257).
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<div class="treasury-payment-page">
|
||||
<div class="page-header">
|
||||
<h2>🔒 资金内控</h2>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" size="small" @click="loadAll">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- ══ Tab 1 支付单 ══ -->
|
||||
<el-tab-pane label="支付单" name="payments">
|
||||
<el-card shadow="never">
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="payFilter.status" size="small" clearable placeholder="状态" style="width:120px">
|
||||
<el-option value="draft" label="草稿" />
|
||||
<el-option value="precheck" label="已拦截" />
|
||||
<el-option value="pending_approval" label="待审批" />
|
||||
<el-option value="approved" label="已放行" />
|
||||
<el-option value="paid" label="已支付" />
|
||||
<el-option value="rejected" label="已驳回" />
|
||||
</el-select>
|
||||
<el-select v-model="payFilter.check_result" size="small" clearable placeholder="预检结果" style="width:120px">
|
||||
<el-option value="pass" label="通过" />
|
||||
<el-option value="warn" label="警告" />
|
||||
<el-option value="blocked" label="拦截" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadPayments">查询</el-button>
|
||||
<el-button size="small" @click="payFilter = { status: '', check_result: '' }">重置</el-button>
|
||||
<el-button type="success" size="small" @click="openPayDialog(null)">新建支付单</el-button>
|
||||
</div>
|
||||
<el-table :data="payments" size="small" v-loading="paymentsLoading" style="margin-top:8px">
|
||||
<el-table-column prop="pay_no" label="单号" width="140" />
|
||||
<el-table-column label="付款账户" min-width="170">
|
||||
<template #default="{ row }">{{ accountLabel(row.from_account_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="to_name" label="收款方" min-width="120" />
|
||||
<el-table-column label="金额(万)" width="100">
|
||||
<template #default="{ row }"><span style="font-weight:600">¥{{ (row.amount ?? 0).toFixed(2) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="purpose" label="用途" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="pay_type" label="类型" width="80" />
|
||||
<el-table-column prop="pay_date" label="付款日期" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="check_result" label="预检" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.check_result" :type="checkType(row.check_result)" size="small">{{ checkLabel(row.check_result) }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="双签" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.dual_sign_effective === 1" type="success" size="small">双签有效</el-tag>
|
||||
<el-tag v-else-if="row.dual_sign_effective === 0" type="danger" size="small">形式双签</el-tag>
|
||||
<el-tag v-else-if="(row.required_approvals ?? 0) >= 2" type="info" size="small">待双签</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submitter" label="提交人" width="90" />
|
||||
<el-table-column label="操作" width="210" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 'draft'">
|
||||
<el-button type="primary" size="small" @click="doPrecheck(row)">预检</el-button>
|
||||
<el-button size="small" @click="openPayDialog(row)">编辑</el-button>
|
||||
</template>
|
||||
<template v-else-if="row.status === 'precheck'">
|
||||
<el-button type="primary" size="small" @click="doPrecheck(row)">预检</el-button>
|
||||
<el-button v-if="row.check_result === 'blocked'" type="warning" size="small" @click="openWaive(row)">豁免</el-button>
|
||||
</template>
|
||||
<template v-else-if="row.status === 'pending_approval'">
|
||||
<el-button type="primary" size="small" @click="openApprove(row)">审批</el-button>
|
||||
</template>
|
||||
<template v-else-if="row.status === 'approved'">
|
||||
<el-button type="success" size="small" @click="doPay(row)">登记支付</el-button>
|
||||
</template>
|
||||
<span v-else-if="row.status === 'paid'" style="color:#bbb;font-size:12px">只读</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ══ Tab 2 规则命中留痕 ══ -->
|
||||
<el-tab-pane label="规则命中留痕" name="hits">
|
||||
<el-card shadow="never">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="hitsFilter.rule_code" size="small" clearable placeholder="规则码" style="width:120px" />
|
||||
<el-select v-model="hitsFilter.verdict" size="small" clearable placeholder="判定" style="width:110px">
|
||||
<el-option value="blocked" label="拦截" />
|
||||
<el-option value="warn" label="警告" />
|
||||
<el-option value="waived" label="豁免" />
|
||||
<el-option value="pass" label="通过" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadHits">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="hits" size="small" v-loading="hitsLoading" style="margin-top:8px">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="hit-detail">
|
||||
<div v-for="(v, k) in row.detail || {}" :key="k" class="hit-detail-row">
|
||||
<span class="hit-detail-key">{{ k }}</span>
|
||||
<span class="hit-detail-val">{{ typeof v === 'object' ? JSON.stringify(v) : v }}</span>
|
||||
</div>
|
||||
<div v-if="!row.detail || !Object.keys(row.detail).length">无结构化明细</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="150">
|
||||
<template #default="{ row }">{{ fmtTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对象" width="110">
|
||||
<template #default="{ row }">{{ row.target_type }}#{{ row.target_id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rule_code" label="规则码" width="100" />
|
||||
<el-table-column prop="verdict" label="判定" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.verdict === 'blocked'" type="danger" size="small">拦截</el-tag>
|
||||
<el-tag v-else-if="row.verdict === 'warn'" type="warning" size="small">警告</el-tag>
|
||||
<el-tag v-else-if="row.verdict === 'waived'" size="small" style="background:#f9f0ff;color:#722ed1;border-color:#d3adf7">豁免</el-tag>
|
||||
<el-tag v-else-if="row.verdict === 'pass'" type="success" size="small">通过</el-tag>
|
||||
<el-tag v-else type="info" size="small">{{ row.verdict }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="waived_by" label="豁免人" width="90" />
|
||||
<el-table-column label="摘要" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.waived_reason || row.detail?.verdict_basis || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ══ Tab 3 规则库 / 授权矩阵 ══ -->
|
||||
<el-tab-pane label="规则库 / 授权矩阵" name="rules">
|
||||
<el-card shadow="never">
|
||||
<template #header>规则库(启用/停用即时生效,params 原样回传)</template>
|
||||
<el-table :data="rules" size="small" v-loading="rulesLoading">
|
||||
<el-table-column prop="rule_code" label="规则码" width="90" />
|
||||
<el-table-column prop="rule_name" label="规则名" min-width="160" />
|
||||
<el-table-column prop="rule_type" label="类型" width="90" />
|
||||
<el-table-column prop="severity" label="严重度" width="80" />
|
||||
<el-table-column prop="rule_version" label="版本" width="70" />
|
||||
<el-table-column label="参数" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="JSON.stringify(row.params || {})" placement="top">
|
||||
<span class="params-preview">{{ JSON.stringify(row.params || {}) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="启用" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-switch :model-value="row.enabled === 1" @change="(v: any) => toggleRule(row, v)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-card shadow="never" style="margin-top:16px">
|
||||
<template #header>授权矩阵(金额段为左闭右开 [from, to),单位万元)</template>
|
||||
<el-table :data="limits" size="small" v-loading="rulesLoading">
|
||||
<el-table-column prop="scope_type" label="范围类型" width="100" />
|
||||
<el-table-column prop="scope_value" label="范围值" width="100" />
|
||||
<el-table-column label="金额段(万)" width="180">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.amount_to != null ? `[${(row.amount_from ?? 0).toFixed(2)}, ${(row.amount_to ?? 0).toFixed(2)})` : `${(row.amount_from ?? 0).toFixed(2)} 及以上` }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所需审批级数" width="110">
|
||||
<template #default="{ row }">{{ row.required_approvals }} 级</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批角色" min-width="130">
|
||||
<template #default="{ row }">{{ (row.approver_roles || []).join('、') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="需预算依据" width="90">
|
||||
<template #default="{ row }">{{ row.require_budget ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="日累计限额(万)" width="120">
|
||||
<template #default="{ row }">{{ row.daily_cum_limit != null ? (row.daily_cum_limit ?? 0).toFixed(2) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">{{ row.status === 'active' ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 新建/编辑支付单弹窗 -->
|
||||
<el-dialog v-model="payDialog.visible" :title="payDialog.mode === 'create' ? '新建支付单' : '编辑支付单'" width="520px" append-to-body>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="付款账户" required>
|
||||
<el-select v-model="payForm.from_account_id" filterable style="width:100%" placeholder="仅列出启用账户">
|
||||
<el-option v-for="a in activeAccounts" :key="a.id" :value="a.id" :label="`${a.account_no} ${a.account_name}`" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="收款方" required>
|
||||
<el-input v-model="payForm.to_name" placeholder="收款单位/个人名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款账号">
|
||||
<el-input v-model="payForm.to_account_no" />
|
||||
</el-form-item>
|
||||
<el-form-item label="金额" required>
|
||||
<el-input-number v-model="payForm.amount" :min="0" :precision="2" style="width:160px" />
|
||||
<span style="margin-left:6px;color:#888">万元</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="用途" required>
|
||||
<el-input v-model="payForm.purpose" type="textarea" :rows="2" placeholder="支付用途(必填)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款类型">
|
||||
<el-input v-model="payForm.pay_type" placeholder="如 货款/服务费/工资社保/税费/报销" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款日期">
|
||||
<el-date-picker v-model="payForm.pay_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提交人">
|
||||
<el-input v-model="payForm.submitter" placeholder="默认当前用户" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="payDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="payDialog.saving" @click="savePayment">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<el-dialog v-model="approveVisible" title="审批支付单" width="460px" append-to-body>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="单号">{{ currentRow?.pay_no }}</el-form-item>
|
||||
<el-form-item label="收款方">{{ currentRow?.to_name }}</el-form-item>
|
||||
<el-form-item label="金额">¥{{ (currentRow?.amount ?? 0).toFixed(2) }}万</el-form-item>
|
||||
<el-form-item label="所需审批">{{ currentRow?.required_approvals ?? 1 }} 级(双签需不同自然人)</el-form-item>
|
||||
<el-form-item label="审批备注">
|
||||
<el-input v-model="approveComment" type="textarea" :rows="3" placeholder="审批意见" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="approveVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="approving" @click="submitApprove(false)">驳回</el-button>
|
||||
<el-button type="primary" :loading="approving" @click="submitApprove(true)">通过</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 豁免弹窗 -->
|
||||
<el-dialog v-model="waiveVisible" title="豁免放行" width="460px" append-to-body>
|
||||
<el-alert type="warning" :closable="false" style="margin-bottom:12px"
|
||||
title="豁免通道仅对被 block 规则拦截的支付单开放,全程留痕,豁免级别不得低于该笔所需审批级数。" />
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="单号">{{ currentRow?.pay_no }}</el-form-item>
|
||||
<el-form-item label="金额">¥{{ (currentRow?.amount ?? 0).toFixed(2) }}万</el-form-item>
|
||||
<el-form-item label="豁免审批人" required>
|
||||
<el-input v-model="waivedBy" placeholder="系统用户姓名或账号(≥2 级审批需 ceo)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="豁免事由" required>
|
||||
<el-input v-model="waivedReason" type="textarea" :rows="3" placeholder="必填,不少于 4 字" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="waiveVisible = false">取消</el-button>
|
||||
<el-button type="warning" :loading="waiving" @click="submitWaive">确认豁免</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const activeTab = ref('payments')
|
||||
|
||||
const payments = ref<any[]>([])
|
||||
const paymentsLoading = ref(false)
|
||||
const payFilter = ref<any>({ status: '', check_result: '' })
|
||||
const accounts = ref<any[]>([])
|
||||
const activeAccounts = computed(() => accounts.value.filter((a: any) => a.status === 'active'))
|
||||
|
||||
const hits = ref<any[]>([])
|
||||
const hitsLoading = ref(false)
|
||||
const hitsFilter = ref<any>({ rule_code: '', verdict: '' })
|
||||
|
||||
const rules = ref<any[]>([])
|
||||
const limits = ref<any[]>([])
|
||||
const rulesLoading = ref(false)
|
||||
|
||||
// 支付单弹窗
|
||||
const payDialog = ref<any>({ visible: false, mode: 'create', saving: false })
|
||||
const payForm = ref<any>({})
|
||||
// 审批弹窗
|
||||
const approveVisible = ref(false)
|
||||
const approveComment = ref('')
|
||||
const approving = ref(false)
|
||||
const currentRow = ref<any>(null)
|
||||
// 豁免弹窗
|
||||
const waiveVisible = ref(false)
|
||||
const waivedBy = ref('')
|
||||
const waivedReason = ref('')
|
||||
const waiving = ref(false)
|
||||
|
||||
function errDetail(e: any, fallback: string): string {
|
||||
return e?.response?.data?.detail || fallback
|
||||
}
|
||||
|
||||
function statusLabel(v: string): string {
|
||||
return ({ draft: '草稿', precheck: '已拦截', pending_approval: '待审批', approved: '已放行', paid: '已支付', rejected: '已驳回', cancelled: '已取消' } as any)[v] || v || '-'
|
||||
}
|
||||
|
||||
function statusType(v: string): any {
|
||||
return ({ draft: 'info', precheck: 'danger', pending_approval: 'warning', approved: 'success', paid: 'success', rejected: 'info', cancelled: 'info' } as any)[v] || 'info'
|
||||
}
|
||||
|
||||
function checkLabel(v: string): string {
|
||||
return ({ pass: '通过', warn: '警告', blocked: '拦截' } as any)[v] || v || '-'
|
||||
}
|
||||
|
||||
function checkType(v: string): any {
|
||||
return ({ pass: 'success', warn: 'warning', blocked: 'danger' } as any)[v] || 'info'
|
||||
}
|
||||
|
||||
function fmtTime(v: any): string {
|
||||
if (!v) return '-'
|
||||
return String(v).replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
function accountLabel(id: number): string {
|
||||
const a = accounts.value.find((x: any) => x.id === id)
|
||||
return a ? `${a.account_no} ${a.account_name}` : `#${id}`
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
const r: any = await api.get('/treasury/accounts', { params: { status: 'active' } })
|
||||
accounts.value = r.items || r || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '加载账户池失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPayments() {
|
||||
paymentsLoading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (payFilter.value.status) params.status = payFilter.value.status
|
||||
if (payFilter.value.check_result) params.check_result = payFilter.value.check_result
|
||||
const r: any = await api.get('/treasury/payments', { params })
|
||||
payments.value = r.items || r || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '加载支付单失败'))
|
||||
} finally {
|
||||
paymentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHits() {
|
||||
hitsLoading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (hitsFilter.value.rule_code) params.rule_code = hitsFilter.value.rule_code
|
||||
if (hitsFilter.value.verdict) params.verdict = hitsFilter.value.verdict
|
||||
const r: any = await api.get('/treasury/control-hits', { params })
|
||||
hits.value = r.items || r || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '加载规则命中留痕失败'))
|
||||
} finally {
|
||||
hitsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRulesAndLimits() {
|
||||
rulesLoading.value = true
|
||||
try {
|
||||
const [r1, r2]: any[] = await Promise.all([
|
||||
api.get('/treasury/control-rules'),
|
||||
api.get('/treasury/limits'),
|
||||
])
|
||||
rules.value = r1.items || r1 || []
|
||||
limits.value = r2.items || r2 || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '加载规则库失败'))
|
||||
} finally {
|
||||
rulesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
loadPayments()
|
||||
if (activeTab.value === 'hits') loadHits()
|
||||
if (activeTab.value === 'rules') loadRulesAndLimits()
|
||||
}
|
||||
|
||||
// ── 预检 ──
|
||||
async function doPrecheck(row: any) {
|
||||
try {
|
||||
const updated: any = await api.post(`/treasury/payments/${row.id}/precheck`)
|
||||
Object.assign(row, updated)
|
||||
const results = updated.check_detail?.results || []
|
||||
const hitCodes = results.filter((r: any) => r.verdict !== 'pass').map((r: any) => r.rule_code).join('、')
|
||||
if (updated.check_result === 'pass') {
|
||||
ElMessage.success('预检通过:全部规则通过,已进入待审批')
|
||||
} else if (updated.check_result === 'warn') {
|
||||
ElMessage({ message: `预检警告:命中规则 ${hitCodes},请关注后审批`, type: 'warning', duration: 5000 })
|
||||
} else {
|
||||
ElMessage({ message: `预检拦截:命中规则 ${hitCodes},该单被 block,可走豁免通道放行`, type: 'error', duration: 6000 })
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '预检失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// ── 审批 ──
|
||||
function openApprove(row: any) {
|
||||
currentRow.value = row
|
||||
approveComment.value = ''
|
||||
approveVisible.value = true
|
||||
}
|
||||
|
||||
async function submitApprove(approve: boolean) {
|
||||
if (!currentRow.value) return
|
||||
approving.value = true
|
||||
try {
|
||||
const res: any = await api.post(`/treasury/payments/${currentRow.value.id}/approve`, {
|
||||
approve, comment: approveComment.value,
|
||||
})
|
||||
if (!approve) {
|
||||
Object.assign(currentRow.value, res)
|
||||
ElMessage.success('已驳回')
|
||||
} else if (res.approved === false) {
|
||||
Object.assign(currentRow.value, res.payment)
|
||||
ElMessage({ message: res.reason || '审批未通过(双签未满足)', type: 'warning', duration: 6000 })
|
||||
} else {
|
||||
Object.assign(currentRow.value, res.payment)
|
||||
ElMessage.success('审批通过,已放行')
|
||||
}
|
||||
approveVisible.value = false
|
||||
loadPayments()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '审批操作失败'))
|
||||
} finally {
|
||||
approving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 豁免 ──
|
||||
function openWaive(row: any) {
|
||||
currentRow.value = row
|
||||
waivedBy.value = ''
|
||||
waivedReason.value = ''
|
||||
waiveVisible.value = true
|
||||
}
|
||||
|
||||
async function submitWaive() {
|
||||
if (!currentRow.value) return
|
||||
if (!waivedBy.value.trim()) {
|
||||
ElMessage.warning('请填写豁免审批人')
|
||||
return
|
||||
}
|
||||
if ((waivedReason.value || '').trim().length < 4) {
|
||||
ElMessage.warning('豁免事由必填且不少于 4 字')
|
||||
return
|
||||
}
|
||||
waiving.value = true
|
||||
try {
|
||||
await api.post(`/treasury/payments/${currentRow.value.id}/waive`, {
|
||||
waived_by: waivedBy.value.trim(),
|
||||
waived_reason: waivedReason.value.trim(),
|
||||
})
|
||||
ElMessage.success('已豁免放行')
|
||||
waiveVisible.value = false
|
||||
loadPayments()
|
||||
loadHits()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '豁免失败'))
|
||||
} finally {
|
||||
waiving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 登记支付 ──
|
||||
async function doPay(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认登记支付单 ${row.pay_no}(¥${(row.amount ?? 0).toFixed(2)}万)已实际支付?`, '登记支付', { type: 'warning' })
|
||||
await api.post(`/treasury/payments/${row.id}/pay`)
|
||||
ElMessage.success('已登记支付')
|
||||
loadPayments()
|
||||
} catch (e: any) {
|
||||
if (e === 'cancel' || e === 'close') return
|
||||
ElMessage.error(errDetail(e, '登记支付失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// ── 新建/编辑支付单 ──
|
||||
function openPayDialog(row: any) {
|
||||
payDialog.value.mode = row ? 'edit' : 'create'
|
||||
payForm.value = row
|
||||
? {
|
||||
id: row.id, from_account_id: row.from_account_id, to_name: row.to_name,
|
||||
to_account_no: row.to_account_no, amount: row.amount, currency: row.currency || 'CNY',
|
||||
purpose: row.purpose, pay_type: row.pay_type, pay_date: row.pay_date, submitter: row.submitter,
|
||||
}
|
||||
: { id: null, from_account_id: null, to_name: '', to_account_no: '', amount: 0, currency: 'CNY', purpose: '', pay_type: '', pay_date: '', submitter: '' }
|
||||
payDialog.value.visible = true
|
||||
}
|
||||
|
||||
async function savePayment() {
|
||||
const f = payForm.value
|
||||
if (!f.from_account_id) { ElMessage.warning('请选择付款账户'); return }
|
||||
if (!f.to_name.trim()) { ElMessage.warning('请填写收款方'); return }
|
||||
if (!(f.amount > 0)) { ElMessage.warning('金额须大于 0'); return }
|
||||
if (!f.purpose.trim()) { ElMessage.warning('用途为必填项'); return }
|
||||
payDialog.value.saving = true
|
||||
try {
|
||||
const payload = {
|
||||
from_account_id: f.from_account_id, to_name: f.to_name.trim(), to_account_no: f.to_account_no,
|
||||
amount: f.amount, currency: f.currency || 'CNY', purpose: f.purpose.trim(),
|
||||
pay_type: f.pay_type, pay_date: f.pay_date || undefined, submitter: f.submitter || undefined,
|
||||
}
|
||||
if (payDialog.value.mode === 'edit') {
|
||||
await api.put(`/treasury/payments/${f.id}`, payload)
|
||||
ElMessage.success('支付单已更新')
|
||||
} else {
|
||||
await api.post('/treasury/payments', payload)
|
||||
ElMessage.success('支付单已创建(草稿状态,请先预检)')
|
||||
}
|
||||
payDialog.value.visible = false
|
||||
loadPayments()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '保存支付单失败'))
|
||||
} finally {
|
||||
payDialog.value.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 规则启用/停用 ──
|
||||
async function toggleRule(row: any, val: any) {
|
||||
try {
|
||||
await api.put(`/treasury/control-rules/${row.id}`, {
|
||||
rule_code: row.rule_code, rule_name: row.rule_name, rule_type: row.rule_type,
|
||||
severity: row.severity, params: row.params, rule_version: row.rule_version,
|
||||
enabled: val ? 1 : 0, remark: row.remark,
|
||||
})
|
||||
row.enabled = val ? 1 : 0
|
||||
ElMessage.success(val ? `规则 ${row.rule_code} 已启用` : `规则 ${row.rule_code} 已停用`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(errDetail(e, '更新规则失败'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAccounts()
|
||||
loadPayments()
|
||||
loadHits()
|
||||
loadRulesAndLimits()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.treasury-payment-page { max-width: 1400px; margin: 0 auto; padding: 16px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.header-right { display: flex; gap: 8px; }
|
||||
.filter-bar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.params-preview { font-family: monospace; font-size: 12px; color: #666; cursor: pointer; }
|
||||
.hit-detail { padding: 8px 16px; background: #fafafa; }
|
||||
.hit-detail-row { display: flex; margin-bottom: 4px; font-size: 12px; }
|
||||
.hit-detail-key { width: 140px; color: #888; flex-shrink: 0; }
|
||||
.hit-detail-val { color: #333; word-break: break-all; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user