| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="visible" |
| | | :title="dialogTitle" |
| | | :before-close="handleClose" |
| | | destroy-on-close |
| | | class="product-price-dialog" |
| | | :width="dialogWidth" |
| | | > |
| | | <div> |
| | | <div class="price-viewer-container" v-loading="loading"> |
| | | <!-- 价格对比表格 --> |
| | | <div class="pricing-table-container" v-if="showPricePanel && priceList.length > 0"> |
| | |
| | | > |
| | | <div class="pricing-table-wrapper"> |
| | | <table class="pricing-table"> |
| | | <thead> |
| | | <thead> |
| | | <tr> |
| | | <th class="feature-column">功能对比</th> |
| | | <th |
| | |
| | | <span class="label">产品名称:</span><span class="value strong">{{ productHeader.name }}</span> |
| | | </div> |
| | | <div class="grid-item"> |
| | | <span class="label">行业板块:</span><span class="value">{{ productHeader.industry }}</span> |
| | | <span class="label">行业板块:</span><span class="value">{{ productHeader.industrialChainName }}</span> |
| | | </div> |
| | | <div class="grid-item"> |
| | | <span class="label">提供者:</span><span class="value">{{ productHeader.provider }}</span> |
| | | <span class="label">提供者:</span><span class="value">{{ productHeader.submissionUnit }}</span> |
| | | </div> |
| | | <div class="grid-item"> |
| | | <span class="label">用户姓名:</span><span class="value">{{ userInfo.name }}</span> |
| | | </div> |
| | | <div class="grid-item"> |
| | | <span class="label">单位:</span><span class="value">{{ userInfo.unit }}</span> |
| | | <span class="label">单位:</span><span class="value">{{ userInfo.unitName }}</span> |
| | | </div> |
| | | <div class="grid-item"> |
| | | <span class="label">部门:</span><span class="value">{{ userInfo.department }}</span> |
| | | <span class="label">部门:</span><span class="value">{{ userInfo.departmentName }}</span> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | |
| | | <el-empty description="暂无价格信息" /> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer v-if="!showOrderStatus"> |
| | | <span class="dialog-footer"> |
| | | <template v-if="showPricePanel"> |
| | | <el-button @click="handleClose">关闭</el-button> |
| | | <el-button type="primary" @click="handleOrder">立即订购</el-button> |
| | | </template> |
| | | <template v-else> |
| | | <el-button @click="returnPricePanel">返回价格对比</el-button> |
| | | <el-button type="primary" @click="submitOrder">提交申请</el-button> |
| | | </template> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <div class="footer" v-if="showPricePanel"> |
| | | <el-button type="primary" @click="handleOrder">立即订购</el-button> |
| | | </div> |
| | | <div class="footer" v-else> |
| | | <el-button @click="returnPricePanel">返回价格对比</el-button> |
| | | <el-button type="primary" @click="submitOrder">提交申请</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 产品订购对话框移除,改为内嵌展示 --> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { ref, watch, computed } from 'vue' |
| | | import { ref, watch, computed, onMounted } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useRoute } from 'vue-router' |
| | | import productPricingApi from '@/api/productPricingApi' |
| | | import cartApi from '@/api/cartApi' |
| | | import orderApi from '@/api/orderApi' |
| | | import { ShoppingCart, Coin, Money } from '@element-plus/icons-vue' |
| | | import { useUserInfo } from '@/stores/modules/userInfo' |
| | | import { queryUserDetail } from '@/api/userInfo' |
| | | import productApi from '@/api/productApi' |
| | | |
| | | const route = useRoute() |
| | | |
| | |
| | | enableStatus: 'ENABLED' | 'DISABLED' |
| | | } |
| | | |
| | | interface Props { |
| | | modelValue: boolean |
| | | productId?: string |
| | | width?: string |
| | | } |
| | | |
| | | interface Emits { |
| | | (e: 'update:modelValue', value: boolean): void |
| | | (e: 'order', selectedItems: PriceItem[]): void |
| | | } |
| | | |
| | | // 兼容 path 参数 productId 与 query 参数 id/productId |
| | | const currentProductId = computed<string | undefined>(() => { |
| | | return (route.params.productId as string) || (route.query.productId as string) || (route.query.id as string) |
| | | }) |
| | | |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | modelValue: false, |
| | | productId: '', |
| | | width: '90%' |
| | | }) |
| | | |
| | | |
| | | const emit = defineEmits<Emits>() |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (value) => emit('update:modelValue', value) |
| | | }) |
| | | |
| | | const loading = ref(false) |
| | | const activeTab = ref('enterprise') |
| | |
| | | const showPricePanel = ref(true) |
| | | const showOrderStatus = ref(false) |
| | | // 模拟用户信息(实际应从用户状态获取) |
| | | const currentUserId = ref(1) |
| | | const currentUnitId = ref(1) |
| | | const userStore = useUserInfo() |
| | | let currentUserId = computed(() => userStore.getUserId || userStore.getUserInfo?.userId) |
| | | const currentUnitId = computed(() => userStore.getUnitId || userStore.getUserInfo?.unitId || '') |
| | | |
| | | onMounted(async () => { |
| | | if (!currentUserId.value) { |
| | | try { |
| | | const res: any = await queryUserDetail() |
| | | if (res?.code === 200 && res.data) { |
| | | userStore.updateUserDetail(res.data) |
| | | currentUserId = res.data.userId || res.data.id |
| | | userInfo.value = res.data |
| | | } else { |
| | | ElMessage.error(res?.msg || '无法获取用户信息,请先登录') |
| | | return |
| | | } |
| | | } catch (e) { |
| | | console.error('获取用户详情失败:', e) |
| | | ElMessage.error('获取用户信息失败,请稍后重试') |
| | | return |
| | | } |
| | | }else{ |
| | | userInfo.value = userStore.getUserInfos |
| | | } |
| | | |
| | | }) |
| | | type PriceTypeKey = 'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE' |
| | | interface OrderSuite extends PriceItem { |
| | | selected: boolean |
| | |
| | | |
| | | // 产品信息/用户信息(静态) |
| | | const productHeader = ref({ |
| | | name: '中交方远智能实测实量管理系统', |
| | | industry: '公路,市政,建筑', |
| | | provider: '中交建筑集团第一工程有限公司' |
| | | name: '', |
| | | industrialChainName: '', |
| | | submissionUnit: '', |
| | | createUserId: '' |
| | | }) |
| | | const userInfo = ref({ |
| | | name: '张静', |
| | | unit: '信科集团', |
| | | department: '门户系统临时组' |
| | | name: '', |
| | | departmentName: '', |
| | | unitName: '' |
| | | }) |
| | | |
| | | // 全选 |
| | |
| | | const suite = orderSuites.value[index] |
| | | if (!suite) return |
| | | try { |
| | | await removeFromCart(suite.id) |
| | | await removeFromCart(String(suite.id)) |
| | | orderSuites.value.splice(index, 1) |
| | | } catch (e) { |
| | | } |
| | |
| | | ElMessage.success('订购申请提交成功!') |
| | | // 清空选择状态 |
| | | selectedSuiteIds.value.clear() |
| | | // 关闭价格查看器 |
| | | visible.value = false |
| | | } |
| | | |
| | | // 弹窗标题 |
| | |
| | | return `【${productName.value}】产品定价` |
| | | }) |
| | | |
| | | // 弹窗宽度 |
| | | const dialogWidth = computed(() => { |
| | | return props.width |
| | | }) |
| | | |
| | | // 按客户对象分组的价格数据 |
| | | const groupedPriceData = computed(() => { |
| | |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | //获取产品详情信息 |
| | | try { |
| | | // productDetail.value = mockProductMap[productId] || null |
| | | const data = { |
| | | id: productId |
| | | } |
| | | const detailRes: any = await productApi.getProductById(data) |
| | | if (detailRes?.code === 200) { |
| | | productHeader.value = detailRes.data || { name: '', industrialChainName: '', submissionUnit: '', createUserId: '' } |
| | | } else { |
| | | productHeader.value = { name: '', industrialChainName: '', submissionUnit: '', createUserId: '' } |
| | | ElMessage.error(detailRes?.msg || '获取产品详情失败') |
| | | } |
| | | } catch (e) { |
| | | productHeader.value = { name: '', industrialChainName: '', submissionUnit: '', createUserId: '' } |
| | | ElMessage.error('获取产品详情失败') |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | // 监听产品ID变化 |
| | | watch(() => props.productId, (newProductId) => { |
| | | if (newProductId && visible.value) { |
| | | watch(currentProductId, (newProductId) => { |
| | | if (newProductId) { |
| | | fetchProductData(newProductId) |
| | | } |
| | | }) |
| | | |
| | | // 监听弹窗显示状态 |
| | | watch(() => visible.value, (newVisible) => { |
| | | if (newVisible && props.productId) { |
| | | fetchProductData(props.productId) |
| | | } |
| | | }) |
| | | |
| | | // 关闭弹窗 |
| | | const handleClose = () => { |
| | | visible.value = false |
| | | showOrderPanel.value = false |
| | | showOrderStatus.value = false |
| | | showPricePanel.value = true |
| | | orderSuites.value = [] |
| | | selectedSuiteIds.value.clear() |
| | | } |
| | | },{ immediate: true }) |
| | | |
| | | // 套件选择处理 |
| | | const handleSuiteSelect = (suiteId: number, checked: any) => { |
| | |
| | | } else { |
| | | selectedSuiteIds.value.delete(suiteId) |
| | | // 从购物车移除 |
| | | removeFromCart(suiteId) |
| | | removeFromCart(String(suiteId)) |
| | | } |
| | | } |
| | | |
| | |
| | | ElMessage.error('暂不支持货币交易,请选择积分或协议支付方式') |
| | | return |
| | | } |
| | | |
| | | |
| | | if (!idempotencyToken.value) { |
| | | ElMessage.error('下单Token获取失败,请返回重试') |
| | | return |
| | | } |
| | | |
| | | const hasPoints = items.some(item => item.priceType === 'POINTS') |
| | | const hasAGREEMENT = items.some(item => item.priceType === 'AGREEMENT') |
| | | let paymentType = '' |
| | | if(hasPoints){ |
| | | paymentType = '积分' |
| | | }else{ |
| | | paymentType = '协议' |
| | | } |
| | | // 组装创建订单参数(CreateOrderDTO) |
| | | const payload = { |
| | | userId: currentUserId.value, |
| | | unitId: currentUnitId.value, |
| | | productName: productHeader.value.name, |
| | | providerName: productHeader.value.provider, |
| | | providerId: 3, |
| | | paymentType: '积分', |
| | | providerName: productHeader.value.submissionUnit, |
| | | providerId: productHeader.value.createUserId, |
| | | paymentType: paymentType, |
| | | buyerRemarks: '', |
| | | items: items.map(it => ({ |
| | | pricingId: it.id, |
| | | productId: Number(it.productId), |
| | | productId: it.productId, |
| | | suiteName: getProductSuiteText(it.productSuite), |
| | | salesForm: getSalesFormText(it.salesForm), |
| | | customerType: getCustomerObjectText(it.customerObject), |
| | |
| | | quantity: it.quantity, |
| | | duration: it.duration, |
| | | totalPrice: undefined, |
| | | providerId: 3, |
| | | providerName: productHeader.value.provider, |
| | | providerId: productHeader.value.createUserId, |
| | | providerName: productHeader.value.submissionUnit, |
| | | remarks: '' |
| | | })) |
| | | } |
| | |
| | | orderStatus.value = { |
| | | id: data.orderId || '—', |
| | | productName: data.productName || productHeader.value.name, |
| | | provider: data.providerName || productHeader.value.provider, |
| | | provider: data.providerName || productHeader.value.submissionUnit, |
| | | status: 'SUBMITTED', |
| | | submitTime: data.applyTime ? String(data.applyTime) : new Date().toLocaleString(), |
| | | applyTime: data.applyTime ? String(data.applyTime) : new Date().toLocaleString() |
| | | } |
| | | // 调用工作流接口发起审批流程,拿到流程实例ID后回写订单workflow_id |
| | | try { |
| | | // 根据是否包含协议明细,配置不同流程定义与业务Key(先用静态值占位) |
| | | const processdefId = hasAGREEMENT ? 'Process_Agreement_Static' : 'Process_Points_Static' |
| | | const businessKey = hasAGREEMENT ? 'agreement_biz_key' : 'points_biz_key' |
| | | const wfRes: any = await orderApi.startWorkflowAndComplete({ |
| | | processdefId, |
| | | userid: String(currentUserId.value || ''), |
| | | businessKey |
| | | }) |
| | | if (wfRes?.code === 200 && wfRes.data?.processinstId) { |
| | | await orderApi.updateWorkflowId(data.orderId, wfRes.data.processinstId) |
| | | } |
| | | } catch (e) { |
| | | console.warn('启动工作流失败或更新workflow_id失败', e) |
| | | } |
| | | |
| | | // 清空购物车(后端 + 本地状态) |
| | | try { |
| | | const clearRes: any = await cartApi.clearCart(currentUserId.value, currentUnitId.value) |
| | |
| | | try { |
| | | const cartData = { |
| | | pricingId: priceItem.id, |
| | | productId: Number(priceItem.productId), |
| | | productId: priceItem.productId, |
| | | productName: productName.value, |
| | | suiteName: getProductSuiteText(priceItem.productSuite), |
| | | salesForm: getSalesFormText(priceItem.salesForm), |
| | |
| | | } |
| | | } |
| | | |
| | | const removeFromCart = async (pricingId: number) => { |
| | | const removeFromCart = async (pricingId: string) => { |
| | | try { |
| | | const res: any = await cartApi.removeFromCart(currentUserId.value, currentUnitId.value, pricingId) |
| | | if (res?.code === 200) { |
| | |
| | | .pricing-table-container { |
| | | .pricing-tabs { |
| | | :deep(.el-tabs__header) { |
| | | margin-bottom: 5px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | /* 居中 tabs 导航 */ |
| | | :deep(.el-tabs__nav-wrap) { |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | :deep(.el-tabs__nav) { |
| | | margin: 0 auto; |
| | | border: none !important; |
| | | } |
| | | /* 卡片型 tabs 美化为圆角胶囊效果 */ |
| | | :deep(.el-tabs--card > .el-tabs__header .el-tabs__nav) { |
| | | border: none; |
| | | } |
| | | :deep(.el-tabs--card > .el-tabs__header .el-tabs__item) { |
| | | border: none; |
| | | } |
| | | :deep(.el-tabs__item) { |
| | | border-radius: 999px; |
| | | padding: 8px 18px; |
| | | margin: 0 6px; |
| | | color: #606266; |
| | | background: #f6f8fb; |
| | | transition: all .2s ease; |
| | | } |
| | | :deep(.el-tabs__item:hover) { |
| | | color: #3a7afe; |
| | | background: #eff4ff; |
| | | } |
| | | :deep(.el-tabs__item.is-active) { |
| | | font-weight: bold; |
| | | font-weight: 600; |
| | | color: #fff; |
| | | background: #3a7afe; |
| | | box-shadow: 0 6px 16px rgba(58, 122, 254, 0.25); |
| | | } |
| | | } |
| | | |
| | | .pricing-table-wrapper { |
| | | overflow-x: auto; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06); |
| | | padding: 8px; |
| | | |
| | | .pricing-table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | border: 1px solid #e4e7ed; |
| | | border-collapse: separate; |
| | | border-spacing: 0; |
| | | background: #fff; |
| | | border-radius: 10px; |
| | | |
| | | th, td { |
| | | border: 1px solid #e4e7ed; |
| | | padding: 6px 8px; |
| | | border-bottom: 1px solid #eef2f7; |
| | | border-right: 1px solid #f3f5f7; |
| | | padding: 12px 14px; |
| | | text-align: center; |
| | | vertical-align: middle; |
| | | font-size: 16px; |
| | | font-size: 14px; |
| | | } |
| | | tr th:first-child, td:first-child{ |
| | | font-weight: 700; |
| | |
| | | } |
| | | |
| | | th { |
| | | background-color: #f5f7fa; |
| | | background: linear-gradient(180deg, #f7f9fc 0%, #eef2f7 100%); |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | |
| | | |
| | | .sub-header { |
| | | th { |
| | | background-color: #fafafa; |
| | | background-color: #f8fafc; |
| | | font-weight: 500; |
| | | font-size: 16px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | .feature-value { |
| | | color: #606266; |
| | | background: #fafafa; /* 行为灰色 */ |
| | | color: #303133; |
| | | background: #fff; |
| | | } |
| | | |
| | | tbody tr:hover td { |
| | | background: #fafcff; |
| | | } |
| | | |
| | | .order-methods { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | gap: 6px; |
| | | align-items: center; |
| | | |
| | | .method-item { |
| | | font-size: 16px; |
| | | color: #409eff; |
| | | font-size: 12px; |
| | | color: #3a7afe; |
| | | background: #f4f7ff; |
| | | border: 1px solid #e3ebff; |
| | | border-radius: 999px; |
| | | padding: 2px 10px; |
| | | } |
| | | } |
| | | |
| | | .price-info { |
| | | display: flex; |
| | | flex-direction: column; /* 竖排 */ |
| | | gap: 8px; |
| | | gap: 10px; |
| | | align-items: center; |
| | | |
| | | .price-item { |
| | |
| | | align-items: flex-start; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | padding: 10px 12px; |
| | | border: 1px solid #f0f2f5; |
| | | border-radius: 10px; |
| | | background: #fff; |
| | | box-shadow: inset 0 -1px 0 rgba(0,0,0,0.02); |
| | | .price-lable-icon{ |
| | | display: flex; |
| | | gap: 6px; |
| | | } |
| | | .price-label { |
| | | font-size: 16px; |
| | | color: #606266; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .price-value { |
| | | font-weight: 500; |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | font-size: 18px; |
| | | &.points { |
| | | color: #e7900d; |
| | | } |
| | | |
| | | &.currency { |
| | | color: #e7900d; |
| | | color: #10b981; |
| | | } |
| | | |
| | | &.free { |
| | |
| | | } |
| | | } |
| | | .price-icon.points { color: #e6a23c; } |
| | | .price-icon.currency { color: #e7900d; } |
| | | .price-icon.currency { color: #16a34a; } |
| | | } |
| | | |
| | | .add-checkbox { |
| | |
| | | justify-content: center; |
| | | align-items: center; |
| | | padding: 8px 0; |
| | | .add-cart-icon { color: #409eff; font-size:22px} |
| | | .add-cart-icon { color: #3a7afe; font-size:22px} |
| | | } |
| | | } |
| | | } |
| | |
| | | .order-summary .summary-value.highlight{ color:#409eff; } |
| | | .order-summary .summary-value.total{ color:#e6a23c; } |
| | | .order-summary .summary-value.currency{ color:#f56c6c; } |
| | | .footer { display: flex; flex-direction: row-reverse; margin-right: 50px; gap: 50px; margin-top: 20px;} |
| | | </style> |