add.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <div class="p-2">
  3. <!-- 页面标题 -->
  4. <el-card shadow="never" class="mb-2">
  5. <div class="flex justify-between items-center">
  6. <div class="flex items-center">
  7. <el-button link @click="handleBack" class="mr-2">
  8. <el-icon class="mr-1"><ArrowLeft /></el-icon>
  9. 返回
  10. </el-button>
  11. <span class="text-lg font-bold">{{ pageTitle }}</span>
  12. </div>
  13. <el-button type="primary" @click="submitForm" :loading="buttonLoading">保存</el-button>
  14. </div>
  15. </el-card>
  16. <!-- 基本信息 -->
  17. <el-card shadow="never" class="mb-2">
  18. <el-form ref="revenueHeaderFormRef" :model="form" :rules="rules" label-width="120px">
  19. <el-row :gutter="20" v-if="form.revenueType == '0'">
  20. <el-col :span="8">
  21. <el-form-item label="客户编号:" prop="customerCode">
  22. <el-select
  23. v-model="form.customerCode"
  24. placeholder="请输入客户编号"
  25. style="width: 100%"
  26. filterable
  27. remote
  28. :remote-method="handleSearchCustomer"
  29. :loading="customerSearchLoading"
  30. @change="handleCustomerChange"
  31. clearable
  32. >
  33. <el-option
  34. v-for="customer in filteredCustomerList"
  35. :key="customer.id"
  36. :label="`${customer.customerNo} - ${customer.customerName}`"
  37. :value="customer.customerNo"
  38. />
  39. </el-select>
  40. </el-form-item>
  41. </el-col>
  42. <el-col :span="8">
  43. <el-form-item label="收入单类型:" prop="revenueType">
  44. <el-select v-model="form.revenueType" placeholder="请选择" style="width: 100%" disabled>
  45. <el-option v-for="dict in revenue_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  46. </el-select>
  47. </el-form-item>
  48. </el-col>
  49. <el-col :span="8">
  50. <el-form-item label="所属公司:" prop="companyName">
  51. <el-input v-model="form.companyName" disabled />
  52. </el-form-item>
  53. </el-col>
  54. </el-row>
  55. <el-row :gutter="20" v-if="form.revenueType == '1'">
  56. <el-col :span="8">
  57. <el-form-item label="收入单类型:" prop="revenueType">
  58. <el-select v-model="form.revenueType" placeholder="请选择" style="width: 100%" disabled>
  59. <el-option v-for="dict in revenue_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  60. </el-select>
  61. </el-form-item>
  62. </el-col>
  63. <el-col :span="8">
  64. <el-form-item label="所属公司:" prop="companyName">
  65. <el-input v-model="form.companyName" disabled />
  66. </el-form-item>
  67. </el-col>
  68. <el-col :span="8">
  69. <el-form-item label="供应商:" prop="supplierName">
  70. <el-select
  71. v-model="form.supplierName"
  72. placeholder="请输入供应商名称"
  73. style="width: 100%"
  74. filterable
  75. remote
  76. :remote-method="handleSearchCustomer"
  77. :loading="customerSearchLoading"
  78. @change="handleCustomerChange"
  79. clearable
  80. >
  81. <el-option
  82. v-for="customer in filteredCustomerList"
  83. :key="customer.id"
  84. :label="`${customer.customerNo} - ${customer.customerName}`"
  85. :value="customer.customerNo"
  86. />
  87. </el-select>
  88. </el-form-item>
  89. </el-col>
  90. </el-row>
  91. <el-row :gutter="20">
  92. <el-col :span="8">
  93. <el-form-item label="业务部门:" prop="businessDept">
  94. <el-input v-model="form.businessDept" disabled />
  95. </el-form-item>
  96. </el-col>
  97. <el-col :span="8">
  98. <el-form-item label="客服人员:" prop="customerService">
  99. <el-input v-model="form.customerService" disabled />
  100. </el-form-item>
  101. </el-col>
  102. <el-col :span="8">
  103. <el-form-item label="业务员:" prop="businessStaff">
  104. <el-input v-model="form.businessStaff" disabled />
  105. </el-form-item>
  106. </el-col>
  107. </el-row>
  108. <el-row :gutter="20">
  109. <el-col :span="8">
  110. <el-form-item label="是否含税:" prop="isPrwTax">
  111. <el-radio-group v-model="form.isPrwTax">
  112. <el-radio v-for="dict in sys_platform_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  113. </el-radio-group>
  114. </el-form-item>
  115. </el-col>
  116. <el-col :span="8">
  117. <el-form-item label="交易币别:" prop="currencyId">
  118. <el-select v-model="form.currencyId" placeholder="请选择" style="width: 100%" clearable>
  119. <el-option v-for="currency in currencyList" :key="currency.id" :label="currency.currencyName" :value="currency.id" />
  120. </el-select>
  121. </el-form-item>
  122. </el-col>
  123. </el-row>
  124. <el-row :gutter="20">
  125. <el-col :span="16">
  126. <el-form-item label="备注:" prop="remark">
  127. <el-input v-model="form.remark" type="textarea" :min="3" placeholder="请输入备注" />
  128. </el-form-item>
  129. </el-col>
  130. </el-row>
  131. </el-form>
  132. </el-card>
  133. <!-- 其他信息 -->
  134. <el-card shadow="never">
  135. <template #header>
  136. <div class="flex justify-between items-center">
  137. <span>其他信息:</span>
  138. <el-button type="primary" plain @click="handleAddDetail">新增</el-button>
  139. </div>
  140. <div style="margin-left: 60px">
  141. <span>总计:{{ totalAmount.toFixed(2) }}</span>
  142. </div>
  143. </template>
  144. <el-table :data="detailList" border style="width: 100%">
  145. <el-table-column label="费用类型" min-width="150" align="center">
  146. <template #default="scope">
  147. <el-select v-model="scope.row.revenueId" placeholder="请选择" style="width: 100%">
  148. <el-option v-for="item in feeTypeList" :key="item.id" :label="`${item.revenueCode},${item.revenueName}`" :value="item.id" />
  149. </el-select>
  150. </template>
  151. </el-table-column>
  152. <el-table-column label="数量" min-width="120" align="center">
  153. <template #default="scope">
  154. <el-input-number
  155. v-model="scope.row.quantity"
  156. :precision="0"
  157. :controls="false"
  158. style="width: 100%"
  159. placeholder="请输入"
  160. @change="handleQuantityOrPriceChange(scope.$index)"
  161. />
  162. </template>
  163. </el-table-column>
  164. <el-table-column label="单价" min-width="120" align="center">
  165. <template #default="scope">
  166. <el-input-number
  167. v-model="scope.row.unitPrice"
  168. :precision="2"
  169. :controls="false"
  170. style="width: 100%"
  171. placeholder="请输入"
  172. @change="handleQuantityOrPriceChange(scope.$index)"
  173. />
  174. </template>
  175. </el-table-column>
  176. <el-table-column label="总金额" min-width="120" align="center">
  177. <template #default="scope">
  178. <el-input-number
  179. v-model="scope.row.totalAmount"
  180. :precision="2"
  181. :controls="false"
  182. style="width: 100%"
  183. placeholder="请输入"
  184. @change="calculateTotal"
  185. />
  186. </template>
  187. </el-table-column>
  188. <el-table-column label="备注" min-width="150" align="center">
  189. <template #default="scope">
  190. <el-input v-model="scope.row.remark" placeholder="请输入" />
  191. </template>
  192. </el-table-column>
  193. <el-table-column label="操作" width="80" fixed="right" align="center">
  194. <template #default="scope">
  195. <el-button link type="danger" @click="handleDeleteDetail(scope.$index)">删除</el-button>
  196. </template>
  197. </el-table-column>
  198. </el-table>
  199. </el-card>
  200. </div>
  201. </template>
  202. <script setup lang="ts">
  203. import { useRouter } from 'vue-router';
  204. import { listRevenueHeader, getRevenueHeader, addRevenueHeader, updateRevenueHeader } from '@/api/order/revenueHeader';
  205. import { RevenueHeaderVO, RevenueHeaderQuery, RevenueHeaderForm } from '@/api/order/revenueHeader/types';
  206. import { listCustomerInfo, getCustomerInfo } from '@/api/customer/customerFile/customerInfo';
  207. import { CustomerInfoVO } from '@/api/customer/customerFile/customerInfo/types';
  208. import { listRevenueExpense } from '@/api/company/revenueExpense';
  209. import { RevenueExpenseVO } from '@/api/company/revenueExpense/types';
  210. import { listComCurrency } from '@/api/company/comCurrency';
  211. import { ComCurrencyVO } from '@/api/company/comCurrency/types';
  212. import { getCompany } from '@/api/company/company';
  213. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  214. const { sys_platform_yes_no, check_status, revenue_type } = toRefs<any>(proxy?.useDict('sys_platform_yes_no', 'check_status', 'revenue_type'));
  215. const router = useRouter();
  216. const route = useRoute();
  217. const revenueHeaderFormRef = ref();
  218. const buttonLoading = ref(false);
  219. const customerList = ref<CustomerInfoVO[]>([]);
  220. const filteredCustomerList = ref<CustomerInfoVO[]>([]);
  221. const customerSearchLoading = ref(false);
  222. const feeTypeList = ref<RevenueExpenseVO[]>([]);
  223. const currencyList = ref<ComCurrencyVO[]>([]);
  224. // 防抖定时器
  225. let searchTimer: NodeJS.Timeout | null = null;
  226. const detailList = ref<any[]>([]);
  227. // 计算总金额
  228. const totalAmount = computed(() => {
  229. return detailList.value.reduce((sum, item) => {
  230. const amount = Number(item.totalAmount) || 0;
  231. return sum + amount;
  232. }, 0);
  233. });
  234. // 表单初始数据
  235. const initFormData: RevenueHeaderForm = {
  236. id: undefined,
  237. revenueType: '收入申请',
  238. orderRevenueCode: undefined,
  239. incomeOrderCode: undefined,
  240. otherRevenueType: undefined,
  241. customerId: undefined,
  242. customerCode: undefined,
  243. supplierId: undefined,
  244. isPrwTax: '0',
  245. currencyId: undefined,
  246. pushStatus: undefined,
  247. orderFile: undefined,
  248. status: undefined,
  249. remark: undefined,
  250. companyId: undefined,
  251. businessDept: undefined,
  252. customerService: undefined,
  253. businessStaff: undefined,
  254. createTime: new Date().toISOString().split('T')[0],
  255. orderRevenueDetails: []
  256. };
  257. const data = reactive<PageData<RevenueHeaderForm, RevenueHeaderQuery>>({
  258. form: { ...initFormData },
  259. queryParams: {
  260. pageNum: 1,
  261. pageSize: 10,
  262. revenueType: undefined,
  263. orderRevenueCode: undefined,
  264. incomeOrderCode: undefined,
  265. otherRevenueType: undefined,
  266. customerId: undefined,
  267. supplierId: undefined,
  268. isPrwTax: undefined,
  269. currencyId: undefined,
  270. pushStatus: undefined,
  271. orderFile: undefined,
  272. status: undefined,
  273. platformCode: undefined,
  274. params: {}
  275. },
  276. rules: {
  277. revenueType: [{ required: true, message: '收入单类型不能为空', trigger: 'change' }],
  278. customerCode: [{ required: true, message: '客户编号不能为空', trigger: 'blur' }],
  279. companyId: [{ required: true, message: '所属公司不能为空', trigger: 'change' }],
  280. currencyId: [{ required: true, message: '交易币别不能为空', trigger: 'change' }],
  281. isPrwTax: [{ required: true, message: '是否含税不能为空', trigger: 'change' }]
  282. }
  283. });
  284. const { queryParams, form, rules } = toRefs(data);
  285. // 防抖搜索客户
  286. const handleSearchCustomer = (query: string) => {
  287. if (searchTimer) {
  288. clearTimeout(searchTimer);
  289. }
  290. if (!query) {
  291. filteredCustomerList.value = [];
  292. return;
  293. }
  294. customerSearchLoading.value = true;
  295. searchTimer = setTimeout(async () => {
  296. try {
  297. const res = await listCustomerInfo({
  298. customerNo: query,
  299. pageNum: 1,
  300. pageSize: 10
  301. });
  302. filteredCustomerList.value = res.rows || [];
  303. } catch (error) {
  304. console.error('搜索客户失败:', error);
  305. filteredCustomerList.value = [];
  306. } finally {
  307. customerSearchLoading.value = false;
  308. }
  309. }, 500);
  310. };
  311. // 客户选择变化
  312. const handleCustomerChange = async (customerNo: string) => {
  313. if (!customerNo) {
  314. // 清空相关字段
  315. form.value.customerId = undefined;
  316. form.value.customerName = '';
  317. form.value.companyName = '';
  318. form.value.businessDept = '';
  319. form.value.customerService = '';
  320. form.value.businessStaff = '';
  321. return;
  322. }
  323. try {
  324. // 从筛选列表中找到选中的客户
  325. const selectedCustomer = filteredCustomerList.value.find((c) => c.customerNo === customerNo);
  326. if (!selectedCustomer) return;
  327. // 获取客户详细信息
  328. const res = await getCustomerInfo(selectedCustomer.id);
  329. const customerInfo = res.data;
  330. // 填充客户信息
  331. form.value.customerId = customerInfo.id;
  332. form.value.customerName = customerInfo.customerName;
  333. // 从客户销售信息中获取业务信息
  334. if (customerInfo.customerSalesInfoVo) {
  335. const salesInfo = customerInfo.customerSalesInfoVo as any;
  336. form.value.businessStaff = salesInfo.salesPerson || '';
  337. form.value.customerService = salesInfo.serviceStaff || '';
  338. form.value.businessDept = salesInfo.belongingDepartment || '';
  339. }
  340. // 获取所属公司信息
  341. if (customerInfo.belongCompanyId) {
  342. try {
  343. const companyRes = await getCompany(customerInfo.belongCompanyId);
  344. form.value.companyName = companyRes.data.companyName || '';
  345. form.value.companyId = customerInfo.belongCompanyId;
  346. } catch (error) {
  347. console.error('获取公司信息失败:', error);
  348. }
  349. }
  350. } catch (error) {
  351. console.error('获取客户信息失败:', error);
  352. ElMessage.error('获取客户信息失败');
  353. }
  354. };
  355. // 加载费用类型列表
  356. const loadFeeTypeList = async () => {
  357. try {
  358. // 根据revenueType决定查询条件
  359. const queryParams: any = {
  360. isShow: '0',
  361. pageNum: 1,
  362. pageSize: 1000
  363. };
  364. if (form.value.revenueType === '0') {
  365. // 收入申请:查询收入类
  366. queryParams.revenueFlag = '0';
  367. } else if (form.value.revenueType === '1') {
  368. // 费用申请:查询费用类
  369. queryParams.expenseFlag = '0';
  370. }
  371. const res = await listRevenueExpense(queryParams);
  372. feeTypeList.value = res.rows || [];
  373. } catch (error) {
  374. console.error('加载费用类型列表失败:', error);
  375. feeTypeList.value = [];
  376. }
  377. };
  378. // 加载币种列表
  379. const loadCurrencyList = async () => {
  380. try {
  381. const res = await listComCurrency({
  382. isShow: '0',
  383. pageNum: 1,
  384. pageSize: 1000
  385. });
  386. currencyList.value = res.rows || [];
  387. } catch (error) {
  388. console.error('加载币种列表失败:', error);
  389. currencyList.value = [];
  390. }
  391. };
  392. // 新增明细行
  393. const handleAddDetail = () => {
  394. detailList.value.push({
  395. revenueId: undefined,
  396. quantity: 0,
  397. unitPrice: 0,
  398. totalAmount: 0,
  399. remark: ''
  400. });
  401. };
  402. // 数量或单价变化时自动计算总金额
  403. const handleQuantityOrPriceChange = (index: number) => {
  404. const detail = detailList.value[index];
  405. if (detail) {
  406. const quantity = Number(detail.quantity) || 0;
  407. const unitPrice = Number(detail.unitPrice) || 0;
  408. detail.totalAmount = quantity * unitPrice;
  409. }
  410. };
  411. // 删除明细行
  412. const handleDeleteDetail = (index: number) => {
  413. detailList.value.splice(index, 1);
  414. };
  415. // 计算总金额(手动触发)
  416. const calculateTotal = () => {
  417. // 触发计算属性重新计算
  418. };
  419. // 提交表单
  420. const submitForm = async () => {
  421. try {
  422. await revenueHeaderFormRef.value?.validate();
  423. // 验证是否有明细数据
  424. if (!detailList.value || detailList.value.length === 0) {
  425. ElMessage.warning('请至少添加一条其他信息');
  426. return;
  427. }
  428. buttonLoading.value = true;
  429. // 组装明细数据到 orderRevenueDetails
  430. form.value.orderRevenueDetails = detailList.value.map((detail) => ({
  431. revenueId: detail.revenueId,
  432. quantity: detail.quantity,
  433. unitPrice: detail.unitPrice,
  434. totalAmount: detail.totalAmount,
  435. remark: detail.remark
  436. }));
  437. // 调用保存接口
  438. if (form.value.id) {
  439. await updateRevenueHeader(form.value);
  440. } else {
  441. await addRevenueHeader(form.value);
  442. }
  443. ElMessage.success('保存成功');
  444. // 跳转回列表页
  445. router.push('/order-center/order-revenue');
  446. } catch (error) {
  447. console.error('保存失败:', error);
  448. if (error !== false) {
  449. ElMessage.error('保存失败,请检查数据后重试');
  450. }
  451. } finally {
  452. buttonLoading.value = false;
  453. }
  454. };
  455. // 返回按钮
  456. const handleBack = () => {
  457. router.back();
  458. };
  459. // 取消
  460. const cancel = () => {
  461. router.back();
  462. };
  463. // 页面标题
  464. const pageTitle = computed(() => {
  465. const isEdit = !!route.query.id;
  466. if (isEdit) {
  467. return form.value.revenueType === '1' ? '编辑费用申请单' : '编辑收入申请单';
  468. }
  469. return form.value.revenueType === '1' ? '新增费用申请单' : '新增收入申请单';
  470. });
  471. // 加载订单详情
  472. const loadOrderDetail = async (id: string | number) => {
  473. try {
  474. const res = await getRevenueHeader(id);
  475. const data = res.data;
  476. // 填充表单数据
  477. Object.assign(form.value, data);
  478. // 填充明细列表
  479. if (data.orderRevenueDetailList && data.orderRevenueDetailList.length > 0) {
  480. detailList.value = data.orderRevenueDetailList.map((item: any) => ({
  481. revenueId: item.revenueId,
  482. quantity: item.quantity || 0,
  483. unitPrice: item.unitPrice || 0,
  484. totalAmount: item.totalAmount || 0,
  485. remark: item.remark || ''
  486. }));
  487. }
  488. } catch (error) {
  489. console.error('加载订单详情失败:', error);
  490. ElMessage.error('加载订单详情失败');
  491. }
  492. };
  493. // 组件挂载时加载数据
  494. onMounted(async () => {
  495. // 从路由参数获取revenueType和id
  496. const revenueType = route.query.revenueType as string;
  497. const orderId = route.query.id as string;
  498. if (revenueType) {
  499. form.value.revenueType = revenueType;
  500. } else {
  501. // 默认为收入申请
  502. form.value.revenueType = '0';
  503. }
  504. // 加载基础数据
  505. await loadFeeTypeList();
  506. await loadCurrencyList();
  507. // 如果有id,说明是编辑模式,加载订单详情
  508. if (orderId) {
  509. await loadOrderDetail(orderId);
  510. } else {
  511. // 新增模式,默认添加一个空行
  512. handleAddDetail();
  513. }
  514. });
  515. </script>
  516. <style scoped lang="scss">
  517. .mb-2 {
  518. margin-bottom: 16px;
  519. }
  520. .mt-4 {
  521. margin-top: 16px;
  522. }
  523. </style>