index.vue 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632
  1. <template>
  2. <div class="page-container">
  3. <div class="page-head">
  4. <h1>客户结算管理</h1>
  5. <p class="page-desc">按区域管理待结算订单与结算单记录,支持结算、查看和编辑结算单。</p>
  6. </div>
  7. <div class="region-tabs region-tab-strip">
  8. <div class="tab-button-group region-tab-group">
  9. <el-button
  10. v-for="r in regionList"
  11. :key="r.id"
  12. class="region-tab-btn"
  13. :type="activeRegionId === r.id ? 'primary' : 'default'"
  14. @click="selectRegion(r.id)"
  15. >
  16. {{ r.name }}
  17. </el-button>
  18. </div>
  19. </div>
  20. <el-tabs v-model="activeTab" class="settlement-page-tabs" @tab-change="onTabChange">
  21. <el-tab-pane label="待结算订单" name="pending">
  22. <section class="panel">
  23. <div class="panel-top">
  24. <h2>
  25. <el-icon class="title-ic"><Setting /></el-icon>
  26. <span>待结算订单</span>
  27. </h2>
  28. </div>
  29. <div class="stats-row stats-row--compact">
  30. <el-card shadow="never" class="stat-card"
  31. ><div class="stat-title">可结算金额(元)(已完结·按售价)</div>
  32. <div class="stat-value">¥{{ formatAmount(summary.pendingAmount) }}</div></el-card
  33. >
  34. <el-card shadow="never" class="stat-card"
  35. ><div class="stat-title">当前页·可结算售价(元)</div>
  36. <div class="stat-value">¥{{ formatAmount(summary.currentPageAmount) }}</div></el-card
  37. >
  38. <el-card shadow="never" class="stat-card"
  39. ><div class="stat-title">已选订单数</div>
  40. <div class="stat-value">{{ selectedCount }}</div></el-card
  41. >
  42. <el-card shadow="never" class="stat-card"
  43. ><div class="stat-title">已选结算金额(元)</div>
  44. <div class="stat-value amount">¥{{ formatAmount(selectedAmount) }}</div></el-card
  45. >
  46. </div>
  47. <el-card shadow="never" class="pending-main-card">
  48. <div class="filter-block">
  49. <el-form :model="searchForm" class="filter-form" label-position="top">
  50. <div class="filter-grid">
  51. <el-form-item label="客户">
  52. <el-select v-model="searchForm.clientId" filterable clearable placeholder="请选择客户">
  53. <el-option
  54. v-for="u in clientOptions"
  55. :key="u.id"
  56. :label="u.realName || u.username"
  57. :value="u.id"
  58. />
  59. </el-select>
  60. </el-form-item>
  61. <el-form-item label="订单号">
  62. <el-input v-model="searchForm.keyword" clearable placeholder="请输入订单号" />
  63. </el-form-item>
  64. <el-form-item label="日期区间">
  65. <el-date-picker
  66. v-model="dateRange"
  67. type="daterange"
  68. value-format="YYYY-MM-DDTHH:mm:ss"
  69. range-separator="至"
  70. start-placeholder="开始日期"
  71. end-placeholder="结束日期"
  72. />
  73. </el-form-item>
  74. </div>
  75. <div class="filter-actions-row">
  76. <div class="filter-actions-end">
  77. <el-button class="btn-ghost" @click="handleReset">重置</el-button>
  78. <el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
  79. </div>
  80. </div>
  81. </el-form>
  82. </div>
  83. <div class="toolbar">
  84. <span class="list-toolbar-selected" aria-live="polite"
  85. >已选<span class="sel-num">{{ selectedCount }}</span
  86. >项</span
  87. >
  88. <el-button class="btn-ghost" @click="handleSelectAllFiltered">全选可结算订单(已完结·跨分页)</el-button>
  89. <el-button class="btn-main" :disabled="!selectedCount" @click="openSettlementDialog"
  90. >结算</el-button
  91. >
  92. <el-tag v-if="selectionMode === 'allFiltered'" type="warning"
  93. >已启用跨分页全选,可取消少量订单</el-tag
  94. >
  95. </div>
  96. <el-table
  97. ref="tableRef"
  98. class="client-settlement-table"
  99. :data="tableData"
  100. row-key="id"
  101. stripe
  102. v-loading="loading"
  103. max-height="calc(100vh - 440px)"
  104. @selection-change="handleSelectionChange"
  105. @expand-change="handleRowExpand"
  106. >
  107. <el-table-column type="expand" width="48">
  108. <template #default="{ row }">
  109. <div class="order-row-expand-wrap">
  110. <div
  111. v-if="getExpandedItems(row.id).length"
  112. class="settlement-row-expand-table-wrap sqxp-subtable-wrap"
  113. >
  114. <el-table
  115. :data="getExpandedItems(row.id)"
  116. stripe
  117. size="small"
  118. class="order-row-expand-table sqxp-subtable"
  119. @row-dblclick="openOrderProductView"
  120. >
  121. <el-table-column prop="productName" label="商品" min-width="220">
  122. <template #default="{ row: line }">
  123. <div class="expand-product-cell">
  124. <div class="expand-product-thumb-wrap">
  125. <el-image
  126. v-if="line.imageUrl"
  127. :src="line.imageUrl"
  128. fit="cover"
  129. class="expand-product-thumb"
  130. />
  131. <span v-else class="expand-product-thumb placeholder">无图</span>
  132. </div>
  133. <span class="expand-product-name">{{ line.productName || '-' }}</span>
  134. </div>
  135. </template>
  136. </el-table-column>
  137. <el-table-column label="计量" width="88" align="center">
  138. <template #default="{ row: line }">{{ formatMeasureCell(line.measureValue) }}</template>
  139. </el-table-column>
  140. <el-table-column prop="unitName" label="单位" width="70" align="center" />
  141. <el-table-column prop="quantity" label="数量" width="80" align="right" />
  142. <el-table-column label="供货单价(元)" min-width="110" align="right">
  143. <template #default="{ row: line }">¥{{ formatAmount(line.supplierUnitPrice) }}</template>
  144. </el-table-column>
  145. <el-table-column label="供货金额(元)" min-width="110" align="right">
  146. <template #default="{ row: line }">¥{{ formatAmount(line.supplierAmount) }}</template>
  147. </el-table-column>
  148. <el-table-column label="售价单价(元)" min-width="110" align="right">
  149. <template #default="{ row: line }">¥{{ formatAmount(line.sellingUnitPrice) }}</template>
  150. </el-table-column>
  151. <el-table-column label="售出金额(元)" min-width="110" align="right">
  152. <template #default="{ row: line }">¥{{ formatAmount(line.sellingAmount) }}</template>
  153. </el-table-column>
  154. </el-table>
  155. <div class="order-row-expand-summary sqxp-subtable-summary">
  156. <span
  157. >供货总金额(元):<span class="sqxp-subtable-summary__amount"
  158. >¥{{ formatAmount(row.supplierReferenceTotal) }}</span
  159. ></span
  160. >
  161. <span
  162. >售出总金额(元):<span class="sqxp-subtable-summary__amount"
  163. >¥{{ formatAmount(row.settlementAmount) }}</span
  164. ></span
  165. >
  166. </div>
  167. </div>
  168. <el-empty v-else description="暂无商品明细" :image-size="72" />
  169. </div>
  170. </template>
  171. </el-table-column>
  172. <el-table-column type="selection" width="48" :selectable="rowSelectable" />
  173. <el-table-column label="订单编号" min-width="180" show-overflow-tooltip>
  174. <template #default="{ row }">
  175. <el-button link type="primary" @click="toggleRowExpand(row)">{{ row.orderNo }}</el-button>
  176. </template>
  177. </el-table-column>
  178. <el-table-column prop="clientName" label="客户名称" min-width="150" show-overflow-tooltip />
  179. <el-table-column prop="regionName" label="区域" min-width="120" show-overflow-tooltip />
  180. <el-table-column prop="supplierReferenceTotal" label="供货总金额(元)" min-width="140" align="right">
  181. <template #default="{ row }">
  182. <span class="amount-text">¥{{ formatAmount(row.supplierReferenceTotal) }}</span>
  183. </template>
  184. </el-table-column>
  185. <el-table-column prop="totalAmount" label="订单金额(元)" min-width="130" align="right">
  186. <template #default="{ row }">
  187. <span class="amount-text">¥{{ formatAmount(orderTotalAmount(row)) }}</span>
  188. </template>
  189. </el-table-column>
  190. <el-table-column label="订单状态" min-width="120">
  191. <template #default="{ row }">
  192. <el-tag :type="orderStatusTagType(resolveOrderStatus(row))" size="small">
  193. {{ orderStatusLabel(resolveOrderStatus(row)) }}
  194. </el-tag>
  195. </template>
  196. </el-table-column>
  197. <el-table-column label="配送状态" min-width="120">
  198. <template #default="{ row }">
  199. <el-tag :type="deliveryStatusTagType(resolveDeliveryStatus(row))" size="small">
  200. {{ deliveryStatusLabel(resolveDeliveryStatus(row)) }}
  201. </el-tag>
  202. </template>
  203. </el-table-column>
  204. <el-table-column label="支付状态" min-width="120">
  205. <template #default="{ row }">
  206. <el-tag :type="payStatusTagType(resolvePayStatus(row))" size="small">
  207. {{ payStatusLabel(resolvePayStatus(row)) }}
  208. </el-tag>
  209. </template>
  210. </el-table-column>
  211. <el-table-column label="支付方式" min-width="110" align="center">
  212. <template #default="{ row }">
  213. <el-tag :type="resolvePaymentMethod(row) === 'offline' ? 'warning' : 'primary'" size="small">
  214. {{ paymentMethodLabel(resolvePaymentMethod(row)) }}
  215. </el-tag>
  216. </template>
  217. </el-table-column>
  218. <el-table-column prop="createAt" label="下单时间" min-width="170" />
  219. <el-table-column prop="clientSettlementStatus" label="客户结算" width="100">
  220. <template #default="{ row }">
  221. <el-tag :type="row.clientSettlementStatus === 'settled' ? 'success' : 'warning'" size="small">
  222. {{ row.clientSettlementStatus === 'settled' ? '已结算' : '待结算' }}
  223. </el-tag>
  224. </template>
  225. </el-table-column>
  226. </el-table>
  227. <div class="pagination list-pager-bar">
  228. <el-pagination
  229. :current-page="pagination.page"
  230. :page-size="pagination.pageSize"
  231. :page-sizes="[10, 20, 50, 100]"
  232. :total="pagination.total"
  233. layout="total, sizes, prev, pager, next, jumper"
  234. @size-change="
  235. (val) => {
  236. pagination.pageSize = val
  237. loadData()
  238. }
  239. "
  240. @current-change="
  241. (val) => {
  242. pagination.page = val
  243. loadData()
  244. }
  245. "
  246. />
  247. </div>
  248. </el-card>
  249. </section>
  250. </el-tab-pane>
  251. <el-tab-pane label="结算单记录" name="sheets">
  252. <section class="panel">
  253. <div class="panel-top">
  254. <h2>
  255. <el-icon class="title-ic"><Setting /></el-icon>
  256. <span>结算单记录</span>
  257. </h2>
  258. </div>
  259. <el-card shadow="never" class="sheet-main-card" v-loading="sheetLoading">
  260. <div class="filter-block">
  261. <el-form :model="sheetSearchForm" class="filter-form" label-position="top">
  262. <div class="filter-grid">
  263. <el-form-item label="客户">
  264. <el-select v-model="sheetSearchForm.clientId" filterable clearable placeholder="全部">
  265. <el-option
  266. v-for="u in clientOptions"
  267. :key="u.id"
  268. :label="u.realName || u.username"
  269. :value="u.id"
  270. />
  271. </el-select>
  272. </el-form-item>
  273. <el-form-item label="结算单号">
  274. <el-input v-model="sheetSearchForm.settlementNo" clearable placeholder="模糊查询" />
  275. </el-form-item>
  276. <el-form-item label="结算时间">
  277. <el-date-picker
  278. v-model="sheetDateRange"
  279. type="daterange"
  280. value-format="YYYY-MM-DDTHH:mm:ss"
  281. range-separator="至"
  282. start-placeholder="开始"
  283. end-placeholder="结束"
  284. />
  285. </el-form-item>
  286. </div>
  287. <div class="filter-actions-row">
  288. <div class="filter-actions-end">
  289. <el-button class="btn-ghost" @click="handleSheetReset">重置</el-button>
  290. <el-button class="btn-main" type="primary" @click="handleSheetSearch">搜索</el-button>
  291. </div>
  292. </div>
  293. </el-form>
  294. </div>
  295. <el-table
  296. class="client-settlement-sheet-table"
  297. :data="sheetTableData"
  298. stripe
  299. @expand-change="handleSheetRowExpand"
  300. >
  301. <el-table-column type="expand" width="48">
  302. <template #default="{ row }">
  303. <div class="sheet-row-expand-wrap" v-loading="isSheetRowLoading(row.id)">
  304. <el-table
  305. v-if="getSheetOrders(row.id).length"
  306. :data="getSheetOrders(row.id)"
  307. row-key="id"
  308. stripe
  309. size="small"
  310. class="sheet-orders-table"
  311. @expand-change="handleSheetOrderExpand(row.id)"
  312. >
  313. <el-table-column type="expand" width="48">
  314. <template #default="{ row: ord }">
  315. <div class="order-row-expand-wrap">
  316. <div
  317. v-if="Array.isArray(ord.children) && ord.children.length"
  318. class="settlement-row-expand-table-wrap sqxp-subtable-wrap"
  319. >
  320. <el-table
  321. :data="ord.children"
  322. stripe
  323. size="small"
  324. class="order-row-expand-table sqxp-subtable"
  325. @row-dblclick="openOrderProductView"
  326. >
  327. <el-table-column prop="productName" label="商品" min-width="220">
  328. <template #default="{ row: line }">
  329. <div class="expand-product-cell">
  330. <div class="expand-product-thumb-wrap">
  331. <el-image
  332. v-if="line.imageUrl"
  333. :src="line.imageUrl"
  334. fit="cover"
  335. class="expand-product-thumb"
  336. />
  337. <span v-else class="expand-product-thumb placeholder">无图</span>
  338. </div>
  339. <span class="expand-product-name">{{ line.productName || '-' }}</span>
  340. </div>
  341. </template>
  342. </el-table-column>
  343. <el-table-column label="计量" width="88" align="center">
  344. <template #default="{ row: line }">{{
  345. formatMeasureCell(line.measureValue)
  346. }}</template>
  347. </el-table-column>
  348. <el-table-column prop="unitName" label="单位" width="70" align="center" />
  349. <el-table-column prop="quantity" label="数量" width="80" align="right" />
  350. <el-table-column label="供货单价(元)" min-width="110" align="right">
  351. <template #default="{ row: line }"
  352. >¥{{ formatAmount(line.supplierUnitPrice) }}</template
  353. >
  354. </el-table-column>
  355. <el-table-column label="供货金额(元)" min-width="110" align="right">
  356. <template #default="{ row: line }"
  357. >¥{{ formatAmount(line.supplierAmount) }}</template
  358. >
  359. </el-table-column>
  360. <el-table-column label="售价单价(元)" min-width="110" align="right">
  361. <template #default="{ row: line }"
  362. >¥{{ formatAmount(line.sellingUnitPrice) }}</template
  363. >
  364. </el-table-column>
  365. <el-table-column label="售出金额(元)" min-width="110" align="right">
  366. <template #default="{ row: line }"
  367. >¥{{ formatAmount(line.sellingAmount) }}</template
  368. >
  369. </el-table-column>
  370. </el-table>
  371. <div class="order-row-expand-summary sqxp-subtable-summary">
  372. <span
  373. >供货总金额(元):<span class="sqxp-subtable-summary__amount"
  374. >¥{{ formatAmount(sheetOrderSupplierTotal(ord)) }}</span
  375. ></span
  376. >
  377. <span
  378. >售出总金额(元):<span class="sqxp-subtable-summary__amount"
  379. >¥{{ formatAmount(sheetOrderSellingTotal(ord)) }}</span
  380. ></span
  381. >
  382. </div>
  383. </div>
  384. <el-empty v-else description="暂无商品明细" :image-size="72" />
  385. </div>
  386. </template>
  387. </el-table-column>
  388. <el-table-column label="订单编号" min-width="180" show-overflow-tooltip>
  389. <template #default="{ row: ord }">
  390. <strong>{{ ord.orderNo }}</strong>
  391. </template>
  392. </el-table-column>
  393. <el-table-column prop="clientName" label="客户名称" min-width="150" show-overflow-tooltip />
  394. <el-table-column prop="regionName" label="区域" min-width="110" show-overflow-tooltip />
  395. <el-table-column label="订单金额(元)" min-width="130" align="right">
  396. <template #default="{ row: ord }">
  397. <span class="amount-text">¥{{ formatAmount(orderTotalAmount(ord)) }}</span>
  398. </template>
  399. </el-table-column>
  400. <el-table-column label="供货总金额(元)" min-width="140" align="right">
  401. <template #default="{ row: ord }">
  402. <span class="amount-text">¥{{ formatAmount(sheetOrderSupplierTotal(ord)) }}</span>
  403. </template>
  404. </el-table-column>
  405. <el-table-column label="售出总金额(元)" min-width="140" align="right">
  406. <template #default="{ row: ord }">
  407. <span class="amount-text">¥{{ formatAmount(sheetOrderSellingTotal(ord)) }}</span>
  408. </template>
  409. </el-table-column>
  410. <el-table-column label="应收金额(元)" min-width="130" align="right">
  411. <template #default="{ row: ord }">
  412. <span class="amount-text">¥{{ formatAmount(orderReceivableAmount(ord)) }}</span>
  413. </template>
  414. </el-table-column>
  415. <el-table-column label="订单状态" min-width="120">
  416. <template #default="{ row: ord }">
  417. <el-tag :type="orderStatusTagType(resolveOrderStatus(ord))" size="small">
  418. {{ orderStatusLabel(resolveOrderStatus(ord)) }}
  419. </el-tag>
  420. </template>
  421. </el-table-column>
  422. <el-table-column label="配送状态" min-width="120">
  423. <template #default="{ row: ord }">
  424. <el-tag :type="deliveryStatusTagType(resolveDeliveryStatus(ord))" size="small">
  425. {{ deliveryStatusLabel(resolveDeliveryStatus(ord)) }}
  426. </el-tag>
  427. </template>
  428. </el-table-column>
  429. <el-table-column label="支付状态" min-width="120">
  430. <template #default="{ row: ord }">
  431. <el-tag :type="payStatusTagType(resolvePayStatus(ord))" size="small">
  432. {{ payStatusLabel(resolvePayStatus(ord)) }}
  433. </el-tag>
  434. </template>
  435. </el-table-column>
  436. <el-table-column label="支付方式" min-width="110" align="center">
  437. <template #default="{ row: ord }">
  438. <el-tag
  439. :type="resolvePaymentMethod(ord) === 'offline' ? 'warning' : 'primary'"
  440. size="small"
  441. >
  442. {{ paymentMethodLabel(resolvePaymentMethod(ord)) }}
  443. </el-tag>
  444. </template>
  445. </el-table-column>
  446. <el-table-column prop="orderCreateAt" label="下单时间" min-width="170" />
  447. </el-table>
  448. <el-empty v-else description="暂无明细" :image-size="72" />
  449. </div>
  450. </template>
  451. </el-table-column>
  452. <el-table-column prop="settlementNo" label="结算单号" min-width="200" />
  453. <el-table-column prop="clientName" label="客户" min-width="120" />
  454. <el-table-column prop="orderCount" label="订单数" width="90" align="center" />
  455. <el-table-column prop="totalAmount" label="结算金额(元)" min-width="120" align="right">
  456. <template #default="{ row }">
  457. <span class="amount-text">¥{{ formatAmount(row.totalAmount) }}</span>
  458. </template>
  459. </el-table-column>
  460. <el-table-column prop="settlementAt" label="结算时间" min-width="170" />
  461. <el-table-column prop="createName" label="操作人" width="100" />
  462. <el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
  463. <template #default="{ row }">
  464. <el-button class="btn-op-item primary" @click="openSheetDetail(row.id)">详情</el-button>
  465. <el-button class="btn-op-item" @click="openSheetEdit(row)">编辑</el-button>
  466. </template>
  467. </el-table-column>
  468. </el-table>
  469. <div class="pagination list-pager-bar">
  470. <el-pagination
  471. :current-page="sheetPagination.page"
  472. :page-size="sheetPagination.pageSize"
  473. :page-sizes="[10, 20, 50]"
  474. :total="sheetPagination.total"
  475. layout="total, sizes, prev, pager, next, jumper"
  476. @size-change="
  477. (val) => {
  478. sheetPagination.pageSize = val
  479. loadSheetList()
  480. }
  481. "
  482. @current-change="
  483. (val) => {
  484. sheetPagination.page = val
  485. loadSheetList()
  486. }
  487. "
  488. />
  489. </div>
  490. </el-card>
  491. </section>
  492. </el-tab-pane>
  493. </el-tabs>
  494. <el-dialog
  495. v-model="settlementDialogVisible"
  496. title="确认客户结算"
  497. width="720px"
  498. destroy-on-close
  499. @closed="resetSettlementDialog"
  500. class="client-settlement-dialog settlement-confirm-dialog"
  501. modal-class="client-settlement-dialog-mask"
  502. >
  503. <el-alert type="info" :closable="false" class="mb-3">
  504. 已选 {{ selectedCount }} 笔订单,合计
  505. <strong>¥{{ formatAmount(selectedAmount) }}</strong>
  506. </el-alert>
  507. <VoucherMediaUploader v-model="settlementVoucherUrls" business-type="client_settlement" />
  508. <el-form label-width="100px" class="settlement-remark-form">
  509. <el-form-item label="结算备注">
  510. <el-input
  511. v-model="settlementRemark"
  512. type="textarea"
  513. :rows="3"
  514. maxlength="500"
  515. show-word-limit
  516. placeholder="选填"
  517. />
  518. </el-form-item>
  519. </el-form>
  520. <template #footer>
  521. <el-button class="btn-ghost" @click="settlementDialogVisible = false">取消</el-button>
  522. <el-button class="btn-main" :loading="submitLoading" @click="confirmSettlementSubmit"
  523. >确认结算</el-button
  524. >
  525. </template>
  526. </el-dialog>
  527. <el-dialog
  528. v-model="sheetDetailVisible"
  529. title="结算单详情"
  530. width="1240px"
  531. destroy-on-close
  532. class="client-settlement-dialog sheet-detail-dialog"
  533. modal-class="client-settlement-dialog-mask"
  534. >
  535. <SettlementSheetDetailPanel
  536. v-if="sheetDetail"
  537. :detail="sheetDetail"
  538. counterparty-label="客户"
  539. :counterparty-name="sheetDetail.clientName"
  540. />
  541. </el-dialog>
  542. <el-dialog
  543. v-model="sheetEditVisible"
  544. title="编辑结算单"
  545. width="720px"
  546. destroy-on-close
  547. @closed="resetSheetEdit"
  548. class="client-settlement-dialog sheet-edit-dialog"
  549. modal-class="client-settlement-dialog-mask"
  550. >
  551. <div class="dialog-scroll sheet-edit-dialog-scroll">
  552. <div class="form-section">
  553. <h4>结算信息</h4>
  554. <el-form label-position="top">
  555. <div class="form-grid-2">
  556. <el-form-item label="结算备注" class="form-col-span-2">
  557. <el-input
  558. v-model="sheetEditForm.remark"
  559. type="textarea"
  560. :rows="4"
  561. maxlength="500"
  562. show-word-limit
  563. placeholder="请输入结算备注(选填)"
  564. />
  565. </el-form-item>
  566. </div>
  567. </el-form>
  568. </div>
  569. <div class="form-section">
  570. <h4>结算凭证</h4>
  571. <VoucherMediaUploader
  572. v-model="sheetEditForm.voucherUrls"
  573. business-type="client_settlement"
  574. :business-id="sheetEditId || 'temp'"
  575. />
  576. </div>
  577. </div>
  578. <template #footer>
  579. <el-button class="btn-ghost" @click="sheetEditVisible = false">取消</el-button>
  580. <el-button class="btn-main" :loading="sheetEditSaving" @click="saveSheetEdit">保存</el-button>
  581. </template>
  582. </el-dialog>
  583. <ProductViewDialog v-model="productViewDialogVisible" :product-id="productViewTargetId" />
  584. </div>
  585. </template>
  586. <script setup lang="ts">
  587. import { computed, nextTick, onMounted, reactive, ref } from 'vue'
  588. import { ElMessage, ElMessageBox } from 'element-plus'
  589. import { Setting } from '@element-plus/icons-vue'
  590. import {
  591. fetchCreateClientSettlement,
  592. fetchClientSettlementOrderList,
  593. fetchClientSettlementSheetList,
  594. fetchClientSettlementSheetDetail,
  595. fetchUpdateClientSettlementSheet
  596. } from '@/api/business'
  597. import { fetchUserList, fetchActiveRegions } from '@/api/admin'
  598. import { useUserStore } from '@/store/modules/user'
  599. import ProductViewDialog from '@/components/business/product-view-dialog/index.vue'
  600. import VoucherMediaUploader from '@/components/settlement/VoucherMediaUploader.vue'
  601. import SettlementSheetDetailPanel from '@/components/settlement/SettlementSheetDetailPanel.vue'
  602. defineOptions({ name: 'ClientSettlement' })
  603. const userStore = useUserStore()
  604. const regionList = ref<any[]>([])
  605. const activeRegionId = ref('')
  606. const activeTab = ref<'pending' | 'sheets'>('pending')
  607. const loading = ref(false)
  608. const submitLoading = ref(false)
  609. const tableRef = ref()
  610. /** 程序化勾选/清空时跳过 selection-change,避免跨分页全选逻辑被误改 */
  611. const skipSelectionSync = ref(false)
  612. const tableData = ref<any[]>([])
  613. const expandedItemsMap = ref<Record<string, any[]>>({})
  614. const productViewDialogVisible = ref(false)
  615. const productViewTargetId = ref('')
  616. const clientOptions = ref<any[]>([])
  617. const currentPageIds = ref<string[]>([])
  618. const selectionMode = ref<'ids' | 'allFiltered'>('ids')
  619. const selectedIds = ref<Set<string>>(new Set())
  620. const excludedIds = ref<Set<string>>(new Set())
  621. const amountMap = ref<Record<string, number>>({})
  622. const settlementDialogVisible = ref(false)
  623. const settlementVoucherUrls = ref<string[]>([])
  624. const settlementRemark = ref('')
  625. const dateRange = ref<string[]>([])
  626. const searchForm = reactive({
  627. clientId: '',
  628. keyword: ''
  629. })
  630. const pagination = reactive({
  631. page: 1,
  632. pageSize: 20,
  633. total: 0
  634. })
  635. const summary = reactive({
  636. pendingAmount: 0,
  637. currentPageAmount: 0,
  638. settleableOrderCount: 0
  639. })
  640. const sheetLoading = ref(false)
  641. const sheetTableData = ref<any[]>([])
  642. const sheetSearchForm = reactive({
  643. clientId: '',
  644. settlementNo: ''
  645. })
  646. const sheetDateRange = ref<string[]>([])
  647. const sheetPagination = reactive({
  648. page: 1,
  649. pageSize: 20,
  650. total: 0
  651. })
  652. const sheetDetailVisible = ref(false)
  653. const sheetDetail = ref<any>(null)
  654. const sheetExpandedTreeMap = ref<Record<string, any[]>>({})
  655. const sheetRowLoadingMap = ref<Record<string, boolean>>({})
  656. const sheetEditVisible = ref(false)
  657. const sheetEditSaving = ref(false)
  658. const sheetEditId = ref('')
  659. const sheetEditForm = reactive({
  660. remark: '',
  661. voucherUrls: [] as string[]
  662. })
  663. const selectedCount = computed(() => {
  664. if (selectionMode.value === 'allFiltered')
  665. return Math.max(0, summary.settleableOrderCount - excludedIds.value.size)
  666. return selectedIds.value.size
  667. })
  668. const selectedAmount = computed(() => {
  669. if (selectionMode.value === 'allFiltered') {
  670. const excludedAmount = Array.from(excludedIds.value).reduce(
  671. (acc, id) => acc + (amountMap.value[id] || 0),
  672. 0
  673. )
  674. return Math.max(0, summary.pendingAmount - excludedAmount)
  675. }
  676. return Array.from(selectedIds.value).reduce((acc, id) => acc + (amountMap.value[id] || 0), 0)
  677. })
  678. const formatAmount = (val: number) => (Number(val || 0) || 0).toFixed(2)
  679. const formatMeasureCell = (v: unknown) => {
  680. if (v == null || v === '') return '-'
  681. const n = Number(v)
  682. if (Number.isNaN(n)) return '-'
  683. return n.toFixed(3).replace(/\.?0+$/, '')
  684. }
  685. const formatQty = (v: any) => (v == null || v === '' ? '—' : String(v))
  686. const formatSheetMeasure = (row: any) => {
  687. const u = row.unitName || ''
  688. const m = row.measureValue
  689. if (m != null && m !== '') return `${m} · ${u || '—'}`
  690. return u || '—'
  691. }
  692. const sheetOrderSupplierTotal = (orderRow: any) => {
  693. const children = Array.isArray(orderRow?.children) ? orderRow.children : []
  694. return children.reduce((acc: number, ln: any) => acc + Number(ln?.supplierAmount || 0), 0)
  695. }
  696. const sheetOrderSellingTotal = (orderRow: any) => {
  697. const children = Array.isArray(orderRow?.children) ? orderRow.children : []
  698. return children.reduce((acc: number, ln: any) => acc + Number(ln?.sellingAmount || 0), 0)
  699. }
  700. const getSheetOrders = (sheetId: string) => {
  701. const items = sheetExpandedTreeMap.value[sheetId] || []
  702. return items.filter((x: any) => x && x.rowType === 'order')
  703. }
  704. const handleSheetOrderExpand = (_sheetId: string) => (_row: any, _expandedRows: any[]) => {
  705. // 明细数据已在结算单详情接口一次性带回;此处仅占位以统一交互(可后续做只允许展开一行等优化)
  706. }
  707. const getSheetTreeItems = (sheetId: string) => sheetExpandedTreeMap.value[sheetId] || []
  708. const isSheetRowLoading = (sheetId: string) => !!sheetRowLoadingMap.value[sheetId]
  709. const handleSheetRowExpand = async (row: any, expandedRows: any[]) => {
  710. const expanded = expandedRows.some((r) => r.id === row.id)
  711. if (!expanded) return
  712. if (sheetExpandedTreeMap.value[row.id]) return
  713. sheetRowLoadingMap.value = { ...sheetRowLoadingMap.value, [row.id]: true }
  714. try {
  715. const detail: any = await fetchClientSettlementSheetDetail(row.id)
  716. sheetExpandedTreeMap.value = { ...sheetExpandedTreeMap.value, [row.id]: detail?.items || [] }
  717. } finally {
  718. sheetRowLoadingMap.value = { ...sheetRowLoadingMap.value, [row.id]: false }
  719. }
  720. }
  721. const ORDER_STATUS_MAP: Record<string, string> = {
  722. pending_payment: '待支付',
  723. pending_review_cancel: '待审核取消',
  724. pending_delivery: '待分配配送',
  725. delivering: '配送中',
  726. partial_delivered: '部分送达',
  727. pending_confirm: '待确认收货',
  728. completed: '已完结',
  729. cancelled: '已取消'
  730. }
  731. const orderStatusLabel = (s: string) => (s ? ORDER_STATUS_MAP[s] || s : '—')
  732. const resolveOrderStatus = (row: any) => {
  733. if (row?.cancelStatus === 'pending_review') return 'pending_review_cancel'
  734. return row?.orderStatus || row?.status || ''
  735. }
  736. const orderStatusTagType = (s: string) => {
  737. if (s === 'completed') return 'success'
  738. if (s === 'cancelled') return 'info'
  739. if (s === 'pending_payment' || s === 'partial_delivered') return 'warning'
  740. return 'primary'
  741. }
  742. const DELIVERY_STATUS_MAP: Record<string, string> = {
  743. pending_dispatch: '待分配配送',
  744. pending_delivery: '待配送',
  745. delivering: '配送中',
  746. partial_delivered: '部分送达',
  747. delivered: '配送完成',
  748. pending: '待取货',
  749. picking: '取货中',
  750. completed: '已完成'
  751. }
  752. const resolveDeliveryStatus = (row: any) =>
  753. row?.deliveryStatus ||
  754. (['pending_payment', 'pending_dispatch', 'pending_delivery', 'pending_review_cancel'].includes(
  755. resolveOrderStatus(row)
  756. )
  757. ? 'pending_dispatch'
  758. : resolveOrderStatus(row) === 'delivering'
  759. ? 'delivering'
  760. : resolveOrderStatus(row) === 'partial_delivered'
  761. ? 'partial_delivered'
  762. : ['pending_confirm', 'completed'].includes(resolveOrderStatus(row))
  763. ? 'delivered'
  764. : 'pending_dispatch')
  765. const deliveryStatusLabel = (s: string) => (s ? DELIVERY_STATUS_MAP[s] || s : '—')
  766. const deliveryStatusTagType = (s: string) => {
  767. if (s === 'delivered' || s === 'completed') return 'success'
  768. if (s === 'delivering') return 'info'
  769. if (s === 'partial_delivered' || s === 'pending_dispatch' || s === 'pending_delivery') return 'warning'
  770. if (s === 'picking') return 'primary'
  771. return 'info'
  772. }
  773. const resolvePayStatus = (row: any) =>
  774. row?.payStatus || (Number(row?.paidAmount || 0) > 0 ? 'paid' : 'pending_payment')
  775. const payStatusLabel = (s: string) => (s === 'paid' ? '已支付' : s === 'pending_payment' ? '待支付' : s || '—')
  776. const payStatusTagType = (s: string) => (s === 'paid' ? 'success' : s === 'pending_payment' ? 'warning' : 'info')
  777. const resolvePaymentMethod = (row: any) => String(row?.paymentMethod || '').toLowerCase()
  778. const paymentMethodLabel = (m: string) => {
  779. if (['online', 'wechat', 'wx'].includes(m)) return '线上支付'
  780. if (m === 'offline') return '线下支付'
  781. return m || '-'
  782. }
  783. const orderTotalAmount = (row: any) =>
  784. Number(row?.totalAmount ?? row?.sellingTotalAmount ?? row?.settlementAmount ?? 0)
  785. const orderReceivableAmount = (row: any) => Number(row?.receivableAmount ?? orderTotalAmount(row))
  786. const rowSelectable = (row: any) => row.rowType === 'order' && !!row.settlementSelectable
  787. const getExpandedItems = (orderId: string) => expandedItemsMap.value[orderId] || []
  788. const handleRowExpand = (row: any, expandedRows: any[]) => {
  789. const expanded = expandedRows.some((r) => r.id === row.id)
  790. if (!expanded) return
  791. expandedItemsMap.value = {
  792. ...expandedItemsMap.value,
  793. [row.id]: Array.isArray(row.children) ? row.children : []
  794. }
  795. }
  796. const toggleRowExpand = (row: any) => {
  797. tableRef.value?.toggleRowExpansion?.(row)
  798. if (!expandedItemsMap.value[row.id]) {
  799. expandedItemsMap.value = {
  800. ...expandedItemsMap.value,
  801. [row.id]: Array.isArray(row.children) ? row.children : []
  802. }
  803. }
  804. }
  805. const openOrderProductView = (line: any) => {
  806. const pid = line?.productId
  807. if (!pid) return
  808. productViewTargetId.value = String(pid)
  809. productViewDialogVisible.value = true
  810. }
  811. const buildFilters = () => ({
  812. regionId: activeRegionId.value || undefined,
  813. clientId: searchForm.clientId || undefined,
  814. keyword: searchForm.keyword || undefined,
  815. startDate: dateRange.value?.[0] || undefined,
  816. endDate: dateRange.value?.[1] || undefined
  817. })
  818. const loadRegions = async () => {
  819. const res: any = await fetchActiveRegions()
  820. const all = res || []
  821. const bindId = userStore.getUserInfo?.regionId
  822. if (bindId) {
  823. regionList.value = all.filter((r: any) => r.id === bindId)
  824. if (!regionList.value.length && userStore.getUserInfo?.regionName) {
  825. regionList.value = [{ id: bindId, name: userStore.getUserInfo.regionName }]
  826. }
  827. } else {
  828. regionList.value = all
  829. }
  830. if (!activeRegionId.value && regionList.value.length > 0) {
  831. activeRegionId.value = regionList.value[0].id
  832. }
  833. }
  834. const selectRegion = async (id: string) => {
  835. if (!id || activeRegionId.value === id) return
  836. activeRegionId.value = id
  837. selectionMode.value = 'ids'
  838. selectedIds.value = new Set()
  839. excludedIds.value = new Set()
  840. pagination.page = 1
  841. sheetPagination.page = 1
  842. await loadClientOptions()
  843. if (activeTab.value === 'pending') {
  844. await loadData()
  845. } else {
  846. await loadSheetList()
  847. }
  848. await nextTick()
  849. tableRef.value?.clearSelection?.()
  850. }
  851. const loadClientOptions = async () => {
  852. const pageSize = 100
  853. const maxPages = 100
  854. const all: any[] = []
  855. let page = 1
  856. let total = 0
  857. while (page <= maxPages) {
  858. const res: any = await fetchUserList({
  859. page,
  860. pageSize,
  861. role: 'client',
  862. isActive: true,
  863. regionId: activeRegionId.value || undefined
  864. })
  865. const list = res?.list || []
  866. total = Number(res?.total || 0)
  867. all.push(...list)
  868. if (list.length < pageSize || all.length >= total) break
  869. page++
  870. }
  871. clientOptions.value = all
  872. }
  873. const loadData = async () => {
  874. loading.value = true
  875. try {
  876. const res: any = await fetchClientSettlementOrderList({
  877. page: pagination.page,
  878. pageSize: pagination.pageSize,
  879. ...buildFilters()
  880. })
  881. tableData.value = res?.list || []
  882. expandedItemsMap.value = {}
  883. pagination.total = res?.total || 0
  884. summary.pendingAmount = Number(res?.summary?.pendingAmount || 0)
  885. summary.currentPageAmount = Number(res?.summary?.currentPageAmount || 0)
  886. summary.settleableOrderCount = Number(res?.summary?.settleableOrderCount || 0)
  887. currentPageIds.value = tableData.value
  888. .filter((x: any) => x.rowType === 'order')
  889. .map((x: any) => x.id)
  890. tableData.value.forEach((x: any) => {
  891. if (x.rowType === 'order') {
  892. amountMap.value[x.id] = Number(x.settlementAmount ?? 0)
  893. }
  894. })
  895. await nextTick()
  896. skipSelectionSync.value = true
  897. tableRef.value?.clearSelection?.()
  898. await nextTick()
  899. if (selectionMode.value === 'ids') {
  900. tableData.value.forEach((row: any) => {
  901. if (row.rowType === 'order' && selectedIds.value.has(row.id)) {
  902. tableRef.value?.toggleRowSelection?.(row, true)
  903. }
  904. })
  905. } else {
  906. tableData.value.forEach((row: any) => {
  907. if (row.rowType === 'order' && !excludedIds.value.has(row.id)) {
  908. tableRef.value?.toggleRowSelection?.(row, true)
  909. }
  910. })
  911. }
  912. await nextTick()
  913. skipSelectionSync.value = false
  914. } finally {
  915. loading.value = false
  916. }
  917. }
  918. const loadSheetList = async () => {
  919. sheetLoading.value = true
  920. try {
  921. const res: any = await fetchClientSettlementSheetList({
  922. page: sheetPagination.page,
  923. pageSize: sheetPagination.pageSize,
  924. regionId: activeRegionId.value || undefined,
  925. clientId: sheetSearchForm.clientId || undefined,
  926. settlementNo: sheetSearchForm.settlementNo || undefined,
  927. startDate: sheetDateRange.value?.[0] || undefined,
  928. endDate: sheetDateRange.value?.[1] || undefined
  929. })
  930. sheetTableData.value = res?.list || []
  931. sheetExpandedTreeMap.value = {}
  932. sheetRowLoadingMap.value = {}
  933. sheetPagination.total = res?.total || 0
  934. } finally {
  935. sheetLoading.value = false
  936. }
  937. }
  938. const onTabChange = (name: string | number) => {
  939. if (name === 'sheets') loadSheetList()
  940. }
  941. const handleSearch = () => {
  942. pagination.page = 1
  943. selectionMode.value = 'ids'
  944. selectedIds.value = new Set()
  945. excludedIds.value = new Set()
  946. loadData()
  947. }
  948. const handleReset = () => {
  949. searchForm.clientId = ''
  950. searchForm.keyword = ''
  951. dateRange.value = []
  952. handleSearch()
  953. }
  954. const handleSheetSearch = () => {
  955. sheetPagination.page = 1
  956. loadSheetList()
  957. }
  958. const handleSheetReset = () => {
  959. sheetSearchForm.clientId = ''
  960. sheetSearchForm.settlementNo = ''
  961. sheetDateRange.value = []
  962. handleSheetSearch()
  963. }
  964. const handleSelectionChange = (rows: any[]) => {
  965. if (skipSelectionSync.value) return
  966. const orderRows = rows.filter((x: any) => x.rowType === 'order')
  967. const selectedOnPage = new Set(orderRows.map((x: any) => x.id))
  968. if (selectionMode.value === 'allFiltered') {
  969. currentPageIds.value.forEach((id) => {
  970. if (selectedOnPage.has(id)) excludedIds.value.delete(id)
  971. else excludedIds.value.add(id)
  972. })
  973. return
  974. }
  975. currentPageIds.value.forEach((id) => selectedIds.value.delete(id))
  976. selectedOnPage.forEach((id) => selectedIds.value.add(id))
  977. }
  978. const handleSelectAllFiltered = async () => {
  979. if (!summary.settleableOrderCount) {
  980. ElMessage.warning('当前检索条件下没有可结算的已完结订单')
  981. return
  982. }
  983. await ElMessageBox.confirm(
  984. `将全选当前检索条件下的全部已完结订单(共 ${summary.settleableOrderCount} 笔,可结算售价合计约 ¥${formatAmount(summary.pendingAmount)})。未完成订单不会参与结算。是否继续?`,
  985. '跨分页全选',
  986. { type: 'warning' }
  987. )
  988. selectionMode.value = 'allFiltered'
  989. selectedIds.value = new Set()
  990. excludedIds.value = new Set()
  991. ElMessage.success('已启用跨分页全选')
  992. await nextTick()
  993. skipSelectionSync.value = true
  994. tableRef.value?.clearSelection?.()
  995. await nextTick()
  996. tableData.value.forEach((row: any) => {
  997. if (row.rowType === 'order' && row.settlementSelectable && !excludedIds.value.has(row.id)) {
  998. tableRef.value?.toggleRowSelection?.(row, true)
  999. }
  1000. })
  1001. await nextTick()
  1002. skipSelectionSync.value = false
  1003. }
  1004. const openSettlementDialog = () => {
  1005. if (!selectedCount.value) {
  1006. ElMessage.warning('请先选择要结算的订单')
  1007. return
  1008. }
  1009. if (selectionMode.value === 'ids') {
  1010. const picked = tableData.value.filter(
  1011. (x: any) => x.rowType === 'order' && selectedIds.value.has(x.id)
  1012. )
  1013. const cids = [...new Set(picked.map((x: any) => x.clientId).filter(Boolean))]
  1014. if (cids.length > 1) {
  1015. ElMessage.warning('请选择同一客户的订单后再结算')
  1016. return
  1017. }
  1018. }
  1019. settlementVoucherUrls.value = []
  1020. settlementRemark.value = ''
  1021. settlementDialogVisible.value = true
  1022. }
  1023. const resetSettlementDialog = () => {
  1024. settlementVoucherUrls.value = []
  1025. settlementRemark.value = ''
  1026. }
  1027. const confirmSettlementSubmit = async () => {
  1028. if (!selectedCount.value) return ElMessage.warning('请先选择要结算的订单')
  1029. if (!settlementVoucherUrls.value.length) return ElMessage.warning('请先上传结算凭证')
  1030. submitLoading.value = true
  1031. try {
  1032. await fetchCreateClientSettlement({
  1033. selectionMode: selectionMode.value,
  1034. orderIds: Array.from(selectedIds.value),
  1035. filters: buildFilters(),
  1036. excludedOrderIds: Array.from(excludedIds.value),
  1037. voucherUrls: settlementVoucherUrls.value,
  1038. remark: settlementRemark.value || undefined
  1039. })
  1040. ElMessage.success('客户结算完成')
  1041. settlementDialogVisible.value = false
  1042. selectionMode.value = 'ids'
  1043. selectedIds.value = new Set()
  1044. excludedIds.value = new Set()
  1045. loadData()
  1046. if (activeTab.value === 'sheets') loadSheetList()
  1047. await nextTick()
  1048. tableRef.value?.clearSelection?.()
  1049. } finally {
  1050. submitLoading.value = false
  1051. }
  1052. }
  1053. const openSheetDetail = async (id: string) => {
  1054. const res: any = await fetchClientSettlementSheetDetail(id)
  1055. sheetDetail.value = res
  1056. sheetDetailVisible.value = true
  1057. }
  1058. const openSheetEdit = async (row: any) => {
  1059. sheetEditId.value = row.id
  1060. const res: any = await fetchClientSettlementSheetDetail(row.id)
  1061. sheetEditForm.remark = res?.remark || ''
  1062. sheetEditForm.voucherUrls = Array.isArray(res?.voucherUrls) ? [...res.voucherUrls] : []
  1063. sheetEditVisible.value = true
  1064. }
  1065. const resetSheetEdit = () => {
  1066. sheetEditId.value = ''
  1067. sheetEditForm.remark = ''
  1068. sheetEditForm.voucherUrls = []
  1069. }
  1070. const saveSheetEdit = async () => {
  1071. if (!sheetEditId.value) return
  1072. sheetEditSaving.value = true
  1073. try {
  1074. await fetchUpdateClientSettlementSheet(sheetEditId.value, {
  1075. remark: sheetEditForm.remark,
  1076. voucherUrls: sheetEditForm.voucherUrls
  1077. })
  1078. ElMessage.success('已保存')
  1079. sheetEditVisible.value = false
  1080. loadSheetList()
  1081. } finally {
  1082. sheetEditSaving.value = false
  1083. }
  1084. }
  1085. onMounted(async () => {
  1086. await loadRegions()
  1087. await loadClientOptions()
  1088. await loadData()
  1089. })
  1090. </script>
  1091. <style lang="scss" scoped>
  1092. .page-container {
  1093. display: flex;
  1094. flex-direction: column;
  1095. gap: 16px;
  1096. }
  1097. .settlement-page-tabs :deep(.el-tabs__header) {
  1098. margin-bottom: 12px;
  1099. }
  1100. .stats-row {
  1101. display: grid;
  1102. grid-template-columns: repeat(4, minmax(0, 1fr));
  1103. gap: 12px;
  1104. margin-bottom: 16px;
  1105. }
  1106. .stats-row--compact {
  1107. max-width: 980px;
  1108. margin-left: auto;
  1109. margin-right: auto;
  1110. }
  1111. .stats-row--compact .stat-card {
  1112. border: 0 !important;
  1113. }
  1114. .stat-title {
  1115. color: #64748b;
  1116. font-size: 13px;
  1117. font-weight: 600;
  1118. line-height: 1.35;
  1119. }
  1120. .stat-value {
  1121. margin-top: 6px;
  1122. font-size: 24px;
  1123. font-weight: 700;
  1124. line-height: 1.1;
  1125. }
  1126. .stat-value.amount {
  1127. color: #dc2626;
  1128. }
  1129. .pending-main-card,
  1130. .sheet-main-card,
  1131. .stat-card {
  1132. border: 1px solid var(--mint-border, #d9e8e2);
  1133. border-radius: 10px;
  1134. background: #fff;
  1135. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  1136. overflow: hidden;
  1137. }
  1138. /* 客户结算管理 - 结算单记录主卡片:按需求去掉外边框 */
  1139. .sheet-main-card {
  1140. border: 0 !important;
  1141. }
  1142. .pending-main-card :deep(.el-card__body),
  1143. .sheet-main-card :deep(.el-card__body) {
  1144. padding: 0;
  1145. }
  1146. .filter-block {
  1147. padding: 12px 16px 10px;
  1148. border-bottom: 1px solid #eef4f0;
  1149. background: #fff;
  1150. }
  1151. .filter-form :deep(.el-form-item) {
  1152. margin-bottom: 0;
  1153. }
  1154. .filter-form :deep(.el-form-item__label) {
  1155. font-size: 13px;
  1156. font-weight: 500;
  1157. color: var(--mint-text-muted, #64748b);
  1158. padding-bottom: 4px;
  1159. line-height: 1.2;
  1160. }
  1161. .filter-grid {
  1162. display: grid;
  1163. grid-template-columns: repeat(3, minmax(0, 1fr));
  1164. gap: 10px 14px;
  1165. }
  1166. .filter-actions-row {
  1167. display: flex;
  1168. justify-content: flex-end;
  1169. margin-top: 10px;
  1170. }
  1171. .filter-actions-end {
  1172. display: inline-flex;
  1173. align-items: center;
  1174. gap: 8px;
  1175. }
  1176. .toolbar {
  1177. display: flex;
  1178. align-items: center;
  1179. gap: 12px;
  1180. padding: 8px 16px;
  1181. margin-bottom: 0;
  1182. border-bottom: 1px solid var(--mint-border, #d9e8e2);
  1183. background: #f8fafc;
  1184. }
  1185. .amount-text {
  1186. color: #dc2626;
  1187. font-weight: 600;
  1188. }
  1189. .pagination {
  1190. display: flex;
  1191. justify-content: flex-end;
  1192. margin-top: 0;
  1193. padding: 10px 16px 12px;
  1194. border-top: 1px solid var(--mint-border, #d9e8e2);
  1195. background: #fff;
  1196. }
  1197. .order-row-expand-wrap {
  1198. padding: 0;
  1199. background: #f8fafc;
  1200. border-radius: 0;
  1201. }
  1202. .client-settlement-table :deep(td.el-table__expanded-cell),
  1203. .client-settlement-sheet-table :deep(td.el-table__expanded-cell),
  1204. .sheet-orders-table :deep(td.el-table__expanded-cell),
  1205. .client-settlement-table :deep(.el-table__body td.el-table__expanded-cell),
  1206. .client-settlement-sheet-table :deep(.el-table__body td.el-table__expanded-cell),
  1207. .sheet-orders-table :deep(.el-table__body td.el-table__expanded-cell) {
  1208. padding: 12px !important;
  1209. text-align: left !important;
  1210. vertical-align: top;
  1211. background: #f8fafc;
  1212. box-sizing: border-box;
  1213. overflow: hidden;
  1214. }
  1215. .client-settlement-table :deep(td.el-table__expanded-cell > .cell),
  1216. .client-settlement-sheet-table :deep(td.el-table__expanded-cell > .cell),
  1217. .sheet-orders-table :deep(td.el-table__expanded-cell > .cell) {
  1218. display: block !important;
  1219. padding: 0 !important;
  1220. text-align: left !important;
  1221. }
  1222. /* 第二层:结算单展开后的“订单列表”容器 */
  1223. .sheet-row-expand-wrap {
  1224. margin: 0;
  1225. border-radius: 10px;
  1226. max-width: 100%;
  1227. box-sizing: border-box;
  1228. overflow: hidden;
  1229. overflow-x: auto;
  1230. position: relative;
  1231. padding: 12px 12px 14px 18px;
  1232. background: #f8fafc;
  1233. }
  1234. .sheet-row-expand-wrap::after {
  1235. content: '';
  1236. position: absolute;
  1237. left: 0;
  1238. top: 0;
  1239. bottom: 0;
  1240. width: 4px;
  1241. background: #10b981;
  1242. z-index: 2;
  1243. pointer-events: none;
  1244. }
  1245. .sheet-row-expand-wrap :deep(.sheet-orders-table) {
  1246. border: 1px solid var(--mint-border, #d9e8e2);
  1247. border-radius: 8px;
  1248. overflow: hidden;
  1249. background: #fff;
  1250. position: relative;
  1251. z-index: 1;
  1252. }
  1253. .sheet-row-expand-wrap :deep(.sheet-orders-table th.el-table__cell) {
  1254. background: #e2e8f0 !important;
  1255. color: var(--mint-text-muted, #64748b);
  1256. font-size: 11px;
  1257. font-weight: 600;
  1258. }
  1259. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-table__cell) {
  1260. padding: 8px 10px;
  1261. }
  1262. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-table__inner-wrapper),
  1263. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-table__header-wrapper),
  1264. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-table__body-wrapper) {
  1265. width: max-content !important;
  1266. min-width: 100% !important;
  1267. }
  1268. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-table__body-wrapper),
  1269. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-scrollbar__wrap) {
  1270. overflow-x: auto !important;
  1271. }
  1272. .sheet-row-expand-wrap :deep(.sheet-orders-table.el-table),
  1273. .sheet-row-expand-wrap :deep(.sheet-orders-table .el-table) {
  1274. width: max-content !important;
  1275. min-width: 100% !important;
  1276. }
  1277. .settlement-row-expand-table-wrap {
  1278. display: block;
  1279. width: 100%;
  1280. max-width: 100%;
  1281. overflow: hidden;
  1282. border-radius: 0;
  1283. text-align: left;
  1284. position: relative;
  1285. background: #f8fafc;
  1286. padding: 12px 12px 14px 16px;
  1287. :deep(.el-table__inner-wrapper),
  1288. :deep(.el-table__header-wrapper),
  1289. :deep(.el-table__body-wrapper) {
  1290. width: 100%;
  1291. }
  1292. }
  1293. .settlement-row-expand-table-wrap::before {
  1294. content: '';
  1295. position: absolute;
  1296. left: 0;
  1297. top: 0;
  1298. bottom: 0;
  1299. width: 3px;
  1300. background: #10b981;
  1301. }
  1302. /* 第三层:孙列表琥珀层级 */
  1303. .client-settlement-sheet-table :deep(.sheet-orders-table .settlement-row-expand-table-wrap) {
  1304. margin-top: 4px;
  1305. padding-left: 14px;
  1306. padding-top: 10px;
  1307. padding-right: 0;
  1308. padding-bottom: 0;
  1309. border-left: 3px solid #f59e0b;
  1310. background: linear-gradient(90deg, rgba(254, 243, 199, 0.35) 0%, transparent 12px);
  1311. }
  1312. .client-settlement-sheet-table :deep(.sheet-orders-table .settlement-row-expand-table-wrap::before) {
  1313. background: #f59e0b;
  1314. }
  1315. .client-settlement-sheet-table :deep(.sheet-orders-table .order-row-expand-table) {
  1316. border-color: #fcd34d !important;
  1317. }
  1318. .client-settlement-sheet-table :deep(.sheet-orders-table .order-row-expand-table th.el-table__cell) {
  1319. background: #fef3c7 !important;
  1320. color: #92400e !important;
  1321. font-size: 10px !important;
  1322. border-bottom: 1px solid #fcd34d !important;
  1323. }
  1324. .client-settlement-sheet-table :deep(.sheet-orders-table .order-row-expand-table td.el-table__cell) {
  1325. font-size: 12px;
  1326. border-bottom: 1px solid #fef3c7 !important;
  1327. }
  1328. .client-settlement-sheet-table :deep(.sheet-orders-table .order-row-expand-table .el-table__cell) {
  1329. padding: 6px 8px !important;
  1330. }
  1331. .client-settlement-sheet-table :deep(.sheet-orders-table .order-row-expand-table tr:hover td.el-table__cell) {
  1332. background: #fffbeb !important;
  1333. }
  1334. .client-settlement-table :deep(td.el-table__cell.el-table__expand-column > .cell),
  1335. .client-settlement-sheet-table :deep(td.el-table__cell.el-table__expand-column > .cell),
  1336. .client-settlement-sheet-table :deep(.sheet-orders-table td.el-table__cell.el-table__expand-column > .cell) {
  1337. display: flex;
  1338. align-items: center;
  1339. justify-content: center;
  1340. font-size: 0;
  1341. line-height: 0;
  1342. overflow: hidden;
  1343. }
  1344. .client-settlement-table :deep(.el-table__expand-icon),
  1345. .client-settlement-sheet-table :deep(.el-table__expand-icon),
  1346. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon) {
  1347. width: 28px;
  1348. height: 28px;
  1349. border-radius: 6px;
  1350. border: 1px solid var(--mint-border, #d9e8e2);
  1351. display: inline-flex;
  1352. align-items: center;
  1353. justify-content: center;
  1354. transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
  1355. color: var(--mint-text-muted, #64748b);
  1356. background: var(--mint-surface, #fff);
  1357. font-size: 0;
  1358. line-height: 0;
  1359. }
  1360. .client-settlement-table :deep(.el-table__expand-icon:hover),
  1361. .client-settlement-sheet-table :deep(.el-table__expand-icon:hover),
  1362. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon:hover) {
  1363. background: var(--mint-primary-bg, #ecfdf5);
  1364. border-color: #10b981;
  1365. color: var(--mint-primary-dark, #059669);
  1366. }
  1367. .client-settlement-table :deep(.el-table__expand-icon--expanded),
  1368. .client-settlement-sheet-table :deep(.el-table__expand-icon--expanded),
  1369. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon--expanded) {
  1370. background: var(--mint-primary-bg, #ecfdf5);
  1371. border-color: #10b981;
  1372. color: var(--mint-primary-dark, #059669);
  1373. }
  1374. .client-settlement-table :deep(.el-table__expand-icon svg),
  1375. .client-settlement-sheet-table :deep(.el-table__expand-icon svg),
  1376. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon svg) {
  1377. transition: transform 0.15s ease;
  1378. width: 14px;
  1379. height: 14px;
  1380. }
  1381. .client-settlement-table :deep(.el-table__expand-icon--expanded svg),
  1382. .client-settlement-sheet-table :deep(.el-table__expand-icon--expanded svg),
  1383. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon--expanded svg) {
  1384. transform: rotate(180deg);
  1385. }
  1386. /* 二级子表 expand icon:强制与一级一致(避免 size="small" 影响) */
  1387. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon) {
  1388. width: 28px !important;
  1389. height: 28px !important;
  1390. min-width: 28px !important;
  1391. min-height: 28px !important;
  1392. border-radius: 6px !important;
  1393. }
  1394. .client-settlement-sheet-table :deep(.sheet-orders-table .el-table__expand-icon svg) {
  1395. width: 14px !important;
  1396. height: 14px !important;
  1397. }
  1398. /* 二/三级子表:商品缩略图尺寸对齐【商家结算管理】 */
  1399. .expand-product-cell {
  1400. display: inline-flex;
  1401. align-items: center;
  1402. gap: 10px;
  1403. max-width: 100%;
  1404. min-width: 0;
  1405. }
  1406. .expand-product-thumb-wrap {
  1407. flex: 0 0 auto;
  1408. }
  1409. .expand-product-thumb {
  1410. width: 34px;
  1411. height: 34px;
  1412. border-radius: 6px;
  1413. display: block;
  1414. }
  1415. .expand-product-thumb.placeholder {
  1416. width: 34px;
  1417. height: 34px;
  1418. border-radius: 6px;
  1419. display: flex;
  1420. align-items: center;
  1421. justify-content: center;
  1422. font-size: 11px;
  1423. color: #909399;
  1424. background: #f5f7fa;
  1425. border: 1px dashed #dcdfe6;
  1426. }
  1427. .expand-product-name {
  1428. min-width: 0;
  1429. overflow: hidden;
  1430. text-overflow: ellipsis;
  1431. white-space: nowrap;
  1432. }
  1433. </style>
  1434. <!-- Teleport 到 body:弹窗内容需非 scoped 才能稳定命中 -->
  1435. <style lang="scss">
  1436. .client-settlement-dialog.el-dialog {
  1437. display: flex;
  1438. flex-direction: column;
  1439. max-height: calc(100vh - 64px);
  1440. }
  1441. .client-settlement-dialog .el-dialog__header {
  1442. flex-shrink: 0;
  1443. background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 70%);
  1444. border-bottom: 1px solid var(--mint-border, #d9e8e2);
  1445. }
  1446. .client-settlement-dialog .el-dialog__body {
  1447. flex: 1 1 auto;
  1448. min-height: 0;
  1449. padding: 0 !important;
  1450. overflow: hidden;
  1451. }
  1452. .client-settlement-dialog .dialog-scroll {
  1453. overflow-y: auto;
  1454. overflow-x: hidden;
  1455. padding: 20px 22px;
  1456. flex: 1;
  1457. }
  1458. .client-settlement-dialog .el-dialog__footer {
  1459. padding: 12px 22px !important;
  1460. border-top: 1px solid var(--mint-border, #d9e8e2);
  1461. background: #f8fafc;
  1462. }
  1463. .client-settlement-dialog .el-dialog__footer .el-button {
  1464. height: 36px;
  1465. border-radius: 8px;
  1466. font-weight: 600;
  1467. display: inline-flex;
  1468. align-items: center;
  1469. justify-content: center;
  1470. }
  1471. .client-settlement-dialog-mask {
  1472. backdrop-filter: none !important;
  1473. }
  1474. .client-settlement-dialog.sheet-edit-dialog .form-section {
  1475. margin-bottom: 20px;
  1476. padding-bottom: 16px;
  1477. border-bottom: 1px dashed var(--mint-border, #d9e8e2);
  1478. }
  1479. .client-settlement-dialog.sheet-edit-dialog .form-section:last-of-type {
  1480. margin-bottom: 0;
  1481. padding-bottom: 0;
  1482. border-bottom: none;
  1483. }
  1484. .client-settlement-dialog.sheet-edit-dialog .form-section h4 {
  1485. margin: 0 0 12px;
  1486. font-size: 13px;
  1487. font-weight: 700;
  1488. color: var(--mint-text-muted, #64748b);
  1489. letter-spacing: 0.04em;
  1490. }
  1491. .client-settlement-dialog.sheet-edit-dialog .form-grid-2 {
  1492. display: grid;
  1493. grid-template-columns: 1fr 1fr;
  1494. gap: 12px 16px;
  1495. }
  1496. .client-settlement-dialog.sheet-edit-dialog .form-col-span-2 {
  1497. grid-column: 1 / -1;
  1498. }
  1499. .client-settlement-dialog.sheet-edit-dialog .el-form-item {
  1500. margin-bottom: 0;
  1501. }
  1502. .client-settlement-dialog.sheet-edit-dialog .el-form-item__label {
  1503. color: var(--mint-text-muted, #64748b) !important;
  1504. font-weight: 600;
  1505. font-size: 12px;
  1506. line-height: 1.2;
  1507. padding-bottom: 6px;
  1508. }
  1509. @media (max-width: 760px) {
  1510. .client-settlement-dialog.sheet-edit-dialog .form-grid-2 {
  1511. grid-template-columns: 1fr;
  1512. }
  1513. }
  1514. </style>