diff --git a/src/App.vue b/src/App.vue index c06763c..61073d9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,6 +4,12 @@ + + @@ -19,10 +25,12 @@ import setIntroduction from '/@/utils/setIconfont'; import LockScreen from '/@/layout/lockScreen/index.vue'; import Setings from '/@/layout/navBars/breadcrumb/setings.vue'; import CloseFull from '/@/layout/navBars/breadcrumb/closeFull.vue'; +import AssetSubscribeDialog from '/@/components/assetSubscribe/index.vue'; +import { assetSubscribeState } from '/@/utils/assetSubscribe'; export default defineComponent({ name: 'app', - components: { LockScreen, Setings, CloseFull }, + components: { LockScreen, Setings, CloseFull, AssetSubscribeDialog }, setup() { const { proxy } = getCurrentInstance(); const setingsRef = ref(); @@ -89,6 +97,7 @@ export default defineComponent({ themeConfig, setingsRef, getGlobalComponentSize, + assetSubscribeState, ...toRefs(state), }; }, diff --git a/src/api/assets/asset/index.ts b/src/api/assets/asset/index.ts index 8fc2218..f36e3f9 100644 --- a/src/api/assets/asset/index.ts +++ b/src/api/assets/asset/index.ts @@ -119,6 +119,15 @@ export function listAssetSkus(params: SkuQueryParams) { }); } +// 根据assetId获取资产和SKU信息(用于套餐开通弹窗) +export function getAssetAndSku(params: { assetId: string }) { + return newService({ + url: '/assets/asset/getAssetAndSku', + method: 'get', + params, + }); +} + // 创建 SKU export function createAssetSku(data: CreateSkuParams) { return newService({ @@ -220,3 +229,18 @@ export function listLogs(params: LogQueryParams) { params, }); } + +// 订阅/开通资产服务参数 +export interface SubscribeAssetParams { + skuId: string; + assetId?: string; +} + +// 订阅/开通资产服务 +export function subscribeAsset(data: SubscribeAssetParams) { + return newService({ + url: '/assets/asset/subscribe', + method: 'post', + data, + }); +} diff --git a/src/components/assetSubscribe/index.vue b/src/components/assetSubscribe/index.vue new file mode 100644 index 0000000..c4ce99d --- /dev/null +++ b/src/components/assetSubscribe/index.vue @@ -0,0 +1,277 @@ + + + + + + {{ serviceName }} + 服务未开通,请选择套餐进行开通 + + + + + + + + + + + + + + {{ sku.skuName }} + 不限库存 + + + + {{ sku.specsCount }} + {{ sku.specsUnit?.value || '' }} + + + ¥ + {{ (sku.price / 100).toFixed(2) }} + + + + + + + + 取消 + + 立即开通 + + + + + + + + diff --git a/src/utils/assetSubscribe.ts b/src/utils/assetSubscribe.ts new file mode 100644 index 0000000..1df0289 --- /dev/null +++ b/src/utils/assetSubscribe.ts @@ -0,0 +1,83 @@ +import { ref } from 'vue'; + +// 路由路径与 assetId 的映射关系 +const ROUTE_ASSET_MAP: Record = { + // CID广告业务(聚合广告) + '/cidService': { assetId: '696f423705e496ba4ccbe665', serviceName: '聚合广告' }, + + // AI客服业务 + '/customerService': { assetId: '696f421205e496ba4ccbe662', serviceName: 'AI客服' }, + + // 聚合电商业务(资产管理) + '/assets': { assetId: '696b4acd1be1c8b76c4b4c15', serviceName: '资产管理' }, + + // 订单 + // '/order': { assetId: '696b4acd1be1c8b76c4b4c15', serviceName: '资产管理' }, + + // AI数字人 + // '/digitalHuman': { assetId: '696f421205e496ba4ccbe662', serviceName: 'AI客服' }, +}; + +// 当前弹窗状态(响应式,供组件使用) +export const assetSubscribeState = ref({ + visible: false, + assetId: '', + serviceName: '', +}); + +/** + * 根据路由路径获取对应的 assetId 和服务名称 + */ +export function getAssetInfoByRoute(routePath: string): { assetId: string; serviceName: string } | null { + // 精确匹配 + if (ROUTE_ASSET_MAP[routePath]) { + return ROUTE_ASSET_MAP[routePath]; + } + + // 前缀匹配 + for (const [prefix, info] of Object.entries(ROUTE_ASSET_MAP)) { + if (routePath.startsWith(prefix)) { + return info; + } + } + + return null; +} + +/** + * 显示服务开通弹窗 + */ +export function showAssetSubscribeDialog(assetId: string, serviceName: string) { + console.log('[showAssetSubscribeDialog] 显示弹窗:', { assetId, serviceName }); + console.log('[showAssetSubscribeDialog] 修改前状态:', JSON.stringify(assetSubscribeState.value)); + assetSubscribeState.value.visible = true; + assetSubscribeState.value.assetId = assetId; + assetSubscribeState.value.serviceName = serviceName; + console.log('[showAssetSubscribeDialog] 修改后状态:', JSON.stringify(assetSubscribeState.value)); +} + +/** + * 关闭服务开通弹窗 + */ +export function closeAssetSubscribeDialog() { + assetSubscribeState.value.visible = false; +} + +/** + * 处理 403 错误码(模块未开通) + */ +export function handleModuleNotEnabled(routePath: string): boolean { + console.log('[模块未开通] 当前路由路径:', routePath); + const assetInfo = getAssetInfoByRoute(routePath); + console.log('[模块未开通] 匹配到的资产信息:', assetInfo); + + if (assetInfo) { + showAssetSubscribeDialog(assetInfo.assetId, assetInfo.serviceName); + return true; + } + + // 如果没有匹配到路由,尝试使用默认的资产管理 + console.warn('[模块未开通] 未匹配到路由,使用默认资产管理'); + showAssetSubscribeDialog('696b4acd1be1c8b76c4b4c15', '资产管理'); + return true; +} diff --git a/src/utils/request.ts b/src/utils/request.ts index cad63b3..5ca7d7a 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -3,6 +3,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'; import { Session } from '/@/utils/storage'; import qs from 'qs'; import { getChangedFields } from '/@/utils/diffUtils'; +import { handleModuleNotEnabled } from '/@/utils/assetSubscribe'; // 标记是否正在处理 token 过期,避免重复弹窗 let isHandlingTokenExpired = false; @@ -156,8 +157,20 @@ const responseInterceptor = (response: AxiosResponse) => { return Promise.reject(new Error('登录状态已过期')); } - // 业务逻辑错误处理 - if (code !== undefined && code !== 0 && code !== 200) { + // 处理模块未开通错误 (403) + // 跳过资产SKU查询接口,避免弹窗内部请求触发循环 + const requestUrl = response.config.url || ''; + if (code === 402 && !requestUrl.includes('/assets/asset/sku/')) { + // 获取当前路由路径 + const currentPath = window.location.hash.replace('#', '') || window.location.pathname; + console.log('[request.ts] 检测到403错误,当前路径:', currentPath); + handleModuleNotEnabled(currentPath); + // 直接返回,不再显示错误消息 + return Promise.reject(new Error('模块未开通')); + } + + // 业务逻辑错误处理(排除403,因为上面已处理) + if (code !== undefined && code !== 0 && code !== 200 && code !== 403) { const errorMsg = message || `请求失败(${code})`; showErrorMessage(errorMsg); return Promise.reject(new Error(errorMsg)); @@ -189,10 +202,21 @@ const responseErrorHandler = (error: any) => { const responseMessage = error.response.data?.message; // 处理 HTTP 错误状态 + const requestUrl = error.response.config?.url || ''; switch (httpStatus) { case 401: handleTokenExpired(); break; + case 402: + // 模块未开通处理,跳过SKU相关接口避免循环 + if (!requestUrl.includes('/assets/asset/sku/') && !requestUrl.includes('getAssetAndSku')) { + const currentPath = window.location.hash.replace('#', '') || window.location.pathname; + console.log('[responseErrorHandler] 检测到HTTP 402错误,当前路径:', currentPath); + handleModuleNotEnabled(currentPath); + return Promise.reject(new Error('模块未开通')); + } + showErrorMessage(responseMessage || '服务未开通'); + break; case 403: showErrorMessage(responseMessage || '没有权限访问该资源'); break;