Compare commits
8 Commits
v1.0
...
9e65007e65
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e65007e65 | |||
| e2dd8dbc18 | |||
| 08b697556b | |||
| b0ff82599a | |||
| 57b584df1b | |||
| 6350a9b5a9 | |||
| 614e843673 | |||
| 894592c290 |
@@ -6,3 +6,27 @@
|
|||||||
- 在 `src/views/shop/dashboard/index.vue` 的欢迎横幅右侧添加新订单提醒开关栏(开关 + 测试按钮),检测到新订单后自动调用 loadData() 刷新 Dashboard 数据。
|
- 在 `src/views/shop/dashboard/index.vue` 的欢迎横幅右侧添加新订单提醒开关栏(开关 + 测试按钮),检测到新订单后自动调用 loadData() 刷新 Dashboard 数据。
|
||||||
- 在 `src/views/cms/dashboard/index.vue` 的概况卡片标题栏添加新订单提醒控件,同样检测到新订单后刷新数据。
|
- 在 `src/views/cms/dashboard/index.vue` 的概况卡片标题栏添加新订单提醒控件,同样检测到新订单后刷新数据。
|
||||||
- 提取了 CMS Dashboard 的 loadData 函数使其可复用。
|
- 提取了 CMS Dashboard 的 loadData 函数使其可复用。
|
||||||
|
|
||||||
|
## 更新 shop/dashboard 快捷操作按钮为商城常用功能
|
||||||
|
|
||||||
|
- 快捷操作按钮从原来的通用功能(参数配置/用户管理/站点管理/登录日志)改为商城系统常用功能:订单管理、商品管理、商品分类、优惠券管理、会员管理、商城设置、清除缓存。
|
||||||
|
- 路由路径统一使用 `/shop/shopXxx` 格式(与 `/shop/shopOrder` 等已确认路径一致),修复了快速入口中 `/shopGoods` → `/shop/shopGoods` 的路径不一致问题。
|
||||||
|
- 快速入口九宫格也同步改为商城相关入口。
|
||||||
|
- 图标导入更新:移除 UngroupOutlined/CalendarOutlined/UserOutlined/FileTextOutlined,新增 ShoppingCartOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined。
|
||||||
|
|
||||||
|
## 修复 shop/dashboard 待发货订单数量统计不准确
|
||||||
|
|
||||||
|
- 根因:订单列表页 datasource 中 `where.type = 0`(只查商城订单),但 Dashboard 统计未传 `type: 0`,导致把预定订单/外卖(type=1)和会员卡订单(type=2)也计入。
|
||||||
|
- 修复:为 pendingShipmentCount 和 pendingRefundCount 查询都加上 `type: 0`。
|
||||||
|
|
||||||
|
## 修复 Dashboard 运行天数为 0 的问题
|
||||||
|
|
||||||
|
- 根因:`getTenantInfo()` 被放在 `await Promise.all([...])` 之后,如果 `statisticsStore.fetchStatistics()` 抛异常(`couponUsedCount` 错误),`Promise.all` reject 后直接跳到 `catch`,`getTenantInfo` 根本不会执行,`tenantCreateTime` 永远是空值。
|
||||||
|
- 修复:在 `shop/dashboard/index.vue` 和 `cms/dashboard/index.vue` 的 `loadData` 中,把 `getTenantInfo()` 放到 `Promise.all` 外面,用 `.then` 独立执行,不受其他请求失败影响。
|
||||||
|
- 运行天数计算基于 `getTenantInfo()` 返回的 `Company.createTime`(租户创建时间)。
|
||||||
|
|
||||||
|
## 修复 statisticsStore couponUsedCount 冲突
|
||||||
|
|
||||||
|
- 根因:`couponUsedCount` 同时被定义成 state 属性和 getter,Pinia 中同名冲突导致 action 里 `this.couponUsedCount = ...` 报错:`'set' on proxy: trap returned falsish`。
|
||||||
|
- 修复:将 getter 改名为 `safeCouponUsedCount`,`shop/dashboard/index.vue` 中同步更新引用(该引用当前被注释,但保持一致性)。
|
||||||
|
|
||||||
|
|||||||
43
.workbuddy/memory/2026-07-15.md
Normal file
43
.workbuddy/memory/2026-07-15.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 2026-07-15
|
||||||
|
|
||||||
|
## 修复 shopOrder 页面默认待发货tab数据不正确
|
||||||
|
|
||||||
|
- 根因:`activeKey` 默认为 `'undelivered'`,但 `datasource` 函数只设了 `where.type = 0`,没有设 `statusFilter`。`statusFilter` 只在用户点击 tab 触发 `onTabs` 时才设置。所以页面初次加载时查的是全部订单而非待发货订单。
|
||||||
|
- 修复:提取 `getStatusFilterByTab(key)` 公共映射函数,在 `datasource` 中当 `where.statusFilter` 未设置时从 `activeKey` 自动推导。`onTabs` 也简化为复用同一函数。
|
||||||
|
- 文件:`src/views/shop/shopOrder/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 订单总数点击跳转支持指定 tab
|
||||||
|
|
||||||
|
- Dashboard 中"订单总数"/"总营业额"跳转链接改为 `/shop/shopOrder?tab=all`,"待发货订单"改为 `?tab=undelivered`,"退款申请"改为 `?tab=refunded`。
|
||||||
|
- shopOrder 页面读取 `route.query.tab` 初始化 `activeKey`,无效值回退到 `'undelivered'`。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`、`src/views/shop/shopOrder/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 待处理事项新增"待付款订单"
|
||||||
|
|
||||||
|
- 在待发货订单上方新增"待付款订单"统计项,使用 `statusFilter=0` 查询,点击跳转 `/shop/shopOrder?tab=unpaid`。
|
||||||
|
- shopOrder 页面取消注释"待付款"tab,`validTabs` 加入 `unpaid`。
|
||||||
|
- 新增 `dot-gold` 样式。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`、`src/views/shop/shopOrder/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 引入 useTenantStore,创建时间读租户信息
|
||||||
|
|
||||||
|
- 引入 `useTenantStore`,`loadData` 中调用 `tenantStore.fetchTenantInfo()` 替代直接调用 `getTenantInfo()` API。
|
||||||
|
- 基本信息"创建时间"改为读取 `tenantStore.company?.createTime`。
|
||||||
|
- 运行天数也改为从 `tenantStore.company?.createTime` 计算,移除独立的 `tenantCreateTime` ref。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 欢迎横幅新增商城Logo
|
||||||
|
|
||||||
|
- 通过 `getShopSettingCategoryValues('basic')` 获取商城设置,读取 `shopLogo` 字段。
|
||||||
|
- 使用 `getCompressedImageUrl(shopLogo, { width: 200, quality: 90 })` 压缩图片。
|
||||||
|
- 在 welcome-banner 左侧用 `a-avatar` (64px, square) 展示,无 Logo 时不显示。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`
|
||||||
|
|
||||||
|
## 新增 useAppSubscriptionStore(websopy 应用订阅特殊接口)
|
||||||
|
|
||||||
|
- 新增 `src/api/app/appSubscription/model.ts`:AppSubscription 实体及参数/返回类型,对照后端 `com.gxwebsoft.app.entity.AppSubscription`。
|
||||||
|
- 新增 `src/api/app/appSubscription/index.ts`:封装 `AppSubscriptionController` 全部 14 个接口。
|
||||||
|
- 特殊接口域名 `https://websopy-api.websoft.top/api`(与项目主 API 域名不同),复用 `@/utils/request`(共享登录态 token、401 处理),传绝对 URL。
|
||||||
|
- 后端无 context-path;baseURL=`https://websopy-api.websoft.top/api`,接口路径用 `/app/subscription/xxx`。
|
||||||
|
- 后端 `BaseController` 成功 code=0;`success(IPage)` 转成 `PageResult{list,count}`,与项目 `@/api` 的 PageResult 结构一致。
|
||||||
|
- 新增 `src/store/modules/appSubscription.ts`:`useAppSubscriptionStore`(Pinia Options API 风格,对齐 site.ts/statistics.ts),聚合列表/详情/支付状态/订阅管理方法。列表/详情缓存5分钟;`checkStatus`/`checkPurchased` 实时请求;订阅/支付/管理类操作成功后调 `invalidateCache()` 失效缓存。
|
||||||
269
src/api/app/appSubscription/index.ts
Normal file
269
src/api/app/appSubscription/index.ts
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type {
|
||||||
|
AppSubscription,
|
||||||
|
AppSubscriptionQueryParam,
|
||||||
|
SubscribeParam,
|
||||||
|
SubscribeResult,
|
||||||
|
GeneratePayQrcodeParam,
|
||||||
|
GeneratePayQrcodeResult,
|
||||||
|
CheckStatusResult,
|
||||||
|
PayResult,
|
||||||
|
WechatNativePayResult,
|
||||||
|
MpPrepayParam,
|
||||||
|
MpPrepayResult,
|
||||||
|
MpConfirmParam,
|
||||||
|
BalanceResult
|
||||||
|
} from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websopy 特殊接口域名(与项目主 API 域名不同)
|
||||||
|
* 后端 Controller: com.gxwebsoft.app.controller.AppSubscriptionController
|
||||||
|
* @RequestMapping("/api/app/subscription"),后端无 context-path
|
||||||
|
* 因此完整 URL = https://websopy-api.websoft.top + /api/app/subscription/xxx
|
||||||
|
*/
|
||||||
|
const WEBSOPY_API_BASE = 'https://websopy-api.websoft.top/api';
|
||||||
|
const BASE = `${WEBSOPY_API_BASE}/app/subscription`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的订阅列表(分页)
|
||||||
|
* GET /app/subscription/my/page
|
||||||
|
*/
|
||||||
|
export async function pageMySubscriptions(
|
||||||
|
params: AppSubscriptionQueryParam
|
||||||
|
): Promise<PageResult<AppSubscription>> {
|
||||||
|
const res = await request.get<ApiResult<PageResult<AppSubscription>>>(
|
||||||
|
`${BASE}/my/page`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return (
|
||||||
|
res.data.data || { list: [], count: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅详情(按ID)
|
||||||
|
* GET /app/subscription/detail/{id}
|
||||||
|
*/
|
||||||
|
export async function getSubscriptionDetail(
|
||||||
|
id: number
|
||||||
|
): Promise<AppSubscription> {
|
||||||
|
const res = await request.get<ApiResult<AppSubscription>>(
|
||||||
|
`${BASE}/detail/${id}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as AppSubscription;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订阅编号查询详情(小程序入口用)
|
||||||
|
* GET /app/subscription/detail-by-no/{subscriptionNo}
|
||||||
|
*/
|
||||||
|
export async function getSubscriptionDetailByNo(
|
||||||
|
subscriptionNo: string
|
||||||
|
): Promise<AppSubscription> {
|
||||||
|
const res = await request.get<ApiResult<AppSubscription>>(
|
||||||
|
`${BASE}/detail-by-no/${subscriptionNo}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as AppSubscription;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态(前端轮询用)
|
||||||
|
* GET /app/subscription/check-status/{subscriptionNo}
|
||||||
|
*/
|
||||||
|
export async function checkSubscriptionStatus(
|
||||||
|
subscriptionNo: string
|
||||||
|
): Promise<CheckStatusResult> {
|
||||||
|
const res = await request.get<ApiResult<CheckStatusResult>>(
|
||||||
|
`${BASE}/check-status/${subscriptionNo}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as CheckStatusResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已购买某应用
|
||||||
|
* GET /app/subscription/check-purchased/{productId}
|
||||||
|
* 返回 boolean
|
||||||
|
*/
|
||||||
|
export async function checkPurchased(
|
||||||
|
productId: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
const res = await request.get<ApiResult<boolean>>(
|
||||||
|
`${BASE}/check-purchased/${productId}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data === true;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户余额
|
||||||
|
* GET /app/subscription/balance
|
||||||
|
*/
|
||||||
|
export async function getBalance(): Promise<BalanceResult> {
|
||||||
|
const res = await request.get<ApiResult<BalanceResult>>(`${BASE}/balance`);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return (
|
||||||
|
res.data.data || { balance: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅
|
||||||
|
* POST /app/subscription/subscribe
|
||||||
|
* 免费应用直接激活,付费应用创建待支付记录
|
||||||
|
*/
|
||||||
|
export async function subscribe(
|
||||||
|
data: SubscribeParam
|
||||||
|
): Promise<SubscribeResult> {
|
||||||
|
const res = await request.post<ApiResult<SubscribeResult>>(
|
||||||
|
`${BASE}/subscribe`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as SubscribeResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码(同时创建订阅记录)
|
||||||
|
* POST /app/subscription/generate-pay-qrcode
|
||||||
|
*/
|
||||||
|
export async function generatePayQrcode(
|
||||||
|
data: GeneratePayQrcodeParam
|
||||||
|
): Promise<GeneratePayQrcodeResult> {
|
||||||
|
const res = await request.post<ApiResult<GeneratePayQrcodeResult>>(
|
||||||
|
`${BASE}/generate-pay-qrcode`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as GeneratePayQrcodeResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起支付
|
||||||
|
* POST /app/subscription/pay/{id}?method=balance|wechat&envVersion=xxx
|
||||||
|
* - method=balance:余额支付,返回 PayResult
|
||||||
|
* - method=wechat:微信 Native 支付,返回小程序码 WechatNativePayResult
|
||||||
|
*/
|
||||||
|
export async function paySubscription(
|
||||||
|
id: number,
|
||||||
|
method: 'balance' | 'wechat' = 'wechat',
|
||||||
|
envVersion?: string
|
||||||
|
): Promise<PayResult | WechatNativePayResult> {
|
||||||
|
const res = await request.post<ApiResult<PayResult | WechatNativePayResult>>(
|
||||||
|
`${BASE}/pay/${id}`,
|
||||||
|
undefined,
|
||||||
|
{ params: { method, envVersion } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as PayResult | WechatNativePayResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 JSAPI 预支付下单
|
||||||
|
* POST /app/subscription/mp-prepay/{id}
|
||||||
|
* Body: { openid }
|
||||||
|
*/
|
||||||
|
export async function mpPrepay(
|
||||||
|
id: number,
|
||||||
|
data: MpPrepayParam
|
||||||
|
): Promise<MpPrepayResult> {
|
||||||
|
const res = await request.post<ApiResult<MpPrepayResult>>(
|
||||||
|
`${BASE}/mp-prepay/${id}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as MpPrepayResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付成功确认
|
||||||
|
* POST /app/subscription/mp-confirm/{subscriptionNo}
|
||||||
|
* Body: { transactionId? }
|
||||||
|
*/
|
||||||
|
export async function mpConfirm(
|
||||||
|
subscriptionNo: string,
|
||||||
|
data?: MpConfirmParam
|
||||||
|
): Promise<PayResult> {
|
||||||
|
const res = await request.post<ApiResult<PayResult>>(
|
||||||
|
`${BASE}/mp-confirm/${subscriptionNo}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return (
|
||||||
|
res.data.data || { paid: true, subscriptionNo }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续费
|
||||||
|
* POST /app/subscription/renew/{id}?period=month|year
|
||||||
|
*/
|
||||||
|
export async function renewSubscription(
|
||||||
|
id: number,
|
||||||
|
period: 'month' | 'year' = 'month'
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await request.post<ApiResult<string>>(`${BASE}/renew/${id}`, undefined, {
|
||||||
|
params: { period }
|
||||||
|
});
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message || '续费成功';
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退订/取消
|
||||||
|
* POST /app/subscription/cancel/{id}
|
||||||
|
*/
|
||||||
|
export async function cancelSubscription(id: number): Promise<string> {
|
||||||
|
const res = await request.post<ApiResult<string>>(`${BASE}/cancel/${id}`);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message || '退订成功';
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/禁用
|
||||||
|
* POST /app/subscription/toggle-enable/{id}?enabled=true|false
|
||||||
|
*/
|
||||||
|
export async function toggleEnable(
|
||||||
|
id: number,
|
||||||
|
enabled: boolean
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await request.post<ApiResult<string>>(
|
||||||
|
`${BASE}/toggle-enable/${id}`,
|
||||||
|
undefined,
|
||||||
|
{ params: { enabled } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message || (enabled ? '已启用' : '已禁用');
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
219
src/api/app/appSubscription/model.ts
Normal file
219
src/api/app/appSubscription/model.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅状态: pending-待支付 active-已激活 expired-已过期 cancelled-已取消
|
||||||
|
*/
|
||||||
|
export type SubscriptionStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'active'
|
||||||
|
| 'expired'
|
||||||
|
| 'cancelled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价格类型: free-免费 one_time-买断 subscription-订阅
|
||||||
|
*/
|
||||||
|
export type PriceType = 'free' | 'one_time' | 'subscription';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅周期: month-月 quarter-季 year-年
|
||||||
|
*/
|
||||||
|
export type SubscriptionPeriod = 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付方式: 0-余额 1-微信 2-支付宝 12-免费
|
||||||
|
*/
|
||||||
|
export type PayType = 0 | 1 | 2 | 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用订阅实体(对应表 app_subscription)
|
||||||
|
* 字段对照后端 com.gxwebsoft.app.entity.AppSubscription
|
||||||
|
*/
|
||||||
|
export interface AppSubscription {
|
||||||
|
// 主键ID
|
||||||
|
id?: number;
|
||||||
|
// 订阅编号(业务唯一)
|
||||||
|
subscriptionNo?: string;
|
||||||
|
// 购买用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 应用产品ID
|
||||||
|
productId?: number;
|
||||||
|
// 租户ID
|
||||||
|
tenantId?: number;
|
||||||
|
// 订阅状态
|
||||||
|
status?: SubscriptionStatus;
|
||||||
|
// 价格类型
|
||||||
|
priceType?: PriceType;
|
||||||
|
// 原价(单位:元)
|
||||||
|
originalPrice?: number;
|
||||||
|
// 实付金额(单位:元)
|
||||||
|
payPrice?: number;
|
||||||
|
// 支付方式
|
||||||
|
payType?: PayType;
|
||||||
|
// 支付状态: 0-未支付 1-已支付
|
||||||
|
payStatus?: number;
|
||||||
|
// 支付时间
|
||||||
|
payTime?: string;
|
||||||
|
// 第三方交易号
|
||||||
|
transactionId?: string;
|
||||||
|
// 订阅周期
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
// 生效时间
|
||||||
|
startTime?: string;
|
||||||
|
// 到期时间(订阅型)
|
||||||
|
expireTime?: string;
|
||||||
|
// 是否自动续费 0-否 1-是
|
||||||
|
autoRenew?: number;
|
||||||
|
// 分配的域名
|
||||||
|
instanceDomain?: string;
|
||||||
|
// 实例管理后台URL
|
||||||
|
instanceAdminUrl?: string;
|
||||||
|
// 实例配置(JSON)
|
||||||
|
instanceConfig?: string;
|
||||||
|
// 关联的支付订单号
|
||||||
|
orderNo?: string;
|
||||||
|
// 关联的支付订单ID
|
||||||
|
orderId?: number;
|
||||||
|
// 备注
|
||||||
|
remark?: string;
|
||||||
|
// 排序
|
||||||
|
sortNumber?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 更新时间
|
||||||
|
updateTime?: string;
|
||||||
|
// ===== 关联查询字段(非数据库字段) =====
|
||||||
|
productName?: string;
|
||||||
|
productIcon?: string;
|
||||||
|
productLogo?: string;
|
||||||
|
productAppType?: number;
|
||||||
|
productDescription?: string;
|
||||||
|
developerName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的订阅分页查询参数
|
||||||
|
*/
|
||||||
|
export interface AppSubscriptionQueryParam extends PageParam {
|
||||||
|
// 订阅状态过滤
|
||||||
|
status?: SubscriptionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅参数
|
||||||
|
*/
|
||||||
|
export interface SubscribeParam {
|
||||||
|
// 应用产品ID
|
||||||
|
productId: number;
|
||||||
|
// 订阅周期,默认 month
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码参数
|
||||||
|
*/
|
||||||
|
export interface GeneratePayQrcodeParam {
|
||||||
|
// 应用产品ID
|
||||||
|
productId: number;
|
||||||
|
// 订阅周期,默认 month
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
// 小程序版本:develop / trial / release,默认 trial
|
||||||
|
envVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅返回结果
|
||||||
|
*/
|
||||||
|
export interface SubscribeResult {
|
||||||
|
subscriptionId: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
message: string;
|
||||||
|
payPrice?: number;
|
||||||
|
orderNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码返回结果
|
||||||
|
*/
|
||||||
|
export interface GeneratePayQrcodeResult {
|
||||||
|
subscriptionNo: string;
|
||||||
|
subscriptionId: number;
|
||||||
|
qrcodeBase64: string;
|
||||||
|
productName: string;
|
||||||
|
payPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态返回结果
|
||||||
|
*/
|
||||||
|
export interface CheckStatusResult {
|
||||||
|
paid: boolean;
|
||||||
|
payStatus: number;
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
payTime?: string;
|
||||||
|
transactionId?: string;
|
||||||
|
id: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
productId: number;
|
||||||
|
productName?: string;
|
||||||
|
productLogo?: string;
|
||||||
|
priceType?: PriceType;
|
||||||
|
payPrice?: number;
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 余额支付返回结果
|
||||||
|
*/
|
||||||
|
export interface PayResult {
|
||||||
|
paid: boolean;
|
||||||
|
balance?: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信 Native 支付(Web 端)返回结果
|
||||||
|
*/
|
||||||
|
export interface WechatNativePayResult {
|
||||||
|
subscriptionId: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
miniappQrcode: string;
|
||||||
|
miniappPagePath: string;
|
||||||
|
payPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 JSAPI 预支付返回结果
|
||||||
|
*/
|
||||||
|
export interface MpPrepayResult {
|
||||||
|
subscriptionId: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
outTradeNo: string;
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
package: string;
|
||||||
|
signType: string;
|
||||||
|
paySign: string;
|
||||||
|
payPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取余额返回结果
|
||||||
|
*/
|
||||||
|
export interface BalanceResult {
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序预支付参数
|
||||||
|
*/
|
||||||
|
export interface MpPrepayParam {
|
||||||
|
openid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付确认参数
|
||||||
|
*/
|
||||||
|
export interface MpConfirmParam {
|
||||||
|
transactionId?: string;
|
||||||
|
}
|
||||||
@@ -150,3 +150,19 @@ export async function refundShopOrder(data: ShopOrder) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认线下付款收款
|
||||||
|
* 商家确认已收到线下转账(微信转账/银行汇款等),确认后订单进入待发货状态
|
||||||
|
*/
|
||||||
|
export async function confirmOfflinePayment(id: number, remarks?: string) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
MODULES_API_URL + '/shop/shop-order/confirm-offline-payment/' + id,
|
||||||
|
null,
|
||||||
|
{ params: { remarks } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ export interface ShopOrder {
|
|||||||
coachId?: number;
|
coachId?: number;
|
||||||
// 支付的用户id
|
// 支付的用户id
|
||||||
payUserId?: number;
|
payUserId?: number;
|
||||||
// 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9~18 已废弃
|
// 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9线下付款, 10~18 已废弃
|
||||||
payType?: number;
|
payType?: number;
|
||||||
// 代付支付方式, 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9~18 已废弃
|
// 代付支付方式, 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9线下付款, 10~18 已废弃
|
||||||
friendPayType?: number;
|
friendPayType?: number;
|
||||||
// 0未付款,1已付款
|
// 0未付款,1已付款
|
||||||
payStatus?: number;
|
payStatus?: number;
|
||||||
|
|||||||
@@ -84,6 +84,12 @@
|
|||||||
label: '货到付款',
|
label: '货到付款',
|
||||||
key: 'codPay',
|
key: 'codPay',
|
||||||
icon: 'IdcardOutlined'
|
icon: 'IdcardOutlined'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: '线下付款',
|
||||||
|
key: 'offlinePay',
|
||||||
|
icon: 'IdcardOutlined'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
|
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
|
||||||
/* 主框架 */
|
/* 主框架 */
|
||||||
export default {
|
export default {
|
||||||
system: '小程序开发',
|
system: '小程序商城',
|
||||||
home: '主页',
|
home: '主页',
|
||||||
header: {
|
header: {
|
||||||
profile: '个人资料',
|
profile: '个人资料',
|
||||||
|
|||||||
368
src/store/modules/appSubscription.ts
Normal file
368
src/store/modules/appSubscription.ts
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* 应用订阅 store
|
||||||
|
* 数据来源:websopy 特殊接口(db_websopy.app_subscription 表)
|
||||||
|
* 接口域名:https://websopy-api.websoft.top/api
|
||||||
|
* 后端:com.gxwebsoft.app.controller.AppSubscriptionController
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import {
|
||||||
|
pageMySubscriptions,
|
||||||
|
getSubscriptionDetail,
|
||||||
|
getSubscriptionDetailByNo,
|
||||||
|
checkSubscriptionStatus,
|
||||||
|
checkPurchased,
|
||||||
|
getBalance,
|
||||||
|
subscribe,
|
||||||
|
generatePayQrcode,
|
||||||
|
paySubscription,
|
||||||
|
mpPrepay,
|
||||||
|
mpConfirm,
|
||||||
|
renewSubscription,
|
||||||
|
cancelSubscription,
|
||||||
|
toggleEnable
|
||||||
|
} from '@/api/app/appSubscription';
|
||||||
|
import type {
|
||||||
|
AppSubscription,
|
||||||
|
AppSubscriptionQueryParam,
|
||||||
|
SubscribeParam,
|
||||||
|
SubscribeResult,
|
||||||
|
GeneratePayQrcodeParam,
|
||||||
|
GeneratePayQrcodeResult,
|
||||||
|
CheckStatusResult,
|
||||||
|
PayResult,
|
||||||
|
WechatNativePayResult,
|
||||||
|
MpPrepayResult
|
||||||
|
} from '@/api/app/appSubscription/model';
|
||||||
|
import type { PageResult } from '@/api';
|
||||||
|
|
||||||
|
export interface AppSubscriptionState {
|
||||||
|
// 我的订阅列表
|
||||||
|
subscriptionList: AppSubscription[];
|
||||||
|
// 总数量
|
||||||
|
total: number;
|
||||||
|
// 当前查看的订阅详情
|
||||||
|
currentSubscription: AppSubscription | null;
|
||||||
|
// 用户余额
|
||||||
|
balance: number;
|
||||||
|
// 加载状态
|
||||||
|
loading: boolean;
|
||||||
|
// 最后更新时间
|
||||||
|
lastUpdateTime: number | null;
|
||||||
|
// 缓存有效期(毫秒)
|
||||||
|
cacheExpiry: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppSubscriptionStore = defineStore('appSubscription', {
|
||||||
|
state: (): AppSubscriptionState => ({
|
||||||
|
subscriptionList: [],
|
||||||
|
total: 0,
|
||||||
|
currentSubscription: null,
|
||||||
|
balance: 0,
|
||||||
|
loading: false,
|
||||||
|
lastUpdateTime: null,
|
||||||
|
// 默认缓存5分钟(订阅涉及支付状态,不宜过长)
|
||||||
|
cacheExpiry: 5 * 60 * 1000
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* 已激活订阅
|
||||||
|
*/
|
||||||
|
activeSubscriptions: (state): AppSubscription[] => {
|
||||||
|
return state.subscriptionList.filter((s) => s.status === 'active');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待支付订阅
|
||||||
|
*/
|
||||||
|
pendingSubscriptions: (state): AppSubscription[] => {
|
||||||
|
return state.subscriptionList.filter((s) => s.status === 'pending');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已过期订阅
|
||||||
|
*/
|
||||||
|
expiredSubscriptions: (state): AppSubscription[] => {
|
||||||
|
return state.subscriptionList.filter((s) => s.status === 'expired');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否有效
|
||||||
|
*/
|
||||||
|
isCacheValid: (state): boolean => {
|
||||||
|
if (!state.lastUpdateTime) return false;
|
||||||
|
const now = Date.now();
|
||||||
|
return now - state.lastUpdateTime < state.cacheExpiry;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// ============================================================
|
||||||
|
// 查询类
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的订阅列表(分页,带缓存)
|
||||||
|
* @param params 查询参数(page/limit/status)
|
||||||
|
* @param forceRefresh 是否强制刷新(切换 status 过滤时建议传 true)
|
||||||
|
*/
|
||||||
|
async fetchMySubscriptions(
|
||||||
|
params: AppSubscriptionQueryParam = { page: 1, limit: 10 },
|
||||||
|
forceRefresh = false
|
||||||
|
): Promise<PageResult<AppSubscription>> {
|
||||||
|
// 缓存有效且不强制刷新,直接返回缓存列表
|
||||||
|
if (!forceRefresh && this.isCacheValid && this.subscriptionList.length > 0) {
|
||||||
|
return { list: this.subscriptionList, count: this.total };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const data = await pageMySubscriptions(params);
|
||||||
|
this.subscriptionList = data.list || [];
|
||||||
|
this.total = data.count || 0;
|
||||||
|
this.lastUpdateTime = Date.now();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取我的订阅列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅详情(按ID)
|
||||||
|
*/
|
||||||
|
async fetchDetail(id: number): Promise<AppSubscription> {
|
||||||
|
try {
|
||||||
|
const data = await getSubscriptionDetail(id);
|
||||||
|
this.currentSubscription = data;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订阅详情失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订阅编号查询详情(小程序入口用)
|
||||||
|
*/
|
||||||
|
async fetchDetailByNo(subscriptionNo: string): Promise<AppSubscription> {
|
||||||
|
try {
|
||||||
|
const data = await getSubscriptionDetailByNo(subscriptionNo);
|
||||||
|
this.currentSubscription = data;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('根据订阅编号查询详情失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态(前端轮询用,强制实时请求,不走缓存)
|
||||||
|
*/
|
||||||
|
async checkStatus(subscriptionNo: string): Promise<CheckStatusResult> {
|
||||||
|
return await checkSubscriptionStatus(subscriptionNo);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已购买某应用(强制实时请求,不走缓存)
|
||||||
|
*/
|
||||||
|
async checkPurchased(productId: number): Promise<boolean> {
|
||||||
|
return await checkPurchased(productId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户余额
|
||||||
|
*/
|
||||||
|
async fetchBalance(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const data = await getBalance();
|
||||||
|
this.balance = data.balance;
|
||||||
|
return data.balance;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户余额失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 订阅与支付操作
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅
|
||||||
|
* 免费应用直接激活,付费应用创建待支付记录
|
||||||
|
*/
|
||||||
|
async subscribe(data: SubscribeParam): Promise<SubscribeResult> {
|
||||||
|
try {
|
||||||
|
const result = await subscribe(data);
|
||||||
|
// 订阅状态变化,失效缓存
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建订阅失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码(同时创建订阅记录)
|
||||||
|
*/
|
||||||
|
async generatePayQrcode(
|
||||||
|
data: GeneratePayQrcodeParam
|
||||||
|
): Promise<GeneratePayQrcodeResult> {
|
||||||
|
try {
|
||||||
|
const result = await generatePayQrcode(data);
|
||||||
|
// 已创建订阅记录,失效缓存
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成支付小程序码失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起支付
|
||||||
|
* @param id 订阅ID
|
||||||
|
* @param method 支付方式:balance-余额支付 / wechat-微信支付
|
||||||
|
* @param envVersion 小程序版本(微信支付时使用):develop/trial/release
|
||||||
|
*/
|
||||||
|
async pay(
|
||||||
|
id: number,
|
||||||
|
method: 'balance' | 'wechat' = 'wechat',
|
||||||
|
envVersion?: string
|
||||||
|
): Promise<PayResult | WechatNativePayResult> {
|
||||||
|
try {
|
||||||
|
const result = await paySubscription(id, method, envVersion);
|
||||||
|
// 支付成功后余额/订阅状态变化,失效缓存并刷新余额
|
||||||
|
this.invalidateCache();
|
||||||
|
if (method === 'balance') {
|
||||||
|
const payResult = result as PayResult;
|
||||||
|
if (typeof payResult.balance === 'number') {
|
||||||
|
this.balance = payResult.balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发起支付失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 JSAPI 预支付下单
|
||||||
|
*/
|
||||||
|
async mpPrepay(id: number, openid: string): Promise<MpPrepayResult> {
|
||||||
|
try {
|
||||||
|
return await mpPrepay(id, { openid });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('小程序预支付失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付成功确认
|
||||||
|
*/
|
||||||
|
async mpConfirm(
|
||||||
|
subscriptionNo: string,
|
||||||
|
transactionId?: string
|
||||||
|
): Promise<PayResult> {
|
||||||
|
try {
|
||||||
|
const result = await mpConfirm(subscriptionNo, { transactionId });
|
||||||
|
// 支付确认后订阅状态变化,失效缓存
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('小程序支付确认失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 订阅管理
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续费
|
||||||
|
* @param id 订阅ID
|
||||||
|
* @param period 周期:month/year
|
||||||
|
*/
|
||||||
|
async renew(id: number, period: 'month' | 'year' = 'month'): Promise<string> {
|
||||||
|
try {
|
||||||
|
const msg = await renewSubscription(id, period);
|
||||||
|
this.invalidateCache();
|
||||||
|
return msg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('续费失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退订/取消
|
||||||
|
*/
|
||||||
|
async cancel(id: number): Promise<string> {
|
||||||
|
try {
|
||||||
|
const msg = await cancelSubscription(id);
|
||||||
|
this.invalidateCache();
|
||||||
|
return msg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('退订失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/禁用
|
||||||
|
*/
|
||||||
|
async toggleEnable(id: number, enabled: boolean): Promise<string> {
|
||||||
|
try {
|
||||||
|
const msg = await toggleEnable(id, enabled);
|
||||||
|
this.invalidateCache();
|
||||||
|
return msg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启用/禁用失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 缓存控制
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失效缓存(仅重置时间标记,保留已有数据,下次拉取时更新)
|
||||||
|
* 用于订阅/支付/管理操作后,确保下次查询获取最新数据
|
||||||
|
*/
|
||||||
|
invalidateCache() {
|
||||||
|
this.lastUpdateTime = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存(清空数据并重置时间标记)
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.subscriptionList = [];
|
||||||
|
this.total = 0;
|
||||||
|
this.currentSubscription = null;
|
||||||
|
this.lastUpdateTime = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制刷新订阅列表
|
||||||
|
*/
|
||||||
|
async refresh() {
|
||||||
|
return await this.fetchMySubscriptions({ page: 1, limit: 10 }, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存有效期
|
||||||
|
*/
|
||||||
|
setCacheExpiry(expiry: number) {
|
||||||
|
this.cacheExpiry = expiry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -90,9 +90,9 @@ export const useStatisticsStore = defineStore('statistics', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取今日使用优惠券数量
|
* 获取今日使用优惠券数量(安全取值)
|
||||||
*/
|
*/
|
||||||
couponUsedCount: (state): number => {
|
safeCouponUsedCount: (state): number => {
|
||||||
return safeNumber(state.couponUsedCount);
|
return safeNumber(state.couponUsedCount);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export function getPayType(index?: number): any {
|
|||||||
{
|
{
|
||||||
value: 8,
|
value: 8,
|
||||||
label: '货到付款'
|
label: '货到付款'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: '线下付款'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
// 系统信息
|
// 系统信息
|
||||||
const systemInfo = ref({
|
const systemInfo = ref({
|
||||||
name: '小程序开发',
|
name: '小程序商城',
|
||||||
description:
|
description:
|
||||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
|
|||||||
@@ -213,11 +213,14 @@
|
|||||||
import { openNew } from '@/utils/common';
|
import { openNew } from '@/utils/common';
|
||||||
import { useSiteStore } from '@/store/modules/site';
|
import { useSiteStore } from '@/store/modules/site';
|
||||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
import { getTenantInfo } from '@/api/layout';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
// 使用状态管理
|
// 使用状态管理
|
||||||
const siteStore = useSiteStore();
|
const siteStore = useSiteStore();
|
||||||
const statisticsStore = useStatisticsStore();
|
const statisticsStore = useStatisticsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// 从 store 中获取响应式数据
|
// 从 store 中获取响应式数据
|
||||||
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
||||||
@@ -225,7 +228,7 @@
|
|||||||
|
|
||||||
// 系统信息
|
// 系统信息
|
||||||
const systemInfo = ref({
|
const systemInfo = ref({
|
||||||
name: '小程序开发',
|
name: '小程序商城',
|
||||||
description:
|
description:
|
||||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
@@ -238,7 +241,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const runDays = computed(() => siteStore.runDays);
|
const now = ref(Date.now());
|
||||||
|
const tenantCreateTime = ref<string>('');
|
||||||
|
let runDaysTimer: ReturnType<typeof setInterval>;
|
||||||
|
const runDays = computed(() => {
|
||||||
|
if (!tenantCreateTime.value) return 0;
|
||||||
|
return Math.floor((now.value - new Date(tenantCreateTime.value).getTime()) / (24 * 60 * 60 * 1000));
|
||||||
|
});
|
||||||
const userCount = computed(() => statisticsStore.userCount);
|
const userCount = computed(() => statisticsStore.userCount);
|
||||||
const orderCount = computed(() => statisticsStore.orderCount);
|
const orderCount = computed(() => statisticsStore.orderCount);
|
||||||
const totalSales = computed(() => statisticsStore.totalSales);
|
const totalSales = computed(() => statisticsStore.totalSales);
|
||||||
@@ -248,6 +257,15 @@
|
|||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
// 独立请求租户信息,不受其他请求失败影响
|
||||||
|
getTenantInfo()
|
||||||
|
.then((res) => {
|
||||||
|
tenantCreateTime.value = res?.createTime || '';
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('获取租户信息失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
siteStore.fetchSiteInfo(),
|
siteStore.fetchSiteInfo(),
|
||||||
@@ -267,11 +285,14 @@
|
|||||||
await loadData();
|
await loadData();
|
||||||
// 开始自动刷新统计数据(每5分钟)
|
// 开始自动刷新统计数据(每5分钟)
|
||||||
statisticsStore.startAutoRefresh();
|
statisticsStore.startAutoRefresh();
|
||||||
|
// 运行天数每小时检查一次(跨天自动更新)
|
||||||
|
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时停止自动刷新
|
// 组件卸载时停止自动刷新
|
||||||
statisticsStore.stopAutoRefresh();
|
statisticsStore.stopAutoRefresh();
|
||||||
|
clearInterval(runDaysTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
<div class="shop-dashboard">
|
<div class="shop-dashboard">
|
||||||
<!-- 欢迎横幅 -->
|
<!-- 欢迎横幅 -->
|
||||||
<div class="welcome-banner">
|
<div class="welcome-banner">
|
||||||
<div class="welcome-left">
|
<div class="welcome-left flex flex-row gap-2 items-center">
|
||||||
<h2 class="welcome-title">🏸 {{ userStore.info?.tenantName }}</h2>
|
<a-avatar
|
||||||
<p class="welcome-sub">欢迎回来,管理员,今日数据已更新</p>
|
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>
|
||||||
<div class="welcome-right">
|
<div class="welcome-right">
|
||||||
<div class="welcome-actions">
|
<div class="welcome-actions">
|
||||||
@@ -147,7 +156,7 @@
|
|||||||
<div style="padding: 16px 18px;">
|
<div style="padding: 16px 18px;">
|
||||||
<a-descriptions :column="1" size="small">
|
<a-descriptions :column="1" size="small">
|
||||||
<a-descriptions-item label="系统名称">
|
<a-descriptions-item label="系统名称">
|
||||||
{{ siteStore.appName }}
|
{{ systemInfo.name }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="版本号">
|
<a-descriptions-item label="版本号">
|
||||||
{{ systemInfo.version }}
|
{{ systemInfo.version }}
|
||||||
@@ -156,10 +165,10 @@
|
|||||||
{{ siteStore.statusText || '正常' }}
|
{{ siteStore.statusText || '正常' }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="创建时间">
|
<a-descriptions-item label="创建时间">
|
||||||
{{ siteInfo?.createTime }}
|
{{ tenantStore.company?.createTime || '-' }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="到期时间">
|
<a-descriptions-item label="到期时间">
|
||||||
{{ siteInfo?.expirationTime }}
|
{{ siteInfo?.expirationTime || '-' }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="系统运行">
|
<a-descriptions-item label="系统运行">
|
||||||
{{ runDays }} 天
|
{{ runDays }} 天
|
||||||
@@ -180,27 +189,30 @@
|
|||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
block
|
block
|
||||||
@click="navigateTo('/website/field')"
|
@click="navigateTo('/shop/shopOrder')"
|
||||||
:loading="loading"
|
|
||||||
>
|
>
|
||||||
<UngroupOutlined />
|
<ShoppingCartOutlined />
|
||||||
参数配置
|
|
||||||
</a-button>
|
|
||||||
<a-button block @click="navigateTo('/shop/shopOrder')">
|
|
||||||
<CalendarOutlined />
|
|
||||||
订单管理
|
订单管理
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button block @click="navigateTo('/system/user')">
|
<a-button block @click="navigateTo('/shop/shopGoods')">
|
||||||
<UserOutlined />
|
|
||||||
用户管理
|
|
||||||
</a-button>
|
|
||||||
<a-button block @click="navigateTo('/website/index')">
|
|
||||||
<ShopOutlined />
|
<ShopOutlined />
|
||||||
站点管理
|
商品管理
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button block @click="navigateTo('/system/login-record')">
|
<a-button block @click="navigateTo('/shop/shopGoodsCategory')">
|
||||||
<FileTextOutlined />
|
<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>
|
||||||
<a-button block @click="handleClearCache">
|
<a-button block @click="handleClearCache">
|
||||||
<ClearOutlined />
|
<ClearOutlined />
|
||||||
@@ -221,11 +233,12 @@ import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
|||||||
import {
|
import {
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
UngroupOutlined,
|
ShoppingCartOutlined,
|
||||||
CalendarOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
ShopOutlined,
|
ShopOutlined,
|
||||||
FileTextOutlined,
|
AppstoreOutlined,
|
||||||
|
GiftOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
SettingOutlined,
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
InfoCircleOutlined
|
InfoCircleOutlined
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
@@ -233,7 +246,10 @@ import { message } from 'ant-design-vue/es';
|
|||||||
import { useSiteStore } from '@/store/modules/site';
|
import { useSiteStore } from '@/store/modules/site';
|
||||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
import { useTenantStore } from '@/store/modules/tenant';
|
||||||
import { pageShopOrder } from '@/api/shop/shopOrder';
|
import { pageShopOrder } from '@/api/shop/shopOrder';
|
||||||
|
import { getShopSettingCategoryValues } from '@/api/shop/shopSetting';
|
||||||
|
import { getCompressedImageUrl } from '@/utils/image';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
|
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
|
||||||
|
|
||||||
@@ -241,6 +257,7 @@ import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
|
|||||||
const siteStore = useSiteStore();
|
const siteStore = useSiteStore();
|
||||||
const statisticsStore = useStatisticsStore();
|
const statisticsStore = useStatisticsStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const tenantStore = useTenantStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 从 store 中获取响应式数据
|
// 从 store 中获取响应式数据
|
||||||
@@ -257,7 +274,13 @@ const systemInfo = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const runDays = computed(() => siteStore.runDays);
|
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 userCount = computed(() => statisticsStore.userCount);
|
||||||
const orderCount = computed(() => statisticsStore.orderCount);
|
const orderCount = computed(() => statisticsStore.orderCount);
|
||||||
const totalSales = computed(() => statisticsStore.totalSales);
|
const totalSales = computed(() => statisticsStore.totalSales);
|
||||||
@@ -271,21 +294,29 @@ const loading = computed(() => siteLoading.value || statisticsLoading.value);
|
|||||||
// 核心统计数据(使用computed确保响应式更新)
|
// 核心统计数据(使用computed确保响应式更新)
|
||||||
const coreStats = computed(() => [
|
const coreStats = computed(() => [
|
||||||
{ icon: '🏸', label: '用户总数', value: userCount.value || 0, desc: '注册用户', color: 'blue', to: '/system/user' },
|
{ icon: '🏸', label: '用户总数', value: userCount.value || 0, desc: '注册用户', color: 'blue', to: '/system/user' },
|
||||||
{ icon: '📦', label: '订单总数', value: orderCount.value || 0, desc: '全部订单', color: 'orange', to: '/shop/shopOrder' },
|
{ 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' },
|
{ icon: '💰', label: '总营业额', value: totalSales.value || 0, desc: '元', color: 'purple', to: '/shop/shopOrder?tab=all' },
|
||||||
{ icon: '⏱️', label: '运行天数', value: runDays.value || 0, desc: '系统运行', color: 'green', to: '' },
|
{ icon: '⏱️', label: '运行天数', value: runDays.value || 0, desc: '系统运行', color: 'green', to: '' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 待处理事项数据
|
// 待处理事项数据
|
||||||
|
const pendingPaymentCount = ref(0);
|
||||||
const pendingShipmentCount = ref(0);
|
const pendingShipmentCount = ref(0);
|
||||||
const pendingRefundCount = ref(0);
|
const pendingRefundCount = ref(0);
|
||||||
const couponUsedCount = computed(() => statisticsStore.couponUsedCount);
|
const couponUsedCount = computed(() => statisticsStore.safeCouponUsedCount);
|
||||||
|
|
||||||
|
// 商城Logo
|
||||||
|
const shopLogo = ref('');
|
||||||
|
const shopLogoUrl = computed(() =>
|
||||||
|
shopLogo.value ? getCompressedImageUrl(shopLogo.value, { width: 200, quality: 90 }) : ''
|
||||||
|
);
|
||||||
|
|
||||||
// 待处理事项(使用computed确保响应式更新)
|
// 待处理事项(使用computed确保响应式更新)
|
||||||
const todoItems = computed(() => [
|
const todoItems = computed(() => [
|
||||||
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
|
{ label: '待付款订单', value: pendingPaymentCount.value, to: '/shop/shopOrder?tab=unpaid', tagColor: 'gold', dotColor: 'dot-gold', urgent: pendingPaymentCount.value > 0 },
|
||||||
{ label: '退款申请', value: pendingRefundCount.value, to: '/shop/shopOrder', tagColor: 'orange', dotColor: 'dot-orange', urgent: pendingRefundCount.value > 0 },
|
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder?tab=undelivered', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
|
||||||
// { label: '优惠券使用', value: couponUsedCount.value, to: '/market/coupon', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
|
{ 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确保响应式更新)
|
// 今日统计(使用computed确保响应式更新)
|
||||||
@@ -298,12 +329,12 @@ const todayStats = computed(() => ({
|
|||||||
|
|
||||||
// 快速入口
|
// 快速入口
|
||||||
const quickLinks = [
|
const quickLinks = [
|
||||||
{ to: '/website/field', icon: '⚙️', label: '参数配置', bg: '#eff6ff' },
|
{ to: '/shop/shopOrder?tab=all', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
|
||||||
{ to: '/shop/shopOrder', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
|
{ to: '/shop/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
|
||||||
{ to: '/system/user', icon: '👥', label: '用户管理', bg: '#fff7ed' },
|
{ to: '/shop/shopGoodsCategory', icon: '🗂️', label: '商品分类', bg: '#eff6ff' },
|
||||||
{ to: '/website/index', icon: '🏪', label: '站点管理', bg: '#faf5ff' },
|
{ to: '/market/coupon', icon: '🎁', label: '优惠券', bg: '#fff7ed' },
|
||||||
{ to: '/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
|
{ to: '/system/user', icon: '👥', label: '会员管理', bg: '#faf5ff' },
|
||||||
{ to: '/cmsArticle', icon: '📝', label: '文章管理', bg: '#fefce8' },
|
{ to: '/shop/shopSetting', icon: '⚙️', label: '商城设置', bg: '#fefce8' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 导航跳转
|
// 导航跳转
|
||||||
@@ -348,6 +379,20 @@ const refreshStatistics = async () => {
|
|||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
// 独立请求租户信息,不受其他请求失败影响
|
||||||
|
tenantStore.fetchTenantInfo().catch((e) => {
|
||||||
|
console.warn('获取租户信息失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 独立请求商城Logo,不受其他请求失败影响
|
||||||
|
getShopSettingCategoryValues('basic')
|
||||||
|
.then((values) => {
|
||||||
|
shopLogo.value = values?.shopLogo || '';
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('获取商城设置失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
siteStore.fetchSiteInfo(),
|
siteStore.fetchSiteInfo(),
|
||||||
@@ -356,8 +401,23 @@ const loadData = async () => {
|
|||||||
|
|
||||||
// 获取待处理事项数据
|
// 获取待处理事项数据
|
||||||
try {
|
try {
|
||||||
// 获取待发货订单数(使用 statusFilter=1 对应待发货)
|
// 获取待付款订单数(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({
|
const shipmentResult = await pageShopOrder({
|
||||||
|
type: 0,
|
||||||
statusFilter: 1,
|
statusFilter: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 1
|
limit: 1
|
||||||
@@ -369,8 +429,9 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取退款/售后订单数(使用 statusFilter=6 对应退款/售后)
|
// 获取退款/售后订单数(type=0商城订单, statusFilter=6退款/售后)
|
||||||
const refundResult = await pageShopOrder({
|
const refundResult = await pageShopOrder({
|
||||||
|
type: 0,
|
||||||
statusFilter: 6,
|
statusFilter: 6,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 1
|
limit: 1
|
||||||
@@ -391,11 +452,15 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 开始自动刷新统计数据(每5分钟)
|
// 开始自动刷新统计数据(每5分钟)
|
||||||
statisticsStore.startAutoRefresh();
|
statisticsStore.startAutoRefresh();
|
||||||
|
|
||||||
|
// 运行天数每小时检查一次(跨天自动更新)
|
||||||
|
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时停止自动刷新
|
// 组件卸载时停止自动刷新
|
||||||
statisticsStore.stopAutoRefresh();
|
statisticsStore.stopAutoRefresh();
|
||||||
|
clearInterval(runDaysTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -419,6 +484,11 @@ onUnmounted(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
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-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-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
|
||||||
|
|
||||||
@@ -485,6 +555,7 @@ onUnmounted(() => {
|
|||||||
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.dot-orange { background: #f97316; }
|
.dot-orange { background: #f97316; }
|
||||||
.dot-blue { background: #3b82f6; }
|
.dot-blue { background: #3b82f6; }
|
||||||
|
.dot-gold { background: #faad14; }
|
||||||
.dot-cyan { background: #06b6d4; }
|
.dot-cyan { background: #06b6d4; }
|
||||||
.dot-purple { background: #a855f7; }
|
.dot-purple { background: #a855f7; }
|
||||||
.todo-content { flex: 1; display: flex; align-items: center; }
|
.todo-content { flex: 1; display: flex; align-items: center; }
|
||||||
|
|||||||
@@ -71,9 +71,11 @@
|
|||||||
label="支付状态"
|
label="支付状态"
|
||||||
:labelStyle="{ width: '90px', color: '#808080' }"
|
:labelStyle="{ width: '90px', color: '#808080' }"
|
||||||
>
|
>
|
||||||
<a-tag v-if="form.payStatus == 1 && form.payType !== 8" color="green">已付款</a-tag>
|
<a-tag v-if="form.payStatus == 1 && form.payType !== 8 && form.payType !== 9" color="green">已付款</a-tag>
|
||||||
<a-tag v-if="form.payStatus == 1 && form.payType === 8" color="blue">待收货付款</a-tag>
|
<a-tag v-if="form.payStatus == 1 && form.payType === 8" color="blue">待收货付款</a-tag>
|
||||||
<a-tag v-if="form.payStatus == 0">未付款</a-tag>
|
<a-tag v-if="form.payStatus == 1 && form.payType === 9" color="green">已确认收款</a-tag>
|
||||||
|
<a-tag v-if="form.payStatus == 0 && form.payType === 9" color="orange">待确认收款</a-tag>
|
||||||
|
<a-tag v-if="form.payStatus == 0 && form.payType !== 9">未付款</a-tag>
|
||||||
<a-tag v-if="form.payStatus == 3">未付款,占场中</a-tag>
|
<a-tag v-if="form.payStatus == 3">未付款,占场中</a-tag>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<!-- 第四排-->
|
<!-- 第四排-->
|
||||||
@@ -147,7 +149,7 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-if="form.payType == 9">
|
<a-tag v-if="form.payType == 9">
|
||||||
<IdcardOutlined class="tag-icon" />
|
<IdcardOutlined class="tag-icon" />
|
||||||
IC月卡
|
线下付款
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-if="form.payType == 10">
|
<a-tag v-if="form.payType == 10">
|
||||||
<IdcardOutlined class="tag-icon" />
|
<IdcardOutlined class="tag-icon" />
|
||||||
@@ -187,7 +189,16 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="text-gray-400">未支付</span>
|
<!-- 线下付款未确认时也显示支付方式 -->
|
||||||
|
<a-tag v-if="form.payType == 9">
|
||||||
|
<IdcardOutlined class="tag-icon" />
|
||||||
|
线下付款
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-if="form.payType == 8">
|
||||||
|
<IdcardOutlined class="tag-icon" />
|
||||||
|
货到付款
|
||||||
|
</a-tag>
|
||||||
|
<span v-if="form.payType !== 9 && form.payType !== 8" class="text-gray-400">未支付</span>
|
||||||
</template>
|
</template>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
@@ -235,7 +246,7 @@
|
|||||||
<template v-if="column.key === 'goodsName'">
|
<template v-if="column.key === 'goodsName'">
|
||||||
<div style="display: flex; align-items: center; gap: 12px">
|
<div style="display: flex; align-items: center; gap: 12px">
|
||||||
<a-avatar
|
<a-avatar
|
||||||
:src="record.image || record.goodsImage"
|
:src="getCompressedImageUrl(record.image, { width: 100 })"
|
||||||
shape="square"
|
shape="square"
|
||||||
:size="50"
|
:size="50"
|
||||||
style="flex-shrink: 0"
|
style="flex-shrink: 0"
|
||||||
@@ -414,6 +425,7 @@
|
|||||||
import { updateShopOrder, removeShopOrder, refundShopOrder } from '@/api/shop/shopOrder';
|
import { updateShopOrder, removeShopOrder, refundShopOrder } from '@/api/shop/shopOrder';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
import DeliveryModal from './deliveryModal.vue';
|
import DeliveryModal from './deliveryModal.vue';
|
||||||
|
import {getCompressedImageUrl} from "@/utils/image";
|
||||||
|
|
||||||
const useForm = Form.useForm;
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
|
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
|
||||||
<a-tab-pane key="all" tab="全部" />
|
<a-tab-pane key="all" tab="全部" />
|
||||||
|
<a-tab-pane key="unpaid" tab="待付款" />
|
||||||
<a-tab-pane key="undelivered" tab="待发货" />
|
<a-tab-pane key="undelivered" tab="待发货" />
|
||||||
<a-tab-pane key="unreceived" tab="待收货" />
|
<a-tab-pane key="unreceived" tab="待收货" />
|
||||||
<a-tab-pane key="completed" tab="已完成" />
|
<a-tab-pane key="completed" tab="已完成" />
|
||||||
<!-- <a-tab-pane key="unpaid" tab="待付款" />-->
|
|
||||||
<a-tab-pane key="refunded" tab="退货/售后" />
|
<a-tab-pane key="refunded" tab="退货/售后" />
|
||||||
<a-tab-pane key="cancelled" tab="已关闭" />
|
<a-tab-pane key="cancelled" tab="已关闭" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@@ -93,10 +93,15 @@
|
|||||||
v-if="record.payType === 8"
|
v-if="record.payType === 8"
|
||||||
color="blue"
|
color="blue"
|
||||||
>货到付款</a-tag>
|
>货到付款</a-tag>
|
||||||
|
<!-- 线下付款标识 -->
|
||||||
|
<a-tag
|
||||||
|
v-if="record.payType === 9"
|
||||||
|
color="orange"
|
||||||
|
>线下付款</a-tag>
|
||||||
|
|
||||||
<!-- 支付状态 -->
|
<!-- 支付状态 -->
|
||||||
<a-tag
|
<a-tag
|
||||||
v-if="record.payStatus == 1 && record.payType !== 8"
|
v-if="record.payStatus == 1 && record.payType !== 8 && record.payType !== 9"
|
||||||
color="green"
|
color="green"
|
||||||
@click.stop="updatePayStatus(record)"
|
@click.stop="updatePayStatus(record)"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@@ -109,6 +114,20 @@
|
|||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>待收货付款</a-tag
|
>待收货付款</a-tag
|
||||||
>
|
>
|
||||||
|
<a-tag
|
||||||
|
v-else-if="record.payStatus == 1 && record.payType === 9"
|
||||||
|
color="green"
|
||||||
|
@click.stop="updatePayStatus(record)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>已确认收款</a-tag
|
||||||
|
>
|
||||||
|
<a-tag
|
||||||
|
v-else-if="record.payStatus == 0 && record.payType === 9"
|
||||||
|
color="orange"
|
||||||
|
@click.stop="updatePayStatus(record)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>待确认收款</a-tag
|
||||||
|
>
|
||||||
<a-tag
|
<a-tag
|
||||||
v-else-if="record.payStatus == 0 || record.payStatus == null"
|
v-else-if="record.payStatus == 0 || record.payStatus == null"
|
||||||
@click.stop="updatePayStatus(record)"
|
@click.stop="updatePayStatus(record)"
|
||||||
@@ -174,7 +193,7 @@
|
|||||||
<template v-for="(item, index) in record.orderGoods" :key="index">
|
<template v-for="(item, index) in record.orderGoods" :key="index">
|
||||||
<div class="item py-1">
|
<div class="item py-1">
|
||||||
<a-space :id="`g-${index}`">
|
<a-space :id="`g-${index}`">
|
||||||
<a-avatar :src="getCompressedImageUrl(item.image)" shape="square" :size="80" />
|
<a-avatar :src="getCompressedImageUrl(item.image,{ width: 100 })" shape="square" :size="50" />
|
||||||
<span>{{ item.goodsName }}</span>
|
<span>{{ item.goodsName }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,6 +204,10 @@
|
|||||||
<template v-if="record.payType === 8">
|
<template v-if="record.payType === 8">
|
||||||
<a-tag color="blue">货到付款</a-tag>
|
<a-tag color="blue">货到付款</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 线下付款特殊标识 -->
|
||||||
|
<template v-else-if="record.payType === 9">
|
||||||
|
<a-tag color="orange">线下付款</a-tag>
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="item in getPayType()">
|
<template v-for="item in getPayType()">
|
||||||
<template v-if="record.payStatus == 1">
|
<template v-if="record.payStatus == 1">
|
||||||
@@ -230,8 +253,8 @@
|
|||||||
<!-- 查看详情 - 所有状态都可以查看 -->
|
<!-- 查看详情 - 所有状态都可以查看 -->
|
||||||
<a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
|
<a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
|
||||||
|
|
||||||
<!-- 未付款状态的操作 -->
|
<!-- 未付款状态的操作(排除线下付款) -->
|
||||||
<template v-if="!record.payStatus && record.orderStatus === 0">
|
<template v-if="!record.payStatus && record.orderStatus === 0 && record.payType !== 9">
|
||||||
<a @click.stop="handleEditOrder(record)">
|
<a @click.stop="handleEditOrder(record)">
|
||||||
<EditOutlined /> 修改
|
<EditOutlined /> 修改
|
||||||
</a>
|
</a>
|
||||||
@@ -240,6 +263,16 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 线下付款·待确认收款状态的操作 -->
|
||||||
|
<template v-if="!record.payStatus && record.orderStatus === 0 && record.payType === 9">
|
||||||
|
<a @click.stop="handleConfirmOfflinePayment(record)" class="ele-text-success">
|
||||||
|
<CheckCircleOutlined /> 确认收款
|
||||||
|
</a>
|
||||||
|
<a @click.stop="handleCancelOrder(record)">
|
||||||
|
<span class="ele-text-warning"> <CloseOutlined /> 关闭 </span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 已付款未发货状态的操作 -->
|
<!-- 已付款未发货状态的操作 -->
|
||||||
<template
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
@@ -341,6 +374,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createVNode, ref } from 'vue';
|
import { createVNode, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import type { EleProTable } from 'ele-admin-pro';
|
import type { EleProTable } from 'ele-admin-pro';
|
||||||
import type {
|
import type {
|
||||||
DatasourceFunction,
|
DatasourceFunction,
|
||||||
@@ -371,7 +405,8 @@
|
|||||||
repairOrder,
|
repairOrder,
|
||||||
removeShopOrder,
|
removeShopOrder,
|
||||||
removeBatchShopOrder,
|
removeBatchShopOrder,
|
||||||
updateShopOrder, refundShopOrder
|
updateShopOrder, refundShopOrder,
|
||||||
|
confirmOfflinePayment
|
||||||
} from '@/api/shop/shopOrder';
|
} from '@/api/shop/shopOrder';
|
||||||
import { updateUser } from '@/api/system/user';
|
import { updateUser } from '@/api/system/user';
|
||||||
import { getPayType } from '@/utils/shop';
|
import { getPayType } from '@/utils/shop';
|
||||||
@@ -394,13 +429,35 @@
|
|||||||
const showDelivery = ref(false);
|
const showDelivery = ref(false);
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
// 激活的标签
|
// 激活的标签(支持从路由参数初始化,如 /shop/shopOrder?tab=all)
|
||||||
const activeKey = ref<string>('undelivered');
|
const route = useRoute();
|
||||||
|
const validTabs = ['all', 'unpaid', 'undelivered', 'unreceived', 'completed', 'refunded', 'cancelled'];
|
||||||
|
const tabFromQuery = route.query.tab as string;
|
||||||
|
const activeKey = ref<string>(
|
||||||
|
validTabs.includes(tabFromQuery) ? tabFromQuery : 'undelivered'
|
||||||
|
);
|
||||||
|
|
||||||
// ============ 新订单提醒 ============
|
// ============ 新订单提醒 ============
|
||||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||||
onNewOrder: () => reload()
|
onNewOrder: () => reload()
|
||||||
});
|
});
|
||||||
|
// 根据tab key获取对应的statusFilter值
|
||||||
|
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除,8已关闭
|
||||||
|
const getStatusFilterByTab = (key: string): number | undefined => {
|
||||||
|
const filterMap: Record<string, number> = {
|
||||||
|
unpaid: 0,
|
||||||
|
undelivered: 1,
|
||||||
|
unverified: 2,
|
||||||
|
unreceived: 3,
|
||||||
|
unevaluated: 4,
|
||||||
|
completed: 5,
|
||||||
|
refunded: 6,
|
||||||
|
deleted: 7,
|
||||||
|
cancelled: 8
|
||||||
|
};
|
||||||
|
return filterMap[key];
|
||||||
|
};
|
||||||
|
|
||||||
// 表格数据源
|
// 表格数据源
|
||||||
const datasource: DatasourceFunction = ({
|
const datasource: DatasourceFunction = ({
|
||||||
page,
|
page,
|
||||||
@@ -413,6 +470,10 @@
|
|||||||
where.status = filters.status;
|
where.status = filters.status;
|
||||||
}
|
}
|
||||||
where.type = 0;
|
where.type = 0;
|
||||||
|
// 确保初次加载时也按当前tab筛选(statusFilter未设置时从activeKey推导)
|
||||||
|
if (where.statusFilter === undefined) {
|
||||||
|
where.statusFilter = getStatusFilterByTab(activeKey.value);
|
||||||
|
}
|
||||||
return pageShopOrder({
|
return pageShopOrder({
|
||||||
...where,
|
...where,
|
||||||
...orders,
|
...orders,
|
||||||
@@ -493,54 +554,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onTabs = () => {
|
const onTabs = () => {
|
||||||
// 使用statusFilter进行筛选,这是后端专门为订单状态筛选设计的字段
|
|
||||||
const filterParams: Record<string, any> = {};
|
const filterParams: Record<string, any> = {};
|
||||||
|
const sf = getStatusFilterByTab(activeKey.value);
|
||||||
// 根据后端 statusFilter 的值对应:
|
if (sf !== undefined) {
|
||||||
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除
|
filterParams.statusFilter = sf;
|
||||||
switch (activeKey.value) {
|
|
||||||
case 'all':
|
|
||||||
// 全部订单:不传statusFilter参数
|
|
||||||
// filterParams.statusFilter = undefined; // 不设置该字段
|
|
||||||
break;
|
|
||||||
case 'unpaid':
|
|
||||||
// 待付款:pay_status = false
|
|
||||||
filterParams.statusFilter = 0;
|
|
||||||
break;
|
|
||||||
case 'undelivered':
|
|
||||||
// 待发货:pay_status = true AND delivery_status = 10
|
|
||||||
filterParams.statusFilter = 1;
|
|
||||||
break;
|
|
||||||
case 'unverified':
|
|
||||||
// 待核销:pay_status = true AND delivery_status = 10 (与待发货相同)
|
|
||||||
filterParams.statusFilter = 2;
|
|
||||||
break;
|
|
||||||
case 'unreceived':
|
|
||||||
// 待收货:pay_status = true AND delivery_status = 20
|
|
||||||
filterParams.statusFilter = 3;
|
|
||||||
break;
|
|
||||||
case 'unevaluated':
|
|
||||||
// 待评价:order_status = 1 (与已完成相同)
|
|
||||||
filterParams.statusFilter = 4;
|
|
||||||
break;
|
|
||||||
case 'completed':
|
|
||||||
// 已完成:order_status = 1
|
|
||||||
filterParams.statusFilter = 5;
|
|
||||||
break;
|
|
||||||
case 'cancelled':
|
|
||||||
// 已关闭:order_status = 2
|
|
||||||
filterParams.statusFilter = 8;
|
|
||||||
break;
|
|
||||||
case 'refunded':
|
|
||||||
// 退款/售后:order_status = 6
|
|
||||||
filterParams.statusFilter = 6;
|
|
||||||
break;
|
|
||||||
case 'deleted':
|
|
||||||
// 已删除:deleted = 1
|
|
||||||
filterParams.statusFilter = 7;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload(filterParams);
|
reload(filterParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -627,6 +645,35 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确认线下付款收款
|
||||||
|
const handleConfirmOfflinePayment = (record: ShopOrder) => {
|
||||||
|
let remarksValue = '';
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认线下收款',
|
||||||
|
content: createVNode('div', null, [
|
||||||
|
createVNode('p', { style: 'margin-bottom: 8px' }, '确认已收到该订单的线下付款(微信转账/银行汇款等)?'),
|
||||||
|
createVNode('p', { style: 'margin-bottom: 8px; color: #999; font-size: 12px' }, `订单号:${record.orderNo}`),
|
||||||
|
createVNode('input', {
|
||||||
|
id: 'offline-remarks-input',
|
||||||
|
placeholder: '可填写备注(如:微信转账已收到)',
|
||||||
|
style: 'width: 100%; padding: 4px 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
|
||||||
|
onInput: (e: any) => { remarksValue = e.target.value; }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
okText: '确认收款',
|
||||||
|
okType: 'success',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await confirmOfflinePayment(record.orderId!, remarksValue || undefined);
|
||||||
|
message.success('确认收款成功,订单已进入待发货状态');
|
||||||
|
reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '确认收款失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 发货处理
|
// 发货处理
|
||||||
const handleDelivery = (record: ShopOrder) => {
|
const handleDelivery = (record: ShopOrder) => {
|
||||||
current.value = record;
|
current.value = record;
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<a-tab-pane tab="分销设置" key="dealer">
|
<a-tab-pane tab="分销设置" key="dealer">
|
||||||
<Dealer />
|
<Dealer />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane tab="支付设置" key="payment">
|
<!-- <a-tab-pane tab="支付设置" key="payment">-->
|
||||||
<Payment />
|
<!-- <Payment />-->
|
||||||
</a-tab-pane>
|
<!-- </a-tab-pane>-->
|
||||||
<a-tab-pane tab="通知设置" key="notify">
|
<a-tab-pane tab="通知设置" key="notify">
|
||||||
<Notify />
|
<Notify />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
@@ -43,7 +43,7 @@ import Basic from './components/basic.vue';
|
|||||||
import Order from './components/order.vue';
|
import Order from './components/order.vue';
|
||||||
import Points from './components/points.vue';
|
import Points from './components/points.vue';
|
||||||
import Dealer from './components/dealer.vue';
|
import Dealer from './components/dealer.vue';
|
||||||
import Payment from './components/payment.vue';
|
// import Payment from './components/payment.vue';
|
||||||
import Notify from './components/notify.vue';
|
import Notify from './components/notify.vue';
|
||||||
import Upload from './components/upload.vue';
|
import Upload from './components/upload.vue';
|
||||||
import Sms from './components/sms.vue';
|
import Sms from './components/sms.vue';
|
||||||
|
|||||||
Reference in New Issue
Block a user