OrderDetailView.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. <template>
  2. <div class="app-page">
  3. <section class="glass-card section-card header-section">
  4. <div class="order-detail-header">
  5. <div class="header-left">
  6. <div class="header-top">
  7. <el-button @click="$router.back()">返回列表</el-button>
  8. <el-button type="primary" @click="handleContact">联系买家</el-button>
  9. </div>
  10. <h2>订单详情 <span class="order-no">{{ order.orderNo }}</span></h2>
  11. <div class="header-tags">
  12. <el-tag v-if="order.channelOrderNo" type="info">渠道: {{ order.channelOrderNo }}</el-tag>
  13. <el-tag v-if="order.priority === 'urgent'" type="danger">加急</el-tag>
  14. <el-tag v-if="order.buyerLevel" :type="buyerLevelType(order.buyerLevel)">{{ order.buyerLevel }}</el-tag>
  15. <el-tag v-for="tag in (order.orderTags || [])" :key="tag" size="small" type="warning">{{ tag }}</el-tag>
  16. </div>
  17. </div>
  18. <div class="header-actions">
  19. <el-button @click="handlePrint">打印面单</el-button>
  20. <el-button @click="handlePrintInvoice">打印发货单</el-button>
  21. </div>
  22. </div>
  23. <div class="stat-grid">
  24. <article class="stat-card">
  25. <div class="stat-card__icon" :style="{ background: statusColor }">
  26. <el-icon><Check /></el-icon>
  27. </div>
  28. <div class="stat-card__content">
  29. <div class="stat-card__label">订单状态</div>
  30. <div class="stat-card__value">{{ statusLabel(order.orderStatus ?? '') }}</div>
  31. </div>
  32. </article>
  33. <article class="stat-card">
  34. <div class="stat-card__icon" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%)">
  35. <el-icon><Money /></el-icon>
  36. </div>
  37. <div class="stat-card__content">
  38. <div class="stat-card__label">实付金额</div>
  39. <div class="stat-card__value">{{ order.currency }} {{ order.actualPaid || order.amount }}</div>
  40. </div>
  41. </article>
  42. <article class="stat-card">
  43. <div class="stat-card__icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)">
  44. <el-icon><Van /></el-icon>
  45. </div>
  46. <div class="stat-card__content">
  47. <div class="stat-card__label">发货状态</div>
  48. <div class="stat-card__value">{{ order.shippingStatus || '待发货' }}</div>
  49. </div>
  50. </article>
  51. <article class="stat-card">
  52. <div class="stat-card__icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
  53. <el-icon><Warning /></el-icon>
  54. </div>
  55. <div class="stat-card__content">
  56. <div class="stat-card__label">异常标签</div>
  57. <el-tag :type="order.exceptionTag === '正常' ? 'success' : 'warning'" size="default">{{ order.exceptionTag || '正常' }}</el-tag>
  58. </div>
  59. </article>
  60. <article class="stat-card">
  61. <div class="stat-card__icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
  62. <el-icon><OfficeBuilding /></el-icon>
  63. </div>
  64. <div class="stat-card__content">
  65. <div class="stat-card__label">分配仓库</div>
  66. <div class="stat-card__value">{{ order.warehouse || '待分配' }}</div>
  67. </div>
  68. </article>
  69. </div>
  70. </section>
  71. <div class="detail-grid">
  72. <div class="detail-left">
  73. <article class="glass-card section-card">
  74. <div class="section-header">
  75. <h3>买家信息</h3>
  76. </div>
  77. <el-descriptions :column="2" border size="default">
  78. <el-descriptions-item label="买家ID">{{ order.buyerId }}</el-descriptions-item>
  79. <el-descriptions-item label="买家昵称">{{ order.buyer }}</el-descriptions-item>
  80. <el-descriptions-item label="邮箱">{{ order.buyerEmail }}</el-descriptions-item>
  81. <el-descriptions-item label="电话">{{ order.buyerPhone }}</el-descriptions-item>
  82. <el-descriptions-item label="国家">{{ order.buyerCountry }} {{ getCountryFlag(order.buyerCountry) }}</el-descriptions-item>
  83. <el-descriptions-item label="会员等级">{{ order.buyerLevel || '-' }}</el-descriptions-item>
  84. <el-descriptions-item label="注册时间">{{ order.buyerRegisterTime || '-' }}</el-descriptions-item>
  85. <el-descriptions-item label="历史订单">{{ order.buyerOrderCount || 0 }} 单</el-descriptions-item>
  86. <el-descriptions-item label="历史消费">{{ order.buyerTotalSpent || '-' }}</el-descriptions-item>
  87. <el-descriptions-item label="设备信息">{{ order.device }} / {{ order.browser }} / {{ order.os }}</el-descriptions-item>
  88. </el-descriptions>
  89. </article>
  90. <article class="glass-card section-card">
  91. <div class="section-header">
  92. <h3>收货信息</h3>
  93. <el-button link type="primary" @click="editAddressDialog = true" :disabled="order.orderStatus === 'cancelled'">修改地址</el-button>
  94. </div>
  95. <el-descriptions :column="2" border size="default">
  96. <el-descriptions-item label="收货人">
  97. <span class="value-bold">{{ order.receiverName }}</span>
  98. <el-button link type="primary" size="small" @click="handleCall">拨打电话</el-button>
  99. </el-descriptions-item>
  100. <el-descriptions-item label="联系电话">{{ order.receiverPhone }}</el-descriptions-item>
  101. <el-descriptions-item label="国家/地区">{{ order.receiverCountry }} {{ getCountryFlag(order.receiverCountry) }}</el-descriptions-item>
  102. <el-descriptions-item label="州/省">{{ order.receiverState || '-' }}</el-descriptions-item>
  103. <el-descriptions-item label="城市">{{ order.receiverCity }}</el-descriptions-item>
  104. <el-descriptions-item label="区县">{{ order.receiverDistrict || '-' }}</el-descriptions-item>
  105. <el-descriptions-item label="邮编">{{ order.receiverPostalCode || '-' }}</el-descriptions-item>
  106. <el-descriptions-item label="详细地址" :span="2">
  107. <span class="address-text">{{ order.receiverAddress }}</span>
  108. </el-descriptions-item>
  109. <el-descriptions-item label="坐标">{{ order.latitude?.toFixed(4) }}, {{ order.longitude?.toFixed(4) }}</el-descriptions-item>
  110. <el-descriptions-item label="买家备注">
  111. <span class="buyer-remark">{{ order.buyerRemark || '无' }}</span>
  112. </el-descriptions-item>
  113. </el-descriptions>
  114. </article>
  115. <article class="glass-card section-card">
  116. <div class="section-header">
  117. <h3>支付信息</h3>
  118. </div>
  119. <el-descriptions :column="2" border size="default">
  120. <el-descriptions-item label="支付方式">{{ order.paymentMethod }}</el-descriptions-item>
  121. <el-descriptions-item label="支付时间">{{ order.paymentTime || '-' }}</el-descriptions-item>
  122. <el-descriptions-item label="交易流水">{{ order.transactionId || '-' }}</el-descriptions-item>
  123. <el-descriptions-item label="订单货币">{{ order.currency }} (汇率: {{ order.exchangeRate || 1 }})</el-descriptions-item>
  124. <el-descriptions-item label="商品金额">{{ order.orderAmount || order.amount }}</el-descriptions-item>
  125. <el-descriptions-item label="折合CNY">¥{{ order.orderAmountCNY || '-' }}</el-descriptions-item>
  126. <el-descriptions-item label="税额">{{ order.taxAmount || '-' }}</el-descriptions-item>
  127. <el-descriptions-item label="运费">{{ order.shippingFee || '0.00' }}</el-descriptions-item>
  128. <el-descriptions-item label="优惠折扣" :span="2">
  129. <span v-if="order.couponCode" class="discount-tag">
  130. {{ order.couponCode }} (-{{ order.couponDiscount || order.discountAmount || '0.00' }})
  131. </span>
  132. <span v-else>-</span>
  133. </el-descriptions-item>
  134. <el-descriptions-item label="实付金额">
  135. <span class="value-bold actual-paid">{{ order.currency }} {{ order.actualPaid || order.amount }}</span>
  136. </el-descriptions-item>
  137. <el-descriptions-item label="退款金额">{{ order.refundAmount || '0.00' }}</el-descriptions-item>
  138. </el-descriptions>
  139. </article>
  140. <article class="glass-card section-card">
  141. <div class="section-header">
  142. <h3>物流追踪</h3>
  143. <el-button link type="primary" @click="trackMore">查看更多</el-button>
  144. </div>
  145. <div v-if="order.trackingNo" class="tracking-info">
  146. <div class="tracking-info__main">
  147. <el-tag type="success" size="default">{{ order.carrier || '顺丰速运' }}</el-tag>
  148. <span class="tracking-no">{{ order.trackingNo }}</span>
  149. <el-button link type="primary" @click="copyTrackingNo">复制</el-button>
  150. <el-button link type="primary" @click="openTrackingUrl">查询物流</el-button>
  151. </div>
  152. <div class="tracking-meta">
  153. <span>配送方式: {{ order.shippingMethod || '-' }}</span>
  154. <span>仓库: {{ order.warehouse || '-' }}</span>
  155. <span>库位: {{ order.warehouseLocation || '-' }}</span>
  156. </div>
  157. <el-timeline class="track-timeline" v-if="trackingSteps.length">
  158. <el-timeline-item v-for="(step, index) in trackingSteps" :key="index" :timestamp="step.time" :type="step.type" :hollow="index === 0">
  159. <p class="track-title">{{ step.title }}</p>
  160. <p class="track-detail" v-if="step.detail">{{ step.detail }}</p>
  161. </el-timeline-item>
  162. </el-timeline>
  163. <el-empty v-else description="暂无物流信息" />
  164. </div>
  165. <div v-else class="tracking-empty">
  166. <el-empty description="暂无物流信息" />
  167. <el-button v-if="order.orderStatus === 'shipped'" type="primary" size="default" @click="addTrackingDialog = true">添加物流信息</el-button>
  168. </div>
  169. </article>
  170. <article class="glass-card section-card">
  171. <div class="section-header">
  172. <h3>营销归因</h3>
  173. </div>
  174. <el-descriptions :column="2" border size="default">
  175. <el-descriptions-item label="来源(UTM)">{{ order.utmSource || '-' }}</el-descriptions-item>
  176. <el-descriptions-item label="媒介(UTM)">{{ order.utmMedium || '-' }}</el-descriptions-item>
  177. <el-descriptions-item label="活动(UTM)">{{ order.utmCampaign || '-' }}</el-descriptions-item>
  178. <el-descriptions-item label="内容(UTM)">{{ order.utmContent || '-' }}</el-descriptions-item>
  179. <el-descriptions-item label="关键词(UTM)">{{ order.utmTerm || '-' }}</el-descriptions-item>
  180. <el-descriptions-item label="下单IP">{{ order.ip || '-' }}</el-descriptions-item>
  181. <el-descriptions-item label="IP归属国">{{ order.ipCountry || '-' }}</el-descriptions-item>
  182. <el-descriptions-item label="落地页">{{ order.landingPage || '-' }}</el-descriptions-item>
  183. </el-descriptions>
  184. </article>
  185. </div>
  186. <div class="detail-right">
  187. <article class="glass-card section-card">
  188. <h3>快捷操作</h3>
  189. <div class="action-menu">
  190. <el-dropdown trigger="click" @command="handleActionCommand">
  191. <el-button type="primary">
  192. 订单操作 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
  193. </el-button>
  194. <template #dropdown>
  195. <el-dropdown-menu>
  196. <el-dropdown-item command="assign" :disabled="order.orderStatus === 'cancelled'">
  197. <el-icon><Van /></el-icon> 提交仓库
  198. </el-dropdown-item>
  199. <el-dropdown-item command="lock" :disabled="order.orderStatus === 'cancelled'">
  200. <el-icon><Lock /></el-icon> 锁定库存
  201. </el-dropdown-item>
  202. <el-dropdown-item command="split" :disabled="order.orderStatus === 'cancelled'">
  203. <el-icon><Connection /></el-icon> 拆单
  204. </el-dropdown-item>
  205. <el-dropdown-item command="merge" :disabled="order.orderStatus === 'cancelled'">
  206. <el-icon><FolderMerged /></el-icon> 合单
  207. </el-dropdown-item>
  208. <el-dropdown-item command="address" :disabled="order.orderStatus === 'cancelled'">
  209. <el-icon><Edit /></el-icon> 修改地址
  210. </el-dropdown-item>
  211. </el-dropdown-menu>
  212. </template>
  213. </el-dropdown>
  214. <el-dropdown trigger="click" @command="handlePrintCommand">
  215. <el-button>
  216. 打印 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
  217. </el-button>
  218. <template #dropdown>
  219. <el-dropdown-menu>
  220. <el-dropdown-item command="label">
  221. <el-icon><Printer /></el-icon> 打印面单
  222. </el-dropdown-item>
  223. <el-dropdown-item command="invoice">
  224. <el-icon><Document /></el-icon> 打印发货单
  225. </el-dropdown-item>
  226. </el-dropdown-menu>
  227. </template>
  228. </el-dropdown>
  229. <el-dropdown trigger="click" @command="handleRefundCommand">
  230. <el-button type="success">
  231. 退款 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
  232. </el-button>
  233. <template #dropdown>
  234. <el-dropdown-menu>
  235. <el-dropdown-item command="refund" :disabled="!canRefund">
  236. <el-icon><Money /></el-icon> 发起退款
  237. </el-dropdown-item>
  238. <el-dropdown-item command="viewRefund">
  239. <el-icon><Tickets /></el-icon> 查看退款记录
  240. </el-dropdown-item>
  241. </el-dropdown-menu>
  242. </template>
  243. </el-dropdown>
  244. <el-button type="danger" plain @click="cancelDialog = true" :disabled="order.orderStatus === 'cancelled'">
  245. 取消订单
  246. </el-button>
  247. </div>
  248. </article>
  249. <article class="glass-card section-card">
  250. <h3>订单关联</h3>
  251. <el-descriptions :column="1" border size="default">
  252. <el-descriptions-item label="母订单">{{ order.parentOrderId || '-' }}</el-descriptions-item>
  253. <el-descriptions-item label="子订单">{{ order.childOrderIds?.join(', ') || '-' }}</el-descriptions-item>
  254. <el-descriptions-item label="合单订单">{{ order.mergeOrderId || '-' }}</el-descriptions-item>
  255. <el-descriptions-item label="关联订单">{{ order.relatedOrderId || '-' }}</el-descriptions-item>
  256. <el-descriptions-item label="原订单(退款/重发)">{{ order.originalOrderId || '-' }}</el-descriptions-item>
  257. </el-descriptions>
  258. </article>
  259. <article class="glass-card section-card">
  260. <h3>客服信息</h3>
  261. <el-descriptions :column="1" border size="default">
  262. <el-descriptions-item label="处理人">{{ order.handler || '待分配' }}</el-descriptions-item>
  263. <el-descriptions-item label="处理组">{{ order.handlerGroup || '-' }}</el-descriptions-item>
  264. </el-descriptions>
  265. </article>
  266. <article class="glass-card section-card">
  267. <h3>内部备注</h3>
  268. <el-input v-model="order.internalRemark" type="textarea" :rows="3" placeholder="客服内部备注信息" />
  269. <el-button type="primary" size="default" @click="saveRemark" class="save-btn">保存备注</el-button>
  270. </article>
  271. <article class="glass-card section-card">
  272. <h3>操作日志</h3>
  273. <el-timeline>
  274. <el-timeline-item v-for="(log, index) in operationLogs" :key="index" :timestamp="log.time" :type="log.type" :hollow="index === 0">
  275. <p class="log-title">{{ log.title }}</p>
  276. <p class="log-detail" v-if="log.operator">操作人: {{ log.operator }} ({{ log.operatorRole }})</p>
  277. </el-timeline-item>
  278. </el-timeline>
  279. </article>
  280. </div>
  281. </div>
  282. <section class="glass-card section-card products-section">
  283. <div class="section-header">
  284. <h3>商品明细 ({{ order.items?.length || 0 }} 件)</h3>
  285. </div>
  286. <el-table :data="order.items" stripe>
  287. <el-table-column prop="sku" label="SKU编码" width="160">
  288. <template #default="{ row }">
  289. <span class="sku-code">{{ row.sku }}</span>
  290. </template>
  291. </el-table-column>
  292. <el-table-column prop="productTitle" label="商品信息" min-width="260">
  293. <template #default="{ row }">
  294. <div class="product-info">
  295. <el-avatar v-if="row.productImage" :src="row.productImage" :size="56" shape="square" />
  296. <div v-else class="product-image-placeholder">暂无图</div>
  297. <div class="product-detail">
  298. <div class="product-title">{{ row.productTitle }}</div>
  299. <div class="product-category">{{ row.categoryName }} | {{ row.skuId }}</div>
  300. </div>
  301. </div>
  302. </template>
  303. </el-table-column>
  304. <el-table-column prop="specs" label="规格" width="160">
  305. <template #default="{ row }">
  306. <div class="specs-list">
  307. <el-tag v-for="spec in row.specs" :key="spec.specName" size="small">{{ spec.specValue }}</el-tag>
  308. <span v-if="!row.specs || row.specs.length === 0">-</span>
  309. </div>
  310. </template>
  311. </el-table-column>
  312. <el-table-column prop="barcode" label="条码" width="140">
  313. <template #default="{ row }">
  314. <span class="barcode">{{ row.barcode || '-' }}</span>
  315. </template>
  316. </el-table-column>
  317. <el-table-column prop="qty" label="数量" width="90" align="center">
  318. <template #default="{ row }">
  319. <span class="qty">{{ row.qty }}</span>
  320. <div class="qty-detail" v-if="row.shippedQty || row.returnedQty">
  321. <span>已发: {{ row.shippedQty || 0 }}</span>
  322. <span>退: {{ row.returnedQty || 0 }}</span>
  323. </div>
  324. </template>
  325. </el-table-column>
  326. <el-table-column prop="price" label="单价" width="100" align="right">
  327. <template #default="{ row }">
  328. <span>{{ order.currency }} {{ row.price }}</span>
  329. </template>
  330. </el-table-column>
  331. <el-table-column prop="costPrice" label="成本价" width="100" align="right">
  332. <template #default="{ row }">
  333. <span class="cost-price">{{ order.currency }} {{ row.costPrice }}</span>
  334. </template>
  335. </el-table-column>
  336. <el-table-column prop="profit" label="利润" width="100" align="right">
  337. <template #default="{ row }">
  338. <span class="profit">{{ order.currency }} {{ row.profit }}</span>
  339. <div class="profit-rate">{{ row.profitRate }}%</div>
  340. </template>
  341. </el-table-column>
  342. <el-table-column prop="subtotal" label="小计" width="100" align="right">
  343. <template #default="{ row }">
  344. <span class="subtotal">{{ order.currency }} {{ row.subtotal }}</span>
  345. </template>
  346. </el-table-column>
  347. <el-table-column prop="inventory" label="库存" width="150" align="center">
  348. <template #default="{ row }">
  349. <div class="inventory-info">
  350. <span class="inv-available">可用: {{ row.available }}</span>
  351. <span class="inv-locked">锁定: {{ row.locked }}</span>
  352. <span class="inv-transit">在途: {{ row.inTransit }}</span>
  353. </div>
  354. </template>
  355. </el-table-column>
  356. <el-table-column prop="giftFlag" label="赠品" width="80" align="center">
  357. <template #default="{ row }">
  358. <el-tag v-if="row.giftFlag" type="warning" size="small">赠品</el-tag>
  359. <span v-else>-</span>
  360. </template>
  361. </el-table-column>
  362. </el-table>
  363. <div class="amount-summary">
  364. <div class="summary-row">
  365. <span>商品总额:</span>
  366. <span>{{ order.currency }} {{ order.orderAmount || order.amount }}</span>
  367. </div>
  368. <div class="summary-row">
  369. <span>税额:</span>
  370. <span>{{ order.currency }} {{ order.taxAmount || '0.00' }}</span>
  371. </div>
  372. <div class="summary-row">
  373. <span>运费:</span>
  374. <span>{{ order.currency }} {{ order.shippingFee || '0.00' }}</span>
  375. </div>
  376. <div class="summary-row" v-if="order.discountAmount && parseFloat(order.discountAmount) > 0">
  377. <span>优惠:</span>
  378. <span class="discount">-{{ order.currency }} {{ order.discountAmount }}</span>
  379. </div>
  380. <div class="summary-row" v-if="order.couponDiscount && parseFloat(order.couponDiscount) > 0">
  381. <span>优惠券:</span>
  382. <span class="discount">-{{ order.currency }} {{ order.couponDiscount }} ({{ order.couponCode }})</span>
  383. </div>
  384. <div class="summary-row summary-row--total">
  385. <span>实付金额:</span>
  386. <span class="total-amount">{{ order.currency }} {{ order.actualPaid || order.amount }}</span>
  387. </div>
  388. <div class="summary-row summary-row--profit">
  389. <span>订单利润:</span>
  390. <span class="profit-amount">{{ order.currency }} {{ totalProfit }}</span>
  391. <span class="profit-rate">({{ totalProfitRate }}%)</span>
  392. </div>
  393. </div>
  394. </section>
  395. <section class="glass-card section-card package-section">
  396. <div class="section-header">
  397. <h3>包裹信息</h3>
  398. </div>
  399. <el-descriptions :column="4" border size="default">
  400. <el-descriptions-item label="重量">{{ order.weight ? order.weight.toFixed(2) + ' kg' : '-' }}</el-descriptions-item>
  401. <el-descriptions-item label="体积">L: {{ order.length || '-' }}cm x W: {{ order.width || '-' }}cm x H: {{ order.height || '-' }}cm</el-descriptions-item>
  402. <el-descriptions-item label="仓库">{{ order.warehouse || '-' }}</el-descriptions-item>
  403. <el-descriptions-item label="库位">{{ order.warehouseLocation || '-' }}</el-descriptions-item>
  404. </el-descriptions>
  405. </section>
  406. <el-dialog v-model="cancelDialog" title="取消订单" width="520px">
  407. <el-alert title="取消订单需二次确认,操作不可撤销" type="warning" :closable="false" style="margin-bottom:16px" />
  408. <el-form label-position="top">
  409. <el-form-item label="取消原因" required>
  410. <el-select v-model="cancelReason" style="width:100%">
  411. <el-option label="买家申请取消" value="buyer_cancel" />
  412. <el-option label="地址异常无法发货" value="address_error" />
  413. <el-option label="库存不足" value="out_of_stock" />
  414. <el-option label="商品缺货" value="goods_unavailable" />
  415. <el-option label="其他原因" value="other" />
  416. </el-select>
  417. </el-form-item>
  418. <el-form-item label="补充说明">
  419. <el-input v-model="cancelRemark" type="textarea" :rows="3" />
  420. </el-form-item>
  421. </el-form>
  422. <template #footer>
  423. <el-button @click="cancelDialog = false">关闭</el-button>
  424. <el-button type="danger" @click="confirmCancel">确认取消</el-button>
  425. </template>
  426. </el-dialog>
  427. <el-dialog v-model="splitDialog" title="拆单处理" width="620px">
  428. <el-alert title="拆单后原订单将变更为主订单,新订单将从原订单拆分" type="info" :closable="false" style="margin-bottom:16px" />
  429. <el-table :data="order.items">
  430. <el-table-column prop="sku" label="SKU" min-width="160" />
  431. <el-table-column prop="productTitle" label="商品" min-width="180" show-overflow-tooltip />
  432. <el-table-column prop="qty" label="原数量" width="90" align="center" />
  433. <el-table-column label="主单数量" width="120">
  434. <template #default="{ row }">
  435. <el-input-number v-model="row.mainQty" :min="0" :max="row.qty" size="small" @change="updateSplitQty(row)" />
  436. </template>
  437. </el-table-column>
  438. <el-table-column label="新单数量" width="100">
  439. <template #default="{ row }">
  440. <span class="split-qty">{{ row.qty - (row.mainQty || 0) }}</span>
  441. </template>
  442. </el-table-column>
  443. </el-table>
  444. <template #footer>
  445. <el-button @click="splitDialog = false">取消</el-button>
  446. <el-button type="primary" @click="confirmSplit">确认拆单</el-button>
  447. </template>
  448. </el-dialog>
  449. <el-dialog v-model="mergeDialog" title="合单处理" width="480px">
  450. <el-form label-position="top">
  451. <el-form-item label="目标订单号" required>
  452. <el-input v-model="mergeTarget" placeholder="请输入待合并的订单号" />
  453. </el-form-item>
  454. <el-form-item>
  455. <el-alert title="合单要求:两个订单买家信息一致,且均未发货" type="warning" :closable="false" />
  456. </el-form-item>
  457. </el-form>
  458. <template #footer>
  459. <el-button @click="mergeDialog = false">取消</el-button>
  460. <el-button type="primary" @click="confirmMerge">确认合单</el-button>
  461. </template>
  462. </el-dialog>
  463. <el-dialog v-model="editAddressDialog" title="修改收货地址" width="520px">
  464. <el-form :model="editAddress" label-position="top">
  465. <el-form-item label="收货人" required>
  466. <el-input v-model="editAddress.receiverName" placeholder="请输入收货人姓名" />
  467. </el-form-item>
  468. <el-form-item label="联系电话" required>
  469. <el-input v-model="editAddress.receiverPhone" placeholder="请输入联系电话" />
  470. </el-form-item>
  471. <el-form-item label="国家" required>
  472. <el-input v-model="editAddress.receiverCountry" placeholder="请输入国家" />
  473. </el-form-item>
  474. <el-form-item label="州/省">
  475. <el-input v-model="editAddress.receiverState" placeholder="请输入州/省" />
  476. </el-form-item>
  477. <el-form-item label="城市" required>
  478. <el-input v-model="editAddress.receiverCity" placeholder="请输入城市" />
  479. </el-form-item>
  480. <el-form-item label="邮编">
  481. <el-input v-model="editAddress.receiverPostalCode" placeholder="请输入邮编" />
  482. </el-form-item>
  483. <el-form-item label="详细地址" required>
  484. <el-input v-model="editAddress.receiverAddress" type="textarea" :rows="3" placeholder="请输入详细地址" />
  485. </el-form-item>
  486. </el-form>
  487. <template #footer>
  488. <el-button @click="editAddressDialog = false">取消</el-button>
  489. <el-button type="primary" @click="confirmEditAddress">确认修改</el-button>
  490. </template>
  491. </el-dialog>
  492. <el-drawer v-model="assignDrawer" title="分配仓库" size="400px">
  493. <el-form label-position="top">
  494. <el-form-item label="选择仓库" required>
  495. <el-select v-model="order.warehouse" style="width:100%">
  496. <el-option label="深圳南山仓" value="深圳南山仓" />
  497. <el-option label="义乌商贸仓" value="义乌商贸仓" />
  498. <el-option label="洛杉矶海外仓" value="洛杉矶海外仓" />
  499. </el-select>
  500. </el-form-item>
  501. <el-form-item label="库位">
  502. <el-input v-model="order.warehouseLocation" placeholder="可选填写库位号" />
  503. </el-form-item>
  504. <el-form-item label="仓库备注">
  505. <el-input v-model="warehouseRemark" type="textarea" :rows="4" placeholder="可选填写仓库操作备注" />
  506. </el-form-item>
  507. </el-form>
  508. <template #footer>
  509. <el-button @click="assignDrawer = false">取消</el-button>
  510. <el-button type="primary" @click="submitWarehouse">确认提交</el-button>
  511. </template>
  512. </el-drawer>
  513. </div>
  514. </template>
  515. <script setup lang="ts">
  516. import { onMounted, reactive, ref, computed } from 'vue';
  517. import { useRoute } from 'vue-router';
  518. import { ElMessage, ElMessageBox } from 'element-plus';
  519. import { api } from '@/api/services';
  520. import type { OrderItem, OrderProductItem } from '@/types/page';
  521. const route = useRoute();
  522. const cancelDialog = ref(false);
  523. const splitDialog = ref(false);
  524. const mergeDialog = ref(false);
  525. const assignDrawer = ref(false);
  526. const editAddressDialog = ref(false);
  527. const cancelReason = ref('');
  528. const cancelRemark = ref('');
  529. const mergeTarget = ref('');
  530. const warehouseRemark = ref('');
  531. const canRefund = computed(() => ['paid', 'allocated', 'shipped'].includes(order.orderStatus || ''));
  532. const countryFlags: Record<string, string> = {
  533. 'US': '🇺🇸', 'UK': '🇬🇧', 'GB': '🇬🇧', 'JP': '🇯🇵', 'DE': '🇩🇪', 'FR': '🇫🇷',
  534. 'CA': '🇨🇦', 'AU': '🇦🇺', 'IT': '🇮🇹', 'ES': '🇪🇸', 'NL': '🇳🇱', 'BR': '🇧🇷',
  535. 'MX': '🇲🇽', 'KR': '🇰🇷', 'CN': '🇨🇳', 'HK': '🇭🇰', 'TW': '🇹🇼', 'SG': '🇸🇬'
  536. };
  537. const getCountryFlag = (country: string): string => countryFlags[country] || '🌍';
  538. const statusColor = computed(() => {
  539. const colors: Record<string, string> = {
  540. created: 'linear-gradient(135deg, #909399 0%, #b1b3b8 100%)',
  541. paid: 'linear-gradient(135deg, #409eff 0%, #66b1ff 100%)',
  542. allocated: 'linear-gradient(135deg, #e6a23c 0%, #ebb563 100%)',
  543. shipped: 'linear-gradient(135deg, #67c23a 0%, #85ce61 100%)',
  544. delivered: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)',
  545. completed: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)',
  546. cancelled: 'linear-gradient(135deg, #f56c6c 0%, #f78989 100%)',
  547. refunded: 'linear-gradient(135deg, #f56c6c 0%, #f78989 100%)'
  548. };
  549. return colors[order.orderStatus || ''] || colors.created;
  550. });
  551. const order = reactive<OrderItem>({
  552. id: '', orderNo: '', channelOrderNo: '', channel: '', orderStatus: '', shippingStatus: '', paymentStatus: '', exceptionTag: '', priority: 'normal',
  553. createdAt: '', updatedAt: '', paidAt: '', shippedAt: '', deliveredAt: '',
  554. buyerId: '', buyer: '', buyerEmail: '', buyerPhone: '', buyerCountry: '', buyerLevel: '', buyerTags: [], buyerRegisterTime: '', buyerOrderCount: 0, buyerTotalSpent: '',
  555. receiverName: '', receiverPhone: '', receiverCountry: '', receiverState: '', receiverCity: '', receiverDistrict: '', receiverPostalCode: '', receiverAddress: '', receiverAddressLang: '', latitude: 0, longitude: 0,
  556. transactionId: '', paymentMethod: '', paymentTime: '', currency: 'USD', exchangeRate: 1, orderAmount: '', orderAmountCNY: '', taxAmount: '', shippingFee: '', discountAmount: '', couponCode: '', couponDiscount: '', actualPaid: '', refundAmount: '', refundStatus: '',
  557. shippingMethod: '', carrier: '', trackingNo: '', trackingUrl: '', warehouse: '', warehouseLocation: '', estimatedDelivery: '', weight: 0, length: 0, width: 0, height: 0,
  558. utmSource: '', utmMedium: '', utmCampaign: '', utmContent: '', utmTerm: '', referrerUrl: '', landingPage: '', ip: '', ipCountry: '', device: '', browser: '', os: '',
  559. parentOrderId: '', childOrderIds: [], mergeOrderId: '', relatedOrderId: '', originalOrderId: '',
  560. handler: '', handlerGroup: '', orderTags: [], internalRemark: '', buyerRemark: '',
  561. itemCount: 0, amount: '', items: [], timeline: [], logs: []
  562. });
  563. const operationLogs = ref<{ time: string; title: string; type: string; operator?: string; operatorRole?: string }[]>([]);
  564. const trackingSteps = ref<{ time: string; title: string; detail?: string; type: string }[]>([]);
  565. const editAddress = reactive({ receiverName: '', receiverPhone: '', receiverCountry: '', receiverState: '', receiverCity: '', receiverPostalCode: '', receiverAddress: '' });
  566. const statusLabel = (s: string) => {
  567. const map: Record<string, string> = { created: '待支付', paid: '已支付', allocated: '已分配', shipped: '已发货', delivered: '已签收', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
  568. return map[s] || s;
  569. };
  570. const buyerLevelType = (level: string) => {
  571. const map: Record<string, string> = { 'VIP': 'danger', '黄金': 'warning', '普通': 'info' };
  572. return map[level] || 'info';
  573. };
  574. const refundStatusType = (status: string) => {
  575. const map: Record<string, string> = { '无退款': 'info', '部分退款': 'warning', '已全额退款': 'danger' };
  576. return map[status] || 'info';
  577. };
  578. const totalProfit = computed(() => {
  579. if (!order.items || order.items.length === 0) return '0.00';
  580. return order.items.reduce((sum, item) => sum + (parseFloat(item.profit) || 0), 0).toFixed(2);
  581. });
  582. const totalProfitRate = computed(() => {
  583. if (!order.items || order.items.length === 0) return '0';
  584. const totalAmount = order.items.reduce((sum, item) => sum + (parseFloat(item.subtotal) || 0), 0);
  585. const totalCost = order.items.reduce((sum, item) => sum + (parseFloat(item.costPrice) * item.qty || 0), 0);
  586. if (totalCost === 0) return '0';
  587. return ((parseFloat(totalProfit.value) / totalCost) * 100).toFixed(1);
  588. });
  589. const loadData = async () => {
  590. const id = route.query.id as string;
  591. if (!id) return;
  592. Object.assign(order, generateMockOrderDetail(id));
  593. };
  594. const generateMockOrderDetail = (id: string): Partial<OrderItem> => {
  595. const orderNo = `OMS-20260419-0012`;
  596. const items: OrderProductItem[] = [
  597. {
  598. id: 'item-1',
  599. productId: 'prod-001',
  600. skuId: 'skuid-001',
  601. sku: 'SKU-LUGG-20-BLK',
  602. productTitle: 'TravelFlex Expandable Carry-On 20寸 行李箱 / Black 黑色',
  603. productImage: '',
  604. categoryId: 'cat-001',
  605. categoryName: '行李箱',
  606. specs: [
  607. { specName: '尺寸', specValue: '20寸' },
  608. { specName: '颜色', specValue: '黑色' }
  609. ],
  610. barcode: `BC${Date.now()}001`,
  611. qty: 1,
  612. price: '89.00',
  613. costPrice: '45.00',
  614. profit: '44.00',
  615. profitRate: 49,
  616. subtotal: '89.00',
  617. weight: 3.5,
  618. available: 45,
  619. locked: 5,
  620. inTransit: 20,
  621. reservedQty: 0,
  622. shippedQty: 0,
  623. returnedQty: 0,
  624. giftFlag: false
  625. },
  626. {
  627. id: 'item-2',
  628. productId: 'prod-002',
  629. skuId: 'skuid-002',
  630. sku: 'SKU-TAG-SET-GRY',
  631. productTitle: 'Travel Tag Set 旅行标签牌套装 / Gray 灰色',
  632. productImage: '',
  633. categoryId: 'cat-002',
  634. categoryName: '旅行配件',
  635. specs: [
  636. { specName: '颜色', specValue: '灰色' }
  637. ],
  638. barcode: `BC${Date.now()}002`,
  639. qty: 2,
  640. price: '19.50',
  641. costPrice: '8.00',
  642. profit: '23.00',
  643. profitRate: 59,
  644. subtotal: '39.00',
  645. weight: 0.2,
  646. available: 120,
  647. locked: 10,
  648. inTransit: 50,
  649. reservedQty: 0,
  650. shippedQty: 0,
  651. returnedQty: 0,
  652. giftFlag: false
  653. }
  654. ];
  655. const amount = items.reduce((sum, item) => sum + parseFloat(item.subtotal), 0);
  656. return {
  657. id,
  658. orderNo,
  659. channelOrderNo: `CH${Date.now()}`,
  660. channel: 'Shopify US',
  661. orderStatus: 'paid',
  662. shippingStatus: 'unshipped',
  663. paymentStatus: 'paid',
  664. exceptionTag: '正常',
  665. priority: 'normal',
  666. createdAt: '2026-04-19 20:42:00',
  667. updatedAt: '2026-04-19 21:16:00',
  668. paidAt: '2026-04-19 20:43:00',
  669. shippedAt: '',
  670. deliveredAt: '',
  671. buyerId: 'buyer-1001',
  672. buyer: 'Olivia Zhang',
  673. buyerEmail: 'olivia.zhang@example.com',
  674. buyerPhone: '+1 213-555-4401',
  675. buyerCountry: 'US',
  676. buyerLevel: 'VIP',
  677. buyerTags: ['高价值'],
  678. buyerRegisterTime: '2025-06-15',
  679. buyerOrderCount: 12,
  680. buyerTotalSpent: '$1,234.56',
  681. receiverName: 'Olivia Zhang',
  682. receiverPhone: '+1 213-555-4401',
  683. receiverCountry: 'US',
  684. receiverState: 'California',
  685. receiverCity: 'Los Angeles',
  686. receiverDistrict: 'Downtown',
  687. receiverPostalCode: '90001',
  688. receiverAddress: '1234 Main St, Apt 5B, Los Angeles, CA 90001, United States',
  689. receiverAddressLang: '',
  690. latitude: 34.0522,
  691. longitude: -118.2437,
  692. transactionId: `TXN${Date.now()}`,
  693. paymentMethod: 'Visa (****4242)',
  694. paymentTime: '2026-04-19 20:43:00',
  695. currency: 'USD',
  696. exchangeRate: 1,
  697. orderAmount: amount.toFixed(2),
  698. orderAmountCNY: (amount * 7.2).toFixed(2),
  699. taxAmount: (amount * 0.08).toFixed(2),
  700. shippingFee: '0.00',
  701. discountAmount: '5.00',
  702. couponCode: 'SAVE10',
  703. couponDiscount: '5.00',
  704. actualPaid: (amount - 5).toFixed(2),
  705. refundAmount: '0.00',
  706. refundStatus: '无退款',
  707. shippingMethod: '标快',
  708. carrier: '顺丰速运',
  709. trackingNo: '',
  710. trackingUrl: '',
  711. warehouse: '深圳南山仓',
  712. warehouseLocation: 'A-12-C',
  713. estimatedDelivery: '2026-04-25',
  714. weight: 3.7,
  715. length: 55,
  716. width: 35,
  717. height: 25,
  718. utmSource: 'google',
  719. utmMedium: 'cpc',
  720. utmCampaign: 'Spring Sale 2026',
  721. utmContent: 'Product luggage',
  722. utmTerm: 'carry on luggage',
  723. referrerUrl: 'https://www.google.com/search?q=carry+on+luggage',
  724. landingPage: '/product/luggage-001',
  725. ip: '192.168.1.100',
  726. ipCountry: 'US',
  727. device: 'iPhone',
  728. browser: 'Safari',
  729. os: 'iOS',
  730. parentOrderId: '',
  731. childOrderIds: [],
  732. mergeOrderId: '',
  733. relatedOrderId: '',
  734. originalOrderId: '',
  735. handler: '陈欣',
  736. handlerGroup: '运营组A',
  737. orderTags: ['VIP客户'],
  738. internalRemark: '地址楼栋不清晰,需要客服复核后再提交仓库',
  739. buyerRemark: 'Please deliver to door',
  740. itemCount: items.reduce((sum, item) => sum + item.qty, 0),
  741. amount: amount.toFixed(2),
  742. items,
  743. timeline: [],
  744. logs: []
  745. };
  746. };
  747. const updateSplitQty = (row: OrderProductItem) => {
  748. row.mainQty = row.mainQty || 0;
  749. };
  750. const lockInventory = () => { ElMessage.success('库存已锁定'); };
  751. const confirmCancel = async () => {
  752. if (!cancelReason.value) { ElMessage.warning('请选择取消原因'); return; }
  753. await ElMessageBox.confirm('确认取消此订单?操作不可撤销。', '二次确认', { type: 'warning' });
  754. await api.updateOrder(order.id!, { orderStatus: 'cancelled' } as Partial<OrderItem>);
  755. order.orderStatus = 'cancelled';
  756. operationLogs.value.unshift({ time: '刚刚', title: '订单已取消', summary: cancelReason.value, type: 'danger', operator: '客服', operatorRole: '客服组' });
  757. cancelDialog.value = false;
  758. ElMessage.success('订单已取消');
  759. };
  760. const confirmSplit = () => {
  761. const hasSplit = order.items?.some(i => (i.mainQty || 0) < i.qty);
  762. if (!hasSplit) { ElMessage.warning('请至少设置一个 SKU 的主单数量小于原数量'); return; }
  763. splitDialog.value = false;
  764. operationLogs.value.unshift({ time: '刚刚', title: '拆单方案已生成', summary: '待仓库确认后生成新发货单', type: 'primary', operator: '客服', operatorRole: '客服组' });
  765. ElMessage.success('拆单方案已生成');
  766. };
  767. const confirmMerge = () => {
  768. if (!mergeTarget.value) { ElMessage.warning('请输入目标订单号'); return; }
  769. mergeDialog.value = false;
  770. ElMessage.success(`已提交与 ${mergeTarget.value} 的合单申请`);
  771. };
  772. const submitWarehouse = async () => {
  773. await api.updateOrder(order.id!, {
  774. warehouse: order.warehouse,
  775. warehouseLocation: order.warehouseLocation,
  776. orderStatus: 'allocated'
  777. } as Partial<OrderItem>);
  778. for (const item of order.items || []) {
  779. if (item.skuId) {
  780. const inventoryRes = await api.getInventory();
  781. const invItem = inventoryRes.items.find((inv: any) => inv.skuId === item.skuId);
  782. if (invItem) {
  783. const newAvailable = Math.max(0, (invItem.available || 0) - item.qty);
  784. const newLocked = (invItem.locked || 0) + item.qty;
  785. await api.updateInventory(invItem.id, {
  786. locked: newLocked,
  787. available: newAvailable
  788. });
  789. await api.createInventoryLog({
  790. source: '订单分配锁定',
  791. relatedOrder: order.orderNo,
  792. operator: '客服',
  793. quantity: -item.qty,
  794. afterQty: newAvailable,
  795. time: new Date().toISOString()
  796. });
  797. }
  798. }
  799. }
  800. order.orderStatus = 'allocated';
  801. assignDrawer.value = false;
  802. operationLogs.value.unshift({ time: '刚刚', title: '已提交仓库', summary: `分配至 ${order.warehouse} (${order.warehouseLocation}),库存已锁定`, type: 'success', operator: '客服', operatorRole: '客服组' });
  803. ElMessage.success(`已提交仓库,订单状态已更新为已分配,${order.items?.length || 0} 个 SKU 库存已锁定`);
  804. };
  805. const confirmEditAddress = () => {
  806. if (!editAddress.receiverName || !editAddress.receiverPhone || !editAddress.receiverAddress) {
  807. ElMessage.warning('请填写完整地址信息');
  808. return;
  809. }
  810. Object.assign(order, {
  811. receiverName: editAddress.receiverName,
  812. receiverPhone: editAddress.receiverPhone,
  813. receiverCountry: editAddress.receiverCountry,
  814. receiverState: editAddress.receiverState,
  815. receiverCity: editAddress.receiverCity,
  816. receiverPostalCode: editAddress.receiverPostalCode,
  817. receiverAddress: editAddress.receiverAddress
  818. });
  819. editAddressDialog.value = false;
  820. operationLogs.value.unshift({ time: '刚刚', title: '收货地址已修改', summary: `${editAddress.receiverName}, ${editAddress.receiverCity}`, type: 'warning', operator: '客服', operatorRole: '客服组' });
  821. ElMessage.success('地址已修改');
  822. };
  823. const saveRemark = () => {
  824. ElMessage.success('备注已保存');
  825. };
  826. const handlePrint = () => { ElMessage.success(`正在生成 ${order.orderNo} 的面单...`); };
  827. const handlePrintInvoice = () => { ElMessage.success(`正在生成 ${order.orderNo} 的发货单...`); };
  828. const handleContact = () => { ElMessage.info(`正在打开与 ${order.buyer} 的对话窗口...`); };
  829. const handleRefund = () => { ElMessage.success('正在发起退款流程...'); };
  830. const handleCall = () => { ElMessage.info(`正在拨打 ${order.receiverPhone}...`); };
  831. const copyTrackingNo = () => {
  832. if (order.trackingNo) {
  833. navigator.clipboard.writeText(order.trackingNo);
  834. ElMessage.success('复制成功');
  835. }
  836. };
  837. const openTrackingUrl = () => { ElMessage.info('正在打开物流追踪页面...'); };
  838. const trackMore = () => { ElMessage.info('正在加载完整物流信息...'); };
  839. const handleActionCommand = (command: string) => {
  840. switch (command) {
  841. case 'assign': assignDrawer.value = true; break;
  842. case 'lock': lockInventory(); break;
  843. case 'split': splitDialog.value = true; break;
  844. case 'merge': mergeDialog.value = true; break;
  845. case 'address': editAddressDialog.value = true; break;
  846. }
  847. };
  848. const handlePrintCommand = (command: string) => {
  849. if (command === 'label') {
  850. handlePrint();
  851. } else if (command === 'invoice') {
  852. handlePrintInvoice();
  853. }
  854. };
  855. const handleRefundCommand = (command: string) => {
  856. if (command === 'refund') {
  857. handleRefund();
  858. } else if (command === 'viewRefund') {
  859. ElMessage.info('正在打开退款记录...');
  860. }
  861. };
  862. onMounted(() => {
  863. loadData();
  864. operationLogs.value = [
  865. { time: '2026-04-19 21:16', title: '地址校验告警', type: 'warning', operator: '系统', operatorRole: '自动' },
  866. { time: '2026-04-19 21:10', title: '库存锁定完成', type: 'success', operator: '系统', operatorRole: '自动' },
  867. { time: '2026-04-19 20:43', title: '支付确认', type: 'primary', operator: '支付网关', operatorRole: '自动' },
  868. { time: '2026-04-19 20:42', title: '订单接入 OMS', type: 'primary', operator: '渠道同步', operatorRole: '自动' }
  869. ];
  870. });
  871. </script>
  872. <style scoped>
  873. .order-detail-header {
  874. display: flex;
  875. justify-content: space-between;
  876. align-items: flex-start;
  877. padding-bottom: 20px;
  878. border-bottom: 1px solid #f0f0f0;
  879. }
  880. .header-left { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
  881. .header-left h2 { margin: 0; font-size: 22px; display: flex; align-items: center; gap: 12px; }
  882. .order-no { color: var(--cb-primary); font-size: 18px; }
  883. .header-actions { display: flex; gap: 10px; }
  884. .stat-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
  885. .stat-card { display: flex; align-items: center; gap: 16px; padding: 18px 16px; background: #fafafa; border-radius: 12px; transition: all 0.2s; }
  886. .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
  887. .stat-card__icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; color: #fff; flex-shrink: 0; }
  888. .stat-card__content { flex: 1; min-width: 0; }
  889. .stat-card__label { font-size: 13px; color: #888; margin-bottom: 4px; }
  890. .stat-card__value { font-size: 17px; font-weight: 600; color: #333; }
  891. .detail-grid { display: grid; grid-template-columns: 1fr 360px; gap: 20px; margin-top: 20px; }
  892. .detail-left, .detail-right { display: flex; flex-direction: column; gap: 20px; }
  893. .glass-card { padding: 20px; }
  894. .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }
  895. .section-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #333; }
  896. .value-bold { font-weight: 600; }
  897. .address-text { line-height: 1.7; color: #555; }
  898. .tracking-info__main { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding: 12px 16px; background: #f8f9fa; border-radius: 8px; }
  899. .tracking-no { font-family: 'Monaco', 'Menlo', monospace; font-size: 15px; font-weight: 600; letter-spacing: 0.5px; }
  900. .tracking-meta { display: flex; gap: 20px; font-size: 14px; color: #666; margin-bottom: 20px; }
  901. .track-timeline { padding: 0 8px; }
  902. .track-title { margin: 0; font-weight: 600; }
  903. .track-detail { margin: 6px 0 0; color: #999; font-size: 13px; }
  904. .tracking-empty { text-align: center; padding: 32px 0; }
  905. .action-menu { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
  906. .action-menu .el-dropdown { display: flex; }
  907. .action-menu .el-button { display: flex; align-items: center; gap: 6px; }
  908. :deep(.el-dropdown-menu__item) {
  909. display: flex;
  910. align-items: center;
  911. gap: 10px;
  912. padding: 10px 16px;
  913. font-size: 14px;
  914. }
  915. :deep(.el-dropdown-menu__item .el-icon) {
  916. font-size: 16px;
  917. }
  918. .log-title { margin: 0; font-weight: 600; }
  919. .log-detail { margin: 6px 0 0; color: #999; font-size: 13px; }
  920. .product-info { display: flex; gap: 14px; align-items: flex-start; }
  921. .product-image-placeholder { width: 56px; height: 56px; background: #f5f5f5; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #999; flex-shrink: 0; }
  922. .product-detail { display: flex; flex-direction: column; gap: 5px; }
  923. .product-title { font-weight: 600; font-size: 14px; line-height: 1.4; }
  924. .product-category { font-size: 12px; color: #999; }
  925. .sku-code, .barcode { font-family: 'Monaco', 'Menlo', monospace; font-size: 12px; }
  926. .specs-list { display: flex; flex-wrap: wrap; gap: 5px; }
  927. .qty { font-weight: 700; font-size: 17px; }
  928. .qty-detail { display: flex; gap: 10px; font-size: 11px; color: #999; margin-top: 4px; }
  929. .cost-price { color: #999; }
  930. .profit { color: #67c23a; font-weight: 600; }
  931. .profit-rate { font-size: 11px; color: #67c23a; }
  932. .subtotal { font-weight: 700; color: var(--cb-accent); }
  933. .inventory-info { display: flex; flex-direction: column; gap: 3px; font-size: 12px; }
  934. .inv-available { color: #67c23a; }
  935. .inv-locked { color: #e6a23c; }
  936. .inv-transit { color: #409eff; }
  937. .amount-summary { margin-top: 20px; padding: 20px; background: #fafafa; border-radius: 10px; display: flex; flex-direction: column; gap: 10px; align-items: flex-end; }
  938. .summary-row { display: flex; gap: 48px; font-size: 14px; width: 100%; justify-content: flex-end; }
  939. .summary-row span:first-child { color: #666; min-width: 90px; text-align: right; }
  940. .summary-row span:last-child { min-width: 110px; text-align: right; }
  941. .summary-row--total { font-size: 17px; font-weight: 600; padding-top: 12px; border-top: 1px solid #eee; }
  942. .total-amount { color: var(--cb-accent); font-size: 20px; }
  943. .summary-row--profit { color: #67c23a; }
  944. .profit-amount { font-weight: 600; }
  945. .profit-rate { color: #67c23a; }
  946. .discount { color: #67c23a; }
  947. .glass-card.section-card { padding: 24px; }
  948. .header-section { padding: 24px; }
  949. .header-top { display: flex; gap: 10px; margin-bottom: 12px; }
  950. .header-tags { display: flex; gap: 8px; flex-wrap: wrap; }
  951. .buyer-remark { color: #888; font-style: italic; }
  952. .discount-tag { color: #67c23a; font-weight: 500; }
  953. .actual-paid { color: var(--cb-accent); font-size: 16px; }
  954. .products-section { margin-top: 20px; }
  955. .package-section { margin-top: 20px; }
  956. .save-btn { margin-top: 12px; }
  957. </style>