feat(web-admin): add OA receive account management page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 13:53:58 +08:00
1 parent cf61ce340d
commit 0d59627fa3
2 files changed
+657

No files matched your search

+11
View File
@@ -1385,6 +1385,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
menuCode: 'oa:roles:list'
}
},
{
id: 'OA000000000000000000000000000108',
name: 'OaApprovalReceiveAccounts',
path: 'receive-accounts',
component: '/oa-approval/receive-accounts/index',
meta: {
title: '收款账户管理',
keepAlive: true,
menuCode: 'oa:receive-accounts:list'
}
},
{
id: 'OA000000000000000000000000000103',
name: 'OaApprovalStart',
@@ -0,0 +1,646 @@
<!-- OA 收款账户管理(租户级公共主数据) -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">收款账户管理</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="户名 / 账号 / 银行"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="账户类型">
<el-select v-model="searchForm.accountType" placeholder="请选择" clearable>
<el-option
v-for="(label, value) in ACCOUNT_TYPE_LABEL"
:key="value"
:label="label"
:value="value"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增账户
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>
批量删除
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="(rows: any[]) => (selectedRows = rows)"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="accountType"
label="账户类型"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="typeTagType(row.accountType)" size="small">
{{ ACCOUNT_TYPE_LABEL[row.accountType as ReceiveAccountType] || row.accountType }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="accountName"
label="户名"
min-width="140"
align="left"
header-align="left"
show-overflow-tooltip
/>
<el-table-column
prop="accountNo"
label="账号"
min-width="160"
align="left"
header-align="left"
show-overflow-tooltip
/>
<el-table-column
prop="bankName"
label="银行"
min-width="120"
align="left"
header-align="left"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.accountType === 'alipay' ? '—' : formatDisplayOrDash(row.bankName) }}
</template>
</el-table-column>
<el-table-column
label="开户地 / 支行"
min-width="200"
align="left"
header-align="left"
show-overflow-tooltip
>
<template #default="{ row }">
<template v-if="row.accountType === 'alipay'">—</template>
<template v-else>
<div>{{ regionText(row) }}</div>
<div v-if="row.bankBranch" class="cell-sub">{{ row.bankBranch }}</div>
</template>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="left"
header-align="left"
show-overflow-tooltip
>
<template #default="{ row }">{{ formatDisplayOrDash(row.remark) }}</template>
</el-table-column>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<div style="display: flex; justify-content: center; gap: 8px">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button class="btn-op-item" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
v-model:current-page="pager.page"
v-model:page-size="pager.pageSize"
:total="pager.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
background
@size-change="loadData"
@current-change="loadData"
/>
</div>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="720px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="账户类型" prop="accountType" class="form-col-span-2">
<el-select
v-model="formData.accountType"
placeholder="请选择"
:disabled="isEdit"
@change="onTypeChange"
>
<el-option
v-for="(label, value) in ACCOUNT_TYPE_LABEL"
:key="value"
:label="label"
:value="value"
/>
</el-select>
</el-form-item>
<el-form-item label="户名" prop="accountName">
<el-input v-model="formData.accountName" placeholder="请输入户名" maxlength="128" />
</el-form-item>
<el-form-item label="账号" prop="accountNo">
<el-input v-model="formData.accountNo" placeholder="请输入账号" maxlength="128" />
</el-form-item>
<template v-if="formData.accountType !== 'alipay'">
<el-form-item label="银行" prop="bankName">
<el-input
v-model="formData.bankName"
placeholder="如:中国工商银行"
maxlength="128"
/>
</el-form-item>
<el-form-item label="银行所在省" prop="bankProvince">
<el-select
v-model="formData.bankProvince"
filterable
clearable
placeholder="请选择省"
@change="onProvinceChange"
>
<el-option
v-for="p in provinceOptions"
:key="p.value"
:label="p.label"
:value="p.label"
/>
</el-select>
</el-form-item>
<el-form-item label="银行所在市" prop="bankCity">
<el-select
v-model="formData.bankCity"
filterable
clearable
placeholder="请选择市"
:disabled="!formData.bankProvince"
>
<el-option
v-for="c in cityOptions"
:key="c.value"
:label="c.label"
:value="c.label"
/>
</el-select>
</el-form-item>
<el-form-item label="银行支行" prop="bankBranch" class="form-col-span-2">
<el-input
v-model="formData.bankBranch"
placeholder="请输入开户支行全称"
maxlength="256"
/>
</el-form-item>
</template>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="formData.remark"
type="textarea"
:rows="2"
placeholder="选填"
maxlength="512"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="viewVisible"
title="查看收款账户"
width="640px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
>
<div v-if="viewRow" class="view-grid">
<div class="view-item">
<div class="k">账户类型</div>
<div class="v">
<el-tag :type="typeTagType(viewRow.accountType)" size="small">
{{
ACCOUNT_TYPE_LABEL[viewRow.accountType as ReceiveAccountType] ||
viewRow.accountType
}}
</el-tag>
</div>
</div>
<div class="view-item">
<div class="k">户名</div>
<div class="v">{{ formatDisplayOrDash(viewRow.accountName) }}</div>
</div>
<div class="view-item">
<div class="k">账号</div>
<div class="v">{{ formatDisplayOrDash(viewRow.accountNo) }}</div>
</div>
<template v-if="viewRow.accountType !== 'alipay'">
<div class="view-item">
<div class="k">银行</div>
<div class="v">{{ formatDisplayOrDash(viewRow.bankName) }}</div>
</div>
<div class="view-item">
<div class="k">银行所在省 / 市</div>
<div class="v">{{ regionText(viewRow) }}</div>
</div>
<div class="view-item full">
<div class="k">银行支行</div>
<div class="v">{{ formatDisplayOrDash(viewRow.bankBranch) }}</div>
</div>
</template>
<div class="view-item full">
<div class="k">备注</div>
<div class="v">{{ formatDisplayOrDash(viewRow.remark) }}</div>
</div>
</div>
<template #footer>
<el-button class="btn-main" type="primary" @click="viewVisible = false">
<el-icon><Check /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Close,
Check,
Edit,
Delete,
View,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import { formatDisplayOrDash } from '@/utils/datetime-display'
import {
fetchBatchDeleteOaReceiveAccounts,
fetchCreateOaReceiveAccount,
fetchDeleteOaReceiveAccount,
fetchOaReceiveAccountList,
fetchUpdateOaReceiveAccount
} from '@/api/oaApproval'
import {
fetchChinaRegionTree,
type ChinaRegionTreeNode
} from '@/api/chinaRegion'
import {
ACCOUNT_TYPE_LABEL,
type ReceiveAccountType
} from '@/utils/oa-receive-account-value'
defineOptions({ name: 'OaApprovalReceiveAccounts' })
const searchForm = reactive({ keyword: '', accountType: '' })
const tableData = ref<any[]>([])
const selectedRows = ref<any[]>([])
const loading = ref(false)
const pager = reactive({ page: 1, pageSize: 10, total: 0 })
const dialogVisible = ref(false)
const dialogTitle = ref('新增收款账户')
const isEdit = ref(false)
const editingId = ref<string | null>(null)
const submitLoading = ref(false)
const formRef = ref()
const formData = reactive({
accountType: 'alipay' as ReceiveAccountType,
accountName: '',
accountNo: '',
bankName: '',
bankProvince: '',
bankCity: '',
bankBranch: '',
remark: ''
})
const requireWhenBank = (_rule: any, value: string, callback: (e?: Error) => void) => {
if (formData.accountType === 'alipay') {
callback()
return
}
if (!String(value || '').trim()) {
callback(new Error('请填写'))
return
}
callback()
}
const formRules = computed(() => ({
accountType: [{ required: true, message: '请选择账户类型', trigger: 'change' }],
accountName: [{ required: true, message: '请输入户名', trigger: 'blur' }],
accountNo: [{ required: true, message: '请输入账号', trigger: 'blur' }],
bankName: [{ validator: requireWhenBank, trigger: 'blur' }],
bankProvince: [{ validator: requireWhenBank, trigger: 'change' }],
bankCity: [{ validator: requireWhenBank, trigger: 'change' }],
bankBranch: [{ validator: requireWhenBank, trigger: 'blur' }]
}))
const viewVisible = ref(false)
const viewRow = ref<any>(null)
const regionTree = ref<ChinaRegionTreeNode[]>([])
const provinceOptions = computed(() => regionTree.value || [])
const cityOptions = computed(() => {
const name = formData.bankProvince
if (!name) return [] as ChinaRegionTreeNode[]
const p = regionTree.value.find((x) => x.label === name)
return p?.children || []
})
function typeTagType(t: string): 'success' | 'primary' | 'warning' | 'info' {
if (t === 'alipay') return 'success'
if (t === 'personal') return 'primary'
if (t === 'corporate') return 'warning'
return 'info'
}
function regionText(row: any) {
const p = String(row?.bankProvince || '').trim()
const c = String(row?.bankCity || '').trim()
if (p && c) return `${p} / ${c}`
return p || c || '—'
}
async function loadRegionTree() {
try {
const res = await fetchChinaRegionTree()
regionTree.value = res?.tree || []
} catch {
regionTree.value = []
}
}
async function loadData() {
loading.value = true
try {
const res = await fetchOaReceiveAccountList({
keyword: searchForm.keyword || undefined,
accountType: searchForm.accountType || undefined,
page: pager.page,
pageSize: pager.pageSize
})
tableData.value = res?.list || []
pager.total = Number(res?.total || 0)
} finally {
loading.value = false
}
}
function handleSearch() {
pager.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.accountType = ''
pager.page = 1
loadData()
}
function clearBankFields() {
formData.bankName = ''
formData.bankProvince = ''
formData.bankCity = ''
formData.bankBranch = ''
}
function onTypeChange() {
if (formData.accountType === 'alipay') clearBankFields()
formRef.value?.clearValidate?.()
}
function onProvinceChange() {
formData.bankCity = ''
}
function resetForm() {
formData.accountType = 'alipay'
formData.accountName = ''
formData.accountNo = ''
clearBankFields()
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增收款账户'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: any) {
isEdit.value = true
dialogTitle.value = '编辑收款账户'
editingId.value = row.id
formData.accountType = (row.accountType || 'alipay') as ReceiveAccountType
formData.accountName = row.accountName || ''
formData.accountNo = row.accountNo || ''
formData.bankName = row.bankName || ''
formData.bankProvince = row.bankProvince || ''
formData.bankCity = row.bankCity || ''
formData.bankBranch = row.bankBranch || ''
formData.remark = row.remark || ''
if (formData.accountType === 'alipay') clearBankFields()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleView(row: any) {
viewRow.value = row
viewVisible.value = true
}
async function handleSubmit() {
try {
await formRef.value?.validate?.()
} catch {
ElMessageBox.alert('请完善必填项后再提交', '提示', { type: 'warning' })
return
}
submitLoading.value = true
try {
const payload: Record<string, unknown> = {
accountType: formData.accountType,
accountName: formData.accountName.trim(),
accountNo: formData.accountNo.trim(),
remark: formData.remark?.trim() || undefined
}
if (formData.accountType === 'alipay') {
payload.bankName = ''
payload.bankProvince = ''
payload.bankCity = ''
payload.bankBranch = ''
} else {
payload.bankName = formData.bankName.trim()
payload.bankProvince = formData.bankProvince.trim()
payload.bankCity = formData.bankCity.trim()
payload.bankBranch = formData.bankBranch.trim()
}
if (isEdit.value && editingId.value) {
await fetchUpdateOaReceiveAccount(editingId.value, payload)
} else {
await fetchCreateOaReceiveAccount(payload)
}
dialogVisible.value = false
await loadData()
} finally {
submitLoading.value = false
}
}
async function handleDelete(row: any) {
await ElMessageBox.confirm(`确定删除收款账户「${row.accountName}」吗?`, '提示', {
type: 'warning'
})
await fetchDeleteOaReceiveAccount(row.id)
await loadData()
}
async function handleBatchDelete() {
if (!selectedRows.value.length) return
await ElMessageBox.confirm(
`确定删除已选 ${selectedRows.value.length} 个收款账户吗?`,
'提示',
{ type: 'warning' }
)
const ids = selectedRows.value.map((r) => r.id).filter(Boolean)
await fetchBatchDeleteOaReceiveAccounts(ids)
selectedRows.value = []
await loadData()
}
onMounted(async () => {
await Promise.all([loadRegionTree(), loadData()])
})
</script>
<style scoped lang="scss">
.cell-sub {
margin-top: 2px;
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.view-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 20px;
}
.view-item.full {
grid-column: 1 / -1;
}
.view-item .k {
margin-bottom: 4px;
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.view-item .v {
font-weight: 500;
word-break: break-all;
}
@media (max-width: 640px) {
.view-grid {
grid-template-columns: 1fr;
}
}
</style>