feat(shopOrder): 实现电脑版后台全局新订单红点提示
- 新建模块级全局单例 useOrderNotify,统一轮询检测新订单,避免多组件重复请求 - 轮询检测到新订单时触发叮声、语音播报,通知所有已注册回调刷新数据 - 订单管理菜单及 dashboard 快速入口显示红点提示新订单未查看 - 进入订单列表页时调用 clearBadge 同步最新订单ID并清除红点,标记订单已查看 - layout 全局启动新订单提醒,任意页面均生效 - menu-title 组件新增纯红点(dot)徽章显示支持 - dashboard 快速入口图标加红点显示及脉冲动画,提升视觉提示效果 - 维护新订单提醒回调集合,组件卸载自动移除回调避免内存泄漏 - 新订单提醒配置项和状态均为模块共享响应式,支持多组件无冲突使用
This commit is contained in:
@@ -113,3 +113,19 @@
|
||||
- 引入 `getCompressedImageUrl`(来自 `src/utils/image.ts`),将商品图片 `:src="record.goodsImage"` 改为 `:src="getCompressedImageUrl(record.goodsImage)"`。
|
||||
- 与 `shopGoods`、`shopOrder`、`dashboard` 等页面保持一致:默认宽度 240px、质量 90、自动补全 OSS 域名、跳过已含 `x-oss-process` 的 URL。
|
||||
- 预览行为未改(仍走 a-image 默认 src 预览)。
|
||||
|
||||
## 电脑版后台新订单红点提示(全局)
|
||||
|
||||
- 需求:电脑版后台有新订单时显示红点,让管理员知道有新订单。
|
||||
- 关键发现:系统已有菜单红点基础设施(`menu-title.vue` 读 `item.meta.badge` 渲染徽章;`user.ts` `setMenuBadge(path,value,color)`;`layout/index.vue` 有 `.ele-menu-badge` 样式),但 `useOrderNotify` 原为组件级(dashboard/cms dashboard/shopOrder 三处各自独立轮询),未接通红点。
|
||||
- 改造 `src/views/shop/shopOrder/useOrderNotify.ts` 为**模块级全局单例**:
|
||||
- 轮询/状态/红点全部模块级变量(enabled、hasNewOrder、lastOrderId、timer),多组件调用只一个轮询。
|
||||
- 检测到新订单 → triggerNotify(叮声+语音+遍历回调Set)+ `setOrderBadge(true)` 调 `userStore.setMenuBadge('/shop/shopOrder','dot','#ff4d4f')`。
|
||||
- 新增 `clearBadge()`:立即清红点 + 重查最新订单同步 lastOrderId(标记"已查看")。
|
||||
- `onNewOrder` 回调用 Set 管理,组件 onBeforeUnmount 自动移除;`start()` 幂等,由 layout 调一次。
|
||||
- `src/layout/components/menu-title.vue`:`badge==='dot'` 时用 `<a-badge dot />` 纯红点,否则原数字徽章逻辑。
|
||||
- `src/layout/index.vue`:setup 调 `useOrderNotify().start()` 全局启动,后台任意页面生效。
|
||||
- `src/views/shop/shopOrder/index.vue`:`onMounted` + `onActivated` 调 `clearBadge()`(兼容 keep-alive `cache-key`,用 `orderBadgeMounted` 标志防首次双触发)。
|
||||
- `src/views/shop/dashboard/index.vue`:`quickLinks` 改 computed,订单管理项 `badge: hasNewOrder.value`;`quick-icon` 加红点 span + pulse 动画样式。
|
||||
- 决策(用户确认):红点=新订单未查看(进订单页清除);位置=订单管理菜单+dashboard快速入口订单管理图标;声音保留并全局化。
|
||||
- 构建验证:`vite build --outDir dist_verify` 通过(EXIT:0)。原 `pnpm build` 失败仅因 vite 清空 `dist/assets` 被安全删除保护拦截(83文件>50阈值),与代码无关。
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
|
||||
<!-- dot 模式:纯红点,不显示数字(用于新订单提醒等) -->
|
||||
<a-badge v-if="item.meta.badge === 'dot'" dot />
|
||||
<!-- 数字徽章模式 -->
|
||||
<a-badge
|
||||
v-else
|
||||
:count="item.meta.badge"
|
||||
:number-style="{ background: item.meta.badgeColor as string }"
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
import HeaderTools from './components/header-tools.vue';
|
||||
import PageFooter from './components/page-footer.vue';
|
||||
import MenuTitle from './components/menu-title.vue';
|
||||
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
||||
import {
|
||||
HIDE_SIDEBARS,
|
||||
HIDE_FOOTERS,
|
||||
@@ -127,6 +128,11 @@
|
||||
const { t, locale } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 全局启动新订单提醒:任意页面都会轮询检测新订单,
|
||||
// 检测到后在「订单管理」菜单显示红点 + 叮声/语音播报
|
||||
const { start: startOrderNotify } = useOrderNotify();
|
||||
startOrderNotify();
|
||||
|
||||
// 是否刷新页面
|
||||
if (localStorage.getItem('Reload')) {
|
||||
window.location.reload();
|
||||
|
||||
@@ -101,7 +101,10 @@
|
||||
class="quick-card"
|
||||
@click="navigateTo(item.to)"
|
||||
>
|
||||
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
|
||||
<div class="quick-icon" :style="{ background: item.bg }">
|
||||
{{ item.icon }}
|
||||
<span v-if="item.badge" class="quick-icon-dot"></span>
|
||||
</div>
|
||||
<div class="quick-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,7 +293,6 @@
|
||||
<!-- 支付 Modal:微信扫码支付 -->
|
||||
<a-modal
|
||||
v-model:open="payModalVisible"
|
||||
title="微信扫码支付"
|
||||
:footer="null"
|
||||
:mask-closable="false"
|
||||
@cancel="onPayModalClose"
|
||||
@@ -644,15 +646,15 @@ const todayStats = computed(() => ({
|
||||
couponUsed: couponUsedCount.value || 0,
|
||||
}));
|
||||
|
||||
// 快速入口
|
||||
const quickLinks = [
|
||||
{ to: '/shop/shopOrder?tab=all', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
|
||||
// 快速入口(订单管理项随新订单状态显示红点)
|
||||
const quickLinks = computed(() => [
|
||||
{ to: '/shop/shopOrder?tab=all', icon: '📦', label: '订单管理', bg: '#f0fdf4', badge: hasNewOrder.value },
|
||||
{ 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) => {
|
||||
@@ -678,8 +680,8 @@ const handleClearCache = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 新订单声音提醒
|
||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||
// 新订单声音提醒(全局单例:轮询由 layout 启动,此处仅消费状态 + UI)
|
||||
const { enabled: notifyEnabled, hasNewOrder, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||
onNewOrder: () => loadData()
|
||||
});
|
||||
|
||||
@@ -903,6 +905,24 @@ onUnmounted(() => {
|
||||
.quick-icon {
|
||||
width: 44px; height: 44px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
||||
position: relative;
|
||||
}
|
||||
/* 快速入口新订单红点 */
|
||||
.quick-icon-dot {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #ff4d4f;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.3);
|
||||
animation: quick-dot-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes quick-dot-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.25); opacity: 0.85; }
|
||||
}
|
||||
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { createVNode, ref, onMounted, onActivated } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
@@ -447,9 +447,19 @@
|
||||
);
|
||||
|
||||
// ============ 新订单提醒 ============
|
||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify, clearBadge } = useOrderNotify({
|
||||
onNewOrder: () => reload()
|
||||
});
|
||||
// 进入订单列表页时清除菜单红点(标记已查看),并同步 lastOrderId 为当前最新订单
|
||||
// 兼容 keep-alive:onMounted 处理首次进入,onActivated 处理缓存再次激活
|
||||
let orderBadgeMounted = false;
|
||||
onMounted(() => {
|
||||
clearBadge();
|
||||
orderBadgeMounted = true;
|
||||
});
|
||||
onActivated(() => {
|
||||
if (orderBadgeMounted) clearBadge();
|
||||
});
|
||||
// 根据tab key获取对应的statusFilter值
|
||||
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除,8已关闭
|
||||
const getStatusFilterByTab = (key: string): number | undefined => {
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { ref, onBeforeUnmount } from 'vue';
|
||||
import { pageShopOrder } from '@/api/shop/shopOrder';
|
||||
import type { ShopOrder } from '@/api/shop/shopOrder/model';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
/**
|
||||
* 新订单提醒 - 轮询检测 + 声音/语音播报
|
||||
* 新订单提醒 - 全局单例轮询 + 红点徽章 + 声音/语音播报
|
||||
*
|
||||
* 原理:
|
||||
* 1. 定时轮询 pageShopOrder(只查最新 1 条)
|
||||
* 2. 与上次记录的 orderId 比较,不同则说明有新订单
|
||||
* 3. 播放"叮"声(Web Audio API) + 语音播报"您有一条新的订单"(Speech Synthesis API)
|
||||
* 4. 同时触发 onNewOrder 回调,通知调用方刷新页面数据
|
||||
* 设计要点:
|
||||
* 1. **全局单例**:轮询状态(timer、lastOrderId、enabled、hasNewOrder)全部为模块级变量,
|
||||
* 无论多少个组件调用 useOrderNotify(),始终只有一个轮询在运行,避免重复请求。
|
||||
* 2. **红点徽章**:检测到新订单时,调用 userStore.setMenuBadge 给「订单管理」菜单挂红点;
|
||||
* 进入订单列表页时调用 clearBadge() 清除,并同步 lastOrderId 为最新(标记“已查看”)。
|
||||
* 3. **声音/语音**:保留原 Web Audio 叮声 + Speech Synthesis 语音播报。
|
||||
* 4. **回调集合**:多组件可各自注册 onNewOrder 回调(如 dashboard 刷新统计、订单页刷新表格),
|
||||
* 检测到新订单时统一遍历触发。
|
||||
*
|
||||
* 浏览器策略:音频播放需要用户交互后才允许。
|
||||
* 默认开启轮询,音频在用户首次交互后自动解锁。
|
||||
*
|
||||
* **实例安全**:每次调用 useOrderNotify() 创建独立实例,
|
||||
* 多个页面可同时使用而不冲突。AudioContext 在模块级共享。
|
||||
* 生命周期:
|
||||
* - layout 组件挂载时调用 useOrderNotify() 并触发 start(),全局启动一次,常驻不停。
|
||||
* - dashboard / 订单列表页 / cms dashboard 调用 useOrderNotify() 仅消费状态与 UI,不重复启动。
|
||||
*/
|
||||
|
||||
// ============ 配置 ============
|
||||
@@ -32,6 +34,9 @@ const DING_DURATION = 0.3;
|
||||
/** 语音播报文案 */
|
||||
const SPEECH_TEXT = '您有一条新的订单';
|
||||
|
||||
/** 订单管理菜单路径(红点挂在这里) */
|
||||
const ORDER_MENU_PATH = '/shop/shopOrder';
|
||||
|
||||
// ============ 模块级共享状态(音频相关) ============
|
||||
|
||||
/** 音频上下文(模块级共享,延迟创建,需用户交互后) */
|
||||
@@ -40,6 +45,32 @@ let audioCtx: AudioContext | null = null;
|
||||
/** 用户交互解锁音频的监听器(模块级,全局只注册一次) */
|
||||
let unlockHandler: (() => void) | null = null;
|
||||
|
||||
// ============ 模块级单例状态(轮询 + 红点) ============
|
||||
|
||||
/** 是否开启提醒(所有组件共享同一开关) */
|
||||
const enabled = ref(true);
|
||||
|
||||
/** 是否有未查看的新订单(驱动红点 / 快速入口标记) */
|
||||
const hasNewOrder = ref(false);
|
||||
|
||||
/** 轮询是否进行中 */
|
||||
let polling = false;
|
||||
|
||||
/** 轮询定时器 */
|
||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/** 上次记录的最新订单 ID(用于判断是否有新订单) */
|
||||
let lastOrderId: number | undefined = undefined;
|
||||
|
||||
/** 是否已完成首次初始化(首次只记录基准,不触发提醒) */
|
||||
let initialized = false;
|
||||
|
||||
/** 是否已全局启动(避免重复 start) */
|
||||
let started = false;
|
||||
|
||||
/** 新订单回调集合(多组件注册,统一触发) */
|
||||
const newOrderCallbacks = new Set<() => void>();
|
||||
|
||||
// ============ 声音播放 ============
|
||||
|
||||
/**
|
||||
@@ -130,43 +161,52 @@ function speak(text: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发提醒:叮声 + 语音播报 + 通知回调
|
||||
* 触发提醒:叮声 + 语音播报 + 通知所有已注册回调
|
||||
*/
|
||||
function triggerNotify(onNewOrderCallback: (() => void) | null) {
|
||||
function triggerNotify() {
|
||||
initAudioContext();
|
||||
playDingSound();
|
||||
speak(SPEECH_TEXT);
|
||||
if (onNewOrderCallback) {
|
||||
onNewOrderCallback();
|
||||
// 触发所有已注册回调(刷新各页面数据)
|
||||
newOrderCallbacks.forEach((cb) => {
|
||||
try {
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.warn('[订单提醒] 回调执行失败:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 红点徽章控制 ============
|
||||
|
||||
/**
|
||||
* 设置/清除订单管理菜单的红点徽章
|
||||
* @param on true=显示红点,false=清除红点
|
||||
*/
|
||||
function setOrderBadge(on: boolean) {
|
||||
hasNewOrder.value = on;
|
||||
try {
|
||||
const userStore = useUserStore();
|
||||
if (on) {
|
||||
// 'dot' 表示纯红点(不显示数字),由 menu-title.vue 渲染为 a-badge dot
|
||||
userStore.setMenuBadge(ORDER_MENU_PATH, 'dot', '#ff4d4f');
|
||||
} else {
|
||||
// 传 undefined 清除徽章
|
||||
userStore.setMenuBadge(ORDER_MENU_PATH, undefined);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[订单提醒] 设置菜单红点失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 对外接口 ============
|
||||
// ============ 轮询逻辑 ============
|
||||
|
||||
/**
|
||||
* 新订单提醒组合式函数
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* const { enabled, toggle, testNotify } = useOrderNotify({
|
||||
* onNewOrder: () => reload()
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param options.onNewOrder 检测到新订单时的回调(通常用于刷新页面数据)
|
||||
* 检测是否有新订单
|
||||
* 只查最新 1 条,与 lastOrderId 比较:不同且更大则说明有新订单
|
||||
*/
|
||||
export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
// ============ 实例级状态 ============
|
||||
const enabled = ref(true);
|
||||
let polling = false;
|
||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||
let lastOrderId: number | undefined = undefined;
|
||||
let initialized = false;
|
||||
let onNewOrderCallback: (() => void) | null = options?.onNewOrder ?? null;
|
||||
|
||||
// ============ 轮询逻辑 ============
|
||||
|
||||
async function checkNewOrder() {
|
||||
async function checkNewOrder() {
|
||||
if (!enabled.value) return;
|
||||
try {
|
||||
const result = await pageShopOrder({
|
||||
page: 1,
|
||||
@@ -184,6 +224,7 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
const currentOrderId = latestOrder.orderId;
|
||||
|
||||
if (!initialized) {
|
||||
// 首次只记录基准,不触发提醒
|
||||
lastOrderId = currentOrderId;
|
||||
initialized = true;
|
||||
return;
|
||||
@@ -193,46 +234,98 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
currentOrderId !== lastOrderId &&
|
||||
(lastOrderId === undefined || (currentOrderId ?? 0) > lastOrderId)
|
||||
) {
|
||||
triggerNotify(onNewOrderCallback);
|
||||
// 有新订单:声音 + 语音 + 回调 + 红点
|
||||
triggerNotify();
|
||||
setOrderBadge(true);
|
||||
}
|
||||
|
||||
lastOrderId = currentOrderId;
|
||||
} catch (error) {
|
||||
console.warn('[订单提醒] 轮询查询失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
function startPolling() {
|
||||
if (polling) return;
|
||||
polling = true;
|
||||
initialized = false;
|
||||
|
||||
checkNewOrder();
|
||||
timerId = setInterval(checkNewOrder, POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
function stopPolling() {
|
||||
polling = false;
|
||||
if (timerId) {
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 全局启动 / 停止 ============
|
||||
|
||||
/**
|
||||
* 全局启动订单提醒(由 layout 组件调用一次即可)
|
||||
* 内部用 started 标志保证幂等,重复调用安全
|
||||
*/
|
||||
function start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
registerAudioUnlock();
|
||||
if (enabled.value) {
|
||||
startPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除红点并同步 lastOrderId 为当前最新订单(标记“已查看”)
|
||||
* 进入订单列表页时调用
|
||||
*/
|
||||
async function clearBadge() {
|
||||
// 立即清除红点(同步,体验即时)
|
||||
setOrderBadge(false);
|
||||
// 重新查询最新订单,把 lastOrderId 同步为最新,标记“已查看到此订单”
|
||||
// 这样后续只有比这个更新的订单才会再次触发红点
|
||||
try {
|
||||
const result = await pageShopOrder({ page: 1, limit: 1, type: 0 });
|
||||
const latest: ShopOrder | undefined = result?.list?.[0];
|
||||
lastOrderId = latest?.orderId;
|
||||
initialized = true;
|
||||
} catch (e) {
|
||||
console.warn('[订单提醒] 同步最新订单失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 对外接口 ============
|
||||
|
||||
/**
|
||||
* 新订单提醒组合式函数(全局单例)
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* // layout 全局启动(仅一次)
|
||||
* const { start } = useOrderNotify();
|
||||
* start();
|
||||
*
|
||||
* // dashboard / 订单页消费状态与 UI
|
||||
* const { enabled, hasNewOrder, toggle, testNotify, clearBadge } = useOrderNotify({
|
||||
* onNewOrder: () => reload()
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param options.onNewOrder 检测到新订单时的回调(组件卸载时自动移除)
|
||||
*/
|
||||
export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
// 注册回调(组件卸载时自动移除,避免内存泄漏)
|
||||
const cb = options?.onNewOrder;
|
||||
if (cb) {
|
||||
newOrderCallbacks.add(cb);
|
||||
onBeforeUnmount(() => {
|
||||
newOrderCallbacks.delete(cb);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 生命周期 ============
|
||||
|
||||
onMounted(() => {
|
||||
registerAudioUnlock();
|
||||
startPolling();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
onNewOrderCallback = null;
|
||||
});
|
||||
|
||||
// ============ 对外方法 ============
|
||||
|
||||
// 开关切换
|
||||
const toggle = (value: boolean) => {
|
||||
enabled.value = value;
|
||||
if (value) {
|
||||
@@ -240,17 +333,28 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
startPolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
setOrderBadge(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 测试提醒
|
||||
const testNotify = () => {
|
||||
initAudioContext();
|
||||
triggerNotify(onNewOrderCallback);
|
||||
triggerNotify();
|
||||
};
|
||||
|
||||
return {
|
||||
/** 是否开启提醒(响应式,所有组件共享) */
|
||||
enabled,
|
||||
/** 是否有未查看的新订单(响应式,驱动红点) */
|
||||
hasNewOrder,
|
||||
/** 开关切换 */
|
||||
toggle,
|
||||
testNotify
|
||||
/** 测试提醒(声音 + 语音) */
|
||||
testNotify,
|
||||
/** 清除红点并标记已查看(进入订单页时调用) */
|
||||
clearBadge,
|
||||
/** 全局启动(layout 调用一次) */
|
||||
start
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user