Files
cma-management/frontend/src/views/Receivables.vue
T
wecom-fullstack f51fc72ac7
ci/woodpecker/push/main Pipeline failed
style(page-title): B 全站规范——页面内同名标题清零 + 增量副标题弱化 + 标题 emoji 换 Lucide
- 39+ 页删除与布局标题同名/基本同名的页面内 h1-h4(同屏三标题→两处)
- 增量副标题弱化为 14px #64748B 小字:收付款计划/战略回顾/地图版本/会计科目打标
- 标题 emoji 清零,保留标题处改 Lucide 内联 SVG(bar-chart-3/video/scale/clipboard-list/alert-circle/map/history)
- KPIListView 增加 hideTitle(仅展示),客户/学习看板隐藏组件内重复页头
- 不动 MainLayout、不改接口/业务逻辑、不碰 MapCanvas.vue
2026-10-18 19:33:11 +08:00

196 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="receivables-page">
<div class="page-header">
<div class="header-right">
<el-select v-model="owner" size="small" clearable placeholder="按负责人筛选" style="width:140px" @change="loadData">
<el-option v-for="o in owners" :key="o" :value="o" :label="o" />
</el-select>
<el-select v-model="status" size="small" clearable placeholder="按状态筛选" style="width:130px" @change="loadData">
<el-option value="pending" label="待收款" />
<el-option value="overdue" label="已逾期" />
<el-option value="completed" label="已回款" />
</el-select>
<el-button type="primary" size="small" @click="loadData">刷新</el-button>
</div>
</div>
<!-- 汇总卡片 -->
<el-row :gutter="16">
<el-col :span="6"><el-card shadow="never" class="stat-card red"><div class="stat-val">¥{{ (summary.pending_balance || 0).toFixed(2) }}万</div><div class="stat-label">待收余额</div></el-card></el-col>
<el-col :span="6"><el-card shadow="never" class="stat-card orange"><div class="stat-val">¥{{ (summary.overdue_amount || 0).toFixed(2) }}万</div><div class="stat-label">逾期金额 ({{ summary.overdue_count || 0 }}笔)</div></el-card></el-col>
<el-col :span="6"><el-card shadow="never" class="stat-card green"><div class="stat-val">¥{{ (summary.completed_amount || 0).toFixed(2) }}万</div><div class="stat-label">已回款</div></el-card></el-col>
<el-col :span="6"><el-card shadow="never" class="stat-card blue"><div class="stat-val">{{ summary.completed_count || 0 }}</div><div class="stat-label">已完成笔数</div></el-card></el-col>
</el-row>
<!-- 账龄分布 -->
<el-row style="margin-top:16px">
<el-col :span="24">
<el-card shadow="never">
<template #header>账龄分布(万元)</template>
<div ref="agingRef" style="height:220px"></div>
</el-card>
</el-col>
</el-row>
<!-- 应收明细 -->
<el-row style="margin-top:16px">
<el-col :span="24">
<el-card shadow="never">
<template #header>应收明细</template>
<el-table :data="items" size="small" v-loading="loading">
<el-table-column label="客户/单位" min-width="140">
<template #default="{ row }">{{ row.counterparty || (row.description || '').slice(0, 20) || '—' }}</template>
</el-table-column>
<el-table-column label="负责人" width="90">
<template #default="{ row }">{{ row.owner || '未分配' }}</template>
</el-table-column>
<el-table-column prop="amount" label="金额(万)" width="90">
<template #default="{ row }"><span style="font-weight:600">¥{{ row.amount }}</span></template>
</el-table-column>
<el-table-column prop="settled_amount" label="已回款(万)" width="100" />
<el-table-column prop="plan_date" label="到期日" width="100" />
<el-table-column prop="aging_bucket" label="账龄" width="100">
<template #default="{ row }">
<el-tag :type="row.overdue ? 'danger' : (row.aging_bucket === '未到期' ? 'success' : 'warning')" size="small">
{{ row.aging_bucket }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 'completed' ? 'success' : (row.overdue ? 'danger' : 'info')" size="small">
{{ row.status === 'completed' ? '已回款' : row.overdue ? '已逾期' : '待收款' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button v-if="row.status !== 'completed'" type="primary" size="small" @click="openPayment(row)">登记回款</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 回款登记弹窗 -->
<MyDialog v-model="paymentVisible" title="登记回款" :width="420">
<el-form label-width="90px">
<el-form-item label="客户">{{ currentRow?.name }}</el-form-item>
<el-form-item label="应收金额">¥{{ currentRow?.amount }}万</el-form-item>
<el-form-item label="回款金额">
<el-input-number v-model="paymentAmount" :min="0" :max="currentRow?.amount || 0" :precision="2" style="width:180px" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="paymentNote" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="paymentVisible = false">取消</el-button>
<el-button type="primary" :loading="paying" @click="submitPayment">确认回款</el-button>
</template>
</MyDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import api from '@/api'
import { ElMessage } from 'element-plus'
import MyDialog from '../components/MyDialog.vue'
const items = ref<any[]>([])
const owners = ref<string[]>([])
const summary = ref<any>({})
const owner = ref('')
const status = ref('')
const loading = ref(false)
const paymentVisible = ref(false)
const currentRow = ref<any>(null)
const paymentAmount = ref(0)
const paymentNote = ref('')
const paying = ref(false)
const agingRef = ref<HTMLElement>()
async function loadData() {
loading.value = true
try {
const params: any = {}
if (owner.value) params.owner = owner.value
if (status.value) params.status = status.value
const r: any = await api.get('/cash/receivables', { params })
const d = r || {}
summary.value = d.summary || {}
owners.value = d.owners || []
const rawItems = d.data || d.items || d.receivables || []
items.value = rawItems
await nextTick()
renderAging()
} catch (e) {
ElMessage.error('加载应收数据失败')
} finally {
loading.value = false
}
}
function renderAging() {
if (!agingRef.value) return
const chart = echarts.init(agingRef.value)
const aging = summary.value.aging || {}
chart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: Object.keys(aging) },
yAxis: { type: 'value', name: '万元' },
series: [{
type: 'bar',
data: Object.values(aging),
itemStyle: { color: '#409EFF', borderRadius: [4, 4, 0, 0] },
label: { show: true, position: 'top' },
}],
})
}
function openPayment(row: any) {
currentRow.value = row
paymentAmount.value = row.amount || 0
paymentNote.value = ''
paymentVisible.value = true
}
async function submitPayment() {
if (!currentRow.value || paymentAmount.value <= 0) return
paying.value = true
try {
await api.post(`/cash/receivables/${currentRow.value.id}/payment`, {
amount: paymentAmount.value,
note: paymentNote.value,
})
ElMessage.success('回款登记成功')
paymentVisible.value = false
loadData()
} catch (e) {
ElMessage.error('回款登记失败')
} finally {
paying.value = false
}
}
onMounted(loadData)
</script>
<style scoped>
.receivables-page { max-width: 1400px; margin: 0 auto; padding: 16px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.header-right { display: flex; gap: 8px; }
.stat-card { text-align: center; }
.stat-val { font-size: 24px; font-weight: 700; }
.stat-label { font-size: 12px; color: #888; margin-top: 4px; }
.red .stat-val { color: #f56c6c; }
.orange .stat-val { color: #e6a23c; }
.green .stat-val { color: #67c23a; }
.blue .stat-val { color: #409EFF; }
</style>