446 lines
18 KiB
Vue
446 lines
18 KiB
Vue
<template>
|
||
<view class="home">
|
||
<!-- 顶部统计卡片 -->
|
||
<view class="hero">
|
||
<view class="hero-top">
|
||
<text class="brand">五金配件管家</text>
|
||
<view class="cta" @click="onConsultTap" hover-class="cta-active" hover-stay-time="80">
|
||
<text class="cta-text">{{ consultLabel }}</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 v-if="consultDialogVisible" class="dialog-mask" @touchmove.stop.prevent @click.stop>
|
||
<view class="dialog">
|
||
<view class="dialog-title">咨询</view>
|
||
<textarea class="dialog-textarea" v-model="consultMessage" placeholder="请输入咨询内容..." maxlength="500"></textarea>
|
||
<view class="dialog-actions">
|
||
<view class="btn" @click="closeConsultDialog">取消</view>
|
||
<view class="btn primary" @click="submitConsult">提交</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 公告栏:放在常用功能上方、KPI 下方 -->
|
||
<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="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, post, put } 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: '',
|
||
consultLabel: '咨询',
|
||
consultDialogVisible: false,
|
||
consultMessage: '',
|
||
features: [
|
||
{ key: 'product', title: '货品', img: '/static/icons/product.png', emoji: '📦' },
|
||
{ key: 'customer', title: '客户', img: '/static/icons/webwxgetmsgimg.png', emoji: '👥' },
|
||
{ key: 'sale', title: '销售', img: '/static/icons/webwxgetmsgimg.jpg', emoji: '💰' },
|
||
{ key: 'account', title: '账户', img: '/static/icons/icons8-profile-50.png', emoji: '💳' },
|
||
{ key: 'supplier', title: '供应商', img: '/static/icons/icons8-supplier-50.png', emoji: '🚚' },
|
||
{ key: 'purchase', title: '进货', img: '/static/icons/icons8-dollar-ethereum-exchange-50.png', emoji: '🛒' },
|
||
{ key: 'otherPay', title: '其他支出', img: '/static/icons/icons8-expenditure-64.png', emoji: '💸' },
|
||
{ key: 'vip', title: 'VIP会员', img: '/static/icons/icons8-vip-48.png', emoji: '👑' },
|
||
{ key: 'report', title: '报表', img: '/static/icons/icons8-graph-report-50.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()
|
||
this.fetchLatestConsult()
|
||
},
|
||
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 fetchLatestConsult() {
|
||
try {
|
||
const d = await get('/api/consults')
|
||
if (d && d.replied) this.consultLabel = '已回复'
|
||
else this.consultLabel = '咨询'
|
||
this._latestConsult = d
|
||
} catch(e) { this.consultLabel = '咨询' }
|
||
},
|
||
onConsultTap() {
|
||
if (this.consultLabel === '已回复' && this._latestConsult && this._latestConsult.id) {
|
||
// 展示最近一次咨询与管理员最新回复
|
||
const msg = (this._latestConsult.latestReply ? (this._latestConsult.latestReply) : (this._latestConsult.message || ''))
|
||
uni.showModal({ title: '咨询回复', content: msg || '暂无内容', showCancel: false, success: async (res) => {
|
||
if (!res || res.confirm !== true) return
|
||
try {
|
||
const r = await put(`/api/consults/${this._latestConsult.id}/ack`, {})
|
||
this.consultLabel = '咨询'
|
||
this._latestConsult = null
|
||
setTimeout(() => this.fetchLatestConsult(), 200)
|
||
} catch(e) {
|
||
try { uni.showToast({ title: (e && e.message) || '已读同步失败', icon: 'none' }) } catch(_){}
|
||
}
|
||
}})
|
||
return
|
||
}
|
||
this.consultMessage = ''
|
||
this.consultDialogVisible = true
|
||
},
|
||
closeConsultDialog() { this.consultDialogVisible = false },
|
||
async submitConsult() {
|
||
const text = String(this.consultMessage || '').trim()
|
||
if (!text) { uni.showToast({ title: '请输入咨询内容', icon: 'none' }); return }
|
||
try {
|
||
await post('/api/consults', { message: text })
|
||
this.consultDialogVisible = false
|
||
uni.showToast({ title: '已提交', icon: 'success' })
|
||
setTimeout(() => this.fetchLatestConsult(), 300)
|
||
} catch (e) {
|
||
uni.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
|
||
}
|
||
},
|
||
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">
|
||
page {
|
||
height: 100%;
|
||
overflow: hidden;
|
||
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
|
||
}
|
||
.home {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx);
|
||
position: relative;
|
||
/* 渐变背景:顶部淡蓝过渡到白色 */
|
||
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
|
||
/* 首页横幅(移除) */
|
||
|
||
|
||
/* 公告栏 */
|
||
.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; flex: 0 0 auto; }
|
||
.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 18rpx 12rpx;
|
||
border-radius: 20rpx;
|
||
background: #ffffff;
|
||
border: 2rpx solid $uni-border-color;
|
||
box-shadow: none;
|
||
color: $uni-text-color;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.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; }
|
||
|
||
/* 简易弹层样式 */
|
||
.dialog-mask { position: fixed; left:0; right:0; top:0; bottom:0; background: rgba(0,0,0,0.45); display:flex; align-items:center; justify-content:center; z-index: 10000; }
|
||
.dialog { width: 82vw; background: #fff; border-radius: 16rpx; padding: 20rpx; border: 2rpx solid #eef2f6; }
|
||
.dialog-title { font-size: 32rpx; font-weight: 800; color: $uni-text-color; margin-bottom: 16rpx; }
|
||
.dialog-textarea { width: 100%; min-height: 180rpx; border: 2rpx solid #e8eef8; border-radius: 12rpx; padding: 12rpx; box-sizing: border-box; }
|
||
.dialog-actions { display:flex; justify-content:flex-end; gap: 18rpx; margin-top: 16rpx; }
|
||
.btn { padding: 10rpx 22rpx; border-radius: 999rpx; background: #f3f6fb; color: #334155; border: 2rpx solid #e2e8f0; font-weight: 700; }
|
||
.btn.primary { background: #4C8DFF; color: #fff; border-color: #4C8DFF; }
|
||
|
||
/* KPI 卡片化布局:横向铺满 */
|
||
.kpi { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12rpx; }
|
||
.kpi-item { text-align: center; background:#ffffff; border: 2rpx solid $uni-border-color; border-radius: 16rpx; padding: 16rpx 8rpx; }
|
||
|
||
/* KPI 卡片(更扁平,降低高度) */
|
||
.kpi-grid { gap: 12rpx; }
|
||
.kpi-card { display:flex; flex-direction:column; align-items:flex-start; justify-content:center; gap: 8rpx; text-align:left; padding: 12rpx 14rpx; border-radius: 14rpx; background:#fff; border:2rpx solid #eef2f6; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); min-height: 120rpx; }
|
||
.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: 34rpx; line-height: 38rpx; 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 {
|
||
flex: 1 1 auto;
|
||
display:flex;
|
||
align-items:stretch;
|
||
justify-content:center;
|
||
margin: 16rpx 20rpx 28rpx;
|
||
padding: 32rpx 30rpx;
|
||
border-radius: 26rpx;
|
||
background: rgba(255,255,255,0.96);
|
||
border: 2rpx solid #edf2f9;
|
||
box-shadow: 0 12rpx 28rpx rgba(32,75,143,0.10);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 功能卡片宫格:方形竖排,图标在上文字在下(与截图一致) */
|
||
.feature-grid { flex: 1 1 auto; width: 100%; height: 100%; display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 32rpx 28rpx; align-content: space-evenly; justify-items: center; }
|
||
.feature-card { width: 168rpx; height: 176rpx; background:#fff; border:2rpx solid #eef2f6; border-radius: 20rpx; box-shadow: 0 10rpx 24rpx rgba(0,0,0,0.05); padding: 18rpx 16rpx; display:flex; flex-direction: column; align-items:center; justify-content:center; }
|
||
.fc-icon { width: 78rpx; height: 78rpx; border-radius: 18rpx; background: #f7faff; display:flex; align-items:center; justify-content:center; }
|
||
.fc-img { width: 54rpx; height: 54rpx; 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: 12rpx; font-size: 28rpx; font-weight: 700; color: $uni-text-color; letter-spacing: 1rpx; }
|
||
|
||
/* 底部操作条:浅色半透明 + 金色主按钮 */
|
||
.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>
|