538 lines
16 KiB
Vue
538 lines
16 KiB
Vue
<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: 'asset_identifier' },
|
|
{ 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: 'asset_identifier',
|
|
label: '资产标识',
|
|
minWidth: 200,
|
|
showOverflowTooltip: true,
|
|
formatter: (row: EmployeeCollectionBill) =>
|
|
h(
|
|
'span',
|
|
{
|
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
|
onClick: () => handleViewDetail(row)
|
|
},
|
|
row.customer_snapshot?.asset_identifier || '-'
|
|
)
|
|
},
|
|
{
|
|
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.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>
|