Files
yunzerwebsiteallinone/uniapp/pages/erp/dashboard/index.vue
T
2026-10-19 01:13:17 +08:00

497 lines
11 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>
<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>