detailDrawer.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <template>
  2. <!-- 对账详情抽屉 -->
  3. <el-drawer v-model="drawer.visible" size="75%" direction="rtl" :before-close="handleDrawerClose" :close-on-click-modal="true">
  4. <template #header>
  5. <div class="drawer-header">
  6. <span class="order-title">对账详情</span>
  7. </div>
  8. </template>
  9. <div class="drawer-content">
  10. <el-form ref="formRef" :model="form" :rules="rules" label-width="135px">
  11. <!-- 基本信息 -->
  12. <el-divider content-position="left">
  13. <span style="color: #409eff; font-weight: 600">基本信息</span>
  14. </el-divider>
  15. <el-row :gutter="20">
  16. <el-col :span="8">
  17. <el-form-item label="对账单号:" prop="statementOrderNo">
  18. <el-input v-model="form.statementOrderNo" disabled />
  19. </el-form-item>
  20. </el-col>
  21. <el-col :span="8">
  22. <el-form-item label="对账状态:" prop="statementStatus">
  23. <dict-tag :options="statement_status" :value="form.statementStatus" />
  24. </el-form-item>
  25. </el-col>
  26. <el-col :span="8">
  27. <el-form-item label="客户名称:" prop="customerName">
  28. <el-input v-model="form.customerName" disabled />
  29. </el-form-item>
  30. </el-col>
  31. </el-row>
  32. <el-row :gutter="20">
  33. <el-col :span="8">
  34. <el-form-item label="对账单金额:" prop="amount">
  35. <el-input v-model="form.amount" disabled />
  36. </el-form-item>
  37. </el-col>
  38. <el-col :span="8">
  39. <el-form-item label="对账人:" prop="statementSelf">
  40. <el-input v-model="form.statementSelf" disabled />
  41. </el-form-item>
  42. </el-col>
  43. <el-col :span="8">
  44. <el-form-item label="对账人联系方式:" prop="statementSelfPhone">
  45. <el-input v-model="form.statementSelfPhone" placeholder="请填写对账人联系方式" disabled />
  46. </el-form-item>
  47. </el-col>
  48. </el-row>
  49. <el-row :gutter="20">
  50. <el-col :span="8">
  51. <el-form-item label="对账日期:" prop="statementDate">
  52. <el-date-picker
  53. v-model="form.statementDate"
  54. type="date"
  55. placeholder="请选择对账日期"
  56. style="width: 100%"
  57. value-format="YYYY-MM-DD"
  58. disabled
  59. />
  60. </el-form-item>
  61. </el-col>
  62. <el-col :span="8">
  63. <el-form-item label="支付状态:" prop="isPaymentStatus">
  64. <el-select v-model="form.isPaymentStatus" placeholder="请选择支付状态" clearable style="width: 100%" disabled>
  65. <el-option v-for="dict in payment_status" :key="dict.value" :label="dict.label" :value="dict.value" />
  66. </el-select>
  67. </el-form-item>
  68. </el-col>
  69. <el-col :span="8">
  70. <el-form-item label="发票状态:" prop="isInvoiceStatus">
  71. <el-select v-model="form.isInvoiceStatus" placeholder="请选择发票状态" clearable style="width: 100%" disabled>
  72. <el-option v-for="dict in invoice_issuance_status" :key="dict.value" :label="dict.label" :value="dict.value" />
  73. </el-select>
  74. </el-form-item>
  75. </el-col>
  76. </el-row>
  77. <el-row :gutter="20">
  78. <el-col :span="24">
  79. <el-form-item label="备注:" prop="remark">
  80. <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" maxlength="500" show-word-limit disabled />
  81. </el-form-item>
  82. </el-col>
  83. </el-row>
  84. <!-- 对账明细 -->
  85. <el-divider content-position="left">
  86. <span style="color: #409eff; font-weight: 600">对账明细</span>
  87. </el-divider>
  88. <el-table :data="form.detailList" border style="width: 100%; margin-bottom: 20px">
  89. <el-table-column type="index" label="序号" width="60" align="center" />
  90. <el-table-column prop="orderNo" label="订单编号" min-width="150" align="center" />
  91. <el-table-column prop="amount" label="金额" min-width="120" align="center" />
  92. <el-table-column prop="orderTime" label="下单日期" min-width="120" align="center" />
  93. <el-table-column prop="userName" label="下单人" min-width="100" align="center" />
  94. <el-table-column prop="userDept" label="部门" min-width="120" align="center" />
  95. </el-table>
  96. <!--
  97. <div v-if="form.detailList.length === 0" class="empty-data">
  98. <el-empty description="暂无数据" :image-size="100" />
  99. </div> -->
  100. <!-- 商品清单 -->
  101. <el-divider content-position="left">
  102. <span style="color: #409eff; font-weight: 600">商品清单</span>
  103. </el-divider>
  104. <el-table :data="pagedProductList" border style="width: 100%; margin-bottom: 20px">
  105. <el-table-column
  106. type="index"
  107. label="序号"
  108. width="60"
  109. align="center"
  110. :index="(index) => (productPage.pageNum - 1) * productPage.pageSize + index + 1"
  111. />
  112. <el-table-column prop="orderNo" label="订单编号" min-width="120" align="center" />
  113. <el-table-column prop="productNo" label="商品编号" min-width="120" align="center" />
  114. <el-table-column prop="itemName" label="商品名称" min-width="180" align="center" />
  115. <el-table-column prop="unitName" label="单位" width="80" align="center" />
  116. <el-table-column prop="quantity" label="数量" width="100" align="center" />
  117. <el-table-column prop="unitPrice" label="单价" width="100" align="center">
  118. <template #default="scope">
  119. {{ scope.row.unitPrice ? Number(scope.row.unitPrice).toFixed(2) : '0.00' }}
  120. </template>
  121. </el-table-column>
  122. <el-table-column label="小计" width="120" align="center">
  123. <template #default="scope">
  124. {{ (Number(scope.row.quantity || 0) * Number(scope.row.unitPrice || 0)).toFixed(2) }}
  125. </template>
  126. </el-table-column>
  127. </el-table>
  128. <!-- 分页 -->
  129. <el-pagination
  130. v-if="form.productList.length > 0"
  131. v-model:current-page="productPage.pageNum"
  132. v-model:page-size="productPage.pageSize"
  133. :page-sizes="[10, 20, 50, 100]"
  134. layout="total, sizes, prev, pager, next, jumpers"
  135. :total="productPage.total"
  136. @size-change="handleProductSizeChange"
  137. @current-change="handleProductCurrentChange"
  138. style="margin-top: 15px"
  139. />
  140. <!-- 对账附件 -->
  141. <el-divider content-position="left">
  142. <span style="color: #409eff; font-weight: 600">对账附件</span>
  143. </el-divider>
  144. <el-table :data="fileList" border style="width: 100%; margin-bottom: 20px">
  145. <el-table-column type="index" label="序号" width="80" align="center" />
  146. <el-table-column prop="name" label="文件名称" min-width="200" align="center" />
  147. <el-table-column prop="url" label="文件地址" min-width="250" align="center">
  148. <template #default="scope">
  149. {{ scope.row.url || '暂无地址' }}
  150. </template>
  151. </el-table-column>
  152. <el-table-column label="操作" width="160" align="center">
  153. <template #default="scope">
  154. <el-button type="primary" link @click="handleDownloadFile(scope.row)">下载</el-button>
  155. <el-button type="primary" link @click="handlePreviewFile(scope.row)">预览</el-button>
  156. </template>
  157. </el-table-column>
  158. </el-table>
  159. <!--
  160. <div v-if="fileList.length === 0" class="empty-data">
  161. <el-empty description="暂无附件" :image-size="100" />
  162. </div> -->
  163. </el-form>
  164. </div>
  165. <template #footer>
  166. <div class="drawer-footer">
  167. <el-button @click="handleDrawerClose(() => (drawer.visible = false))">取消</el-button>
  168. <el-button type="primary" :loading="buttonLoading" @click="handleDrawerClose(() => (drawer.visible = false))">提交</el-button>
  169. </div>
  170. </template>
  171. </el-drawer>
  172. </template>
  173. <script setup name="AddDrawer" lang="ts">
  174. import { addStatementOrder, updateStatementOrder } from '@/api/bill/statementOrder';
  175. import { StatementOrderForm } from '@/api/bill/statementOrder/types';
  176. import { CustomerInfoVO } from '@/api/customer/customerFile/customerInfo/types';
  177. import { listOrderMain, getOrderMain } from '@/api/order/orderMain';
  178. import { OrderMainVO, OrderMainQuery, OrderMainForm } from '@/api/order/orderMain/types';
  179. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  180. const { statement_status, invoice_issuance_status, payment_status } = toRefs<any>(
  181. proxy?.useDict('statement_status', 'statement_order_type', 'invoice_issuance_status', 'payment_status')
  182. );
  183. const initFormData: StatementOrderForm = {
  184. id: undefined,
  185. statementOrderNo: undefined,
  186. customerId: undefined,
  187. customerNo: undefined,
  188. customerName: undefined,
  189. amount: undefined,
  190. statementSelfId: undefined,
  191. statementSelf: undefined,
  192. statementSelfPhone: undefined,
  193. statementStatus: undefined,
  194. isPaymentStatus: undefined,
  195. isInvoiceStatus: undefined,
  196. statementDate: undefined,
  197. annexAddress: undefined,
  198. rejectRemark: undefined,
  199. remark: undefined,
  200. detailList: [],
  201. productList: []
  202. };
  203. const emit = defineEmits(['success']);
  204. const formRef = ref<any>();
  205. const buttonLoading = ref(false);
  206. const form = ref<
  207. StatementOrderForm & {
  208. actualAmount?: string;
  209. accountingVerification?: string;
  210. skuProductName?: string;
  211. skuProductModel?: string;
  212. attachmentRemark?: string;
  213. }
  214. >({ ...initFormData });
  215. const rules = reactive({
  216. customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
  217. statementSelfId: [{ required: true, message: '请选择对账人', trigger: 'blur' }],
  218. statementSelfPhone: [
  219. { required: true, message: '请输入对账人手机号', trigger: 'blur' },
  220. { pattern: /^1[3-9]\d{9}$/ as RegExp, message: '请输入正确的手机号格式', trigger: 'blur' }
  221. ]
  222. } as any);
  223. const fileList = ref<any[]>([]);
  224. const productPage = reactive({
  225. pageNum: 1,
  226. pageSize: 10,
  227. total: 0
  228. });
  229. const drawer = reactive<DialogOption>({
  230. visible: false,
  231. title: '对账详情'
  232. });
  233. const isEdit = ref(false); // 是否为编辑模式
  234. const customerOptions = ref<CustomerInfoVO[]>([]);
  235. const preloadedOrders = ref<OrderMainVO[]>([]); // 预加载的订单列表
  236. const preloadedTotal = ref(0); // 预加载的订单总数
  237. /** 计算当前页的商品列表 */
  238. const pagedProductList = computed(() => {
  239. const start = (productPage.pageNum - 1) * productPage.pageSize;
  240. const end = start + productPage.pageSize;
  241. return form.value.productList?.slice(start, end) || [];
  242. });
  243. /** 打开新增/编辑对账详情抽屉 */
  244. const open = (id?: string | number, data?: StatementOrderForm) => {
  245. reset();
  246. if (id && data) {
  247. // 查看模式
  248. drawer.title = '对账详情';
  249. Object.assign(form.value, data);
  250. // 将客户信息添加到下拉选项中,以便回显
  251. if (data.customerId && data.customerName) {
  252. customerOptions.value = [
  253. {
  254. id: data.customerId,
  255. customerName: data.customerName,
  256. customerNo: data.customerNo || ''
  257. } as CustomerInfoVO
  258. ];
  259. }
  260. // 解析附件地址并回显附件列表
  261. if (data.annexAddress) {
  262. const urls = data.annexAddress.split(',').filter((url) => url.trim());
  263. fileList.value = urls.map((url, index) => {
  264. const fileName = url.split('/').pop() || `附件${index + 1}`;
  265. return {
  266. name: fileName,
  267. url: url.trim(),
  268. id: undefined
  269. };
  270. });
  271. }
  272. // 设置商品列表分页总数
  273. if (data.productList && data.productList.length > 0) {
  274. productPage.total = data.productList.length;
  275. }
  276. }
  277. // 先打开抽屉
  278. drawer.visible = true;
  279. };
  280. /** 表单重置 */
  281. const reset = () => {
  282. form.value = { ...initFormData };
  283. fileList.value = [];
  284. productPage.pageNum = 1;
  285. productPage.pageSize = 10;
  286. productPage.total = 0;
  287. customerOptions.value = [];
  288. formRef.value?.clearValidate();
  289. };
  290. /** 预加载订单列表 */
  291. const preloadOrders = async (customerId: string | number) => {
  292. try {
  293. const params: OrderMainQuery = {
  294. pageNum: 1,
  295. pageSize: 10,
  296. customerId: customerId
  297. };
  298. const res = await listOrderMain(params);
  299. preloadedOrders.value = res.rows || [];
  300. preloadedTotal.value = res.total || 0;
  301. } catch (error) {
  302. console.error('预加载订单列表失败:', error);
  303. preloadedOrders.value = [];
  304. preloadedTotal.value = 0;
  305. }
  306. };
  307. /** 产品分页大小改变 */
  308. const handleProductSizeChange = (val: number) => {
  309. productPage.pageSize = val;
  310. };
  311. /** 产品分页页码改变 */
  312. const handleProductCurrentChange = (val: number) => {
  313. productPage.pageNum = val;
  314. };
  315. /** 下载文件 */
  316. const handleDownloadFile = async (file: any) => {
  317. if (!file.url) {
  318. proxy?.$modal.msgWarning('文件地址不存在');
  319. return;
  320. }
  321. try {
  322. // 使用fetch获取文件
  323. const response = await fetch(file.url);
  324. const blob = await response.blob();
  325. // 创建Blob URL
  326. const blobUrl = window.URL.createObjectURL(blob);
  327. // 创建下载链接
  328. const link = document.createElement('a');
  329. link.href = blobUrl;
  330. link.download = file.name || '附件';
  331. document.body.appendChild(link);
  332. link.click();
  333. // 清理
  334. document.body.removeChild(link);
  335. window.URL.revokeObjectURL(blobUrl);
  336. } catch (error) {
  337. console.error('下载失败:', error);
  338. // 如果fetch失败,回退到直接打开链接的方式
  339. const link = document.createElement('a');
  340. link.href = file.url;
  341. link.download = file.name || '附件';
  342. link.target = '_blank';
  343. document.body.appendChild(link);
  344. link.click();
  345. document.body.removeChild(link);
  346. }
  347. };
  348. /** 预览文件 */
  349. const handlePreviewFile = (file: any) => {
  350. if (!file.url) {
  351. proxy?.$modal.msgWarning('文件地址不存在');
  352. return;
  353. }
  354. // 在新窗口打开文件
  355. window.open(file.url, '_blank');
  356. };
  357. /** 提交表单 */
  358. const handleSubmit = async () => {
  359. if (!formRef.value) return;
  360. await formRef.value.validate(async (valid) => {
  361. if (valid) {
  362. buttonLoading.value = true;
  363. try {
  364. if (isEdit.value) {
  365. // 编辑模式
  366. await updateStatementOrder(form.value);
  367. proxy?.$modal.msgSuccess('修改成功');
  368. } else {
  369. // 新增模式
  370. await addStatementOrder(form.value);
  371. proxy?.$modal.msgSuccess('新增成功');
  372. }
  373. drawer.visible = false;
  374. emit('success');
  375. } catch (error) {
  376. console.error(error);
  377. } finally {
  378. buttonLoading.value = false;
  379. }
  380. }
  381. });
  382. };
  383. /** 关闭抽屉前的回调 */
  384. const handleDrawerClose = (done: () => void) => {
  385. if (buttonLoading.value) {
  386. return;
  387. }
  388. done();
  389. reset();
  390. };
  391. defineExpose({
  392. open
  393. });
  394. </script>
  395. <style scoped lang="scss">
  396. .drawer-header {
  397. display: flex;
  398. align-items: center;
  399. gap: 10px;
  400. .order-title {
  401. font-size: 16px;
  402. font-weight: 600;
  403. color: #303133;
  404. }
  405. }
  406. .drawer-content {
  407. padding: 0 20px 20px;
  408. height: calc(100% - 60px);
  409. overflow-y: auto;
  410. }
  411. .assign-dialog-content {
  412. padding: 0 10px;
  413. }
  414. .assign-target-section {
  415. margin-bottom: 20px;
  416. .target-input-wrapper {
  417. display: flex;
  418. align-items: center;
  419. gap: 8px;
  420. .required-mark {
  421. color: #f56c6c;
  422. font-size: 14px;
  423. }
  424. .label {
  425. font-size: 14px;
  426. color: #606266;
  427. white-space: nowrap;
  428. }
  429. }
  430. .or-divider {
  431. display: flex;
  432. align-items: center;
  433. justify-content: center;
  434. font-size: 14px;
  435. color: #909399;
  436. }
  437. }
  438. .product-list-section {
  439. margin-top: 20px;
  440. }
  441. .detail-header {
  442. padding: 15px 20px;
  443. background-color: #f5f7fa;
  444. border-radius: 4px;
  445. margin-bottom: 20px;
  446. .info-item {
  447. display: flex;
  448. flex-direction: column;
  449. gap: 5px;
  450. .label {
  451. font-size: 12px;
  452. color: #909399;
  453. }
  454. .value {
  455. font-size: 14px;
  456. color: #303133;
  457. font-weight: 500;
  458. }
  459. }
  460. }
  461. .detail-tabs {
  462. .tab-actions {
  463. margin-bottom: 15px;
  464. }
  465. }
  466. :deep(.el-alert__title) {
  467. font-size: 13px;
  468. }
  469. .empty-data {
  470. padding: 20px 0;
  471. text-align: center;
  472. }
  473. .drawer-footer {
  474. display: flex;
  475. justify-content: flex-end;
  476. gap: 10px;
  477. padding: 15px 20px;
  478. border-top: 1px solid #e4e7ed;
  479. }
  480. :deep(.el-upload__tip) {
  481. margin-top: 8px;
  482. font-size: 12px;
  483. color: #909399;
  484. }
  485. </style>