|
|
@@ -0,0 +1,447 @@
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <section class="panel">
|
|
|
+ <div class="panel-top">
|
|
|
+ <h2 class="title-bar-heading">顺丰月结</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="filter-block">
|
|
|
+ <el-form :model="searchForm" class="filter-form" @submit.prevent>
|
|
|
+ <div class="filter-grid">
|
|
|
+ <el-form-item label="顺丰单号">
|
|
|
+ <el-input v-model="searchForm.waybillNo" placeholder="顺丰单号" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="结算状态">
|
|
|
+ <el-select
|
|
|
+ v-model="searchForm.settleStatus"
|
|
|
+ placeholder="全部"
|
|
|
+ clearable
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option label="未结算" value="wjs" />
|
|
|
+ <el-option label="已结算" value="yjs" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="创建时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="searchForm.createTimeRange"
|
|
|
+ type="daterange"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ <div class="filter-actions-row">
|
|
|
+ <div class="filter-actions-end">
|
|
|
+ <el-button class="btn-ghost" @click="handleReset">
|
|
|
+ <el-icon><RefreshLeft /></el-icon>
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ <el-button class="btn-main" type="primary" @click="handleSearch">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="list-toolbar">
|
|
|
+ <div class="list-toolbar-row">
|
|
|
+ <div class="list-toolbar-selected">
|
|
|
+ 已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
|
|
+ </div>
|
|
|
+ <el-button
|
|
|
+ class="btn-main"
|
|
|
+ type="primary"
|
|
|
+ :disabled="settleDisabled"
|
|
|
+ @click="handleSettle"
|
|
|
+ >
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
+ 标记已结算
|
|
|
+ </el-button>
|
|
|
+ <el-button class="btn-ghost" @click="handleExport">
|
|
|
+ <el-icon><Download /></el-icon>
|
|
|
+ 导出
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="list-body list-wrap">
|
|
|
+ <el-table
|
|
|
+ class="list-table"
|
|
|
+ :data="tableRows"
|
|
|
+ v-loading="loading"
|
|
|
+ stripe
|
|
|
+ row-key="id"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
|
|
+ <el-table-column
|
|
|
+ prop="waybillNo"
|
|
|
+ label="顺丰单号"
|
|
|
+ min-width="160"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ show-overflow-tooltip
|
|
|
+ >
|
|
|
+ <template #default="{ row }">{{ row.waybillNo || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ prop="createByName"
|
|
|
+ label="创建人"
|
|
|
+ min-width="100"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ show-overflow-tooltip
|
|
|
+ >
|
|
|
+ <template #default="{ row }">{{ row.createByName || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ prop="createTime"
|
|
|
+ label="创建时间"
|
|
|
+ min-width="170"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">{{ row.createTime || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结算状态" min-width="110" align="center" header-align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="row.settleStatus === 'yjs'" type="success" size="small">已结算</el-tag>
|
|
|
+ <el-tag v-else-if="row.settleStatus === 'wjs'" type="info" size="small"
|
|
|
+ >未结算</el-tag
|
|
|
+ >
|
|
|
+ <span v-else class="muted">—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ prop="freightAmountLabel"
|
|
|
+ label="运费"
|
|
|
+ min-width="110"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">{{ row.freightAmountLabel || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ prop="settleTime"
|
|
|
+ label="结算时间"
|
|
|
+ min-width="170"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">{{ row.settleTime || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ label="操作"
|
|
|
+ min-width="160"
|
|
|
+ fixed="right"
|
|
|
+ align="center"
|
|
|
+ header-align="center"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button
|
|
|
+ v-if="row.settleStatus === 'yjs' && row.freightAmountLabel === '月结'"
|
|
|
+ class="btn-op-item"
|
|
|
+ @click="openFill(row)"
|
|
|
+ >
|
|
|
+ <el-icon><EditPen /></el-icon>
|
|
|
+ 补填运费
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div class="pagination list-pager-bar">
|
|
|
+ <el-pagination
|
|
|
+ :current-page="pagination.page"
|
|
|
+ :page-size="pagination.pageSize"
|
|
|
+ :total="pagination.total"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ @size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
|
|
+ @current-change="(val) => ((pagination.page = val), loadData())"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ v-model="settleVisible"
|
|
|
+ title="标记已结算"
|
|
|
+ width="560px"
|
|
|
+ class="dialog-form-layout"
|
|
|
+ align-center
|
|
|
+ append-to-body
|
|
|
+ modal-class="protocol-dialog-mask"
|
|
|
+ destroy-on-close
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ >
|
|
|
+ <el-form label-position="top" @submit.prevent>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 运费<span class="form-label-tip">不填则保持当前展示:月结或原金额</span>
|
|
|
+ </template>
|
|
|
+ <el-input v-model="settleAmount" placeholder="选填,0 也是有效金额" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button class="btn-ghost" @click="settleVisible = false">
|
|
|
+ <el-icon><Close /></el-icon>
|
|
|
+ 取消
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ class="btn-main"
|
|
|
+ type="primary"
|
|
|
+ :loading="settleSubmitting"
|
|
|
+ @click="confirmSettle"
|
|
|
+ >
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ v-model="fillVisible"
|
|
|
+ title="补填运费"
|
|
|
+ width="560px"
|
|
|
+ class="dialog-form-layout"
|
|
|
+ align-center
|
|
|
+ append-to-body
|
|
|
+ modal-class="protocol-dialog-mask"
|
|
|
+ destroy-on-close
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ >
|
|
|
+ <el-form label-position="top" @submit.prevent>
|
|
|
+ <el-form-item label="运费">
|
|
|
+ <el-input v-model="fillAmount" placeholder="请填写运费,0 也是有效金额" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button class="btn-ghost" @click="fillVisible = false">
|
|
|
+ <el-icon><Close /></el-icon>
|
|
|
+ 取消
|
|
|
+ </el-button>
|
|
|
+ <el-button class="btn-main" type="primary" :loading="fillSubmitting" @click="confirmFill">
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import { Check, Close, Download, EditPen, RefreshLeft, Search } from '@element-plus/icons-vue'
|
|
|
+ import {
|
|
|
+ fetchSfExpressSettlementExport,
|
|
|
+ fetchSfExpressSettlements,
|
|
|
+ fillSfExpressSettlementAmount,
|
|
|
+ settleSfExpressSettlements,
|
|
|
+ type SfExpressSettlement
|
|
|
+ } from '@/api/sf-express-settlement'
|
|
|
+ import { buildSettlePayload } from '@/utils/sf-express-freight'
|
|
|
+ import { saveSfExpressSettlementWorkbook } from '@/utils/sf-express-settlement-export'
|
|
|
+
|
|
|
+ defineOptions({ name: 'CellStorageSfExpressSettlement' })
|
|
|
+
|
|
|
+ const loading = ref(false)
|
|
|
+ const tableRows = ref<SfExpressSettlement[]>([])
|
|
|
+ const selectedRows = ref<SfExpressSettlement[]>([])
|
|
|
+ const searchForm = reactive({
|
|
|
+ waybillNo: '',
|
|
|
+ settleStatus: '',
|
|
|
+ createTimeRange: null as [string, string] | null
|
|
|
+ })
|
|
|
+ const pagination = reactive({
|
|
|
+ page: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ total: 0
|
|
|
+ })
|
|
|
+
|
|
|
+ const settleVisible = ref(false)
|
|
|
+ const settleAmount = ref('')
|
|
|
+ const settleIds = ref<string[]>([])
|
|
|
+ const settleSubmitting = ref(false)
|
|
|
+ const fillVisible = ref(false)
|
|
|
+ const fillAmount = ref('')
|
|
|
+ const fillId = ref('')
|
|
|
+ const fillSubmitting = ref(false)
|
|
|
+
|
|
|
+ const settleDisabled = computed(
|
|
|
+ () =>
|
|
|
+ selectedRows.value.length === 0 ||
|
|
|
+ selectedRows.value.some((row) => row.settleStatus === 'yjs')
|
|
|
+ )
|
|
|
+
|
|
|
+ function currentFilters() {
|
|
|
+ const [from, to] = searchForm.createTimeRange || []
|
|
|
+ const waybillNo = searchForm.waybillNo.trim()
|
|
|
+ return {
|
|
|
+ waybillNo: waybillNo || undefined,
|
|
|
+ settleStatus: searchForm.settleStatus || undefined,
|
|
|
+ createTimeFrom: from || undefined,
|
|
|
+ createTimeTo: to || undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function unwrapBody(res: unknown) {
|
|
|
+ const record = res as { data?: { list?: unknown }; list?: unknown; code?: unknown }
|
|
|
+ if (record?.data?.list) return record.data
|
|
|
+ return record?.list ? record : record?.data || record
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseNonNegativeAmount(raw: unknown): 'blank' | 'invalid' | number {
|
|
|
+ const text = String(raw ?? '').trim()
|
|
|
+ if (!text) return 'blank'
|
|
|
+ const value = Number(text)
|
|
|
+ if (!Number.isFinite(value) || value < 0) return 'invalid'
|
|
|
+ return value
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await fetchSfExpressSettlements({
|
|
|
+ page: pagination.page,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ ...currentFilters()
|
|
|
+ })
|
|
|
+ const data = unwrapBody(res) as { list?: SfExpressSettlement[]; total?: number }
|
|
|
+ tableRows.value = Array.isArray(data?.list) ? data.list : []
|
|
|
+ pagination.total = Number(data?.total || 0)
|
|
|
+ selectedRows.value = []
|
|
|
+ } catch {
|
|
|
+ tableRows.value = []
|
|
|
+ pagination.total = 0
|
|
|
+ selectedRows.value = []
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleSearch() {
|
|
|
+ pagination.page = 1
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleReset() {
|
|
|
+ searchForm.waybillNo = ''
|
|
|
+ searchForm.settleStatus = ''
|
|
|
+ searchForm.createTimeRange = null
|
|
|
+ handleSearch()
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleSelectionChange(rows: SfExpressSettlement[]) {
|
|
|
+ selectedRows.value = rows
|
|
|
+ }
|
|
|
+
|
|
|
+ async function submitSettle(payload: { ids: string[]; freightAmount?: number }) {
|
|
|
+ settleSubmitting.value = true
|
|
|
+ try {
|
|
|
+ await settleSfExpressSettlements(payload)
|
|
|
+ settleVisible.value = false
|
|
|
+ await loadData()
|
|
|
+ } catch {
|
|
|
+ // 拦截器已提示失败原因
|
|
|
+ } finally {
|
|
|
+ settleSubmitting.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleSettle() {
|
|
|
+ if (settleDisabled.value) return
|
|
|
+ const ids = selectedRows.value.map((row) => row.id).filter(Boolean)
|
|
|
+ if (!ids.length) return
|
|
|
+ if (ids.length > 1) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('本次仅更新结算状态和结算时间。', '标记已结算', {
|
|
|
+ type: 'warning',
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消'
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await submitSettle(buildSettlePayload(ids, ''))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ settleIds.value = ids
|
|
|
+ settleAmount.value = ''
|
|
|
+ settleVisible.value = true
|
|
|
+ }
|
|
|
+
|
|
|
+ async function confirmSettle() {
|
|
|
+ const parsed = parseNonNegativeAmount(settleAmount.value)
|
|
|
+ if (parsed === 'invalid') {
|
|
|
+ ElMessage.warning('请填写运费')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const text = parsed === 'blank' ? '' : String(parsed)
|
|
|
+ await submitSettle(buildSettlePayload(settleIds.value, text))
|
|
|
+ }
|
|
|
+
|
|
|
+ function openFill(row: SfExpressSettlement) {
|
|
|
+ if (row.settleStatus !== 'yjs' || row.freightAmountLabel !== '月结') return
|
|
|
+ fillId.value = row.id
|
|
|
+ fillAmount.value = ''
|
|
|
+ fillVisible.value = true
|
|
|
+ }
|
|
|
+
|
|
|
+ async function confirmFill() {
|
|
|
+ const parsed = parseNonNegativeAmount(fillAmount.value)
|
|
|
+ if (parsed === 'blank' || parsed === 'invalid') {
|
|
|
+ ElMessage.warning('请填写运费')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ fillSubmitting.value = true
|
|
|
+ try {
|
|
|
+ await fillSfExpressSettlementAmount(fillId.value, parsed)
|
|
|
+ fillVisible.value = false
|
|
|
+ await loadData()
|
|
|
+ } catch {
|
|
|
+ // 拦截器已提示失败原因
|
|
|
+ } finally {
|
|
|
+ fillSubmitting.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleExport() {
|
|
|
+ try {
|
|
|
+ const res = await fetchSfExpressSettlementExport(currentFilters())
|
|
|
+ const code = (res as { code?: unknown })?.code
|
|
|
+ if (code != null && Number(code) !== 200) {
|
|
|
+ ElMessage.warning('没有可导出的数据')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const body = (res as { data?: { list?: unknown } })?.data?.list
|
|
|
+ ? (res as { data?: { list?: SfExpressSettlement[] } }).data
|
|
|
+ : (res as { list?: SfExpressSettlement[] })
|
|
|
+ const list = body?.list || []
|
|
|
+ if (!list.length) {
|
|
|
+ ElMessage.warning('没有可导出的数据')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await saveSfExpressSettlementWorkbook(list as Array<Record<string, string>>)
|
|
|
+ } catch (error: any) {
|
|
|
+ const message = String(error?.message || '')
|
|
|
+ const code = error?.code
|
|
|
+ if (message.includes('没有可导出的数据') || (code != null && Number(code) !== 200)) {
|
|
|
+ ElMessage.warning('没有可导出的数据')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ loadData()
|
|
|
+ })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ .muted {
|
|
|
+ color: var(--mint-text-muted, #64748b);
|
|
|
+ }
|
|
|
+</style>
|