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