Files
guilixu-admin/src/views/shop/dashboard/index.vue
赵忠林 7c73f9f821 feat(subscription): 优化订阅及支付二维码生成流程
- 统一调用 subscribe 接口创建订阅,确保后端设置价格类型
- 免费应用订阅成功后直接激活,提示订阅成功并刷新订阅信息
- 付费应用订阅成功后调用 pay 接口生成支付小程序码
- 支持显示支付二维码,展示付费信息,弹出支付模态框
- 增加支付状态轮询逻辑以跟踪支付进度
- 优化错误处理,打印错误日志并提示用户重试
2026-07-15 14:25:14 +08:00

1037 lines
34 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>
<div class="shop-dashboard">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="welcome-left flex flex-row gap-2 items-center">
<a-avatar
v-if="shopLogoUrl"
:src="shopLogoUrl"
:size="64"
shape="square"
class="welcome-logo"
/>
<div>
<h2 class="welcome-title">{{ userStore.info?.tenantName }}</h2>
<p class="welcome-sub">欢迎回来管理员今日数据已更新</p>
</div>
</div>
<div class="welcome-right">
<div class="welcome-actions">
<div class="order-notify-bar">
<span class="order-notify-label">新订单提醒</span>
<a-switch v-model:checked="notifyEnabled" @change="onNotifyToggle" size="small" />
<a-tooltip title="开启后每60秒自动检测新订单检测到后播放叮声并语音播报">
<InfoCircleOutlined class="order-notify-info" />
</a-tooltip>
<a-button size="small" :disabled="!notifyEnabled" @click="onTestNotify">测试</a-button>
</div>
<a-space>
<a-tag color="green" style="font-size:13px;padding:4px 12px">{{ siteStore.statusText || '正常' }}</a-tag>
<a-button size="small" @click="refreshStatistics" :loading="loading">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</a-space>
</div>
</div>
</div>
<!-- 核心数据统计 -->
<a-row :gutter="[16, 16]">
<a-col :xs="12" :sm="12" :md="6" v-for="stat in coreStats" :key="stat.label">
<div class="stat-block" :class="stat.color" @click="navigateTo(stat.to)" :style="{ cursor: stat.to ? 'pointer' : 'default' }">
<div class="stat-block-header">
<span class="stat-block-icon">{{ stat.icon }}</span>
<span class="stat-block-label">{{ stat.label }}</span>
</div>
<div class="stat-block-value">
<template v-if="loading">
<a-skeleton-input :active="true" size="small" style="width:60px" />
</template>
<template v-else>{{ stat.value }}</template>
</div>
<div class="stat-block-desc">{{ stat.desc }}</div>
</div>
</a-col>
</a-row>
<!-- 待办事项 + 快捷操作 -->
<a-row :gutter="[16, 16]">
<!-- 待处理事项 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🔔 待处理事项</span>
</div>
<div class="todo-list">
<div
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
>
<div class="todo-dot" :class="todo.dotColor"></div>
<div class="todo-content">
<span class="todo-label">{{ todo.label }}</span>
<a-tag :color="todo.tagColor" style="margin-left:8px">
<template v-if="loading">...</template>
<template v-else>{{ todo.value }}</template>
</a-tag>
</div>
<RightOutlined class="todo-arrow" />
</div>
<div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">
🎉 暂无待处理事项一切正常
</div>
</div>
</div>
</a-col>
<!-- 快速入口 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快速入口</span>
</div>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
<div class="quick-label">{{ item.label }}</div>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- 今日数据趋势 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">📊 今日数据概况</span>
</div>
<div class="today-stats">
<div class="today-stat">
<div class="today-stat-icon">💰</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ formatMoney(todayStats.salesAmount) }}</div>
<div class="today-stat-label">今日销售额</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">📦</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.orderCount || 0 }}</div>
<div class="today-stat-label">今日订单数</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">👥</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.newUsers || 0 }}</div>
<div class="today-stat-label">新增用户</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">🎁</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.couponUsed || 0 }}</div>
<div class="today-stat-label">使用优惠券</div>
</div>
</div>
</div>
</div>
<!-- 系统基本信息 -->
<a-row :gutter="[16, 16]" style="margin-top: 16px">
<a-col :span="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📋 基本信息</span>
</div>
<div style="padding: 16px 18px;">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="系统名称">
{{ systemInfo.name }}
</a-descriptions-item>
<a-descriptions-item label="版本号">
{{ systemInfo.version }}
</a-descriptions-item>
<a-descriptions-item label="运行状态">
{{ siteStore.statusText || '正常' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ tenantStore.company?.createTime || '-' }}
</a-descriptions-item>
<a-descriptions-item label="到期时间">
<template v-if="subscriptionLoading">
<a-skeleton-input :active="true" size="small" style="width: 140px" />
</template>
<template v-else-if="currentSubscription && currentSubscription.status === 'active' && currentSubscription.expireTime">
<span>{{ formatDateTime(currentSubscription.expireTime) }}</span>
<a-tag color="green" style="margin-left: 8px">已激活</a-tag>
<span v-if="daysToExpire !== null && daysToExpire >= 0 && daysToExpire <= 7" class="expire-warn">
{{ daysToExpire }}天后到期
</span>
<span v-else-if="daysToExpire !== null && daysToExpire < 0" class="expire-warn">
已过期
</span>
</template>
<template v-else-if="currentSubscription && currentSubscription.status === 'pending'">
<span class="text-orange">待支付 ¥{{ Number(currentSubscription.payPrice || 0).toFixed(2) }}</span>
<a-button type="link" size="small" @click="repayPending" style="padding: 0 0 0 8px">去支付</a-button>
</template>
<template v-else>
<span class="text-muted">未订阅</span>
<a-button type="link" size="small" @click="onSubscribe" style="padding: 0 0 0 8px">立即订阅</a-button>
</template>
</a-descriptions-item>
<a-descriptions-item label="系统运行">
{{ runDays }}
</a-descriptions-item>
</a-descriptions>
</div>
</div>
</a-col>
<!-- 快捷操作 -->
<a-col :span="12">
<div class="panel" style="min-height: 353px">
<div class="panel-header">
<span class="panel-title"> 快捷操作</span>
</div>
<div style="padding: 16px 18px;">
<a-space direction="vertical" style="width: 100%">
<a-button
type="primary"
block
@click="navigateTo('/shop/shopOrder')"
>
<ShoppingCartOutlined />
订单管理
</a-button>
<a-button block @click="navigateTo('/shop/shopGoods')">
<ShopOutlined />
商品管理
</a-button>
<a-button block @click="navigateTo('/shop/shopGoodsCategory')">
<AppstoreOutlined />
商品分类
</a-button>
<a-button block @click="navigateTo('/market/coupon')">
<GiftOutlined />
优惠券管理
</a-button>
<a-button block @click="navigateTo('/system/user')">
<TeamOutlined />
会员管理
</a-button>
<a-button block @click="navigateTo('/shop/shopSetting')">
<SettingOutlined />
商城设置
</a-button>
<a-button block @click="handleClearCache">
<ClearOutlined />
清除缓存
</a-button>
</a-space>
</div>
</div>
</a-col>
</a-row>
<!-- 订阅 Modal选择订阅周期 -->
<a-modal
v-model:open="subscribeModalVisible"
title="订阅应用"
:confirm-loading="subscribing"
ok-text="确认订阅"
cancel-text="取消"
:mask-closable="false"
@ok="confirmSubscribe"
>
<div class="subscribe-modal-body">
<div class="subscribe-product-name">
{{ currentProduct?.productName || '应用订阅' }}
</div>
<a-alert
v-if="isFreeProduct"
type="success"
message="该应用为免费应用,订阅后立即激活"
show-icon
style="margin-bottom: 12px"
/>
<div v-else class="period-options">
<div
class="period-option"
:class="{ active: selectedPeriod === 'month' }"
@click="selectedPeriod = 'month'"
>
<div class="period-option-label">月付</div>
<div class="period-option-price">¥{{ monthPrice.toFixed(2) }}/</div>
</div>
<div
class="period-option"
:class="{ active: selectedPeriod === 'year' }"
@click="selectedPeriod = 'year'"
>
<div class="period-option-label">年付</div>
<div class="period-option-price">¥{{ yearPrice.toFixed(2) }}/</div>
<div class="period-option-tag">更划算</div>
</div>
</div>
</div>
</a-modal>
<!-- 支付 Modal微信扫码支付 -->
<a-modal
v-model:open="payModalVisible"
title="微信扫码支付"
:footer="null"
:mask-closable="false"
@cancel="onPayModalClose"
>
<div class="pay-modal-body">
<div class="pay-product-name">{{ payProductName }}</div>
<div class="pay-amount">
<span class="pay-amount-label">支付金额</span>
<span class="pay-amount-value">¥{{ payPrice.toFixed(2) }}</span>
</div>
<div class="pay-qrcode-wrap">
<img
v-if="payQrcode"
:src="payQrcode"
alt="支付二维码"
class="pay-qrcode-img"
/>
<a-spin v-else />
</div>
<div class="pay-tip">
<QrcodeOutlined />
请使用微信扫描上方二维码完成支付
</div>
<div class="pay-order-no">订单号{{ paySubscriptionNo }}</div>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
import {
ReloadOutlined,
RightOutlined,
ShoppingCartOutlined,
ShopOutlined,
AppstoreOutlined,
GiftOutlined,
TeamOutlined,
SettingOutlined,
ClearOutlined,
InfoCircleOutlined,
QrcodeOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue/es';
import { useSiteStore } from '@/store/modules/site';
import { useStatisticsStore } from '@/store/modules/statistics';
import { useUserStore } from '@/store/modules/user';
import { useTenantStore } from '@/store/modules/tenant';
import { useAppSubscriptionStore } from '@/store/modules/appSubscription';
import { pageShopOrder } from '@/api/shop/shopOrder';
import { getShopSettingCategoryValues } from '@/api/shop/shopSetting';
import { getCompressedImageUrl } from '@/utils/image';
import { storeToRefs } from 'pinia';
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
import { pageProducts } from '@/api/app/appProduct';
import type { AppProduct } from '@/api/app/appProduct/model';
import type { AppSubscription } from '@/api/app/appSubscription/model';
import dayjs from 'dayjs';
// 使用状态管理
const siteStore = useSiteStore();
const statisticsStore = useStatisticsStore();
const userStore = useUserStore();
const tenantStore = useTenantStore();
const subscriptionStore = useAppSubscriptionStore();
const router = useRouter();
// 从 store 中获取响应式数据
const { loading: siteLoading } = storeToRefs(siteStore);
const { loading: statisticsLoading } = storeToRefs(statisticsStore);
// 系统信息
const systemInfo = reactive({
name: '小程序商城',
version: '2.0.0',
environment: '生产环境',
database: 'MySQL 8.0',
server: 'Linux CentOS 7.9',
});
// ============================================================
// 应用订阅相关数据来源websopy app_subscription 表)
// productId 通过 当前登录用户租户ID 查询 app_product 获得
// ============================================================
// 当前应用产品按租户ID查询
const currentProduct = ref<AppProduct | null>(null);
// 当前应用的有效订阅
const currentSubscription = ref<AppSubscription | null>(null);
// 订阅信息加载中
const subscriptionLoading = ref(false);
// 订阅 Modal选择周期
const subscribeModalVisible = ref(false);
const subscribing = ref(false);
const selectedPeriod = ref<'month' | 'year'>('month');
// 支付 Modal小程序码扫码支付
const payModalVisible = ref(false);
const payQrcode = ref('');
const payPrice = ref(0);
const paySubscriptionNo = ref('');
const payProductName = ref('');
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null);
const pollCount = ref(0);
const MAX_POLL_COUNT = 120; // 2.5s * 120 = 5分钟
// 微信小程序环境版本:开发用 trial生产用 release
const envVersion = import.meta.env.DEV ? 'trial' : 'release';
// 是否免费应用
const isFreeProduct = computed(() => {
const p = currentProduct.value;
if (!p) return false;
return p.priceType === 'free' || !p.price || Number(p.price) === 0;
});
// 月付参考价格product.price 单位为分除以100转为元
const monthPrice = computed(() => (Number(currentProduct.value?.price) || 0) / 100);
// 年付参考价格按10个月计费对齐后端 subscribe/generatePayQrcode 年付优惠)
const yearPrice = computed(() => monthPrice.value * 10);
// 距到期天数负数表示已过期null 表示无到期时间
const daysToExpire = computed(() => {
const expire = currentSubscription.value?.expireTime;
if (!expire) return null;
return dayjs(expire).startOf('day').diff(dayjs().startOf('day'), 'day');
});
// 格式化时间
const formatDateTime = (dt?: string) => {
if (!dt) return '-';
return dayjs(dt).format('YYYY-MM-DD HH:mm:ss');
};
// 加载当前应用的订阅信息
const loadSubscriptionInfo = async () => {
const tenantId = userStore.info?.tenantId;
if (!tenantId) {
console.warn('无法获取租户ID跳过订阅信息加载');
return;
}
subscriptionLoading.value = true;
try {
// 1. 按租户ID查询应用产品取第一条
const productRes = await pageProducts({
tenantId,
current: 1,
size: 1
});
const product = productRes.list?.[0];
if (!product || !product.productId) {
console.warn('当前租户未找到应用产品');
return;
}
currentProduct.value = product;
// 2. 拉取我的订阅列表,过滤出该产品的订阅
const subRes = await subscriptionStore.fetchMySubscriptions(
{ page: 1, limit: 50 },
false
);
const subs = (subRes.list || []).filter(
(s) => s.productId === product.productId
);
// 优先取 active 中 expireTime 最新;否则取 pending 最新
const active = subs
.filter((s) => s.status === 'active')
.sort(
(a, b) =>
dayjs(b.expireTime).valueOf() - dayjs(a.expireTime).valueOf()
)[0];
const pending = subs
.filter((s) => s.status === 'pending')
.sort(
(a, b) => dayjs(b.createTime).valueOf() - dayjs(a.createTime).valueOf()
)[0];
currentSubscription.value = active || pending || null;
} catch (e) {
console.warn('获取订阅信息失败:', e);
} finally {
subscriptionLoading.value = false;
}
};
// 刷新订阅信息(支付成功/订阅成功后调用)
const refreshSubscription = () => {
loadSubscriptionInfo().catch(() => {});
};
// 打开订阅 Modal
const onSubscribe = () => {
if (!currentProduct.value?.productId) {
message.warning('应用信息加载中,请稍后再试');
return;
}
selectedPeriod.value = 'month';
subscribeModalVisible.value = true;
};
// 确认订阅
const confirmSubscribe = async () => {
const product = currentProduct.value;
if (!product?.productId) return;
subscribing.value = true;
try {
// 1. 创建订阅subscribe 后端会 setPriceType避免 generatePayQrcode 的 price_type 报错)
const subResult = await subscriptionStore.subscribe({
productId: product.productId,
subscriptionPeriod: selectedPeriod.value
});
// 免费应用subscribe 直接激活status=active
if (subResult.status === 'active') {
message.success('订阅成功');
subscribeModalVisible.value = false;
refreshSubscription();
return;
}
// 2. 付费应用:用 pay 生成支付小程序码(参考 websopy-pc 的 subscribe + pay 模式)
const payResult = await subscriptionStore.pay(
subResult.subscriptionId,
'wechat',
envVersion
);
if ('miniappQrcode' in payResult) {
paySubscriptionNo.value =
payResult.subscriptionNo || subResult.subscriptionNo;
payQrcode.value = payResult.miniappQrcode;
payPrice.value = Number(payResult.payPrice) || 0;
payProductName.value = product.productName || '应用订阅';
subscribeModalVisible.value = false;
payModalVisible.value = true;
// 开始轮询支付状态
startPolling(paySubscriptionNo.value);
}
} catch (e: any) {
console.error('创建订阅失败:', e);
message.error(e?.message || '创建订阅失败,请重试');
} finally {
subscribing.value = false;
}
};
// 重新支付pending 订阅,复用已有订阅记录生成小程序码)
const repayPending = async () => {
const sub = currentSubscription.value;
if (!sub?.id || !sub.subscriptionNo) return;
try {
const result = await subscriptionStore.pay(sub.id, 'wechat', envVersion);
if ('miniappQrcode' in result) {
paySubscriptionNo.value = sub.subscriptionNo;
payQrcode.value = result.miniappQrcode;
payPrice.value = Number(result.payPrice) || 0;
payProductName.value =
sub.productName || currentProduct.value?.productName || '应用订阅';
payModalVisible.value = true;
startPolling(sub.subscriptionNo);
}
} catch (e: any) {
message.error(e?.message || '生成支付码失败');
}
};
// 开始轮询支付状态(每 2.5s 查一次,最多 5 分钟)
const startPolling = (subscriptionNo: string) => {
stopPolling();
pollCount.value = 0;
pollingTimer.value = setInterval(async () => {
pollCount.value++;
if (pollCount.value > MAX_POLL_COUNT) {
stopPolling();
message.warning('支付状态查询超时,如已支付请刷新页面');
return;
}
try {
const status = await subscriptionStore.checkStatus(subscriptionNo);
if (status.paid) {
stopPolling();
payModalVisible.value = false;
message.success('支付成功');
refreshSubscription();
}
} catch (e) {
console.warn('查询支付状态失败:', e);
}
}, 2500);
};
// 停止轮询
const stopPolling = () => {
if (pollingTimer.value) {
clearInterval(pollingTimer.value);
pollingTimer.value = null;
}
pollCount.value = 0;
};
// 关闭支付 Modal
const onPayModalClose = () => {
stopPolling();
payModalVisible.value = false;
payQrcode.value = '';
paySubscriptionNo.value = '';
};
// 计算属性
const now = ref(Date.now());
let runDaysTimer: ReturnType<typeof setInterval>;
const runDays = computed(() => {
const createTime = tenantStore.company?.createTime;
if (!createTime) return 0;
return Math.floor((now.value - new Date(createTime).getTime()) / (24 * 60 * 60 * 1000));
});
const userCount = computed(() => statisticsStore.userCount);
const orderCount = computed(() => statisticsStore.orderCount);
const totalSales = computed(() => statisticsStore.totalSales);
const todaySales = computed(() => statisticsStore.todaySales);
const todayOrders = computed(() => statisticsStore.todayOrders);
const todayUsers = computed(() => statisticsStore.todayUsers);
// 加载状态
const loading = computed(() => siteLoading.value || statisticsLoading.value);
// 核心统计数据使用computed确保响应式更新
const coreStats = computed(() => [
{ icon: '🏸', label: '用户总数', value: userCount.value || 0, desc: '注册用户', color: 'blue', to: '/system/user' },
{ icon: '📦', label: '订单总数', value: orderCount.value || 0, desc: '全部订单', color: 'orange', to: '/shop/shopOrder?tab=all' },
{ icon: '💰', label: '总营业额', value: totalSales.value || 0, desc: '元', color: 'purple', to: '/shop/shopOrder?tab=all' },
{ icon: '⏱️', label: '运行天数', value: runDays.value || 0, desc: '系统运行', color: 'green', to: '' },
]);
// 待处理事项数据
const pendingPaymentCount = ref(0);
const pendingShipmentCount = ref(0);
const pendingRefundCount = ref(0);
const couponUsedCount = computed(() => statisticsStore.safeCouponUsedCount);
// 商城Logo
const shopLogo = ref('');
const shopLogoUrl = computed(() =>
shopLogo.value ? getCompressedImageUrl(shopLogo.value, { width: 200, quality: 90 }) : ''
);
// 待处理事项使用computed确保响应式更新
const todoItems = computed(() => [
{ label: '待付款订单', value: pendingPaymentCount.value, to: '/shop/shopOrder?tab=unpaid', tagColor: 'gold', dotColor: 'dot-gold', urgent: pendingPaymentCount.value > 0 },
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder?tab=undelivered', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
{ label: '退款申请', value: pendingRefundCount.value, to: '/shop/shopOrder?tab=refunded', tagColor: 'orange', dotColor: 'dot-orange', urgent: pendingRefundCount.value > 0 },
{ label: '优惠券使用', value: couponUsedCount.value, to: '/market/coupon', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
]);
// 今日统计使用computed确保响应式更新
const todayStats = computed(() => ({
salesAmount: todaySales.value || 0,
orderCount: todayOrders.value || 0,
newUsers: todayUsers.value || 0,
couponUsed: couponUsedCount.value || 0,
}));
// 快速入口
const quickLinks = [
{ to: '/shop/shopOrder?tab=all', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
{ to: '/shop/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
{ to: '/shop/shopGoodsCategory', icon: '🗂️', label: '商品分类', bg: '#eff6ff' },
{ to: '/market/coupon', icon: '🎁', label: '优惠券', bg: '#fff7ed' },
{ to: '/system/user', icon: '👥', label: '会员管理', bg: '#faf5ff' },
{ to: '/shop/shopSetting', icon: '⚙️', label: '商城设置', bg: '#fefce8' },
];
// 导航跳转
const navigateTo = (path: string) => {
if (path) {
router.push(path);
}
};
// 金额格式化(保留两位小数)
const formatMoney = (value?: number | string) => {
const num = Number(value);
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
};
// 清除缓存
const handleClearCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
// 新订单声音提醒
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
onNewOrder: () => loadData()
});
// 刷新统计数据
const refreshStatistics = async () => {
try {
await statisticsStore.fetchStatistics();
message.success('统计数据刷新成功');
} catch (error) {
console.error('刷新统计数据失败:', error);
message.error('刷新统计数据失败');
}
};
// 加载数据
const loadData = async () => {
// 独立请求租户信息,不受其他请求失败影响
tenantStore.fetchTenantInfo().catch((e) => {
console.warn('获取租户信息失败:', e);
});
// 独立加载订阅信息(到期时间),不受其他请求失败影响
loadSubscriptionInfo().catch((e) => {
console.warn('加载订阅信息失败:', e);
});
// 独立请求商城Logo不受其他请求失败影响
getShopSettingCategoryValues('basic')
.then((values) => {
shopLogo.value = values?.shopLogo || '';
})
.catch((e) => {
console.warn('获取商城设置失败:', e);
});
try {
await Promise.all([
siteStore.fetchSiteInfo(),
statisticsStore.fetchStatistics()
]);
// 获取待处理事项数据
try {
// 获取待付款订单数type=0商城订单, statusFilter=0待付款
const paymentResult = await pageShopOrder({
type: 0,
statusFilter: 0,
page: 1,
limit: 1
});
pendingPaymentCount.value = paymentResult?.count || 0;
} catch (e) {
console.warn('获取待付款订单数失败:', e);
pendingPaymentCount.value = 0;
}
try {
// 获取待发货订单数type=0商城订单, statusFilter=1待发货
const shipmentResult = await pageShopOrder({
type: 0,
statusFilter: 1,
page: 1,
limit: 1
});
pendingShipmentCount.value = shipmentResult?.count || 0;
} catch (e) {
console.warn('获取待发货订单数失败:', e);
pendingShipmentCount.value = 0;
}
try {
// 获取退款/售后订单数type=0商城订单, statusFilter=6退款/售后)
const refundResult = await pageShopOrder({
type: 0,
statusFilter: 6,
page: 1,
limit: 1
});
pendingRefundCount.value = refundResult?.count || 0;
} catch (e) {
console.warn('获取退款/售后订单数失败:', e);
pendingRefundCount.value = 0;
}
} catch (error) {
console.error('加载数据失败:', error);
}
};
onMounted(async () => {
await loadData();
// 开始自动刷新统计数据每5分钟
statisticsStore.startAutoRefresh();
// 运行天数每小时检查一次(跨天自动更新)
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
});
onUnmounted(() => {
// 组件卸载时停止自动刷新
statisticsStore.stopAutoRefresh();
clearInterval(runDaysTimer);
// 停止支付状态轮询
stopPolling();
});
</script>
<style scoped>
.shop-dashboard {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px 24px;
}
/* 欢迎横幅 */
.welcome-banner {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #00704A 0%, #1a3931 100%);
border-radius: 14px;
padding: 24px 28px;
color: #fff;
flex-wrap: wrap;
gap: 12px;
}
.welcome-logo {
flex-shrink: 0;
border: 2px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.1);
}
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
/* 欢迎横幅右侧操作区 */
.welcome-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
/* 新订单提醒栏 */
.order-notify-bar {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255,255,255,0.12);
border-radius: 8px;
padding: 5px 12px;
}
.order-notify-label {
font-size: 13px;
color: rgba(255,255,255,0.85);
white-space: nowrap;
}
.order-notify-info {
color: rgba(255,255,255,0.6);
font-size: 14px;
cursor: help;
}
/* 核心统计块 */
.stat-block {
padding: 18px 20px;
border-radius: 12px;
border: 2px solid transparent;
transition: all 0.2s;
}
.stat-block:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-block.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-block.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-block.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-block.purple { background: #faf5ff; border-color: #e9d5ff; }
.stat-block-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.stat-block-icon { font-size: 18px; }
.stat-block-label { font-size: 13px; color: rgba(0,0,0,0.55); }
.stat-block-value { font-size: 32px; font-weight: 800; color: rgba(0,0,0,0.85); line-height: 1.1; margin-bottom: 4px; }
.stat-block-desc { font-size: 12px; color: rgba(0,0,0,0.4); }
/* Panel */
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
.panel-header { padding: 14px 18px; border-bottom: 1px solid #f5f5f5; }
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
/* 待办 */
.todo-list { padding: 8px 0; }
.todo-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
}
.todo-item:hover { background: #f9fafb; }
.todo-item-urgent .todo-label { font-weight: 600; }
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-orange { background: #f97316; }
.dot-blue { background: #3b82f6; }
.dot-gold { background: #faad14; }
.dot-cyan { background: #06b6d4; }
.dot-purple { background: #a855f7; }
.todo-content { flex: 1; display: flex; align-items: center; }
.todo-label { font-size: 14px; color: rgba(0,0,0,0.75); }
.todo-arrow { font-size: 11px; color: rgba(0,0,0,0.3); }
.todo-empty { text-align: center; padding: 20px 0; color: rgba(0,0,0,0.4); font-size: 14px; }
/* 快速入口九宫格 */
.quick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: #f5f5f5;
}
.quick-card {
display: flex; flex-direction: column; align-items: center;
gap: 8px; padding: 18px 12px; background: #fff;
cursor: pointer; transition: background 0.15s;
}
.quick-card:hover { background: #f9fafb; }
.quick-icon {
width: 44px; height: 44px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 22px;
}
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
/* 今日数据 */
.today-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 20px;
}
.today-stat {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
}
.today-stat-icon {
font-size: 28px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.today-stat-value {
font-size: 20px;
font-weight: 700;
color: #1a3931;
}
.today-stat-label {
font-size: 12px;
color: #666;
margin-top: 2px;
}
@media (max-width: 768px) {
.today-stats {
grid-template-columns: repeat(2, 1fr);
}
}
/* 到期时间状态 */
.text-muted { color: rgba(0,0,0,0.45); }
.text-orange { color: #fa8c16; }
.expire-warn { color: #ff4d4f; font-size: 12px; margin-left: 4px; }
/* 订阅 Modal */
.subscribe-modal-body { padding: 8px 0; }
.subscribe-product-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: rgba(0,0,0,0.85);
}
.period-options {
display: flex;
gap: 16px;
}
.period-option {
flex: 1;
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.period-option:hover { border-color: #69b1ff; }
.period-option.active {
border-color: #00704A;
background: #f0fdf4;
}
.period-option-label { font-size: 14px; color: rgba(0,0,0,0.65); margin-bottom: 8px; }
.period-option-price { font-size: 18px; font-weight: 700; color: #00704A; }
.period-option-tag {
position: absolute;
top: -8px;
right: 8px;
background: #ff4d4f;
color: #fff;
font-size: 11px;
padding: 1px 8px;
border-radius: 8px;
}
/* 支付 Modal */
.pay-modal-body { text-align: center; padding: 8px 0; }
.pay-product-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: rgba(0,0,0,0.85);
}
.pay-amount { margin-bottom: 16px; }
.pay-amount-label { font-size: 13px; color: rgba(0,0,0,0.45); margin-right: 8px; }
.pay-amount-value { font-size: 24px; font-weight: 800; color: #ff4d4f; }
.pay-qrcode-wrap {
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.pay-qrcode-img {
width: 220px;
height: 220px;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.pay-tip {
font-size: 13px;
color: rgba(0,0,0,0.55);
margin-bottom: 8px;
}
.pay-order-no {
font-size: 12px;
color: rgba(0,0,0,0.35);
}
</style>