Files
PartsInquiry/frontend/pages/index/index.vue
2025-09-24 20:35:15 +08:00

366 lines
14 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="home">
<!-- 公告栏置顶显示可点击查看详情 -->
<view class="notice">
<view class="notice-left">公告</view>
<view v-if="loadingNotices" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">加载中...</view>
<view v-else-if="noticeError" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d;">{{ noticeError }}</view>
<view v-else-if="!notices.length" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">暂无公告</view>
<swiper v-else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
<swiper-item v-for="(n, idx) in notices" :key="idx">
<view class="notice-item" @click="onNoticeTap(n)">
<text class="notice-text">{{ n.text }}</text>
<text v-if="n.tag" class="notice-tag">{{ n.tag }}</text>
</view>
</swiper-item>
</swiper>
</view>
<!-- 顶部统计卡片 -->
<view class="hero">
<view class="hero-top">
<text class="brand">五金配件管家</text>
<view class="cta">
<text class="cta-text">咨询</text>
</view>
</view>
<view class="kpi kpi-grid">
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.todaySales" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">今日销售额</text>
<text class="kpi-value">{{ kpi.todaySales }}</text>
</view>
</view>
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.monthSales" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">本月销售额</text>
<text class="kpi-value">{{ kpi.monthSales }}</text>
</view>
</view>
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.monthProfit" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">本月利润</text>
<text class="kpi-value">{{ kpi.monthProfit }}</text>
</view>
</view>
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.stockCount" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">库存商品数量</text>
<text class="kpi-value">{{ kpi.stockCount }}</text>
</view>
</view>
</view>
</view>
<!-- 公告栏已上移到顶部 -->
<!-- 分割标题产品与功能 -->
<view class="section-title">
<text class="section-text">常用功能</text>
</view>
<!-- 功能九宫格玻璃容器 + 圆角方形图标 -->
<view class="grid-wrap">
<view class="feature-grid">
<view class="feature-card" v-for="item in features" :key="item.key" @click="onFeatureTap(item)">
<view class="fc-icon">
<image v-if="item.img" :src="item.img" class="fc-img" mode="aspectFit" @error="onIconError(item)"></image>
<text v-else-if="item.emoji" class="fc-emoji">{{ item.emoji }}</text>
<view v-else class="fc-placeholder"></view>
</view>
<view class="fc-title">{{ item.title }}</view>
</view>
</view>
</view>
<!-- 底部操作条改为原生 tabBar移除自定义栏 -->
</view>
</template>
<script>
import { get } from '../../common/http.js'
import { ROUTES } from '../../common/constants.js'
import { KPI_ICONS as KPI_ICON_MAP } from '../../common/config.js'
export default {
data() {
return {
KPI_ICONS: KPI_ICON_MAP,
kpi: { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' },
activeTab: 'home',
notices: [],
loadingNotices: false,
noticeError: '',
features: [
{ key: 'product', title: '货品', img: '/static/icons/product.png', emoji: '📦' },
{ key: 'customer', title: '客户', img: '/static/icons/customer.png', emoji: '👥' },
{ key: 'sale', title: '销售', img: '/static/icons/sale.png', emoji: '💰' },
{ key: 'account', title: '账户', img: '/static/icons/account.png', emoji: '💳' },
{ key: 'supplier', title: '供应商', img: '/static/icons/supplier.png', emoji: '🚚' },
{ key: 'purchase', title: '进货', img: '/static/icons/purchase.png', emoji: '🛒' },
{ key: 'otherPay', title: '其他支出', img: '/static/icons/other-pay.png', emoji: '💸' },
{ key: 'vip', title: 'VIP会员', img: '/static/icons/vip.png', emoji: '👑' },
{ key: 'report', title: '报表', img: '/static/icons/report.png', emoji: '📊' },
{ key: 'more', title: '更多', img: '/static/icons/more.png', emoji: '⋯' }
]
}
},
onLoad() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.kpi = { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' }
this.notices = []
uni.showToast({ title: '请登录使用该功能', icon: 'none' })
return
}
this.fetchMetrics()
this.fetchNotices()
},
methods: {
async fetchMetrics() {
try {
const d = await get('/api/dashboard/overview')
const toNum = v => (typeof v === 'number' ? v : Number(v || 0))
this.kpi = {
...this.kpi,
todaySales: toNum(d && d.todaySalesAmount).toFixed(2),
monthSales: toNum(d && d.monthSalesAmount).toFixed(2),
monthProfit: toNum(d && d.monthGrossProfit).toFixed(2),
stockCount: String((d && d.stockTotalQuantity) != null ? d.stockTotalQuantity : 0)
}
} catch (e) {
// 忽略错误,保留默认值
}
},
async fetchNotices() {
this.loadingNotices = true
this.noticeError = ''
try {
const list = await get('/api/notices')
this.notices = Array.isArray(list) ? list.map(n => ({
text: n.content || n.title || '',
tag: n.tag || ''
})) : []
} catch (e) {
this.noticeError = (e && e.message) || '公告加载失败'
} finally {
this.loadingNotices = false
}
},
onFeatureTap(item) {
if (item.key === 'product') {
uni.switchTab({ url: '/pages/product/list' })
return
}
if (item.key === 'sale') {
// 进入开单页并预选“销售-出货”
try { uni.setStorageSync('ORDER_DEFAULT_PARAMS', { biz: 'sale', type: 'out' }) } catch(e) {}
uni.switchTab({ url: '/pages/order/create' })
return
}
if (item.key === 'customer') {
uni.navigateTo({ url: '/pages/customer/select' })
return
}
if (item.key === 'account') {
// 进入账户模块(先使用账户选择页,已对接后端 /api/accounts
uni.navigateTo({ url: '/pages/account/select' })
return
}
if (item.key === 'supplier') {
uni.navigateTo({ url: '/pages/supplier/select' })
return
}
if (item.key === 'purchase') {
// 进入开单页并预选“进货-进货”
try { uni.setStorageSync('ORDER_DEFAULT_PARAMS', { biz: 'purchase', type: 'in' }) } catch(e) {}
uni.switchTab({ url: '/pages/order/create' })
return
}
if (item.key === 'report') {
// 报表非 tab 页,使用 navigateTo 进入
uni.navigateTo({ url: ROUTES.report })
return
}
if (item.key === 'otherPay') {
// 进入开单页并预选“其他支出”
try { uni.setStorageSync('ORDER_DEFAULT_PARAMS', { biz: 'expense' }) } catch(e) {}
uni.switchTab({ url: '/pages/order/create' })
return
}
uni.showToast({ title: item.title + '(开发中)', icon: 'none' })
},
goProduct() { uni.switchTab({ url: '/pages/product/list' }) },
onCreateOrder() { uni.switchTab({ url: '/pages/order/create' }) },
goDetail() {
try { console.log('[index] goDetail → /pages/detail/index') } catch(e){}
uni.switchTab({ url: '/pages/detail/index' })
},
goMe() { uni.switchTab({ url: '/pages/my/index' }) },
onNoticeTap(n) {
uni.showModal({
title: '广告',
content: n && (n.text || n.title || n.content) || '',
showCancel: false
})
},
onIconError(item) {
item.img = ''
}
}
}
</script>
<style lang="scss">
.home {
padding-bottom: 140rpx;
position: relative;
/* 渐变背景:顶部淡蓝过渡到白色 */
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
min-height: 100vh;
}
/* 首页横幅(移除) */
/* 公告栏 */
.notice {
margin: 0 24rpx 24rpx;
padding: 20rpx 22rpx;
border-radius: 20rpx;
background: #ffffff;
border: 2rpx solid $uni-border-color;
display: flex;
align-items: center;
gap: 16rpx;
}
.notice-left {
flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center;
min-width: 100rpx; height: 52rpx;
padding: 0 16rpx;
border-radius: 999rpx;
background: $uni-color-primary;
color: #fff;
font-size: 28rpx;
font-weight: 800;
}
.notice-swiper { height: 72rpx; flex: 1; }
.notice-item { display: flex; align-items: center; gap: 12rpx; min-height: 72rpx; }
.notice-text { color: $uni-text-color; font-size: 28rpx; line-height: 36rpx; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.notice-tag { color: $uni-color-primary; font-size: 22rpx; padding: 4rpx 10rpx; border-radius: 999rpx; background: rgba(76,141,255,0.18); }
/* 分割标题 */
.section-title { display: flex; align-items: center; gap: 16rpx; padding: 10rpx 28rpx 0; }
.section-title::before { content: ''; display: block; width: 8rpx; height: 28rpx; border-radius: 8rpx; background: $uni-color-primary; }
.section-text { color: $uni-text-color; font-size: 30rpx; font-weight: 700; letter-spacing: 1rpx; }
/* 顶部英雄区:浅色玻璃卡片,带金色描边与柔和阴影 */
.hero {
margin: 16rpx 20rpx;
padding: 18rpx;
border-radius: 20rpx;
background: #ffffff;
border: 2rpx solid $uni-border-color;
box-shadow: none;
color: $uni-text-color;
}
.hero-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.hero-sub { display:flex; gap: 12rpx; margin-bottom: 22rpx; flex-wrap: wrap; }
.chip { padding: 8rpx 16rpx; border-radius: 999rpx; background: rgba(76,141,255,0.10); color: $uni-color-primary; font-size: 24rpx; font-weight: 700; border: 2rpx solid rgba(76,141,255,0.25); }
.brand {
font-size: 36rpx;
font-weight: 700;
letter-spacing: 1rpx;
color: $uni-color-primary;
}
.cta {
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: $uni-color-primary;
border: 2rpx solid $uni-color-primary;
box-shadow: none;
}
.cta-text { color: #fff; font-size: 30rpx; font-weight: 700; letter-spacing: 1rpx; }
/* KPI 卡片化布局2×2 */
.kpi { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; }
.kpi-item { text-align: center; background:#ffffff; border: 2rpx solid $uni-border-color; border-radius: 16rpx; padding: 16rpx 8rpx; }
/* KPI 卡片(更扁平,降低高度) */
.kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 16rpx; }
.kpi-card { display:flex; align-items:center; gap: 12rpx; text-align:left; padding: 12rpx 14rpx; border-radius: 12rpx; background:#fff; border:2rpx solid #eef2f6; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); }
.kpi-icon { width: 44rpx; height: 44rpx; opacity: .9; }
.kpi-content { display:flex; flex-direction:column; }
.kpi-label { color:#6b778c; font-weight:700; font-size: 24rpx; line-height: 30rpx; }
.kpi-value { color:#4C8DFF; font-size: 36rpx; line-height: 40rpx; margin-top: 0; font-weight: 800; }
/* 常用功能:胶囊+阴影卡片样式的图标栅格(旧风格保留以防回退) */
.grid { grid-row-gap: 36rpx; grid-column-gap: 26rpx; padding: 32rpx 24rpx 28rpx; }
.grid-item { position: relative; }
.icon-squircle { width: 140rpx; height: 140rpx; border-radius: 28rpx; background: #fff; border: 2rpx solid $uni-border-color; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.06); }
.grid-chip { margin-top: 12rpx; padding: 6rpx 14rpx; border-radius: 999rpx; background: rgba(76,141,255,0.12); color: $uni-color-primary; font-size: 26rpx; font-weight: 700; }
/* 功能容器:更轻的留白 */
.grid-wrap {
margin: 8rpx 12rpx 24rpx;
padding: 8rpx 8rpx 0;
border-radius: 20rpx;
background: transparent;
border: 0;
}
/* 功能卡片宫格:方形竖排,图标在上文字在下(与截图一致) */
.feature-grid { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14rpx; padding: 8rpx 8rpx 18rpx; }
.feature-card { height: 164rpx; background:#fff; border:2rpx solid #eef2f6; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); padding: 12rpx; display:flex; flex-direction: column; align-items:center; justify-content:center; }
.fc-icon { width: 86rpx; height: 86rpx; border-radius: 18rpx; background: #f7faff; border:2rpx solid #e8eef8; display:flex; align-items:center; justify-content:center; }
.fc-img { width: 56rpx; height: 56rpx; opacity: .95; }
.fc-emoji { font-size: 48rpx; }
.fc-placeholder { width: 56rpx; height: 56rpx; border-radius: 12rpx; background: $uni-bg-color-hover; border: 2rpx solid #e8eef8; }
.fc-title { margin-top: 10rpx; font-size: 26rpx; font-weight: 700; color: $uni-text-color; }
/* 底部操作条:浅色半透明 + 金色主按钮 */
.bottom-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: space-around;
padding: 14rpx 18rpx calc(env(safe-area-inset-bottom) + 14rpx);
background: rgba(255,255,255,0.85);
box-shadow: 0 -6rpx 18rpx rgba(0,0,0,0.08);
backdrop-filter: blur(10rpx);
z-index: 9999;
}
.tab { flex: 1; text-align: center; color: #8a7535; font-size: 26rpx; }
.tab.active { color: #B4880F; }
.tab.primary {
flex: 0 0 auto;
min-width: 180rpx;
margin: 0 18rpx;
padding: 18rpx 32rpx;
background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%);
color: #493c1b;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 800;
box-shadow: 0 10rpx 22rpx rgba(215,167,46,0.25), 0 0 0 2rpx rgba(255,255,255,0.70) inset;
}
</style>