appoint.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <!--
  2. * @Author: wanglj wanglijie@dashoo.cn
  3. * @Date: 2025-03-24 09:17:15
  4. * @LastEditors: wanglj wanglijie@dashoo.cn
  5. * @LastEditTime: 2025-03-28 11:44:38
  6. * @FilePath: \labsop_h5\src\view\instr\detail.vue
  7. * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  8. -->
  9. <template>
  10. <div class="container">
  11. <van-form ref="formRef" required="auto">
  12. <van-cell-group>
  13. <van-field label="预约仪器" v-model="state.form.instName" readonly :rules="[{ required: true }]" />
  14. </van-cell-group>
  15. <h4>预约时间</h4>
  16. <van-cell-group class="mt10">
  17. <van-field
  18. v-model="state.form.startTime"
  19. is-link
  20. readonly
  21. label="开始时间"
  22. placeholder="开始时间"
  23. @click="onRouterPush('/instr-calendar', { id: state.form.instId })"
  24. :rules="[{ required: true, message: '开始时间不能为空' }]"
  25. />
  26. <van-field
  27. v-model="state.form.endTime"
  28. is-link
  29. readonly
  30. label="结束时间"
  31. placeholder="结束时间"
  32. @click="onRouterPush('/instr-calendar', { id: state.form.instId })"
  33. :rules="[{ required: true, message: '结束时间不能为空' }]"
  34. />
  35. </van-cell-group>
  36. <h4>费用预估</h4>
  37. <van-cell-group class="mt10">
  38. <van-field
  39. label="预估费用"
  40. v-model="state.estimateFee"
  41. placeholder="请选择预约时间和相关信息后自动计算"
  42. readonly
  43. is-link
  44. @click="showCostDetails"
  45. />
  46. </van-cell-group>
  47. <h4>申请明细</h4>
  48. <template v-if="state.appointId == 0">
  49. <van-cell-group class="mt10" v-if="state.isActiveService">
  50. <!-- <van-field name="radio" label="课题/服务" :rules="[{ required: true }]">
  51. <template #input>
  52. <van-radio-group v-model="state.form.projectType" direction="horizontal" @change="changeProjectType">
  53. <van-radio style="margin-right: 20px" name="project">课题</van-radio>
  54. <van-radio name="service">服务</van-radio>
  55. </van-radio-group>
  56. </template>
  57. </van-field> -->
  58. <van-field
  59. v-if="state.form.projectType == 'project'"
  60. label="课题组"
  61. placeholder="课题组"
  62. @click="state.showProject = true"
  63. v-model="state.form.projectName"
  64. :rules="[{ required: true, message: '课题不能为空' }]"
  65. >
  66. </van-field>
  67. <van-field
  68. v-if="state.form.projectType == 'service'"
  69. label="服务"
  70. placeholder="服务"
  71. @click="state.shwoService = true"
  72. v-model="state.form.serviceName"
  73. :rules="[{ required: true, message: '服务不能为空' }]"
  74. >
  75. </van-field>
  76. <van-field
  77. v-if="state.form.projectType == 'project'"
  78. label="经费卡"
  79. placeholder="经费卡"
  80. is-link
  81. readonly
  82. @click="state.showExpenseCard = true"
  83. v-model="state.form.expenseCardName"
  84. ></van-field>
  85. <van-field
  86. label="预约人"
  87. placeholder="预约人"
  88. is-link
  89. readonly
  90. @click="openSelectUser"
  91. v-model="state.form.nickName"
  92. :rules="[{ required: true, message: '预约人不能为空' }]"
  93. ></van-field>
  94. <van-field
  95. label="联系电话"
  96. placeholder="联系电话"
  97. v-model="state.form.userContact"
  98. :rules="[{ required: true, message: '联系电话不能为空' }]"
  99. ></van-field>
  100. <van-field name="assistEnable" label="辅助上机" >
  101. <template #input>
  102. <van-radio-group v-model="state.form.assistEnable" direction="horizontal">
  103. <van-radio style="margin-right: 20px" :name="false">否</van-radio>
  104. <van-radio :name="true">是</van-radio>
  105. </van-radio-group>
  106. </template>
  107. </van-field>
  108. <van-field label="备注" placeholder="备注" v-model="state.form.remark" rows="2" autosize type="textarea" maxlength="300" show-word-limit></van-field>
  109. </van-cell-group>
  110. </template>
  111. <CustomForm ref="customFormRef" :formData="state.form.createForm"></CustomForm>
  112. </van-form>
  113. </div>
  114. <van-action-bar placeholder>
  115. <van-action-bar-button class="w100" type="primary" text="提交" @click="onClickButton" />
  116. </van-action-bar>
  117. <!-- 选择服务 -->
  118. <van-popup v-model:show="state.shwoService" position="bottom">
  119. <van-picker :columns="serviceList" :columns-field-names="{ text: 'name', value: 'id' }" @confirm="pickService" @cancel="state.shwoService = false" />
  120. </van-popup>
  121. <!-- 选择课题 -->
  122. <van-popup v-model:show="state.showProject" position="bottom">
  123. <van-picker
  124. :columns="projectList"
  125. :columns-field-names="{ text: 'projectName', value: 'projectId' }"
  126. @confirm="pickProject"
  127. @cancel="state.showProject = false"
  128. />
  129. </van-popup>
  130. <!-- 选择经费卡 -->
  131. <van-popup v-model:show="state.showExpenseCard" position="bottom">
  132. <van-picker
  133. :columns="fundsList"
  134. :columns-field-names="{ text: 'finAccount', value: 'id' }"
  135. @confirm="pickExpenseCard"
  136. @cancel="state.showExpenseCard = false"
  137. />
  138. </van-popup>
  139. <!-- 选择预约人 -->
  140. <van-popup v-model:show="state.showAppointUser" position="bottom">
  141. <van-picker
  142. :columns="userList"
  143. :columns-field-names="{ text: 'nickName', value: 'id' }"
  144. @confirm="pickAppointUser"
  145. @cancel="state.showAppointUser = false"
  146. />
  147. </van-popup>
  148. <AppointDialog ref="appointDialogRef" />
  149. </template>
  150. <script lang="ts" setup>
  151. import to from 'await-to-js'
  152. import { useRoute, useRouter } from 'vue-router'
  153. import { useInstrApi } from '/@/api/instr'
  154. import { useInstDocApi } from '/@/api/instr/document'
  155. import { defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
  156. import { formatDate } from '/@/utils/formatTime'
  157. import { showNotify, showDialog } from 'vant'
  158. import download from 'downloadjs'
  159. import { useNoticeApi } from '/@/api/instr/notice'
  160. import { useProApi } from '/@/api/project'
  161. import technicalApi from '/@/api/technical/index'
  162. import { Local } from '/@/utils/storage'
  163. import { useUserApi } from '/@/api/system/user'
  164. import { useUserInfo } from '/@/stores/userInfo'
  165. import { storeToRefs } from 'pinia'
  166. import instAppoint from '/@/api/instr/instAppoint'
  167. import { useConfigApi } from '/@/api/system/config'
  168. const CustomForm = defineAsyncComponent(() => import('/@/components/CustomForm.vue'))
  169. const storesUseUserInfo = useUserInfo()
  170. const { userInfos } = storeToRefs(storesUseUserInfo)
  171. const route = useRoute()
  172. const router = useRouter()
  173. const projApi = useProApi()
  174. const instApi = useInstrApi()
  175. const userApi = useUserApi()
  176. const configApi = useConfigApi()
  177. const serviceList = ref([])
  178. const projectList = ref([])
  179. const fundsList = ref([])
  180. const userList = ref([])
  181. const appointDialogRef = ref()
  182. const formRef = ref()
  183. const customFormRef = ref()
  184. const state = reactive({
  185. loading: false,
  186. appointId: 0,
  187. isActiveService: false,
  188. showProject: false,
  189. shwoService: false,
  190. showExpenseCard: false,
  191. showAppointUser: false,
  192. isInstrHead: false,
  193. instDetail: {} as any,
  194. estimateFee: '', // 预估费用
  195. costDetails: [] as any[], // 费用明细
  196. form: {
  197. instId: 0,
  198. instName: '',
  199. startTime: '',
  200. endTime: null,
  201. projectName: null,
  202. projectId: null,
  203. serviceId: null,
  204. serviceName: null,
  205. expenseCardId: 0,
  206. expenseCardName: '',
  207. userContact: '',
  208. userId: 0,
  209. nickName: '',
  210. projectType: '',
  211. assistEnable: false, // 是否辅助上机,默认为false(否)
  212. createForm: [],
  213. remark: ''
  214. }
  215. })
  216. // 自定义验证函数:验证是否辅助上机
  217. const validateAssistEnable = (value) => {
  218. // 对于布尔值,只要不是undefined就是有效值
  219. return value !== undefined
  220. }
  221. // 获取预估费用
  222. const getEstimateFee = async () => {
  223. // 必须有开始时间、结束时间和仪器ID才能计算预估费用
  224. if (!state.form.instId || !state.form.startTime || !state.form.endTime) {
  225. state.estimateFee = ''
  226. return
  227. }
  228. // 使用原始格式,不进行时间格式转换
  229. const params = {
  230. instId: state.form.instId,
  231. startTime: state.form.startTime,
  232. endTime: state.form.endTime,
  233. projectId: state.form.projectId || 0,
  234. expenseCardId: state.form.expenseCardId || 0,
  235. assistEnable: state.form.assistEnable
  236. }
  237. const [err, res]: ToResponse = await to(instApi.getEstimateFee(params))
  238. if (err) {
  239. state.estimateFee = '计算失败'
  240. return
  241. }
  242. if (res?.code === 200 && res.data) {
  243. // 根据返回格式获取预估费用
  244. const cost = res.data.estimatedCost || res.data
  245. state.estimateFee = `¥${cost}`
  246. // 保存费用明细数据
  247. if (res.data.costDetails && res.data.costDetails.length > 0) {
  248. state.costDetails = res.data.costDetails
  249. } else {
  250. state.costDetails = []
  251. }
  252. } else {
  253. state.estimateFee = '无法计算'
  254. }
  255. }
  256. // 选择课题还是服务
  257. const changeProjectType = () => {
  258. state.form.serviceId = 0
  259. state.form.serviceName = ''
  260. state.form.projectId = 0
  261. state.form.projectName = ''
  262. state.form.expenseCardId = 0
  263. state.form.expenseCardName = ''
  264. fundsList.value = []
  265. getEstimateFee() // 重新计算预估费用
  266. }
  267. const pickProject = ({ selectedOptions }) => {
  268. state.form.projectId = selectedOptions[0].projectId
  269. state.form.projectName = selectedOptions[0].projectName
  270. state.showProject = false
  271. getFundsData()
  272. getEstimateFee() // 重新计算预估费用
  273. }
  274. // 选择服务
  275. const pickService = ({ selectedOptions }) => {
  276. state.form.serviceId = selectedOptions[0].id
  277. state.form.serviceName = selectedOptions[0].name
  278. state.shwoService = false
  279. getEstimateFee() // 重新计算预估费用
  280. }
  281. // 经费卡选择
  282. const pickExpenseCard = ({ selectedOptions }) => {
  283. state.form.expenseCardId = selectedOptions[0].id
  284. state.form.expenseCardName = selectedOptions[0].finAccount
  285. state.showExpenseCard = false
  286. getEstimateFee() // 重新计算预估费用
  287. }
  288. const getFundsData = async () => {
  289. const [err, res]: ToResponse = await to(projApi.getFinanceAccountList({ projId: state.form.projectId }))
  290. if (err) return
  291. fundsList.value = res?.data.list ? [res?.data.list] : []
  292. if (fundsList.value && fundsList.value.length > 0 && fundsList.value[0].length > 0) {
  293. state.form.expenseCardId = fundsList.value[0][0].id
  294. state.form.expenseCardName = fundsList.value[0][0].finAccount
  295. }
  296. getEstimateFee() // 重新计算预估费用
  297. }
  298. // 预约人选择
  299. const pickAppointUser = ({ selectedOptions }) => {
  300. state.form.nickName = selectedOptions[0].nickName
  301. state.form.userId = selectedOptions[0].id
  302. state.form.userContact = selectedOptions[0].phone
  303. state.showAppointUser = false
  304. state.form.serviceId = 0
  305. state.form.serviceName = ''
  306. state.form.projectId = 0
  307. state.form.projectName = ''
  308. state.form.expenseCardId = 0
  309. state.form.expenseCardName = ''
  310. fundsList.value = []
  311. getMyProjectInfo(state.form.userId)
  312. getEstimateFee() // 重新计算预估费用
  313. }
  314. // 选择预约人
  315. const openSelectUser = () => {
  316. if (!state.isInstrHead) return
  317. state.showAppointUser = true
  318. }
  319. const init = async () => {
  320. //延长预约会传一个预约id 有预约id 获取预约详情
  321. const [err, res]: ToResponse = await to(configApi.getEntityMapByKey({ configKey: 'instr_is_activate_service' }))
  322. if (err) return
  323. state.isActiveService = res.data?.configValue == '10' ? true : false
  324. state.form.projectType = 'project'
  325. state.form.userId = userInfos.value.id || 0
  326. state.form.nickName = userInfos.value.nickName || ''
  327. state.form.userContact = userInfos.value.phone || ''
  328. getMyProjectInfo()
  329. getUserService()
  330. getUserList()
  331. getInstrDetails()
  332. getAppointConfig()
  333. }
  334. const getInstrDetails = async () => {
  335. const [err, res]: ToResponse = await to(instApi.getDetail({ id: state.form.instId }))
  336. if (err) return
  337. if (res?.code === 200) {
  338. state.instDetail = res.data
  339. state.form.instName = state.instDetail.instName
  340. const userInfo = storesUseUserInfo.userInfos
  341. state.isInstrHead = userInfo.id ? res.data.instHeadId.split(',').includes('' + userInfo?.id) : false
  342. }
  343. }
  344. // 获取用户下的服务
  345. const getUserService = async () => {
  346. const [err, res]: ToResponse = await to(technicalApi.getList({ noPage: true }))
  347. if (err) return
  348. serviceList.value = [res?.data.list]
  349. if (state.form.projectType == 'service') {
  350. state.form.serviceId = res?.data.list[0].id || 0
  351. state.form.serviceName = res?.data.list[0].name || ''
  352. }
  353. }
  354. // 获取用户相关的课题组
  355. const getMyProjectInfo = async (id?: number) => {
  356. let params = {}
  357. if (id) {
  358. params = { id }
  359. } else {
  360. params = {}
  361. }
  362. const [err, res]: ToResponse = await to(projApi.getMySelfProjectGroup(params))
  363. if (err) return
  364. // state.form.projectName = res?.data.pgName || ''
  365. // state.form.projectId = res?.data.id || null
  366. projectList.value = [{ projectName: res?.data.pgName || '', projectId: res?.data.id || null }]
  367. if (state.form.projectType == 'project') {
  368. state.form.projectId = res?.data.id || null
  369. state.form.projectName = res?.data.pgName || ''
  370. getFundsData()
  371. } else {
  372. getEstimateFee() // 重新计算预估费用
  373. }
  374. }
  375. const getUserList = async () => {
  376. const [err, res]: ToResponse = await to(userApi.getUserList({ noPage: true }))
  377. if (err) return
  378. userList.value = [res?.data.list]
  379. }
  380. // 预约配置信息
  381. const getAppointConfig = async () => {
  382. const params = {
  383. instId: state.form.instId,
  384. code: 'InstCfgAppoint'
  385. }
  386. const [err, res]: ToResponse = await to(instApi.getSettingDetail({ ...params }))
  387. if (err) return
  388. state.form.createForm = res?.data?.config.createForm ? JSON.parse(res.data.config.createForm) : []
  389. }
  390. const onRouterPush = (val: string, params?: any) => {
  391. router.push({
  392. path: val,
  393. query: { ...params }
  394. })
  395. // 如果是跳转到日历页面,返回后重新计算预估费用
  396. if (val === '/instr-calendar') {
  397. // 使用setTimeout确保从日历页面返回后再计算
  398. setTimeout(() => {
  399. if (state.form.startTime && state.form.endTime) {
  400. getEstimateFee()
  401. }
  402. }, 1000)
  403. }
  404. }
  405. // 展示费用明细
  406. const showCostDetails = () => {
  407. if (state.costDetails.length === 0) {
  408. showNotify({
  409. type: 'warning',
  410. message: '暂无费用明细'
  411. })
  412. return
  413. }
  414. let detailsHtml = '<div class="cost-details-content">'
  415. state.costDetails.forEach((item, index) => {
  416. detailsHtml += `<div class="cost-item">`
  417. detailsHtml += `<div class="cost-item-name">${item.itemName || ''}</div>`
  418. detailsHtml += `<div class="cost-item-amount">¥${item.amount || 0}</div>`
  419. detailsHtml += `</div>`
  420. })
  421. detailsHtml += '</div>'
  422. showDialog({
  423. title: '费用明细',
  424. message: detailsHtml,
  425. className: 'cost-details-dialog',
  426. allowHtml: true,
  427. showConfirmButton: true,
  428. confirmButtonText: '确定'
  429. })
  430. }
  431. const onClickButton = async () => {
  432. state.loading = true
  433. const [errValid] = await to(formRef.value.validate())
  434. const customForm = customFormRef.value.getFormData()
  435. if (errValid || (state.form.createForm.length && !customForm)) {
  436. state.loading = false
  437. return
  438. }
  439. const params = JSON.parse(JSON.stringify(state.form))
  440. params.userName = params.nickName
  441. params.sampleForm = JSON.stringify(customForm)
  442. delete params.createForm
  443. const [err]: ToResponse = await to(instAppoint.add(params))
  444. if (err) {
  445. state.loading = false
  446. return
  447. }
  448. showNotify({
  449. type: 'success',
  450. message: '预约成功'
  451. })
  452. router.push({
  453. path: '/instr-detail',
  454. query: {
  455. id: params.instId
  456. }
  457. })
  458. }
  459. onMounted(() => {
  460. const id = route.query.id ? +route.query.id : 0
  461. const startTime = route.query.startTime ? formatDate(new Date(+route.query.startTime), 'YYYY-mm-dd HH:MM') : ''
  462. const endTime = route.query.endTime ? formatDate(new Date(+route.query.endTime), 'YYYY-mm-dd HH:MM') : ''
  463. state.form.instId = id
  464. state.form.startTime = startTime
  465. state.form.endTime = endTime
  466. init()
  467. // 如果有开始时间和结束时间,初始化后计算预估费用
  468. if (startTime && endTime) {
  469. setTimeout(() => {
  470. getEstimateFee()
  471. }, 500) // 延迟执行,确保其他初始化完成
  472. }
  473. })
  474. // 监听开始时间和结束时间变化,重新计算预估费用
  475. watch(
  476. () => [state.form.startTime, state.form.endTime],
  477. () => {
  478. if (state.form.startTime && state.form.endTime) {
  479. getEstimateFee()
  480. }
  481. }
  482. )
  483. // 监听辅助上机选项变化,重新计算预估费用
  484. watch(
  485. () => state.form.assistEnable,
  486. () => {
  487. if (state.form.startTime && state.form.endTime) {
  488. getEstimateFee()
  489. }
  490. }
  491. )
  492. </script>
  493. <style lang="scss" scoped>
  494. .container {
  495. flex: 1;
  496. padding: 10px;
  497. background-color: #f9f9f9;
  498. overflow-y: auto;
  499. h4 {
  500. height: 18px;
  501. line-height: 18px;
  502. display: flex;
  503. margin: 10px 0;
  504. span {
  505. font-weight: normal;
  506. margin-left: auto;
  507. }
  508. &::before {
  509. display: inline-block;
  510. content: '';
  511. width: 3px;
  512. height: 18px;
  513. background-color: #1c9bfd;
  514. margin-right: 4px;
  515. vertical-align: middle;
  516. }
  517. }
  518. }
  519. </style>
  520. <style lang="scss">
  521. // 费用明细对话框样式
  522. .cost-details-dialog {
  523. .van-dialog__content {
  524. max-height: 60vh;
  525. overflow-y: auto;
  526. }
  527. .cost-details-content {
  528. padding: 16px;
  529. .cost-item {
  530. display: flex;
  531. justify-content: space-between;
  532. align-items: center;
  533. padding: 12px 0;
  534. border-bottom: 1px solid #eee;
  535. &:last-child {
  536. border-bottom: none;
  537. }
  538. .cost-item-name {
  539. font-size: 14px;
  540. color: #333;
  541. }
  542. .cost-item-amount {
  543. font-size: 16px;
  544. font-weight: bold;
  545. color: #f56c6c;
  546. }
  547. }
  548. }
  549. }
  550. </style>