fix: 代理
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user