fix: 代理
This commit is contained in:
@@ -193,12 +193,28 @@
|
||||
prop: 'payment_channel',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '第三方支付流水号',
|
||||
prop: 'payment_transaction_id',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
...(isOfflineRecharge
|
||||
? [
|
||||
{
|
||||
label: '交易流水号',
|
||||
prop: 'external_transaction_no',
|
||||
formatter: (value: string | null | undefined) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '收款方式',
|
||||
formatter: (_: unknown, data: AgentRecharge) =>
|
||||
data.offline_payment_method_name || data.offline_payment_method_code || '-'
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: '第三方支付流水号',
|
||||
prop: 'payment_transaction_id',
|
||||
formatter: (value: string | null | undefined) => value || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]),
|
||||
{
|
||||
label: '支付单号',
|
||||
prop: 'payment_no',
|
||||
@@ -226,6 +242,26 @@
|
||||
() => '查看支付凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
{
|
||||
label: '其他凭证',
|
||||
fullWidth: true,
|
||||
render: (data: AgentRecharge) =>
|
||||
hasVoucherKeys(data.other_voucher_key ?? undefined)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(
|
||||
data.other_voucher_key ?? undefined
|
||||
)
|
||||
}
|
||||
},
|
||||
() => '查看其他凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -121,10 +121,66 @@
|
||||
ref="uploadRef"
|
||||
v-model="createForm.payment_voucher_key"
|
||||
voucher-name="支付凭证"
|
||||
accept="image/*"
|
||||
content-type="image/jpeg"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createMode === 'offline'"
|
||||
label="收款方式"
|
||||
prop="offline_payment_method_id"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="createForm.offline_payment_method_id"
|
||||
placeholder="请选择收款方式"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
:loading="paymentMethodOptionsLoading"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in offlinePaymentMethodOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.payment_method === 'offline'"
|
||||
label="交易流水号"
|
||||
prop="external_transaction_no"
|
||||
>
|
||||
<div class="external-transaction-row">
|
||||
<ElInput
|
||||
v-model="createForm.external_transaction_no"
|
||||
placeholder="请输入交易流水号,或点击右侧识别凭证预填"
|
||||
:disabled="ocrLoading"
|
||||
/>
|
||||
<ElButton
|
||||
:loading="ocrLoading"
|
||||
:disabled="!offlineVoucherKeys.length"
|
||||
@click="handleRecognizeVoucher"
|
||||
>
|
||||
识别凭证
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="external-transaction-tip">
|
||||
识别结果仅供参考,请对照凭证核对后再提交。
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="createMode === 'offline'" label="其他凭证">
|
||||
<VoucherUpload
|
||||
ref="otherUploadRef"
|
||||
v-model="createForm.other_voucher_key"
|
||||
voucher-name="其他凭证"
|
||||
:max-count="5"
|
||||
accept="image/*"
|
||||
content-type="image/jpeg"
|
||||
@uploading-change="otherVoucherUploading = $event"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="createMode === 'offline'" label="运营备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="createForm.remark"
|
||||
@@ -297,7 +353,12 @@
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { AgentRechargeService, CommissionService, ShopService } from '@/api/modules'
|
||||
import {
|
||||
AgentRechargeService,
|
||||
CommissionService,
|
||||
EmployeeCollectionService,
|
||||
ShopService
|
||||
} from '@/api/modules'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
@@ -319,6 +380,7 @@
|
||||
AgentRechargePaymentStatusResponse,
|
||||
CreateAgentRechargeRequest,
|
||||
ConfirmOfflinePaymentRequest,
|
||||
EmployeeCollectionPaymentMethod,
|
||||
RejectAgentRechargeRequest
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
@@ -343,6 +405,7 @@
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
import { buildAgentRechargeActions } from './agentRechargeActions'
|
||||
import { formatRejectionReason } from './agentRechargeDisplay'
|
||||
import { normalizeCollectionList } from '@/views/finance/employee-collection/employeeCollectionDisplay'
|
||||
import {
|
||||
amountYuanToFen,
|
||||
createOnlineRechargeRequestId,
|
||||
@@ -406,6 +469,10 @@
|
||||
max_amount: ONLINE_MAX_RECHARGE_AMOUNT_FEN
|
||||
})
|
||||
const paymentVoucherFileKeys = ref<string[]>([])
|
||||
const offlinePaymentMethodOptions = ref<EmployeeCollectionPaymentMethod[]>([])
|
||||
const paymentMethodOptionsLoading = ref(false)
|
||||
const otherVoucherUploading = ref(false)
|
||||
const ocrLoading = ref(false)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: AgentRechargeQueryParams = {
|
||||
@@ -542,6 +609,8 @@
|
||||
{ label: '业务处理状态', prop: 'processing_status_name' },
|
||||
{ label: '支付方式', prop: 'payment_method' },
|
||||
{ label: '支付通道', prop: 'payment_channel' },
|
||||
{ label: '交易流水号', prop: 'external_transaction_no' },
|
||||
{ label: '收款方式', prop: 'offline_payment_method_name' },
|
||||
{ label: '运营备注', prop: 'remark' },
|
||||
{ label: '驳回原因', prop: 'rejection_reason' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
@@ -554,6 +623,7 @@
|
||||
const confirmPayFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const otherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const OFFLINE_MIN_RECHARGE_AMOUNT = 0.01
|
||||
const OFFLINE_MAX_RECHARGE_AMOUNT = 1_000_000
|
||||
|
||||
@@ -579,6 +649,8 @@
|
||||
() =>
|
||||
createLoading.value ||
|
||||
voucherUploading.value ||
|
||||
otherVoucherUploading.value ||
|
||||
ocrLoading.value ||
|
||||
paymentMethodsLoading.value ||
|
||||
(createMode.value === 'online' && onlinePaymentMethods.value.length === 0)
|
||||
)
|
||||
@@ -631,6 +703,12 @@
|
||||
}
|
||||
if (createMode.value === 'offline') {
|
||||
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
|
||||
rules.offline_payment_method_id = [
|
||||
{ required: true, message: '请选择收款方式', trigger: 'change' }
|
||||
]
|
||||
rules.external_transaction_no = [
|
||||
{ required: true, message: '请输入交易流水号', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
if (createMode.value === 'online') delete rules.shop_id
|
||||
return rules
|
||||
@@ -651,12 +729,18 @@
|
||||
payment_method: AgentRechargePaymentMethod | ''
|
||||
shop_id: number | null
|
||||
payment_voucher_key: string[]
|
||||
offline_payment_method_id: number | null
|
||||
external_transaction_no: string
|
||||
other_voucher_key: string[]
|
||||
remark: string
|
||||
}>({
|
||||
amount: OFFLINE_MIN_RECHARGE_AMOUNT,
|
||||
payment_method: '',
|
||||
shop_id: null,
|
||||
payment_voucher_key: [],
|
||||
offline_payment_method_id: null,
|
||||
external_transaction_no: '',
|
||||
other_voucher_key: [],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
@@ -803,6 +887,21 @@
|
||||
width: 120,
|
||||
formatter: (row: AgentRecharge) => row.payment_channel || '-'
|
||||
},
|
||||
{
|
||||
prop: 'external_transaction_no',
|
||||
label: '交易流水号',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: AgentRecharge) => row.external_transaction_no || '-'
|
||||
},
|
||||
{
|
||||
prop: 'offline_payment_method_name',
|
||||
label: '收款方式',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: AgentRecharge) =>
|
||||
row.payment_method === 'offline' ? row.offline_payment_method_name || '-' : '-'
|
||||
},
|
||||
{
|
||||
prop: 'remark',
|
||||
label: '运营备注',
|
||||
@@ -1000,8 +1099,9 @@
|
||||
if (createMode.value === 'online') {
|
||||
await loadPaymentMethods()
|
||||
} else {
|
||||
// 重新加载店铺列表,确保获取最新数据
|
||||
// 重新加载店铺列表与收款方式字典,确保获取最新数据
|
||||
await loadShops()
|
||||
await loadPaymentMethodOptions()
|
||||
}
|
||||
|
||||
createDialogVisible.value = true
|
||||
@@ -1012,10 +1112,16 @@
|
||||
createForm.payment_method = ''
|
||||
createForm.shop_id = null
|
||||
createForm.payment_voucher_key = []
|
||||
createForm.offline_payment_method_id = null
|
||||
createForm.external_transaction_no = ''
|
||||
createForm.other_voucher_key = []
|
||||
createForm.remark = ''
|
||||
onlineRequestId.value = null
|
||||
voucherUploading.value = false
|
||||
otherVoucherUploading.value = false
|
||||
ocrLoading.value = false
|
||||
uploadRef.value?.clearFiles(false)
|
||||
otherUploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
// 对话框关闭后的清理
|
||||
@@ -1024,12 +1130,12 @@
|
||||
resetCreateForm()
|
||||
}
|
||||
|
||||
// 加载代理在线充值可用支付方式
|
||||
// 加载代理在线充值可用支付方式(超管调用该接口返回 403,仅通过系统配置查看允许范围)
|
||||
const loadPaymentMethods = async () => {
|
||||
paymentMethodsLoading.value = true
|
||||
onlinePaymentMethods.value = []
|
||||
try {
|
||||
const res = await AgentRechargeService.getPaymentMethods()
|
||||
const res = await AgentRechargeService.getSelfRechargePaymentMethods()
|
||||
if (res.code === 0) {
|
||||
onlinePaymentMethods.value = Array.isArray(res.data?.methods) ? res.data.methods : []
|
||||
paymentMethodsBounds.min_amount = ONLINE_MIN_RECHARGE_AMOUNT_FEN
|
||||
@@ -1037,15 +1143,75 @@
|
||||
Number(res.data?.max_amount) || ONLINE_MAX_RECHARGE_AMOUNT_FEN
|
||||
createForm.amount = minimumAmountYuan.value
|
||||
} else {
|
||||
ElMessage.warning(res.msg || '当前暂无可用在线支付方式')
|
||||
ElMessage.warning(res.msg || '当前暂无可用的在线支付方式')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载在线支付方式失败:', error)
|
||||
ElMessage.warning('当前暂无可用的在线支付方式')
|
||||
} finally {
|
||||
paymentMethodsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载线下收款方式字典启用项
|
||||
const loadPaymentMethodOptions = async () => {
|
||||
paymentMethodOptionsLoading.value = true
|
||||
offlinePaymentMethodOptions.value = []
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getPaymentMethods({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
enabled: true
|
||||
})
|
||||
if (res.code === 0) {
|
||||
offlinePaymentMethodOptions.value =
|
||||
normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载线下收款方式失败:', error)
|
||||
} finally {
|
||||
paymentMethodOptionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 已上传的支付凭证对象键(OCR 识别取第一个)
|
||||
const offlineVoucherKeys = computed(() => toVoucherKeyList(createForm.payment_voucher_key))
|
||||
|
||||
// 识别付款凭证中的交易流水号:只做预填,失败不阻断人工填写
|
||||
const handleRecognizeVoucher = async () => {
|
||||
if (ocrLoading.value) return
|
||||
|
||||
const voucherKeys = offlineVoucherKeys.value
|
||||
if (!voucherKeys.length) {
|
||||
ElMessage.warning('请先上传支付凭证')
|
||||
return
|
||||
}
|
||||
|
||||
ocrLoading.value = true
|
||||
try {
|
||||
const res = await AgentRechargeService.recognizePaymentVoucher({
|
||||
payment_voucher_key: voucherKeys[0]
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
ElMessage.warning(res.msg || '识别失败,请手动填写交易流水号')
|
||||
return
|
||||
}
|
||||
const transactionNo = res.data?.external_transaction_no
|
||||
if (!transactionNo) {
|
||||
ElMessage.warning('未识别到交易流水号,请手动填写')
|
||||
return
|
||||
}
|
||||
createForm.external_transaction_no = transactionNo
|
||||
createFormRef.value?.validateField('external_transaction_no')
|
||||
ElMessage.success('已识别交易流水号,请对照凭证核对')
|
||||
} catch (error) {
|
||||
console.error('识别付款凭证失败:', error)
|
||||
ElMessage.warning('识别失败,请手动填写交易流水号')
|
||||
} finally {
|
||||
ocrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建充值订单
|
||||
const handleCreateRecharge = async () => {
|
||||
if (createLoading.value) return
|
||||
@@ -1056,6 +1222,10 @@
|
||||
ElMessage.warning('支付凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
if (otherVoucherUploading.value) {
|
||||
ElMessage.warning('其他凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
createLoading.value = true
|
||||
try {
|
||||
@@ -1071,6 +1241,10 @@
|
||||
ElMessage.warning('请选择目标店铺')
|
||||
return
|
||||
}
|
||||
if (!createForm.offline_payment_method_id) {
|
||||
ElMessage.warning('请选择收款方式')
|
||||
return
|
||||
}
|
||||
|
||||
const voucherKeys = toVoucherKeyList(createForm.payment_voucher_key)
|
||||
if (!hasVoucherKeys(voucherKeys)) {
|
||||
@@ -1078,11 +1252,15 @@
|
||||
return
|
||||
}
|
||||
|
||||
const otherVoucherKeys = toVoucherKeyList(createForm.other_voucher_key)
|
||||
const data: CreateAgentRechargeRequest = {
|
||||
amount: amountYuanToFen(createForm.amount),
|
||||
payment_method: 'offline',
|
||||
shop_id: createForm.shop_id,
|
||||
offline_payment_method_id: createForm.offline_payment_method_id,
|
||||
external_transaction_no: createForm.external_transaction_no.trim(),
|
||||
payment_voucher_key: voucherKeys,
|
||||
other_voucher_key: otherVoucherKeys.length ? otherVoucherKeys : undefined,
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
|
||||
@@ -1430,6 +1608,18 @@
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.external-transaction-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.external-transaction-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.online-recharge-qr-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="760px"
|
||||
destroy-on-close
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="140px">
|
||||
<ElFormItem label="收款方式" prop="payment_method_id">
|
||||
<ElSelect
|
||||
v-model="form.payment_method_id"
|
||||
placeholder="请选择收款方式"
|
||||
style="width: 100%"
|
||||
:loading="paymentMethodsLoading"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in enabledPaymentMethods"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div v-if="!paymentMethodsLoading && !enabledPaymentMethodCount" class="form-tip">
|
||||
暂无可用的收款方式,请联系管理员
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="核销账单" required>
|
||||
<div class="bill-selector">
|
||||
<div v-if="billsLoading" class="bill-selector__empty">加载中...</div>
|
||||
<ElEmpty
|
||||
v-else-if="!candidateBills.length"
|
||||
description="暂无可核销账单"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div v-else class="bill-selector__list">
|
||||
<div v-for="bill in candidateBills" :key="bill.id" class="bill-row">
|
||||
<ElCheckbox
|
||||
:model-value="selectedBillIds.includes(bill.id)"
|
||||
@change="toggleBill(bill)"
|
||||
/>
|
||||
<div class="bill-row__main">
|
||||
<div class="bill-row__title">账单 #{{ bill.id }}</div>
|
||||
<div class="bill-row__meta">
|
||||
单号:{{ bill.source_no || '-' }} · 未核销
|
||||
{{ formatCollectionCurrency(bill.remaining_amount) }}
|
||||
</div>
|
||||
</div>
|
||||
<ElInputNumber
|
||||
v-if="selectedBillIds.includes(bill.id)"
|
||||
v-model="amountMap[bill.id]"
|
||||
:min="0"
|
||||
:max="fenToYuan(bill.remaining_amount)"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
size="small"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bill-selector__total">
|
||||
已选 {{ selectedBillIds.length }} 张账单,核销合计
|
||||
{{ formatCollectionCurrency(totalAmountFen) }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款金额" prop="paid_amount">
|
||||
<ElInputNumber
|
||||
v-model="form.paid_amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<span class="amount-tip">本次线下收款金额(元),不得小于核销合计</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款方名称" prop="payer_name">
|
||||
<ElInput
|
||||
v-model="form.payer_name"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入付款方名称"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款时间" prop="paid_at">
|
||||
<ElDatePicker
|
||||
v-model="form.paid_at"
|
||||
type="datetime"
|
||||
placeholder="请选择付款时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="外部交易流水号" prop="external_transaction_no">
|
||||
<ElInput
|
||||
v-model="form.external_transaction_no"
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
placeholder="请输入经人工核对的外部交易流水号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款凭证" prop="payment_voucher_keys">
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
v-model="form.payment_voucher_keys"
|
||||
voucher-name="付款凭证"
|
||||
:max-count="5"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('payment_voucher_keys')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="选填"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isActing" label="代办原因" prop="acting_reason">
|
||||
<ElInput
|
||||
v-model="form.acting_reason"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="超级管理员代办必须填写原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="emit('update:modelValue', false)">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitting || voucherUploading"
|
||||
:disabled="voucherUploading || !enabledPaymentMethodCount"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionAllocation,
|
||||
EmployeeCollectionAllocationRequest,
|
||||
EmployeeCollectionApplication,
|
||||
EmployeeCollectionApplicationRequest,
|
||||
EmployeeCollectionBill,
|
||||
EmployeeCollectionPaymentMethod
|
||||
} from '@/types/api'
|
||||
import { fenToYuan, yuanToFen } from '@/utils/business/format'
|
||||
import { getErrorMessage, toVoucherKeyList } from '@/utils/business'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import {
|
||||
canCreateApplication,
|
||||
formatCollectionCurrency,
|
||||
normalizeCollectionList
|
||||
} from '../../employeeCollectionDisplay'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
application?: EmployeeCollectionApplication | null
|
||||
presetBill?: EmployeeCollectionBill | null
|
||||
}
|
||||
|
||||
interface BillOption {
|
||||
id: number
|
||||
source_no?: string | null
|
||||
remaining_amount: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
application: null,
|
||||
presetBill: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
|
||||
const paymentMethodsLoading = ref(false)
|
||||
const billsLoading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const paymentMethods = ref<EmployeeCollectionPaymentMethod[]>([])
|
||||
const candidateBills = ref<BillOption[]>([])
|
||||
const selectedBillIds = ref<number[]>([])
|
||||
const amountMap = reactive<Record<number, number>>({})
|
||||
|
||||
const form = reactive({
|
||||
payment_method_id: undefined as number | undefined,
|
||||
paid_amount: 0,
|
||||
payer_name: '',
|
||||
paid_at: '',
|
||||
external_transaction_no: '',
|
||||
payment_voucher_keys: [] as string[],
|
||||
remark: '',
|
||||
acting_reason: ''
|
||||
})
|
||||
|
||||
const isActing = computed(() => userStore.isSuperAdmin)
|
||||
const isResubmit = computed(() => !!props.application)
|
||||
const dialogTitle = computed(() => (isResubmit.value ? '修改并重新提交核销申请' : '创建核销申请'))
|
||||
const enabledPaymentMethods = computed(() => paymentMethods.value.filter((item) => item.enabled))
|
||||
const enabledPaymentMethodCount = computed(() => enabledPaymentMethods.value.length)
|
||||
|
||||
const paidAmountFen = computed(() => yuanToFen(form.paid_amount) || 0)
|
||||
const totalAmountFen = computed(() =>
|
||||
selectedBillIds.value.reduce((total, billId) => total + (yuanToFen(amountMap[billId]) || 0), 0)
|
||||
)
|
||||
|
||||
const rules = computed<FormRules>(() => {
|
||||
const base: FormRules = {
|
||||
payment_method_id: [{ required: true, message: '请选择收款方式', trigger: 'change' }],
|
||||
paid_amount: [
|
||||
{ required: true, message: '请输入付款金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (paidAmountFen.value <= 0) {
|
||||
callback(new Error('付款金额必须大于 0'))
|
||||
return
|
||||
}
|
||||
if (paidAmountFen.value < totalAmountFen.value) {
|
||||
callback(new Error('付款金额不能小于核销合计'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
payer_name: [{ required: true, message: '请输入付款方名称', trigger: 'blur' }],
|
||||
paid_at: [{ required: true, message: '请选择付款时间', trigger: 'change' }],
|
||||
external_transaction_no: [
|
||||
{ required: true, message: '请输入外部交易流水号', trigger: 'blur' }
|
||||
],
|
||||
payment_voucher_keys: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value, callback) => {
|
||||
if (Array.isArray(value) && value.length) callback()
|
||||
else callback(new Error('请上传付款凭证(1-5 个)'))
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
if (isActing.value) {
|
||||
base.acting_reason = [{ required: true, message: '请填写代办原因', trigger: 'blur' }]
|
||||
}
|
||||
return base
|
||||
})
|
||||
|
||||
watch(totalAmountFen, (total) => {
|
||||
if (paidAmountFen.value < total) {
|
||||
form.paid_amount = fenToYuan(total)
|
||||
}
|
||||
})
|
||||
|
||||
const loadPaymentMethods = async () => {
|
||||
paymentMethodsLoading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getPaymentMethods({ page: 1, page_size: 100 })
|
||||
if (res.code === 0) {
|
||||
paymentMethods.value = normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取收款方式失败'))
|
||||
} finally {
|
||||
paymentMethodsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toBillOption = (bill: {
|
||||
id: number
|
||||
source_no?: string | null
|
||||
remaining_amount?: number | null
|
||||
}): BillOption => ({
|
||||
id: bill.id,
|
||||
source_no: bill.source_no,
|
||||
remaining_amount: bill.remaining_amount ?? 0
|
||||
})
|
||||
|
||||
const loadCandidateBills = async () => {
|
||||
billsLoading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getBills({ page: 1, page_size: 100 })
|
||||
const list: BillOption[] =
|
||||
res.code === 0
|
||||
? normalizeCollectionList<EmployeeCollectionBill>(res.data)
|
||||
.filter((bill) => canCreateApplication(bill))
|
||||
.map((bill) => toBillOption(bill))
|
||||
: []
|
||||
|
||||
if (props.presetBill && !list.some((bill) => bill.id === props.presetBill?.id)) {
|
||||
list.unshift(toBillOption(props.presetBill))
|
||||
}
|
||||
|
||||
candidateBills.value = list
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取可核销账单失败'))
|
||||
} finally {
|
||||
billsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const normalizePaidAtForPicker = (value?: string | null): string => {
|
||||
if (!value) return ''
|
||||
const match = value
|
||||
.trim()
|
||||
.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.\d+)?\s*(Z|[+-]\d{2}:?\d{2})?$/)
|
||||
if (!match) return value
|
||||
const base = `${match[1]}T${match[2]}`
|
||||
const zone = match[3]
|
||||
if (!zone) return base
|
||||
if (zone === 'Z') return `${base}+00:00`
|
||||
return `${base}${zone.includes(':') ? zone : `${zone.slice(0, 3)}:${zone.slice(3)}`}`
|
||||
}
|
||||
|
||||
const fetchApplicationAllocations = async (
|
||||
applicationId: number
|
||||
): Promise<EmployeeCollectionAllocation[]> => {
|
||||
const res = await EmployeeCollectionService.getApplicationById(applicationId)
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data.allocations || []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const toggleBill = (bill: BillOption) => {
|
||||
const index = selectedBillIds.value.indexOf(bill.id)
|
||||
if (index >= 0) {
|
||||
selectedBillIds.value.splice(index, 1)
|
||||
delete amountMap[bill.id]
|
||||
} else {
|
||||
selectedBillIds.value.push(bill.id)
|
||||
amountMap[bill.id] = fenToYuan(bill.remaining_amount)
|
||||
}
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
form.payment_method_id = undefined
|
||||
form.paid_amount = 0
|
||||
form.payer_name = ''
|
||||
form.paid_at = ''
|
||||
form.external_transaction_no = ''
|
||||
form.payment_voucher_keys = []
|
||||
form.remark = ''
|
||||
form.acting_reason = ''
|
||||
selectedBillIds.value = []
|
||||
Object.keys(amountMap).forEach((key) => delete amountMap[Number(key)])
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const initialize = async (): Promise<void> => {
|
||||
resetState()
|
||||
await Promise.all([loadPaymentMethods(), loadCandidateBills()])
|
||||
|
||||
if (props.application) {
|
||||
const application = props.application
|
||||
form.payment_method_id = application.payment_method_id ?? undefined
|
||||
form.paid_amount = fenToYuan(application.paid_amount)
|
||||
form.payer_name = application.payer_name || ''
|
||||
form.paid_at = normalizePaidAtForPicker(application.paid_at)
|
||||
form.external_transaction_no = application.external_transaction_no || ''
|
||||
form.payment_voucher_keys = toVoucherKeyList(application.payment_voucher_keys ?? undefined)
|
||||
form.remark = application.remark || ''
|
||||
form.acting_reason = application.acting_reason || ''
|
||||
|
||||
const allocations = await fetchApplicationAllocations(application.id)
|
||||
allocations.forEach((allocation) => {
|
||||
if (!candidateBills.value.some((item) => item.id === allocation.bill_id)) {
|
||||
candidateBills.value.push({
|
||||
id: allocation.bill_id,
|
||||
source_no: allocation.bill_source_no,
|
||||
remaining_amount: Math.max(
|
||||
allocation.amount,
|
||||
(allocation.bill_receivable_amount ?? 0) -
|
||||
(allocation.bill_received_amount ?? 0) -
|
||||
(allocation.bill_reserved_amount ?? 0),
|
||||
0
|
||||
)
|
||||
})
|
||||
}
|
||||
if (!selectedBillIds.value.includes(allocation.bill_id)) {
|
||||
selectedBillIds.value.push(allocation.bill_id)
|
||||
}
|
||||
amountMap[allocation.bill_id] = fenToYuan(allocation.amount)
|
||||
})
|
||||
|
||||
if (paidAmountFen.value < totalAmountFen.value) {
|
||||
form.paid_amount = fenToYuan(totalAmountFen.value)
|
||||
}
|
||||
} else if (props.presetBill) {
|
||||
const bill = candidateBills.value.find((item) => item.id === props.presetBill?.id)
|
||||
selectedBillIds.value = [props.presetBill.id]
|
||||
amountMap[props.presetBill.id] = fenToYuan(
|
||||
bill?.remaining_amount ?? props.presetBill.remaining_amount ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (visible) void initialize()
|
||||
}
|
||||
)
|
||||
|
||||
const handleClosed = () => {
|
||||
resetState()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const buildAllocations = (): EmployeeCollectionAllocationRequest[] | null => {
|
||||
const payload: EmployeeCollectionAllocationRequest[] = []
|
||||
for (const billId of selectedBillIds.value) {
|
||||
const amount = yuanToFen(amountMap[billId]) || 0
|
||||
const option = candidateBills.value.find((bill) => bill.id === billId)
|
||||
if (amount <= 0) {
|
||||
ElMessage.warning('请填写每张账单的核销金额')
|
||||
return null
|
||||
}
|
||||
if (option && amount > option.remaining_amount) {
|
||||
ElMessage.warning('核销金额不能超过账单未核销金额')
|
||||
return null
|
||||
}
|
||||
payload.push({ bill_id: billId, amount })
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
|
||||
if (!selectedBillIds.value.length) {
|
||||
ElMessage.warning('请至少选择一张待核销账单')
|
||||
return
|
||||
}
|
||||
const allocations = buildAllocations()
|
||||
if (!allocations) return
|
||||
|
||||
const payload: EmployeeCollectionApplicationRequest = {
|
||||
payment_method_id: form.payment_method_id as number,
|
||||
paid_amount: paidAmountFen.value,
|
||||
paid_at: form.paid_at,
|
||||
payer_name: form.payer_name.trim(),
|
||||
external_transaction_no: form.external_transaction_no.trim(),
|
||||
payment_voucher_keys: toVoucherKeyList(form.payment_voucher_keys),
|
||||
remark: form.remark.trim() || undefined,
|
||||
allocations,
|
||||
acting_reason: isActing.value ? form.acting_reason.trim() : undefined
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = props.application
|
||||
? await EmployeeCollectionService.updateApplication(props.application.id, payload)
|
||||
: await EmployeeCollectionService.createApplication(payload)
|
||||
if (res.code !== 0) return
|
||||
ElMessage.success(isResubmit.value ? '重新提交成功' : '核销申请已提交')
|
||||
emit('update:modelValue', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '提交核销申请失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.amount-tip {
|
||||
margin-left: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.bill-selector {
|
||||
width: 100%;
|
||||
|
||||
&__empty {
|
||||
padding: 16px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 280px;
|
||||
padding-right: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__total {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.bill-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
308
src/views/finance/employee-collection/applications/detail.vue
Normal file
308
src/views/finance/employee-collection/applications/detail.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div class="employee-collection-application-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">核销申请详情</h2>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="application" :sections="detailSections" :data="application" />
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElCard v-if="allocations.length" shadow="never" class="block-card">
|
||||
<template #header>
|
||||
<div class="block-title">分摊账单</div>
|
||||
</template>
|
||||
<ElTable :data="allocations" border>
|
||||
<ElTableColumn label="账单编号" width="130">
|
||||
<template #default="{ row }">
|
||||
<span class="link-text" @click="handleViewBill(row.bill_id)">#{{ row.bill_id }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="bill_source_no" label="来源单号" min-width="190" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.bill_source_no || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="本次核销金额" width="140">
|
||||
<template #default="{ row }">{{ formatCollectionCurrency(row.amount) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="账单状态" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.bill_status_name || getBillStatusLabel(row.bill_status) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分摊状态" width="130">
|
||||
<template #default="{ row }">{{ row.status_name || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created_at" label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="released_at" label="释放时间" width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.released_at) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never" class="block-card">
|
||||
<template #header>
|
||||
<div class="block-title">付款凭证</div>
|
||||
</template>
|
||||
<ElButton v-if="voucherKeys.length" @click="voucherVisible = true">查看付款凭证</ElButton>
|
||||
<span v-else class="empty-text">暂无付款凭证</span>
|
||||
</ElCard>
|
||||
|
||||
<ElCard v-if="attempts.length" shadow="never" class="block-card">
|
||||
<template #header>
|
||||
<div class="block-title">提交与审批记录</div>
|
||||
</template>
|
||||
<ElTimeline>
|
||||
<ElTimelineItem
|
||||
v-for="(item, index) in attempts"
|
||||
:key="index"
|
||||
:timestamp="item.created_at ? formatDateTime(item.created_at) : ''"
|
||||
placement="top"
|
||||
>
|
||||
<div class="timeline-title">
|
||||
第 {{ item.attempt_no || index + 1 }} 次提交 · {{ item.approval_status_name || '-' }}
|
||||
</div>
|
||||
<div class="timeline-operator">
|
||||
付款方:{{ item.payer_name || '-' }} · 金额:{{
|
||||
formatCollectionCurrency(item.paid_amount)
|
||||
}}
|
||||
</div>
|
||||
<div class="timeline-operator">
|
||||
流水号:{{ item.external_transaction_no || '-' }} · 收款方式:{{
|
||||
item.payment_method_name || '-'
|
||||
}}
|
||||
</div>
|
||||
<div v-if="item.acting_reason" class="timeline-comment"
|
||||
>代办原因:{{ item.acting_reason }}</div
|
||||
>
|
||||
<div v-if="item.approval_opinion" class="timeline-comment">
|
||||
审批意见:{{ item.approval_opinion }}
|
||||
</div>
|
||||
<div v-if="item.remark" class="timeline-comment">备注:{{ item.remark }}</div>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-keys="voucherKeys" @close="voucherVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTimeline,
|
||||
ElTimelineItem
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionAllocation,
|
||||
EmployeeCollectionApplication,
|
||||
EmployeeCollectionApplicationAttempt,
|
||||
EmployeeCollectionApplicationDetailData
|
||||
} from '@/types/api'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getErrorMessage, toVoucherKeyList } from '@/utils/business'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import {
|
||||
formatCollectionCurrency,
|
||||
getApplicationStatusLabel,
|
||||
getBillStatusLabel
|
||||
} from '../employeeCollectionDisplay'
|
||||
|
||||
defineOptions({ name: 'EmployeeCollectionApplicationDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<EmployeeCollectionApplicationDetailData | null>(null)
|
||||
const voucherVisible = ref(false)
|
||||
|
||||
const application = computed<EmployeeCollectionApplication | null>(
|
||||
() => detail.value?.application || null
|
||||
)
|
||||
const allocations = computed<EmployeeCollectionAllocation[]>(
|
||||
() => detail.value?.allocations || []
|
||||
)
|
||||
const attempts = computed<EmployeeCollectionApplicationAttempt[]>(
|
||||
() => detail.value?.attempts || []
|
||||
)
|
||||
const voucherKeys = computed(() =>
|
||||
toVoucherKeyList(application.value?.payment_voucher_keys ?? undefined)
|
||||
)
|
||||
|
||||
const detailSections = computed((): DetailSection[] => [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请编号', prop: 'id', formatter: (value) => (value ? `#${value}` : '-') },
|
||||
{ label: '收款方式', prop: 'payment_method_name', formatter: (value) => value || '-' },
|
||||
{
|
||||
label: '付款金额',
|
||||
formatter: (_, data) => formatCollectionCurrency(data.paid_amount)
|
||||
},
|
||||
{ label: '付款方名称', prop: 'payer_name', formatter: (value) => value || '-' },
|
||||
{
|
||||
label: '付款时间',
|
||||
prop: 'paid_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
},
|
||||
{
|
||||
label: '外部交易流水号',
|
||||
prop: 'external_transaction_no',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
formatter: (value, data) => data.status_name || getApplicationStatusLabel(value)
|
||||
},
|
||||
{
|
||||
label: '审批实例ID',
|
||||
prop: 'latest_approval_instance_id',
|
||||
formatter: (value) => (value ? String(value) : '-')
|
||||
},
|
||||
{ label: '提交时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
||||
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) },
|
||||
{ label: '审批终态时间', prop: 'decided_at', formatter: (value) => formatDateTime(value) },
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '代办原因',
|
||||
prop: 'acting_reason',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '异常终态说明',
|
||||
prop: 'terminal_reason',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const loadDetail = async () => {
|
||||
const id = Number(route.params.id)
|
||||
if (!id) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getApplicationById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取核销申请详情失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewBill = (billId: number) => {
|
||||
if (!billId) return
|
||||
router.push({ path: `${RoutesAlias.EmployeeCollectionBillDetail}/${billId}` })
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.push({ path: RoutesAlias.EmployeeCollectionApplications })
|
||||
}
|
||||
|
||||
onMounted(() => void loadDetail())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.employee-collection-application-detail-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.block-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.timeline-operator {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.timeline-comment {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
300
src/views/finance/employee-collection/applications/index.vue
Normal file
300
src/views/finance/employee-collection/applications/index.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div id="table-full-screen" class="employee-collection-applications-page">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:column-list="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="getTableData"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-permission="AUGUST_PERMISSIONS.employeeCollection.applicationCreate"
|
||||
type="primary"
|
||||
@click="openCreateApplication"
|
||||
>
|
||||
创建核销申请
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="applicationList"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:margin-top="10"
|
||||
:actions="getActions"
|
||||
:actions-width="200"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ApplicationFormDialog
|
||||
v-model="applicationDialogVisible"
|
||||
:application="currentApplication"
|
||||
@success="handleApplicationSuccess"
|
||||
/>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElMessage, ElTag } from 'element-plus'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionApplication,
|
||||
EmployeeCollectionApplicationQueryParams,
|
||||
EmployeeCollectionPaymentMethod
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ApplicationFormDialog from './components/ApplicationFormDialog.vue'
|
||||
import {
|
||||
APPLICATION_STATUS_OPTIONS,
|
||||
canResubmitApplication,
|
||||
formatCollectionCurrency,
|
||||
getApplicationStatusLabel,
|
||||
getApplicationStatusTagType,
|
||||
normalizeCollectionList,
|
||||
normalizeCollectionPage
|
||||
} from '../employeeCollectionDisplay'
|
||||
|
||||
defineOptions({ name: 'EmployeeCollectionApplications' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const applicationDialogVisible = ref(false)
|
||||
const applicationList = ref<EmployeeCollectionApplication[]>([])
|
||||
const currentApplication = ref<EmployeeCollectionApplication | null>(null)
|
||||
const paymentMethods = ref<EmployeeCollectionPaymentMethod[]>([])
|
||||
|
||||
const initialSearchState = {
|
||||
status: undefined as number | undefined,
|
||||
payment_method_id: undefined as number | undefined,
|
||||
dateRange: [] as string[],
|
||||
created_from: '',
|
||||
created_to: ''
|
||||
}
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: APPLICATION_STATUS_OPTIONS,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '收款方式',
|
||||
prop: 'payment_method_id',
|
||||
type: 'select',
|
||||
placeholder: '请选择收款方式',
|
||||
options: () => paymentMethods.value.map((item) => ({ label: item.name, value: item.id })),
|
||||
config: { clearable: true, filterable: true }
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'dateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
valueFormat: 'YYYY-MM-DD'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '申请编号', prop: 'id' },
|
||||
{ label: '收款方式', prop: 'payment_method_name' },
|
||||
{ label: '付款金额', prop: 'paid_amount' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '提交时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
const handleViewDetail = (row: EmployeeCollectionApplication) => {
|
||||
router.push({ path: `${RoutesAlias.EmployeeCollectionApplicationDetail}/${row.id}` })
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: '申请编号',
|
||||
minWidth: 140,
|
||||
formatter: (row: EmployeeCollectionApplication) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: () => handleViewDetail(row)
|
||||
},
|
||||
`#${row.id}`
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'payment_method_name',
|
||||
label: '收款方式',
|
||||
width: 140,
|
||||
formatter: (row: EmployeeCollectionApplication) => row.payment_method_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'paid_amount',
|
||||
label: '付款金额',
|
||||
width: 130,
|
||||
formatter: (row: EmployeeCollectionApplication) => formatCollectionCurrency(row.paid_amount)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 130,
|
||||
formatter: (row: EmployeeCollectionApplication) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: getApplicationStatusTagType(row.status), effect: 'plain' },
|
||||
() => row.status_name || getApplicationStatusLabel(row.status)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '提交时间',
|
||||
width: 170,
|
||||
formatter: (row: EmployeeCollectionApplication) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const buildQueryParams = (): EmployeeCollectionApplicationQueryParams => ({
|
||||
status: searchForm.status,
|
||||
payment_method_id: searchForm.payment_method_id,
|
||||
created_from: searchForm.created_from || undefined,
|
||||
created_to: searchForm.created_to || undefined
|
||||
})
|
||||
|
||||
const loadPaymentMethods = async () => {
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getPaymentMethods({ page: 1, page_size: 100 })
|
||||
if (res.code === 0) {
|
||||
paymentMethods.value = normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load payment methods failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getApplications({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...buildQueryParams()
|
||||
})
|
||||
if (res.code === 0) {
|
||||
const { list, total } = normalizeCollectionPage<EmployeeCollectionApplication>(res.data)
|
||||
applicationList.value = list
|
||||
pagination.total = total
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取核销申请列表失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
|
||||
searchForm.created_from = searchForm.dateRange[0]
|
||||
searchForm.created_to = searchForm.dateRange[1]
|
||||
} else {
|
||||
searchForm.created_from = ''
|
||||
searchForm.created_to = ''
|
||||
}
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const openCreateApplication = () => {
|
||||
currentApplication.value = null
|
||||
applicationDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleResubmit = (row: EmployeeCollectionApplication) => {
|
||||
currentApplication.value = row
|
||||
applicationDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleApplicationSuccess = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const getActions = (row: EmployeeCollectionApplication) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationDetail)) {
|
||||
actions.push({ label: '详情', handler: () => handleViewDetail(row), type: 'primary' })
|
||||
}
|
||||
if (
|
||||
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationUpdate) &&
|
||||
canResubmitApplication(row)
|
||||
) {
|
||||
actions.push({ label: '修改并重新提交', handler: () => handleResubmit(row), type: 'primary' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void getTableData()
|
||||
void loadPaymentMethods()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.employee-collection-applications-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
368
src/views/finance/employee-collection/bills/detail.vue
Normal file
368
src/views/finance/employee-collection/bills/detail.vue
Normal file
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="employee-collection-bill-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">员工代收款账单详情</h2>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="bill" :sections="detailSections" :data="bill" />
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElCard v-if="allocations.length" shadow="never" class="block-card">
|
||||
<template #header>
|
||||
<div class="block-title">核销分摊</div>
|
||||
</template>
|
||||
<ElTable :data="allocations" border>
|
||||
<ElTableColumn label="核销申请" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.application_id ? `#${row.application_id}` : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="核销金额" width="140">
|
||||
<template #default="{ row }">{{ formatCollectionCurrency(row.amount) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分摊状态" width="130">
|
||||
<template #default="{ row }">{{ row.status_name || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请状态" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.application_status_name || getApplicationStatusLabel(row.application_status) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="所属尝试" width="110">
|
||||
<template #default="{ row }">{{ row.attempt_id ? `#${row.attempt_id}` : '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created_at" label="创建时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="released_at" label="释放时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.released_at) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
|
||||
<ElCard v-if="applications.length" shadow="never" class="block-card">
|
||||
<template #header>
|
||||
<div class="block-title">关联核销申请</div>
|
||||
</template>
|
||||
<ElTable :data="applications" border>
|
||||
<ElTableColumn type="expand" width="48">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.attempts?.length" class="attempt-list">
|
||||
<ElTimeline>
|
||||
<ElTimelineItem
|
||||
v-for="(item, index) in row.attempts"
|
||||
:key="index"
|
||||
:timestamp="item.created_at ? formatDateTime(item.created_at) : ''"
|
||||
placement="top"
|
||||
>
|
||||
<div class="timeline-title">
|
||||
第 {{ item.attempt_no || index + 1 }} 次提交 ·
|
||||
{{ item.approval_status_name || '-' }}
|
||||
</div>
|
||||
<div class="timeline-operator">
|
||||
付款方:{{ item.payer_name || '-' }} · 金额:{{
|
||||
formatCollectionCurrency(item.paid_amount)
|
||||
}}
|
||||
</div>
|
||||
<div class="timeline-operator">
|
||||
流水号:{{ item.external_transaction_no || '-' }} · 收款方式:{{
|
||||
item.payment_method_name || '-'
|
||||
}}
|
||||
</div>
|
||||
<div v-if="item.approval_opinion" class="timeline-comment">
|
||||
审批意见:{{ item.approval_opinion }}
|
||||
</div>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</div>
|
||||
<span v-else class="empty-text">暂无审批记录</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请编号" width="140">
|
||||
<template #default="{ row }">
|
||||
<span class="link-text" @click="handleViewApplication(row)">#{{ row.id }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="收款方式" width="140">
|
||||
<template #default="{ row }">{{ row.payment_method_name || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="付款金额" width="140">
|
||||
<template #default="{ row }">{{ formatCollectionCurrency(row.paid_amount) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.status_name || getApplicationStatusLabel(row.status) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created_at" label="提交时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
|
||||
<ElCard v-if="refunds.length" shadow="never" class="block-card">
|
||||
<template #header>
|
||||
<div class="block-title">退款冲销</div>
|
||||
</template>
|
||||
<ElTable :data="refunds" border>
|
||||
<ElTableColumn label="退款申请ID" width="130">
|
||||
<template #default="{ row }">{{ row.refund_id ? `#${row.refund_id}` : '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="来源订单ID" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.source_order_id ? `#${row.source_order_id}` : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="退款金额" width="130">
|
||||
<template #default="{ row }">{{ formatCollectionCurrency(row.refund_amount) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="冲减应收" width="130">
|
||||
<template #default="{ row }">{{ formatCollectionCurrency(row.reduced_amount) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="冲销前应收" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatCollectionCurrency(row.bill_receivable_amount) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="处理结果" width="150">
|
||||
<template #default="{ row }">{{ row.outcome_name || row.outcome || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created_at" label="创建时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTimeline,
|
||||
ElTimelineItem
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionBill,
|
||||
EmployeeCollectionBillAllocation,
|
||||
EmployeeCollectionBillApplication,
|
||||
EmployeeCollectionBillDetailData,
|
||||
EmployeeCollectionBillRefund
|
||||
} from '@/types/api'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
formatCollectionCurrency,
|
||||
getApplicationStatusLabel,
|
||||
getBillSourceLabel,
|
||||
getBillStatusLabel
|
||||
} from '../employeeCollectionDisplay'
|
||||
|
||||
defineOptions({ name: 'EmployeeCollectionBillDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<EmployeeCollectionBillDetailData | null>(null)
|
||||
|
||||
const bill = computed<EmployeeCollectionBill | null>(() => detail.value?.bill || null)
|
||||
const allocations = computed<EmployeeCollectionBillAllocation[]>(
|
||||
() => detail.value?.allocations || []
|
||||
)
|
||||
const applications = computed<EmployeeCollectionBillApplication[]>(
|
||||
() => detail.value?.applications || []
|
||||
)
|
||||
const refunds = computed<EmployeeCollectionBillRefund[]>(() => detail.value?.refunds || [])
|
||||
|
||||
const detailSections = computed((): DetailSection[] => [
|
||||
{
|
||||
title: '账单信息',
|
||||
fields: [
|
||||
{ label: '账单编号', prop: 'id', formatter: (value) => (value ? `#${value}` : '-') },
|
||||
{
|
||||
label: '来源',
|
||||
prop: 'source_type_name',
|
||||
formatter: (value, data) => value || getBillSourceLabel(data.source_type)
|
||||
},
|
||||
{ label: '关联单号', prop: 'source_no', formatter: (value) => value || '-' },
|
||||
{
|
||||
label: '负责员工',
|
||||
prop: 'debtor_snapshot.account_name',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '客户',
|
||||
prop: 'customer_snapshot.buyer_nickname',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'customer_snapshot.shop_id',
|
||||
formatter: (value) => (value ? `店铺 #${value}` : '-')
|
||||
},
|
||||
{
|
||||
label: '资产标识',
|
||||
prop: 'customer_snapshot.asset_identifier',
|
||||
formatter: (value) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '账单状态',
|
||||
prop: 'status_name',
|
||||
formatter: (value, data) => value || getBillStatusLabel(data.status)
|
||||
},
|
||||
{
|
||||
label: '应收金额',
|
||||
formatter: (_, data) => formatCollectionCurrency(data.receivable_amount)
|
||||
},
|
||||
{
|
||||
label: '已核销金额',
|
||||
formatter: (_, data) => formatCollectionCurrency(data.received_amount)
|
||||
},
|
||||
{
|
||||
label: '审批中预占金额',
|
||||
formatter: (_, data) => formatCollectionCurrency(data.reserved_amount)
|
||||
},
|
||||
{
|
||||
label: '未核销金额',
|
||||
formatter: (_, data) => formatCollectionCurrency(data.remaining_amount)
|
||||
},
|
||||
{
|
||||
label: '审批中申请',
|
||||
formatter: (_, data) => (data.approval_pending ? '存在审批中的申请' : '无')
|
||||
},
|
||||
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
||||
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) },
|
||||
{ label: '关闭时间', prop: 'closed_at', formatter: (value) => formatDateTime(value) },
|
||||
{
|
||||
label: '关闭原因',
|
||||
prop: 'closed_reason',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const loadDetail = async () => {
|
||||
const id = Number(route.params.id)
|
||||
if (!id) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getBillById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取账单详情失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewApplication = (row: EmployeeCollectionBillApplication) => {
|
||||
router.push({ path: `${RoutesAlias.EmployeeCollectionApplicationDetail}/${row.id}` })
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.push({ path: RoutesAlias.EmployeeCollectionBills })
|
||||
}
|
||||
|
||||
onMounted(() => void loadDetail())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.employee-collection-bill-detail-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.block-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attempt-list {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.timeline-operator {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.timeline-comment {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
539
src/views/finance/employee-collection/bills/index.vue
Normal file
539
src/views/finance/employee-collection/bills/index.vue
Normal file
@@ -0,0 +1,539 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div id="table-full-screen" class="employee-collection-bills-page">
|
||||
<div class="stat-row">
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-card__label">应收金额</div>
|
||||
<div class="stat-card__value">
|
||||
{{ formatCollectionCurrency(statistics.receivable_total) }}
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-card__label">已核销金额</div>
|
||||
<div class="stat-card__value stat-card__value--success">
|
||||
{{ formatCollectionCurrency(statistics.received_total) }}
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-card__label">未核销金额</div>
|
||||
<div class="stat-card__value stat-card__value--warning">
|
||||
{{ formatCollectionCurrency(statistics.unsettled_total) }}
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-card__label">待处理账单</div>
|
||||
<div class="stat-card__value">{{ statistics.pending_bill_count }}</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:column-list="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-permission="AUGUST_PERMISSIONS.employeeCollection.applicationCreate"
|
||||
type="primary"
|
||||
:disabled="!hasSelectableBill"
|
||||
@click="openCreateApplication()"
|
||||
>
|
||||
创建核销申请
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="billList"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:margin-top="10"
|
||||
:actions="getActions"
|
||||
:actions-width="200"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ElDialog v-model="closeDialogVisible" title="关闭账单" width="480px" destroy-on-close>
|
||||
<ElForm ref="closeFormRef" :model="closeForm" :rules="closeRules" label-width="88px">
|
||||
<ElFormItem label="账单编号">
|
||||
<span>{{ currentBill ? `#${currentBill.id}` : '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="关闭原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="closeForm.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请填写关闭原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="closeDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="closeSubmitting" @click="handleCloseBill">
|
||||
确定关闭
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ApplicationFormDialog
|
||||
v-model="applicationDialogVisible"
|
||||
:preset-bill="presetBill"
|
||||
@success="handleApplicationSuccess"
|
||||
/>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElMessage, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { EmployeeCollectionService, ShopService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionBill,
|
||||
EmployeeCollectionBillQueryParams,
|
||||
EmployeeCollectionBillStatistics
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ApplicationFormDialog from '../applications/components/ApplicationFormDialog.vue'
|
||||
import {
|
||||
BILL_SOURCE_OPTIONS,
|
||||
BILL_STATUS_OPTIONS,
|
||||
canCloseBill,
|
||||
canCreateApplication,
|
||||
formatCollectionCurrency,
|
||||
getBillCustomerLabel,
|
||||
getBillDebtorName,
|
||||
getBillSourceLabel,
|
||||
getBillStatusLabel,
|
||||
getBillStatusTagType,
|
||||
normalizeCollectionPage
|
||||
} from '../employeeCollectionDisplay'
|
||||
|
||||
defineOptions({ name: 'EmployeeCollectionBills' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const closeSubmitting = ref(false)
|
||||
const tableRef = ref()
|
||||
const closeFormRef = ref<FormInstance>()
|
||||
const closeDialogVisible = ref(false)
|
||||
const applicationDialogVisible = ref(false)
|
||||
const currentBill = ref<EmployeeCollectionBill | null>(null)
|
||||
const presetBill = ref<EmployeeCollectionBill | null>(null)
|
||||
const billList = ref<EmployeeCollectionBill[]>([])
|
||||
const shopOptions = ref<Array<{ id: number; shop_name: string }>>([])
|
||||
const statistics = ref<EmployeeCollectionBillStatistics>({
|
||||
receivable_total: 0,
|
||||
received_total: 0,
|
||||
unsettled_total: 0,
|
||||
pending_bill_count: 0
|
||||
})
|
||||
|
||||
const initialSearchState = {
|
||||
source_type: undefined as string | undefined,
|
||||
source_no: '',
|
||||
status: undefined as number | undefined,
|
||||
customer_id: undefined as number | undefined,
|
||||
dateRange: [] as string[],
|
||||
created_from: '',
|
||||
created_to: ''
|
||||
}
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
const closeForm = reactive({ reason: '' })
|
||||
|
||||
const closeRules: FormRules = {
|
||||
reason: [{ required: true, message: '请填写关闭原因', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const shopNameMap = computed<Record<number, string>>(() =>
|
||||
shopOptions.value.reduce<Record<number, string>>((map, shop) => {
|
||||
map[shop.id] = shop.shop_name
|
||||
return map
|
||||
}, {})
|
||||
)
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '来源',
|
||||
prop: 'source_type',
|
||||
type: 'select',
|
||||
placeholder: '请选择来源',
|
||||
options: BILL_SOURCE_OPTIONS,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '来源单号',
|
||||
prop: 'source_no',
|
||||
type: 'input',
|
||||
placeholder: '请输入来源单号',
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '核销状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
placeholder: '请选择核销状态',
|
||||
options: BILL_STATUS_OPTIONS,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'customer_id',
|
||||
type: 'select',
|
||||
placeholder: '请选择店铺',
|
||||
options: () => shopOptions.value.map((shop) => ({ label: shop.shop_name, value: shop.id })),
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: (query: string) => searchShops(query)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '起止时间',
|
||||
prop: 'dateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
valueFormat: 'YYYY-MM-DD'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '账单编号', prop: 'id' },
|
||||
{ label: '来源', prop: 'source_type' },
|
||||
{ label: '关联单号', prop: 'source_no' },
|
||||
{ label: '负责员工', prop: 'debtor_snapshot' },
|
||||
{ label: '客户/店铺', prop: 'customer_snapshot' },
|
||||
{ label: '应收金额', prop: 'receivable_amount' },
|
||||
{ label: '已核销金额', prop: 'received_amount' },
|
||||
{ label: '未核销金额', prop: 'remaining_amount' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
const hasSelectableBill = computed(() =>
|
||||
billList.value.some((bill) => canCreateApplication(bill))
|
||||
)
|
||||
|
||||
const buildQueryParams = (): EmployeeCollectionBillQueryParams => ({
|
||||
source_type: searchForm.source_type,
|
||||
source_no: searchForm.source_no.trim() || undefined,
|
||||
status: searchForm.status,
|
||||
customer_id: searchForm.customer_id,
|
||||
created_from: searchForm.created_from || undefined,
|
||||
created_to: searchForm.created_to || undefined
|
||||
})
|
||||
|
||||
const searchShops = async (query: string) => {
|
||||
try {
|
||||
const params: any = { page: 1, page_size: 20 }
|
||||
if (query) params.shop_name = query
|
||||
const res = await ShopService.getShops(params)
|
||||
if (res.code === 0) {
|
||||
shopOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search shops failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: EmployeeCollectionBill) => {
|
||||
router.push({ path: `${RoutesAlias.EmployeeCollectionBillDetail}/${row.id}` })
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: '账单编号',
|
||||
minWidth: 120,
|
||||
formatter: (row: EmployeeCollectionBill) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: () => handleViewDetail(row)
|
||||
},
|
||||
`#${row.id}`
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'source_type',
|
||||
label: '来源',
|
||||
width: 160,
|
||||
formatter: (row: EmployeeCollectionBill) =>
|
||||
row.source_type_name || getBillSourceLabel(row.source_type)
|
||||
},
|
||||
{ prop: 'source_no', label: '关联单号', width: 210, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'debtor_snapshot',
|
||||
label: '负责员工',
|
||||
width: 120,
|
||||
formatter: (row: EmployeeCollectionBill) => getBillDebtorName(row)
|
||||
},
|
||||
{
|
||||
prop: 'customer_snapshot',
|
||||
label: '客户/店铺',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: EmployeeCollectionBill) => getBillCustomerLabel(row, shopNameMap.value)
|
||||
},
|
||||
{
|
||||
prop: 'receivable_amount',
|
||||
label: '应收金额',
|
||||
width: 120,
|
||||
formatter: (row: EmployeeCollectionBill) => formatCollectionCurrency(row.receivable_amount)
|
||||
},
|
||||
{
|
||||
prop: 'received_amount',
|
||||
label: '已核销金额',
|
||||
width: 130,
|
||||
formatter: (row: EmployeeCollectionBill) => formatCollectionCurrency(row.received_amount)
|
||||
},
|
||||
{
|
||||
prop: 'remaining_amount',
|
||||
label: '未核销金额',
|
||||
width: 130,
|
||||
formatter: (row: EmployeeCollectionBill) => formatCollectionCurrency(row.remaining_amount)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 110,
|
||||
formatter: (row: EmployeeCollectionBill) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: getBillStatusTagType(row.status), effect: 'plain' },
|
||||
() => row.status_name || getBillStatusLabel(row.status)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: EmployeeCollectionBill) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getBills({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...buildQueryParams()
|
||||
})
|
||||
if (res.code === 0) {
|
||||
const { list, total } = normalizeCollectionPage<EmployeeCollectionBill>(res.data)
|
||||
billList.value = list
|
||||
pagination.total = total
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取账单列表失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getStatistics = async () => {
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getBillStatistics(buildQueryParams())
|
||||
if (res.code === 0 && res.data) {
|
||||
statistics.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取账单统计失败'))
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
void getTableData()
|
||||
void getStatistics()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
|
||||
searchForm.created_from = searchForm.dateRange[0]
|
||||
searchForm.created_to = searchForm.dateRange[1]
|
||||
} else {
|
||||
searchForm.created_from = ''
|
||||
searchForm.created_to = ''
|
||||
}
|
||||
pagination.page = 1
|
||||
reload()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
reload()
|
||||
}
|
||||
|
||||
const handleRefresh = () => reload()
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const openCreateApplication = (row?: EmployeeCollectionBill) => {
|
||||
presetBill.value = row || null
|
||||
applicationDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleApplicationSuccess = () => {
|
||||
reload()
|
||||
}
|
||||
|
||||
const openCloseDialog = (row: EmployeeCollectionBill) => {
|
||||
if (!canCloseBill(row)) {
|
||||
ElMessage.warning('账单已关闭,无需重复操作')
|
||||
return
|
||||
}
|
||||
if (row.approval_pending) {
|
||||
ElMessage.warning('存在审批中的核销申请,账单暂不可关闭')
|
||||
return
|
||||
}
|
||||
currentBill.value = row
|
||||
closeForm.reason = ''
|
||||
closeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCloseBill = async () => {
|
||||
if (!closeFormRef.value || !currentBill.value) return
|
||||
await closeFormRef.value.validate()
|
||||
closeSubmitting.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.closeBill(currentBill.value.id, {
|
||||
reason: closeForm.reason.trim()
|
||||
})
|
||||
if (res.code !== 0) return
|
||||
ElMessage.success('账单已关闭')
|
||||
closeDialogVisible.value = false
|
||||
reload()
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '关闭账单失败'))
|
||||
} finally {
|
||||
closeSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: EmployeeCollectionBill) => {
|
||||
const actions: any[] = []
|
||||
if (
|
||||
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationCreate) &&
|
||||
canCreateApplication(row)
|
||||
) {
|
||||
actions.push({
|
||||
label: '创建核销申请',
|
||||
handler: () => openCreateApplication(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.billDetail)) {
|
||||
actions.push({ label: '详情', handler: () => handleViewDetail(row), type: 'primary' })
|
||||
}
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.billClose) && canCloseBill(row)) {
|
||||
actions.push({ label: '关闭账单', handler: () => openCloseDialog(row), type: 'danger' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reload()
|
||||
void searchShops('')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.employee-collection-bills-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__value {
|
||||
margin-top: 8px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&--success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.stat-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 员工代收款展示辅助函数
|
||||
*/
|
||||
|
||||
import type {
|
||||
EmployeeCollectionApplication,
|
||||
EmployeeCollectionApplicationStatus,
|
||||
EmployeeCollectionBill,
|
||||
EmployeeCollectionBillStatus
|
||||
} from '@/types/api'
|
||||
|
||||
type TagType = 'success' | 'warning' | 'danger' | 'info'
|
||||
|
||||
/** 账单状态:0 待核销 / 1 部分核销 / 2 已核销 / 3 已关闭 */
|
||||
export const BILL_STATUS = {
|
||||
PENDING: 0,
|
||||
PARTIAL: 1,
|
||||
VERIFIED: 2,
|
||||
CLOSED: 3
|
||||
} as const
|
||||
|
||||
/** 申请状态:0 审批中 / 1 已通过 / 2 已驳回 / 3 已撤销或已关闭 */
|
||||
export const APPLICATION_STATUS = {
|
||||
PENDING: 0,
|
||||
APPROVED: 1,
|
||||
REJECTED: 2,
|
||||
REVOKED: 3
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 兼容后端列表返回结构:
|
||||
* - 直接返回数组
|
||||
* - { list: [...] } / { items: [...] } / { records: [...] }
|
||||
*/
|
||||
export const normalizeCollectionPage = <T>(data: unknown): { list: T[]; total: number } => {
|
||||
if (Array.isArray(data)) {
|
||||
return { list: data as T[], total: data.length }
|
||||
}
|
||||
if (data && typeof data === 'object') {
|
||||
const record = data as Record<string, unknown>
|
||||
for (const key of ['items', 'list', 'records']) {
|
||||
const value = record[key]
|
||||
if (Array.isArray(value)) {
|
||||
const total = Number(record.total)
|
||||
return { list: value as T[], total: Number.isNaN(total) ? value.length : total }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { list: [], total: 0 }
|
||||
}
|
||||
|
||||
export const normalizeCollectionList = <T>(data: unknown): T[] =>
|
||||
normalizeCollectionPage<T>(data).list
|
||||
|
||||
/**
|
||||
* 金额格式化(分 -> 元)
|
||||
*/
|
||||
export const formatCollectionCurrency = (amount?: number | null): string => {
|
||||
if (amount === undefined || amount === null || Number.isNaN(amount)) return '-'
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单状态选项
|
||||
*/
|
||||
export const BILL_STATUS_OPTIONS = [
|
||||
{ label: '待核销', value: BILL_STATUS.PENDING },
|
||||
{ label: '部分核销', value: BILL_STATUS.PARTIAL },
|
||||
{ label: '已核销', value: BILL_STATUS.VERIFIED },
|
||||
{ label: '已关闭', value: BILL_STATUS.CLOSED }
|
||||
]
|
||||
|
||||
/**
|
||||
* 核销申请状态选项
|
||||
*/
|
||||
export const APPLICATION_STATUS_OPTIONS = [
|
||||
{ label: '审批中', value: APPLICATION_STATUS.PENDING },
|
||||
{ label: '已通过', value: APPLICATION_STATUS.APPROVED },
|
||||
{ label: '已驳回', value: APPLICATION_STATUS.REJECTED },
|
||||
{ label: '已撤销或已关闭', value: APPLICATION_STATUS.REVOKED }
|
||||
]
|
||||
|
||||
/**
|
||||
* 账单来源选项
|
||||
*/
|
||||
export const BILL_SOURCE_OPTIONS = [
|
||||
{ label: '后台线下套餐订单', value: 'order' },
|
||||
{ label: '代理线下充值', value: 'recharge' }
|
||||
]
|
||||
|
||||
const BILL_STATUS_LABEL_MAP: Record<number, string> = BILL_STATUS_OPTIONS.reduce(
|
||||
(map, item) => ({ ...map, [item.value]: item.label }),
|
||||
{}
|
||||
)
|
||||
|
||||
const APPLICATION_STATUS_LABEL_MAP: Record<number, string> = APPLICATION_STATUS_OPTIONS.reduce(
|
||||
(map, item) => ({ ...map, [item.value]: item.label }),
|
||||
{}
|
||||
)
|
||||
|
||||
const BILL_SOURCE_LABEL_MAP: Record<string, string> = BILL_SOURCE_OPTIONS.reduce(
|
||||
(map, item) => ({ ...map, [item.value]: item.label }),
|
||||
{}
|
||||
)
|
||||
|
||||
/**
|
||||
* 账单状态文案(优先使用后端 status_name)
|
||||
*/
|
||||
export const getBillStatusLabel = (status?: EmployeeCollectionBillStatus | null): string => {
|
||||
if (status === undefined || status === null) return '-'
|
||||
return BILL_STATUS_LABEL_MAP[status] ?? String(status)
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销申请状态文案
|
||||
*/
|
||||
export const getApplicationStatusLabel = (
|
||||
status?: EmployeeCollectionApplicationStatus | null
|
||||
): string => {
|
||||
if (status === undefined || status === null) return '-'
|
||||
return APPLICATION_STATUS_LABEL_MAP[status] ?? String(status)
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单来源文案(优先使用后端 source_type_name)
|
||||
*/
|
||||
export const getBillSourceLabel = (source?: string | null): string => {
|
||||
if (!source) return '-'
|
||||
return BILL_SOURCE_LABEL_MAP[source] || source
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单状态标签类型
|
||||
*/
|
||||
export const getBillStatusTagType = (status?: EmployeeCollectionBillStatus | null): TagType => {
|
||||
switch (status) {
|
||||
case BILL_STATUS.PENDING:
|
||||
return 'warning'
|
||||
case BILL_STATUS.PARTIAL:
|
||||
return 'info'
|
||||
case BILL_STATUS.VERIFIED:
|
||||
return 'success'
|
||||
case BILL_STATUS.CLOSED:
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销申请状态标签类型
|
||||
*/
|
||||
export const getApplicationStatusTagType = (
|
||||
status?: EmployeeCollectionApplicationStatus | null
|
||||
): TagType => {
|
||||
switch (status) {
|
||||
case APPLICATION_STATUS.PENDING:
|
||||
return 'warning'
|
||||
case APPLICATION_STATUS.APPROVED:
|
||||
return 'success'
|
||||
case APPLICATION_STATUS.REJECTED:
|
||||
return 'danger'
|
||||
case APPLICATION_STATUS.REVOKED:
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单责任人(员工)名称
|
||||
*/
|
||||
export const getBillDebtorName = (bill?: EmployeeCollectionBill | null): string =>
|
||||
bill?.debtor_snapshot?.account_name || '-'
|
||||
|
||||
/**
|
||||
* 账单关联店铺 ID
|
||||
*/
|
||||
export const getBillShopId = (bill?: EmployeeCollectionBill | null): number | undefined =>
|
||||
bill?.customer_snapshot?.shop_id ?? bill?.customer_snapshot?.seller_shop_id ?? undefined
|
||||
|
||||
/**
|
||||
* 账单客户展示:优先买家昵称,其次店铺名称,最后店铺 ID
|
||||
*/
|
||||
export const getBillCustomerLabel = (
|
||||
bill?: EmployeeCollectionBill | null,
|
||||
shopNameMap?: Record<number, string>
|
||||
): string => {
|
||||
const nickname = bill?.customer_snapshot?.buyer_nickname
|
||||
if (nickname) return nickname
|
||||
const shopId = getBillShopId(bill)
|
||||
if (!shopId) return '-'
|
||||
return shopNameMap?.[shopId] || `店铺 #${shopId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单是否可关闭:仅待核销或部分核销可关闭(存在审批中预占时由调用方拦截或后端拒绝)
|
||||
*/
|
||||
export const canCloseBill = (bill: EmployeeCollectionBill): boolean =>
|
||||
bill.status === BILL_STATUS.PENDING || bill.status === BILL_STATUS.PARTIAL
|
||||
|
||||
/**
|
||||
* 账单是否可发起核销申请
|
||||
*/
|
||||
export const canCreateApplication = (bill: EmployeeCollectionBill): boolean => {
|
||||
if (bill.status === BILL_STATUS.CLOSED) return false
|
||||
return (bill.remaining_amount ?? 0) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请是否可修改并重新提交
|
||||
*/
|
||||
export const canResubmitApplication = (application: EmployeeCollectionApplication): boolean => {
|
||||
return application.status === APPLICATION_STATUS.REJECTED
|
||||
}
|
||||
348
src/views/finance/employee-collection/payment-methods/index.vue
Normal file
348
src/views/finance/employee-collection/payment-methods/index.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div id="table-full-screen" class="employee-collection-payment-methods-page">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:column-list="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="getTableData"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-permission="AUGUST_PERMISSIONS.employeeCollection.paymentMethodCreate"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>
|
||||
新增收款方式
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="filteredPaymentMethods"
|
||||
:pagination="false"
|
||||
:margin-top="10"
|
||||
:actions="getActions"
|
||||
:actions-width="140"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑收款方式' : '新增收款方式'"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="56px">
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入收款方式名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="编码" prop="code">
|
||||
<ElInput v-model="form.code" maxlength="64" placeholder="请输入唯一编码,如 cash" />
|
||||
<div v-if="editing" class="form-tip">编码在未被核销申请引用时可修改</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="选填"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="saving" @click="handleSubmit">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionPaymentMethod,
|
||||
EmployeeCollectionPaymentMethodQueryParams
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { STATUS_SELECT_OPTIONS } from '@/config/constants/status'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeCollectionList } from '../employeeCollectionDisplay'
|
||||
|
||||
defineOptions({ name: 'EmployeeCollectionPaymentMethods' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(false)
|
||||
const currentId = ref<number | null>(null)
|
||||
const paymentMethods = ref<EmployeeCollectionPaymentMethod[]>([])
|
||||
|
||||
const initialSearchState = {
|
||||
keyword: '',
|
||||
enabled: undefined as number | undefined
|
||||
}
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '关键字',
|
||||
prop: 'keyword',
|
||||
type: 'input',
|
||||
placeholder: '请输入名称或编码',
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'enabled',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: STATUS_SELECT_OPTIONS,
|
||||
config: { clearable: true }
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '名称', prop: 'name' },
|
||||
{ label: '编码', prop: 'code' },
|
||||
{ label: '排序', prop: 'sort' },
|
||||
{ label: '状态', prop: 'enabled' },
|
||||
{ label: '备注', prop: 'remark' },
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ prop: 'name', label: '名称', minWidth: 140 },
|
||||
{ prop: 'code', label: '编码', minWidth: 140 },
|
||||
{ prop: 'sort', label: '排序', width: 90 },
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: EmployeeCollectionPaymentMethod) =>
|
||||
h(ElTag, { type: row.enabled ? 'success' : 'info', effect: 'plain' }, () =>
|
||||
row.enabled ? '启用' : '停用'
|
||||
)
|
||||
},
|
||||
{ prop: 'remark', label: '备注', minWidth: 160, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: EmployeeCollectionPaymentMethod) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const filteredPaymentMethods = computed(() => {
|
||||
const keyword = searchForm.keyword?.trim().toLowerCase()
|
||||
return paymentMethods.value.filter((item) => {
|
||||
const matchKeyword =
|
||||
!keyword ||
|
||||
(item.name || '').toLowerCase().includes(keyword) ||
|
||||
(item.code || '').toLowerCase().includes(keyword)
|
||||
const matchEnabled =
|
||||
searchForm.enabled === undefined ||
|
||||
searchForm.enabled === null ||
|
||||
(searchForm.enabled === 1 ? item.enabled : !item.enabled)
|
||||
return matchKeyword && matchEnabled
|
||||
})
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
code: '',
|
||||
name: '',
|
||||
sort: 0,
|
||||
enabled: true,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入收款方式名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入收款方式编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: EmployeeCollectionPaymentMethodQueryParams = { page: 1, page_size: 100 }
|
||||
if (searchForm.enabled !== undefined && searchForm.enabled !== null) {
|
||||
params.enabled = searchForm.enabled === 1
|
||||
}
|
||||
if (searchForm.keyword?.trim()) {
|
||||
params.keyword = searchForm.keyword.trim()
|
||||
}
|
||||
const res = await EmployeeCollectionService.getPaymentMethods(params)
|
||||
if (res.code === 0) {
|
||||
paymentMethods.value = normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取收款方式失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => getTableData()
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.code = ''
|
||||
form.name = ''
|
||||
form.sort = 0
|
||||
form.enabled = true
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
editing.value = false
|
||||
currentId.value = null
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEdit = (row: EmployeeCollectionPaymentMethod) => {
|
||||
editing.value = true
|
||||
currentId.value = row.id
|
||||
form.code = row.code
|
||||
form.name = row.name
|
||||
form.sort = row.sort ?? 0
|
||||
form.enabled = !!row.enabled
|
||||
form.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
resetForm()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value && currentId.value !== null) {
|
||||
const res = await EmployeeCollectionService.updatePaymentMethod(currentId.value, {
|
||||
code: form.code.trim(),
|
||||
name: form.name.trim(),
|
||||
sort: form.sort,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark.trim()
|
||||
})
|
||||
if (res.code !== 0) return
|
||||
} else {
|
||||
const res = await EmployeeCollectionService.createPaymentMethod({
|
||||
code: form.code.trim(),
|
||||
name: form.name.trim(),
|
||||
sort: form.sort,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark.trim()
|
||||
})
|
||||
if (res.code !== 0) return
|
||||
}
|
||||
ElMessage.success(editing.value ? '修改成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row: EmployeeCollectionPaymentMethod) => {
|
||||
ElMessageBox.confirm(`确定删除收款方式「${row.name}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await EmployeeCollectionService.deletePaymentMethod(row.id)
|
||||
if (res && res.code !== 0) return
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error(
|
||||
getErrorMessage(error, '删除失败;该收款方式可能已被业务引用,请改为停用')
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
const getActions = (row: EmployeeCollectionPaymentMethod) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.paymentMethodEdit)) {
|
||||
actions.push({ label: '编辑', handler: () => openEdit(row), type: 'primary' })
|
||||
}
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.paymentMethodDelete)) {
|
||||
actions.push({ label: '删除', handler: () => handleDelete(row), type: 'danger' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => void getTableData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.employee-collection-payment-methods-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -197,7 +197,11 @@
|
||||
提示: 使用钱包支付时,订单将直接完成
|
||||
</template>
|
||||
<template v-else-if="createForm.payment_method === 'offline'">
|
||||
提示: 线下支付订单需要手动确认支付
|
||||
{{
|
||||
generatesCollectionBill
|
||||
? '提示: 线下支付订单将生成员工代收款账单,付款凭证可在核销申请中提交'
|
||||
: '提示: 线下支付订单需要手动确认支付'
|
||||
}}
|
||||
</template>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
@@ -213,6 +217,12 @@
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
<div
|
||||
v-if="generatesCollectionBill"
|
||||
style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)"
|
||||
>
|
||||
付款凭证为选填,也可在员工代收款核销申请中提交
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -570,8 +580,8 @@
|
||||
]
|
||||
}
|
||||
|
||||
// 线下支付时,支付凭证为必填
|
||||
if (createForm.payment_method === 'offline') {
|
||||
// 线下支付时,支付凭证为必填;生成员工代收款账单时可在核销申请中提交
|
||||
if (createForm.payment_method === 'offline' && !generatesCollectionBill.value) {
|
||||
baseRules.payment_voucher_key = [
|
||||
{
|
||||
required: true,
|
||||
@@ -641,6 +651,34 @@
|
||||
return `¥${(pkg.suggested_retail_price / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
// 平台账号(超级管理员/平台用户)操作
|
||||
const isPlatformAccount = computed(() => {
|
||||
const userType = Number(userStore.info.user_type)
|
||||
return userType === 1 || userType === 2
|
||||
})
|
||||
|
||||
// 选中套餐的实际收款金额(分)
|
||||
const selectedPackageAmountFen = computed(() => {
|
||||
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === createForm.package_id)
|
||||
if (!selectedPackage) return 0
|
||||
return (
|
||||
selectedPackage.effective_retail_price ??
|
||||
selectedPackage.suggested_retail_price ??
|
||||
selectedPackage.retail_price ??
|
||||
0
|
||||
)
|
||||
})
|
||||
|
||||
// 由平台账号操作、实际收款金额大于 0 且非赠送的线下订单会生成员工代收款账单,
|
||||
// 该场景付款凭证由核销申请环节提供,创建订单时可为空
|
||||
const generatesCollectionBill = computed(
|
||||
() =>
|
||||
createForm.payment_method === 'offline' &&
|
||||
isPlatformAccount.value &&
|
||||
!selectedPackageIsGift.value &&
|
||||
selectedPackageAmountFen.value > 0
|
||||
)
|
||||
|
||||
// IoT卡选择变化时,根据series_id加载套餐列表
|
||||
const handleIotCardChange = (cardId: number | null) => {
|
||||
if (!cardId) {
|
||||
@@ -1276,9 +1314,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
// 线下支付时,支付凭证为必填
|
||||
// 线下支付时,支付凭证为必填;生成员工代收款账单时可在核销申请中提交
|
||||
if (
|
||||
createForm.payment_method === 'offline' &&
|
||||
!generatesCollectionBill.value &&
|
||||
!hasVoucherKeys(createForm.payment_voucher_key)
|
||||
) {
|
||||
ElMessage.error('线下支付必须上传支付凭证')
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
:actionsWidth="160"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -48,13 +48,6 @@
|
||||
destroy-on-close
|
||||
@closed="clearSensitiveForm"
|
||||
>
|
||||
<ElAlert
|
||||
title="支付凭证仅用于本次写入,页面不会回显、缓存或记录凭证内容。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="商户名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入商户名称" />
|
||||
@@ -117,12 +110,12 @@
|
||||
</ElFormItem>
|
||||
<template v-if="showCredentialEditor">
|
||||
<ElDivider content-position="left">写入支付凭证</ElDivider>
|
||||
<ElAlert
|
||||
title="凭证值以密码方式输入,提交后立即清空。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="credential-tip">
|
||||
必填字段:{{ credentialFieldSpec.required.join('、') || '-' }}
|
||||
<template v-if="credentialFieldSpec.optional.length">
|
||||
;可选字段:{{ credentialFieldSpec.optional.join('、') }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="credential-list">
|
||||
<div
|
||||
v-for="(entry, index) in form.credentials"
|
||||
@@ -170,37 +163,12 @@
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElDrawer v-model="detailDrawerVisible" title="支付商户详情" size="560px">
|
||||
<ElDescriptions v-if="detail" :column="1" border>
|
||||
<ElDescriptionsItem label="商户名称">{{ detail.name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">{{
|
||||
getPaymentMethodLabel(detail.payment_method)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="服务商类型">{{
|
||||
getPaymentProviderLabel(detail.provider_type)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="商户标识">{{ detail.merchant_identity }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">{{
|
||||
detail.enabled ? '启用' : '停用'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付凭证">{{
|
||||
Number(detail.credential_version) > 0 ? '已配置' : '未配置'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="凭证版本">{{
|
||||
detail.credential_version || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注">{{ detail.remark || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
@@ -211,8 +179,10 @@
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
PAYMENT_PROVIDER_OPTIONS,
|
||||
buildPaymentCredentials,
|
||||
getPaymentMethodLabel,
|
||||
getPaymentProviderLabel,
|
||||
getPaymentCredentialFieldSpec,
|
||||
type PaymentCredentialEntry,
|
||||
type PaymentCredentials,
|
||||
type PaymentMerchant,
|
||||
@@ -221,6 +191,7 @@
|
||||
type PaymentMerchantProviderType,
|
||||
type PaymentMerchantQueryParams
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantManagement' })
|
||||
@@ -228,8 +199,20 @@
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const router = useRouter()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
const handleNameClick = (row: PaymentMerchant) => {
|
||||
if (!canManage.value) {
|
||||
ElMessage.warning('您没有查看支付商户详情的权限')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: `${RoutesAlias.PaymentMerchantPoolsDetail}/${row.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 列表查询
|
||||
const searchForm = reactive<Record<string, FilterVo>>({
|
||||
page: 1,
|
||||
@@ -273,7 +256,24 @@
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '商户名称', prop: 'name', minWidth: 180 },
|
||||
{
|
||||
label: '商户名称',
|
||||
prop: 'name',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PaymentMerchant) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.name
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
@@ -388,6 +388,7 @@
|
||||
const formRef = ref<FormInstance>()
|
||||
const showCredentialEditor = ref(false)
|
||||
const credentialError = ref('')
|
||||
const credentialTemplateKeys = ref<string[]>([])
|
||||
|
||||
const initialFormState = () => ({
|
||||
id: 0,
|
||||
@@ -409,6 +410,9 @@
|
||||
|
||||
const credentialConfigured = computed(() => Number(form.credential_version) > 0)
|
||||
|
||||
// 当前服务商组合下的凭证必填/可选字段枚举
|
||||
const credentialFieldSpec = computed(() => getPaymentCredentialFieldSpec(form.provider_type))
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入商户名称', trigger: 'blur' },
|
||||
@@ -429,6 +433,7 @@
|
||||
form.credentials = []
|
||||
showCredentialEditor.value = false
|
||||
credentialError.value = ''
|
||||
credentialTemplateKeys.value = []
|
||||
}
|
||||
|
||||
const clearSensitiveForm = () => {
|
||||
@@ -439,25 +444,13 @@
|
||||
}
|
||||
|
||||
const buildCredentialPayload = (): PaymentCredentials | null => {
|
||||
const credentials: Record<string, string> = {}
|
||||
for (const entry of form.credentials) {
|
||||
const key = entry.key.trim()
|
||||
if (!key) {
|
||||
credentialError.value = '请填写凭证字段名'
|
||||
return null
|
||||
}
|
||||
if (!entry.value) {
|
||||
credentialError.value = `请填写凭证字段 ${key} 的值`
|
||||
return null
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(credentials, key)) {
|
||||
credentialError.value = `凭证字段 ${key} 重复`
|
||||
return null
|
||||
}
|
||||
credentials[key] = entry.value
|
||||
}
|
||||
if (form.credentials.length === 0) {
|
||||
credentialError.value = '请至少添加一个凭证字段'
|
||||
const { credentials, error } = buildPaymentCredentials(
|
||||
form.provider_type,
|
||||
form.merchant_identity,
|
||||
form.credentials
|
||||
)
|
||||
if (!credentials) {
|
||||
credentialError.value = error
|
||||
return null
|
||||
}
|
||||
credentialError.value = ''
|
||||
@@ -467,6 +460,7 @@
|
||||
const showCreateDrawer = () => {
|
||||
if (!canManage.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
credentialTemplateKeys.value = []
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
showCredentialEditor.value = true
|
||||
@@ -476,8 +470,16 @@
|
||||
const startCredentialReplacement = () => {
|
||||
if (!canManage.value) return
|
||||
form.credentials = []
|
||||
if (credentialTemplateKeys.value.length > 0) {
|
||||
form.credentials = credentialTemplateKeys.value.map((key) => ({
|
||||
localId: generateLocalId(),
|
||||
key,
|
||||
value: ''
|
||||
}))
|
||||
} else {
|
||||
addCredential()
|
||||
}
|
||||
showCredentialEditor.value = true
|
||||
addCredential()
|
||||
}
|
||||
|
||||
const addCredential = () => {
|
||||
@@ -531,6 +533,7 @@
|
||||
ElMessage.success('支付凭证更新成功')
|
||||
}
|
||||
formDrawerVisible.value = false
|
||||
clearSensitiveForm()
|
||||
await loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('提交支付商户失败:', error)
|
||||
@@ -549,6 +552,7 @@
|
||||
})
|
||||
ElMessage.success('支付商户已更新')
|
||||
formDrawerVisible.value = false
|
||||
clearSensitiveForm()
|
||||
await loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('更新支付商户失败:', error)
|
||||
@@ -557,23 +561,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 详情 / 启停 / 删除
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detail = ref<PaymentMerchant | null>(null)
|
||||
// 启停 / 删除
|
||||
const tableRef = ref()
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载支付商户详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const openEditDrawer = async (id: number) => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
@@ -648,11 +638,6 @@
|
||||
}
|
||||
|
||||
const getActions = (row: PaymentMerchant) => [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'primary' as const,
|
||||
handler: () => showDetail(row.id)
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
@@ -682,7 +667,6 @@
|
||||
onUnmounted(() => {
|
||||
resetSensitiveForm()
|
||||
merchants.value = []
|
||||
detail.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -708,6 +692,14 @@
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.credential-tip {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -202,48 +202,12 @@
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElDrawer v-model="detailDrawerVisible" title="商户池详情" size="640px">
|
||||
<ElDescriptions v-if="detail" :column="1" border>
|
||||
<ElDescriptionsItem label="商户池名称">{{ detail.name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">{{
|
||||
getPaymentMethodLabel(detail.payment_method)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成员数量">{{ detail.member_ids.length }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="轮询策略">{{
|
||||
getPaymentPoolStrategyLabel(detail.strategy)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="detail.strategy === 'amount' || detail.strategy === 'count'"
|
||||
label="统计周期"
|
||||
>
|
||||
{{ getPaymentStatisticCycleLabel(detail.statistic_cycle) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'amount'" label="金额阈值">
|
||||
{{ (Number(detail.threshold_amount) / 100).toFixed(2) }} 元
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'count'" label="笔数阈值">
|
||||
{{ detail.threshold_count }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'time'" label="时间周期">
|
||||
{{ detail.time_period_value }}
|
||||
{{ getPaymentTimePeriodUnitLabel(detail.time_period_unit) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">
|
||||
{{ detail.enabled ? '启用' : '停用' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="路由世代">v{{ detail.routing_epoch }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注">{{ detail.remark || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Delete, Plus, Rank } from '@element-plus/icons-vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
@@ -269,14 +233,25 @@
|
||||
type PaymentPoolStrategy
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPoolManagement' })
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const router = useRouter()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
const handleNameClick = (row: PaymentMerchantPool) => {
|
||||
if (!canManage.value) {
|
||||
ElMessage.warning('您没有查看商户池详情的权限')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({ path: `${RoutesAlias.PaymentMerchantPoolDetail}/${row.id}` })
|
||||
}
|
||||
|
||||
const searchForm = reactive<Record<string, FilterVo>>({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
@@ -301,7 +276,24 @@
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '商户池名称', prop: 'name', minWidth: 180 },
|
||||
{
|
||||
label: '商户池名称',
|
||||
prop: 'name',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PaymentMerchantPool) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.name
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
@@ -445,7 +437,7 @@
|
||||
)
|
||||
|
||||
const getMemberName = (id: number) =>
|
||||
availableMerchants.value.find((m) => m.id === id)?.name || `#${id}`
|
||||
availableMerchants.value.find((m) => m.id === id)?.name || '未知商户'
|
||||
|
||||
// 表单
|
||||
type FormMode = 'create' | 'edit'
|
||||
@@ -475,16 +467,13 @@
|
||||
|
||||
const form = reactive(initialFormState())
|
||||
|
||||
const orderedMemberIds = ref<number[]>([])
|
||||
watch(
|
||||
() => form.member_ids,
|
||||
(val) => {
|
||||
orderedMemberIds.value = [...val]
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
watch(orderedMemberIds, (val) => {
|
||||
form.member_ids = [...val]
|
||||
// 成员顺序以 form.member_ids 为单一数据源:VueDraggable 的 v-model 直接写回该数组,
|
||||
// 避免双向 watch 互相赋值导致的递归更新。
|
||||
const orderedMemberIds = computed<number[]>({
|
||||
get: () => form.member_ids,
|
||||
set: (val) => {
|
||||
form.member_ids = [...val]
|
||||
}
|
||||
})
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
@@ -611,7 +600,6 @@
|
||||
const handlePaymentMethodChange = (value: PaymentMerchantMethod) => {
|
||||
form.payment_method = value
|
||||
form.member_ids = []
|
||||
orderedMemberIds.value = []
|
||||
availableMerchants.value = []
|
||||
memberError.value = ''
|
||||
loadAvailableMerchants(value)
|
||||
@@ -688,7 +676,6 @@
|
||||
const showCreateDrawer = async () => {
|
||||
if (!canManage.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
orderedMemberIds.value = []
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
await loadAvailableMerchants(form.payment_method)
|
||||
@@ -712,14 +699,12 @@
|
||||
time_period_started_at: pool.time_period_started_at || '',
|
||||
remark: pool.remark || ''
|
||||
})
|
||||
orderedMemberIds.value = [...pool.member_ids]
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'edit'
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, initialFormState())
|
||||
orderedMemberIds.value = []
|
||||
memberError.value = ''
|
||||
pendingMemberId.value = undefined
|
||||
formRef.value?.clearValidate()
|
||||
@@ -753,23 +738,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 详情
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detail = ref<PaymentMerchantPool | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantPoolById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEnabled = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
const target = !pool.enabled
|
||||
@@ -795,11 +765,6 @@
|
||||
}
|
||||
|
||||
const getActions = (row: PaymentMerchantPool) => [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'primary' as const,
|
||||
handler: () => showDetail(row.id)
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
@@ -822,9 +787,7 @@
|
||||
|
||||
onUnmounted(() => {
|
||||
pools.value = []
|
||||
detail.value = null
|
||||
availableMerchants.value = []
|
||||
orderedMemberIds.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
297
src/views/settings/payment-merchant-pools/detail.vue
Normal file
297
src/views/settings/payment-merchant-pools/detail.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="payment-merchant-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">{{ pageTitle }}</h2>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElIcon } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailField, DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import type { PaymentMerchantDetail } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getPaymentMethodLabel, getPaymentProviderLabel } from '@/types/api/paymentMerchantPools'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPoolsDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<PaymentMerchantDetail | null>(null)
|
||||
|
||||
const merchantId = computed(() => Number(route.params.id))
|
||||
const pageTitle = computed(() => `支付商户详情 #${merchantId.value}`)
|
||||
|
||||
const formatText = (value: string | number | boolean | undefined | null): string => {
|
||||
if (value === undefined || value === null || value === '') return '-'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const formatBoolean = (value: boolean): string => (value ? '是' : '否')
|
||||
|
||||
const formatCredentialStatus = (value: string | number | boolean | undefined | null): string => {
|
||||
if (value === '已配置') return '已配置'
|
||||
if (value === true || value === 1) return '已配置'
|
||||
return '未配置'
|
||||
}
|
||||
|
||||
const baseSection: DetailSection = {
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '商户ID', prop: 'id' },
|
||||
{ label: '商户名称', prop: 'name' },
|
||||
{
|
||||
label: '支付方式',
|
||||
formatter: (_, data) => getPaymentMethodLabel(data.payment_method)
|
||||
},
|
||||
{
|
||||
label: '服务商类型',
|
||||
formatter: (_, data) => getPaymentProviderLabel(data.provider_type)
|
||||
},
|
||||
{ label: '商户标识', prop: 'merchant_identity' },
|
||||
{
|
||||
label: '启停状态',
|
||||
formatter: (_, data) => (data.enabled ? '启用' : '停用')
|
||||
},
|
||||
{
|
||||
label: '凭证状态',
|
||||
formatter: (_, data) =>
|
||||
Number(data.credential_version) > 0 ? '已配置' : '未配置'
|
||||
},
|
||||
{ label: '凭证版本', prop: 'credential_version' },
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
formatter: (value) => formatText(value),
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
formatter: (value) => formatDateTime(value)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const alipayFields: DetailField[] = [
|
||||
{
|
||||
label: '支付宝AppID',
|
||||
prop: 'credentials.ali_app_id',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '支付过期分钟数',
|
||||
prop: 'credentials.ali_pay_expire_minutes',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '生产环境',
|
||||
prop: 'credentials.ali_production',
|
||||
formatter: (value) => formatBoolean(value)
|
||||
},
|
||||
{
|
||||
label: '异步通知地址',
|
||||
prop: 'credentials.ali_notify_url',
|
||||
formatter: (value) => formatText(value),
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '同步跳转地址',
|
||||
prop: 'credentials.ali_return_url',
|
||||
formatter: (value) => formatText(value),
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '应用私钥',
|
||||
prop: 'credentials.ali_private_key',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
},
|
||||
{
|
||||
label: '支付宝公钥',
|
||||
prop: 'credentials.ali_public_key',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
}
|
||||
]
|
||||
|
||||
const wechatPayFields: DetailField[] = [
|
||||
{
|
||||
label: '微信商户号',
|
||||
prop: 'credentials.wx_mch_id',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '证书序列号',
|
||||
prop: 'credentials.wx_serial_no',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '支付回调地址',
|
||||
prop: 'credentials.wx_notify_url',
|
||||
formatter: (value) => formatText(value),
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: 'APIv2密钥',
|
||||
prop: 'credentials.wx_api_v2_key',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
},
|
||||
{
|
||||
label: 'APIv3密钥',
|
||||
prop: 'credentials.wx_api_v3_key',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
},
|
||||
{
|
||||
label: '支付证书',
|
||||
prop: 'credentials.wx_cert_content',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
},
|
||||
{
|
||||
label: '支付密钥',
|
||||
prop: 'credentials.wx_key_content',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
}
|
||||
]
|
||||
|
||||
const fuiouFields: DetailField[] = [
|
||||
{
|
||||
label: '富友API地址',
|
||||
prop: 'credentials.fy_api_url',
|
||||
formatter: (value) => formatText(value),
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '富友机构号',
|
||||
prop: 'credentials.fy_ins_cd',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '富友商户号',
|
||||
prop: 'credentials.fy_mchnt_cd',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '富友终端号',
|
||||
prop: 'credentials.fy_term_id',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '支付回调地址',
|
||||
prop: 'credentials.fy_notify_url',
|
||||
formatter: (value) => formatText(value)
|
||||
},
|
||||
{
|
||||
label: '富友私钥',
|
||||
prop: 'credentials.fy_private_key',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
},
|
||||
{
|
||||
label: '富友公钥',
|
||||
prop: 'credentials.fy_public_key',
|
||||
formatter: (value) => formatCredentialStatus(value)
|
||||
}
|
||||
]
|
||||
|
||||
const credentialSections = computed<DetailSection[]>(() => {
|
||||
if (!detailData.value) return []
|
||||
|
||||
const providerType = detailData.value.provider_type
|
||||
if (providerType === 'alipay') {
|
||||
return [{ title: '支付宝配置', fields: alipayFields }]
|
||||
}
|
||||
|
||||
if (providerType === 'wechat' || providerType === 'wechat_v2') {
|
||||
return [{ title: '微信支付配置', fields: wechatPayFields }]
|
||||
}
|
||||
|
||||
if (providerType === 'fuiou') {
|
||||
return [{ title: '富友支付配置', fields: fuiouFields }]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const detailSections = computed<DetailSection[]>(() => [baseSection, ...credentialSections.value])
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantDetailById(merchantId.value)
|
||||
if (res.code === 0) {
|
||||
detailData.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载支付商户详情失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-merchant-detail-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
243
src/views/settings/payment-merchant-pools/pool-detail.vue
Normal file
243
src/views/settings/payment-merchant-pools/pool-detail.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="payment-merchant-pool-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">商户池详情</h2>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElIcon } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailField, DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import type {
|
||||
PaymentMerchantMethod,
|
||||
PaymentMerchantPool,
|
||||
PaymentMerchantPoolMemberOption
|
||||
} from '@/types/api'
|
||||
import {
|
||||
getPaymentMethodLabel,
|
||||
getPaymentPoolStrategyLabel,
|
||||
getPaymentStatisticCycleLabel,
|
||||
getPaymentTimePeriodUnitLabel
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPoolDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<PaymentMerchantPool | null>(null)
|
||||
const memberMerchants = ref<PaymentMerchantPoolMemberOption[]>([])
|
||||
|
||||
const poolId = computed(() => Number(route.params.id))
|
||||
|
||||
const loadMemberMerchants = async (paymentMethod: PaymentMerchantMethod) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchants({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
payment_method: paymentMethod
|
||||
})
|
||||
if (res.code === 0) {
|
||||
memberMerchants.value = (res.data?.items || []).map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
payment_method: item.payment_method,
|
||||
enabled: item.enabled
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池成员商户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 成员只展示商户名称,不展示 member_ids 原始 ID
|
||||
const memberNamesText = computed(() => {
|
||||
const memberIds = detailData.value?.member_ids || []
|
||||
if (memberIds.length === 0) return '-'
|
||||
return memberIds
|
||||
.map(
|
||||
(memberId) => memberMerchants.value.find((item) => item.id === memberId)?.name || '未知商户'
|
||||
)
|
||||
.join('、')
|
||||
})
|
||||
|
||||
const formatText = (value: string | number | undefined | null): string => {
|
||||
if (value === undefined || value === null || value === '') return '-'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const baseSection: DetailSection = {
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '商户池名称', prop: 'name' },
|
||||
{
|
||||
label: '支付方式',
|
||||
formatter: (_, data) => getPaymentMethodLabel(data.payment_method)
|
||||
},
|
||||
{
|
||||
label: '启停状态',
|
||||
formatter: (_, data) => (data.enabled ? '启用' : '停用')
|
||||
},
|
||||
{
|
||||
label: '成员数量',
|
||||
formatter: (_, data) => `${(data.member_ids || []).length} 个`
|
||||
},
|
||||
{
|
||||
label: '成员商户',
|
||||
formatter: () => memberNamesText.value,
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
formatter: (value) => formatText(value),
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const rotationFields = computed<DetailField[]>(() => {
|
||||
const data = detailData.value
|
||||
if (!data) return []
|
||||
|
||||
const fields: DetailField[] = [
|
||||
{
|
||||
label: '轮询策略',
|
||||
formatter: (_, row) => getPaymentPoolStrategyLabel(row.strategy)
|
||||
}
|
||||
]
|
||||
|
||||
if (data.strategy === 'amount') {
|
||||
fields.push({
|
||||
label: '统计周期',
|
||||
formatter: (_, row) => getPaymentStatisticCycleLabel(row.statistic_cycle)
|
||||
})
|
||||
fields.push({
|
||||
label: '金额阈值',
|
||||
formatter: (_, row) => `${(Number(row.threshold_amount) / 100).toFixed(2)} 元`
|
||||
})
|
||||
} else if (data.strategy === 'count') {
|
||||
fields.push({
|
||||
label: '统计周期',
|
||||
formatter: (_, row) => getPaymentStatisticCycleLabel(row.statistic_cycle)
|
||||
})
|
||||
fields.push({ label: '笔数阈值', formatter: (_, row) => `${row.threshold_count} 笔` })
|
||||
} else if (data.strategy === 'time') {
|
||||
fields.push({
|
||||
label: '时间周期',
|
||||
formatter: (_, row) =>
|
||||
`${row.time_period_value} ${getPaymentTimePeriodUnitLabel(row.time_period_unit)}`
|
||||
})
|
||||
fields.push({
|
||||
label: '时间起点',
|
||||
formatter: (_, row) => formatDateTime(row.time_period_started_at)
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
})
|
||||
|
||||
const runtimeFields = computed<DetailField[]>(() => {
|
||||
const data = detailData.value
|
||||
if (!data) return []
|
||||
|
||||
const fields: DetailField[] = [
|
||||
{ label: '路由世代', formatter: (_, row) => `v${row.routing_epoch}` }
|
||||
]
|
||||
|
||||
if (data.created_at) {
|
||||
fields.push({ label: '创建时间', formatter: (_, row) => formatDateTime(row.created_at) })
|
||||
}
|
||||
if (data.updated_at) {
|
||||
fields.push({ label: '更新时间', formatter: (_, row) => formatDateTime(row.updated_at) })
|
||||
}
|
||||
|
||||
return fields
|
||||
})
|
||||
|
||||
const detailSections = computed<DetailSection[]>(() => [
|
||||
baseSection,
|
||||
{ title: '轮询配置', fields: rotationFields.value },
|
||||
{ title: '运行状态', fields: runtimeFields.value }
|
||||
])
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantPoolById(poolId.value)
|
||||
if (res.code === 0) {
|
||||
detailData.value = res.data
|
||||
await loadMemberMerchants(res.data.payment_method)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池详情失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-merchant-pool-detail-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -68,7 +68,7 @@
|
||||
v-for="item in currentConfig?.enum_values"
|
||||
:key="item"
|
||||
:value="item"
|
||||
:label="item"
|
||||
:label="formatEnumLabel(item)"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
@@ -105,7 +105,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { SystemConfigService } from '@/api/modules'
|
||||
@@ -131,7 +131,21 @@
|
||||
{ label: 'C 端支付方式配置', value: 'c2b.payment' }
|
||||
]
|
||||
|
||||
const searchForm = reactive<{ module?: SystemConfigModule }>({ module: undefined })
|
||||
const configEnumLabels: Record<string, string> = {
|
||||
wechat_only: '仅微信支付',
|
||||
alipay_only: '仅支付宝支付',
|
||||
both: '同时支持微信与支付宝'
|
||||
}
|
||||
|
||||
const formatEnumLabel = (value: string) => configEnumLabels[value] || value
|
||||
|
||||
const isSystemConfigModule = (value: unknown): value is SystemConfigModule =>
|
||||
moduleOptions.some((item) => item.value === value)
|
||||
|
||||
const queryModule = String(route.query.module || '')
|
||||
const searchForm = reactive<{ module?: SystemConfigModule }>({
|
||||
module: isSystemConfigModule(queryModule) ? queryModule : undefined
|
||||
})
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '配置模块',
|
||||
@@ -252,6 +266,9 @@
|
||||
const methods = parsePaymentMethods(config.value)
|
||||
return methods.length ? methods.map((method) => paymentMethodLabels[method]).join('、') : '-'
|
||||
}
|
||||
if (config.enum_values?.length) {
|
||||
return formatEnumLabel(config.value) || '-'
|
||||
}
|
||||
return config.value || '-'
|
||||
}
|
||||
|
||||
@@ -392,10 +409,16 @@
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
// 菜单跳转可能只变更 query(同一组件实例复用),需要跟随 module 变化重新筛选
|
||||
watch(
|
||||
() => route.query.module,
|
||||
(value) => {
|
||||
const module = String(value || '')
|
||||
searchForm.module = isSystemConfigModule(module) ? module : undefined
|
||||
pagination.page = 1
|
||||
loadConfigs()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-configs-page {
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
>
|
||||
<ElOption label="退款审批" value="refund_approval" />
|
||||
<ElOption label="线下代充值审批" value="offline_recharge_approval" />
|
||||
<ElOption label="员工代收款审批" value="employee_collection_approval" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
Reference in New Issue
Block a user