更新
This commit is contained in:
1 parent
5718a00e31
commit
1850ac640e
226 files changed
+37772
-12071
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user