This commit is contained in:
hero920103 committed 2026-10-19 01:13:17 +08:00
1 parent 5718a00e31
commit 1850ac640e
226 files changed
+37772 -12071

No files matched your search

+496
View File
@@ -0,0 +1,496 @@
<template>
<view class="page">
<view v-if="loading && !dash" class="state-box">
<text class="state-text">加载中...</text>
</view>
<block v-else-if="dash">
<!-- 本月经营 -->
<view class="hero-card">
<view class="hero-main">
<text class="hero-label">本月销售金额</text>
<text class="hero-amount">¥{{ moneyText(month.sales_amount) }}</text>
<text class="hero-sub">
销售 {{ month.sales_count || 0 }} 单 · 采购 {{ month.purchase_count || 0 }} 单 · 退货 ¥{{ moneyText(month.return_amount) }}
</text>
</view>
<view class="hero-side">
<view class="hero-line">
<text class="hero-line-label">本月毛利</text>
<text class="hero-line-value">{{ moneyText(month.profit) }}</text>
</view>
<view class="hero-line">
<text class="hero-line-label">毛利率</text>
<text class="hero-line-value">{{ moneyText(month.profit_rate) }}%</text>
</view>
<view class="hero-line">
<text class="hero-line-label">采购金额</text>
<text class="hero-line-value">{{ moneyText(month.purchase_amount) }}</text>
</view>
</view>
</view>
<!-- 指标格子 -->
<view class="kpi-grid">
<view class="kpi-cell" @tap="go('/pages/erp/document/index?doc_type=sales_out')">
<text class="kpi-value">{{ moneyText(today.sales_amount) }}</text>
<text class="kpi-label">今日销售({{ today.sales_count || 0 }} 单)</text>
</view>
<view class="kpi-cell" @tap="go('/pages/erp/stock/index')">
<text class="kpi-value">{{ moneyText(stock.value) }}</text>
<text class="kpi-label">库存货值({{ qtyText(stock.qty) }} 件)</text>
</view>
<view class="kpi-cell" @tap="go('/pages/erp/finance/bill?bill_type=1')">
<text class="kpi-value danger">{{ moneyText(bill.receivable) }}</text>
<text class="kpi-label">未收应收</text>
</view>
<view class="kpi-cell" @tap="go('/pages/erp/finance/bill?bill_type=2')">
<text class="kpi-value">{{ moneyText(bill.payable) }}</text>
<text class="kpi-label">未付应付</text>
</view>
<view class="kpi-cell" @tap="go('/pages/erp/document/index?status=2')">
<text class="kpi-value primary">{{ todo.approval_pending || 0 }}</text>
<text class="kpi-label">待我审批</text>
</view>
<view class="kpi-cell" @tap="go('/pages/erp/document/index?status=0')">
<text class="kpi-value">{{ todo.draft_docs || 0 }}</text>
<text class="kpi-label">草稿单据</text>
</view>
</view>
<!-- 库存预警 -->
<view class="card warn-card">
<view class="block-head">
<text class="block-title">库存预警</text>
<view class="block-more" @tap="go('/pages/erp/stock/warning')">
<text>查看全部</text>
<FaIcon name="chevron-right" color="#94a3b8" :size="14" />
</view>
</view>
<view class="warn-row">
<view class="warn-item" @tap="go('/pages/erp/stock/warning?tab=low')">
<text class="warn-value warning">{{ warning.low || 0 }}</text>
<text class="warn-label">低于安全库存</text>
</view>
<view class="warn-item" @tap="go('/pages/erp/stock/warning?tab=out')">
<text class="warn-value danger">{{ warning.out || 0 }}</text>
<text class="warn-label">缺货</text>
</view>
<view class="warn-item" @tap="go('/pages/erp/stock/warning?tab=over')">
<text class="warn-value primary">{{ warning.over || 0 }}</text>
<text class="warn-label">库存积压</text>
</view>
<view class="warn-item" @tap="go('/pages/erp/stock/batch?expire_state=expiring')">
<text class="warn-value warning">{{ warning.expiring || 0 }}</text>
<text class="warn-label">效期临近</text>
</view>
</view>
</view>
<!-- 近 6 个月购销趋势 -->
<view class="card trend-card">
<view class="block-head">
<text class="block-title">近 6 个月购销趋势</text>
<view class="legend">
<view class="legend-item">
<view class="legend-dot buy"></view>
<text>采购</text>
</view>
<view class="legend-item">
<view class="legend-dot sale"></view>
<text>销售</text>
</view>
</view>
</view>
<view v-for="row in trend" :key="row.month" class="trend-row">
<text class="trend-month">{{ row.month }}</text>
<view class="trend-bars">
<view class="bar-track">
<view class="bar buy" :style="{ width: barWidth(row.buy) }"></view>
</view>
<view class="bar-track">
<view class="bar sale" :style="{ width: barWidth(row.sale) }"></view>
</view>
</view>
<text class="trend-value">¥{{ moneyText(row.sale) }}</text>
</view>
</view>
<!-- 常用入口 -->
<view class="card entry-card">
<view class="block-head">
<text class="block-title">常用功能</text>
</view>
<view class="entry-grid">
<view v-for="item in entries" :key="item.route" class="entry-item" @tap="go(item.route)">
<view class="entry-icon">
<FaIcon :name="item.icon" color="#2563eb" :size="18" />
</view>
<text class="entry-label">{{ item.label }}</text>
</view>
</view>
</view>
</block>
<view v-else class="state-box">
<u-empty mode="network" text="看板加载失败" />
<view class="retry" @tap="load">重新加载</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import FaIcon from '@/components/FaIcon.vue'
import { fetchErpDashboard, moneyText, qtyText } from '@/api/erp.js'
const dash = ref(null)
const loading = ref(false)
const month = computed(() => (dash.value || {}).month || {})
const today = computed(() => (dash.value || {}).today || {})
const stock = computed(() => (dash.value || {}).stock || {})
const bill = computed(() => (dash.value || {}).partnerBill || {})
const todo = computed(() => (dash.value || {}).todo || {})
const warning = computed(() => (dash.value || {}).warning || {})
const trend = computed(() => (dash.value || {}).trend || [])
const maxTrend = computed(() => {
let max = 0
trend.value.forEach(row => {
max = Math.max(max, Number(row.buy || 0), Number(row.sale || 0))
})
return max || 1
})
const entries = [
{ label: '开单', icon: 'plus', route: '/pages/erp/document/edit' },
{ label: '单据', icon: 'clipboard-list', route: '/pages/erp/document/index' },
{ label: '商品', icon: 'boxes-stacked', route: '/pages/erp/product/index' },
{ label: '库存', icon: 'warehouse', route: '/pages/erp/stock/index' },
{ label: '客商', icon: 'users', route: '/pages/erp/partner/index' },
{ label: '收付款', icon: 'wallet', route: '/pages/erp/finance/fund' },
{ label: '报表', icon: 'chart-column', route: '/pages/erp/report/index' },
{ label: '流水', icon: 'arrow-right-arrow-left', route: '/pages/erp/stock/ledger' }
]
onShow(() => {
load()
})
function barWidth(value) {
const pct = (Number(value || 0) / maxTrend.value) * 100
return `${Math.max(pct, 2).toFixed(1)}%`
}
async function load() {
loading.value = true
try {
dash.value = await fetchErpDashboard()
} catch (e) {
dash.value = null
} finally {
loading.value = false
}
}
function go(route) {
uni.navigateTo({ url: route })
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding: 24rpx $page-padding-x 60rpx;
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 160rpx 0;
}
.state-text {
font-size: 28rpx;
color: $color-text-muted;
}
.retry {
margin-top: 24rpx;
padding: 12rpx 36rpx;
border-radius: $radius-full;
background: $color-card;
font-size: 26rpx;
color: $color-primary;
box-shadow: $shadow-card;
}
.card {
@include card;
padding: 28rpx;
margin-bottom: 24rpx;
}
.block-head {
@include block-head;
}
.block-title {
@include section-title;
}
.block-more {
display: flex;
align-items: center;
gap: 4rpx;
font-size: 24rpx;
color: $color-text-muted;
}
.hero-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding: 32rpx 28rpx;
margin-bottom: 24rpx;
border-radius: $radius-lg;
background: $hero-gradient;
color: #ffffff;
box-shadow: $shadow-card;
}
.hero-label {
display: block;
font-size: 24rpx;
opacity: 0.85;
}
.hero-amount {
display: block;
margin-top: 8rpx;
font-size: 48rpx;
font-weight: 600;
}
.hero-sub {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
opacity: 0.85;
}
.hero-side {
flex-shrink: 0;
text-align: right;
}
.hero-line {
margin-bottom: 10rpx;
}
.hero-line-label {
display: block;
font-size: 21rpx;
opacity: 0.8;
}
.hero-line-value {
display: block;
font-size: 28rpx;
font-weight: 600;
}
.kpi-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 24rpx;
}
.kpi-cell {
width: calc((100% - 40rpx) / 3);
box-sizing: border-box;
@include card;
padding: 24rpx 16rpx;
text-align: center;
&:active {
opacity: 0.85;
}
}
.kpi-value {
display: block;
font-size: 30rpx;
font-weight: 600;
color: $color-text;
&.danger {
color: $color-danger;
}
&.warning {
color: $color-warning;
}
&.primary {
color: $color-primary;
}
}
.kpi-label {
display: block;
margin-top: 8rpx;
font-size: 21rpx;
color: $color-text-muted;
line-height: 1.4;
}
.warn-row {
display: flex;
gap: 12rpx;
}
.warn-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
border-radius: $radius-md;
background: $color-bg-page;
}
.warn-value {
display: block;
font-size: 34rpx;
font-weight: 600;
&.warning {
color: $color-warning;
}
&.danger {
color: $color-danger;
}
&.primary {
color: $color-primary;
}
}
.warn-label {
display: block;
margin-top: 6rpx;
font-size: 21rpx;
color: $color-text-muted;
}
.legend {
display: flex;
gap: 20rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.legend-dot {
width: 16rpx;
height: 16rpx;
border-radius: 4rpx;
&.buy {
background: #93c5fd;
}
&.sale {
background: $color-primary;
}
}
.trend-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 14rpx 0;
}
.trend-month {
width: 110rpx;
font-size: 22rpx;
color: $color-text-muted;
flex-shrink: 0;
}
.trend-bars {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.bar-track {
height: 14rpx;
border-radius: $radius-full;
background: $color-bg-page;
overflow: hidden;
}
.bar {
height: 100%;
border-radius: $radius-full;
&.buy {
background: #93c5fd;
}
&.sale {
background: $color-primary;
}
}
.trend-value {
width: 150rpx;
text-align: right;
font-size: 22rpx;
color: $color-text-secondary;
flex-shrink: 0;
}
.entry-grid {
display: flex;
flex-wrap: wrap;
}
.entry-item {
width: 25%;
padding: 18rpx 0;
text-align: center;
}
.entry-icon {
width: 84rpx;
height: 84rpx;
margin: 0 auto 10rpx;
border-radius: $radius-md;
background: $color-primary-bg;
display: flex;
align-items: center;
justify-content: center;
}
.entry-label {
font-size: 24rpx;
color: $color-text-secondary;
}
</style>