新增资产服务订阅功能,在应用中集成模块未开通弹窗组件,当检测到402状态码时自动弹出订阅对话框引导用户开通服务,同时新增资产订阅相关API接口包括获取资产SKU信息和订阅服务接口,在请求拦截器中添加402状态码处理逻辑并过滤SKU查询接口避免循环触发
This commit is contained in:
11
src/App.vue
11
src/App.vue
@@ -4,6 +4,12 @@
|
|||||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
|
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
|
||||||
<CloseFull v-if="!themeConfig.isLockScreen" />
|
<CloseFull v-if="!themeConfig.isLockScreen" />
|
||||||
|
<!-- 模块未开通弹窗 -->
|
||||||
|
<AssetSubscribeDialog
|
||||||
|
v-model="assetSubscribeState.visible"
|
||||||
|
:assetId="assetSubscribeState.assetId"
|
||||||
|
:serviceName="assetSubscribeState.serviceName"
|
||||||
|
/>
|
||||||
</el-config-provider>
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -19,10 +25,12 @@ import setIntroduction from '/@/utils/setIconfont';
|
|||||||
import LockScreen from '/@/layout/lockScreen/index.vue';
|
import LockScreen from '/@/layout/lockScreen/index.vue';
|
||||||
import Setings from '/@/layout/navBars/breadcrumb/setings.vue';
|
import Setings from '/@/layout/navBars/breadcrumb/setings.vue';
|
||||||
import CloseFull from '/@/layout/navBars/breadcrumb/closeFull.vue';
|
import CloseFull from '/@/layout/navBars/breadcrumb/closeFull.vue';
|
||||||
|
import AssetSubscribeDialog from '/@/components/assetSubscribe/index.vue';
|
||||||
|
import { assetSubscribeState } from '/@/utils/assetSubscribe';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: { LockScreen, Setings, CloseFull },
|
components: { LockScreen, Setings, CloseFull, AssetSubscribeDialog },
|
||||||
setup() {
|
setup() {
|
||||||
const { proxy } = <any>getCurrentInstance();
|
const { proxy } = <any>getCurrentInstance();
|
||||||
const setingsRef = ref();
|
const setingsRef = ref();
|
||||||
@@ -89,6 +97,7 @@ export default defineComponent({
|
|||||||
themeConfig,
|
themeConfig,
|
||||||
setingsRef,
|
setingsRef,
|
||||||
getGlobalComponentSize,
|
getGlobalComponentSize,
|
||||||
|
assetSubscribeState,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
// 创建 SKU
|
||||||
export function createAssetSku(data: CreateSkuParams) {
|
export function createAssetSku(data: CreateSkuParams) {
|
||||||
return newService({
|
return newService({
|
||||||
@@ -220,3 +229,18 @@ export function listLogs(params: LogQueryParams) {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅/开通资产服务参数
|
||||||
|
export interface SubscribeAssetParams {
|
||||||
|
skuId: string;
|
||||||
|
assetId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅/开通资产服务
|
||||||
|
export function subscribeAsset(data: SubscribeAssetParams) {
|
||||||
|
return newService({
|
||||||
|
url: '/assets/asset/subscribe',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
277
src/components/assetSubscribe/index.vue
Normal file
277
src/components/assetSubscribe/index.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="开通服务"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="subscribe-container">
|
||||||
|
<div class="service-info">
|
||||||
|
<el-icon class="warning-icon"><WarningFilled /></el-icon>
|
||||||
|
<span class="service-name">{{ serviceName }}</span>
|
||||||
|
<span class="service-tip">服务未开通,请选择套餐进行开通</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="skuList.length === 0" class="empty-container">
|
||||||
|
<el-empty description="暂无可用套餐" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="sku-list">
|
||||||
|
<div
|
||||||
|
v-for="sku in skuList"
|
||||||
|
:key="sku.id"
|
||||||
|
class="sku-item"
|
||||||
|
:class="{ active: selectedSku?.id === sku.id }"
|
||||||
|
@click="selectSku(sku)"
|
||||||
|
>
|
||||||
|
<div class="sku-header">
|
||||||
|
<span class="sku-name">{{ sku.skuName }}</span>
|
||||||
|
<el-tag v-if="sku.unlimitedStock" type="success" size="small">不限库存</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="sku-body">
|
||||||
|
<div class="sku-specs">
|
||||||
|
<span class="specs-count">{{ sku.specsCount }}</span>
|
||||||
|
<span class="specs-unit">{{ sku.specsUnit?.value || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sku-price">
|
||||||
|
<span class="price-symbol">¥</span>
|
||||||
|
<span class="price-value">{{ (sku.price / 100).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!selectedSku" :loading="submitLoading" @click="handleSubscribe">
|
||||||
|
立即开通
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { WarningFilled } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getAssetAndSku, subscribeAsset } from '/@/api/assets/asset/index';
|
||||||
|
|
||||||
|
interface SkuItem {
|
||||||
|
id: string;
|
||||||
|
assetId: string;
|
||||||
|
assetName: string;
|
||||||
|
skuName: string;
|
||||||
|
price: number;
|
||||||
|
specsCount: number;
|
||||||
|
specsUnit: { key: string; value: string } | null;
|
||||||
|
unlimitedStock: boolean;
|
||||||
|
stock: number;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
assetId: string;
|
||||||
|
serviceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: false,
|
||||||
|
assetId: '',
|
||||||
|
serviceName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void;
|
||||||
|
(e: 'success', sku: SkuItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const skuList = ref<SkuItem[]>([]);
|
||||||
|
const selectedSku = ref<SkuItem | null>(null);
|
||||||
|
|
||||||
|
// 监听弹窗打开,加载SKU列表
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val && props.assetId) {
|
||||||
|
loadSkuList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加载SKU列表
|
||||||
|
const loadSkuList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
selectedSku.value = null;
|
||||||
|
try {
|
||||||
|
const res = await getAssetAndSku({
|
||||||
|
assetId: props.assetId,
|
||||||
|
});
|
||||||
|
skuList.value = res.data?.skuList || res.data?.list || [];
|
||||||
|
// 默认选中第一个
|
||||||
|
if (skuList.value.length > 0) {
|
||||||
|
selectedSku.value = skuList.value[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载套餐列表失败:', error);
|
||||||
|
skuList.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择SKU
|
||||||
|
const selectSku = (sku: SkuItem) => {
|
||||||
|
selectedSku.value = sku;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开通服务
|
||||||
|
const handleSubscribe = async () => {
|
||||||
|
if (!selectedSku.value) {
|
||||||
|
ElMessage.warning('请选择套餐');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading.value = true;
|
||||||
|
try {
|
||||||
|
await subscribeAsset({
|
||||||
|
skuId: selectedSku.value.id,
|
||||||
|
assetId: selectedSku.value.assetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
ElMessage.success('开通成功');
|
||||||
|
emit('success', selectedSku.value);
|
||||||
|
handleClose();
|
||||||
|
// 刷新页面以重新加载数据
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('开通失败:', error);
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
selectedSku.value = null;
|
||||||
|
skuList.value = [];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.subscribe-container {
|
||||||
|
.service-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff7e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tip {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.sku-item {
|
||||||
|
border: 2px solid #e8e8e8;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #409eff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.sku-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.sku-specs {
|
||||||
|
.specs-count {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specs-unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-price {
|
||||||
|
color: #f56c6c;
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
83
src/utils/assetSubscribe.ts
Normal file
83
src/utils/assetSubscribe.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// 路由路径与 assetId 的映射关系
|
||||||
|
const ROUTE_ASSET_MAP: Record<string, { assetId: string; serviceName: string }> = {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
|||||||
import { Session } from '/@/utils/storage';
|
import { Session } from '/@/utils/storage';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { getChangedFields } from '/@/utils/diffUtils';
|
import { getChangedFields } from '/@/utils/diffUtils';
|
||||||
|
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
|
||||||
|
|
||||||
// 标记是否正在处理 token 过期,避免重复弹窗
|
// 标记是否正在处理 token 过期,避免重复弹窗
|
||||||
let isHandlingTokenExpired = false;
|
let isHandlingTokenExpired = false;
|
||||||
@@ -156,8 +157,20 @@ const responseInterceptor = (response: AxiosResponse) => {
|
|||||||
return Promise.reject(new Error('登录状态已过期'));
|
return Promise.reject(new Error('登录状态已过期'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 业务逻辑错误处理
|
// 处理模块未开通错误 (403)
|
||||||
if (code !== undefined && code !== 0 && code !== 200) {
|
// 跳过资产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})`;
|
const errorMsg = message || `请求失败(${code})`;
|
||||||
showErrorMessage(errorMsg);
|
showErrorMessage(errorMsg);
|
||||||
return Promise.reject(new Error(errorMsg));
|
return Promise.reject(new Error(errorMsg));
|
||||||
@@ -189,10 +202,21 @@ const responseErrorHandler = (error: any) => {
|
|||||||
const responseMessage = error.response.data?.message;
|
const responseMessage = error.response.data?.message;
|
||||||
|
|
||||||
// 处理 HTTP 错误状态
|
// 处理 HTTP 错误状态
|
||||||
|
const requestUrl = error.response.config?.url || '';
|
||||||
switch (httpStatus) {
|
switch (httpStatus) {
|
||||||
case 401:
|
case 401:
|
||||||
handleTokenExpired();
|
handleTokenExpired();
|
||||||
break;
|
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:
|
case 403:
|
||||||
showErrorMessage(responseMessage || '没有权限访问该资源');
|
showErrorMessage(responseMessage || '没有权限访问该资源');
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user