info.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. <template>
  2. <HcCard actionUi="text-center">
  3. <template #header>
  4. <div class="hc-expense-total-title">出差天数:0.5</div>
  5. </template>
  6. <div class="hac-card-project-box">
  7. <div class="left-box">
  8. <el-scrollbar>
  9. <el-form ref="formRef" :model="formModel" :rules="formRules" label-width="auto" size="large" :disabled="dataType === 'view'">
  10. <div class="project-form-top">
  11. <el-form-item label="出差事由:" prop="tripDesc">
  12. <el-input v-model="formModel.tripDesc"/>
  13. </el-form-item>
  14. <HcCardItem :title="'行程'+ Number(index+1) " class="mt-2 hc-card-item-box" v-for="(item,index) in formModel.journeyList" ui="hac-bg-grey">
  15. <el-form :ref="(el) => setFormItemRefs(el, index)" :model="item" :rules="formItemRules" label-position="left" size="large">
  16. <el-form-item label="交通工具" prop="trafficType">
  17. <el-select v-model="item.trafficType" block clearable placeholder="请选择">
  18. <el-option v-for="item in trafficTypeData" :label="item.dictName" :value="item.dictValue"/>
  19. </el-select>
  20. </el-form-item>
  21. <el-form-item label="单程往返" prop="isSingletonType">
  22. <el-select v-model="item.isSingletonType" block clearable placeholder="请选择">
  23. <el-option v-for="item in wayType" :label="item.name" :value="item.key"/>
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item label="出发城市" prop="startCity">
  27. <el-input v-model="item.startCity" />
  28. </el-form-item>
  29. <el-form-item label="目的城市" prop="endCity">
  30. <el-input v-model="item.endCity" />
  31. </el-form-item>
  32. <el-form-item label="开始时间">
  33. <el-date-picker
  34. v-model="item.startDate"
  35. type="date"
  36. placeholder="请选择开始时间"
  37. style="width: 100%;"
  38. format="YYYY-MM-DD"
  39. value-format="YYYY-MM-DD"
  40. />
  41. </el-form-item>
  42. <el-form-item label="结束时间">
  43. <el-date-picker
  44. v-model="item.endDate"
  45. type="date"
  46. placeholder="请选择结束时间"
  47. style="width: 100%;"
  48. format="YYYY-MM-DD"
  49. value-format="YYYY-MM-DD"
  50. />
  51. </el-form-item>
  52. <el-form-item label="时长" prop="duration">
  53. <el-input v-model="item.duration" disabled :getdata="getDiffDaydata(item)"/>
  54. </el-form-item>
  55. </el-form>
  56. <span class="tip ml-8">时长将自动计入考勤统计</span>
  57. <!-- <template #extra>
  58. <el-button type="primary" size="small" @click="addJourney(item,index)">
  59. <HcIcon name="add"/>
  60. <span>增加行程</span>
  61. </el-button>
  62. <el-button type="warning" size="small" @click="delJourney(item,index)" class="ml-2">
  63. <HcIcon name="delete-bin-2"/>
  64. <span>删除行程</span>
  65. </el-button>
  66. </template> -->
  67. <template #extra>
  68. <el-button type="danger" size="small" @click="delJourney(index)" v-if="index > 0">
  69. <HcIcon name="delete-bin"/>
  70. <span>删除行程</span>
  71. </el-button>
  72. </template>
  73. </HcCardItem>
  74. <div class="mt-16" style="margin-bottom: 40px;">
  75. <el-divider content-position="right" border-style="dashed">
  76. <el-button type="primary" hc-btn @click="addJourney(item,index)">
  77. <HcIcon name="add"/>
  78. <span>添加行程</span>
  79. </el-button>
  80. </el-divider>
  81. </div>
  82. <el-form-item label="出差天数:" prop="durationAll" class="mt-4">
  83. <el-input v-model="formModel.durationAll" disabled/>
  84. </el-form-item>
  85. <el-form-item label="出差备注:" prop="remarks" >
  86. <el-input v-model="formModel.remarks" type="textarea"/>
  87. </el-form-item>
  88. <el-form-item label="同行人" prop="fellowTravelerUserIds">
  89. <el-select
  90. v-model="fellowTravelerUserIds"
  91. block clearable
  92. placeholder="请选择"
  93. style="width: 100%"
  94. multiple
  95. >
  96. <el-option v-for="item in partneroptions" :label="item.name" :value="item.id" />
  97. </el-select>
  98. </el-form-item>
  99. <el-form-item label="所属项目" prop="projectId" >
  100. <el-select v-model="formModel.projectId" block clearable placeholder="请选择">
  101. <el-option v-for="item in projectType" :label="item.projectName" :value="item.projectId"/>
  102. </el-select>
  103. </el-form-item>
  104. <el-form-item label="关联预算计划" prop="key">
  105. <el-button type="primary" size="small" @click="budgetModalShow(item,index)">
  106. <HcIcon name="add"/>
  107. <span>关联预算计划</span>
  108. </el-button>
  109. </el-form-item>
  110. </div>
  111. </el-form>
  112. </el-scrollbar>
  113. </div>
  114. <div class="right-box">
  115. <div class="record-flow-box">
  116. <div class="title">流程</div>
  117. <div class="content">
  118. <el-scrollbar>
  119. <el-timeline>
  120. <el-timeline-item v-for="(item, index) in timeLineData" :key="index">
  121. <div class="timeline-title">{{item.title}}</div>
  122. <div class="timeline-section">{{item.section}}</div>
  123. </el-timeline-item>
  124. </el-timeline>
  125. </el-scrollbar>
  126. </div>
  127. <div class="action">
  128. <el-button type="warning" hc-btn @click="tempDraftData" :loading="tempLoading">
  129. <HcIcon name="draft"/>
  130. <span>暂存草稿</span>
  131. </el-button>
  132. <el-button type="primary" hc-btn @click="saveClick" :loading="saveLoading">
  133. <HcIcon name="check-double"/>
  134. <span>提交出差申请</span>
  135. </el-button>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. <!--关联预算计划-->
  141. <HcDialog bgColor="#ffffff" isToBody isTable
  142. title="关联预算计划" widths="80%" saveText="保存"
  143. :show="budgetModal"
  144. @close="budgetCloseClick"
  145. @save="budgetSaveClick"
  146. >
  147. <HcTable :column="tableBudgetColumn" :datas="tableBudgetData" :loading="tableBudgetLoading">
  148. <template #action="{row,index}">
  149. <el-button size="small" type="danger" @click="row.relevance = false" v-if="row.relevance">取消关联</el-button>
  150. <el-button size="small" type="primary" @click="row.relevance = true" v-else>关联</el-button>
  151. </template>
  152. </HcTable>
  153. </HcDialog>
  154. </HcCard>
  155. </template>
  156. <script setup>
  157. import {ref, onActivated,watch} from "vue";
  158. import {useRoute, useRouter} from 'vue-router'
  159. import {getProjectList, getDictInfo,getuserList} from "~api/other";
  160. import {arrIndex, deepClone, formValidate, getArrValue} from "js-fast-way";
  161. import businessApi from '~api/attendance/business-trip.js';
  162. import {getDiffDay} from "~uti/tools";
  163. import {useAppStore} from "~src/store";
  164. const useAppState = useAppStore();
  165. const router = useRouter()
  166. const useRoutes = useRoute()
  167. //初始变量
  168. const dataType = ref(useRoutes?.query?.type ?? 'view')
  169. const dataId = ref(useRoutes?.query?.id ?? '')
  170. //缓存页面被激活时
  171. onActivated(() => {
  172. dataType.value = useRoutes?.query?.type ?? 'view'
  173. dataId.value = useRoutes?.query?.id ?? ''
  174. getTrafficType()
  175. getProjectData()
  176. getDetailsData()
  177. getUserDict()
  178. })
  179. const getDetailsData = async () => {
  180. if (dataType.value === 'view') {
  181. //预览详情
  182. let newDetails = {}, newDetails1 = {}
  183. const {error, code, data, msg} = await businessApi.detail({
  184. id: dataId.value
  185. })
  186. //判断状态
  187. if (!error && code === 200) {
  188. const res = getObjValue(data)
  189. //基础数据
  190. Object.keys(detailsObj).forEach(key => {
  191. newDetails[key] = res[key]
  192. })
  193. //明细数据
  194. Object.keys(detailsObj1).forEach(key => {
  195. newDetails1[key] = res[key]
  196. })
  197. newDetails1.id = res?.id
  198. newDetails.details = [newDetails1]
  199. fellowTravelerUserIds.value=res.fellowTravelerUserIds.split(',')
  200. } else {
  201. newDetails = detailsObj
  202. newDetails.details = [detailsObj1]
  203. window.$message?.error(msg)
  204. }
  205. detailsData.value = newDetails
  206. } else if (dataType.value === 'draft') {
  207. //草稿详情
  208. const {error, code, data} = await businessApi.draftDetail({
  209. eMDraftIds: dataId.value
  210. })
  211. //判断状态
  212. const res = getArrValue(data)
  213. if (!error && code === 200 && res.length > 0) {
  214. let newDetails = {}, newDetailsArr = []
  215. res.forEach((item, index) => {
  216. //基础数据
  217. if (index === 0) {
  218. Object.keys(detailsObj).forEach(key => {
  219. newDetails[key] = item[key]
  220. })
  221. }
  222. //明细数据
  223. let newDetails1 = {}
  224. Object.keys(detailsObj1).forEach(key => {
  225. newDetails1[key] = item[key]
  226. })
  227. newDetails1.id = item?.id
  228. newDetailsArr.push(newDetails1)
  229. })
  230. newDetails.journeyList = newDetailsArr
  231. fellowTravelerUserIds.value=res[0].fellowTravelerUserIds.split(',')
  232. console.log(formModel.value,'formModel.value');
  233. formModel.value = newDetails
  234. } else {
  235. formModel.value = {
  236. ...detailsObj,
  237. details: [detailsObj1]
  238. }
  239. }
  240. } else {
  241. formModel.value = {
  242. ...detailsObj,
  243. details: [detailsObj1]
  244. }
  245. }
  246. }
  247. //获取间隔天数
  248. const getDiffDaydata=(item)=>{
  249. let time= getDiffDay(item.startDate,item.endDate)
  250. item.duration=time||0
  251. }
  252. //行程表单
  253. const formItemRules = {
  254. isSingletonType: {
  255. required: true,
  256. trigger: 'blur',
  257. message: "请选择单程往返"
  258. },
  259. trafficType: {
  260. required: true,
  261. trigger: 'blur',
  262. message: "请选择交通工具"
  263. },
  264. startCity: {
  265. required: true,
  266. trigger: 'blur',
  267. message: "请选择出发城市"
  268. },
  269. endCity: {
  270. required: true,
  271. trigger: 'blur',
  272. message: "请选择目的城市"
  273. },
  274. startDate: {
  275. required: true,
  276. trigger: 'blur',
  277. message: "请选择开始时间"
  278. },
  279. endDate: {
  280. required: true,
  281. trigger: 'blur',
  282. message: "请选择结束时间"
  283. },
  284. duration: {
  285. required: true,
  286. trigger: 'blur',
  287. message: "请输入时长"
  288. },
  289. durationAll:{
  290. required: true,
  291. trigger: 'blur',
  292. message: "请输入天数"
  293. },
  294. }
  295. const formRefs = ref([])
  296. const setFormItemRefs = (el, index) => {
  297. if (el) {
  298. let indexs = arrIndex(formRefs.value, 'index', index)
  299. if (indexs !== -1) {
  300. formRefs.value[index].ref = el
  301. } else {
  302. formRefs.value.push({index: index, ref: el});
  303. }
  304. }
  305. }
  306. //获取表单的ref
  307. const getFormRef = async (index) => {
  308. const indexs = arrIndex(formRefs.value, 'index', index)
  309. return formRefs.value[indexs].ref
  310. }
  311. //增加行程addJourney
  312. const addJourney=(item,index)=>{
  313. formModel.value?.journeyList.push({})
  314. }
  315. const delJourney=(item,index)=>{
  316. console.log(index,'index');
  317. formModel?.value.journeyList.splice(index, 1)
  318. }
  319. //单程往返
  320. const wayType = ref([
  321. {name: '单程', key: 1},
  322. {name: '往返', key: 2},
  323. ])
  324. //交通工具
  325. const trafficTypeData=ref([
  326. ])
  327. //交通类型字典
  328. const getTrafficType= async () => {
  329. const {error, code, data} = await getDictInfo('traffic_type')
  330. //判断状态
  331. if (!error && code === 200) {
  332. trafficTypeData.value = getArrValue(data)
  333. } else {
  334. trafficTypeData.value = []
  335. }
  336. }
  337. //出发城市cityType
  338. const cityType=ref([
  339. {name: '北京', key: '1'},
  340. {name: '上海', key: '2'},
  341. {name: '深圳', key: '3'},
  342. {name: '成都', key: '4'},
  343. ])
  344. //项目类型
  345. const projectType = ref([
  346. ])
  347. //获取项目数据
  348. const getProjectData = async () => {
  349. const {error, code, data} = await getProjectList()
  350. //判断状态
  351. if (!error && code === 200) {
  352. projectType.value = getArrValue(data)
  353. } else {
  354. projectType.value = []
  355. }
  356. }
  357. //同行人
  358. const partneroptions=ref([])
  359. const getUserDict=async()=>{
  360. const {error, code, data} = await getuserList({tenantId:useAppState.tenantId})
  361. if (!error && code === 200) {
  362. partneroptions.value = getArrValue(data)
  363. } else {
  364. partneroptions.value = []
  365. }
  366. }
  367. //合同类型
  368. const contractType = ref([
  369. {name: '咨询服务+软件', key: '1'},
  370. {name: '软件', key: '2'},
  371. {name: '咨询服务', key: '3'},
  372. {name: '后期服务+软件', key: '4'},
  373. {name: '数字化扫描+软件', key: '5'},
  374. ])
  375. //顶部表单数据
  376. const formRef = ref(null)
  377. const formModel = ref({
  378. journeyList:[{}]
  379. })
  380. //深度监听
  381. watch(() => [
  382. formModel.value.journeyList
  383. ], ([datas]) => {
  384. let timeall=0
  385. formModel.value.journeyList.forEach((ele)=>{
  386. timeall=ele.duration+timeall
  387. })
  388. formModel.value.durationAll=timeall//计算总时长
  389. }, {deep: true})
  390. const formRules = {
  391. tripDesc: {
  392. required: true,
  393. trigger: 'blur',
  394. message: "请输入项出差事由"
  395. },
  396. projectId: {
  397. required: true,
  398. trigger: 'blur',
  399. message: "请选择项目"
  400. },
  401. }
  402. //日期时间被选择
  403. const betweenTime = ref(null)
  404. const betweenTimeUpdate = ({arr, query}) => {
  405. betweenTime.value = arr
  406. //formModel.value.betweenTime = query
  407. }
  408. //时间线数据
  409. const timeLineData = ref([
  410. {title: '审批人', section: '部门负责人'},
  411. {title: '人事审批', section: '人事'},
  412. {title: '抄送人', section: '总经理'},
  413. ])
  414. //返回
  415. const goBackClick = () => {
  416. router.back()
  417. }
  418. //提交保存
  419. const doubleClick = () => {
  420. }
  421. //关联预算计划
  422. const budgetModal = ref(false)
  423. const budgetIds = ref([])
  424. const budgetIndex = ref([])
  425. const budgetModalShow = (item, index) => {
  426. budgetModal.value = true
  427. budgetIndex.value = index
  428. // budgetIds.value = item.budgetPlanIds?.split(',') || []
  429. budgetIds.value=formModel.value.projectId
  430. getBudgetTableData(formModel.value.projectId)
  431. }
  432. const budgetCloseClick = () => {
  433. budgetModal.value = false
  434. }
  435. const budgetSaveClick = () => {
  436. }
  437. //关联预算计划表格
  438. const tableBudgetColumn = [
  439. {key: 'key1', name: '任务所属项目'},
  440. {key: 'key2', name: '状态', width: '120', align: 'center'},
  441. {key: 'key3', name: '任务描述'},
  442. {key: 'key4', name: '完成指标', width: '120', align: 'center'},
  443. {key: 'action', name: '操作', width: '100', align: 'center'},
  444. ]
  445. const tableBudgetData = ref([])
  446. const tableBudgetLoading=ref(false)
  447. const getBudgetTableData = async (projectId) => {
  448. tableBudgetLoading.value = true
  449. const {error, code, data} = await businessApi.budget({projectId})
  450. //判断状态
  451. tableBudgetLoading.value = false
  452. if (!error && code === 200) {
  453. tableBudgetData.value = await isRowRelevance(data)
  454. } else {
  455. tableBudgetData.value = []
  456. }
  457. }
  458. //行程
  459. const detailsObj1 = {
  460. trafficType: '', // 交通工具
  461. isSingletonType: '', // 单程往返
  462. startCity: '', // 出发城市
  463. endCity: '', //目的城市
  464. startDate: null, // 开始时间
  465. endDate: null, // 结束时间
  466. duration: null, // 时长
  467. //details: [detailsObj1],
  468. }
  469. //基础详情
  470. const detailsObj = {
  471. tripDesc: '', // 出差事由
  472. durationAll: 0, // 出差天数
  473. fellowTravelerUserIds: '', // 同行人
  474. remarks: '', //出差备注
  475. projectId: '', // 所属项目
  476. submitStatus: null, // 审提交状态 1=暂存 2=提交审批
  477. journeyList: [detailsObj1],
  478. }
  479. const fellowTravelerUserIds=ref([])
  480. //处理表单数据
  481. const getFormData = (submitStatus = 1) => {
  482. const res = deepClone(formModel.value)
  483. const ccUserList = [{userId: ''}] //抄送人列表
  484. const personnelUser = {userId: ''} //人事
  485. const financeUser = {userId: ''} //财务人员
  486. const responsibleUser = {userId: ''} //部门负责人
  487. //----处理数据----
  488. let newFormData = [], newDetails = {}
  489. //基础数据
  490. Object.keys(detailsObj).forEach(key => {
  491. newDetails[key] = res[key]
  492. })
  493. //组装为数组集合的表单数据
  494. const list = res?.journeyList
  495. list.forEach(item => {
  496. newFormData.push({
  497. ...newDetails,
  498. ...item,
  499. ccUserList,
  500. personnelUser,
  501. financeUser,
  502. responsibleUser,
  503. submitStatus //提交状态 1=暂存 2=提交审批
  504. })
  505. })
  506. return newFormData
  507. }
  508. const saveLoading=ref(false)
  509. const saveClick=async()=>{
  510. const res = await formValidate(formRef.value)
  511. if (!res) return false;
  512. //处理明细表单效验
  513. saveLoading.value = true
  514. const form = getFormData(2)
  515. for (let i = 0; i < form.length; i++) {
  516. const refs = await getFormRef(i)
  517. const res = await formValidate(refs)
  518. if (!res) {
  519. saveLoading.value = false
  520. return
  521. }else{
  522. form.fellowTravelerUserIds=fellowTravelerUserIds.value.join(',')
  523. console.log(form,'form');
  524. const {error, code, msg} = await businessApi.submit(form)
  525. //判断状态
  526. tempLoading.value = false
  527. if (!error && code === 200) {
  528. window.$message?.success('保存成功')
  529. router.push({
  530. name: 'attendance-business-trip',
  531. })
  532. } else {
  533. window.$message?.error(msg)
  534. }
  535. }
  536. }
  537. //发起请求
  538. }
  539. //暂存数据
  540. const tempLoading = ref(false)
  541. const tempDraftData = async () => {
  542. tempLoading.value = true
  543. const form = getFormData(1)
  544. form.forEach((ele)=>{
  545. ele.fellowTravelerUserIds=fellowTravelerUserIds.value.join(',')
  546. })
  547. console.log(form,'form');
  548. const {error, code, msg} = await businessApi.submit(form)
  549. //判断状态
  550. tempLoading.value = false
  551. if (!error && code === 200) {
  552. window.$message?.success('暂存成功')
  553. } else {
  554. window.$message?.error(msg)
  555. }
  556. }
  557. </script>
  558. <style lang="scss" scoped>
  559. .hac-card-project-box {
  560. position: relative;
  561. height: 100%;
  562. display: flex;
  563. .left-box, .right-box {
  564. // flex: 1;
  565. position: relative;
  566. height: 100%;
  567. }
  568. .left-box {
  569. flex: 2;
  570. .project-form-top {
  571. margin-bottom: 24px;
  572. padding-right: 24px;
  573. }
  574. .project-form-bottom {
  575. padding-top: 24px;
  576. padding-right: 24px;
  577. border-top: 1px solid #e9e9e9;
  578. }
  579. }
  580. .right-box {
  581. // flex: 1;
  582. padding-left: 24px;
  583. // border-left: 1px solid #e9e9e9;
  584. .record-flow-box {
  585. position: relative;
  586. height: 100%;
  587. width: 380px;
  588. padding-left: 24px;
  589. border-left: 1px solid #e9e9e9;
  590. .title {
  591. position: relative;
  592. font-size: 20px;
  593. color: #101010;
  594. font-weight: bold;
  595. margin-bottom: 20px;
  596. }
  597. .content {
  598. position: relative;
  599. height: calc(100% - 60px - 43px);
  600. .timeline-title {
  601. font-size: 20px;
  602. color: #101010;
  603. }
  604. .timeline-section {
  605. margin-top: 10px;
  606. color: #1784FC;
  607. font-size: 16px;
  608. }
  609. }
  610. .action {
  611. position: relative;
  612. padding: 10px 0;
  613. display: flex;
  614. justify-content: space-around;
  615. }
  616. }
  617. }
  618. }
  619. .tip{
  620. color: gray;
  621. font-size: 14px;
  622. }
  623. .right-box-title{
  624. font-size: 24px;
  625. font-weight: bold;
  626. margin-bottom: 40px;
  627. }
  628. .hc-expense-total-title {
  629. color: #101010;
  630. font-size: 20px;
  631. }
  632. .hac-card-project-box .left-box .hc-card-item-box.hac-bg-grey {
  633. background: #f7f7f7;
  634. }
  635. </style>
  636. <style lang="scss">
  637. </style>