IotProjectInfoForm.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. <template>
  2. <ContentWrap v-loading="formLoading">
  3. <el-form
  4. ref="formRef"
  5. :model="formData"
  6. :rules="formRules"
  7. label-width="100px"
  8. v-loading="formLoading"
  9. >
  10. <el-row>
  11. <el-col :span="12">
  12. <el-form-item label="客户名称" prop="manufacturerId">
  13. <el-select
  14. clearable
  15. @clear="zzClear"
  16. v-model="formData.manufactureName"
  17. :placeholder="t('deviceForm.mfgHolder')"
  18. @click="openCustomerZz"
  19. />
  20. </el-form-item>
  21. </el-col>
  22. <el-col :span="12">
  23. <el-form-item label="结算方式" prop="payment">
  24. <el-input v-model="formData.payment" placeholder="请输入结算方式" />
  25. </el-form-item>
  26. </el-col>
  27. </el-row>
  28. <el-row>
  29. <el-col :span="12">
  30. <el-form-item label="合同名称" prop="contractName">
  31. <el-input v-model="formData.contractName" placeholder="请输入合同名称" />
  32. </el-form-item>
  33. </el-col>
  34. <el-col :span="12">
  35. <el-form-item label="合同编号" prop="contractCode">
  36. <el-input v-model="formData.contractCode" placeholder="请输入合同编号" />
  37. </el-form-item>
  38. </el-col>
  39. </el-row>
  40. <el-row>
  41. <el-col :span="12">
  42. <el-form-item label="总数" prop="workloadTotal">
  43. <el-input v-model="formData.workloadTotal" placeholder="请输入工作量总数" />
  44. </el-form-item>
  45. </el-col>
  46. <el-col :span="12">
  47. <el-form-item label="已完成" prop="workloadFinish">
  48. <el-input v-model="formData.workloadFinish" placeholder="已完成工作量" disabled/>
  49. </el-form-item>
  50. </el-col>
  51. </el-row>
  52. <el-row>
  53. <el-col :span="12">
  54. <el-form-item label="开始时间" prop="startTime">
  55. <el-date-picker
  56. style="width: 150%"
  57. v-model="formData.startTime"
  58. type="date"
  59. value-format="x"
  60. placeholder="选择开始时间"
  61. />
  62. </el-form-item>
  63. </el-col>
  64. <el-col :span="12">
  65. <el-form-item label="完成时间" prop="endTime">
  66. <el-date-picker
  67. style="width: 150%"
  68. v-model="formData.endTime"
  69. type="date"
  70. value-format="x"
  71. placeholder="选择完成时间"
  72. />
  73. </el-form-item>
  74. </el-col>
  75. </el-row>
  76. <el-row>
  77. <el-col :span="12">
  78. <el-form-item :label="t('iotDevice.company')" prop="deptId">
  79. <el-tree-select
  80. :disabled="isDeptDisabled"
  81. v-model="formData.deptId"
  82. :data="deptList"
  83. :props="defaultProps"
  84. check-strictly
  85. node-key="id"
  86. filterable
  87. placeholder="请选择所属公司"
  88. />
  89. </el-form-item>
  90. </el-col>
  91. <el-col :span="12">
  92. <el-form-item label="责任人" prop="responsiblePerson">
  93. <span v-if="selectedUserNames" class="user-names">
  94. {{ selectedUserNames }}
  95. </span>
  96. <el-button
  97. type="primary"
  98. size="small"
  99. @click="openUserDialog"
  100. style="margin-left: 10px;"
  101. :disabled="!formData.deptId"
  102. >
  103. 选择责任人
  104. </el-button>
  105. <div v-if="!formData.deptId" class="el-form-item__error">请先选择所属公司</div>
  106. </el-form-item>
  107. </el-col>
  108. </el-row>
  109. <el-row>
  110. <el-col :span="24">
  111. <el-form-item label="备注" prop="remark">
  112. <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
  113. </el-form-item>
  114. </el-col>
  115. </el-row>
  116. </el-form>
  117. </ContentWrap>
  118. <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
  119. <!-- 责任人选择对话框 -->
  120. <el-dialog
  121. v-model="userDialogVisible"
  122. title="选择责任人"
  123. width="1000px"
  124. :before-close="handleUserDialogClose"
  125. class="user-select-dialog"
  126. >
  127. <div class="transfer-container">
  128. <el-transfer
  129. v-model="selectedUserIds"
  130. :data="userList"
  131. :titles="['可选人员', '已选人员']"
  132. :props="{ key: 'id', label: 'nickname' }"
  133. filterable
  134. class="transfer-component"
  135. >
  136. <template #default="{ option }">
  137. <el-tooltip
  138. effect="dark"
  139. placement="top"
  140. :content="`${option.nickname} - ${option.deptName || '未分配部门'}`"
  141. >
  142. <span class="transfer-option-text">
  143. {{ option.nickname }} - {{ option.deptName || '未分配部门' }}
  144. </span>
  145. </el-tooltip>
  146. </template>
  147. </el-transfer>
  148. </div>
  149. <template #footer>
  150. <span class="dialog-footer">
  151. <el-button @click="handleUserDialogClose">取消</el-button>
  152. <el-button type="primary" @click="confirmUserSelection">确定</el-button>
  153. </span>
  154. </template>
  155. </el-dialog>
  156. <ContentWrap>
  157. <el-form style="float: right">
  158. <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
  159. <el-button @click="close">取 消</el-button>
  160. </el-form>
  161. </ContentWrap>
  162. </template>
  163. <script setup lang="ts">
  164. import { IotProjectInfoApi, IotProjectInfoVO } from '@/api/pms/iotprojectinfo'
  165. import {defaultProps,handleTree} from "@/utils/tree";
  166. import * as DeptApi from "@/api/system/dept";
  167. import IotProjectTaskForm from '@/views/pms/iotprojectinfo/IotProjectTaskForm.vue'
  168. import CustomerList from '@/views/pms/device/CustomerList.vue'
  169. import {ref} from "vue";
  170. import {useUserStore} from "@/store/modules/user";
  171. import {IotProjectTaskApi, IotProjectTaskVO} from "@/api/pms/iotprojecttask";
  172. import {ElMessageBox} from "element-plus";
  173. import {updateConfig} from "@/api/infra/config";
  174. import {useTagsViewStore} from "@/store/modules/tagsView";
  175. import * as UserApi from "@/api/system/user";
  176. import {UserVO} from "@/api/system/user";
  177. /** 项目信息 表单 */
  178. defineOptions({ name: 'IotProjectInfo' })
  179. const { params, name } = useRoute() // 查询参数
  180. const id = params.id
  181. const { delView } = useTagsViewStore() // 视图操作
  182. const { currentRoute, push } = useRouter()
  183. const { t } = useI18n() // 国际化
  184. const message = useMessage() // 消息弹窗
  185. const deptList = ref<Tree[]>([]) // 树形结构
  186. const dialogVisible = ref(true) // 弹窗的是否展示
  187. const dialogTitle = ref('') // 弹窗的标题
  188. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  189. const taskList = ref<IotProjectTaskVO[]>([]) // 列表的数据
  190. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  191. const loading = ref(true) // 列表的加载中
  192. const simpleUsers = ref<UserVO[]>([]) // 责任人列表
  193. // 添加责任人相关变量
  194. const userDialogVisible = ref(false);
  195. const userList = ref([]); // 所有用户列表
  196. const selectedUserIds = ref([]); // 选中的用户ID
  197. const selectedUserNames = ref(''); // 选中的用户名称显示
  198. const selectedUserList = ref([]); // 存储选中的用户对象列表
  199. const formData = ref({
  200. id: undefined,
  201. deptId: undefined,
  202. deptName: undefined,
  203. contractName: undefined,
  204. contractCode: undefined,
  205. workloadTotal: undefined,
  206. workloadFinish: undefined,
  207. startTime: undefined,
  208. endTime: undefined,
  209. location: undefined,
  210. technique: undefined,
  211. payment: undefined,
  212. userName: useUserStore().getUser.name,
  213. userId: useUserStore().getUser.userId,
  214. })
  215. const close = () => {
  216. delView(unref(currentRoute))
  217. push({ name: 'iotProjectInfo', params:{}})
  218. }
  219. const queryParams = reactive({
  220. projectId:undefined
  221. })
  222. const formRules = reactive({
  223. manufacturerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }],
  224. payment: [{ required: true, message: '结算方式不能为空', trigger: 'blur' }],
  225. contractName: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }],
  226. contractCode: [{ required: true, message: '合同编码不能为空', trigger: 'blur' }],
  227. workloadTotal: [{ required: true, message: '工作量总数不能为空', trigger: 'blur' }],
  228. startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
  229. endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
  230. })
  231. const formRef1 = ref() // 表单 Ref
  232. const openForm = (type: string, id?: number) => {
  233. formRef1.value.open(type, id)
  234. }
  235. const zzClear = () =>{
  236. formData.value.manufacturerId = undefined
  237. formData.value.manufactureName = undefined
  238. }
  239. const customerZzChoose = (row) => {
  240. formData.value.manufacturerId = row.id
  241. // zzLabel.value = row.name
  242. formData.value.manufactureName = row.name
  243. }
  244. const customerZzFormRef = ref()
  245. const openCustomerZz = () => {
  246. customerZzFormRef.value.open()
  247. }
  248. // 格式化用户名称显示
  249. const formatUserNames = (users) => {
  250. if (!users || users.length === 0) return '';
  251. const names = users.map(user => user.nickname);
  252. if (names.length <= 2) {
  253. return names.join(', ');
  254. } else {
  255. return `${names[0]}, ${names[1]}...`;
  256. }
  257. };
  258. // 根据用户ID获取用户信息
  259. const fetchUserInfoByIds = async (userIds) => {
  260. if (!userIds || userIds.length === 0) {
  261. selectedUserList.value = [];
  262. selectedUserNames.value = '';
  263. return;
  264. }
  265. try {
  266. const params = {
  267. userIds: userIds
  268. };
  269. const response = await UserApi.companyDeptsEmployee(params); // 假设有这个API
  270. selectedUserList.value = response;
  271. selectedUserNames.value = formatUserNames(response);
  272. } catch (error) {
  273. console.error('获取用户信息失败:', error);
  274. ElMessage.error('获取用户信息失败');
  275. }
  276. };
  277. // 根据部门ID获取部门名称
  278. const getDeptNames = (deptIds) => {
  279. if (!deptIds || deptIds.length === 0) return '';
  280. const names = [];
  281. const findDept = (list, id) => {
  282. for (const item of list) {
  283. if (item.id === id) {
  284. names.push(item.name);
  285. return true;
  286. }
  287. if (item.children && findDept(item.children, id)) {
  288. return true;
  289. }
  290. }
  291. return false;
  292. };
  293. deptIds.forEach(id => findDept(deptList.value, id));
  294. return names.join(', ');
  295. };
  296. // 计算属性:判断部门选择框是否应该禁用
  297. const isDeptDisabled = computed(() => {
  298. return formType.value === 'update' || deptList.value.length === 1;
  299. });
  300. // 打开用户选择对话框
  301. const openUserDialog = async () => {
  302. try {
  303. // 获取用户列表,传递部门ID参数
  304. const params = {
  305. deptIds: [formData.value.deptId] // 将部门ID转换为数组格式
  306. };
  307. // 获取用户列表
  308. const response = await UserApi.companyDeptsEmployee(params);
  309. userList.value = response;
  310. // 设置当前已选中的用户
  311. selectedUserIds.value = formData.value.responsiblePerson || [];
  312. userDialogVisible.value = true;
  313. } catch (error) {
  314. console.error('获取用户列表失败:', error);
  315. ElMessage.error('获取用户列表失败');
  316. }
  317. };
  318. // 确认用户选择
  319. const confirmUserSelection = () => {
  320. // 更新表单数据
  321. formData.value.responsiblePerson = [...selectedUserIds.value];
  322. // 更新显示的用户名称
  323. updateSelectedUserNames();
  324. userDialogVisible.value = false;
  325. };
  326. // 更新显示的用户名称
  327. const updateSelectedUserNames = () => {
  328. console.log('已经保存的责任人:' + formData.value.responsiblePerson)
  329. if (!formData.value.responsiblePerson || formData.value.responsiblePerson.length === 0) {
  330. selectedUserNames.value = '';
  331. return;
  332. }
  333. const selectedUsers = userList.value.filter(user =>
  334. formData.value.responsiblePerson.includes(user.id)
  335. );
  336. const names = selectedUsers.map(user => user.nickname);
  337. if (names.length <= 2) {
  338. selectedUserNames.value = names.join(', ');
  339. } else {
  340. selectedUserNames.value = `${names[0]}, ${names[1]}...`;
  341. }
  342. };
  343. // 关闭用户选择对话框
  344. const handleUserDialogClose = () => {
  345. userDialogVisible.value = false;
  346. };
  347. const tableData = ref([
  348. {
  349. id: 1,
  350. wellName: '',
  351. wellType: '',
  352. location: '',
  353. technique: '',
  354. workloadDesign: '',
  355. deptIds: [],
  356. remark:'',
  357. editData: {},
  358. editing:true,
  359. originalData: {},
  360. errors: {}
  361. },
  362. ]);
  363. let deptInfo: any[] = "";
  364. /** 打开弹窗 */
  365. const open = async () => {
  366. resetForm()
  367. // 修改时,设置数据
  368. if (id) {
  369. formType.value = 'update';
  370. formLoading.value = true
  371. try {
  372. formData.value = await IotProjectInfoApi.getIotProjectInfo(id)
  373. // 如果已有责任人数据,获取用户信息并显示
  374. if (formData.value.responsiblePerson && formData.value.responsiblePerson.length > 0) {
  375. await fetchUserInfoByIds(formData.value.responsiblePerson);
  376. selectedUserIds.value = [...formData.value.responsiblePerson];
  377. }
  378. // 如果已有责任人数据,更新显示
  379. if (formData.value.responsiblePerson) {
  380. // updateSelectedUserNames();
  381. }
  382. } finally {
  383. formLoading.value = false
  384. }
  385. } else {
  386. formType.value = 'create';
  387. // 如果只有一个部门,则自动选中
  388. if (deptList.value.length === 1) {
  389. formData.value.deptId = deptList.value[0].id;
  390. }
  391. }
  392. }
  393. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  394. /** 提交表单 */
  395. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  396. const formRef = ref() // 表单 Ref
  397. const submitForm = async () => {
  398. // 校验表单
  399. await formRef.value.validate()
  400. // 提交请求
  401. formLoading.value = true
  402. try {
  403. // formData.value.deptId = useUserStore().getUser.deptId;
  404. const data = {
  405. projectData: formData.value as unknown as IotProjectInfoVO,
  406. //taskList:tableData.value
  407. }
  408. if (formType.value === 'create') {
  409. await IotProjectInfoApi.createIotProjectInfo(data)
  410. message.success(t('common.createSuccess'))
  411. } else {
  412. await IotProjectInfoApi.updateIotProjectInfo(data)
  413. message.success(t('common.updateSuccess'))
  414. }
  415. dialogVisible.value = false
  416. // 发送操作成功的事件
  417. emit('success')
  418. close()
  419. } finally {
  420. formLoading.value = false
  421. }
  422. }
  423. /** 重置表单 */
  424. const resetForm = () => {
  425. formData.value = {
  426. id: undefined,
  427. deptId: undefined,
  428. deptName: undefined,
  429. contractName: undefined,
  430. contractCode: undefined,
  431. workloadTotal: undefined,
  432. workloadFinish: undefined,
  433. startTime: undefined,
  434. endTime: undefined,
  435. location: undefined,
  436. technique: undefined,
  437. payment: undefined,
  438. userName: undefined,
  439. userId: undefined,
  440. responsiblePerson: [], // 重置责任人字段
  441. }
  442. selectedUserNames.value = '';
  443. formRef.value?.resetFields()
  444. }
  445. onMounted(async () => {
  446. // deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  447. // 查询当前登录人所属的 公司级部门 如果部门列表只有一个值 默认选中
  448. deptList.value = await DeptApi.companyLevelDepts()
  449. // 如果只有一个部门,则自动选中
  450. if (deptList.value.length === 1) {
  451. formData.value.deptId = deptList.value[0].id;
  452. }
  453. open();
  454. })
  455. const showCloumn = false;
  456. // 表格数据
  457. // 计算正在编辑的行数
  458. const editingRowsCount = computed(() => {
  459. return tableData.value.filter(row => row.editing).length;
  460. });
  461. // 格式化日期
  462. const formatDate = (timestamp) => {
  463. const date = new Date(timestamp);
  464. return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
  465. };
  466. // 为编辑中的行添加特殊样式
  467. const rowClassName = ({ row }) => {
  468. return row.editing ? 'editable-row' : '';
  469. };
  470. // 添加新行
  471. const addNewRow = () => {
  472. const newId = tableData.value.length > 0
  473. ? Math.max(...tableData.value.map(item => item.id)) + 1
  474. : 1;
  475. const newRow = {
  476. id: newId,
  477. wellName: '',
  478. wellType: '',
  479. location: '',
  480. technique: '',
  481. workloadDesign: '',
  482. deptIds: [],
  483. remark:'',
  484. editing: true,
  485. editData: {
  486. wellName: '',
  487. wellType: '',
  488. location: '',
  489. technique: '',
  490. workloadDesign: '',
  491. deptIds:[],
  492. remark:'',
  493. },
  494. originalData: {},
  495. errors: {}
  496. };
  497. tableData.value.unshift(newRow);
  498. };
  499. // 编辑行
  500. const editRow = (row) => {
  501. // 保存原始数据用于取消编辑时恢复
  502. row.originalData = {...row};
  503. // 设置编辑数据
  504. row.editData = {
  505. id: row.id,
  506. wellName: row.wellName,
  507. wellType: row.wellType,
  508. location: row.location,
  509. technique: row.technique,
  510. workloadDesign: row.workloadDesign,
  511. deptIds: row.deptIds,
  512. remark:row.remark,
  513. };
  514. // 进入编辑状态
  515. row.editing = true;
  516. // 清空错误信息
  517. row.errors = {};
  518. };
  519. // 验证行数据
  520. const validateRow = (row) => {
  521. row.errors = {};
  522. let valid = true;
  523. if (!row.editData.wellName || row.editData.wellName.trim() === '') {
  524. row.errors.name = '井号不能为空';
  525. valid = false;
  526. }
  527. if (!row.editData.wellType || row.editData.wellType.trim() === '') {
  528. row.errors.email = '井型不能为空';
  529. valid = false;
  530. }
  531. return valid;
  532. };
  533. // 保存行
  534. const saveRow = (row) => {
  535. if (!validateRow(row)) return;
  536. // 将编辑数据应用到行
  537. row.id = row.editData.id;
  538. row.wellName = row.editData.wellName;
  539. row.wellType = row.editData.wellType;
  540. row.location = row.editData.location;
  541. row.technique = row.editData.technique;
  542. row.workloadDesign = row.editData.workloadDesign;
  543. row.deptIds = row.editData.deptIds;
  544. row.remark = row.editData.remark;
  545. // 退出编辑状态
  546. row.editing = false;
  547. };
  548. // 保存所有更改
  549. const saveAll = () => {
  550. let allValid = true;
  551. // 验证所有编辑中的行
  552. tableData.value.forEach(row => {
  553. if (row.editing && !validateRow(row)) {
  554. allValid = false;
  555. }
  556. });
  557. if (!allValid) {
  558. ElMessage.error('部分数据验证失败,请检查输入');
  559. return;
  560. }
  561. // 保存所有编辑中的行
  562. tableData.value.forEach(row => {
  563. if (row.editing) {
  564. row.id = row.editData.id;
  565. row.wellName = row.editData.wellName;
  566. row.wellType = row.editData.wellType;
  567. row.location = row.editData.location;
  568. row.technique = row.editData.technique;
  569. row.workloadDesign = row.editData.workloadDesign;
  570. row.deptIds = row.editData.deptIds;
  571. row.remark = row.editData.remark;
  572. }
  573. });
  574. ElMessage.success('所有更改已保存');
  575. };
  576. // 取消编辑
  577. const cancelEdit = (row) => {
  578. // 恢复原始数据
  579. if (row.originalData.id) {
  580. row.id = row.originalData.id;
  581. row.wellName = row.originalData.wellName;
  582. row.wellType = row.originalData.wellType;
  583. row.location = row.originalData.location;
  584. row.technique = row.originalData.technique;
  585. row.workloadDesign = row.originalData.workloadDesign;
  586. row.deptIds = row.originalData.deptIds;
  587. row.remark = row.originalData.remark;
  588. } else {
  589. // 如果是新增行,则删除
  590. const index = tableData.value.indexOf(row);
  591. if (index !== -1) {
  592. tableData.value.splice(index, 1);
  593. }
  594. }
  595. row.editing = false;
  596. };
  597. // 删除行
  598. const deleteRow = (index) => {
  599. tableData.value.splice(index, 1);
  600. ElMessage.success('行已删除');
  601. };
  602. </script>
  603. <style scoped>
  604. .user-names {
  605. padding: 5px 10px;
  606. border: 1px solid #dcdfe6;
  607. border-radius: 4px;
  608. background-color: #f5f7fa;
  609. display: inline-block;
  610. min-width: 200px;
  611. }
  612. .transfer-container {
  613. text-align: center;
  614. padding: 20px 0;
  615. }
  616. /* 3. 直接调整 el-transfer 左右窗口的宽度 */
  617. :deep(.el-transfer-panel) {
  618. width: 40% !important; /* 强制设置左右面板宽度,确保两侧面板对等 */
  619. }
  620. .transfer-component {
  621. width: 100%;
  622. min-width: 600px;
  623. }
  624. :deep(.el-transfer-panel__item) {
  625. white-space: nowrap;
  626. overflow: hidden;
  627. text-overflow: ellipsis;
  628. max-width: 100%;
  629. padding: 6px 6px;
  630. }
  631. .transfer-option-text {
  632. display: inline-block;
  633. max-width: 100%;
  634. }
  635. :deep(.el-transfer-panel__list) {
  636. width: 100% !important; /* 使面板内部内容宽度占满 */
  637. }
  638. </style>