addDrawer.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  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">{{ drawer.title }}</span>
  7. </div>
  8. </template>
  9. <div class="drawer-content">
  10. <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
  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="customerId">
  18. <el-select
  19. v-model="form.customerId"
  20. filterable
  21. remote
  22. reserve-keyword
  23. placeholder="请输入客户名称"
  24. :remote-method="remoteSearchCustomer"
  25. :loading="customerLoading"
  26. clearable
  27. style="width: 100%"
  28. @change="handleCustomerChange"
  29. >
  30. <el-option v-for="item in customerOptions" :key="item.id" :label="item.customerName" :value="item.id" />
  31. </el-select>
  32. </el-form-item>
  33. </el-col>
  34. <el-col :span="8">
  35. <el-form-item label="开票金额" prop="invoiceAmount">
  36. <el-input v-model="form.invoiceAmount" disabled />
  37. </el-form-item>
  38. </el-col>
  39. <el-col :span="8">
  40. <el-form-item label="开票日期" prop="invoiceTime">
  41. <el-date-picker v-model="form.invoiceTime" type="date" placeholder="请选择开票日期" style="width: 100%" value-format="YYYY-MM-DD" />
  42. </el-form-item>
  43. </el-col>
  44. </el-row>
  45. <!-- 账单列表 -->
  46. <el-divider content-position="left">
  47. <span style="color: #409eff; font-weight: 600">账单列表</span>
  48. </el-divider>
  49. <div class="table-actions" style="margin-bottom: 15px">
  50. <el-button type="primary" icon="Plus" @click="handleSelectStatement">选择对账单</el-button>
  51. </div>
  52. <el-table :data="form.detailList" border style="width: 100%; margin-bottom: 20px">
  53. <el-table-column type="index" label="序号" width="60" align="center" />
  54. <el-table-column prop="statementOrderNo" label="对账单编号" min-width="150" align="center" />
  55. <el-table-column prop="statementAmount" label="对账金额" min-width="120" align="center" />
  56. <el-table-column prop="orderNo" label="订单编号" min-width="150" align="center" />
  57. <el-table-column prop="orderAmount" label="金额" min-width="120" align="center" />
  58. <el-table-column prop="orderTime" label="下单日期" min-width="120" align="center" />
  59. <el-table-column prop="userName" label="下单人" min-width="100" align="center" />
  60. <el-table-column prop="userDept" label="部门" min-width="120" align="center" />
  61. </el-table>
  62. <!-- 商品清单 -->
  63. <el-divider content-position="left">
  64. <span style="color: #409eff; font-weight: 600">商品清单</span>
  65. </el-divider>
  66. <el-table :data="pagedProductList" border style="width: 100%; margin-bottom: 20px">
  67. <el-table-column
  68. type="index"
  69. label="序号"
  70. width="60"
  71. align="center"
  72. :index="(index) => (productPage.pageNum - 1) * productPage.pageSize + index + 1"
  73. />
  74. <el-table-column prop="orderNo" label="订单编号" min-width="120" align="center" />
  75. <el-table-column prop="productNo" label="商品编号" min-width="120" align="center" />
  76. <el-table-column prop="itemName" label="商品名称" min-width="180" align="center" />
  77. <el-table-column prop="unitName" label="单位" align="center" />
  78. <el-table-column prop="quantity" label="数量" align="center" />
  79. <el-table-column prop="unitPrice" label="单价" align="center">
  80. <template #default="scope">
  81. {{ scope.row.unitPrice ? Number(scope.row.unitPrice).toFixed(2) : '0.00' }}
  82. </template>
  83. </el-table-column>
  84. <el-table-column label="小计" align="center">
  85. <template #default="scope">
  86. {{ (Number(scope.row.quantity || 0) * Number(scope.row.unitPrice || 0)).toFixed(2) }}
  87. </template>
  88. </el-table-column>
  89. </el-table>
  90. <!-- 分页 -->
  91. <el-pagination
  92. v-if="form.productList && form.productList.length > 0"
  93. v-model:current-page="productPage.pageNum"
  94. v-model:page-size="productPage.pageSize"
  95. :page-sizes="[10, 20, 50, 100]"
  96. layout="total, sizes, prev, pager, next, jumpers"
  97. :total="productPage.total"
  98. @size-change="handleProductSizeChange"
  99. @current-change="handleProductCurrentChange"
  100. style="margin-top: 15px"
  101. />
  102. <!-- 发票列表 -->
  103. <el-divider content-position="left">
  104. <span style="color: #409eff; font-weight: 600">发票列表</span>
  105. </el-divider>
  106. <div class="table-actions" style="margin-bottom: 15px">
  107. <el-button type="primary" icon="Plus" @click="handleAddInvoice">新增发票</el-button>
  108. </div>
  109. <!-- 发票表格 -->
  110. <el-table :data="invoiceList" border style="width: 100%; margin-bottom: 20px">
  111. <el-table-column prop="invoiceType" label="发票类型" min-width="120" align="center">
  112. <template #default="scope">
  113. <dict-tag :options="invoice_type" :value="scope.row.invoiceType" />
  114. </template>
  115. </el-table-column>
  116. <el-table-column prop="invoiceDate" label="开票日期" min-width="120" align="center" />
  117. <el-table-column prop="invoiceCode" label="发票代码" min-width="150" align="center" />
  118. <el-table-column prop="invoiceAmount" label="发票金额" min-width="120" align="center">
  119. <template #default="scope">
  120. {{ Number(scope.row.invoiceAmount || 0).toFixed(2) }}
  121. </template>
  122. </el-table-column>
  123. <el-table-column prop="remark" label="备注" align="center" />
  124. <el-table-column label="操作" width="150" align="center">
  125. <template #default="scope">
  126. <el-button link type="primary" size="small" @click="handleEditInvoice(scope.row, scope.$index)">编辑</el-button>
  127. <el-button link type="danger" size="small" @click="handleDeleteInvoice(scope.$index)">删除</el-button>
  128. </template>
  129. </el-table-column>
  130. </el-table>
  131. </el-form>
  132. </div>
  133. <template #footer>
  134. <div class="drawer-footer">
  135. <el-button @click="handleDrawerClose(() => (drawer.visible = false))">取消</el-button>
  136. <el-button type="primary" :loading="buttonLoading" @click="handleSubmit">确定</el-button>
  137. </div>
  138. </template>
  139. </el-drawer>
  140. <!-- 对账单选择抽屉 -->
  141. <StatementOrderDrawer ref="statementOrderDrawerRef" @success="handleStatementSelected" />
  142. <!-- 文件选择器 -->
  143. <FileSelector v-model="fileSelectorVisible" :multiple="true" :allowed-types="[1, 2, 3, 4, 5]" title="选择发票附件" @confirm="handleFileSelected" />
  144. <!-- 新增发票对话框 -->
  145. <AddInvoiceDialog ref="addInvoiceDialogRef" @success="handleInvoiceAdded" />
  146. </template>
  147. <script setup name="InvoiceAddDrawer" lang="ts">
  148. import { addStatementInvoice, updateStatementInvoice } from '@/api/bill/statementInvoice';
  149. import { StatementInvoiceForm } from '@/api/bill/statementInvoice/types';
  150. import { getListBycustomerName } from '@/api/customer/customerFile/customerInfo';
  151. import { CustomerInfoVO } from '@/api/customer/customerFile/customerInfo/types';
  152. import { StatementOrderVO } from '@/api/bill/statementOrder/types';
  153. import StatementOrderDrawer from './statementOrderDrawer.vue';
  154. import FileSelector from '@/components/FileSelector/index.vue';
  155. import AddInvoiceDialog from './addInvoiceDialog.vue';
  156. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  157. const { invoice_type } = toRefs<any>(proxy?.useDict('invoice_type'));
  158. const initFormData: StatementInvoiceForm = {
  159. id: undefined,
  160. statementInvoiceNo: undefined,
  161. customerId: undefined,
  162. customerNo: undefined,
  163. customerName: undefined,
  164. invoiceAmount: undefined,
  165. invoiceStatus: undefined,
  166. invoiceTime: undefined,
  167. rejectRemark: undefined,
  168. remark: undefined,
  169. detailList: [],
  170. productList: [],
  171. invoiceList: []
  172. };
  173. const formRef = ref<ElFormInstance>();
  174. const buttonLoading = ref(false);
  175. const form = ref<StatementInvoiceForm>({ ...initFormData });
  176. const rules = reactive({
  177. customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
  178. invoiceTime: [{ required: true, message: '请选择开票日期', trigger: 'change' }]
  179. } as any);
  180. const fileList = ref<any[]>([]);
  181. const productPage = reactive({
  182. pageNum: 1,
  183. pageSize: 10,
  184. total: 0
  185. });
  186. const drawer = reactive<DialogOption>({
  187. visible: false,
  188. title: '新增发票'
  189. });
  190. const isEdit = ref(false);
  191. const customerLoading = ref(false);
  192. const customerOptions = ref<CustomerInfoVO[]>([]);
  193. const statementOrderDrawerRef = ref<any>();
  194. const fileSelectorVisible = ref(false);
  195. const addInvoiceDialogRef = ref<any>();
  196. const currentSelectedStatements = ref<StatementOrderVO[]>([]);
  197. const invoiceList = ref<any[]>([]); // 发票列表
  198. const editingInvoiceIndex = ref<number>(-1);
  199. /** 计算当前页的商品列表 */
  200. const pagedProductList = computed(() => {
  201. const start = (productPage.pageNum - 1) * productPage.pageSize;
  202. const end = start + productPage.pageSize;
  203. return form.value.productList?.slice(start, end) || [];
  204. });
  205. /** 打开新增/编辑发票抽屉 */
  206. const open = (id?: string | number, data?: StatementInvoiceForm) => {
  207. reset();
  208. if (id && data) {
  209. // 编辑模式
  210. isEdit.value = true;
  211. drawer.title = '编辑发票';
  212. Object.assign(form.value, data);
  213. // 将客户信息添加到下拉选项中,以便回显
  214. if (data.customerId && data.customerName) {
  215. customerOptions.value = [
  216. {
  217. id: data.customerId,
  218. customerName: data.customerName,
  219. customerNo: data.customerNo || ''
  220. } as CustomerInfoVO
  221. ];
  222. }
  223. // 解析附件地址并回显附件列表
  224. if (data.annexAddress) {
  225. const urls = data.annexAddress.split(',').filter((url) => url.trim());
  226. fileList.value = urls.map((url, index) => {
  227. const fileName = url.split('/').pop() || `附件${index + 1}`;
  228. return {
  229. name: fileName,
  230. url: url.trim(),
  231. id: undefined
  232. };
  233. });
  234. }
  235. // 设置商品列表分页总数
  236. if (data.productList && data.productList.length > 0) {
  237. productPage.total = data.productList.length;
  238. }
  239. // 回显发票列表
  240. if (data.invoiceList && data.invoiceList.length > 0) {
  241. invoiceList.value = data.invoiceList;
  242. }
  243. } else {
  244. // 新增模式
  245. isEdit.value = false;
  246. drawer.title = '新增发票';
  247. }
  248. drawer.visible = true;
  249. };
  250. /** 表单重置 */
  251. const reset = () => {
  252. form.value = { ...initFormData };
  253. fileList.value = [];
  254. invoiceList.value = [];
  255. editingInvoiceIndex.value = -1;
  256. productPage.pageNum = 1;
  257. productPage.pageSize = 10;
  258. productPage.total = 0;
  259. customerOptions.value = [];
  260. currentSelectedStatements.value = [];
  261. formRef.value?.clearValidate();
  262. };
  263. /** 远程搜索客户 */
  264. const remoteSearchCustomer = async (query: string) => {
  265. if (query) {
  266. customerLoading.value = true;
  267. try {
  268. const res = await getListBycustomerName(query);
  269. customerOptions.value = res.data || [];
  270. } catch (error) {
  271. console.error('搜索客户失败:', error);
  272. } finally {
  273. customerLoading.value = false;
  274. }
  275. } else {
  276. customerOptions.value = [];
  277. }
  278. };
  279. /** 客户变更 */
  280. const handleCustomerChange = (value: string | number) => {
  281. const selectedCustomer = customerOptions.value.find((customer) => customer.id === value);
  282. if (selectedCustomer) {
  283. form.value.customerId = selectedCustomer.id;
  284. form.value.customerNo = selectedCustomer.customerNo;
  285. form.value.customerName = selectedCustomer.customerName;
  286. // 清空之前选择的对账单和商品
  287. form.value.detailList = [];
  288. form.value.productList = [];
  289. form.value.invoiceAmount = undefined;
  290. productPage.total = 0;
  291. currentSelectedStatements.value = [];
  292. }
  293. };
  294. /** 选择对账单 */
  295. const handleSelectStatement = () => {
  296. if (!form.value.customerId) {
  297. proxy?.$modal.msgWarning('请先选择客户');
  298. return;
  299. }
  300. // 使用nextTick确保组件已挂载
  301. nextTick(() => {
  302. if (statementOrderDrawerRef.value && typeof statementOrderDrawerRef.value.open === 'function') {
  303. statementOrderDrawerRef.value.open(form.value.customerId, currentSelectedStatements.value);
  304. } else {
  305. console.error('对账单抽屉组件未正确加载');
  306. }
  307. });
  308. };
  309. /** 对账单选择成功回调 */
  310. const handleStatementSelected = (data: any) => {
  311. const { statements, details, products } = data;
  312. if (statements && statements.length > 0) {
  313. let totalAmount = 0;
  314. // 保存当前选中的对账单
  315. currentSelectedStatements.value = statements;
  316. // 清空现有数据
  317. form.value.detailList = [];
  318. form.value.productList = [];
  319. // 填充发票明细
  320. if (details && details.length > 0) {
  321. form.value.detailList = details.map((detail: any) => ({
  322. statementOrderId: detail.statementOrderId,
  323. statementOrderNo: detail.statementOrderNo || '',
  324. statementAmount: detail.statementAmount || 0,
  325. orderId: detail.orderId || '',
  326. orderNo: detail.orderNo || '',
  327. orderAmount: detail.amount || 0,
  328. signingDate: detail.signingDate || '',
  329. orderTime: detail.orderTime || '',
  330. userDept: detail.userDept || '',
  331. userName: detail.userName || ''
  332. }));
  333. // 计算总金额
  334. totalAmount = details.reduce((sum: number, detail: any) => sum + Number(detail.amount || 0), 0);
  335. }
  336. // 设置开票金额
  337. form.value.invoiceAmount = totalAmount;
  338. // 直接使用传递过来的产品列表
  339. if (products && products.length > 0) {
  340. form.value.productList = products;
  341. productPage.total = products.length;
  342. }
  343. }
  344. };
  345. /** 打开文件选择器 */
  346. const handleOpenFileSelector = () => {
  347. fileSelectorVisible.value = true;
  348. };
  349. /** 文件选择完成 */
  350. const handleFileSelected = (files: any[]) => {
  351. if (files && files.length > 0) {
  352. files.forEach((file) => {
  353. fileList.value.push({
  354. name: file.fileName || file.name,
  355. url: file.fileUrl || file.url,
  356. id: file.id
  357. });
  358. });
  359. form.value.annexAddress = fileList.value.map((f) => f.url || f.name).join(',');
  360. }
  361. fileSelectorVisible.value = false;
  362. };
  363. /** 删除文件 */
  364. const handleRemoveFile = (index: number) => {
  365. fileList.value.splice(index, 1);
  366. form.value.annexAddress = fileList.value.map((f) => f.url || f.name).join(',');
  367. };
  368. /** 下载文件 */
  369. const handleDownloadFile = async (file: any) => {
  370. if (!file.url) {
  371. proxy?.$modal.msgWarning('文件地址不存在');
  372. return;
  373. }
  374. try {
  375. const response = await fetch(file.url);
  376. const blob = await response.blob();
  377. const blobUrl = window.URL.createObjectURL(blob);
  378. const link = document.createElement('a');
  379. link.href = blobUrl;
  380. link.download = file.name || '附件';
  381. document.body.appendChild(link);
  382. link.click();
  383. document.body.removeChild(link);
  384. window.URL.revokeObjectURL(blobUrl);
  385. } catch (error) {
  386. console.error('下载失败:', error);
  387. const link = document.createElement('a');
  388. link.href = file.url;
  389. link.download = file.name || '附件';
  390. link.target = '_blank';
  391. document.body.appendChild(link);
  392. link.click();
  393. document.body.removeChild(link);
  394. }
  395. };
  396. /** 预览文件 */
  397. const handlePreviewFile = (file: any) => {
  398. if (!file.url) {
  399. proxy?.$modal.msgWarning('文件地址不存在');
  400. return;
  401. }
  402. window.open(file.url, '_blank');
  403. };
  404. /** 产品分页大小改变 */
  405. const handleProductSizeChange = (val: number) => {
  406. productPage.pageSize = val;
  407. };
  408. /** 产品分页页码改变 */
  409. const handleProductCurrentChange = (val: number) => {
  410. productPage.pageNum = val;
  411. };
  412. /** 新增发票 */
  413. const handleAddInvoice = () => {
  414. editingInvoiceIndex.value = -1;
  415. addInvoiceDialogRef.value?.open({
  416. invoiceCompanyName: form.value.customerName
  417. });
  418. };
  419. /** 发票添加成功 */
  420. const handleInvoiceAdded = (invoiceData: any) => {
  421. // 编辑:更新当前行;新增:追加
  422. if (editingInvoiceIndex.value >= 0 && editingInvoiceIndex.value < invoiceList.value.length) {
  423. const prev = invoiceList.value[editingInvoiceIndex.value] || {};
  424. invoiceList.value.splice(editingInvoiceIndex.value, 1, {
  425. ...prev,
  426. ...invoiceData,
  427. id: prev.id
  428. });
  429. } else {
  430. invoiceList.value.push({
  431. ...invoiceData,
  432. id: invoiceData?.id ?? Date.now()
  433. });
  434. }
  435. editingInvoiceIndex.value = -1;
  436. };
  437. /** 编辑发票 */
  438. const handleEditInvoice = (invoice: any, index: number) => {
  439. editingInvoiceIndex.value = index;
  440. addInvoiceDialogRef.value?.open({
  441. ...invoice,
  442. invoiceCompanyName: invoice?.invoiceCompanyName ?? invoice?.invoiceCompany ?? invoice?.companyName ?? invoice?.company ?? form.value.customerName
  443. });
  444. };
  445. /** 删除发票 */
  446. const handleDeleteInvoice = (index: number) => {
  447. proxy?.$modal
  448. .confirm('确认删除该发票吗?')
  449. .then(() => {
  450. invoiceList.value.splice(index, 1);
  451. proxy?.$modal.msgSuccess('删除成功');
  452. })
  453. .catch(() => {});
  454. };
  455. /** 提交表单 */
  456. const handleSubmit = async () => {
  457. if (!formRef.value) return;
  458. await formRef.value.validate(async (valid) => {
  459. if (valid) {
  460. buttonLoading.value = true;
  461. try {
  462. // 组装发票列表数据
  463. form.value.invoiceList = invoiceList.value;
  464. if (isEdit.value) {
  465. await updateStatementInvoice(form.value);
  466. proxy?.$modal.msgSuccess('修改成功');
  467. } else {
  468. await addStatementInvoice(form.value);
  469. proxy?.$modal.msgSuccess('新增成功');
  470. }
  471. drawer.visible = false;
  472. emit('success');
  473. } catch (error) {
  474. console.error(error);
  475. } finally {
  476. buttonLoading.value = false;
  477. }
  478. }
  479. });
  480. };
  481. /** 关闭抽屉前的回调 */
  482. const handleDrawerClose = (done: () => void) => {
  483. if (buttonLoading.value) {
  484. return;
  485. }
  486. done();
  487. reset();
  488. };
  489. const emit = defineEmits(['success']);
  490. defineExpose({
  491. open
  492. });
  493. </script>
  494. <style scoped lang="scss">
  495. .drawer-header {
  496. display: flex;
  497. align-items: center;
  498. gap: 10px;
  499. .order-title {
  500. font-size: 16px;
  501. font-weight: 600;
  502. color: #303133;
  503. }
  504. }
  505. .drawer-content {
  506. padding: 0 20px 20px;
  507. height: calc(100% - 60px);
  508. overflow-y: auto;
  509. }
  510. .empty-data {
  511. text-align: center;
  512. padding: 20px 0;
  513. }
  514. .drawer-footer {
  515. display: flex;
  516. justify-content: flex-end;
  517. gap: 10px;
  518. }
  519. </style>