workOrderDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. <template>
  2. <u-popup
  3. :show="visible"
  4. mode="bottom"
  5. :round="10"
  6. :closeOnClickOverlay="false"
  7. @close="cancel"
  8. class="workorder-popup"
  9. >
  10. <view class="popup-content">
  11. <!-- 头部 -->
  12. <view class="popup-header">
  13. <text class="popup-title">{{ dialogTitle }}</text>
  14. <view class="close-btn" @click="cancel">×</view>
  15. </view>
  16. <scroll-view class="popup-body" scroll-y>
  17. <view class="page">
  18. <view class="card-a">
  19. <!-- ===== 基本信息 ===== -->
  20. <view class="card-section">
  21. <view class="section-title">📋 基本信息</view>
  22. <u--form
  23. labelPosition="left"
  24. labelWidth="160rpx"
  25. :model="form"
  26. :rules="rules"
  27. ref="formRef"
  28. >
  29. <u-form-item label="应急演练计划" prop="planName" borderBottom>
  30. <u--input v-model="form.planName" disabled border="none" inputAlign="right" />
  31. </u-form-item>
  32. <u-form-item label="应急预案" prop="emergencyPlanName" borderBottom>
  33. <view class="link-text" @click="handleViewPlan">
  34. {{ form.emergencyPlanName || '请选择' }}
  35. </view>
  36. <u-icon slot="right" name="arrow-right" />
  37. </u-form-item>
  38. <u-form-item label="演练名称" prop="name" borderBottom required>
  39. <u--input
  40. v-model="form.name"
  41. placeholder="请输入"
  42. :disabled="isView"
  43. border="none"
  44. inputAlign="right"
  45. />
  46. </u-form-item>
  47. <u-form-item label="演练类型" prop="drillType" borderBottom required>
  48. <view class="picker-value" @click="!isView && showTypePicker()">
  49. {{ getDictValue('应急演练类型', form.drillType) || '请选择' }}
  50. </view>
  51. <u-icon slot="right" name="arrow-right" />
  52. </u-form-item>
  53. <u-form-item label="主管单位" prop="superviseDeptName" borderBottom>
  54. <u--input v-model="form.superviseDeptName" disabled border="none" inputAlign="right" />
  55. </u-form-item>
  56. <u-form-item label="演练地点" prop="planLocation" borderBottom required>
  57. <u--input
  58. v-model="form.planLocation"
  59. placeholder="请输入"
  60. :disabled="isView"
  61. border="none"
  62. inputAlign="right"
  63. />
  64. </u-form-item>
  65. <u-form-item label="附件" prop="attachments" borderBottom>
  66. <fileMain
  67. v-model="form.attachments"
  68. :type="isView ? 'view' : ''"
  69. />
  70. </u-form-item>
  71. </u--form>
  72. </view>
  73. <!-- ===== 参演人员 ===== -->
  74. <view class="card-section">
  75. <view class="section-title">
  76. 👥 参演人员
  77. <text class="add-user-btn" v-if="!isView" @click="addUser">+ 添加</text>
  78. </view>
  79. <view
  80. v-for="(person, idx) in form.participantPersonInfo"
  81. :key="idx"
  82. class="person-item"
  83. >
  84. <view class="person-info">
  85. <text class="person-name">{{ person.name }}</text>
  86. <text class="person-dept">{{ person.groupName }}</text>
  87. <text class="person-phone">{{ person.phone }}</text>
  88. </view>
  89. <view class="person-delete" v-if="!isView" @click="removePerson(idx)">
  90. <text>✕</text>
  91. </view>
  92. </view>
  93. <view v-if="form.participantPersonInfo.length === 0" class="empty-tip">
  94. 暂无参演人员
  95. </view>
  96. </view>
  97. <!-- ===== 演练记录(仅查看) ===== -->
  98. <view class="card-section" v-if="isView">
  99. <view class="section-title">📝 演练记录</view>
  100. <view class="info-item">
  101. <text class="info-label">演练实施记录</text>
  102. <text class="info-value">{{ form.processRecord || '-' }}</text>
  103. </view>
  104. <view class="info-item">
  105. <text class="info-label">演练时间</text>
  106. <text class="info-value">{{ form.drillTime || '-' }}</text>
  107. </view>
  108. <view class="info-item">
  109. <text class="info-label">附件</text>
  110. <fileMain v-model="form.processRecordAttachment" type="view" />
  111. </view>
  112. </view>
  113. <!-- ===== 验收信息(仅查看) ===== -->
  114. <view class="card-section" v-if="isView">
  115. <view class="section-title">✅ 验收信息</view>
  116. <view class="info-item">
  117. <text class="info-label">演练效果和总结评价</text>
  118. <text class="info-value">{{ form.drillEvaluation || '-' }}</text>
  119. </view>
  120. <view class="info-item">
  121. <text class="info-label">存在的问题及整改措施</text>
  122. <text class="info-value">{{ form.drillEvaluationMeasure || '-' }}</text>
  123. </view>
  124. <view class="info-item">
  125. <text class="info-label">整改情况</text>
  126. <text class="info-value">{{ form.inspectionRecordRectification || '-' }}</text>
  127. </view>
  128. <view class="info-item">
  129. <text class="info-label">验收结果</text>
  130. <text class="info-value">{{ form.inspectionResult || '-' }}</text>
  131. </view>
  132. <view class="info-item">
  133. <text class="info-label">验收人</text>
  134. <text class="info-value">{{ form.inspectorName || '-' }}</text>
  135. </view>
  136. <view class="info-item">
  137. <text class="info-label">附件</text>
  138. <fileMain v-model="form.inspectionRecordAttachment" type="view" />
  139. </view>
  140. </view>
  141. </view>
  142. </view>
  143. </scroll-view>
  144. <!-- 底部按钮 -->
  145. <view class="popup-footer">
  146. <u-button type="default" @click="cancel">取消</u-button>
  147. <u-button
  148. v-if="!isView"
  149. type="primary"
  150. @click="save"
  151. :loading="loading"
  152. >保存</u-button>
  153. </view>
  154. </view>
  155. <!-- 演练类型选择器 -->
  156. <u-action-sheet
  157. :show="showTypePickerPopup"
  158. :actions="typeActions"
  159. title="请选择演练类型"
  160. @close="showTypePickerPopup = false"
  161. @select="onTypeSelect"
  162. />
  163. <!-- 应急预案查看弹窗 -->
  164. <emergencyPlanDialog ref="emergencyPlanDialogRef" />
  165. <u-toast ref="uToast" />
  166. </u-popup>
  167. </template>
  168. <script>
  169. import emergencyPlanDialog from './emergencyPlanDialog.vue';
  170. import fileMain from '@/pages/doc/index.vue';
  171. import { save, getById, implement } from '@/api/emergencyDrill/workOrder.js';
  172. import dictMixins from '@/mixins/dictMixins';
  173. export default {
  174. components: { fileMain,emergencyPlanDialog },
  175. mixins: [dictMixins],
  176. data() {
  177. return {
  178. visible: false,
  179. loading: false,
  180. dialogType: 'add', // add, edit, view
  181. form: this.getDefaultForm(),
  182. showTypePickerPopup: false,
  183. typeActions: [],
  184. rules: {
  185. name: { type: 'string', required: true, message: '请输入演练名称', trigger: ['blur', 'change'] },
  186. drillType: { type: 'string', required: true, message: '请选择演练类型', trigger: ['change'] },
  187. planLocation: { type: 'string', required: true, message: '请输入演练地点', trigger: ['blur', 'change'] }
  188. }
  189. };
  190. },
  191. computed: {
  192. dialogTitle() {
  193. const map = { add: '新增工单', edit: '编辑工单', view: '工单详情' };
  194. return map[this.dialogType] || '工单信息';
  195. },
  196. isView() {
  197. return this.dialogType === 'view';
  198. }
  199. },
  200. methods: {
  201. getDefaultForm() {
  202. return {
  203. id: '',
  204. planId: '',
  205. planName: '',
  206. emergencyPlanId: '',
  207. emergencyPlanName: '',
  208. name: '',
  209. drillType: '',
  210. superviseDeptId: '',
  211. superviseDeptName: '',
  212. planLocation: '',
  213. attachments: [],
  214. participantPersonInfo: [],
  215. processRecord: '',
  216. drillTime: '',
  217. processRecordAttachment: [],
  218. drillEvaluation: '',
  219. drillEvaluationMeasure: '',
  220. inspectionRecordRectification: '',
  221. inspectionResult: '',
  222. inspectorName: '',
  223. inspectionRecordAttachment: [],
  224. status: 0
  225. };
  226. },
  227. async open(row, type = 'add') {
  228. this.visible = true;
  229. this.dialogType = type;
  230. this.loading = false;
  231. this.form = this.getDefaultForm();
  232. if (row && row.id && type != 'add') {
  233. try {
  234. const data = await getById(row.id);
  235. this.form = { ...data };
  236. } catch (e) {
  237. console.error(e);
  238. }
  239. } else if (row && row.id) {
  240. // 从计划创建工单
  241. this.form.planId = row.id;
  242. this.form.planName = row.name;
  243. this.form.emergencyPlanName = row.emergencyPlanName;
  244. this.form.emergencyPlanId = row.emergencyPlanId;
  245. this.form.drillType = row.drillType;
  246. this.form.superviseDeptId = row.responsibleDeptId;
  247. this.form.superviseDeptName = row.responsibleDeptName;
  248. this.form.planLocation = row.planLocation;
  249. }
  250. // 加载演练类型选项
  251. this.typeActions = this.getDictOptions('应急演练类型').map(item => ({
  252. name: item.label,
  253. value: item.value
  254. }));
  255. this.$nextTick(() => {
  256. if (this.$refs.formRef) {
  257. this.$refs.formRef.setRules(this.rules);
  258. }
  259. });
  260. },
  261. cancel() {
  262. this.visible = false;
  263. this.form = this.getDefaultForm();
  264. },
  265. // 演练类型选择
  266. showTypePicker() {
  267. if (this.isView) return;
  268. this.showTypePickerPopup = true;
  269. },
  270. onTypeSelect(e) {
  271. this.showTypePickerPopup = false;
  272. this.form.drillType = e.value;
  273. if (this.$refs.formRef) {
  274. this.$refs.formRef.validateField('drillType');
  275. }
  276. },
  277. // 查看应急预案
  278. handleViewPlan() {
  279. if (this.form.emergencyPlanId) {
  280. this.$refs.emergencyPlanDialogRef.open({ id: this.form.emergencyPlanId }, 'view');
  281. }
  282. },
  283. // 添加人员
  284. addUser() {
  285. this.$refs.userSelectRef.open(null, 0, true);
  286. },
  287. changePersonel(list) {
  288. this.form.participantPersonInfo.push(...list);
  289. },
  290. removePerson(index) {
  291. this.form.participantPersonInfo.splice(index, 1);
  292. },
  293. // 保存
  294. async save() {
  295. try {
  296. if (this.$refs.formRef) {
  297. await this.$refs.formRef.validate();
  298. }
  299. if (this.form.participantPersonInfo.length === 0) {
  300. this.$refs.uToast.show({ type: 'warning', message: '请选择参演人员' });
  301. return;
  302. }
  303. this.loading = true;
  304. const fn = this.form.id ? save : implement;
  305. const res = await fn({ workOrder: this.form, id: this.form.planId });
  306. this.$refs.uToast.show({ type: 'success', message: '保存成功' });
  307. this.cancel();
  308. this.$emit('reload');
  309. } catch (e) {
  310. if (e && e.message) {
  311. this.$refs.uToast.show({ type: 'error', message: e.message });
  312. }
  313. } finally {
  314. this.loading = false;
  315. }
  316. }
  317. }
  318. };
  319. </script>
  320. <style lang="scss" scoped>
  321. .workorder-popup {
  322. /deep/ .u-popup__content {
  323. border-radius: 32rpx 32rpx 0 0;
  324. overflow: hidden;
  325. }
  326. .popup-content {
  327. height: calc(100vh - 100px);
  328. display: flex;
  329. flex-direction: column;
  330. background: #eff2f7;
  331. }
  332. .popup-header {
  333. display: flex;
  334. justify-content: space-between;
  335. align-items: center;
  336. padding: 30rpx 32rpx;
  337. background: #fff;
  338. border-bottom: 2rpx solid #eef2f6;
  339. flex-shrink: 0;
  340. .popup-title {
  341. font-size: 36rpx;
  342. font-weight: bold;
  343. color: #1f2b3c;
  344. }
  345. .close-btn {
  346. width: 60rpx;
  347. height: 60rpx;
  348. display: flex;
  349. align-items: center;
  350. justify-content: center;
  351. font-size: 52rpx;
  352. color: #8e9aae;
  353. line-height: 1;
  354. }
  355. }
  356. .popup-body {
  357. flex: 1;
  358. overflow-y: auto;
  359. padding: 24rpx;
  360. }
  361. .page {
  362. font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  363. }
  364. .card-a {
  365. background: #ffffff;
  366. border-radius: 48rpx;
  367. box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
  368. overflow: hidden;
  369. }
  370. .card-section {
  371. padding: 30rpx 32rpx;
  372. &:not(:last-child) {
  373. border-bottom: 2rpx solid #f0f2f5;
  374. }
  375. }
  376. .section-title {
  377. font-size: 30rpx;
  378. font-weight: 700;
  379. color: #1f2a44;
  380. margin-bottom: 24rpx;
  381. padding-left: 16rpx;
  382. border-left: 6rpx solid #4caf50;
  383. display: flex;
  384. justify-content: space-between;
  385. align-items: center;
  386. .add-user-btn {
  387. font-size: 26rpx;
  388. color: #2979ff;
  389. font-weight: normal;
  390. padding: 4rpx 16rpx;
  391. background: #e8f0fe;
  392. border-radius: 30rpx;
  393. }
  394. }
  395. /deep/ .u-form-item {
  396. padding: 12rpx 0;
  397. .u-form-item__body__left__label {
  398. font-size: 26rpx !important;
  399. color: #6c7a91 !important;
  400. }
  401. .u-input__content__field-wrapper__field {
  402. font-size: 26rpx !important;
  403. text-align: right;
  404. }
  405. }
  406. .picker-value, .link-text {
  407. font-size: 26rpx;
  408. color: #1e2a3a;
  409. text-align: right;
  410. flex: 1;
  411. min-height: 44rpx;
  412. padding: 4rpx 0;
  413. }
  414. .link-text {
  415. color: #2979ff;
  416. }
  417. // 参演人员
  418. .person-item {
  419. display: flex;
  420. align-items: center;
  421. justify-content: space-between;
  422. padding: 16rpx 20rpx;
  423. background: #f5f7fb;
  424. border-radius: 16rpx;
  425. margin-bottom: 12rpx;
  426. .person-info {
  427. display: flex;
  428. align-items: center;
  429. gap: 20rpx;
  430. flex: 1;
  431. flex-wrap: wrap;
  432. .person-name {
  433. font-size: 28rpx;
  434. font-weight: 600;
  435. color: #1f2b3c;
  436. }
  437. .person-dept {
  438. font-size: 24rpx;
  439. color: #8e9aae;
  440. }
  441. .person-phone {
  442. font-size: 24rpx;
  443. color: #8e9aae;
  444. }
  445. }
  446. .person-delete {
  447. width: 48rpx;
  448. height: 48rpx;
  449. display: flex;
  450. align-items: center;
  451. justify-content: center;
  452. color: #f56c6c;
  453. font-size: 28rpx;
  454. }
  455. }
  456. .empty-tip {
  457. text-align: center;
  458. font-size: 26rpx;
  459. color: #aaa;
  460. padding: 30rpx 0;
  461. }
  462. // 信息展示(查看模式)
  463. .info-item {
  464. display: flex;
  465. flex-direction: column;
  466. gap: 8rpx;
  467. margin-bottom: 20rpx;
  468. &:last-child {
  469. margin-bottom: 0;
  470. }
  471. .info-label {
  472. font-size: 24rpx;
  473. color: #8e9aae;
  474. }
  475. .info-value {
  476. font-size: 26rpx;
  477. color: #1f2b3c;
  478. line-height: 1.6;
  479. }
  480. }
  481. .popup-footer {
  482. display: flex;
  483. padding: 16rpx 24rpx;
  484. background: #fff;
  485. border-top: 2rpx solid #eef2f6;
  486. gap: 16rpx;
  487. flex-shrink: 0;
  488. /deep/ .u-button {
  489. flex: 1;
  490. border-radius: 48rpx;
  491. height: 72rpx;
  492. }
  493. }
  494. }
  495. </style>