IotProjectInfoForm.vue 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238
  1. <template>
  2. <div class="project-form-page">
  3. <ContentWrap class="page-header">
  4. <div class="page-header__main">
  5. <div class="page-header__icon">
  6. <Icon :icon="formType === 'create' ? 'ep:document-add' : 'ep:edit-pen'" />
  7. </div>
  8. <div>
  9. <h2 class="page-header__title">
  10. {{ formType === 'create' ? '新增项目信息' : '编辑项目信息' }}
  11. </h2>
  12. <p class="page-header__subtitle">维护客户、合同、归属部门及项目配置信息</p>
  13. </div>
  14. </div>
  15. <el-button size="default" @click="close">
  16. <Icon icon="ep:arrow-left" class="mr-5px" />返回
  17. </el-button>
  18. </ContentWrap>
  19. <el-form
  20. ref="formRef"
  21. v-loading="formLoading"
  22. :model="formData"
  23. :rules="formRules"
  24. label-width="108px"
  25. size="default"
  26. class="project-form">
  27. <ContentWrap class="project-form-card">
  28. <div class="form-card-header">
  29. <span class="form-card-icon form-card-icon--blue">
  30. <Icon icon="ep:document" />
  31. </span>
  32. <div>
  33. <h3>合同信息</h3>
  34. <p>客户、合同及项目周期信息</p>
  35. </div>
  36. </div>
  37. <div class="form-card-body">
  38. <el-row :gutter="24">
  39. <el-col :xs="24" :md="12">
  40. <el-form-item label="客户名称" prop="manufacturerId">
  41. <el-select
  42. v-model="formData.manufactureName"
  43. clearable
  44. :placeholder="t('deviceForm.mfgHolder')"
  45. @clear="zzClear"
  46. @click="openCustomerZz" />
  47. </el-form-item>
  48. </el-col>
  49. <el-col :xs="24" :md="12">
  50. <el-form-item :label="t('project.payment')" prop="payment">
  51. <el-select v-model="formData.payment" placeholder="请选择" clearable>
  52. <el-option
  53. v-for="dict in getStrDictOptions(DICT_TYPE.PMS_PROJECT_SETTLEMENT)"
  54. :key="dict.id"
  55. :label="dict.label"
  56. :value="dict.value" />
  57. </el-select>
  58. </el-form-item>
  59. </el-col>
  60. <el-col :xs="24" :md="12">
  61. <el-form-item label="合同名称" prop="contractName">
  62. <el-input v-model="formData.contractName" placeholder="请输入合同名称" />
  63. </el-form-item>
  64. </el-col>
  65. <el-col :xs="24" :md="12">
  66. <el-form-item label="合同编号" prop="contractCode">
  67. <el-input v-model="formData.contractCode" placeholder="请输入合同编号" />
  68. </el-form-item>
  69. </el-col>
  70. <el-col :xs="24" :md="12">
  71. <el-form-item label="开始时间" prop="startTime">
  72. <el-date-picker
  73. v-model="formData.startTime"
  74. type="date"
  75. value-format="x"
  76. placeholder="选择开始时间" />
  77. </el-form-item>
  78. </el-col>
  79. <el-col :xs="24" :md="12">
  80. <el-form-item label="完成时间" prop="endTime">
  81. <el-date-picker
  82. v-model="formData.endTime"
  83. type="date"
  84. value-format="x"
  85. placeholder="选择完成时间" />
  86. </el-form-item>
  87. </el-col>
  88. </el-row>
  89. </div>
  90. </ContentWrap>
  91. <ContentWrap class="project-form-card">
  92. <div class="form-card-header">
  93. <span class="form-card-icon form-card-icon--green">
  94. <Icon icon="ep:setting" />
  95. </span>
  96. <div>
  97. <h3>项目配置</h3>
  98. <p>归属部门、责任人与项目属性</p>
  99. </div>
  100. </div>
  101. <div class="form-card-body">
  102. <el-row :gutter="24">
  103. <el-col :xs="24" :md="12">
  104. <el-form-item :label="t('iotDevice.company')" prop="deptId">
  105. <el-tree-select
  106. v-model="formData.deptId"
  107. :disabled="isDeptDisabled"
  108. :data="deptList"
  109. :props="defaultProps"
  110. check-strictly
  111. node-key="id"
  112. filterable
  113. placeholder="请选择所属公司" />
  114. </el-form-item>
  115. </el-col>
  116. <el-col :xs="24" :md="12">
  117. <el-form-item label="项目部" prop="projectDeptId">
  118. <el-select
  119. v-model="formData.projectDeptId"
  120. :options="deptOptions"
  121. :loading="deptLoading"
  122. filterable
  123. placeholder="请选择项目部"
  124. clearable />
  125. </el-form-item>
  126. </el-col>
  127. <el-col :xs="24" :md="12">
  128. <el-form-item label="责任人" prop="responsiblePerson">
  129. <div class="responsible-person-control">
  130. <el-tooltip
  131. v-if="selectedUserNames"
  132. effect="dark"
  133. placement="top"
  134. :content="getFullSelectedUserNames()">
  135. <span class="user-names">{{ selectedUserNames }}</span>
  136. </el-tooltip>
  137. <span v-else class="user-names user-names--empty">暂未选择责任人</span>
  138. <el-button
  139. type="primary"
  140. plain
  141. size="default"
  142. :disabled="!formData.deptId"
  143. @click="openUserDialog">
  144. <Icon icon="ep:user" class="mr-5px" />选择
  145. </el-button>
  146. </div>
  147. <div v-if="!formData.deptId" class="el-form-item__error">请先选择所属公司</div>
  148. </el-form-item>
  149. </el-col>
  150. <el-col :xs="24" :md="12">
  151. <el-form-item :label="t('project.overseaFlag')" prop="overseas">
  152. <el-select v-model="formData.overseas" placeholder="请选择" clearable>
  153. <el-option
  154. v-for="dict in getIntDictOptions(DICT_TYPE.PMS_PROJECT_OVERSEA_FLAG)"
  155. :key="dict.id"
  156. :label="dict.label"
  157. :value="dict.value" />
  158. </el-select>
  159. </el-form-item>
  160. </el-col>
  161. <el-col :xs="24" :md="12">
  162. <el-form-item :label="t('project.overseaArea')" prop="overseasArea">
  163. <el-select v-model="formData.overseasArea" placeholder="请选择" clearable>
  164. <el-option
  165. v-for="dict in getStrDictOptions(DICT_TYPE.PMS_PROJECT_AREA)"
  166. :key="dict.id"
  167. :label="dict.label"
  168. :value="dict.value" />
  169. </el-select>
  170. </el-form-item>
  171. </el-col>
  172. <el-col :xs="24" :md="12">
  173. <el-form-item :label="t('project.workArea')" prop="location">
  174. <el-autocomplete
  175. ref="workAreaAutocomplete"
  176. v-model="formData.location"
  177. :fetch-suggestions="querySearch"
  178. :trigger-on-focus="true"
  179. :disabled="!workAreaOptions.length"
  180. placeholder="请输入施工区域"
  181. popper-class="work-area-autocomplete"
  182. @focus="handleWorkAreaFocus"
  183. @select="handleSelect">
  184. <template #prefix>
  185. <el-icon v-if="loadingWorkAreaOptions" class="is-loading">
  186. <Loading />
  187. </el-icon>
  188. </template>
  189. </el-autocomplete>
  190. <div v-if="!workAreaOptions.length" class="el-form-item__error">
  191. 暂无可用施工区域选项,请手动输入
  192. </div>
  193. </el-form-item>
  194. </el-col>
  195. <el-col :xs="24" :md="12">
  196. <el-form-item label="虚拟项目" prop="contractSubject">
  197. <el-switch
  198. v-model="formData.contractSubject"
  199. active-value="Y"
  200. inactive-value="N"
  201. active-text="是"
  202. inactive-text="否" />
  203. </el-form-item>
  204. </el-col>
  205. <el-col :span="24">
  206. <el-form-item label="备注" prop="remark">
  207. <el-input
  208. v-model="formData.remark"
  209. placeholder="请输入备注"
  210. type="textarea"
  211. :rows="3" />
  212. </el-form-item>
  213. </el-col>
  214. </el-row>
  215. </div>
  216. </ContentWrap>
  217. </el-form>
  218. <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
  219. <el-dialog
  220. v-model="userDialogVisible"
  221. width="920px"
  222. :before-close="handleUserDialogClose"
  223. class="user-select-dialog">
  224. <template #header>
  225. <div class="dialog-title">
  226. <div class="dialog-title__icon"><Icon icon="ep:user" /></div>
  227. <div>
  228. <div class="dialog-title__text">选择责任人</div>
  229. <div class="dialog-title__subtitle">从当前公司人员中选择项目责任人</div>
  230. </div>
  231. </div>
  232. </template>
  233. <div class="transfer-container">
  234. <el-transfer
  235. v-model="selectedUserIds"
  236. :data="userList"
  237. :titles="['可选人员', '已选人员']"
  238. :props="{ key: 'id', label: 'nickname' }"
  239. filterable
  240. class="transfer-component">
  241. <template #default="{ option }">
  242. <el-tooltip
  243. effect="dark"
  244. placement="top"
  245. :content="`${option.nickname} - ${option.deptName || '未分配部门'}`">
  246. <span class="transfer-option-text">
  247. {{ option.nickname }} - {{ option.deptName || '未分配部门' }}
  248. </span>
  249. </el-tooltip>
  250. </template>
  251. </el-transfer>
  252. </div>
  253. <template #footer>
  254. <el-button size="default" @click="handleUserDialogClose">取消</el-button>
  255. <el-button size="default" type="primary" @click="confirmUserSelection">确定</el-button>
  256. </template>
  257. </el-dialog>
  258. <div class="form-actions">
  259. <el-button size="default" type="primary" :loading="formLoading" @click="submitForm">
  260. <Icon icon="ep:check" class="mr-5px" />保存
  261. </el-button>
  262. <el-button size="default" @click="close">取消</el-button>
  263. </div>
  264. </div>
  265. </template>
  266. <script setup lang="ts">
  267. import { IotProjectInfoApi, IotProjectInfoVO } from '@/api/pms/iotprojectinfo'
  268. import { defaultProps } from '@/utils/tree'
  269. import * as DeptApi from '@/api/system/dept'
  270. import CustomerList from '@/views/pms/device/CustomerList.vue'
  271. import { ref } from 'vue'
  272. import { useUserStore } from '@/store/modules/user'
  273. import { IotProjectTaskVO } from '@/api/pms/iotprojecttask'
  274. import { useTagsViewStore } from '@/store/modules/tagsView'
  275. import * as UserApi from '@/api/system/user'
  276. import { UserVO } from '@/api/system/user'
  277. import { DICT_TYPE, getIntDictOptions, getStrDictOptions, getDictLabel } from '@/utils/dict'
  278. import { Loading } from '@element-plus/icons-vue'
  279. /** 项目信息 表单 */
  280. defineOptions({ name: 'IotProjectInfo' })
  281. const { params, name } = useRoute() // 查询参数
  282. const id = params.id
  283. const { delView } = useTagsViewStore() // 视图操作
  284. const { currentRoute, push } = useRouter()
  285. const { t } = useI18n() // 国际化
  286. const message = useMessage() // 消息弹窗
  287. const deptList = ref<Tree[]>([]) // 树形结构
  288. const dialogVisible = ref(true) // 弹窗的是否展示
  289. const dialogTitle = ref('') // 弹窗的标题
  290. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  291. const taskList = ref<IotProjectTaskVO[]>([]) // 列表的数据
  292. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  293. const loading = ref(true) // 列表的加载中
  294. const simpleUsers = ref<UserVO[]>([]) // 责任人列表
  295. const deptLoading = ref(false)
  296. const deptOptions = ref<any[]>([])
  297. const user = useUserStore().getUser
  298. const userdeptId = user.deptId
  299. async function loadDept(deptId?: number) {
  300. deptLoading.value = true
  301. try {
  302. const res = await DeptApi.specifiedSimpleDepts(deptId ?? userdeptId)
  303. deptOptions.value = res
  304. .map((item: any) => ({
  305. label: item.name,
  306. value: item.id,
  307. raw: item
  308. }))
  309. .filter((item) => item.raw.type === '2')
  310. } catch (error) {
  311. } finally {
  312. deptLoading.value = false
  313. }
  314. }
  315. onMounted(() => {
  316. loadDept()
  317. })
  318. // 添加责任人相关变量
  319. const userDialogVisible = ref(false)
  320. const userList = ref([]) // 所有用户列表
  321. const selectedUserIds = ref([]) // 选中的用户ID
  322. const selectedUserNames = ref('') // 选中的用户名称显示
  323. const selectedUserList = ref([]) // 存储选中的用户对象列表
  324. // 添加 workAreaOptions 引用和加载状态
  325. const workAreaOptions = ref<any[]>([])
  326. const loadingWorkAreaOptions = ref(false)
  327. const workAreaAutocomplete = ref() // 添加对el-autocomplete的引用
  328. // currentDictLabel 响应式变量
  329. const currentDictLabel = ref('') // 存储当前项目对应的施工区域字典类型
  330. const formData = ref({
  331. id: undefined,
  332. deptId: undefined,
  333. deptName: undefined,
  334. contractName: undefined,
  335. contractCode: undefined,
  336. contractSubject: 'N', // 添加虚拟项目字段,默认值为'N'
  337. workloadTotal: undefined,
  338. workloadFinish: undefined,
  339. startTime: undefined,
  340. endTime: undefined,
  341. location: undefined,
  342. technique: undefined,
  343. payment: undefined,
  344. overseas: undefined,
  345. overseasArea: undefined,
  346. remark: undefined,
  347. dictType: undefined,
  348. projectDeptId: undefined,
  349. userName: useUserStore().getUser.name,
  350. userId: useUserStore().getUser.userId
  351. })
  352. watch(
  353. () => formData.value.deptId,
  354. (newVal) => {
  355. loadDept(newVal)
  356. }
  357. )
  358. const close = () => {
  359. delView(unref(currentRoute))
  360. push({ name: 'iotProjectInfo', params: {} })
  361. }
  362. const queryParams = reactive({
  363. projectId: undefined
  364. })
  365. const formRules = reactive({
  366. manufacturerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }],
  367. payment: [{ required: true, message: '结算方式不能为空', trigger: 'blur' }],
  368. contractName: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }],
  369. contractCode: [{ required: true, message: '合同编码不能为空', trigger: 'blur' }],
  370. workloadTotal: [{ required: true, message: '工作量总数不能为空', trigger: 'blur' }],
  371. startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
  372. endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }]
  373. })
  374. const formRef1 = ref() // 表单 Ref
  375. const openForm = (type: string, id?: number) => {
  376. formRef1.value.open(type, id)
  377. }
  378. const zzClear = () => {
  379. formData.value.manufacturerId = undefined
  380. formData.value.manufactureName = undefined
  381. }
  382. const customerZzChoose = (row) => {
  383. formData.value.manufacturerId = row.id
  384. // zzLabel.value = row.name
  385. formData.value.manufactureName = row.name
  386. }
  387. const customerZzFormRef = ref()
  388. const openCustomerZz = () => {
  389. customerZzFormRef.value.open()
  390. }
  391. // 格式化用户名称显示
  392. const formatUserNames = (users) => {
  393. if (!users || users.length === 0) return ''
  394. const names = users.map((user) => user.nickname)
  395. if (names.length <= 2) {
  396. return names.join(', ')
  397. } else {
  398. return `${names[0]}, ${names[1]}...`
  399. }
  400. }
  401. // 根据用户ID获取用户信息
  402. const fetchUserInfoByIds = async (userIds) => {
  403. if (!userIds || userIds.length === 0) {
  404. selectedUserList.value = []
  405. selectedUserNames.value = ''
  406. return
  407. }
  408. try {
  409. const params = {
  410. userIds: userIds
  411. }
  412. const response = await UserApi.companyDeptsEmployee(params) // 假设有这个API
  413. selectedUserList.value = response
  414. selectedUserNames.value = formatUserNames(response)
  415. } catch (error) {
  416. console.error('获取用户信息失败:', error)
  417. ElMessage.error('获取用户信息失败')
  418. }
  419. }
  420. // 添加处理施工区域获取焦点的方法
  421. const handleWorkAreaFocus = async () => {
  422. // 如果已经有选项数据,直接显示下拉框
  423. if (workAreaOptions.value.length > 0) {
  424. // 使用Element Plus提供的方法触发下拉框
  425. try {
  426. // 直接调用el-autocomplete的focus方法
  427. if (workAreaAutocomplete.value && typeof workAreaAutocomplete.value.focus === 'function') {
  428. workAreaAutocomplete.value.focus()
  429. }
  430. } catch (error) {
  431. console.error('触发下拉框显示失败:', error)
  432. }
  433. return
  434. }
  435. // 如果没有选项数据,尝试加载
  436. if (formData.value.deptId && !loadingWorkAreaOptions.value) {
  437. await loadWorkAreaOptions(formData.value.deptId)
  438. // 加载完成后尝试显示下拉框
  439. if (workAreaOptions.value.length > 0) {
  440. try {
  441. if (workAreaAutocomplete.value && typeof workAreaAutocomplete.value.focus === 'function') {
  442. workAreaAutocomplete.value.focus()
  443. }
  444. } catch (error) {
  445. console.error('触发下拉框显示失败:', error)
  446. }
  447. }
  448. }
  449. }
  450. // 根据部门ID获取部门名称
  451. const getDeptNames = (deptIds) => {
  452. if (!deptIds || deptIds.length === 0) return ''
  453. const names = []
  454. const findDept = (list, id) => {
  455. for (const item of list) {
  456. if (item.id === id) {
  457. names.push(item.name)
  458. return true
  459. }
  460. if (item.children && findDept(item.children, id)) {
  461. return true
  462. }
  463. }
  464. return false
  465. }
  466. deptIds.forEach((id) => findDept(deptList.value, id))
  467. return names.join(', ')
  468. }
  469. // 计算属性:判断部门选择框是否应该禁用
  470. const isDeptDisabled = computed(() => {
  471. return formType.value === 'update' || deptList.value.length === 1
  472. })
  473. // 打开用户选择对话框
  474. const openUserDialog = async () => {
  475. try {
  476. // 获取用户列表,传递部门ID参数
  477. const params = {
  478. deptIds: [formData.value.deptId] // 将部门ID转换为数组格式
  479. }
  480. // 获取用户列表
  481. const response = await UserApi.companyDeptsEmployee(params)
  482. userList.value = response
  483. // 设置当前已选中的用户
  484. selectedUserIds.value = formData.value.responsiblePerson || []
  485. userDialogVisible.value = true
  486. } catch (error) {
  487. console.error('获取用户列表失败:', error)
  488. ElMessage.error('获取用户列表失败')
  489. }
  490. }
  491. // 确认用户选择
  492. const confirmUserSelection = () => {
  493. // 更新表单数据
  494. formData.value.responsiblePerson = [...selectedUserIds.value]
  495. // 更新显示的用户名称
  496. // updateSelectedUserNames();
  497. selectedUserList.value = userList.value.filter((user) => selectedUserIds.value.includes(user.id))
  498. // 更新显示的用户名称
  499. selectedUserNames.value = formatUserNames(selectedUserList.value)
  500. userDialogVisible.value = false
  501. }
  502. // 更新显示的用户名称
  503. const updateSelectedUserNames = () => {
  504. console.log('已经保存的责任人:' + formData.value.responsiblePerson)
  505. if (!formData.value.responsiblePerson || formData.value.responsiblePerson.length === 0) {
  506. selectedUserNames.value = ''
  507. return
  508. }
  509. const selectedUsers = userList.value.filter((user) =>
  510. formData.value.responsiblePerson.includes(user.id)
  511. )
  512. const names = selectedUsers.map((user) => user.nickname)
  513. if (names.length <= 2) {
  514. selectedUserNames.value = names.join(', ')
  515. } else {
  516. selectedUserNames.value = `${names[0]}, ${names[1]}...`
  517. }
  518. }
  519. // 关闭用户选择对话框
  520. const handleUserDialogClose = () => {
  521. userDialogVisible.value = false
  522. }
  523. const tableData = ref([
  524. {
  525. id: 1,
  526. wellName: '',
  527. wellType: '',
  528. location: '',
  529. technique: '',
  530. workloadDesign: '',
  531. deptIds: [],
  532. remark: '',
  533. editData: {},
  534. editing: true,
  535. originalData: {},
  536. errors: {}
  537. }
  538. ])
  539. let deptInfo: any[] = ''
  540. /** 打开弹窗 */
  541. const open = async () => {
  542. resetForm()
  543. // 修改时,设置数据
  544. if (id) {
  545. formType.value = 'update'
  546. formLoading.value = true
  547. try {
  548. formData.value = await IotProjectInfoApi.getIotProjectInfo(id)
  549. // 如果已有责任人数据,获取用户信息并显示
  550. if (formData.value.responsiblePerson && formData.value.responsiblePerson.length > 0) {
  551. await fetchUserInfoByIds(formData.value.responsiblePerson)
  552. selectedUserIds.value = [...formData.value.responsiblePerson]
  553. }
  554. // 编辑页面:根据项目所属公司加载施工区域选项
  555. if (formData.value.deptId) {
  556. await loadWorkAreaOptions(formData.value.deptId)
  557. }
  558. } finally {
  559. formLoading.value = false
  560. }
  561. } else {
  562. formType.value = 'create'
  563. // 如果只有一个部门,则自动选中
  564. if (deptList.value.length === 1) {
  565. formData.value.deptId = deptList.value[0].id
  566. }
  567. }
  568. }
  569. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  570. /** 提交表单 */
  571. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  572. const formRef = ref() // 表单 Ref
  573. const submitForm = async () => {
  574. // 校验表单
  575. await formRef.value.validate()
  576. // 将当前字典类型赋值给formData
  577. formData.value.dictType = currentDictLabel.value
  578. // 提交请求
  579. formLoading.value = true
  580. try {
  581. // formData.value.deptId = useUserStore().getUser.deptId;
  582. const data = {
  583. projectData: formData.value as unknown as IotProjectInfoVO
  584. //taskList:tableData.value
  585. }
  586. if (formType.value === 'create') {
  587. await IotProjectInfoApi.createIotProjectInfo(data)
  588. message.success(t('common.createSuccess'))
  589. } else {
  590. await IotProjectInfoApi.updateIotProjectInfo(data)
  591. message.success(t('common.updateSuccess'))
  592. }
  593. dialogVisible.value = false
  594. // 发送操作成功的事件
  595. emit('success')
  596. close()
  597. } finally {
  598. formLoading.value = false
  599. }
  600. }
  601. /** 重置表单 */
  602. const resetForm = () => {
  603. formData.value = {
  604. id: undefined,
  605. deptId: undefined,
  606. deptName: undefined,
  607. contractName: undefined,
  608. contractCode: undefined,
  609. contractSubject: 'N', // 重置时设置默认值
  610. workloadTotal: undefined,
  611. workloadFinish: undefined,
  612. startTime: undefined,
  613. endTime: undefined,
  614. location: undefined,
  615. technique: undefined,
  616. payment: undefined,
  617. userName: undefined,
  618. userId: undefined,
  619. responsiblePerson: [] // 重置责任人字段
  620. }
  621. selectedUserNames.value = ''
  622. formRef.value?.resetFields()
  623. }
  624. onMounted(async () => {
  625. // deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  626. // 查询当前登录人所属的 公司级部门 如果部门列表只有一个值 默认选中
  627. deptList.value = await DeptApi.companyLevelDepts()
  628. // 如果只有一个部门,则自动选中
  629. if (deptList.value.length === 1) {
  630. formData.value.deptId = deptList.value[0].id
  631. }
  632. open()
  633. })
  634. const showCloumn = false
  635. // 表格数据
  636. // 计算正在编辑的行数
  637. const editingRowsCount = computed(() => {
  638. return tableData.value.filter((row) => row.editing).length
  639. })
  640. // 格式化日期
  641. const formatDate = (timestamp) => {
  642. const date = new Date(timestamp)
  643. return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
  644. }
  645. // 为编辑中的行添加特殊样式
  646. const rowClassName = ({ row }) => {
  647. return row.editing ? 'editable-row' : ''
  648. }
  649. // 添加新行
  650. const addNewRow = () => {
  651. const newId =
  652. tableData.value.length > 0 ? Math.max(...tableData.value.map((item) => item.id)) + 1 : 1
  653. const newRow = {
  654. id: newId,
  655. wellName: '',
  656. wellType: '',
  657. location: '',
  658. technique: '',
  659. workloadDesign: '',
  660. deptIds: [],
  661. remark: '',
  662. editing: true,
  663. editData: {
  664. wellName: '',
  665. wellType: '',
  666. location: '',
  667. technique: '',
  668. workloadDesign: '',
  669. deptIds: [],
  670. remark: ''
  671. },
  672. originalData: {},
  673. errors: {}
  674. }
  675. tableData.value.unshift(newRow)
  676. }
  677. // 编辑行
  678. const editRow = (row) => {
  679. // 保存原始数据用于取消编辑时恢复
  680. row.originalData = { ...row }
  681. // 设置编辑数据
  682. row.editData = {
  683. id: row.id,
  684. wellName: row.wellName,
  685. wellType: row.wellType,
  686. location: row.location,
  687. technique: row.technique,
  688. workloadDesign: row.workloadDesign,
  689. deptIds: row.deptIds,
  690. remark: row.remark
  691. }
  692. // 进入编辑状态
  693. row.editing = true
  694. // 清空错误信息
  695. row.errors = {}
  696. }
  697. // 验证行数据
  698. const validateRow = (row) => {
  699. row.errors = {}
  700. let valid = true
  701. if (!row.editData.wellName || row.editData.wellName.trim() === '') {
  702. row.errors.name = '井号不能为空'
  703. valid = false
  704. }
  705. if (!row.editData.wellType || row.editData.wellType.trim() === '') {
  706. row.errors.email = '井型不能为空'
  707. valid = false
  708. }
  709. return valid
  710. }
  711. // 保存行
  712. const saveRow = (row) => {
  713. if (!validateRow(row)) return
  714. // 将编辑数据应用到行
  715. row.id = row.editData.id
  716. row.wellName = row.editData.wellName
  717. row.wellType = row.editData.wellType
  718. row.location = row.editData.location
  719. row.technique = row.editData.technique
  720. row.workloadDesign = row.editData.workloadDesign
  721. row.deptIds = row.editData.deptIds
  722. row.remark = row.editData.remark
  723. // 退出编辑状态
  724. row.editing = false
  725. }
  726. // 生成Tooltip要显示的完整责任人名称字符串
  727. const getFullSelectedUserNames = () => {
  728. if (!selectedUserList.value || selectedUserList.value.length === 0) {
  729. return ''
  730. }
  731. return selectedUserList.value.map((user) => user.nickname).join(', ')
  732. }
  733. // 保存所有更改
  734. const saveAll = () => {
  735. let allValid = true
  736. // 验证所有编辑中的行
  737. tableData.value.forEach((row) => {
  738. if (row.editing && !validateRow(row)) {
  739. allValid = false
  740. }
  741. })
  742. if (!allValid) {
  743. ElMessage.error('部分数据验证失败,请检查输入')
  744. return
  745. }
  746. // 保存所有编辑中的行
  747. tableData.value.forEach((row) => {
  748. if (row.editing) {
  749. row.id = row.editData.id
  750. row.wellName = row.editData.wellName
  751. row.wellType = row.editData.wellType
  752. row.location = row.editData.location
  753. row.technique = row.editData.technique
  754. row.workloadDesign = row.editData.workloadDesign
  755. row.deptIds = row.editData.deptIds
  756. row.remark = row.editData.remark
  757. }
  758. })
  759. ElMessage.success('所有更改已保存')
  760. }
  761. // 取消编辑
  762. const cancelEdit = (row) => {
  763. // 恢复原始数据
  764. if (row.originalData.id) {
  765. row.id = row.originalData.id
  766. row.wellName = row.originalData.wellName
  767. row.wellType = row.originalData.wellType
  768. row.location = row.originalData.location
  769. row.technique = row.originalData.technique
  770. row.workloadDesign = row.originalData.workloadDesign
  771. row.deptIds = row.originalData.deptIds
  772. row.remark = row.originalData.remark
  773. } else {
  774. // 如果是新增行,则删除
  775. const index = tableData.value.indexOf(row)
  776. if (index !== -1) {
  777. tableData.value.splice(index, 1)
  778. }
  779. }
  780. row.editing = false
  781. }
  782. // 删除行
  783. const deleteRow = (index) => {
  784. tableData.value.splice(index, 1)
  785. ElMessage.success('行已删除')
  786. }
  787. // 添加监听部门ID变化的方法
  788. watch(
  789. () => formData.value.deptId,
  790. async (newDeptId, oldDeptId) => {
  791. // 只有当部门ID确实发生变化时才加载选项
  792. if (newDeptId && newDeptId !== oldDeptId) {
  793. await loadWorkAreaOptions(newDeptId)
  794. }
  795. }
  796. )
  797. // 添加立即执行的监听器,用于处理编辑页面
  798. watch(
  799. () => formData.value.deptId,
  800. async (newDeptId) => {
  801. if (newDeptId) {
  802. await loadWorkAreaOptions(newDeptId)
  803. } else {
  804. workAreaOptions.value = []
  805. }
  806. },
  807. { immediate: true }
  808. )
  809. // 加载工作区域选项的方法
  810. const loadWorkAreaOptions = async (deptId: number) => {
  811. // 如果正在加载,直接返回
  812. if (loadingWorkAreaOptions.value) return
  813. loadingWorkAreaOptions.value = true
  814. try {
  815. // 先尝试通过 getDictLabel 获取字典标签
  816. const dictLabel = await getDictLabel(DICT_TYPE.PMS_PROJECT_WORK_AREA, deptId)
  817. console.log('当前专业公司的施工区域数据字典类型:' + dictLabel)
  818. // 将 dictLabel 存储到 currentDictLabel
  819. currentDictLabel.value = dictLabel || '' // 如果没有获取到,设为空字符串
  820. if (dictLabel) {
  821. // 如果获取到标签,再获取完整的字典选项
  822. const dictOptions = getStrDictOptions(dictLabel)
  823. console.log('当前专业公司的施工区域数据字典集合长度:' + dictOptions.length)
  824. workAreaOptions.value = dictOptions.map((option) => ({
  825. value: option.label, // 使用标签作为显示值
  826. id: option.value // 保留原始值
  827. }))
  828. } else {
  829. // 如果获取不到标签,清空选项
  830. workAreaOptions.value = []
  831. }
  832. } catch (error) {
  833. console.error('获取工作区域选项失败:', error)
  834. workAreaOptions.value = []
  835. } finally {
  836. loadingWorkAreaOptions.value = false
  837. }
  838. }
  839. // 当没有查询字符串时返回所有选项
  840. const querySearch = (queryString: string, cb: any) => {
  841. // 如果没有查询字符串,返回所有选项
  842. if (!queryString) {
  843. cb(workAreaOptions.value)
  844. return
  845. }
  846. // 有查询字符串时进行筛选
  847. const results = workAreaOptions.value.filter((option) =>
  848. option.value.toLowerCase().includes(queryString.toLowerCase())
  849. )
  850. // 调用 callback 返回建议列表的数据
  851. cb(results)
  852. }
  853. // 添加选择处理方法
  854. const handleSelect = (item: any) => {
  855. // 这里可以根据需要处理选择后的逻辑
  856. console.log('选择了:', item.value)
  857. }
  858. </script>
  859. <style scoped>
  860. .project-form-page {
  861. display: flex;
  862. flex-direction: column;
  863. gap: 12px;
  864. min-height: calc(
  865. 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
  866. );
  867. }
  868. .page-header {
  869. margin-bottom: 0;
  870. }
  871. .page-header :deep(.el-card__body) {
  872. display: flex;
  873. align-items: center;
  874. justify-content: space-between;
  875. min-height: 64px;
  876. padding: 12px 16px;
  877. }
  878. .page-header__main,
  879. .dialog-title {
  880. display: flex;
  881. align-items: center;
  882. min-width: 0;
  883. }
  884. .page-header__icon,
  885. .dialog-title__icon {
  886. display: inline-flex;
  887. flex: 0 0 auto;
  888. align-items: center;
  889. justify-content: center;
  890. width: 38px;
  891. height: 38px;
  892. margin-right: 12px;
  893. color: var(--el-color-primary);
  894. background: var(--el-color-primary-light-9);
  895. border: 1px solid var(--el-color-primary-light-7);
  896. border-radius: 6px;
  897. }
  898. .page-header__title {
  899. margin: 0;
  900. font-size: 16px;
  901. font-weight: 600;
  902. line-height: 24px;
  903. letter-spacing: 0;
  904. color: var(--el-text-color-primary);
  905. }
  906. .page-header__subtitle,
  907. .dialog-title__subtitle {
  908. margin: 2px 0 0;
  909. font-size: 12px;
  910. line-height: 18px;
  911. color: var(--el-text-color-secondary);
  912. }
  913. .project-form {
  914. display: flex;
  915. flex-direction: column;
  916. flex: 1;
  917. gap: 12px;
  918. }
  919. .project-form-card {
  920. margin-bottom: 0;
  921. }
  922. .project-form-card :deep(.el-card__body) {
  923. padding: 0;
  924. }
  925. .form-card-header {
  926. display: flex;
  927. align-items: center;
  928. gap: 12px;
  929. min-height: 64px;
  930. padding: 14px 18px;
  931. background: linear-gradient(90deg, #f7faff 0%, var(--el-bg-color) 78%);
  932. border-bottom: 1px solid var(--el-border-color-lighter);
  933. }
  934. .form-card-header h3 {
  935. margin: 0;
  936. font-size: 15px;
  937. font-weight: 700;
  938. line-height: 22px;
  939. color: var(--el-text-color-primary);
  940. }
  941. .form-card-header p {
  942. margin: 2px 0 0;
  943. font-size: 12px;
  944. line-height: 18px;
  945. color: var(--el-text-color-secondary);
  946. }
  947. .form-card-icon {
  948. display: inline-flex;
  949. flex: 0 0 auto;
  950. align-items: center;
  951. justify-content: center;
  952. width: 36px;
  953. height: 36px;
  954. font-size: 18px;
  955. border-radius: 9px;
  956. }
  957. .form-card-icon--blue {
  958. color: #3478f6;
  959. background: #eaf2ff;
  960. }
  961. .form-card-icon--green {
  962. color: #20a36a;
  963. background: #e7f8f0;
  964. }
  965. .form-card-body {
  966. padding: 18px 20px 0;
  967. }
  968. .project-form :deep(.el-select),
  969. .project-form :deep(.el-tree-select),
  970. .project-form :deep(.el-date-editor),
  971. .project-form :deep(.el-autocomplete) {
  972. width: 100%;
  973. }
  974. .project-form :deep(.el-form-item) {
  975. margin-bottom: 18px;
  976. }
  977. .project-form :deep(.el-form-item__label) {
  978. white-space: nowrap;
  979. }
  980. .responsible-person-control {
  981. display: flex;
  982. align-items: center;
  983. gap: 10px;
  984. width: 100%;
  985. min-width: 0;
  986. }
  987. .user-names {
  988. display: block;
  989. height: 32px;
  990. min-width: 0;
  991. padding: 0 11px;
  992. overflow: hidden;
  993. line-height: 30px;
  994. color: var(--el-text-color-regular);
  995. text-overflow: ellipsis;
  996. white-space: nowrap;
  997. border: 1px solid var(--el-border-color);
  998. border-radius: 4px;
  999. flex: 1;
  1000. }
  1001. .user-names--empty {
  1002. color: var(--el-text-color-placeholder);
  1003. }
  1004. .form-actions {
  1005. position: sticky;
  1006. bottom: 0;
  1007. z-index: 10;
  1008. display: flex;
  1009. flex-direction: row-reverse;
  1010. gap: 10px;
  1011. align-items: center;
  1012. min-height: 58px;
  1013. padding: 10px 16px;
  1014. background: var(--el-bg-color);
  1015. border-top: 1px solid var(--el-border-color-lighter);
  1016. box-shadow: 0 -4px 12px rgb(0 0 0 / 4%);
  1017. }
  1018. .form-actions :deep(.el-button) {
  1019. margin-left: 0;
  1020. }
  1021. .dialog-title__text {
  1022. font-size: 16px;
  1023. font-weight: 600;
  1024. line-height: 24px;
  1025. color: var(--el-text-color-primary);
  1026. }
  1027. .user-select-dialog :deep(.el-dialog__header) {
  1028. padding: 16px 20px;
  1029. margin-right: 0;
  1030. border-bottom: 1px solid var(--el-border-color-lighter);
  1031. }
  1032. .user-select-dialog :deep(.el-dialog__body) {
  1033. padding: 18px 20px;
  1034. }
  1035. .user-select-dialog :deep(.el-dialog__footer) {
  1036. padding: 12px 20px;
  1037. border-top: 1px solid var(--el-border-color-lighter);
  1038. }
  1039. .transfer-container {
  1040. width: 100%;
  1041. }
  1042. :deep(.el-transfer-panel) {
  1043. flex: 1;
  1044. width: auto;
  1045. }
  1046. .transfer-component {
  1047. display: flex;
  1048. align-items: center;
  1049. width: 100%;
  1050. min-width: 0;
  1051. }
  1052. .transfer-component :deep(.el-transfer__buttons) {
  1053. flex: 0 0 auto;
  1054. padding: 0 16px;
  1055. }
  1056. :deep(.el-transfer-panel__item) {
  1057. display: flex;
  1058. align-items: center;
  1059. height: 32px;
  1060. padding: 0 8px;
  1061. margin: 0;
  1062. overflow: hidden;
  1063. line-height: 32px;
  1064. text-overflow: ellipsis;
  1065. white-space: nowrap;
  1066. }
  1067. .transfer-option-text {
  1068. display: inline-block;
  1069. max-width: 100%;
  1070. overflow: hidden;
  1071. text-overflow: ellipsis;
  1072. white-space: nowrap;
  1073. }
  1074. :deep(.el-transfer-panel__list) {
  1075. width: 100%;
  1076. }
  1077. :global(.work-area-autocomplete .el-autocomplete-suggestion__list) {
  1078. max-height: 300px;
  1079. overflow-y: auto;
  1080. }
  1081. :global(.work-area-autocomplete .el-autocomplete-suggestion__wrap) {
  1082. max-height: 300px;
  1083. }
  1084. @media (width <= 768px) {
  1085. .page-header :deep(.el-card__body) {
  1086. align-items: flex-start;
  1087. }
  1088. .page-header__subtitle {
  1089. display: none;
  1090. }
  1091. .form-card-body {
  1092. padding-right: 12px;
  1093. padding-left: 12px;
  1094. }
  1095. .user-select-dialog :deep(.el-dialog) {
  1096. width: calc(100% - 24px) !important;
  1097. }
  1098. .transfer-component {
  1099. display: block;
  1100. }
  1101. .transfer-component :deep(.el-transfer-panel) {
  1102. width: 100%;
  1103. }
  1104. .transfer-component :deep(.el-transfer__buttons) {
  1105. display: flex;
  1106. justify-content: center;
  1107. padding: 12px 0;
  1108. }
  1109. }
  1110. </style>