record.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <template>
  2. <HcCard>
  3. <template #header>
  4. <div class="hc-expense-total-title">报销总额(元):{{totalFrMoney}}</div>
  5. </template>
  6. <div class="hac-expense-record-body">
  7. <div class="record-form-box">
  8. <el-scrollbar>
  9. <HcCardItem :title="'报销明细' + (index + 1)" v-for="(item, index) in detailsData.details" ui="hac-bg-grey mb-5">
  10. <template #extra>
  11. <el-button type="danger" size="small" @click="delDetailsData(index)" v-if="index > 0">
  12. <HcIcon name="delete-bin"/>
  13. <span>删除明细</span>
  14. </el-button>
  15. </template>
  16. <el-form :ref="(el) => setFormItemRefs(el, index)" :model="item" :rules="formRules" label-position="left" label-width="auto" size="large">
  17. <div class="hc-form-item">
  18. <el-form-item label="所属项目:" prop="projectId">
  19. <el-select block v-model="item.projectId">
  20. <el-option v-for="items in projectData" :label="items.projectName" :value="items.projectId"/>
  21. </el-select>
  22. </el-form-item>
  23. <div class="ml-2">
  24. <el-button type="default" @click="budgetModalShow(item,index)">
  25. <HcIcon name="add"/>
  26. <span>关联预算计划</span>
  27. </el-button>
  28. </div>
  29. </div>
  30. <div class="hc-form-item">
  31. <el-form-item label="报销金额(元):" prop="frMoney">
  32. <el-input v-model="item.frMoney" placeholder="请输入报销金额" onkeyup="this.value=this.value.match(/\d+\.?\d{0,2}/)"/>
  33. </el-form-item>
  34. <el-form-item label="费用发生日期:" prop="frDate">
  35. <el-date-picker type="date" class="block" v-model="item.frDate" format="YYYY-MM-DD" value-format="YYYY-MM-DD"/>
  36. </el-form-item>
  37. <el-form-item label="费用类型:" prop="frType">
  38. <el-select block v-model="item.frType">
  39. <el-option v-for="items in frTypeData" :label="items.dictName" :value="items.dictValue"/>
  40. </el-select>
  41. </el-form-item>
  42. </div>
  43. <el-form-item label="费用说明:">
  44. <el-input type="textarea"
  45. v-model="item.frDesc"
  46. :autosize="{ minRows: 3, maxRows: 5 }"
  47. placeholder="请输入费用说明"
  48. />
  49. </el-form-item>
  50. <el-form-item label="电子发票:">
  51. <HcFormUpload :src="item.frElectronicInvoiceUrl"
  52. @upload="invoiceItemUpload(index)"
  53. @change="invoiceItemChange($event, index)"
  54. />
  55. </el-form-item>
  56. <el-form-item label="附件文件:">
  57. <HcFormUpload :src="item.frAttachmentUrl"
  58. @upload="fileItemUpload(index)"
  59. @change="fileItemChange($event, index)"
  60. />
  61. </el-form-item>
  62. </el-form>
  63. </HcCardItem>
  64. <div class="record-form-action-box mt-11">
  65. <el-divider content-position="right" border-style="dashed">
  66. <el-button type="primary" hc-btn @click="addDetailsData">
  67. <HcIcon name="add"/>
  68. <span>添加明细</span>
  69. </el-button>
  70. </el-divider>
  71. <el-form class="mt-16" inline :model="detailsData" label-position="top">
  72. <el-form-item label="归属人">
  73. <el-select v-model="detailsData.userIdVesting">
  74. <el-option v-for="item in userList" :label="item.name" :value="item.id"/>
  75. </el-select>
  76. </el-form-item>
  77. <el-form-item label="是否抵消借款金额">
  78. <el-select v-model="detailsData.isDeductLoan">
  79. <el-option label="否" :value="0" />
  80. <el-option label="是" :value="1" />
  81. </el-select>
  82. </el-form-item>
  83. <el-form-item label="选择借款项">
  84. <el-select v-model="detailsData.deductLoanId">
  85. <el-option v-for="item in loanListData" :label="item.loanName" :value="item.id" />
  86. </el-select>
  87. </el-form-item>
  88. <el-form-item label="冲抵后的实际报销金额">
  89. <el-input v-model="detailsData.frMoneyActual" disabled>
  90. <template #append>元</template>
  91. </el-input>
  92. </el-form-item>
  93. </el-form>
  94. </div>
  95. </el-scrollbar>
  96. </div>
  97. <div class="record-flow-box">
  98. <div class="title">流程</div>
  99. <div class="content">
  100. <el-scrollbar>
  101. <el-timeline>
  102. <el-timeline-item v-for="(item, index) in timeLineData" :key="index">
  103. <div class="timeline-title">{{item.title}}</div>
  104. <div class="timeline-section">{{item.section}}</div>
  105. </el-timeline-item>
  106. </el-timeline>
  107. </el-scrollbar>
  108. </div>
  109. <div class="action">
  110. <el-button type="warning" :loading="tempLoading" hc-btn @click="tempDraftData">
  111. <HcIcon name="draft"/>
  112. <span>暂存草稿</span>
  113. </el-button>
  114. <el-button type="primary" :loading="submitLoading" hc-btn @click="submitFormData">
  115. <HcIcon name="check-double"/>
  116. <span>提交报销申请</span>
  117. </el-button>
  118. </div>
  119. </div>
  120. </div>
  121. <!--上传控件-->
  122. <HcUploadFile ref="HcUploadFileRef"
  123. :options="uploadFileOptions"
  124. :echoParams="uploadEchoParams"
  125. @item="HcUploadFileItem"
  126. @success="HcUploadFileSuccess"
  127. />
  128. <!--关联预算计划-->
  129. <HcDialog bgColor="#ffffff" isToBody isTable
  130. title="关联预算计划" widths="80%" saveText="保存"
  131. :show="budgetModal"
  132. @close="budgetCloseClick"
  133. @save="budgetSaveClick"
  134. >
  135. <HcTable :column="tableBudgetColumn" :datas="tableBudgetData" :loading="tableBudgetLoading">
  136. <template #action="{row,index}">
  137. <el-button size="small" type="danger" @click="rowDisassociate(row)" v-if="row.isRelevance">取消关联</el-button>
  138. <el-button size="small" type="primary" @click="rowRelevance(row)" v-else>关联</el-button>
  139. </template>
  140. </HcTable>
  141. </HcDialog>
  142. </HcCard>
  143. </template>
  144. <script setup>
  145. import {onActivated, ref} from "vue";
  146. import {useRoute, useRouter} from 'vue-router'
  147. import mainApi from "~api/expense/finReimburse";
  148. import {getTokenHeader} from "~src/api/request/header";
  149. import {arrIndex, deepClone, formValidate, getArrValue, getObjValue} from "js-fast-way";
  150. import {getProjectList, getDictInfo,getuserList,getApprovesList} from "~api/other";
  151. import {delMessage} from "~uti/tools";
  152. import {useAppStore} from "~src/store";
  153. const useAppState = useAppStore();
  154. //初始变量
  155. const router = useRouter()
  156. const useRoutes = useRoute()
  157. //路由参数
  158. const dataId = ref(useRoutes?.query?.id ?? '')
  159. const dataType = ref(useRoutes?.query?.type ?? 'add') // add:新增 view:预览 draft:草稿
  160. //页面被激活
  161. onActivated(() => {
  162. dataId.value = useRoutes?.query?.id ?? ''
  163. dataType.value = useRoutes?.query?.type ?? 'add'
  164. getApi()
  165. })
  166. //请求接口
  167. const getApi = () => {
  168. //清空流程数据
  169. timeData.value=[]
  170. timeLineData.value=[]
  171. //下拉框相关数据
  172. getProjectData()
  173. expenseFrType()
  174. getLoanListData()
  175. getUserDict()
  176. getApprovesListData()
  177. //获取数据详情
  178. if (dataId.value > 0 && dataType.value !== 'add') {
  179. getDetailsData()
  180. } else {
  181. totalFrMoney.value = '0'
  182. detailsData.value = {
  183. ...detailsObj,
  184. details: [detailsObj1]
  185. }
  186. }
  187. }
  188. //获取项目数据
  189. const projectData = ref([])
  190. const getProjectData = async () => {
  191. const {error, code, data} = await getProjectList()
  192. //判断状态
  193. if (!error && code === 200) {
  194. projectData.value = getArrValue(data)
  195. } else {
  196. projectData.value = []
  197. }
  198. }
  199. //费用类型字典
  200. const frTypeData = ref([])
  201. const expenseFrType = async () => {
  202. const {error, code, data} = await getDictInfo('expense_fr_type')
  203. //判断状态
  204. if (!error && code === 200) {
  205. frTypeData.value = getArrValue(data)
  206. } else {
  207. frTypeData.value = []
  208. }
  209. }
  210. //获取我的借款列表
  211. const loanListData = ref([])
  212. const getLoanListData = async () => {
  213. const {error, code, data} = await mainApi.loanList()
  214. //判断状态
  215. if (!error && code === 200) {
  216. loanListData.value = getArrValue(data)
  217. } else {
  218. loanListData.value = []
  219. }
  220. }
  221. //获取所有员工
  222. const userList=ref([])
  223. const getUserDict=async()=>{
  224. const {error, code, data} = await getuserList({tenantId:useAppState.tenantId})
  225. if (!error && code === 200) {
  226. userList.value = getArrValue(data)
  227. } else {
  228. userList.value = []
  229. }
  230. }
  231. //基础详情
  232. const detailsObj = {
  233. frMoney: 0, // 报销金额
  234. frMoneyActual: 0, // 实际报销金额
  235. userIdVesting: null, // 归属人id
  236. isDeductLoan: 0, // 是否抵扣借款 0=否 1=是
  237. deductLoanId: null, // 借款信息id
  238. status: null, // 审批状态 0=未上报 1=待审批 2=已审批 3=已驳回
  239. //details: [detailsObj1],
  240. }
  241. //明细
  242. const detailsObj1 = {
  243. projectId: null, // 所属项目id
  244. budgetPlanIds: '', // 预算计划ids
  245. frMoney: 0, // 报销金额
  246. frDate: null, // 报销时间
  247. frDesc: '', // 费用说明
  248. frType: null, // 报销类型
  249. frElectronicInvoiceUrl: '', // 电子发票url地址
  250. frAttachmentUrl: '', // 附件url地址
  251. }
  252. //添加明细
  253. const addDetailsData = () => {
  254. detailsData.value?.details.push({})
  255. }
  256. //删除明细
  257. const delDetailsData = (index) => {
  258. delMessage(() => {
  259. detailsData.value?.details.splice(index, 1)
  260. })
  261. }
  262. //获取详情数据
  263. const totalFrMoney = ref('')
  264. const detailsData = ref({})
  265. const getDetailsData = async () => {
  266. if (dataType.value === 'view') {
  267. //预览详情
  268. let newDetails = {}, newDetails1 = {}
  269. const {error, code, data, msg} = await mainApi.detail({
  270. id: dataId.value
  271. })
  272. //判断状态
  273. if (!error && code === 200) {
  274. const res = getObjValue(data)
  275. //基础数据
  276. Object.keys(detailsObj).forEach(key => {
  277. newDetails[key] = res[key]
  278. })
  279. //明细数据
  280. Object.keys(detailsObj1).forEach(key => {
  281. newDetails1[key] = res[key]
  282. })
  283. newDetails1.id = res?.id
  284. totalFrMoney.value = res?.frMoney
  285. newDetails.details = [newDetails1]
  286. } else {
  287. newDetails = detailsObj
  288. newDetails.details = [detailsObj1]
  289. totalFrMoney.value = '0'
  290. window.$message?.error(msg)
  291. }
  292. detailsData.value = newDetails
  293. } else if (dataType.value === 'draft') {
  294. //草稿详情
  295. const {error, code, data} = await mainApi.draftDetail({
  296. eMDraftIds: dataId.value
  297. })
  298. //判断状态
  299. const res = getArrValue(data)
  300. if (!error && code === 200 && res.length > 0) {
  301. let newDetails = {}, newDetailsArr = [], frMoney = 0
  302. res.forEach((item, index) => {
  303. //基础数据
  304. if (index === 0) {
  305. Object.keys(detailsObj).forEach(key => {
  306. newDetails[key] = item[key]
  307. })
  308. }
  309. //明细数据
  310. let newDetails1 = {}
  311. Object.keys(detailsObj1).forEach(key => {
  312. newDetails1[key] = item[key]
  313. })
  314. newDetails1.id = item?.id
  315. newDetailsArr.push(newDetails1)
  316. frMoney = Number(frMoney) + Number(item?.frMoney)
  317. })
  318. newDetails.details = newDetailsArr
  319. totalFrMoney.value = frMoney
  320. detailsData.value = newDetails
  321. } else {
  322. totalFrMoney.value = '0'
  323. detailsData.value = {
  324. ...detailsObj,
  325. details: [detailsObj1]
  326. }
  327. }
  328. } else {
  329. totalFrMoney.value = '0'
  330. detailsData.value = {
  331. ...detailsObj,
  332. details: [detailsObj1]
  333. }
  334. }
  335. }
  336. //明细表单
  337. const formRefs = ref([])
  338. const setFormItemRefs = (el, index) => {
  339. if (el) {
  340. let indexs = arrIndex(formRefs.value, 'index', index)
  341. if (indexs !== -1) {
  342. formRefs.value[index].ref = el
  343. } else {
  344. formRefs.value.push({
  345. index: index,
  346. ref: el
  347. });
  348. }
  349. }
  350. }
  351. //获取表单的ref
  352. const getFormRef = async (index) => {
  353. const indexs = arrIndex(formRefs.value, 'index', index)
  354. return formRefs.value[indexs].ref
  355. }
  356. const formRules = {
  357. projectId: [{required: true, message: '请选择所属项目', trigger: 'change'}],
  358. frMoney: [{required: true, message: '请输入报销金额', trigger: 'blur'}],
  359. frDate: [{required: true, message: '请选择费用发生日期', trigger: 'change'}],
  360. frType: [{required: true, message: '请选择费用类型', trigger: 'change'}],
  361. }
  362. //关联预算计划
  363. const budgetModal = ref(false)
  364. const budgetIds = ref([])
  365. const budgetIndex = ref([])
  366. const budgetModalShow = (item, index) => {
  367. budgetModal.value = true
  368. budgetIndex.value = index
  369. budgetIds.value = item.budgetPlanIds?.split(',') || []
  370. getBudgetTableData(item.projectId)
  371. }
  372. //关联预算计划表格
  373. const tableBudgetLoading = ref(false)
  374. const tableBudgetColumn = [
  375. {key: 'projectName', name: '任务所属项目'},
  376. {key: 'status', name: '状态', width: '120', align: 'center'},
  377. {key: 'planTaskDesc', name: '任务描述'},
  378. {key: 'planTarget', name: '完成指标', width: '120', align: 'center'},
  379. {key: 'action', name: '操作', width: '100', align: 'center'},
  380. ]
  381. const tableBudgetData = ref([])
  382. const getBudgetTableData = async (projectId) => {
  383. tableBudgetLoading.value = true
  384. const {error, code, data} = await mainApi.budget({projectId})
  385. //判断状态
  386. tableBudgetLoading.value = false
  387. if (!error && code === 200) {
  388. tableBudgetData.value = await isRowRelevance(data)
  389. } else {
  390. tableBudgetData.value = []
  391. }
  392. }
  393. //判断是否关联
  394. const isRowRelevance = async (data) => {
  395. const res = getArrValue(data)
  396. const ids = budgetIds.value
  397. res.forEach(item => {
  398. item.isRelevance = ids.includes(item.id)
  399. })
  400. return res
  401. }
  402. //取消关联
  403. const rowDisassociate = (row) => {
  404. const ids = budgetIds.value
  405. const index = ids.indexOf(row.id)
  406. if (index > -1) {
  407. ids.splice(index, 1)
  408. }
  409. budgetIds.value = ids
  410. row.isRelevance = false
  411. }
  412. //关联
  413. const rowRelevance = (row) => {
  414. const ids = budgetIds.value
  415. ids.push(row.id)
  416. budgetIds.value = ids
  417. row.isRelevance = true
  418. }
  419. //确认关联预算计划
  420. const budgetSaveClick = () => {
  421. const index = budgetIndex.value
  422. const ids = budgetIds.value?.join(',')
  423. console.log('ids', ids)
  424. console.log('index', index)
  425. }
  426. //关闭预算计划
  427. const budgetCloseClick = () => {
  428. budgetModal.value = false
  429. }
  430. //上传配置
  431. const HcUploadFileRef = ref(null)
  432. const uploadEchoParams = ref({})
  433. const uploadFileOptions = {
  434. headers: getTokenHeader(),
  435. }
  436. //电子发票
  437. const invoiceItemUpload = (index) => {
  438. uploadEchoParams.value = {
  439. name: 'frElectronicInvoiceUrl',
  440. index: index
  441. }
  442. HcUploadFileRef.value?.selectFile();
  443. }
  444. //文件列表改变事件
  445. const invoiceItemChange = (src, index) => {
  446. const res = getObjValue(detailsData.value.details[index])
  447. res.frElectronicInvoiceUrl = src
  448. }
  449. //附件上传
  450. const fileItemUpload = (index) => {
  451. uploadEchoParams.value = {
  452. name: 'frAttachmentUrl',
  453. index: index
  454. }
  455. HcUploadFileRef.value?.selectFile();
  456. }
  457. //文件列表改变事件
  458. const fileItemChange = (src, index) => {
  459. const res = getObjValue(detailsData.value.details[index])
  460. res.frAttachmentUrl = src
  461. }
  462. //上传完成
  463. const HcUploadFileSuccess = ({echoParams, resData}) => {
  464. setUploadFileData(echoParams, resData)
  465. }
  466. //使用某个文件
  467. const HcUploadFileItem = ({item}) => {
  468. const {name, index} = uploadEchoParams.value, {pdfUrl} = item.resData
  469. const urls = detailsData.value.details[index][name].split(',')
  470. if (urls.indexOf(pdfUrl) === -1) {
  471. setUploadFileData(uploadEchoParams.value, item.resData)
  472. } else {
  473. window.$message?.warning('文件已存在')
  474. }
  475. }
  476. //设置上传后的数据
  477. const setUploadFileData = (echoParams, resData) => {
  478. const {name, index} = echoParams, {pdfUrl} = resData
  479. let url = detailsData.value.details[index][name]
  480. if (url) {
  481. url += `,${pdfUrl}`
  482. } else {
  483. url = pdfUrl
  484. }
  485. detailsData.value.details[index][name] = url
  486. }
  487. //流程数据
  488. const timeLineData = ref([
  489. // {title: '审批人', section: '部门负责人'},
  490. // {title: '财务审核', section: '财务'},
  491. // {title: '最终确认付款人', section: '总经理'},
  492. // {title: '出纳付款', section: '出纳'},
  493. // {title: '抄送人', section: '总经理、财务、申请人'},
  494. ])
  495. const timeData=ref([])
  496. const getApprovesListData=async()=>{
  497. const {error, code, data} = await getApprovesList()
  498. if (!error && code === 200) {
  499. console.log(data,'data');
  500. timeData.value=data['财务报销流程']
  501. console.log( timeData.value,' timeData.value');
  502. for(var i in timeData.value) {
  503. timeLineData.value.push({
  504. title:i,
  505. section:timeData.value[i].join(',')
  506. })
  507. }
  508. } else {
  509. timeLineData.value=[]
  510. }
  511. }
  512. //处理表单数据
  513. const getFormData = (submitStatus = 1) => {
  514. const res = deepClone(detailsData.value)
  515. const cashierUser = {userId: ''} //出纳人
  516. const ccUserList = [{userId: ''}] //抄送人列表
  517. const finalConfirmationUser = {userId: ''} //最终确认付款人
  518. const financeUser = {userId: ''} //财务人员
  519. const responsibleUser = {userId: ''} //部门负责人
  520. //----处理数据----
  521. let newFormData = [], newDetails = {}
  522. //基础数据
  523. Object.keys(detailsObj).forEach(key => {
  524. newDetails[key] = res[key]
  525. })
  526. //组装为数组集合的表单数据
  527. const list = res?.details
  528. list.forEach(item => {
  529. newFormData.push({
  530. ...newDetails,
  531. ...item,
  532. // cashierUser,
  533. // ccUserList,
  534. // finalConfirmationUser,
  535. // financeUser,
  536. // responsibleUser,
  537. submitStatus //提交状态 1=暂存 2=提交审批
  538. })
  539. })
  540. return newFormData
  541. }
  542. //暂存数据
  543. const tempLoading = ref(false)
  544. const tempDraftData = async () => {
  545. tempLoading.value = true
  546. const form = getFormData(1)
  547. const {error, code, msg} = await mainApi.submit(form)
  548. //判断状态
  549. tempLoading.value = false
  550. if (!error && code === 200) {
  551. window.$message?.success('暂存成功')
  552. } else {
  553. window.$message?.error(msg)
  554. }
  555. }
  556. //提交报销申请
  557. const submitLoading = ref(false)
  558. const submitFormData = async () => {
  559. submitLoading.value = true
  560. const form = getFormData(2)
  561. for (let i = 0; i < form.length; i++) {
  562. const refs = await getFormRef(i)
  563. const res = await formValidate(refs)
  564. if (!res) {
  565. submitLoading.value = false
  566. return
  567. }
  568. }
  569. //发起请求
  570. const {error, code, msg} = await mainApi.submit(form)
  571. //判断状态
  572. submitLoading.value = false
  573. if (!error && code === 200) {
  574. window.$message?.success('提交成功')
  575. router.push({name: 'expense-finReimburse'})
  576. } else {
  577. window.$message?.error(msg)
  578. }
  579. }
  580. </script>
  581. <style scoped lang="scss">
  582. @import "~src/styles/expense/expense.scoped.scss";
  583. .record-form-action-box {
  584. position: relative;
  585. }
  586. </style>
  587. <style lang="scss">
  588. @import "~src/styles/expense/expense.scss";
  589. </style>