index.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. <template>
  2. <el-dialog v-model="visible" title="借用申请单" width="75%" :close-on-click-modal="false" @close="onCancel">
  3. <el-form :model="form" label-width="110px" label-position="top" :rules="rules" ref="formRef">
  4. <el-row :gutter="16">
  5. <el-col :span="8">
  6. <el-form-item label="借用单编号" prop="application_no">
  7. <el-input v-model="form.application_no" placeholder="自动生成" disabled />
  8. </el-form-item>
  9. </el-col>
  10. <el-col :span="8">
  11. <el-form-item label="借用人类型" prop="borrower_type">
  12. <el-select v-model="form.borrower_type" placeholder="请选择借用人角色" >
  13. <el-option label="管理员用户" :value=0 />
  14. <el-option label="应用用户" :value=1 />
  15. <el-option label="外部用户" :value=2 />
  16. </el-select>
  17. </el-form-item>
  18. </el-col>
  19. <el-col :span="8" >
  20. <el-form-item label="借用人" prop="admin_borrower" v-if="form.borrower_type == 0">
  21. <el-select
  22. v-model="form.admin_borrower"
  23. placeholder="请选择或搜索管理员"
  24. filterable
  25. allow-create
  26. :filter-method="filterAdmins"
  27. :loading="adminSearchLoading"
  28. @create="createNewAdmin"
  29. @focus="onAdminSelectFocus"
  30. clearable
  31. style="width: 100%"
  32. >
  33. <el-option
  34. v-for="item in filteredAdminList"
  35. :key="item.id"
  36. :label="item.name"
  37. :value="item.id"
  38. />
  39. </el-select>
  40. </el-form-item>
  41. <el-form-item label="借用人" prop="app_user_borrower" v-if="form.borrower_type == 1">
  42. <el-select
  43. v-model="form.app_user_borrower"
  44. placeholder="请选择或搜索借用人"
  45. filterable
  46. allow-create
  47. :filter-method="filterUsers"
  48. :loading="userSearchLoading"
  49. @create="createNewUser"
  50. @focus="onUserSelectFocus"
  51. clearable
  52. style="width: 100%"
  53. >
  54. <el-option
  55. v-for="item in filteredUserList"
  56. :key="item.id"
  57. :label="item.name"
  58. :value="item.id"
  59. />
  60. </el-select>
  61. </el-form-item>
  62. <el-form-item label="借用人" prop="external_borrower_name" v-if="form.borrower_type == 2">
  63. <el-input v-model="form.external_borrower_name" placeholder="请输入借用人" />
  64. </el-form-item>
  65. </el-col>
  66. <el-col :span="8" v-if="form.borrower_type !== 2">
  67. <el-form-item label="学号/工号" prop="user_code" >
  68. <el-input v-model="form.user_code" placeholder="请输入学号/工号" :disabled="isView" @blur="onUserCodeBlur"/>
  69. </el-form-item>
  70. </el-col>
  71. <el-col :span="8">
  72. <el-form-item label="联系电话" prop="mobile" v-if="form.borrower_type !== 2">
  73. <el-input v-model="form.mobile" placeholder="请输入联系电话" :disabled="isView" />
  74. </el-form-item>
  75. <el-form-item label="联系电话" prop="external_borrower_phone" v-if="form.borrower_type == 2">
  76. <el-input v-model="form.external_borrower_phone" placeholder="请输入联系电话" :disabled="isView"/>
  77. </el-form-item>
  78. </el-col>
  79. <el-col :span="8">
  80. <el-form-item label="借用部门" prop="borrower_dept">
  81. <el-input v-model="form.borrower_dept" placeholder="请输入借用部门" :disabled="isView" />
  82. </el-form-item>
  83. </el-col>
  84. <el-col :span="8">
  85. <el-form-item label="预计归还时间" prop="expected_end_time">
  86. <el-date-picker v-model="form.expected_end_time" type="date"
  87. format="YYYY-MM-DD"
  88. value-format="YYYY-MM-DD"
  89. :disabledDate="disabledDate" placeholder="请选择预计归还时间" style="width: 100%" :disabled="isView"/>
  90. </el-form-item>
  91. </el-col>
  92. <el-col :span="24" v-if="!isView">
  93. <el-alert
  94. v-if="matchedWorkflowName"
  95. type="info"
  96. :closable="false"
  97. show-icon
  98. >
  99. <template #title>将使用审批流程:{{ matchedWorkflowName }}</template>
  100. </el-alert>
  101. <el-alert
  102. v-else-if="workflowsLoadDone && form.expected_end_time"
  103. type="warning"
  104. :closable="false"
  105. show-icon
  106. title="当前条件未匹配到启用中的审批流程,提交后将由后台默认规则处理;请检查流程管理中的天数区间、借用类型(含特殊借用)等配置"
  107. />
  108. </el-col>
  109. <el-col :span="4">
  110. <el-form-item label="个人/团队" prop="team_type" >
  111. <el-select v-model="form.team_type" placeholder="请选择" :disabled="isView">
  112. <el-option label="个人" :value="0" />
  113. <el-option label="团队" :value="1" />
  114. </el-select>
  115. </el-form-item>
  116. </el-col>
  117. <el-col :span="4" v-show="form.team_type===1">
  118. <el-form-item label="团队人数" prop="team_members_count">
  119. <el-input v-model="form.team_members_count" placeholder="请输入" :disabled="isView"/>
  120. </el-form-item>
  121. </el-col>
  122. <el-col :span="16" v-show="form.team_type===1">
  123. <el-form-item label="团队其他人员名单" prop="team_members">
  124. <el-input v-model="form.team_members" placeholder="请输入团队其他人员名单" :disabled="isView"/>
  125. </el-form-item>
  126. </el-col>
  127. <el-col :span="24">
  128. <el-form-item label="借用原因" prop="purpose">
  129. <el-input v-model="form.purpose" type="textarea" placeholder="请输入借用原因" :disabled="isView" />
  130. </el-form-item>
  131. </el-col>
  132. <el-col :span="4">
  133. <el-form-item label="审批人" prop="applicant_remark_type">
  134. <el-select v-model="form.applicant_remark_type" placeholder="请选择" :disabled="isView" >
  135. <el-option label="管理员" value="1"/>
  136. </el-select>
  137. </el-form-item>
  138. </el-col>
  139. <el-col :span="20">
  140. <el-form-item label="备注" prop="applicant_remark">
  141. <el-input v-model="form.applicant_remark" placeholder="请输入备注" :disabled="isView"/>
  142. </el-form-item>
  143. </el-col>
  144. </el-row>
  145. <div style="margin: 16px 0 8px 0; font-weight: bold">借用列表</div>
  146. <!-- {{ isEdit }} -->
  147. <el-table :data="!isEdit ? form.items : form.items.filter(item => item.device === null)" border style="width: 50%; margin-bottom: 12px">
  148. <el-table-column type="index" label="序号" width="60" />
  149. <el-table-column prop="device_category_name" label="设备分类" />
  150. <!-- <el-table-column prop="quantity" label="借用数量" /> -->
  151. <el-table-column label="借用数量">
  152. <template #default="{ row }">
  153. <el-input v-model="row.quantity" type="number" :min="1" prop="quantity" size="small" :disabled="isView" />
  154. </template>
  155. </el-table-column>
  156. <!-- <el-table-column prop="location" label="存放仓库" /> -->
  157. <el-table-column label="操作" width="80">
  158. <template #default="{ $index }">
  159. <el-button type="text" style="color: red" @click="removeItem($index)" :disabled="isView">移除</el-button>
  160. </template>
  161. </el-table-column>
  162. </el-table>
  163. <el-button type="primary" @click="addItem" v-if="!isView">选择设备分类</el-button>
  164. <div style="margin: 16px 0 8px 0; font-weight: bold">预借配件信息</div>
  165. <el-input v-model="form.accessories" placeholder="请输入预借配件信息" :disabled="isView"/>
  166. <div style="margin: 16px 0 8px 0; font-weight: bold">附件</div>
  167. <div>
  168. <el-upload
  169. list-type="picture-card"
  170. :show-file-list="true"
  171. :http-request="handleCustomUpload"
  172. :file-list="fileList"
  173. :on-preview="handlePictureCardPreview"
  174. style="margin-top: 10px;"
  175. >
  176. <el-icon><Plus /></el-icon>
  177. </el-upload>
  178. </div>
  179. </el-form>
  180. <!-- 审批信息流程展示 -->
  181. <div v-if="isView" style="margin-top: 24px; padding: 0 12px">
  182. <div style="font-weight: bold; margin-bottom: 12px">审批流程进度</div>
  183. <el-steps :active="steps.length" align-center>
  184. <el-step
  185. v-for="(step, index) in steps"
  186. :key="index"
  187. :title="step.title"
  188. :description="step.description"
  189. icon="CircleCheck"
  190. :status="step.status"
  191. />
  192. </el-steps>
  193. </div>
  194. <template #footer>
  195. <el-button @click="onCancel">关闭</el-button>
  196. <el-button type="primary" @click="onSubmit" v-if="!isView">提交</el-button>
  197. </template>
  198. </el-dialog>
  199. <el-dialog v-model="dialogVisible">
  200. <img w-full :src="dialogImageUrl" alt="Preview Image" />
  201. </el-dialog>
  202. <!-- <SelectDeviceDialog v-model:visible="showSelectDeviceDialog" @confirm="onDeviceSelected" /> -->
  203. <SelectCatgory
  204. v-model:visible="showSelectDeviceDialog"
  205. @confirm="onDeviceSelected"
  206. :disabled-ids="form.items.map((item) => item.device_category).filter(Boolean)"
  207. />
  208. </template>
  209. <script setup lang="ts">
  210. import { ref, watch, onMounted, computed } from 'vue';
  211. import SelectDeviceDialog from './SelectDeviceDialog/index.vue';
  212. import dayjs from 'dayjs';
  213. import { ElMessage ,UploadProps, UploadUserFile } from 'element-plus';
  214. import * as api from '../../api';
  215. import Cookies from 'js-cookie';
  216. import { request } from '/@/utils/service';
  217. import { Plus } from '@element-plus/icons-vue';
  218. import SelectCatgory from '../SelectCatgory/index.vue';
  219. import { getUserInfo } from '../../../login/api';
  220. import * as workflowApi from '../../workflow/api';
  221. import {
  222. pickWorkflowIdForSpecialBorrow,
  223. findWorkflowNameById,
  224. type SpecialBorrowMatchInput,
  225. } from '../../utils/matchBorrowWorkflow';
  226. // 定义用户和管理员接口
  227. interface User {
  228. id: number | string;
  229. name: string;
  230. user_code?: string;
  231. mobile?: string;
  232. isNew?: boolean;
  233. }
  234. interface Admin {
  235. id: number | string;
  236. name: string;
  237. isNew?: boolean;
  238. }
  239. const dialogImageUrl = ref('')
  240. const dialogVisible = ref(false)
  241. const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
  242. dialogImageUrl.value = uploadFile.url!
  243. dialogVisible.value = true
  244. }
  245. // 禁止选择今天之前的日期
  246. const disabledDate = (time: Date) => {
  247. // 获取今天的开始时间(准确到日)
  248. const today = new Date()
  249. today.setHours(0, 0, 0, 0)
  250. // 返回true表示禁用,禁用今天之前的所有日期
  251. return time.getTime() < today.getTime()
  252. }
  253. /* 获取管理员列表 */
  254. const adminList = ref<Admin[]>([]);
  255. const filteredAdminList = ref<Admin[]>([]);
  256. const adminSearchLoading = ref(false);
  257. const adminSearchQuery = ref('');
  258. async function getAdminList() {
  259. adminSearchLoading.value = true;
  260. try {
  261. const res = await getUserInfo();
  262. adminList.value = res.data;
  263. filteredAdminList.value = res.data;
  264. console.log("adminList.value:::",adminList.value)
  265. } finally {
  266. adminSearchLoading.value = false;
  267. }
  268. }
  269. // 管理员搜索过滤方法
  270. function filterAdmins(query: string) {
  271. adminSearchQuery.value = query;
  272. if (!query) {
  273. filteredAdminList.value = adminList.value;
  274. return;
  275. }
  276. // 根据姓名进行模糊搜索
  277. filteredAdminList.value = adminList.value.filter(admin =>
  278. admin.name.toLowerCase().includes(query.toLowerCase())
  279. );
  280. }
  281. // 创建新管理员
  282. function createNewAdmin(query: string) {
  283. // 检查是否已存在相同姓名的管理员
  284. const existingAdmin = adminList.value.find(admin =>
  285. admin.name.toLowerCase() === query.toLowerCase()
  286. );
  287. if (existingAdmin) {
  288. ElMessage.warning('该管理员已存在,请选择现有管理员');
  289. form.value.admin_borrower = Number(existingAdmin.id);
  290. return;
  291. }
  292. // 创建新管理员对象
  293. const newAdmin: Admin = {
  294. id: `new_admin_${Date.now()}`, // 临时ID
  295. name: query,
  296. isNew: true // 标记为新创建的管理员
  297. };
  298. // 添加到管理员列表
  299. adminList.value.unshift(newAdmin);
  300. filteredAdminList.value = adminList.value;
  301. form.value.admin_borrower = Number(newAdmin.id);
  302. ElMessage.success(`已创建新管理员: ${query}`);
  303. }
  304. // 管理员选择框获得焦点时的处理
  305. function onAdminSelectFocus() {
  306. if (!adminSearchQuery.value) {
  307. filteredAdminList.value = adminList.value;
  308. }
  309. }
  310. /* */
  311. /* 获取所有用户 */
  312. const allUserList = ref<User[]>([]);
  313. const filteredUserList = ref<User[]>([]);
  314. const userSearchLoading = ref(false);
  315. const userSearchQuery = ref('');
  316. async function getAllUserList() {
  317. userSearchLoading.value = true;
  318. try {
  319. const res = await api.GetAllUser({page:1,limit:999});
  320. allUserList.value = res.data;
  321. filteredUserList.value = res.data;
  322. // 设置默认选择第一个用户
  323. /* if (allUserList.value.length > 0) {
  324. form.value.app_user_borrower = allUserList.value[0].id;
  325. } */
  326. console.log("allUserList.value:::",allUserList.value)
  327. } finally {
  328. userSearchLoading.value = false;
  329. }
  330. }
  331. // 用户搜索过滤方法
  332. function filterUsers(query: string) {
  333. userSearchQuery.value = query;
  334. if (!query) {
  335. filteredUserList.value = allUserList.value;
  336. return;
  337. }
  338. // 根据姓名进行模糊搜索
  339. filteredUserList.value = allUserList.value.filter(user =>
  340. user.name.toLowerCase().includes(query.toLowerCase()) ||
  341. (user.user_code && user.user_code.toLowerCase().includes(query.toLowerCase()))
  342. );
  343. }
  344. // 创建新用户
  345. function createNewUser(query: string) {
  346. // 检查是否已存在相同姓名的用户
  347. const existingUser = allUserList.value.find(user =>
  348. user.name.toLowerCase() === query.toLowerCase()
  349. );
  350. if (existingUser) {
  351. ElMessage.warning('该用户已存在,请选择现有用户');
  352. form.value.app_user_borrower = Number(existingUser.id);
  353. return;
  354. }
  355. // 创建新用户对象
  356. const newUser: User = {
  357. id: `new_${Date.now()}`, // 临时ID,实际应该调用API创建
  358. name: query,
  359. user_code: '',
  360. mobile: '',
  361. isNew: true // 标记为新创建的用户
  362. };
  363. // 添加到用户列表
  364. allUserList.value.unshift(newUser);
  365. filteredUserList.value = allUserList.value;
  366. form.value.app_user_borrower = Number(newUser.id);
  367. ElMessage.success(`已创建新用户: ${query}`);
  368. }
  369. // 用户选择框获得焦点时的处理
  370. function onUserSelectFocus() {
  371. if (!userSearchQuery.value) {
  372. filteredUserList.value = allUserList.value;
  373. }
  374. }
  375. const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
  376. const props = defineProps<{ visible: boolean; modelValue?: any }>();
  377. const isEdit = computed(() => props.modelValue?.mode === 'edit');
  378. const isView = computed(() => props.modelValue?.mode === 'view');
  379. const isAdd = computed(() => props.modelValue?.mode === 'add');
  380. const emit = defineEmits(['update:visible', 'update:modelValue', 'submit']);
  381. const visible = ref(props.visible);
  382. const formRef = ref();
  383. const fileList = ref([]);
  384. // const fileList = ref([]);
  385. const handleCustomUpload = async ({ file, onProgress, onSuccess, onError }: any) => {
  386. const token = Cookies.get('token');
  387. const formData = new FormData();
  388. formData.append('file', file);
  389. // console.log("token::::",token);
  390. try {
  391. const res = await request({
  392. url: `/api/system/upload/`,
  393. method: 'post',
  394. headers: {
  395. 'Content-Type': 'multipart/form-data',
  396. Authorization: 'JWT '+ token,
  397. },
  398. data: formData,
  399. timeout: 60000,
  400. onUploadProgress: (p: any) => {
  401. onProgress({percent: Math.round((p.loaded / p.total) * 100)});
  402. }
  403. });
  404. // 构造 Element Plus 需要的 file 对象
  405. const fileObj = {
  406. name: res.data.name,
  407. url: res.data.url,
  408. status: 'success',
  409. response: res.data
  410. };
  411. // 添加到附件列表
  412. form.value.attachments = fileObj.url;
  413. ElMessage.success('上传成功');
  414. onSuccess(fileObj);
  415. } catch (error) {
  416. ElMessage.error('上传失败');
  417. onError(error);
  418. }
  419. };
  420. // import { ElSteps } from 'element-plus';
  421. const steps = ref<any[]>([]);
  422. // 仓库映射(id -> name)
  423. const warehouseMap = ref<Record<string, string>>({});
  424. async function loadWarehouses() {
  425. try {
  426. const res = await request({
  427. url: '/api/system/warehouse/',
  428. method: 'get',
  429. params: { page: 1, limit: 999 }
  430. });
  431. if (res?.code === 2000 && Array.isArray(res.data)) {
  432. warehouseMap.value = Object.fromEntries(
  433. res.data.map((w: any) => [String(w.id), w.name])
  434. );
  435. }
  436. } catch (e) {
  437. /* empty */
  438. }
  439. }
  440. function getWarehouseName(id: any) {
  441. const key = id != null ? String(id) : '';
  442. return warehouseMap.value[key] ?? id;
  443. }
  444. const timeline = ref({
  445. setpstatus:0,
  446. create: '',
  447. approve: '',
  448. approver: '',
  449. approvemsg:''
  450. });
  451. function buildSteps(data: any) {
  452. steps.value = [
  453. {
  454. title: '创建申请',
  455. description: `创建时间:${data.create_datetime}`,
  456. status: 'finish'
  457. },
  458. {
  459. title: '待审批',
  460. description: '待审批人审批',
  461. status: data.status >= 2 ? 'finish' : 'process'
  462. }
  463. ];
  464. if (data.status >= 2&&data.status!=3) {
  465. steps.value.push({
  466. title: '审批通过',
  467. description: `审批人:${data.approver_info.name},时间:${data.approve_time},仓库:${getWarehouseName(data.warehouse)||'暂无仓库'}`,
  468. status: 'finish'
  469. });
  470. } else if (data.status === 3) {
  471. steps.value.push({
  472. title: '审批拒绝',
  473. description: `审批人:${data.approver_info.name},拒绝原因: ${data.approve_remark},时间:${data.approve_time}`,
  474. status: 'error'
  475. });
  476. }
  477. if (data.status >= 4) {
  478. steps.value.push({
  479. title: '流程完成',
  480. description: '流程已结束',
  481. status: 'finish'
  482. });
  483. }
  484. }
  485. // const activeStep = ref(1);
  486. const form = ref({
  487. borrower_type: 1,
  488. application_no: '',
  489. external_borrower_name:"",
  490. external_borrower_phone:"",
  491. emergency_contact: '111',
  492. emergency_phone: '11111',
  493. app_user_borrower: null as number | null,
  494. admin_borrower: null as number | null,
  495. team_type: 0,
  496. team_members_count: 2,
  497. team_members: '',
  498. expected_start_time:'',
  499. expected_end_time: '',
  500. borrower_dept: '',
  501. purpose: '',
  502. applicant_remark_type: "1",
  503. applicant_remark: '',
  504. items: [] as any[],
  505. accessories: '',
  506. borrow_type: 2,
  507. return_location:'',
  508. attachments:'',
  509. user_code: '',
  510. mobile: '',
  511. mode: '' as 'add' | 'edit' | 'view' | '',
  512. });
  513. const showSelectDeviceDialog = ref(false);
  514. /** 用于按「流程管理」中的条件自动匹配 workflow id */
  515. const workflowsForMatch = ref<Record<string, any>[]>([]);
  516. const workflowsLoadDone = ref(false);
  517. async function loadWorkflowsForMatch() {
  518. workflowsLoadDone.value = false;
  519. try {
  520. const res: any = await workflowApi.GetList({ page: 1, limit: 999 } as any);
  521. workflowsForMatch.value = Array.isArray(res?.data) ? res.data : [];
  522. } catch {
  523. workflowsForMatch.value = [];
  524. } finally {
  525. workflowsLoadDone.value = true;
  526. }
  527. }
  528. const resolvedAppUserType = computed(() => {
  529. if (form.value.borrower_type !== 1 || form.value.app_user_borrower == null) return null;
  530. const u = allUserList.value.find((x) => Number(x.id) === Number(form.value.app_user_borrower));
  531. return u != null ? (u as any).user_type ?? null : null;
  532. });
  533. const specialBorrowMatchInput = computed((): SpecialBorrowMatchInput => ({
  534. borrow_type: 2,
  535. borrower_type: form.value.borrower_type,
  536. team_type: form.value.team_type,
  537. expected_start_time: form.value.expected_start_time || undefined,
  538. expected_end_time: form.value.expected_end_time || undefined,
  539. app_user_type: resolvedAppUserType.value,
  540. }));
  541. const matchedWorkflowId = computed(() => {
  542. if (!workflowsForMatch.value.length) return null;
  543. return pickWorkflowIdForSpecialBorrow(workflowsForMatch.value, specialBorrowMatchInput.value);
  544. });
  545. const matchedWorkflowName = computed(() =>
  546. findWorkflowNameById(workflowsForMatch.value, matchedWorkflowId.value)
  547. );
  548. watch(
  549. () => props.visible,
  550. (v) => {
  551. if (v && !isView.value) {
  552. void loadWorkflowsForMatch();
  553. }
  554. }
  555. );
  556. const rules = {
  557. external_borrower_name: [
  558. { required: true, message: '请输入借用人', trigger: 'change' },
  559. ],
  560. app_user_borrower: [
  561. { required: true, message: '请输入学号或工号', trigger: 'change' },
  562. ],
  563. external_borrower_phone: [
  564. { required: true, message: '请输入联系电话', trigger: 'change' },
  565. ],
  566. /* borrower_dept: [
  567. { required: true, message: '请输入借用部门', trigger: 'change' },
  568. ], */
  569. expected_end_time: [
  570. { required: true, message: '请输选择归还时间', trigger: 'change' },
  571. ],
  572. team_type: [
  573. { required: true, message: '请选择个人或者团队', trigger: 'change' },
  574. ],
  575. purpose: [
  576. { required: true, message: '请输入借用原因', trigger: 'change' },
  577. ],
  578. applicant_remark_type: [
  579. { required: true, message: '请选择审批人', trigger: 'change' },
  580. ],
  581. applicant_remark: [
  582. { required: false, message: '请输入备注', trigger: 'change' },
  583. ],
  584. };
  585. // 自动填充用户信息
  586. onMounted(() => {
  587. loadWarehouses()
  588. getAdminList()
  589. getAllUserList()
  590. try {
  591. // const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
  592. // if (userInfo) {
  593. // form.value.borrower_type = 1;
  594. // form.value.external_borrower_name = '';/* userInfo.username || */
  595. // form.value.app_user_borrower = null;/* Number(userInfo.dept_info?.dept_id )|| */
  596. // form.value.external_borrower_phone = '';/* userInfo.mobile || */
  597. // form.value.items = [];
  598. // }
  599. } catch (e) {
  600. /* empty */
  601. }
  602. });
  603. async function queryUserByCode(userCode: string) {
  604. try {
  605. // 先在已加载的全量列表中查找,避免重复请求
  606. if (allUserList.value && allUserList.value.length > 0) {
  607. const local = allUserList.value.find((u: any) => String(u.user_code || '').trim() === String(userCode).trim());
  608. if (local) {
  609. form.value.app_user_borrower = Number(local.id);
  610. form.value.user_code = local.user_code || '';
  611. form.value.mobile = local.mobile || '';
  612. return;
  613. }
  614. }
  615. // 远端查询:尽量使用通用查询参数,兼容不同实现|| res.data[0]
  616. const tryParamsList = [
  617. { user_code: userCode, page: 1, limit: 20 },
  618. { search: userCode, page: 1, limit: 20 },
  619. { code: userCode, page: 1, limit: 20 },
  620. ];
  621. let found: any = null;
  622. for (const params of tryParamsList) {
  623. const res: any = await api.GetAllUser(params);
  624. if (res && (res.code === 2000 || res.status === 200) && Array.isArray(res.data) && res.data.length > 0) {
  625. found = res.data.find((u: any) => String(u.user_code || '').trim() === String(userCode).trim()) ;
  626. break;
  627. }
  628. }
  629. if (found) {
  630. form.value.app_user_borrower = Number(found.id);
  631. form.value.user_code = found.user_code || '';
  632. form.value.mobile = found.mobile || '';
  633. // 部门信息将由现有的 app_user_borrower 监听逻辑自动回填
  634. } else {
  635. // 未找到时不覆盖已有填写,给出轻提示
  636. ElMessage.warning('未找到匹配的借用人');
  637. }
  638. } catch (e) {
  639. /* empty */
  640. }
  641. }
  642. function onUserCodeBlur() {
  643. const userCode = String(form.value.user_code || '').trim();
  644. if (!userCode || userCode.length < 2) return;
  645. if (form.value.borrower_type === 2) return; // 外部用户不查
  646. if (isView.value) return; // 查看态不查
  647. queryUserByCode(userCode);
  648. }
  649. watch(
  650. () => props.visible,
  651. (v) => (visible.value = v)
  652. );
  653. watch(() => props.modelValue, async (val) => {
  654. console.log("val+++",val && val.id && isView.value,val&& isEdit.value);
  655. if (val && val.id && isView.value) {
  656. try {
  657. const res = await api.GetApplicationDetail(val.id);
  658. if (res.code === 2000 && res.data) {
  659. const data = res.data;
  660. Object.assign(form.value, data);
  661. buildSteps(data);
  662. // console.log("data:::",data);
  663. }
  664. } catch (e) {
  665. ElMessage.error('获取审批信息失败');
  666. }
  667. }else if(val&& isEdit.value){
  668. try {
  669. const res = await api.GetApplicationDetail(val.id);
  670. if (res.code === 2000 && res.data) {
  671. const data = res.data;
  672. /* if(isEdit.value){
  673. data.app_user_borrower = data.borrower_info.id;
  674. } */
  675. console.log(data);
  676. Object.assign(form.value, data);
  677. console.log("form.value:::",form.value);
  678. }
  679. } catch (e) {
  680. ElMessage.error('获取审批信息失败');
  681. }
  682. }
  683. }, { immediate: true });
  684. // 监听借用人选择变化
  685. watch(() => form.value.app_user_borrower, (newValue) => {
  686. if (newValue && allUserList.value.length > 0) {
  687. const selectedUser = allUserList.value.find(user => Number(user.id) === newValue);
  688. if (selectedUser) {
  689. form.value.user_code = selectedUser.user_code || '';
  690. form.value.mobile = selectedUser.mobile || '';
  691. // 根据用户类型动态设置借用部门:
  692. // 学生(user_type===0):学院/专业/班级;教师或领导(user_type===1或3):学院
  693. let deptName: string = '';
  694. const orgDetail: any = (selectedUser as any).organization_detail || {};
  695. const parentChain: any[] = Array.isArray(orgDetail.parent_chain) ? orgDetail.parent_chain : [];
  696. const isValid = (val: any) => {
  697. const s = String(val).trim();
  698. return val !== null && val !== undefined && s !== '' && s.toLowerCase() !== 'nan' && !Number.isNaN(val);
  699. };
  700. if ((selectedUser as any).user_type === 0) {
  701. // 学生:优先按 parent_chain 顺序拼接“学院/专业/班级”,过滤掉“学校”层级
  702. const chainNames: string[] = parentChain
  703. .filter((n: any) => Number(n?.type) !== 1) // 1=学校,过滤
  704. .map((n: any) => n?.name)
  705. .filter((n: any) => isValid(n));
  706. // 取前3级:学院(0)/专业(1)/班级(2)
  707. const college = chainNames[1] || '';
  708. const major = chainNames[2] || (selectedUser as any).sub_organization || '';
  709. const clazz = orgDetail.name || (selectedUser as any).class_or_group || '';
  710. const parts = [college, major, clazz].filter((p) => isValid(p));
  711. deptName = parts.join('/');
  712. if (!deptName) {
  713. // 兜底:若 parent_chain 不可用,则使用已有汇总字段但尽量去掉“学校/”前缀
  714. const full = (selectedUser as any).full_organization_name || '';
  715. deptName = full.replace(/^学校\s*\/\s*/,'');
  716. // 兜底后继续清洗 'nan' 与空白段
  717. deptName = deptName
  718. .split('/')
  719. .map((s: string) => s.trim())
  720. .filter((s: string) => isValid(s))
  721. .join('/');
  722. }
  723. // 最终统一清洗,避免出现重复斜杠或 'nan'
  724. deptName = deptName
  725. .split('/')
  726. .map((s: string) => s.trim())
  727. .filter((s: string) => isValid(s))
  728. .join('/');
  729. } else if ((selectedUser as any).user_type === 1 || (selectedUser as any).user_type === 3) {
  730. // 教师或学院领导:显示学院名称
  731. deptName = orgDetail?.name || (selectedUser as any).organization || (selectedUser as any).full_organization_name || '';
  732. } else {
  733. // 其他类型兜底
  734. deptName = (selectedUser as any).full_organization_name || '';
  735. }
  736. form.value.borrower_dept = isValid(deptName)?deptName:'';
  737. // 如果是新创建的用户,提示用户填写完整信息
  738. if (selectedUser.isNew) {
  739. ElMessage.info('请为新用户填写学号/工号和联系电话');
  740. }
  741. }
  742. }
  743. });
  744. // watch(
  745. // () => props.modelValue,
  746. // (v) => {
  747. // if (v) {
  748. // // eslint-disable-next-line no-console
  749. // console.log("val:::::",v);
  750. // Object.assign(form.value, v);
  751. // }
  752. // }, { immediate: true }
  753. // );
  754. function addItem() {
  755. showSelectDeviceDialog.value = true;
  756. }
  757. function onDeviceSelected(devices: any[]) {
  758. console.log("devices:::",devices);
  759. // 获取已存在的设备分类ID列表
  760. const existingIds = new Set(form.value.items.map((item: any) => item.device_category).filter(Boolean));
  761. // 过滤掉已存在的设备分类,只添加新的
  762. const newDevices = devices.filter((d: any) => !existingIds.has(d.id));
  763. if (newDevices.length === 0) {
  764. ElMessage.warning('所选设备分类已存在,未添加重复项');
  765. return;
  766. }
  767. // 如果有部分重复,提示用户
  768. if (newDevices.length < devices.length) {
  769. ElMessage.warning(`已过滤 ${devices.length - newDevices.length} 个重复的设备分类`);
  770. }
  771. form.value.items.push(
  772. ...newDevices.map((d: any) => ({
  773. device_category: d.id,
  774. remark: d.name,
  775. quantity: 1,
  776. location: d.warehouse,
  777. device_category_name: d.name,
  778. }))
  779. );
  780. }
  781. function removeItem(index: number) {
  782. form.value.items.splice(index, 1);
  783. }
  784. function onCancel() {
  785. emit('update:visible', false);
  786. }
  787. function onSubmit() {
  788. formRef.value.validate(async (valid: boolean) => {
  789. if (!valid) return;
  790. if (!workflowsForMatch.value.length) {
  791. await loadWorkflowsForMatch();
  792. }
  793. form.value.items = form.value.items.map((item: any) => ({
  794. ...item,
  795. quantity: Number(item.quantity) || 0,
  796. }));
  797. const firstLocation = form.value.items[0]?.location || '';
  798. form.value.return_location = firstLocation;
  799. form.value.expected_start_time = now;
  800. const endDay = form.value.expected_end_time;
  801. form.value.expected_end_time = endDay
  802. ? dayjs(endDay).endOf('day').format('YYYY-MM-DD HH:mm:ss')
  803. : dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
  804. form.value.team_members_count = Number(form.value.team_members_count) || 1;
  805. form.value.mode = isEdit.value ? 'edit' : 'add';
  806. form.value.borrow_type = 2;
  807. const matchPayload: SpecialBorrowMatchInput = {
  808. borrow_type: 2,
  809. borrower_type: form.value.borrower_type,
  810. team_type: form.value.team_type,
  811. expected_start_time: form.value.expected_start_time,
  812. expected_end_time: endDay || undefined,
  813. app_user_type: resolvedAppUserType.value,
  814. };
  815. const workflowId = pickWorkflowIdForSpecialBorrow(workflowsForMatch.value, matchPayload);
  816. const submitBody: Record<string, any> = { ...form.value };
  817. if (workflowId != null) {
  818. submitBody.workflow = workflowId;
  819. }
  820. emit('submit', submitBody);
  821. emit('update:visible', false);
  822. });
  823. }
  824. </script>