OrderDetailCard.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <template>
  2. <div class="order-detail-card-container">
  3. <InfoCard title="基本信息">
  4. <div class="order-detail-card-content">
  5. <div class="order-detail-card-item" v-for="(item, index) in basicInfoItems" :key="index">
  6. {{ item.label }}:{{ getFilteredValue(orderData[item.key], item.filter) || '-' }}
  7. </div>
  8. </div>
  9. </InfoCard>
  10. <InfoCard title="产品信息">
  11. <div class="order-detail-card-content">
  12. <div class="order-detail-card-item" v-for="(item, index) in productInfoTotalItems" :key="index">
  13. <div v-if="item.key === 'pure_amount'">
  14. {{ item.label }}:¥{{ formatNumber(orderData[item.key]) || '-' }}
  15. </div>
  16. <div v-else-if="item.key === 'commission'">
  17. {{ item.label }}:¥{{ formatNumber(orderData[item.key]) || '0.00' }}
  18. </div>
  19. <div v-else-if="item.key === 'zero_type'">
  20. <span v-if="shouldRenderItem(item)">
  21. {{ item.label }}:{{ getFilteredValue(orderData[item.key], item.filter) || '-' }}
  22. </span>
  23. </div>
  24. <div v-else>{{ item.label }}:{{ getFilteredValue(orderData[item.key], item.filter ) || '-' }}</div>
  25. </div>
  26. </div>
  27. <div class="order-detail-product-list">
  28. <ProductCard
  29. v-for="(product, index) in productData"
  30. :key="product.id"
  31. :title="setProductTitle(product, index)"
  32. :subtitle="product.auto !== 1 ? '该产品暂不支持系统自动开通权限,请联系运维开通': ''"
  33. >
  34. <div class="order-detail-product-content">
  35. <div class="grouped-items">
  36. <div
  37. v-for="(item, index) in productInfoItems"
  38. :key="index"
  39. :class="`item-span-${item.span}`" >
  40. <div
  41. class="order-detail-card-item"
  42. v-if="shouldRenderItem(item, product)"
  43. >
  44. <div v-if="item.key === 'linkedOrder'" class="linkedOrder">
  45. {{ item.label }}:
  46. <TableCard :span-method="objectSpanMethod" v-if="product[item.key].length" :table-data="product[item.key]" :columns="linkOrderColumns">
  47. </TableCard>
  48. <span v-else>-</span>
  49. </div>
  50. <div v-else-if="item.key === 'subAccountCount'">
  51. <span v-html="getValidityPeriodHtml(product, item)"></span>
  52. </div>
  53. <div v-else-if="item.key === 'mainAccountCount'">
  54. <span v-html="getValidityPeriodHtml(product, item)"></span>
  55. </div>
  56. <div v-else-if="item.key ==='rate'">
  57. {{ item.label }}:
  58. <span :class="{'no_open_root': product.original_price === '0.00'}">{{ getFilteredValue(product[item.key], item.filter) || '-' }}</span>
  59. </div>
  60. <div v-else-if="item.key === 'final_price' || item.key === 'original_price'">
  61. {{ item.label }}:¥{{ getFilteredValue(product[item.key], item.filter) || '-' }}
  62. </div>
  63. <div v-else-if="item.key === 'validity_period'">
  64. <span v-html="getValidityPeriodHtml(product, item)"></span>
  65. </div>
  66. <div v-else-if="item.key === 'supServicelds'">
  67. {{ item.label }}:{{ getFilteredValue(product[item.key], item.filter) || '-' }}
  68. </div>
  69. <div v-else-if="item.key === 'service_starttime'">
  70. {{ item.label }}:
  71. <span :class="{'no_open_root':!product.is_service_open}" >{{ product.is_service_open? (product.service_starttime || '-') : '暂未开通' }}</span>
  72. </div>
  73. <div v-else-if="item.key === 'service_endtime'">
  74. {{ item.label }}:
  75. <span :class="{'no_open_root': !product.is_service_open}" >{{ product.is_service_open ? (product.service_endtime || '-') : '暂未开通' }}</span>
  76. </div>
  77. <div v-else-if="item.key ==='phone'">
  78. {{ item.label }}:{{ orderData.user_phone || '-' }}
  79. </div>
  80. <div v-else>{{ item.label }}:{{ getFilteredValue(product[item.key], item.filter) || '-' }}</div>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. </ProductCard>
  86. </div>
  87. </InfoCard>
  88. <InfoCard title="其他信息">
  89. <div class="order-detail-card-content">
  90. <div class="order-detail-card-item" v-for="(item, index) in otherInfoItems" :key="index">
  91. {{ item.label }}:{{ getFilteredValue(orderData[item.key], item.filter) || '-' }}
  92. </div>
  93. </div>
  94. </InfoCard>
  95. </div>
  96. </template>
  97. <script>
  98. import InfoCard from '../../ui/InfoCard.vue';
  99. import ProductCard from '../../ui/ProductCard.vue';
  100. import { paymentTypeOptions } from '../../data/options.js';
  101. import { div, calcDiscountRate, roundToTwoDecimals } from '@/utils/number/';
  102. import TableCard from '../../ui/TableCard.vue';
  103. export default {
  104. name: 'OrderDetailCard',
  105. components: {
  106. InfoCard,
  107. ProductCard,
  108. TableCard
  109. },
  110. props: {
  111. orderDetail: {
  112. type: Object,
  113. default: () => {}
  114. }
  115. },
  116. data() {
  117. return {
  118. orderDetailInfo: {},
  119. basicInfoItems: [
  120. { label: '创建人', key: 'create_person' },
  121. { label: '创建时间', key: 'create_time' },
  122. { label: '最近更新人', key: 'last_update_person' },
  123. { label: '最近更新时间', key: 'autoUpdate' },
  124. { label: '订单审核状态', key: 'audit_status', filter: 'orderCoursed' },
  125. { label: '订单状态', key: 'order_status', filter: 'orderStatus' }
  126. ],
  127. otherInfoItems: [
  128. { label: '约定支付方式', key: 'pay_way' },
  129. { label: '下单渠道', key: 'order_channel_new' },
  130. { label: '付款户名', key: 'payment_user'},
  131. { label: '订单备注', key: 'remark' },
  132. ],
  133. productInfoTotalItems: [
  134. { label: '合同金额合计', key: 'final_price_total' },
  135. { label: '标准售价合计', key: 'original_price_total'},
  136. { label: '折扣率', key: 'rate_total' },
  137. { label: '渠道佣金', key: 'commission'},
  138. { label: '净合同金额合计', key: 'pure_amount'},
  139. { label: '0元订单类型', key: 'zero_type', condition: () => this.orderData.final_price_total === '0.00' },
  140. ],
  141. productInfoItems: [
  142. { label: '活动产品', key: 'activity_code', span: 1, condition: (product) => product.activity_code },
  143. { label: '付费类型', key: 'service_type', filter: 'orderServiceType', span: 3},
  144. { label: '升级内容', key: 'supServicelds', span: 3, condition: (product) => product.supServicelds },
  145. { label: '产品规格', key: 'productName', span: 3},
  146. { label: '服务列表', key: 'bigServiceNames', span: 1, condition: (product) => product.productName && product.productName.includes('自定义') && product.product_code === 'dyh001'},
  147. { label: '有效周期', key: 'validity_period', span: 1},
  148. { label: '合同金额', key: 'final_price', span: 3},
  149. { label: '标准售价', key: 'original_price', span: 3},
  150. { label: '折扣率', key: 'rate', span: 3},
  151. { label: '子账号数量', key: 'subAccountCount', span: 1, condition: (product) => product.product_type === 'VIP订阅' && product.buyAccountCount && product.giftAccountCount },
  152. { label: '主账号数量', key: 'mainAccountCount', span: 1, condition: (product) => product.product_type === 'VIP订阅' },
  153. { label: '关联订单', key: 'linkedOrder', span: 1},
  154. { label: '开通权益手机号', key: 'phone', span: 3},
  155. { label: '服务开始时间', key: 'service_starttime', span: 3},
  156. { label: '服务结束时间', key: 'service_endtime', span: 3}
  157. ],
  158. orderData: {},
  159. productData: [],
  160. linkOrderColumns: [
  161. {
  162. prop: 'name',
  163. label: '产品类型及规格',
  164. width: 154
  165. },
  166. {
  167. prop: 'empowerCount',
  168. label: '账号数量',
  169. width: 80
  170. },
  171. {
  172. prop: 'serviceEndTime',
  173. label: '到期时间',
  174. width: 102
  175. },
  176. {
  177. prop: 'buySubject',
  178. label: '购买主体',
  179. width: 80
  180. },
  181. {
  182. prop:'order_code',
  183. label: '订单编号',
  184. width: 154
  185. },
  186. {
  187. prop:'service_type',
  188. label: '付费类型',
  189. width: 80
  190. },
  191. {
  192. prop:'create_time',
  193. label: '创建时间',
  194. width: 102
  195. }
  196. ]
  197. }
  198. },
  199. watch: {
  200. orderDetail: {
  201. handler(newVal) {
  202. this.orderDetailInfo = newVal || {};
  203. this.init();
  204. },
  205. deep: true,
  206. immediate: true
  207. }
  208. },
  209. // async mounted() {
  210. // this.orderDetailInfo = await this.orderDetail || {}
  211. // this.init()
  212. // },
  213. computed: {},
  214. methods: {
  215. init() {
  216. this.orderData = this.orderDetailInfo?.orderData || {}
  217. let productData = this.orderDetailInfo?.productData || []
  218. if(productData.length > 0) {
  219. productData = productData.map(product => {
  220. try {
  221. const parsedFilter = this.parseProductFilter(product);
  222. const rate = this.calculateDiscountRate(product);
  223. const validityPeriod = this.calculateValidityPeriod(
  224. parsedFilter.buy_cycle,
  225. parsedFilter.buy_type,
  226. parsedFilter.give_cycle,
  227. parsedFilter.give_type,
  228. product
  229. );
  230. const subAccountCount = this.buildSubAccountCount(product);
  231. const mainAccountCount = this.buildMainAccountCount(product);
  232. const linkedOrder = this.processLinkedOrder(product);
  233. const finalPrice = this.formatNumber(product.final_price);
  234. const originalPrice = this.formatNumber(product.original_price);
  235. return {
  236. ...product,
  237. filter: parsedFilter,
  238. rate,
  239. validity_period: validityPeriod,
  240. subAccountCount,
  241. mainAccountCount,
  242. linkedOrder,
  243. final_price: finalPrice,
  244. original_price: originalPrice
  245. };
  246. } catch (error) {
  247. console.error('产品信息初始化失败:', error);
  248. return product;
  249. }
  250. });
  251. }
  252. this.setTotalAmounts(productData);
  253. this.productData = productData;
  254. },
  255. parseProductFilter(product) {
  256. let parsedFilter = {};
  257. if (typeof product.filter === 'string') {
  258. try {
  259. parsedFilter = JSON.parse(product.filter || '{}');
  260. } catch {
  261. parsedFilter = {};
  262. }
  263. } else {
  264. parsedFilter = product.filter || {};
  265. }
  266. return parsedFilter;
  267. },
  268. // 计算折扣率
  269. calculateDiscountRate(product) {
  270. let rate = '无法计算';
  271. if (product.original_price && Number(product.original_price) !== 0) {
  272. rate = (calcDiscountRate(product.final_price, product.original_price)) + '%';
  273. }
  274. return rate;
  275. },
  276. // 构造子账号数量字符串。
  277. buildSubAccountCount(product) {
  278. if (product.product_type === 'VIP订阅') {
  279. const buyCount = Number(product.buyAccountCount) || 0;
  280. const giftCount = Number(product.giftAccountCount) || 0;
  281. const countTotal = buyCount + giftCount;
  282. return `付费${buyCount}个,赠送${giftCount}个,合计:<span class="color_main">${countTotal}</span>个`;
  283. }
  284. return '';
  285. },
  286. // 构造主账号数量字符串。
  287. buildMainAccountCount(product) {
  288. if (product.product_type === 'VIP订阅') {
  289. return `付费1个,合计<span class="color_main">1</span>个`;
  290. }
  291. return '';
  292. },
  293. // 处理关联订单数据。
  294. processLinkedOrder(product) {
  295. if (product.linkedOrder && Object.keys(product.linkedOrder).length > 0) {
  296. const orderList = [product.linkedOrder];
  297. return this.flattenLinkOrderList(orderList);
  298. }
  299. return [];
  300. },
  301. // 设置合同金额合计和标准售价合计,以及计算折扣率总和。
  302. setTotalAmounts(productData) {
  303. const totalFinalPrice = productData.reduce((acc, cur) => acc + Number(cur.final_price), 0).toFixed(2);
  304. const totalOriginalPrice = productData.reduce((acc, cur) => acc + Number(cur.original_price), 0).toFixed(2);
  305. const rateTotal = div(totalFinalPrice, totalOriginalPrice) ? (div(totalFinalPrice, totalOriginalPrice) * 100).toFixed(2) + '%' : '无法计算';
  306. this.orderData.final_price_total = '¥' + totalFinalPrice;
  307. this.orderData.original_price_total = '¥' + totalOriginalPrice;
  308. this.orderData.rate_total = rateTotal;
  309. },
  310. flattenLinkOrderList(linkOrderList) {
  311. const result = [];
  312. if (!linkOrderList || linkOrderList.length === 0) return result;
  313. linkOrderList.forEach(item => {
  314. const { orderArr, ...rest } = item; // 拆分 orderArr 和其他字段
  315. if (!orderArr || orderArr.length === 0) return;
  316. orderArr.forEach(order => {
  317. result.push({
  318. ...rest, // 非 orderArr 的字段(如 name, empowerCount 等)
  319. ...order // orderArr 中的字段(如 order_code, service_type 等)
  320. });
  321. });
  322. });
  323. return result;
  324. },
  325. shouldRenderItem(item, product) {
  326. if (item.condition && typeof item.condition === 'function') {
  327. const conditionResult = item.condition(product)
  328. if(!conditionResult) {
  329. // item.span = 0
  330. }
  331. return conditionResult
  332. }
  333. return true;
  334. },
  335. calculateValidityPeriod(buyCycle, buyType, giveCycle, giveType, product) {
  336. let totalMonths = 0;
  337. let buyText = '';
  338. let giveText = '';
  339. const TIME_MAP = {
  340. '1': '天',
  341. '2': '月',
  342. '3': '年',
  343. '4': '季度'
  344. }
  345. // 安全地将字符串转为整数,默认为 0
  346. const parseCycle = (cycle) => {
  347. const num = parseInt(cycle);
  348. return isNaN(num) ? 0 : num;
  349. };
  350. // 计算周期并返回文本和月份数
  351. const processCycle = (cycle, type) => {
  352. const textValue = TIME_MAP[type] || '';
  353. const value = parseCycle(cycle);
  354. let months = 0;
  355. let text = '';
  356. if (textValue.includes('月')) {
  357. months += value;
  358. text = `${value}个月`;
  359. } else if (textValue.includes('年')) {
  360. months += value * 12;
  361. text = `${value}年`;
  362. } else if (textValue.includes('季度')) {
  363. months += value * 3;
  364. text = `${value}季度`;
  365. } else if (textValue.includes('天')) {
  366. months += value / 30; // 注意:此处为近似值
  367. text = `${value}天`;
  368. }
  369. return { months, text };
  370. };
  371. const buyResult = processCycle(buyCycle, buyType);
  372. const giveResult = processCycle(giveCycle, giveType);
  373. totalMonths = buyResult.months + giveResult.months;
  374. buyText = buyResult.text;
  375. giveText = giveResult.text;
  376. const returned_open = product.returned_open === '1' ? '(全额回款当日开通)' : '';
  377. if(totalMonths === 0) return '-'
  378. return `付费${buyText},赠送${giveText},合计<span class="color_main">${totalMonths.toFixed(0)}</span>个月${returned_open}`;
  379. },
  380. getValidityPeriodHtml(product, item) {
  381. const label = item.label;
  382. const value = product[item.key] || '-';
  383. return `${label}:${value}`;
  384. },
  385. // 替代过滤器的通用方法
  386. getFilteredValue(value, filterName) {
  387. if (!filterName) return value || '-';
  388. return this[filterName](value);
  389. },
  390. setProductTitle(product, index) {
  391. const tactics = product.tactics === '2' ? '【赠送】' : '【售卖】';
  392. return `${index + 1}.${tactics}${product.product_type}`;
  393. },
  394. // 格式化数字,保留两位小数
  395. formatNumber(num, x = 2) {
  396. if(!num) return 0.00
  397. const newnum = Number(num) / 100;
  398. return roundToTwoDecimals(newnum, x)
  399. },
  400. orderCoursed(val) {
  401. if (val == 0) {
  402. return '待提交'
  403. } else if (val == 1) {
  404. return '待一审'
  405. } else if (val == 2) {
  406. return '待二审'
  407. } else if (val == 4) {
  408. return '待三审'
  409. } else if (val == 3) {
  410. return '已通过'
  411. } else if (val == -2 || val == -3 || val == -4) {
  412. return '已退回'
  413. }
  414. },
  415. orderServiceType(val) {
  416. const matchedOption = paymentTypeOptions.find(option => option.value === val);
  417. return matchedOption ? matchedOption.label : val; // 如果未找到匹配项,返回原始值
  418. },
  419. orderStatus(val) {
  420. if (!val || val == 0) {
  421. return '未完成'
  422. } else if (val == 1) {
  423. return '已完成'
  424. } else if (val == -1) {
  425. return '逻辑删除'
  426. } else if (val == -2) {
  427. return '已取消'
  428. } else if (val == -3) {
  429. return '已退款'
  430. } else if (val == -3) {
  431. return '已退款'
  432. }
  433. },
  434. objectSpanMethod({ row, column, rowIndex, columnIndex }) {
  435. if (columnIndex <= 3) {
  436. if (rowIndex % 2 === 0) {
  437. return {
  438. rowspan: 2,
  439. colspan: 1
  440. };
  441. } else {
  442. return {
  443. rowspan: 0,
  444. colspan: 0
  445. };
  446. }
  447. }
  448. }
  449. }
  450. }
  451. </script>
  452. <style lang="scss" scoped>
  453. .order-detail-card-container {
  454. background: #F2F2F4;
  455. ::v-deep {
  456. .info-card {
  457. margin-bottom: 16px;
  458. }
  459. }
  460. .order-detail-card-content {
  461. display: flex;
  462. flex-wrap: wrap;
  463. align-items: center;
  464. margin-bottom: 6px;
  465. }
  466. .grouped-items {
  467. display: flex;
  468. flex-wrap: wrap;
  469. margin-bottom: 10px;
  470. .item-span-1 {
  471. width: 100%;
  472. }
  473. .item-span-3 {
  474. min-width: 255px;
  475. }
  476. }
  477. .order-detail-product-content {
  478. display: flex;
  479. flex-wrap: wrap;
  480. align-items: center;
  481. margin-bottom: 6px;
  482. padding: 0 24px;
  483. }
  484. .order-detail-card-item {
  485. min-width: 255px;
  486. margin-right: 32px;
  487. margin-bottom: 10px;
  488. font-size: 14px;
  489. line-height: 22px;
  490. color: $gray_10;
  491. .linkedOrder {
  492. display: flex;
  493. align-items: flex-start;
  494. }
  495. }
  496. .no_open_root {
  497. color: #F56500;
  498. }
  499. ::v-deep {
  500. .color_main {
  501. color: #2ABED1;
  502. }
  503. }
  504. }
  505. </style>