From 954446f5a88735b0033b49492ea7b658f2bb9e07 Mon Sep 17 00:00:00 2001 From: seatonwan9 Date: 星期二, 19 八月 2025 01:37:53 +0800 Subject: [PATCH] 定价管理-产品价格 --- src/views/productManage/product/index.vue | 6 src/views/layout/components/sideBar/index.vue | 2 src/views/productManage/productPriceViewer/index.vue | 793 ++++++++++++++++++++++++++----------- src/views/productManage/price/index.vue | 454 ++++++++++++++------- 4 files changed, 871 insertions(+), 384 deletions(-) diff --git a/src/views/layout/components/sideBar/index.vue b/src/views/layout/components/sideBar/index.vue index deb2260..048351d 100644 --- a/src/views/layout/components/sideBar/index.vue +++ b/src/views/layout/components/sideBar/index.vue @@ -160,7 +160,7 @@ "id": "4fa90aab-2e3c-4bc4-abbf-fbe536723fa2", "icon": "", "name": "鏌ョ湅鎶ヤ环", - "url": "/producePrice/priceViewer", + "url": "/product/priceViewer", "parentId": "266e3ff1-ca98-4946-8975-7526ef83cd5c", "sort": "3" }] diff --git a/src/views/productManage/price/index.vue b/src/views/productManage/price/index.vue index 0cecbf8..55821cb 100644 --- a/src/views/productManage/price/index.vue +++ b/src/views/productManage/price/index.vue @@ -18,13 +18,13 @@ </template> <el-descriptions v-if="productDetail" :column="2" border> - <el-descriptions-item label="浜у搧鍚嶇О">{{ productDetail.name }}</el-descriptions-item> - <el-descriptions-item label="鎻愭姤鍗曚綅">{{ productDetail.submitUnit }}</el-descriptions-item> - <el-descriptions-item label="鎻愭姤浜�">{{ productDetail.submitter }}</el-descriptions-item> - <el-descriptions-item label="琛屼笟棰嗗煙">{{ productDetail.industry }}</el-descriptions-item> - <el-descriptions-item label="鍗曚綅宸ョ▼">{{ productDetail.projectUnit }}</el-descriptions-item> - <el-descriptions-item label="浜т笟闃舵">{{ productDetail.industryStage }}</el-descriptions-item> - <el-descriptions-item label="浜у搧绫诲瀷">{{ productDetail.productType }}</el-descriptions-item> + <el-descriptions-item label="浜у搧鍚嶇О" label-width="10%">{{ productDetail.name }}</el-descriptions-item> + <el-descriptions-item label="鎻愭姤鍗曚綅" label-width="10%">{{ productDetail.submitUnit }}</el-descriptions-item> + <el-descriptions-item label="鎻愭姤浜�" label-width="10%">{{ productDetail.submitter }}</el-descriptions-item> + <el-descriptions-item label="琛屼笟棰嗗煙" label-width="10%">{{ productDetail.industry }}</el-descriptions-item> + <el-descriptions-item label="鍗曚綅宸ョ▼" label-width="10%">{{ productDetail.projectUnit }}</el-descriptions-item> + <el-descriptions-item label="浜т笟闃舵" label-width="10%">{{ productDetail.industryStage }}</el-descriptions-item> + <el-descriptions-item label="浜у搧绫诲瀷"label-width="10%">{{ productDetail.productType }}</el-descriptions-item> <el-descriptions-item label="浜у搧绠�浠�" :span="3"> <div class="intro">{{ productDetail.description }}</div> </el-descriptions-item> @@ -43,16 +43,22 @@ </div> </template> <div class="pricing-cards-container" v-if="priceList.length > 0"> - <div class="pricing-cards-wrapper"> - <div - v-for="pricing in priceList" - :key="pricing.id" - class="pricing-card" - :class="{ - 'pricing-card-enabled': pricing.enableStatus === 'ENABLED', - 'pricing-card-disabled': pricing.enableStatus === 'DISABLED' - }" - > + <div + v-for="group in priceList" + :key="group.suiteName" + class="pricing-group" + > + <div class="group-header">{{ group.suiteName }}</div> + <div class="pricing-cards-wrapper"> + <div + v-for="pricing in group.items" + :key="pricing.id" + class="pricing-card" + :class="{ + 'pricing-card-enabled': pricing.enableStatus === 'ENABLED', + 'pricing-card-disabled': pricing.enableStatus === 'DISABLED' + }" + > <div class="pricing-card-table"> <div class="pricing-row"> <div class="pricing-cell label-cell">閿�鍞舰寮�</div> @@ -71,19 +77,19 @@ <div class="pricing-cell value-cell purchase-methods-cell"> <div class="method-item"> <span class="method-label">绉垎:</span> - <span class="method-value points">{{ pricing.priceSettings?.includes('POINTS') ? (pricing.pointsAmount || '-') + '/濂�' : '-' }}</span> + <span class="method-value points">{{ pricing.priceSettings?.includes('POINTS') ? (pricing.pointsPrice || '-') + '/' + getPriceUnitText(pricing.priceUnit) : '-' }}</span> </div> - <div class="method-item"> + <div class="method-item" v-if="pricing.priceSettings?.includes('CURRENCY')"> <span class="method-label">璐у竵:</span> - <span class="method-value currency">{{ pricing.priceSettings?.includes('CURRENCY') ? (pricing.currencyAmount || '-') + '/濂�' : '-' }}</span> + <span class="method-value currency">{{ (pricing.currencyPrice || '-') + '/' + getPriceUnitText(pricing.priceUnit) }}</span> </div> - <div class="method-item"> + <div class="method-item" v-if="pricing.priceSettings?.includes('AGREEMENT')"> <span class="method-label">鍗忚:</span> - <span class="method-value agreement">{{ pricing.priceSettings?.includes('AGREEMENT') ? '姣忓' : '-' }}</span> + <span class="method-value agreement">{{ '/' + getPriceUnitText(pricing.priceUnit) }}</span> </div> - <div class="method-item"> + <div class="method-item" v-if="pricing.priceSettings?.includes('FREE')"> <span class="method-label">鍏嶈垂:</span> - <span class="method-value free">{{ pricing.priceSettings?.includes('FREE') ? '鍏嶈垂' : '-' }}</span> + <span class="method-value free">{{ '/' + getPriceUnitText(pricing.priceUnit) }}</span> </div> </div> </div> @@ -109,6 +115,7 @@ </el-icon> {{ pricing.enableStatus === 'ENABLED' ? '鍋滅敤' : '鍚敤' }} </span> + </div> </div> </div> </div> @@ -136,57 +143,48 @@ size="large" class="price-form" > - <el-row :gutter="8"> - <el-col :span="12"> - <el-form-item label="浜у搧濂椾欢" prop="productSuite"> - <el-select v-model="formData.productSuite" placeholder="璇烽�夋嫨" style="width: 100%"> + + <el-form-item label="浜у搧濂椾欢" prop="productSuite"> + <el-select v-model="formData.productSuite" placeholder="璇烽�夋嫨" style="width: 60%"> <el-option v-for="opt in productSuiteOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> </el-select> </el-form-item> - </el-col> - <el-col :span="12"> - <el-form-item label="瀹㈡埛瀵硅薄" prop="customerObject"> + <el-form-item label="瀹㈡埛瀵硅薄" prop="customerObject"> <el-radio-group v-model="formData.customerObject"> <el-radio v-for="opt in customerObjectOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-radio> </el-radio-group> - </el-form-item> - </el-col> - </el-row> - + </el-form-item> <el-form-item label="閿�鍞舰寮�" prop="salesForm" class="sales-form-item"> <el-radio-group v-model="formData.salesForm" class="sales-form-group"> <el-radio v-for="opt in salesFormOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-radio> </el-radio-group> </el-form-item> - <el-row :gutter="8"> - <el-col :span="12"> + <el-form-item label="璐︽埛鏁伴噺" prop="accountQuantity"> <div class="quantity-input-group"> <el-input-number v-model="formData.accountQuantity" :min="1" :controls="false" style="width: 180px" :disabled="formData.accountQuantityUnlimited" /> <el-checkbox v-model="formData.accountQuantityUnlimited" style="margin-left: 6px">涓嶉檺</el-checkbox> </div> </el-form-item> - </el-col> - <el-col :span="12"> + <el-form-item label="骞跺彂鑺傜偣" prop="concurrentNodeQuantity"> <div class="quantity-input-group"> <el-input-number v-model="formData.concurrentNodeQuantity" :min="1" :controls="false" style="width: 180px" :disabled="formData.concurrentNodeQuantityUnlimited" /> <el-checkbox v-model="formData.concurrentNodeQuantityUnlimited" style="margin-left: 6px">涓嶉檺</el-checkbox> </div> </el-form-item> - </el-col> - </el-row> + <el-form-item label="浠锋牸璁剧疆" prop="priceSettings"> <div class="price-settings"> <div class="price-setting-item"> <el-checkbox label="POINTS" :disabled="formData.priceSettings.includes('FREE')" v-model="checkboxPoints">绉垎</el-checkbox> - <el-input-number v-if="formData.priceSettings.includes('POINTS')" v-model="formData.pointsAmount" :min="0" :precision="2" :controls="false" placeholder="绉垎閲戦" style="width: 120px; margin-left: 6px" /> + <el-input-number v-if="formData.priceSettings.includes('POINTS')" v-model="formData.pointsPrice" :min="0" :precision="2" :controls="false" placeholder="绉垎閲戦" style="width: 120px; margin-left: 6px" /> </div> <div class="price-setting-item"> <el-checkbox label="CURRENCY" :disabled="formData.priceSettings.includes('FREE')" v-model="checkboxCurrency">璐у竵</el-checkbox> - <el-input-number v-if="formData.priceSettings.includes('CURRENCY')" v-model="formData.currencyAmount" :min="0" :precision="2" :controls="false" placeholder="璐у竵閲戦" style="width: 120px; margin-left: 6px" /> + <el-input-number v-if="formData.priceSettings.includes('CURRENCY')" v-model="formData.currencyPrice" :min="0" :precision="2" :controls="false" placeholder="璐у竵閲戦" style="width: 120px; margin-left: 6px" /> </div> <div class="price-setting-item"> <el-checkbox label="AGREEMENT" :disabled="formData.priceSettings.includes('FREE')" v-model="checkboxAgreement">鍗忚</el-checkbox> @@ -197,24 +195,20 @@ </div> </el-form-item> - <el-row :gutter="8"> - <el-col :span="12"> + + <el-form-item label="浠锋牸鍗曚綅" prop="priceUnit"> <el-select v-model="formData.priceUnit" placeholder="璇烽�夋嫨" style="width: 100%"> <el-option v-for="opt in priceUnitOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> </el-select> </el-form-item> - </el-col> - <el-col :span="12"> + <el-form-item label="鍚敤鐘舵��" prop="enableStatus"> <el-radio-group v-model="formData.enableStatus"> <el-radio v-for="opt in enableStatusOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-radio> </el-radio-group> </el-form-item> - </el-col> - </el-row> - - + <el-form-item label="澶囨敞"> <el-input type="textarea" v-model="formData.remark" :rows="3" placeholder="璇疯緭鍏ュ娉�" /> @@ -244,6 +238,8 @@ import { useRoute } from 'vue-router' import { Goods, Lock, Unlock } from '@element-plus/icons-vue' import ProductPriceViewer from '@/views/productManage/productPriceViewer/index.vue' +import productPricingApi from '@/api/productPricingApi' + interface ProductDetail { id: string @@ -269,8 +265,8 @@ concurrentNodeQuantity: number | '' concurrentNodeQuantityUnlimited: boolean priceSettings: Array<'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE'> - pointsAmount: number | '' - currencyAmount: number | '' + pointsPrice: number | '' + currencyPrice: number | '' priceUnit: '' | 'SET' | 'SET_PER_YEAR' | 'YEAR' enableStatus: 'ENABLED' | 'DISABLED' authorizationStartTime?: string @@ -302,8 +298,8 @@ // 妯℃嫙浜у搧璇︽儏鏁版嵁婧� const mockProductMap: Record<string, ProductDetail> = { - '10001': { - id: '10001', + '1': { + id: '1', name: '鏁板瓧鍖栦骇鍝丄', submitUnit: '涓氦涓�鍏眬', submitter: '寮犱笁', @@ -314,8 +310,8 @@ description: '鏈骇鍝佸畾浣嶄负浠ュ缓璁炬湡BIM鏁板瓧璧勪骇浣滀负鏁板瓧搴曠洏锛岀粨鍚堥」鐩繍钀ョ淮淇濋渶姹傜殑瀹炴椂鎬с�佷氦浜掓�с�佷究鎹锋�х殑涓夌淮鍙鍖栬繍缁寸鐞嗙郴缁熴�傜郴缁熸彁渚涢」鐩暟瀛楀寲銆佹櫤鑳藉寲杩愮淮绠$悊鍔熻兘锛岃兘澶熻В鍐冲缓绛戣繍琛岀淮鎶ょ鐞嗕腑鐨勫疄闄呴棶棰橈紝瀹炵幇淇℃伅蹇�熸暣鍚堜笌鏌ヨ銆佷俊鎭湁鏁堝叡浜笌浼犻�掞紝鎻愬崌椤圭洰缁煎悎绠$悊涓庣淮鎶ゆ按骞炽��', shelfStatus: '寰呬笂鏋�' }, - '10002': { - id: '10002', + '2': { + id: '2', name: '鏁板瓧鍖栦骇鍝丅', submitUnit: '涓氦浜岃埅灞�', submitter: '鏉庡洓', @@ -343,13 +339,13 @@ productSuite: '', salesForm: 'BUYOUT', customerObject: 'ENTERPRISE', - accountQuantity: '', + accountQuantity: 1, accountQuantityUnlimited: false, - concurrentNodeQuantity: '', + concurrentNodeQuantity: 1, concurrentNodeQuantityUnlimited: false, priceSettings: [], - pointsAmount: '', - currencyAmount: '', + pointsPrice: '', + currencyPrice: '', priceUnit: '', enableStatus: 'ENABLED', authorizationStartTime: undefined, @@ -359,15 +355,15 @@ // 閫夐」 const productSuiteOptions = [ - { label: '浼佷笟绉佹湁SaaS鐗堣鍙�', value: 'ENTERPRISE_PRIVATE_SAAS_LICENSE' }, - { label: '浼佷笟绉佹湁SaaS鐗圤TA鏈嶅姟', value: 'ENTERPRISE_PRIVATE_SAAS_OTA' }, - { label: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', value: 'ENTERPRISE_PRIVATE_SAAS_USER_INCREMENT' }, - { label: '浼佷笟鍏湁SaaS鐗堣鍙�', value: 'ENTERPRISE_PUBLIC_SAAS_LICENSE' }, - { label: '浼佷笟鍏湁SaaS鐗圤TA鏈嶅姟', value: 'ENTERPRISE_PUBLIC_SAAS_OTA' }, - { label: '浼佷笟鍏湁SaaS鐗堢敤鎴峰閲忓寘', value: 'ENTERPRISE_PUBLIC_SAAS_USER_INCREMENT' }, - { label: '涓汉鍏湁SaaS鍖栬鍙�', value: 'PERSONAL_PUBLIC_SAAS_LICENSE' }, - { label: 'web杞欢', value: 'WEB_SOFTWARE' }, - { label: '妗岄潰杞欢', value: 'DESKTOP_SOFTWARE' } + { label: '浼佷笟绉佹湁SaaS鐗堣鍙�', value: '浼佷笟绉佹湁SaaS鐗堣鍙�' }, + { label: '浼佷笟绉佹湁SaaS鐗圤TA鏈嶅姟', value: '浼佷笟绉佹湁SaaS鐗圤TA鏈嶅姟' }, + { label: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', value: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘' }, + { label: '浼佷笟鍏湁SaaS鐗堣鍙�', value: '浼佷笟鍏湁SaaS鐗堣鍙�' }, + { label: '浼佷笟鍏湁SaaS鐗圤TA鏈嶅姟', value: '浼佷笟鍏湁SaaS鐗圤TA鏈嶅姟' }, + { label: '浼佷笟鍏湁SaaS鐗堢敤鎴峰閲忓寘', value: '浼佷笟鍏湁SaaS鐗堢敤鎴峰閲忓寘' }, + { label: '涓汉鍏湁SaaS鍖栬鍙�', value: '涓汉鍏湁SaaS鍖栬鍙�' }, + { label: 'web杞欢', value: 'web杞欢' }, + { label: '妗岄潰杞欢', value: '妗岄潰杞欢' } ] const salesFormOptions = [ { label: '涔版柇', value: 'BUYOUT' }, @@ -418,8 +414,8 @@ // 鑻ラ�夋嫨 FREE锛屾竻绌哄叾浠� if (key === 'FREE') { formData.priceSettings = ['FREE'] - formData.pointsAmount = '' - formData.currencyAmount = '' + formData.pointsPrice = '' + formData.currencyPrice = '' return } // 鑻ユ鍓嶆湁 FREE锛屽幓鎺� FREE @@ -456,8 +452,8 @@ validator: (_: any, value: any[], cb: any) => { if (!value || value.length === 0) return cb(new Error('璇疯嚦灏戦�夋嫨涓�绉嶄环鏍艰缃�')) if (value.includes('FREE') && value.length > 1) return cb(new Error('鍏嶈垂閫夐」涓嶈兘涓庡叾浠栭�夐」鍚屾椂閫夋嫨')) - if (value.includes('POINTS') && (formData.pointsAmount === '' || formData.pointsAmount === undefined)) return cb(new Error('璇疯緭鍏ョН鍒嗛噾棰�')) - if (value.includes('CURRENCY') && (formData.currencyAmount === '' || formData.currencyAmount === undefined)) return cb(new Error('璇疯緭鍏ヨ揣甯侀噾棰�')) + if (value.includes('POINTS') && (formData.pointsPrice === '' || formData.pointsPrice === undefined)) return cb(new Error('璇疯緭鍏ョН鍒嗛噾棰�')) + if (value.includes('CURRENCY') && (formData.currencyPrice === '' || formData.currencyPrice === undefined)) return cb(new Error('璇疯緭鍏ヨ揣甯侀噾棰�')) cb() }, trigger: 'change' @@ -499,61 +495,164 @@ return map[unit || ''] || unit || '' } -function fetchProductDetail(productId?: string) { +async function fetchProductDetail(productId?: string) { if (!productId) { productDetail.value = null ElMessage.warning('鏈彁渚涗骇鍝両D锛屾棤娉曞姞杞戒骇鍝佽鎯�') return } - // 妯℃嫙寮傛 loading.value = true - setTimeout(() => { + try { productDetail.value = mockProductMap[productId] || null - // 鏋勯�犲崱鐗囨墍闇�鐨勬紨绀哄畾浠锋暟鎹� - priceList.value = productDetail.value - ? [ - { - id: 1, - productId: productId, - productSuite: 'STANDARD', - salesForm: 'BUYOUT', - customerObject: 'ENTERPRISE', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['POINTS'], - pointsAmount: 50000, - currencyAmount: 0, - priceUnit: 'SET_PER_YEAR', - enableStatus: 'ENABLED', - authorizationStartTime: '2025-01-01', - authorizationEndTime: '2025-12-31', - remark: '婕旂ず鏁版嵁' - }, - { - id: 2, - productId: productId, - productSuite: 'PRO', - salesForm: 'PRIVATE_INCREMENT', - customerObject: 'ENTERPRISE', - accountQuantity: 100, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 100, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['CURRENCY'], - pointsAmount: 0, - currencyAmount: 7500, - priceUnit: 'SET', - enableStatus: 'DISABLED', - authorizationStartTime: '2025-01-01', - authorizationEndTime: '2025-06-30', - remark: '婕旂ず鏁版嵁' - } - ] - : [] + // const detailRes: any = await productApi.getProductDetail({ id: productId }) + // if (detailRes?.code === 200) { + // productDetail.value = detailRes.data || null + // } else { + // productDetail.value = null + // ElMessage.error(detailRes?.msg || '鑾峰彇浜у搧璇︽儏澶辫触') + // } + await loadPricingList(productId) + } catch (e) { + productDetail.value = null + ElMessage.error('鑾峰彇浜у搧璇︽儏澶辫触') + } finally { loading.value = false - }, 300) + } +} + +function mapSalesFormToCN(val: string) { + const map: Record<string, string> = { + BUYOUT: '涔版柇', + LEASE: '绉熻祦', + PRIVATE_INCREMENT: '绉佹湁澧炲寘閲�', + PUBLIC_INCREMENT: '鍏湁澧炲寘閲�', + OTA: 'OTA鏈嶅姟', + CLOUD: '浜戞湇鍔�', + RESOURCE_PACKAGE: '璧勬簮鍖�', + PERSONAL: '涓汉', + } + return map[val] || val +} + +function mapSalesFormFromCN(val: string) { + const map: Record<string, string> = { + '涔版柇': 'BUYOUT', + '绉熻祦': 'LEASE', + '绉佹湁澧炲寘閲�': 'PRIVATE_INCREMENT', + '鍏湁澧炲寘閲�': 'PUBLIC_INCREMENT', + 'OTA鏈嶅姟': 'OTA', + '浜戞湇鍔�': 'CLOUD', + '璧勬簮鍖�': 'RESOURCE_PACKAGE', + '涓汉': 'PERSONAL', + } + return (map[val] as any) || val +} + +function mapCustomerToCN(val: string) { + const map: Record<string, string> = { ENTERPRISE: '浼佷笟', PERSONAL: '涓汉', PROJECT_DEPARTMENT: '椤圭洰閮�' } + return map[val] || val +} + +function mapCustomerFromCN(val: string) { + const map: Record<string, string> = { '浼佷笟': 'ENTERPRISE', '涓汉': 'PERSONAL', '椤圭洰閮�': 'PROJECT_DEPARTMENT' } + return (map[val] as any) || val +} + +function mapUnitToCN(val: string) { + const map: Record<string, string> = { SET: '濂�', SET_PER_YEAR: '濂�/骞�', YEAR: '骞�' } + return map[val] || val +} + +function mapUnitFromCN(val: string) { + const map: Record<string, string> = { '濂�': 'SET', '濂�/骞�': 'SET_PER_YEAR', '骞�': 'YEAR' } + return (map[val] as any) || val +} + +function mapPriceTypeToCN(key: 'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE') { + const map: Record<string, string> = { POINTS: '绉垎', CURRENCY: '璐у竵', AGREEMENT: '鍗忚', FREE: '鍏嶈垂' } + return map[key] +} + +function mapPriceTypeFromCN(val: string): 'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE' | undefined { + const map: Record<string, any> = { '绉垎': 'POINTS', '璐у竵': 'CURRENCY', '鍗忚': 'AGREEMENT', '鍏嶈垂': 'FREE' } + return map[val] +} + +async function loadPricingList(productId: string) { + try { + const res: any = await productPricingApi.listByProductId(productId) + if (res?.code === 200) { + const list = Array.isArray(res.data) ? res.data : [] + const newList = list.map((it: any) => { + const priceSettings = it.priceType.split(',').map((t:string) => mapPriceTypeFromCN(t)) + const pointsPrice = priceSettings.includes('POINTS') ? Number(it.pointsPrice || 0) : 0 + const currencyPrice = priceSettings.includes('CURRENCY') ? Number(it.currencyPrice || 0) : 0 + return { + id: it.id, + productId: String(it.productId || productId), + productSuite: it.suiteName || '', + salesForm: mapSalesFormFromCN(it.salesForm) as any, + customerObject: mapCustomerFromCN(it.customerType) as any, + accountQuantityUnlimited: (it.accountLimit === '涓嶉檺'), + accountQuantity: it.accountLimit === '涓嶉檺' ? '' : Number(it.accountLimit || ''), + concurrentNodeQuantityUnlimited: (it.concurrentNodes === '涓嶉檺'), + concurrentNodeQuantity: it.concurrentNodes === '涓嶉檺' ? '' : Number(it.concurrentNodes || ''), + priceSettings: priceSettings, + pointsPrice, + currencyPrice, + priceUnit: mapUnitFromCN(it.priceUnit) as any, + enableStatus: it.isActive ? 'ENABLED' : 'DISABLED', + authorizationStartTime: undefined, + authorizationEndTime: undefined, + remark: it.description || '', + } + }) + const groupedList = newList.reduce((acc: any[], it: any) => { + // 鎸塸roductSuite鍒嗙粍 + const productSuite = it.productSuite + if (!acc[productSuite]) { + acc[productSuite] = [] + } + acc[productSuite].push(it) + return acc + }, {}) + const newGroupedList:any = [] + Object.entries(groupedList).forEach(([k,v]) =>{ + newGroupedList.push({ + suiteName: k, + items: v + }) + }) + priceList.value = newGroupedList + console.log(priceList.value) + console.log(groupedList) + } else { + priceList.value = [] + } + } catch (e) { + priceList.value = [] + } +} + +function buildPayloadFromRow(row: any, overrides: Partial<any> = {}) {const priceTypeCN = row.priceSettings?.[0] ? mapPriceTypeToCN(row.priceSettings[0]) : undefined + + return { + id: row.id, + productId: Number(row.productId), + productName: productDetail.value?.name, + suiteName: row.productSuite, + salesForm: mapSalesFormToCN(row.salesForm), + customerType: mapCustomerToCN(row.customerObject), + accountLimit: row.accountQuantityUnlimited ? '涓嶉檺' : String(row.accountQuantity || ''), + concurrentNodes: row.concurrentNodeQuantityUnlimited ? '涓嶉檺' : String(row.concurrentNodeQuantity || ''), + priceType: row.priceSettings.map((t:string)=>mapPriceTypeToCN(t as any)).join(','), + priceUnit: mapUnitToCN(row.priceUnit || ''), + pointsPrice: row.priceSettings.includes('POINTS') ? row.pointsPrice : '', + currencyPrice: row.priceSettings.includes('CURRENCY') ? row.currencyPrice : '', + isActive: row.enableStatus === 'ENABLED', + description: row.remark || '', + ...overrides, + } } // 鏌ョ湅浠锋牸 @@ -570,7 +669,6 @@ isEditMode.value = false resetForm() formData.productId = currentProductId.value - formData.id = generateId() dialogVisible.value = true } @@ -579,7 +677,7 @@ isEditMode.value = true resetForm() Object.assign(formData, { - id: row.id ?? generateId(), + id: row.id, productId: currentProductId.value, productSuite: row.productSuite || '', salesForm: row.salesForm || 'BUYOUT', @@ -589,8 +687,8 @@ concurrentNodeQuantity: row.concurrentNodeQuantity ?? '', concurrentNodeQuantityUnlimited: !!row.concurrentNodeQuantityUnlimited, priceSettings: Array.isArray(row.priceSettings) ? row.priceSettings : [], - pointsAmount: row.pointsAmount ?? '', - currencyAmount: row.currencyAmount ?? '', + pointsPrice: row.pointsPrice ?? '', + currencyPrice: row.currencyPrice ?? '', priceUnit: row.priceUnit || '', enableStatus: row.enableStatus || 'ENABLED', authorizationStartTime: row.authorizationStartTime, @@ -611,10 +709,10 @@ type: 'warning' } ) - .then(() => { - const idx = priceList.value.findIndex((p: any) => p.id === row.id) - if (idx !== -1) priceList.value.splice(idx, 1) + .then(async () => { + await productPricingApi.remove(row.id) ElMessage.success('鍒犻櫎鎴愬姛') + if (currentProductId.value) await loadPricingList(currentProductId.value) }) .catch(() => { ElMessage.info('宸插彇娑堝垹闄�') @@ -637,13 +735,13 @@ productSuite: '', salesForm: 'BUYOUT', customerObject: 'ENTERPRISE', - accountQuantity: '', + accountQuantity: 1, accountQuantityUnlimited: false, - concurrentNodeQuantity: '', + concurrentNodeQuantity: 1, concurrentNodeQuantityUnlimited: false, priceSettings: [], - pointsAmount: '', - currencyAmount: '', + pointsPrice: '', + currencyPrice: '', priceUnit: '', enableStatus: 'ENABLED', authorizationStartTime: undefined, @@ -653,10 +751,6 @@ if (formRef.value) formRef.value.clearValidate() } -function generateId() { - const maxId = priceList.value.reduce((m: number, it: any) => Math.max(m, Number(it.id || 0)), 0) - return maxId + 1 -} async function handleSave() { try { @@ -665,17 +759,77 @@ return } + if (!currentProductId.value) { + ElMessage.warning('鏈彁渚涗骇鍝両D锛屾棤娉曚繚瀛樺畾浠�') + return + } + + const selectedTypes = formData.priceSettings + if (!selectedTypes || selectedTypes.length === 0) { + ElMessage.warning('璇疯嚦灏戦�夋嫨涓�绉嶄环鏍艰缃�') + return + } + + const mapSalesFormToCN = (val: string) => ({ + BUYOUT: '涔版柇', + LEASE: '绉熻祦', + PRIVATE_INCREMENT: '绉佹湁澧炲寘閲�', + PUBLIC_INCREMENT: '鍏湁澧炲寘閲�', + OTA: 'OTA鏈嶅姟', + CLOUD: '浜戞湇鍔�', + RESOURCE_PACKAGE: '璧勬簮鍖�', + PERSONAL: '涓汉', + } as any)[val] || val + + const mapCustomerToCN = (val: string) => ({ ENTERPRISE: '浼佷笟', PERSONAL: '涓汉', PROJECT_DEPARTMENT: '椤圭洰閮�' } as any)[val] || val + const mapUnitToCN = (val: string) => ({ SET: '濂�', SET_PER_YEAR: '濂�/骞�', YEAR: '骞�' } as any)[val] || val + const mapPriceTypeToCN = (key: 'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE') => ({ POINTS: '绉垎', CURRENCY: '璐у竵', AGREEMENT: '鍗忚', FREE: '鍏嶈垂' } as any)[key] + + const buildPayload = () => { + + return { + id: isEditMode.value && formData.id ? formData.id : undefined, + productId: Number(currentProductId.value), + productName: productDetail.value?.name, + suiteName: formData.productSuite, + salesForm: mapSalesFormToCN(formData.salesForm), + customerType: mapCustomerToCN(formData.customerObject), + accountLimit: formData.accountQuantityUnlimited ? '涓嶉檺' : String(formData.accountQuantity || ''), + concurrentNodes: formData.concurrentNodeQuantityUnlimited ? '涓嶉檺' : String(formData.concurrentNodeQuantity || ''), + priceType: formData.priceSettings.map(t=>mapPriceTypeToCN(t)).join(','), + priceUnit: mapUnitToCN(formData.priceUnit || ''), + pointsPrice: formData.pointsPrice, + currencyPrice: formData.currencyPrice, + isActive: formData.enableStatus === 'ENABLED', + description: formData.remark || '', + } + } + saving.value = true try { - const record: PriceItem = JSON.parse(JSON.stringify(formData)) - const idx = priceList.value.findIndex((p: any) => p.id === record.id) - if (idx === -1) { - priceList.value.push(record) + if (isEditMode.value && formData.id) { + const payload = buildPayload() + const res: any = await productPricingApi.update(payload) + if (res?.code === 200) { + ElMessage.success('淇敼鎴愬姛') + } else { + ElMessage.error(res?.msg || '淇敼澶辫触') + return + } } else { - priceList.value.splice(idx, 1, record) + // const tasks = selectedTypes.map(t => productPricingApi.add(buildPayload(t))) + // const results: any[] = await Promise.all(tasks) + // const ok = results.every(r => r && r.code === 200) + const results: any = await productPricingApi.add(buildPayload()) + if (results?.code !== 200) { + ElMessage.error('閮ㄥ垎瀹氫环淇濆瓨澶辫触') + return + } + ElMessage.success('鏂板缓鎴愬姛') } - ElMessage.success(isEditMode.value ? '淇敼鎴愬姛' : '鏂板缓鎴愬姛') + dialogVisible.value = false + await loadPricingList(currentProductId.value) } finally { saving.value = false } @@ -695,11 +849,10 @@ '鐘舵�佸彉鏇�', { confirmButtonText: '纭畾', cancelButtonText: '鍙栨秷', type: 'warning' } ) - .then(() => { - const idx = priceList.value.findIndex((p: any) => p.id === row.id) - if (idx !== -1) { - priceList.value[idx] = { ...priceList.value[idx], enableStatus: newStatus } - } + .then(async () => { + const payload = buildPayloadFromRow(row, { isActive: newStatus === 'ENABLED' }) + await productPricingApi.update(payload) + await loadPricingList(currentProductId.value as string) ElMessage.success(newStatus === 'ENABLED' ? '鍚敤鎴愬姛' : '鍋滅敤鎴愬姛') }) .catch(() => {}) @@ -741,6 +894,11 @@ .content-area { .price-card { + /* 璁╀环鏍煎垪琛ㄥ彲婊氬姩 */ + :deep(.el-card__body) { + max-height: 600px; + overflow-y: auto; + } .card-header { display: flex; justify-content: space-between; @@ -801,7 +959,7 @@ .quantity-input-group { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } .sales-form-item :deep(.el-form-item__content) { white-space: nowrap; } .sales-form-group { display: inline-flex; gap: 12px; flex-wrap: nowrap; } -.price-form :deep(.el-form-item) { margin-bottom: 12px; } +.price-form :deep(.el-form-item) { margin-bottom: 20px; } /* 涓婃灦鐘舵�侀鑹� */ .status-online { color: #67C23A; } .status-pending { color: #E6A23C; } diff --git a/src/views/productManage/product/index.vue b/src/views/productManage/product/index.vue index c2974f4..1bd5240 100644 --- a/src/views/productManage/product/index.vue +++ b/src/views/productManage/product/index.vue @@ -94,7 +94,7 @@ // 妯℃嫙浜у搧鏁版嵁 const mockProductList: ProductItem[] = [ { - id: '10001', + id: '1', name: '鏁板瓧鍖栦骇鍝丄', productType: '杞欢/骞冲彴', industry: '浜ら�氬熀纭�璁炬柦', @@ -106,7 +106,7 @@ shelfStatus: '寰呬笂鏋�' }, { - id: '10002', + id: '2', name: '鏁板瓧鍖栦骇鍝丅', productType: '纭欢/浼犳劅', industry: '甯傛斂宸ョ▼', @@ -118,7 +118,7 @@ shelfStatus: '宸蹭笂鏋�' }, { - id: '10003', + id: '3', name: '鏁板瓧鍖栦骇鍝丆', productType: '杞欢/骞冲彴', industry: '寤虹瓚宸ョ▼', diff --git a/src/views/productManage/productPriceViewer/index.vue b/src/views/productManage/productPriceViewer/index.vue index d1649a2..fb0bd06 100644 --- a/src/views/productManage/productPriceViewer/index.vue +++ b/src/views/productManage/productPriceViewer/index.vue @@ -9,7 +9,7 @@ > <div class="price-viewer-container" v-loading="loading"> <!-- 浠锋牸瀵规瘮琛ㄦ牸 --> - <div class="pricing-table-container" v-if="priceList.length > 0"> + <div class="pricing-table-container" v-if="showPricePanel && priceList.length > 0"> <el-tabs v-model="activeTab" type="card" class="pricing-tabs"> <el-tab-pane v-for="(tabData, tabKey) in groupedPriceData" @@ -19,7 +19,7 @@ > <div class="pricing-table-wrapper"> <table class="pricing-table"> - <thead> + <thead> <tr> <th class="feature-column">鍔熻兘瀵规瘮</th> <th @@ -107,25 +107,27 @@ v-if="priceItem.priceSettings.includes('POINTS') && priceItem.pointsAmount > 0" class="price-item" > - <span class="price-label">绉垎:</span> - <span class="price-value points"> - {{ formatNumber(priceItem.pointsAmount) }} 绉垎{{ getPriceUnitText(priceItem.priceUnit) }} - </span> + <div class="price-lable-icon"> + <el-icon class="price-icon points"><Coin /></el-icon> + <span class="price-label">绉垎</span> + </div> + <span class="price-value points">{{ formatNumber(priceItem.pointsAmount) }} / {{ getPriceUnitText(priceItem.priceUnit) }}</span> </div> <div v-if="priceItem.priceSettings.includes('CURRENCY') && priceItem.currencyAmount > 0" class="price-item" > - <span class="price-label">楼璐у竵:</span> - <span class="price-value currency"> - {{ formatNumber(priceItem.currencyAmount) }} 鍏儃{ getPriceUnitText(priceItem.priceUnit) }} - </span> + <div class="price-lable-icon"> + <el-icon class="price-icon currency"><Money /></el-icon> + <span class="price-label">璐у竵</span> + </div> + <span class="price-value currency">{{ formatNumber(priceItem.currencyAmount) }} / {{ getPriceUnitText(priceItem.priceUnit) }}</span> </div> <div v-if="priceItem.priceSettings.includes('FREE')" class="price-item" > - <span class="price-label">楼璐у竵:</span> + <span class="price-label">璐圭敤</span> <span class="price-value free">鍏嶈垂</span> </div> <div @@ -151,7 +153,7 @@ :model-value="selectedSuiteIds.has(priceItem.id)" @change="handleSuiteSelect(priceItem.id, $event)" > - 娣诲姞 + <el-icon class="add-cart-icon"><ShoppingCart /></el-icon> </el-checkbox> </div> </td> @@ -162,6 +164,159 @@ </el-tab-pane> </el-tabs> </div> + + <!-- 鍐呭祵璁㈣喘淇℃伅闈㈡澘 --> + <div class="order-panel" v-else-if="showOrderPanel"> + <!-- 绗竴閮ㄥ垎锛氫骇鍝佷俊鎭� + 姹囨�� + 鎿嶄綔锛堥潤鎬佹暟鎹級 --> + <el-card class="order-header-card" shadow="never"> + <div class="basic-grid"> + <div class="grid-item"> + <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> + </div> + <div class="grid-item"> + <span class="label">鎻愪緵鑰咃細</span><span class="value">{{ productHeader.provider }}</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> + </div> + <div class="grid-item"> + <span class="label">閮ㄩ棬锛�</span><span class="value">{{ userInfo.department }}</span> + </div> + </div> + </el-card> + + <!-- 绗簩閮ㄥ垎锛氶�夎喘浜у搧鍒楄〃 --> + <el-card class="selected-suites-card" shadow="never"> + + <div v-if="orderSuites.length > 0" class="suite-table-wrapper"> + <table class="suite-table"> + <thead> + <tr> + <th class="th-left">璇︽儏</th> + <th class="th-left">瑙勬牸</th> + <th class="th-left">鍗曚环</th> + <th class="th-center">鏁伴噺</th> + <th class="th-center">骞撮檺</th> + <th class="th-center">鎿嶄綔</th> + </tr> + </thead> + <tbody> + <tr v-for="(suite, idx) in orderSuites" :key="suite.id"> + <td class="cell-detail"> + <div class="detail-content"> + <el-checkbox v-model="suite.selected" /> + <span class="suite-name">{{ getProductSuiteText(suite.productSuite) }}</span> + </div> + </td> + <td class="cell-spec cell-spec-gg"> + <div class="spec-line-gg"> + <div class="spec-line">閿�鍞舰寮忥細{{ getSalesFormText(suite.salesForm) }}</div> + <div class="spec-line">瀹㈡埛瀵硅薄锛歿{ getCustomerObjectText(suite.customerObject) }}</div> + </div> + <div class="spec-line-gg"> + <div class="spec-line">璐︽埛鏁伴噺锛歿{ suite.accountQuantityUnlimited ? '涓嶉檺' : suite.accountQuantity }}</div> + <div class="spec-line">骞跺彂鑺傜偣鏁帮細{{ suite.concurrentNodeQuantityUnlimited ? '涓嶉檺' : suite.concurrentNodeQuantity }}</div> + </div> + + </td> + <td class="cell-price"> + <div class="price-inline"> + <el-select v-model="suite.priceType" size="large" style="width: 90px"> + <el-option v-for="t in getAvailablePriceTypes(suite)" :key="t.value" :label="t.label" :value="t.value" /> + </el-select> + <div class="price-display"> + <template v-if="suite.priceType === 'POINTS'"> + <span class="price-tag points">{{ formatNumber(suite.pointsAmount) }}</span> + <span class="price-unit">{{ getUnitSuffix(suite.priceUnit) }}</span> + </template> + <template v-else-if="suite.priceType === 'CURRENCY'"> + <span class="price-tag currency">{{ formatNumber(suite.currencyAmount) }}</span> + <span class="price-unit">{{ getUnitSuffix(suite.priceUnit) }}</span> + </template> + <template v-else-if="suite.priceType === 'AGREEMENT'"> + <span class="price-agreement">/{{ getPriceUnitText(suite.priceUnit) }}</span> + </template> + <template v-else> + <span class="price-free">/{{ getPriceUnitText(suite.priceUnit) }}</span> + </template> + </div> + </div> + </td> + <td class="th-center"> + <el-input-number v-model="suite.quantity" :min="1" :max="999" :controls="false" size="small" /> + </td> + <td class="th-center"> + <el-input-number v-model="suite.duration" :min="1" :max="10" :controls="false" size="small" /> + </td> + <td class="th-center"> + <el-button type="danger" link @click="removeSuite(idx)">鍒犻櫎</el-button> + </td> + </tr> + </tbody> + </table> + </div> + <el-empty v-else description="鏆傛棤閫変腑鐨勮蒋浠跺浠�" /> + <div class="table-toolbar"> + <div class="toolbar-left"> + <el-checkbox v-model="selectAllChecked">鍏ㄩ��</el-checkbox> + <span class="summary-info"> + 宸查�� <span class="summary-count">{{ selectedCount }}</span> 浠朵骇鍝� + 鎬昏 <span class="summary-points">{{ formatNumber(totalByType.points) }}</span> 绉垎 + </span> + </div> + </div> + </el-card> + </div> + + <!-- 璁㈠崟鐘舵�佸睍绀洪潰鏉� --> + <div class="order-status-panel" v-else-if="showOrderStatus"> + <div class="steps-wrapper"> + <el-steps :active="activeStep" align-center finish-status="success" process-status="process"> + <el-step title="鎻愪氦鐢宠" :description="orderStatus.submitTime" /> + <el-step title="寰呮巿鏉�" /> + <el-step title="浜ゆ槗纭" /> + <el-step title="璇勪环" /> + </el-steps> + </div> + + <div class="result-text"> + 鎮ㄧ殑浜у搧璁㈣喘鐢宠宸叉垚鍔熸彁浜わ紝 璇风瓑寰呮巿鏉� + </div> + + <el-card class="order-info-card" shadow="never"> + <div class="order-info-title">璁㈠崟淇℃伅</div> + <table class="order-info-table"> + <tbody> + <tr> + <td class="label">璁㈠崟缂栧彿锛�</td> + <td class="value">{{ orderStatus.id }}</td> + <td class="label">鐢宠鏃堕棿锛�</td> + <td class="value">{{ orderStatus.applyTime }}</td> + </tr> + <tr> + <td class="label">浜у搧鍚嶇О锛�</td> + <td class="value">{{ orderStatus.productName }}</td> + <td class="label">鎻愪緵鑰咃細</td> + <td class="value">{{ orderStatus.provider }}</td> + </tr> + <tr> + <td class="label">璁㈠崟鐘舵�侊細</td> + <td class="value">{{ statusText(orderStatus.status) }}</td> + <td></td> + <td class="value link"> + <el-link type="primary" @click="viewOrder">鏌ョ湅璁㈠崟淇℃伅</el-link> + </td> + </tr> + </tbody> + </table> + </el-card> + </div> <!-- 鏃犱环鏍兼暟鎹椂鐨勬彁绀� --> <div v-else class="no-price-data"> @@ -169,27 +324,31 @@ </div> </div> - <template #footer> + <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="showOrderPanel = false">杩斿洖浠锋牸瀵规瘮</el-button> + <el-button type="primary" @click="submitOrder">鎻愪氦鐢宠</el-button> + </template> </span> </template> </el-dialog> - <!-- 浜у搧璁㈣喘瀵硅瘽妗� --> - <ProductOrderDialog - v-model="showOrderDialog" - :product-id="props.productId" - :suite-ids="Array.from(selectedSuiteIds)" - @order-success="handleOrderSuccess" - /> + <!-- 浜у搧璁㈣喘瀵硅瘽妗嗙Щ闄わ紝鏀逛负鍐呭祵灞曠ず --> </template> <script setup lang="ts"> import { ref, watch, computed } from 'vue' import { ElMessage } from 'element-plus' -import ProductOrderDialog from '../productOrderDialog/index.vue' +import { useRoute } from 'vue-router' +import productPricingApi from '@/api/productPricingApi' +import { ShoppingCart, Coin, Money } from '@element-plus/icons-vue' + +const route = useRoute() interface PriceItem { id: number @@ -219,11 +378,18 @@ (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>() @@ -237,7 +403,126 @@ const priceList = ref<PriceItem[]>([]) const productName = ref('') const selectedSuiteIds = ref<Set<number>>(new Set()) -const showOrderDialog = ref(false) +const showOrderPanel = ref(false) +const showPricePanel = ref(true) +const showOrderStatus = ref(false) +type PriceTypeKey = 'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE' +interface OrderSuite extends PriceItem { + selected: boolean + quantity: number + duration: number + priceType?: PriceTypeKey +} +const orderSuites = ref<OrderSuite[]>([]) + +// 璁㈠崟鐘舵�佺浉鍏� +type OrderStatus = 'SUBMITTED' | 'AUTHORIZED' | 'CONFIRMED' | 'EVALUATED' +interface OrderStatusDetail { + id: string + productName: string + provider: string + status: OrderStatus + submitTime: string + applyTime: string +} +const orderStatus = ref<OrderStatusDetail>({ + id: '', + productName: '', + provider: '', + status: 'SUBMITTED', + submitTime: '', + applyTime: '' +}) + +const activeStep = computed(() => { + switch (orderStatus.value?.status) { + case 'SUBMITTED': return 1 + case 'AUTHORIZED': return 2 + case 'CONFIRMED': return 3 + case 'EVALUATED': return 4 + default: return 1 + } +}) + +const statusText = (s: OrderStatus) => { + const map: Record<OrderStatus, string> = { + SUBMITTED: '寰呮巿鏉�', + AUTHORIZED: '宸叉巿鏉冿紝寰呯‘璁や氦鏄�', + CONFIRMED: '浜ゆ槗宸茬‘璁わ紝寰呰瘎浠�', + EVALUATED: '宸插畬鎴�' + } + return map[s] +} + +const viewOrder = () => { + window.alert('杩欓噷鍙烦杞埌璁㈠崟璇︽儏椤碉紙绀轰緥锛�') +} + +const selectedCount = computed(() => orderSuites.value.filter(s => s.selected).length) +const totalByType = computed(() => { + const result = { points: 0, currency: 0 } + orderSuites.value + .filter(s => s.selected) + .forEach(s => { + const q = s.quantity || 1 + const d = s.duration || 1 + const multiplier = (s.priceUnit === 'SET_PER_YEAR' || s.priceUnit === 'YEAR') ? d : 1 + switch (s.priceType) { + case 'POINTS': + result.points += (s.pointsAmount || 0) * q * multiplier + break + case 'CURRENCY': + result.currency += (s.currencyAmount || 0) * q * multiplier + break + } + }) + return result +}) + +// 绗竴閮ㄥ垎锛氶潤鎬佺Н鍒嗕俊鎭睍绀� +const pointsInfo = ref({ + accountName: '涓汉绉垎璐︽埛', + balance: 50000, + frozen: 0, +}) + +// 浜у搧淇℃伅/鐢ㄦ埛淇℃伅锛堥潤鎬侊級 +const productHeader = ref({ + name: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', + industry: '鍏矾,甯傛斂,寤虹瓚', + provider: '涓氦寤虹瓚闆嗗洟绗竴宸ョ▼鏈夐檺鍏徃' +}) +const userInfo = ref({ + name: '寮犻潤', + unit: '淇$闆嗗洟', + department: '闂ㄦ埛绯荤粺涓存椂缁�' +}) + +// 鍏ㄩ�� +const selectAllChecked = ref(false) +watch(selectAllChecked, (val) => { + orderSuites.value.forEach(s => { s.selected = !!val }) +}) + +const getAvailablePriceTypes = (suite: OrderSuite) => { + const t: { label: string; value: PriceTypeKey }[] = [] + if (suite.priceSettings.includes('POINTS')) t.push({ label: '绉垎', value: 'POINTS' }) + if (suite.priceSettings.includes('CURRENCY')) t.push({ label: '璐у竵', value: 'CURRENCY' }) + if (suite.priceSettings.includes('AGREEMENT')) t.push({ label: '鍗忚', value: 'AGREEMENT' }) + if (suite.priceSettings.includes('FREE')) t.push({ label: '鍏嶈垂', value: 'FREE' }) + return t +} +const getDefaultPriceType = (suite: OrderSuite): PriceTypeKey => { + if (suite.priceSettings.includes('POINTS')) return 'POINTS' + if (suite.priceSettings.includes('CURRENCY')) return 'CURRENCY' + if (suite.priceSettings.includes('AGREEMENT')) return 'AGREEMENT' + return 'FREE' +} +const getUnitSuffix = (unit?: string) => { + const map: Record<string, string> = { SET: '/濂�', SET_PER_YEAR: '/濂�/骞�', YEAR: '/骞�' } + return map[unit || ''] || '' +} +const removeSuite = (index: number) => { orderSuites.value.splice(index, 1) } // 寮圭獥鏍囬 const dialogTitle = computed(() => { @@ -277,197 +562,43 @@ ) }) -// 妯℃嫙浠锋牸鏁版嵁 -const mockPriceData: Record<string, PriceItem[]> = { - '10001': [ - { - id: 1, - productId: '10001', - productSuite: 'ENTERPRISE_PRIVATE_SAAS_LICENSE', - salesForm: 'BUYOUT', - customerObject: 'ENTERPRISE', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['POINTS', 'CURRENCY', 'AGREEMENT'], - pointsAmount: 50000, - currencyAmount: 50000, - priceUnit: 'SET', - enableStatus: 'ENABLED' - }, - { - id: 2, - productId: '10001', - productSuite: 'ENTERPRISE_PRIVATE_SAAS_LICENSE', - salesForm: 'LEASE', - customerObject: 'ENTERPRISE', - accountQuantity: 0, - accountQuantityUnlimited: true, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['POINTS', 'CURRENCY', 'AGREEMENT'], - pointsAmount: 800000, - currencyAmount: 800000, - priceUnit: 'YEAR', - enableStatus: 'ENABLED' - }, - { - id: 3, - productId: '10001', - productSuite: 'ENTERPRISE_PRIVATE_SAAS_OTA', - salesForm: 'OTA', - customerObject: 'ENTERPRISE', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['CURRENCY'], - pointsAmount: 0, - currencyAmount: 7500, - priceUnit: 'SET_PER_YEAR', - enableStatus: 'ENABLED' - }, - { - id: 4, - productId: '10001', - productSuite: 'ENTERPRISE_PRIVATE_SAAS_USER_INCREMENT', - salesForm: 'PRIVATE_INCREMENT', - customerObject: 'ENTERPRISE', - accountQuantity: 100, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 100, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['CURRENCY'], - pointsAmount: 0, - currencyAmount: 10000, - priceUnit: 'YEAR', - enableStatus: 'ENABLED' - }, - { - id: 9, - productId: '10001', - productSuite: 'PROJECT_PRIVATE_SAAS_OTA', - salesForm: 'OTA', - customerObject: 'PROJECT_DEPARTMENT', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['FREE'], - pointsAmount: 0, - currencyAmount: 0, - priceUnit: 'SET', - enableStatus: 'ENABLED' - }, - { - id: 10, - productId: '10001', - productSuite: 'PROJECT_PRIVATE_SAAS_USER_INCREMENT', - salesForm: 'PRIVATE_INCREMENT', - customerObject: 'PROJECT_DEPARTMENT', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['CURRENCY'], - pointsAmount: 0, - currencyAmount: 50000, - priceUnit: 'YEAR', - enableStatus: 'ENABLED' - }, - { - id: 5, - productId: '10001', - productSuite: 'ENTERPRISE_PUBLIC_SAAS_LICENSE', - salesForm: 'LEASE', - customerObject: 'PERSONAL', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['CURRENCY'], - pointsAmount: 0, - currencyAmount: 15000, - priceUnit: 'YEAR', - enableStatus: 'ENABLED' - }, - { - id: 6, - productId: '10001', - productSuite: 'ENTERPRISE_PUBLIC_SAAS_USER_INCREMENT', - salesForm: 'PUBLIC_INCREMENT', - customerObject: 'PERSONAL', - accountQuantity: 50, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 50, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['CURRENCY'], - pointsAmount: 0, - currencyAmount: 5000, - priceUnit: 'YEAR', - enableStatus: 'ENABLED' - }, - { - id: 7, - productId: '10001', - productSuite: 'ENTERPRISE_PUBLIC_SAAS_CLOUD', - salesForm: 'CLOUD', - customerObject: 'PERSONAL', - accountQuantity: 1, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 1, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['FREE'], - pointsAmount: 0, - currencyAmount: 0, - priceUnit: 'SET', - enableStatus: 'ENABLED' - } - ], - '10002': [ - { - id: 8, - productId: '10002', - productSuite: 'ENTERPRISE_PRIVATE_SAAS_LICENSE', - salesForm: 'BUYOUT', - customerObject: 'ENTERPRISE', - accountQuantity: 30, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 30, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['POINTS', 'CURRENCY', 'AGREEMENT'], - pointsAmount: 30000, - currencyAmount: 30000, - priceUnit: 'SET', - enableStatus: 'ENABLED' - }, - { - id: 11, - productId: '10002', - productSuite: 'PROJECT_PRIVATE_SAAS_OTA', - salesForm: 'OTA', - customerObject: 'PROJECT_DEPARTMENT', - accountQuantity: 30, - accountQuantityUnlimited: false, - concurrentNodeQuantity: 30, - concurrentNodeQuantityUnlimited: false, - priceSettings: ['FREE'], - pointsAmount: 0, - currencyAmount: 0, - priceUnit: 'SET', - enableStatus: 'ENABLED' - } - ] +// 鏄犲皠宸ュ叿 +function mapSalesFormFromCN(val: string) { + const map: Record<string, string> = { + '涔版柇': 'BUYOUT', + '绉熻祦': 'LEASE', + '绉佹湁澧炲寘閲�': 'PRIVATE_INCREMENT', + '鍏湁澧炲寘閲�': 'PUBLIC_INCREMENT', + 'OTA鏈嶅姟': 'OTA', + '浜戞湇鍔�': 'CLOUD', + '璧勬簮鍖�': 'RESOURCE_PACKAGE', + '涓汉': 'PERSONAL', + } + return (map[val] as any) || val +} + +function mapCustomerFromCN(val: string) { + const map: Record<string, string> = { '浼佷笟': 'ENTERPRISE', '涓汉': 'PERSONAL', '椤圭洰閮�': 'PROJECT_DEPARTMENT' } + return (map[val] as any) || val +} + +function mapUnitFromCN(val: string) { + const map: Record<string, string> = { '濂�': 'SET', '濂�/骞�': 'SET_PER_YEAR', '骞�': 'YEAR' } + return (map[val] as any) || val +} + +function mapPriceTypeFromCN(val: string): 'POINTS' | 'CURRENCY' | 'AGREEMENT' | 'FREE' | undefined { + const map: Record<string, any> = { '绉垎': 'POINTS', '璐у竵': 'CURRENCY', '鍗忚': 'AGREEMENT', '鍏嶈垂': 'FREE' } + return map[val] } // 妯℃嫙浜у搧鍚嶇О鏁版嵁 const mockProductNames: Record<string, string> = { - '10001': '鏁板瓧鍖栦骇鍝丄', - '10002': '鏁板瓧鍖栦骇鍝丅' + '1': '鏁板瓧鍖栦骇鍝丄', + '2': '鏁板瓧鍖栦骇鍝丅' } -// 鑾峰彇浠锋牸淇℃伅 +// 鑾峰彇浠锋牸淇℃伅锛堟敼涓烘帴鍙h幏鍙栧畾浠峰垪琛級 const fetchProductData = async (productId: string) => { if (!productId) { ElMessage.warning('鏈彁渚涗骇鍝両D') @@ -476,20 +607,53 @@ loading.value = true try { - // 妯℃嫙API璋冪敤 - await new Promise(resolve => setTimeout(resolve, 500)) - - // 鑾峰彇浜у搧鍚嶇О - productName.value = mockProductNames[productId] || '鏈煡浜у搧' - - // 鑾峰彇浠锋牸鏁版嵁 - priceList.value = mockPriceData[productId] || [] - + const res: any = await productPricingApi.listByProductId(productId) + if (res?.code === 200) { + const list = Array.isArray(res.data) ? res.data : [] + const mapped: PriceItem[] = list.map((it: any) => { + const priceSettings = String(it.priceType || '') + .split(',') + .map((t: string) => mapPriceTypeFromCN(t)) + .filter(Boolean) + + const hasPoints = (priceSettings as string[]).includes('POINTS') + const hasCurrency = (priceSettings as string[]).includes('CURRENCY') + + const accountUnlimited = it.accountLimit === '涓嶉檺' + const concurrentUnlimited = it.concurrentNodes === '涓嶉檺' + + return { + id: it.id, + productId: String(it.productId || productId), + productSuite: it.suiteName || '', + salesForm: mapSalesFormFromCN(it.salesForm) as any, + customerObject: mapCustomerFromCN(it.customerType) as any, + accountQuantity: accountUnlimited ? 0 : Number(it.accountLimit || 0), + accountQuantityUnlimited: accountUnlimited, + concurrentNodeQuantity: concurrentUnlimited ? 0 : Number(it.concurrentNodes || 0), + concurrentNodeQuantityUnlimited: concurrentUnlimited, + priceSettings: priceSettings as any, + pointsAmount: hasPoints ? Number(it.pointsPrice || 0) : 0, + currencyAmount: hasCurrency ? Number(it.currencyPrice || 0) : 0, + priceUnit: mapUnitFromCN(it.priceUnit) as any, + enableStatus: it.isActive ? 'ENABLED' : 'DISABLED', + } + }) + priceList.value = mapped if (priceList.value.length === 0) { ElMessage.warning('璇ヤ骇鍝佹殏鏃犱环鏍间俊鎭�') + } + // 濡傛灉鍚庣杩斿洖浜嗕骇鍝佸悕绉板彲浠ュ湪姝よ祴鍊硷紱鍚﹀垯淇濇寔鐜版湁鏍囬鏍煎紡 + if (res.data && res.data.length > 0 && res.data[0].productName) { + productName.value = res.data[0].productName + } + } else { + priceList.value = [] + ElMessage.error(res?.msg || '鑾峰彇瀹氫环鍒楄〃澶辫触') } } catch (error) { console.error('鑾峰彇浠锋牸鏁版嵁澶辫触:', error) + priceList.value = [] ElMessage.error('鑾峰彇浠锋牸鏁版嵁澶辫触') } finally { loading.value = false @@ -513,6 +677,11 @@ // 鍏抽棴寮圭獥 const handleClose = () => { visible.value = false + showOrderPanel.value = false + showOrderStatus.value = false + showPricePanel.value = true + orderSuites.value = [] + selectedSuiteIds.value.clear() } // 濂椾欢閫夋嫨澶勭悊 @@ -531,13 +700,57 @@ ElMessage.warning('璇烽�夋嫨瑕佽璐殑杞欢濂椾欢') return } - - // 鑾峰彇閫変腑鐨勪环鏍奸」 const selectedItems = priceList.value.filter(item => selectedSuiteIds.value.has(item.id)) - emit('order', selectedItems) + // 鏋勫缓鍐呭祵璁㈣喘鍒楄〃 + orderSuites.value = selectedItems.map(it => ({ + ...it, + selected: true, + quantity: 1, + duration: 1, + priceType: getDefaultPriceType(it as any) + })) as any + showOrderPanel.value = true + showPricePanel.value = false +} + +// 澶勭悊璁㈣喘缁撴灉 +const handleOrderResult = (selectedItems: any[]) => {} + +// 鍚敤/鍋滅敤鎸夐挳 +function handleToggleEnableStatus(row: any) { + // ... existing code ... +} + +async function submitOrder() { + const items = orderSuites.value.filter(s => s.selected) + if (items.length === 0) { + ElMessage.warning('璇疯嚦灏戦�夋嫨涓�涓浠�') + return + } + // 妫�鏌ユ槸鍚︽湁璐у竵浜ゆ槗 + const hasCurrency = items.some(item => item.priceType === 'CURRENCY') + if (hasCurrency) { + ElMessage.error('鏆備笉鏀寔璐у竵浜ゆ槗锛岃閫夋嫨绉垎鎴栧崗璁敮浠樻柟寮�') + return + } - // 鎵撳紑璁㈣喘瀵硅瘽妗� - showOrderDialog.value = true + // 杩欓噷鍙鎺ュ疄闄呬笅鍗曟帴鍙o紱鍏堝仛鍓嶇鎻愮ず + // emit('order', items as any) + + // 妯℃嫙璁㈠崟鐘舵�佹暟鎹� + orderStatus.value = { + id: '4348442557619205545', + productName: productHeader.value.name, + provider: productHeader.value.provider, + status: 'SUBMITTED', + submitTime: new Date().toLocaleString(), + applyTime: new Date().toLocaleString() + } + + // 鏄剧ず璁㈠崟鐘舵�侀潰鏉� + showOrderPanel.value = false + showPricePanel.value = false + showOrderStatus.value = true } // 璁㈣喘鎴愬姛鍥炶皟 @@ -681,7 +894,11 @@ padding: 6px 8px; text-align: center; vertical-align: middle; - font-size: 12px; + font-size: 16px; + } + tr th:first-child, td:first-child{ + font-weight: 700; + color: #303133; } th { @@ -699,7 +916,7 @@ th { background-color: #fafafa; font-weight: 500; - font-size: 12px; + font-size: 16px; color: #606266; } } @@ -712,7 +929,8 @@ } .feature-value { - color: #303133; + color: #606266; + background: #fafafa; /* 琛屼负鐏拌壊 */ } .order-methods { @@ -722,32 +940,36 @@ align-items: center; .method-item { - font-size: 13px; + font-size: 16px; color: #409eff; } } .price-info { display: flex; - flex-direction: column; + flex-direction: column; /* 绔栨帓 */ gap: 8px; align-items: center; .price-item { display: flex; - align-items: center; - gap: 4px; - + align-items: flex-start; + flex-direction: column; + gap: 6px; + .price-lable-icon{ + display: flex; + gap: 6px; + } .price-label { - font-size: 13px; + font-size: 16px; color: #606266; } .price-value { font-weight: 500; - + font-size: 20px; &.points { - color: #e6a23c; + color: #e7900d; } &.currency { @@ -758,6 +980,8 @@ color: #67c23a; } } + .price-icon.points { color: #e6a23c; } + .price-icon.currency { color: #f56c6c; } } .add-checkbox { @@ -770,6 +994,7 @@ justify-content: center; align-items: center; padding: 8px 0; + .add-cart-icon { color: #409eff; font-size:22px} } } } @@ -792,6 +1017,7 @@ } .product-price-dialog { + :deep(.el-dialog__title) { font-size: 14px; font-weight: 600; @@ -800,6 +1026,7 @@ :deep(.el-dialog) { max-width: 95vw; min-width: 800px; + height: 700px !important; } :deep(.el-dialog__body) { @@ -807,4 +1034,106 @@ overflow-y: auto; } } + +.order-header-card { + :deep(.el-card__body) { padding: 10px; } + .basic-grid { display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 30px 16px;} + .grid-item { font-size: 16px; color:#606266; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .label { color:#606266; } + .value { color:#303133; } + .value.strong { font-weight: 600; } + margin-bottom: 20px; + margin-top: 10px; +} + +.points-info-card { + :deep(.el-card__body) { padding: 8px 10px; } + .points-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px 12px; + .points-item { font-size: 12px; color: #606266; } + .label { color: #606266; } + .value { font-weight: 600; color: #303133; } + .value.emphasis { color: #409eff; } + .value.warn { color: #e6a23c; } + } +} + +.selected-suites-card { + :deep(.el-card__body) { padding: 8px; } +} + +.suite-table-wrapper { overflow-x: auto; width: 100%; } +.suite-table { + width: 100%; + border-collapse: collapse; + background: #fff; + table-layout: fixed; +} +.suite-table th, +.suite-table td { + border: 1px solid #e4e7ed; + padding: 6px 8px; + vertical-align: middle; + font-size: 16px; +} +.suite-table thead th { background: #f0f2f5; color: #303133; font-weight: 600; font-size: 16px;} + +.table-toolbar { display:flex; justify-content: space-between; align-items:center; padding: 4px 0 8px; } +.toolbar-left { display:flex; align-items:center; gap: 12px; width: 100%;justify-content: space-between;} +.toolbar-right { display:flex; align-items:center; gap: 12px; } +.summary-info { font-size: 14px; color: #606266; } +.summary-count { color: #409eff; font-weight: 600; } +.summary-points { color: #e6a23c; font-weight: 600; } + +.suite-table .th-left { text-align: center; width: 21%;} +.suite-table .th-center { text-align: center; } +.cell-detail .detail-content { display: inline-flex; align-items: center; gap: 8px; } +.suite-name { font-weight: 600; color: #303133; } +.cell-spec .spec-line { color: #606266; font-size: 16px; line-height: 20px; } +.cell-spec-gg {display: flex; flex-direction: column; gap: 16px; + .spec-line-gg{ + display: flex; + gap: 30px; + } +} +.cell-price .price-inline { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; } +.cell-price .price-tag.points { color: #E6A23C; font-weight: 600; } +.cell-price .price-tag.currency { color: #F56C6C; font-weight: 600; } +.cell-price .price-free { color: #67C23A; font-weight: 600; } +.cell-price .price-agreement { color: #409EFF; } +.cell-summary .summary-item { font-size: 14px; color: #606266; line-height: 1.5; } + +.order-status-panel { + .steps-wrapper { padding: 6px 8px 0 8px; } + .result-text { text-align: center; margin: 18px 0 12px; font-size: 18px; color: #303133; } + .order-info-card { + :deep(.el-card__body) { padding: 0; } + } + .order-info-title { + background: #f5f7fa; + padding: 12px 16px; + font-weight: 600; + border-bottom: 1px solid #ebeef5; + } + .order-info-table { + width: 100%; + border-collapse: collapse; + td { + padding: 12px 16px; + border-bottom: 1px solid #ebeef5; + } + .label { width: 120px; color: #606266; } + .value { color: #303133; } + .link { text-align: right; } + } +} + +.order-summary { padding: 10px 0; display:flex; align-items:center; justify-content:flex-end; gap: 8px; } +.order-summary .summary-label { color:#606266; } +.order-summary .summary-value { font-weight:600; } +.order-summary .summary-value.highlight{ color:#409eff; } +.order-summary .summary-value.total{ color:#e6a23c; } +.order-summary .summary-value.currency{ color:#f56c6c; } </style> -- Gitblit v1.8.0