From 1be22664c4b1eb4f01296d6ac5bbe4c19fe3c153 Mon Sep 17 00:00:00 2001 From: zhangzijienbplus <17738440858@163.com> Date: Tue, 21 Oct 2025 11:33:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(subscription):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 trialExpiredType 类型,新增 'subscribe' 状态以支持主动订阅场景 - 新增 openSubscriptionDialog 方法,用于处理 VIP 状态点击事件 - 优化 VIP 状态卡片 UI,添加悬停与点击效果,提升交互体验 - 调整过期状态样式,保持水平布局并移除冗余按钮样式 - 在 Rakuten 组件中引入请求中断机制,提升任务控制灵活性- 更新 TrialExpiredDialog 组件,支持订阅类型提示与微信复制反馈- 修复部分 API 调用未传递 signal 参数的问题,增强请求管理能力 - 切换 Ruoyi 服务地址至生产环境配置,确保接口通信正常 - 移除部分无用代码与样式,精简组件结构 --- electron-vue-template/src/renderer/App.vue | 66 ++++++++----------- .../src/renderer/api/http.ts | 6 +- .../src/renderer/api/rakuten.ts | 8 +-- .../components/amazon/AmazonDashboard.vue | 4 +- .../components/common/TrialExpiredDialog.vue | 31 +++++++-- .../components/rakuten/RakutenDashboard.vue | 28 +++++--- .../components/zebra/ZebraDashboard.vue | 18 +---- .../src/main/resources/application.yml | 2 - .../system/ClientDeviceController.java | 1 - 9 files changed, 83 insertions(+), 81 deletions(-) diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index 0050cf6..d4b2f41 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -84,7 +84,7 @@ const showSettingsDialog = ref(false) // 试用期过期对话框 const showTrialExpiredDialog = ref(false) -const trialExpiredType = ref<'device' | 'account' | 'both'>('device') +const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('device') // 菜单配置 - 复刻ERP客户端格式 const menuConfig = [ @@ -310,7 +310,7 @@ async function refreshVipStatus() { } // 判断过期类型 -function checkExpiredType(): 'device' | 'account' | 'both' { +function checkExpiredType(): 'device' | 'account' | 'both' | 'subscribe' { const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value const deviceExpired = deviceTrialExpired.value @@ -320,6 +320,17 @@ function checkExpiredType(): 'device' | 'account' | 'both' { return 'account' // 默认 } +// 打开订阅对话框 +function openSubscriptionDialog() { + // 如果VIP有效,显示订阅/续费提示;如果已过期,显示过期提示 + if (vipStatus.value.isVip) { + trialExpiredType.value = 'subscribe' + } else { + trialExpiredType.value = checkExpiredType() + } + showTrialExpiredDialog.value = true +} + // 提供给子组件使用 provide('refreshVipStatus', refreshVipStatus) provide('checkExpiredType', checkExpiredType) @@ -519,22 +530,18 @@ onUnmounted(() => { -
+
-
有效期至:{{ vipExpireTime ? new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) : '-' }}
-
@@ -924,6 +931,17 @@ onUnmounted(() => { box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15); transition: all 0.3s ease; position: relative; + cursor: pointer; + user-select: none; +} + +.vip-status-card:hover { + box-shadow: 0 3px 10px rgba(255, 215, 0, 0.25); + transform: translateY(-1px); +} + +.vip-status-card:active { + transform: translateY(0); } /* 正常状态和警告状态 - 统一温暖金色渐变 */ @@ -934,15 +952,10 @@ onUnmounted(() => { box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15); } -/* 过期状态 - 灰色,垂直布局 */ +/* 过期状态 - 灰色,保持水平布局 */ .vip-status-card.vip-expired { background: linear-gradient(135deg, #FAFAFA 0%, #E8E8E8 100%); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); - flex-direction: column; - justify-content: space-between; - align-items: stretch; - padding: 10px; - gap: 8px; } .vip-info { @@ -970,33 +983,6 @@ onUnmounted(() => { opacity: 0.9; } -/* 右侧徽章按钮 */ -.vip-badge { - padding: 5px 10px; - border-radius: 5px; - font-size: 11px; - font-weight: 600; - white-space: nowrap; - flex-shrink: 0; - background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); - color: white; - box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3); -} - -/* 过期状态续费按钮 - 置底 */ -.vip-renew-btn { - padding: 7px 0; - text-align: center; - background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); - color: white; - border-radius: 5px; - font-size: 11px; - font-weight: 600; - width: 100%; - flex-shrink: 0; - box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3); -} - .vip-status-card.vip-expired .vip-info { align-items: flex-start; } diff --git a/electron-vue-template/src/renderer/api/http.ts b/electron-vue-template/src/renderer/api/http.ts index 4974d50..3cb2625 100644 --- a/electron-vue-template/src/renderer/api/http.ts +++ b/electron-vue-template/src/renderer/api/http.ts @@ -2,9 +2,9 @@ export type HttpMethod = 'GET' | 'POST' | 'DELETE'; export const CONFIG = { CLIENT_BASE: 'http://localhost:8081', - //RUOYI_BASE: 'http://8.138.23.49:8085', - RUOYI_BASE: 'http://192.168.1.89:8085', - SSE_URL: 'http://192.168.1.89:8085/monitor/account/events' + RUOYI_BASE: 'http://8.138.23.49:8085', + //RUOYI_BASE: 'http://192.168.1.89:8085', + SSE_URL: 'http://8.138.23.49:8085/monitor/account/events' } as const; function resolveBase(path: string): string { diff --git a/electron-vue-template/src/renderer/api/rakuten.ts b/electron-vue-template/src/renderer/api/rakuten.ts index eb1c80f..ba4ffde 100644 --- a/electron-vue-template/src/renderer/api/rakuten.ts +++ b/electron-vue-template/src/renderer/api/rakuten.ts @@ -1,18 +1,18 @@ import { http } from './http' export const rakutenApi = { - getProducts(params: { file?: File; shopName?: string; batchId?: string }) { + getProducts(params: { file?: File; shopName?: string; batchId?: string }, signal?: AbortSignal) { const formData = new FormData() if (params.file) formData.append('file', params.file) if (params.batchId) formData.append('batchId', params.batchId) if (params.shopName) formData.append('shopName', params.shopName) - return http.upload('/api/rakuten/products', formData) + return http.upload('/api/rakuten/products', formData, signal) }, - search1688(imageUrl: string, sessionId?: string) { + search1688(imageUrl: string, sessionId?: string, signal?: AbortSignal) { const payload: Record = { imageUrl } if (sessionId) payload.sessionId = sessionId - return http.post('/api/rakuten/search1688', payload) + return http.post('/api/rakuten/search1688', payload, signal) }, getLatestProducts() { diff --git a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue index 7f61e9c..ebcbaff 100644 --- a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue +++ b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue @@ -32,9 +32,9 @@ const dragActive = ref(false) // 试用期过期弹框 const showTrialExpiredDialog = ref(false) -const trialExpiredType = ref<'device' | 'account' | 'both'>('account') +const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account') -const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType') +const checkExpiredType = inject<() => 'device' | 'account' | 'both' | 'subscribe'>('checkExpiredType') // 计算属性 - 当前页数据 const paginatedData = computed(() => { diff --git a/electron-vue-template/src/renderer/components/common/TrialExpiredDialog.vue b/electron-vue-template/src/renderer/components/common/TrialExpiredDialog.vue index e230a4f..f8c0791 100644 --- a/electron-vue-template/src/renderer/components/common/TrialExpiredDialog.vue +++ b/electron-vue-template/src/renderer/components/common/TrialExpiredDialog.vue @@ -1,9 +1,10 @@ @@ -71,6 +77,7 @@ function copyWechat() {
客服微信
_linhong
+
📋
@@ -137,11 +144,13 @@ function copyWechat() { margin-bottom: 20px; width: 90%; cursor: pointer; - transition: background 0.3s; + transition: all 0.3s; + position: relative; } .wechat-card:hover { - background: #ebebeb; + background: #e8f5e9; + box-shadow: 0 2px 8px rgba(9, 187, 7, 0.15); } .wechat-icon { @@ -170,6 +179,18 @@ function copyWechat() { color: #1f1f1f; } +.copy-icon { + margin-left: auto; + font-size: 16px; + opacity: 0.5; + transition: all 0.3s; +} + +.wechat-card:hover .copy-icon { + opacity: 1; + transform: scale(1.1); +} + .confirm-btn { height: 40px; font-size: 14px; diff --git a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue index ec052f0..6b7d1eb 100644 --- a/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue +++ b/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue @@ -20,6 +20,7 @@ const tableLoading = ref(false) const exportLoading = ref(false) const statusMessage = ref('') const statusType = ref<'info' | 'success' | 'warning' | 'error'>('info') +let abortController: AbortController | null = null // 查询与上传 const singleShopName = ref('') @@ -55,9 +56,9 @@ const activeStep = computed(() => { // 试用期过期弹框 const showTrialExpiredDialog = ref(false) -const trialExpiredType = ref<'device' | 'account' | 'both'>('account') +const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account') -const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType') +const checkExpiredType = inject<() => 'device' | 'account' | 'both' | 'subscribe'>('checkExpiredType') // 左侧:上传文件名与地区 const selectedFileName = ref('') @@ -153,7 +154,7 @@ async function searchProductInternal(product: any) { showTrialExpiredDialog.value = true return false } - const res: any = await rakutenApi.search1688(product.imgUrl, currentBatchId.value) + const res: any = await rakutenApi.search1688(product.imgUrl, currentBatchId.value, abortController?.signal) const data = res.data if (!hasValid1688Data(data)) return false const skuJson = data.skuPriceJson || data.skuPrice @@ -176,7 +177,8 @@ async function searchProductWithRetry(product: any, maxRetry = 2) { try { const ok = await searchProductInternal(product) if (ok) return true - } catch (e) { + } catch (e: any) { + if (e.name === 'AbortError') return false console.warn('search1688 failed', e) } if (attempt < maxRetry) await delay(600) @@ -244,6 +246,8 @@ async function handleStartSearch() { return } + abortController = new AbortController() + if (pendingFile.value) { try { loading.value = true @@ -255,7 +259,7 @@ async function handleStartSearch() { progressPercentage.value = 0 totalProducts.value = 0 processedProducts.value = 0 - const resp: any = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value}) + const resp: any = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value}, abortController?.signal) const products = (resp.data.products || []).map((p: any) => ({...p, skuPrices: parseSkuPrices(p)})) if (products.length === 0) { @@ -264,9 +268,11 @@ async function handleStartSearch() { allProducts.value = products pendingFile.value = null - } catch (e) { - statusType.value = 'error' - statusMessage.value = '解析失败,请重试' + } catch (e: any) { + if (e.name !== 'AbortError') { + statusType.value = 'error' + statusMessage.value = '解析失败,请重试' + } } finally { loading.value = false tableLoading.value = false @@ -280,17 +286,21 @@ async function handleStartSearch() { progressPercentage.value = 100 statusType.value = 'success' statusMessage.value = '' + abortController = null return } if (items.length === 0) { statusType.value = 'warning' statusMessage.value = '没有可处理的商品,请先导入或查询店铺' + abortController = null return } await startBatch1688Search(items) } function stopTask() { + abortController?.abort() + abortController = null loading.value = false tableLoading.value = false statusType.value = 'warning' @@ -305,6 +315,7 @@ async function startBatch1688Search(products: any[]) { progressPercentage.value = 100 statusType.value = 'success' statusMessage.value = '所有商品都已获取1688数据!' + abortController = null return } loading.value = true @@ -323,6 +334,7 @@ async function startBatch1688Search(products: any[]) { statusMessage.value = '' } loading.value = false + abortController = null } async function serialSearch1688(products: any[]) { diff --git a/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue b/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue index 9f98441..ec9beae 100644 --- a/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue +++ b/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue @@ -6,26 +6,19 @@ import AccountManager from '../common/AccountManager.vue' import { batchConvertImages } from '../../utils/imageProxy' import { handlePlatformFileExport } from '../../utils/settings' import { getUsernameFromToken } from '../../utils/token' - const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue')) - const refreshVipStatus = inject<() => Promise>('refreshVipStatus') - // 接收VIP状态 const props = defineProps<{ isVip: boolean }>() - type Shop = { id: string; shopName: string } - const accounts = ref([]) const accountId = ref() // 收起功能移除 - const shopList = ref([]) const selectedShops = ref([]) const dateRange = ref([]) - const loading = ref(false) const exportLoading = ref(false) const progressPercentage = ref(0) @@ -44,9 +37,8 @@ let abortController: AbortController | null = null // 试用期过期弹框 const showTrialExpiredDialog = ref(false) -const trialExpiredType = ref<'device' | 'account' | 'both'>('account') - -const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType') +const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account') +const checkExpiredType = inject<() => 'device' | 'account' | 'both' | 'subscribe'>('checkExpiredType') function selectAccount(id: number) { accountId.value = id loadShops() @@ -106,7 +98,6 @@ async function fetchData() { // 刷新VIP状态 if (refreshVipStatus) await refreshVipStatus() - // VIP检查 if (!props.isVip) { if (checkExpiredType) trialExpiredType.value = checkExpiredType() @@ -123,14 +114,11 @@ async function fetchData() { fetchCurrentPage.value = 1 fetchTotalItems.value = 0 currentBatchId.value = `ZEBRA_${Date.now()}` - const [startDate = '', endDate = ''] = dateRange.value || [] await fetchPageData(startDate, endDate) } - async function fetchPageData(startDate: string, endDate: string) { if (!isFetching.value) return - try { const response = await zebraApi.getOrders({ accountId: Number(accountId.value) || undefined, @@ -145,10 +133,8 @@ async function fetchPageData(startDate: string, endDate: string) { const data = (response as any)?.data || response const orders = data.orders || [] allOrderData.value = [...allOrderData.value, ...orders] - fetchTotalPages.value = data.totalPages || 0 fetchTotalItems.value = data.total || 0 - if (fetchCurrentPage.value < fetchTotalPages.value && isFetching.value) { progressPercentage.value = Math.round((fetchCurrentPage.value / fetchTotalPages.value) * 100) fetchCurrentPage.value++ diff --git a/erp_client_sb/src/main/resources/application.yml b/erp_client_sb/src/main/resources/application.yml index bd0e4e7..df40b0e 100644 --- a/erp_client_sb/src/main/resources/application.yml +++ b/erp_client_sb/src/main/resources/application.yml @@ -39,7 +39,6 @@ spring: server: port: 8081 address: 127.0.0.1 - # 外部API服务配置 api: server: @@ -61,7 +60,6 @@ project: version: @project.version@ build: time: @maven.build.timestamp@ - logging: level: com.tashow.erp: INFO diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java index 656e18a..2d7691e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/ClientDeviceController.java @@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Map; - @RestController @RequestMapping("/monitor/device") @Anonymous