diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 0d9fc22..6fdca1a 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -7,7 +7,7 @@ export {} declare global { const EffectScope: typeof import('vue')['EffectScope'] - const ElLoading: typeof import('element-plus')['ElLoading'] + const ElLoading: typeof import('element-plus/es')['ElLoading'] const ElMessage: typeof import('element-plus/es')['ElMessage'] const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] const ElNotification: typeof import('element-plus')['ElNotification'] diff --git a/src/api/index.js b/src/api/index.js index d5d23b8..1b98333 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -63,7 +63,19 @@ export const productApi = { // 下载接口文档(支持PDF和Markdown) downloadProductDocumentation: (productId) => request.get(`/products/${productId}/documentation/download`, { responseType: 'blob' - }) + }), + + // 组件报告下载相关API + // 检查产品是否可以下载示例报告 + checkComponentReportAvailability: (productId) => request.get(`/products/${productId}/component-report/check`), + // 获取产品示例报告下载信息和价格计算 + getComponentReportInfo: (productId) => request.get(`/products/${productId}/component-report/info`), + // 创建示例报告下载支付订单 + createComponentReportPaymentOrder: (productId, data) => request.post(`/products/${productId}/component-report/create-order`, data), + // 检查示例报告下载支付状态 + checkComponentReportPaymentStatus: (orderId) => request.get(`/component-report/check-payment/${orderId}`), + // 生成并下载示例报告ZIP文件 + generateAndDownloadComponentReport: (data) => request.post('/component-report/generate-and-download', data, { responseType: 'blob' }) } // 分类相关接口 - 数据大厅 @@ -130,7 +142,15 @@ export const financeApi = { // 充值记录相关接口 getUserRechargeRecords: (params) => request.get('/finance/wallet/recharge-records', { params }), - getAdminRechargeRecords: (params) => request.get('/admin/finance/recharge-records', { params }) + getAdminRechargeRecords: (params) => request.get('/admin/finance/recharge-records', { params }), + + // 购买记录相关接口 + getUserPurchaseRecords: (params) => request.get('/finance/purchase-records', { params }), + getAdminPurchaseRecords: (params) => request.get('/admin/finance/purchase-records', { params }), + exportAdminPurchaseRecords: (params) => request.get('/admin/finance/purchase-records/export', { + params, + responseType: 'blob' + }) } // 认证相关接口 diff --git a/src/components/admin/ProductFormDialog.vue b/src/components/admin/ProductFormDialog.vue index a6e2deb..e73e8cb 100644 --- a/src/components/admin/ProductFormDialog.vue +++ b/src/components/admin/ProductFormDialog.vue @@ -114,6 +114,45 @@ + + +
+

UI组件配置

+ + + + + + + + + + + +
@@ -375,6 +414,9 @@ const form = reactive({ is_enabled: true, is_visible: true, is_package: false, + // UI组件相关字段 + sell_ui_component: false, + ui_component_price: 0, seo_title: '', seo_description: '', seo_keywords: '' @@ -399,6 +441,22 @@ const rules = { cost_price: [ { type: 'number', min: 0, message: '成本价不能小于0', trigger: 'blur' } ], + ui_component_price: [ + { + type: 'number', + min: 0, + message: 'UI组件价格不能小于0', + trigger: 'blur', + validator: (rule, value, callback) => { + // 如果是组合包且出售UI组件,则价格必须大于0 + if (form.is_package && form.sell_ui_component && (!value || value < 0)) { + callback(new Error('出售UI组件时,价格不能小于0')) + } else { + callback() + } + } + } + ], remark: [ { max: 1000, message: '备注长度不能超过1000个字符', trigger: 'blur' } ], @@ -467,6 +525,15 @@ const handleEditMode = async () => { form[key] = props.product[key] } }) + + // 确保UI组件相关字段正确加载 + // 如果后端返回的字段名不同,这里可以添加映射 + if (props.product.sell_ui_component !== undefined) { + form.sell_ui_component = props.product.sell_ui_component + } + if (props.product.ui_component_price !== undefined) { + form.ui_component_price = props.product.ui_component_price + } // 如果是组合包,处理子产品数据 if (props.product.is_package) { @@ -502,8 +569,10 @@ const handleCreateMode = () => { // 保持默认值 true,但允许用户在界面上切换 // 注意:这里不强制设置,让用户可以在界面上自由切换 // 默认值已经在 form 初始化时设置为 true(第375-376行) - } else if (key === 'price' || key === 'cost_price') { + } else if (key === 'price' || key === 'cost_price' || key === 'ui_component_price') { form[key] = 0 + } else if (key === 'sell_ui_component') { + form[key] = false } else { form[key] = '' } diff --git a/src/constants/menu.js b/src/constants/menu.js index b451b12..1c8ba00 100644 --- a/src/constants/menu.js +++ b/src/constants/menu.js @@ -41,6 +41,7 @@ export const userMenuItems = [ children: [ { name: '余额充值', path: '/finance/wallet', icon: CreditCard, requiresCertification: true }, { name: '充值记录', path: '/finance/recharge-records', icon: CreditCard, requiresCertification: true }, + { name: '购买记录', path: '/finance/purchase-records', icon: ShoppingCart, requiresCertification: true }, { name: '消费记录', path: '/finance/transactions', icon: Clipboard, requiresCertification: true }, { name: '发票申请', path: '/finance/invoice', icon: Wallet, requiresCertification: true } ] @@ -124,6 +125,7 @@ export const getUserAccessibleMenuItems = (userType = 'user') => { { name: '调用记录', path: '/admin/usage', icon: Clipboard }, { name: '消费记录', path: '/admin/transactions', icon: Clipboard }, { name: '充值记录', path: '/admin/recharge-records', icon: CreditCard }, + { name: '购买记录', path: '/admin/purchase-records', icon: ShoppingCart }, { name: '发票管理', path: '/admin/invoices', icon: Wallet }, { name: '组件管理', path: '/admin/ui-components', icon: Cube } ] @@ -137,6 +139,7 @@ export const getUserAccessibleMenuItems = (userType = 'user') => { export const requiresCertificationPaths = [ '/finance/wallet', '/finance/recharge-records', + '/finance/purchase-records', '/finance/transactions', '/apis/usage', '/apis/whitelist' @@ -166,6 +169,10 @@ export const getCurrentPageCertificationConfig = (path) => { title: '充值记录', description: '为了查看完整的充值记录,请先完成企业入驻认证。认证成功后我们将赠送您一定的调用额度!' }, + '/finance/purchase-records': { + title: '购买记录', + description: '为了查看完整的购买记录,请先完成企业入驻认证。认证成功后我们将赠送您一定的调用额度!' + }, '/finance/transactions': { title: '消费记录', description: '为了查看完整的消费记录,请先完成企业入驻认证。认证成功后我们将赠送您一定的调用额度!' diff --git a/src/pages/admin/purchase-records/index.vue b/src/pages/admin/purchase-records/index.vue new file mode 100644 index 0000000..0a173ca --- /dev/null +++ b/src/pages/admin/purchase-records/index.vue @@ -0,0 +1,857 @@ + + + + + diff --git a/src/pages/admin/ui-components/index.vue b/src/pages/admin/ui-components/index.vue index ac8081c..90872ac 100644 --- a/src/pages/admin/ui-components/index.vue +++ b/src/pages/admin/ui-components/index.vue @@ -1,126 +1,328 @@ @@ -272,15 +474,20 @@ - \ No newline at end of file diff --git a/src/pages/admin/users/index.vue b/src/pages/admin/users/index.vue index f3acdfd..9c57036 100644 --- a/src/pages/admin/users/index.vue +++ b/src/pages/admin/users/index.vue @@ -178,6 +178,10 @@ 充值记录 + + + 购买记录 + @@ -367,6 +371,10 @@ 充值记录 + + + 购买记录 + @@ -643,7 +651,7 @@ import FilterItem from '@/components/common/FilterItem.vue' import FilterSection from '@/components/common/FilterSection.vue' import ListPageLayout from '@/components/common/ListPageLayout.vue' import { useMobileTable } from '@/composables/useMobileTable' -import { ArrowDown, Document, Money, Tickets, Wallet, Warning } from '@element-plus/icons-vue' +import { ArrowDown, Document, Money, ShoppingCart, Tickets, Wallet, Warning } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import { useRoute, useRouter } from 'vue-router' @@ -950,6 +958,12 @@ const handleMoreAction = (command) => { query: { user_id: user.id } }) break + case 'purchase_records': + router.push({ + name: 'AdminPurchaseRecords', + query: { user_id: user.id } + }) + break default: ElMessage.warning('未知操作') } diff --git a/src/pages/finance/purchase-records/index.vue b/src/pages/finance/purchase-records/index.vue new file mode 100644 index 0000000..891e055 --- /dev/null +++ b/src/pages/finance/purchase-records/index.vue @@ -0,0 +1,555 @@ + + + + + diff --git a/src/pages/products/detail.vue b/src/pages/products/detail.vue index 68a3808..6ceeeb2 100644 --- a/src/pages/products/detail.vue +++ b/src/pages/products/detail.vue @@ -51,6 +51,28 @@ > 前往在线调试 + + + + {{ canDownloadReport ? '下载示例报告' : '购买示例报告' }} + + + + + 检查支付状态 + @@ -274,6 +296,82 @@ + + + +
+ + + + +
+

包含的组件

+
+ + {{ item.product_code }} + + +
+
+ 暂无组件信息 +
+
+ +
+

价格信息

+
+
+ 价格: + ¥{{ formatPrice(product.ui_component_price) }} +
+
+
+ +
+

支付方式

+
+ + + 支付宝支付 + + + + 微信支付 + +
+
+
+ + +
@@ -281,8 +379,9 @@ import { productApi, subscriptionApi } from '@/api' import { useMobileTable } from '@/composables/useMobileTable' import { DocumentCopy, Download } from '@element-plus/icons-vue' -import { ElMessage, ElMessageBox } from 'element-plus' +import { ElLoading, ElMessage, ElMessageBox } from 'element-plus' import { marked } from 'marked' +import { h } from 'vue' const route = useRoute() const router = useRouter() @@ -297,8 +396,16 @@ const userSubscriptions = ref([]) const subscribing = ref(false) const cancelling = ref(false) const downloading = ref(false) +const downloadingReport = ref(false) const activeTab = ref('content') const currentTimestamp = ref('') +const showDownloadReportButton = ref(false) +const canDownloadReport = ref(false) +const reportDownloadInfo = ref(null) +const reportPaymentDialogVisible = ref(false) +const reportPaymentStatus = ref('pending') +const currentReportOrderId = ref('') +const checkingPaymentStatus = ref(false) // DOM 引用 const basicInfoRef = ref(null) @@ -324,15 +431,35 @@ onMounted(() => { loadUserSubscriptions() loadProductDetail().then(() => { addCollapsibleFeatures() + // 检查是否可以下载示例报告 + if (product.value?.is_package) { + checkReportDownloadAvailability() + } }) startTimestampUpdate() }) // 组件卸载时清理定时器 onUnmounted(() => { + // 清理时间戳更新定时器 if (timestampTimer.value) { clearInterval(timestampTimer.value) } + + // 清理支付状态检查定时器 + if (window.paymentCheckIntervals) { + Object.keys(window.paymentCheckIntervals).forEach(orderId => { + clearInterval(window.paymentCheckIntervals[orderId]) + }) + window.paymentCheckIntervals = {} + } + + // 关闭所有消息框 + ElMessageBox.close() + + // 关闭所有加载提示 + const loadingInstance = ElLoading.service() + loadingInstance.close() }) // 时间戳更新定时器 @@ -654,6 +781,29 @@ const openProductDetail = (productId) => { window.open(url, '_blank') } +/* + * 示例报告下载功能说明 + * + * 功能概述: + * - 只有组合包产品才能下载示例报告 + * - 根据用户是否已下载过子产品,计算价格(已下载的产品不需要再次付费) + * - 支持微信和支付宝两种支付方式 + * - 支付成功后可以下载包含所有子产品UI组件和示例数据的ZIP文件 + * + * 流程说明: + * 1. 检查产品是否为组合包,以及是否可以下载示例报告 + * 2. 如果可以直接下载(免费或已付费),直接提供下载 + * 3. 如果需要付费,显示支付对话框,包含价格明细和支付方式选择 + * 4. 用户选择支付方式后,创建支付订单 + * 5. 定期检查支付状态,成功后自动下载示例报告 + * + * 注意事项: + * - 微信支付显示二维码,需要用户扫码支付 + * - 支付宝支付会跳转到支付宝页面 + * - 支付状态检查会在后台持续进行,直到成功或超时 + * - 下载的ZIP文件包含UI组件和示例数据 + */ + // 获取默认的请求方式内容 const getDefaultBasicInfo = () => { return `## 请求头 @@ -825,6 +975,532 @@ const downloadMarkdown = (type) => { ElMessage.success('文档下载成功') } + +// 检查报告下载可用性 +const checkReportDownloadAvailability = async () => { + console.log('[checkReportDownloadAvailability] 开始检查下载可用性') + console.log('[checkReportDownloadAvailability] 产品信息:', product.value) + console.log('[checkReportDownloadAvailability] sell_ui_component:', product.value?.sell_ui_component) + + if (!product.value?.id || !product.value?.is_package) { + console.log('[checkReportDownloadAvailability] 产品不存在或不是组合包,隐藏下载按钮') + showDownloadReportButton.value = false + return + } + + if (!product.value?.sell_ui_component) { + console.log('[checkReportDownloadAvailability] 产品不出售UI组件,隐藏下载按钮') + showDownloadReportButton.value = false + return + } + + try { + console.log('[checkReportDownloadAvailability] 检查组件报告可用性,产品ID:', product.value.id) + const response = await productApi.checkComponentReportAvailability(product.value.id) + console.log('[checkReportDownloadAvailability] 检查可用性响应:', response) + const { can_download } = response.data + + showDownloadReportButton.value = true + + console.log('[checkReportDownloadAvailability] 获取组件报告信息,产品ID:', product.value.id) + // 获取下载信息和价格 + const infoResponse = await productApi.getComponentReportInfo(product.value.id) + console.log('[checkReportDownloadAvailability] 组件报告信息响应:', infoResponse) + + // 检查响应数据结构 + if (infoResponse.data && infoResponse.data.data) { + // 如果响应是嵌套结构,提取实际数据 + reportDownloadInfo.value = infoResponse.data.data + } else if (infoResponse.data) { + // 直接使用响应数据 + reportDownloadInfo.value = infoResponse.data + } else { + console.error('[checkReportDownloadAvailability] 响应数据格式不正确:', infoResponse) + showDownloadReportButton.value = false + return + } + + canDownloadReport.value = reportDownloadInfo.value.can_download + } catch (error) { + console.error('[checkReportDownloadAvailability] 检查报告下载可用性失败:', error) + console.error('[checkReportDownloadAvailability] 错误详情:', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }) + showDownloadReportButton.value = false + } +} + +// 处理下载报告按钮点击 +const handleDownloadReport = async () => { + console.log('[handleDownloadReport] 开始处理下载报告按钮点击') + console.log('[handleDownloadReport] 产品信息:', product.value) + + if (!product.value?.id) { + console.log('[handleDownloadReport] 产品信息不存在') + ElMessage.warning('产品信息不存在') + return + } + + // 如果可以直接下载 + if (canDownloadReport.value) { + console.log('[handleDownloadReport] 可以直接下载,开始下载') + downloadComponentReport() + return + } + + console.log('[handleDownloadReport] 需要购买,检查下载信息是否存在') + // 需要购买,显示支付对话框 + if (!reportDownloadInfo.value || !reportDownloadInfo.value.product_id) { + console.log('[handleDownloadReport] 下载信息不存在或不完整,重新获取') + try { + const response = await productApi.getComponentReportInfo(product.value.id) + console.log('[handleDownloadReport] 获取到的下载信息:', response) + + // 检查响应数据结构 + if (response.data && response.data.data) { + // 如果响应是嵌套结构,提取实际数据 + reportDownloadInfo.value = response.data.data + } else if (response.data) { + // 直接使用响应数据 + reportDownloadInfo.value = response.data + } else { + console.error('[handleDownloadReport] 响应数据格式不正确:', response) + ElMessage.error('获取报告下载信息失败') + return + } + + // 详细日志记录数据结构 + console.log('[handleDownloadReport] 下载信息详情:', { + 产品ID: reportDownloadInfo.value.product_id, + 产品名称: reportDownloadInfo.value.product_name, + 是否组合包: reportDownloadInfo.value.is_package, + 子产品数量: reportDownloadInfo.value.sub_products?.length || 0, + 子产品列表: reportDownloadInfo.value.sub_products, + 价格: product.value.ui_component_price, + 可下载: reportDownloadInfo.value.can_download + }) + } catch (error) { + console.error('[handleDownloadReport] 获取报告下载信息失败:', error) + console.error('[handleDownloadReport] 错误详情:', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }) + ElMessage.error('获取报告下载信息失败') + return + } + } else { + console.log('[handleDownloadReport] 下载信息已存在,使用缓存数据') + // 详细日志记录数据结构 + console.log('[handleDownloadReport] 缓存的下载信息详情:', { + 产品ID: reportDownloadInfo.value.product_id, + 产品名称: reportDownloadInfo.value.product_name, + 是否组合包: reportDownloadInfo.value.is_package, + 子产品数量: reportDownloadInfo.value.sub_products?.length || 0, + 子产品列表: reportDownloadInfo.value.sub_products, + 价格: product.value.ui_component_price, + 可下载: reportDownloadInfo.value.can_download + }) + } + + console.log('[handleDownloadReport] 显示支付对话框') + reportPaymentDialogVisible.value = true +} + +// 下载组件报告 +const downloadComponentReport = async () => { + if (!product.value?.id) { + ElMessage.warning('产品信息不存在') + return + } + + downloadingReport.value = true + try { + // 获取子产品代码列表 + let subProductCodes = [] + if (reportDownloadInfo.value && reportDownloadInfo.value.sub_products && reportDownloadInfo.value.sub_products.length > 0) { + subProductCodes = reportDownloadInfo.value.sub_products.map(item => item.product_code) + console.log('[downloadComponentReport] 提取子产品代码列表:', subProductCodes) + } else { + console.log('[downloadComponentReport] 未找到子产品信息,使用空列表') + } + + const response = await productApi.generateAndDownloadComponentReport({ + product_id: product.value.id, + sub_product_codes: subProductCodes + }) + + // 创建下载链接 + // 直接从response中获取数据,不再检查Blob类型 + const url = URL.createObjectURL(response.data) + const link = document.createElement('a') + link.href = url + link.download = `${product.value.name || '产品'}_示例报告.zip` + + // 触发下载 + document.body.appendChild(link) + link.click() + + // 清理 + document.body.removeChild(link) + URL.revokeObjectURL(url) + + ElMessage.success('示例报告下载成功') + } catch (error) { + console.error('下载示例报告失败:', error) + ElMessage.error('下载示例报告失败') + } finally { + downloadingReport.value = false + } +} + +// 创建支付订单 +const createReportPaymentOrder = async (paymentType) => { + console.log('[createReportPaymentOrder] 开始创建支付订单') + console.log('[createReportPaymentOrder] 支付类型:', paymentType) + console.log('[createReportPaymentOrder] 产品信息:', product.value) + + if (!product.value?.id) { + console.log('[createReportPaymentOrder] 产品信息不存在') + ElMessage.warning('产品信息不存在') + return + } + + try { + // 显示加载状态 + const loadingInstance = ElLoading.service({ + lock: true, + text: '正在创建支付订单,请稍候...', + background: 'rgba(0, 0, 0, 0.7)' + }) + + console.log('[createReportPaymentOrder] 调用创建支付订单API') + const response = await productApi.createComponentReportPaymentOrder(product.value.id, { + payment_type: paymentType + }) + console.log('[createReportPaymentOrder] 支付订单创建响应:', response) + + // 关闭加载状态 + loadingInstance.close() + + // 根据响应拦截器的处理方式,正确访问嵌套的 data 对象 + // 响应格式为 {code: 200, data: {...}} + const responseData = response.data.data || response.data + console.log('[createReportPaymentOrder] 解析响应数据:', responseData) + + const { order_id, code_url, pay_url, payment_type } = responseData + + currentReportOrderId.value = order_id + reportPaymentStatus.value = 'pending' + reportPaymentDialogVisible.value = false + + // 根据支付类型处理 + if (payment_type === 'wechat' && code_url) { + console.log('[createReportPaymentOrder] 微信支付,显示二维码') + // 显示微信支付二维码 + showWechatPaymentQRCode(code_url, order_id) + } else if (payment_type === 'alipay' && pay_url) { + console.log('[createReportPaymentOrder] 支付宝支付,跳转到支付页面') + // 跳转到支付宝支付页面 + window.open(pay_url, '_blank') + // 显示支付状态检查提示 + ElMessage.info('已打开支付页面,请在支付完成后返回此页面') + // 开始检查支付状态 + startPaymentStatusCheck(order_id) + } else { + console.error('[createReportPaymentOrder] 支付类型或支付链接不匹配', { + payment_type, + code_url: !!code_url, + pay_url: !!pay_url + }) + ElMessage.error('支付方式不支持或支付链接获取失败') + } + } catch (error) { + console.error('[createReportPaymentOrder] 创建支付订单失败:', error) + console.error('[createReportPaymentOrder] 错误详情:', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }) + + // 显示具体错误信息 + const errorMessage = error.response?.data?.message || error.message || '创建支付订单失败' + ElMessage.error(errorMessage) + } +} + +// 显示微信支付二维码 +const showWechatPaymentQRCode = (codeUrl, orderId) => { + // 验证二维码URL是否有效 + if (!codeUrl || typeof codeUrl !== 'string') { + ElMessage.error('微信支付二维码获取失败') + return + } + + console.log('[showWechatPaymentQRCode] 准备显示二维码,URL:', codeUrl) + + // 使用第三方二维码生成服务将微信支付URL转换为二维码图片 + const qrCodeImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(codeUrl)}` + + // 使用自定义对话框而不是ElMessageBox.alert,以确保HTML内容正确渲染 + const qrDialog = ElMessageBox({ + title: '微信扫码支付', + message: h('div', { style: 'text-align: center; padding: 20px;' }, [ + h('h3', { style: 'margin-bottom: 20px;' }, '微信扫码支付'), + h('div', { style: 'margin-bottom: 20px;' }, '请使用微信扫描下方二维码完成支付'), + h('div', { style: 'display: flex; justify-content: center; margin-bottom: 20px;' }, [ + h('img', { + src: qrCodeImageUrl, + alt: '微信支付二维码', + style: 'width: 200px; height: 200px; border: 1px solid #eee;', + onError: (e) => { + console.error('[showWechatPaymentQRCode] 二维码图片加载失败:', e) + // 尝试使用备用方案:显示一个链接,让用户点击打开微信 + ElMessage({ + message: '二维码加载失败,请尝试直接打开微信支付', + type: 'warning', + duration: 5000, + showClose: true + }) + + // 更新对话框内容,显示支付链接 + instance.message = h('div', { style: 'text-align: center; padding: 20px;' }, [ + h('h3', { style: 'margin-bottom: 20px;' }, '微信扫码支付'), + h('div', { style: 'margin-bottom: 20px;' }, '二维码生成失败,请点击下方链接打开微信支付'), + h('div', { style: 'margin-bottom: 20px; padding: 10px; border: 1px dashed #ccc; border-radius: 4px;' }, [ + h('a', { + href: codeUrl, + onClick: (e) => { + e.preventDefault() + // 尝试打开微信支付链接 + window.open(codeUrl, '_self') + }, + style: 'color: #409EFF; text-decoration: underline; cursor: pointer;' + }, '点击打开微信支付') + ]), + h('div', { style: 'font-size: 14px; color: #666;' }, [ + h('p', `支付金额:¥${formatPrice(product.value?.ui_component_price || 0)}`), + h('p', '支付完成后,请点击"我已支付"按钮') + ]) + ]) + } + }) + ]), + h('div', { style: 'font-size: 14px; color: #666;' }, [ + h('p', `支付金额:¥${formatPrice(product.value?.ui_component_price || 0)}`), + h('p', '支付完成后,系统将自动检测支付状态') + ]) + ]), + confirmButtonText: '我已支付', + cancelButtonText: '支付遇到问题', + type: 'info', + showClose: false, + closeOnClickModal: false, + closeOnPressEscape: false, + beforeClose: (action, instance, done) => { + if (action === 'confirm') { + // 用户点击了"我已支付"按钮,开始检查支付状态 + startPaymentStatusCheck(orderId) + } else { + // 用户点击了"支付遇到问题"按钮 + ElMessage.warning('如果支付遇到问题,请稍后重试或联系客服') + } + done() + } + }).catch(() => { + // 对话框被关闭(通过取消按钮或其他方式) + }) + + // 不立即开始检查支付状态,等待用户确认支付后再开始 + // 这样可以避免过早开始状态检查导致用户体验不佳 +} + +// 开始检查支付状态 +const startPaymentStatusCheck = (orderId) => { + if (!orderId) { + ElMessage.error('订单ID不存在') + return + } + + // 显示支付状态检查提示 + const loadingInstance = ElLoading.service({ + lock: false, // 不锁定屏幕,允许用户操作 + text: '正在检查支付状态,请完成支付后等待(检查超过1分钟将自动刷新页面)...', + background: 'rgba(0, 0, 0, 0.5)' + }) + + // 显示一个提示对话框,告知用户正在检查支付状态 + ElMessageBox.alert( + '我们正在检测您的支付状态,支付完成后会自动为您下载示例报告。检查超过1分钟页面将自动刷新以获取最新状态。请勿关闭此页面。', + '支付状态检测', + { + confirmButtonText: '我知道了', + type: 'info', + showClose: false, + closeOnClickModal: false, + closeOnPressEscape: false + } + ).catch(() => { + // 用户点击了确定按钮,继续检查 + }) + + let checkCount = 0 + const maxCheckCount = 60 // 最多检查3分钟(60次 * 3秒) + + // 设置定时器检查支付状态 + const checkInterval = setInterval(async () => { + checkCount++ + + // 更新提示文本 + loadingInstance.text = `正在检查支付状态... (${Math.ceil(checkCount/20)}分钟/${Math.ceil(maxCheckCount/20)}分钟)` + + try { + const response = await productApi.checkComponentReportPaymentStatus(orderId) + // 正确处理响应数据格式 + const responseData = response.data.data || response.data + const { payment_status, can_download } = responseData + + console.log('[startPaymentStatusCheck] 支付状态检查结果:', { + payment_status, + can_download, + check_count: checkCount + }) + + if (payment_status === 'success' && can_download) { + // 支付成功 + clearInterval(checkInterval) + loadingInstance.close() + + canDownloadReport.value = true + ElMessage.success('支付成功,现在可以下载示例报告了') + + // 关闭之前的提示框 + ElMessageBox.close() + + // 自动下载 + downloadComponentReport() + } else if (payment_status === 'failed') { + // 支付失败 + clearInterval(checkInterval) + loadingInstance.close() + + // 关闭之前的提示框 + ElMessageBox.close() + + ElMessage.error('支付失败,请重试') + } else if (payment_status === 'cancelled') { + // 支付取消 + clearInterval(checkInterval) + loadingInstance.close() + + // 关闭之前的提示框 + ElMessageBox.close() + + ElMessage.info('支付已取消') + } else if (checkCount >= 20) { + // 检查超过1分钟(20次 * 3秒) + clearInterval(checkInterval) + loadingInstance.close() + + // 关闭之前的提示框 + ElMessageBox.close() + + ElMessage.warning('支付状态检查超过1分钟,页面将自动刷新以获取最新状态') + + // 1秒后刷新页面 + setTimeout(() => { + window.location.reload() + }, 1000) + } else if (checkCount >= maxCheckCount) { + // 超时(3分钟) + clearInterval(checkInterval) + loadingInstance.close() + + // 关闭之前的提示框 + ElMessageBox.close() + + ElMessage.warning('支付状态检查超时,请刷新页面后检查') + + // 显示手动检查选项 + ElMessageBox.confirm( + '支付状态检查已超时,您可以选择继续等待或稍后手动检查支付状态。', + '支付状态检查', + { + confirmButtonText: '继续等待', + cancelButtonText: '稍后手动检查', + type: 'warning' + } + ).then(() => { + // 继续等待,重置计数器 + checkCount = 0 + // 继续检查,不再设置超时 + }).catch(() => { + // 用户选择稍后手动检查 + ElMessage.info('您可以在页面中点击"下载示例报告"按钮来手动检查支付状态') + }) + } + } catch (error) { + console.error('[startPaymentStatusCheck] 检查支付状态失败:', error) + console.error('[startPaymentStatusCheck] 错误详情:', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }) + + // 不显示错误消息,避免打扰用户,继续检查 + } + }, 3000) // 每3秒检查一次,减少服务器压力 + + // 将定时器ID存储在组件实例中,以便在组件卸载时清理 + if (!window.paymentCheckIntervals) { + window.paymentCheckIntervals = {} + } + window.paymentCheckIntervals[orderId] = checkInterval +} + +// 手动检查支付状态 +const checkPaymentStatusManually = async () => { + if (!currentReportOrderId.value) { + ElMessage.warning('没有正在处理的支付订单') + return + } + + checkingPaymentStatus.value = true + try { + const response = await productApi.checkComponentReportPaymentStatus(currentReportOrderId.value) + const { payment_status, can_download } = response.data + + console.log('[checkPaymentStatusManually] 支付状态检查结果:', { + payment_status, + can_download + }) + + if (payment_status === 'success' && can_download) { + // 支付成功 + canDownloadReport.value = true + ElMessage.success('支付成功,现在可以下载示例报告了') + + // 自动下载 + downloadComponentReport() + } else if (payment_status === 'failed') { + // 支付失败 + ElMessage.error('支付失败,请重试') + } else if (payment_status === 'pending') { + // 支付中 + ElMessage.info('支付仍在处理中,请稍后再次检查') + } else { + // 其他状态 + ElMessage.warning(`支付状态: ${payment_status}`) + } + } catch (error) { + console.error('[checkPaymentStatusManually] 检查支付状态失败:', error) + ElMessage.error('检查支付状态失败,请稍后重试') + } finally { + checkingPaymentStatus.value = false + } +} diff --git a/src/router/index.js b/src/router/index.js index 447de42..ff20bee 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -136,6 +136,12 @@ const routes = [ name: 'FinanceRechargeRecords', component: () => import('@/pages/finance/recharge-records/index.vue'), meta: { title: '充值记录' } + }, + { + path: 'purchase-records', + name: 'FinancePurchaseRecords', + component: () => import('@/pages/finance/purchase-records/index.vue'), + meta: { title: '购买记录' } } ] }, @@ -292,6 +298,12 @@ const routes = [ name: 'AdminUIComponents', component: () => import('@/pages/admin/ui-components/index.vue'), meta: { title: '组件管理' } + }, + { + path: 'purchase-records', + name: 'AdminPurchaseRecords', + component: () => import('@/pages/admin/purchase-records/index.vue'), + meta: { title: '购买记录管理' } } ] }, diff --git a/src/utils/request.js b/src/utils/request.js index 3c812b6..4a4519c 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -52,14 +52,16 @@ request.interceptors.request.use( // 响应拦截器 request.interceptors.response.use( response => { - const { data, status } = response + const { data, status, config } = response // 检查HTTP状态码 if (status >= 200 && status < 300) { - // 如果是blob响应(文件下载),直接返回data - if (data instanceof Blob) { - return data + // 检查是否是文件下载API(通过URL判断) + if (config.url && config.url.includes('/generate-and-download')) { + // 直接返回原始响应,让前端处理文件下载 + return response } + // 严格按照后端响应格式处理 if (data.success === true) { // 成功响应,返回data字段