Files
2025-09-27 22:57:59 +08:00

172 lines
6.6 KiB
Vue

<template>
<view class="report">
<view class="header">销售报表</view>
<view class="toolbar">
<picker mode="date" :value="startDate" @change="onStartChange"><view class="date">{{ startDate }}</view></picker>
<text style="margin: 0 8rpx;"></text>
<picker mode="date" :value="endDate" @change="onEndChange"><view class="date">{{ endDate }}</view></picker>
</view>
<view class="tabs">
<view class="tab" :class="{active: dim==='customer'}" @click="setDimension('customer')">按客户</view>
<view class="tab" :class="{active: dim==='product'}" @click="setDimension('product')">按货品</view>
</view>
<view class="summary" v-if="summaryItems.length">
<view class="summary-item" v-for="(item, ix) in summaryItems" :key="ix">
<text class="label">{{ item.label }}</text>
<text class="value">{{ item.value }}</text>
</view>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="error" class="empty">{{ error }}</view>
<view v-else-if="!rows.length" class="empty">暂无统计数据</view>
<view v-else>
<view v-for="(row, idx) in rows" :key="idx" class="card">
<view class="row-head">
<view class="row-title">
<view class="title">{{ row.name }}</view>
<view class="subtitle" v-if="showProductSpec(row)">{{ row.spec }}</view>
</view>
</view>
<view class="row-body">
<view class="metric" v-for="(metric, mIdx) in rowMetrics(row)" :key="mIdx">
<text class="metric-label">{{ metric.label }}</text>
<text class="metric-value">{{ metric.value }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
function formatDate(d) {
const y = d.getFullYear()
const m = String(d.getMonth()+1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
export default {
data() {
const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth(), 1)
return {
startDate: formatDate(start),
endDate: formatDate(now),
dim: 'customer',
rows: [],
summary: { salesAmount: 0, costAmount: 0, profit: 0, profitRate: 0, itemCount: 0 },
loading: false,
error: ''
}
},
onLoad(query) {
try {
const d = query && query.dim
if (d === 'product' || d === 'customer') this.dim = d
} catch(e){}
this.refresh()
},
computed: {
profitRateText() {
const rate = Number(this.summary?.profitRate || 0)
return rate.toFixed(2) + '%'
},
summaryItems() {
if (!this.rows.length) return []
return [
{ label: '销售额', value: `${this.fmt(this.summary.salesAmount)}` },
{ label: '成本', value: `${this.fmt(this.summary.costAmount)}` },
{ label: '利润', value: `${this.fmt(this.summary.profit)}` },
{ label: '利润率', value: this.profitRateText }
]
}
},
methods: {
fmt(n) { return Number(n || 0).toFixed(2) },
showProductSpec(row) { return this.dim === 'product' && row && row.spec },
rowMetrics(row) {
if (!row) return []
return [
{ label: '销售额', value: `${this.fmt(row.salesAmount)}` },
{ label: '成本', value: `${this.fmt(row.costAmount)}` },
{ label: '利润', value: `${this.fmt(row.profit)}` },
{ label: '利润率', value: `${Number(row.profitRate || 0).toFixed(2)}%` }
]
},
setDimension(d) {
if (d !== 'customer' && d !== 'product') return
if (this.dim === d) return
this.dim = d
this.refresh()
},
onStartChange(e) { this.startDate = e.detail.value; this.refresh() },
onEndChange(e) { this.endDate = e.detail.value; this.refresh() },
async refresh() {
this.loading = true
this.error = ''
try {
const resp = await get('/api/report/sales', {
dimension: this.dim,
startDate: this.startDate,
endDate: this.endDate
})
const items = Array.isArray(resp?.items) ? resp.items : []
this.rows = items.map(it => ({
name: it?.name || (this.dim === 'product' ? '未命名商品' : '未指定客户'),
spec: it?.spec || '',
salesAmount: Number(it?.salesAmount || 0),
costAmount: Number(it?.costAmount || 0),
profit: Number(it?.profit || 0),
profitRate: Number(it?.profitRate || 0)
}))
this.summary = {
salesAmount: Number(resp?.summary?.salesAmount || 0),
costAmount: Number(resp?.summary?.costAmount || 0),
profit: Number(resp?.summary?.profit || 0),
profitRate: Number(resp?.summary?.profitRate || 0),
itemCount: Number(resp?.summary?.itemCount || this.rows.length)
}
} catch (e) {
this.error = (e && e.message) || '报表加载失败'
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss">
.report { padding: 24rpx 20rpx 36rpx; display:flex; flex-direction:column; gap: 18rpx; }
.header { font-size: 34rpx; font-weight: 700; color:#1f2a44; padding-left: 8rpx; }
.toolbar { display:flex; align-items:center; justify-content:center; gap: 12rpx; background: #f7f9fc; border-radius: 16rpx; padding: 18rpx; }
.date { min-width: 200rpx; padding: 12rpx 18rpx; border-radius: 12rpx; background: #fff; border: 1rpx solid rgba(91,107,139,0.16); text-align:center; color:#32445b; }
.tabs { display:flex; gap: 12rpx; justify-content:center; }
.tab { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f0f4ff; color:#5b6b8b; transition: all .2s ease; }
.tab.active { background: rgba(76,141,255,0.18); color:#3467d6; box-shadow: inset 0 0 0 2rpx rgba(76,141,255,0.45); }
.summary { display:grid; grid-template-columns: repeat(auto-fill, minmax(240rpx,1fr)); gap: 12rpx; }
.summary-item { background: #f7f9fc; border-radius: 16rpx; padding: 20rpx; display:flex; flex-direction:column; gap: 10rpx; }
.summary-item .label { font-size: 24rpx; color:#6e7a96; }
.summary-item .value { font-size: 32rpx; font-weight:700; color:#1f2a44; }
.card { background:#fff; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 8rpx 20rpx rgba(31,42,68,0.08); display:flex; flex-direction:column; gap: 14rpx; }
.row-head { display:flex; justify-content:space-between; align-items:flex-start; }
.row-title { display:flex; flex-direction:column; gap: 6rpx; }
.title { font-size: 30rpx; font-weight:700; color:#1f2a44; }
.subtitle { font-size: 24rpx; color:#6e7a96; }
.row-body { display:flex; flex-wrap:wrap; gap: 12rpx 24rpx; }
.metric { display:flex; gap: 8rpx; align-items:center; background:#f4f6fb; border-radius: 12rpx; padding: 10rpx 16rpx; }
.metric-label { font-size: 24rpx; color:#6e7a96; }
.metric-value { font-size: 28rpx; color:#1f2a44; font-weight:600; }
.empty { text-align:center; padding: 80rpx 0; color:#9aa4be; font-size: 26rpx; }
.loading { text-align:center; padding: 40rpx 0; color:#5b6b8b; font-size: 24rpx; }
</style>