fix: 代理

This commit is contained in:
luo
2026-09-12 11:27:50 +08:00
parent d3d257cdf8
commit 2308d82d0f
44 changed files with 5470 additions and 200 deletions

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