Просмотр исходного кода

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

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 месяцев назад
Родитель
Сommit
0d59627fa3

+ 11 - 0
web-admin/src/router/modules/sqxp.ts

@@ -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',

+ 646 - 0
web-admin/src/views/oa-approval/receive-accounts/index.vue

@@ -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>