snapshotDialog.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <template>
  2. <u-popup
  3. :show="visible"
  4. :round="0"
  5. :closeOnClickOverlay="false"
  6. :zIndex="99999"
  7. @close="handleCancel"
  8. class="u-popup-my"
  9. >
  10. <view class="popup-content">
  11. <view class="popup-header">
  12. <text class="popup-title">{{ dialogTitle }}</text>
  13. <view class="close-btn" @click="handleCancel">×</view>
  14. </view>
  15. <scroll-view class="popup-body" scroll-y>
  16. <view class="page">
  17. <view class="card-a">
  18. <!-- 头部 -->
  19. <view class="a-header">
  20. <text class="a-main-title">随手拍记录</text>
  21. </view>
  22. <!-- 基础信息 -->
  23. <view class="card-section">
  24. <view class="section-title">📝 基础信息</view>
  25. <view class="info-list">
  26. <view class="info-item">
  27. <text class="info-label">问题描述</text>
  28. <textarea
  29. v-if="canEdit"
  30. class="info-textarea"
  31. v-model="formData.description"
  32. placeholder="请输入问题描述"
  33. :disabled="!canEdit"
  34. />
  35. <text v-else class="info-value readonly">{{
  36. formData.description
  37. }}</text>
  38. </view>
  39. <view class="info-item">
  40. <text class="info-label">问题所在位置</text>
  41. <input
  42. v-if="canEdit"
  43. class="info-input"
  44. v-model="formData.location"
  45. placeholder="请输入位置"
  46. :disabled="!canEdit"
  47. />
  48. <text v-else class="info-value readonly">{{
  49. formData.location
  50. }}</text>
  51. </view>
  52. <view class="info-item">
  53. <text class="info-label">问题所属部门</text>
  54. <view
  55. class="info-value"
  56. :class="{ disabled: !canEdit }"
  57. @click="title != '详情' && $refs.treePicker._show()"
  58. >
  59. {{ formData.problemDeptName || "请选择" }}
  60. </view>
  61. </view>
  62. <view class="info-item">
  63. <text class="info-label">随手拍图片</text>
  64. <fileMain
  65. v-model="formData.attachment"
  66. :type="canEdit ? '' : 'view'"
  67. />
  68. </view>
  69. </view>
  70. </view>
  71. <!-- 上报信息 -->
  72. <view class="card-section">
  73. <view class="section-title">👤 上报信息</view>
  74. <view class="info-list">
  75. <view class="info-item">
  76. <text class="info-label">上报人</text>
  77. <text class="info-value readonly">{{
  78. formData.reporterName
  79. }}</text>
  80. </view>
  81. <view class="info-item">
  82. <text class="info-label">上报人部门</text>
  83. <text class="info-value readonly">{{
  84. formData.reporterDeptName
  85. }}</text>
  86. </view>
  87. <view class="info-item" v-if="!canEdit">
  88. <text class="info-label">处理结果</text>
  89. <view>
  90. <text
  91. v-if="formData.handleResult == 0"
  92. class="status-pending"
  93. >待处理</text
  94. >
  95. <text
  96. v-else
  97. class="status-link"
  98. :class="{
  99. 'status-done': formData.handleResult == 1,
  100. 'status-discard': formData.handleResult == 2,
  101. }"
  102. @click="handleViewResult"
  103. >{{
  104. getResultLabel(formData.handleResult)
  105. }}(点击查看)</text
  106. >
  107. </view>
  108. </view>
  109. </view>
  110. </view>
  111. </view>
  112. </view>
  113. </scroll-view>
  114. <view class="popup-footer">
  115. <template v-if="isView">
  116. <u-button type="default" @click="handleCancel">关闭</u-button>
  117. </template>
  118. <template v-else-if="isHandle">
  119. <u-button type="default" @click="handleDiscard">废弃</u-button>
  120. <u-button type="primary" @click="handleRectify">下发整改</u-button>
  121. </template>
  122. <template v-else>
  123. <u-button type="default" @click="handleCancel">取消</u-button>
  124. <u-button type="primary" @click="handleSubmit" :loading="loading"
  125. >保存</u-button
  126. >
  127. </template>
  128. </view>
  129. </view>
  130. <u-toast ref="uToast"></u-toast>
  131. <ba-tree-picker
  132. ref="treePicker"
  133. key="verify"
  134. :multiple="false"
  135. @select-change="searchDeptNodeClick"
  136. title="选择部门"
  137. :localdata="classificationList"
  138. valueKey="id"
  139. textKey="name"
  140. childrenKey="children"
  141. />
  142. </u-popup>
  143. </template>
  144. <script>
  145. import { getById, save, update } from "@/api/snapshot/index.js";
  146. import fileMain from "@/pages/doc/index.vue";
  147. import { listOrganizations } from "@/api/common.js";
  148. import { toTreeData } from "@/utils/utils.js";
  149. export default {
  150. emits: ["reload", "rectify", "discard"],
  151. components: {
  152. fileMain,
  153. },
  154. data() {
  155. return {
  156. visible: false,
  157. loading: false,
  158. dialogType: "add",
  159. formData: this.getDefaultForm(),
  160. classificationList: [],
  161. resultOptions: [
  162. {
  163. value: 1,
  164. label: "下发整改",
  165. },
  166. {
  167. value: 2,
  168. label: "废弃",
  169. },
  170. ],
  171. };
  172. },
  173. computed: {
  174. dialogTitle() {
  175. const map = {
  176. add: "新增随手拍",
  177. edit: "编辑随手拍",
  178. view: "随手拍详情",
  179. handle: "随手拍处理",
  180. };
  181. return map[this.dialogType] || "随手拍";
  182. },
  183. isView() {
  184. return this.dialogType === "view";
  185. },
  186. isHandle() {
  187. return this.dialogType === "handle";
  188. },
  189. canEdit() {
  190. return ["add", "edit"].includes(this.dialogType);
  191. },
  192. },
  193. methods: {
  194. getDefaultForm() {
  195. return {
  196. id: "",
  197. description: "",
  198. location: "",
  199. problemDeptId: "",
  200. problemDeptName: "",
  201. attachment: [],
  202. reporterId: "",
  203. reporterName: "",
  204. reporterDeptId: "",
  205. reporterDeptName: "",
  206. handleResult: 0,
  207. handleOpinion: "",
  208. invHazardId: "",
  209. };
  210. },
  211. async getTreeList() {
  212. const data = await listOrganizations({});
  213. let treeList = toTreeData({
  214. data: data || [],
  215. idField: "id",
  216. parentIdField: "parentId",
  217. });
  218. this.classificationList = treeList;
  219. },
  220. async open(type, row = null) {
  221. console.log(row, "type");
  222. this.dialogType = type;
  223. if (row && row.id && ["edit", "view", "handle"].includes(type)) {
  224. try {
  225. const data = await getById(row.id);
  226. this.formData = {
  227. ...data,
  228. attachment: data.attachment || [],
  229. };
  230. } catch (e) {
  231. console.error(e);
  232. }
  233. } else {
  234. this.formData = this.getDefaultForm();
  235. const userInfo = uni.getStorageSync("userInfo") || {};
  236. this.formData.reporterName = userInfo.name || "";
  237. this.formData.reporterId = userInfo.userId || "";
  238. this.formData.reporterDeptName = userInfo.groupName || "";
  239. this.formData.reporterDeptId = userInfo.groupId || "";
  240. }
  241. this.getTreeList();
  242. this.visible = true;
  243. },
  244. //选择部门(搜索)
  245. async searchDeptNodeClick(id, name, type) {
  246. this.formData.problemDeptId = id[0];
  247. this.formData.problemDeptName = name;
  248. },
  249. handleCancel() {
  250. this.visible = false;
  251. this.resetForm();
  252. },
  253. resetForm() {
  254. this.formData = this.getDefaultForm();
  255. },
  256. getResultLabel(result) {
  257. const item = this.resultOptions.find((opt) => opt.value == result);
  258. return item ? item.label : "待处理";
  259. },
  260. previewImage(url) {
  261. uni.previewImage({
  262. urls: [url],
  263. });
  264. },
  265. handleViewResult() {
  266. console.log(this.formData)
  267. if (this.formData.handleResult == 2) {
  268. this.$emit("discard", this.formData, "view");
  269. } else if (this.formData.handleResult == 1 && this.formData.invHazardId) {
  270. this.$emit("rectify", { id: this.formData.invHazardId }, "view");
  271. }
  272. },
  273. async handleSubmit() {
  274. // 简单校验
  275. if (!this.formData.description) {
  276. this.$refs.uToast.show({
  277. type: "error",
  278. message: "请输入问题描述",
  279. });
  280. return;
  281. }
  282. if (!this.formData.location) {
  283. this.$refs.uToast.show({
  284. type: "error",
  285. message: "请输入问题所在位置",
  286. });
  287. return;
  288. }
  289. const api = this.dialogType === "add" ? save : update;
  290. this.loading = true;
  291. try {
  292. await api(this.formData);
  293. this.$refs.uToast.show({
  294. type: "success",
  295. message: this.dialogType === "add" ? "新增成功" : "编辑成功",
  296. });
  297. this.visible = false;
  298. this.$emit("reload");
  299. this.resetForm();
  300. } catch (e) {
  301. console.error(e);
  302. } finally {
  303. this.loading = false;
  304. }
  305. },
  306. handleRectify() {
  307. this.$emit("rectify", this.formData);
  308. this.visible = false;
  309. },
  310. handleDiscard() {
  311. this.$emit("discard", this.formData);
  312. this.visible = false;
  313. },
  314. },
  315. };
  316. </script>
  317. <style lang="scss" scoped>
  318. .popup-content {
  319. width: 100vw;
  320. height: calc(100vh - 100px);
  321. background: #fff;
  322. border-radius: 0;
  323. display: flex;
  324. flex-direction: column;
  325. background-color: #eff2f7;
  326. }
  327. .popup-header {
  328. display: flex;
  329. justify-content: space-between;
  330. align-items: center;
  331. padding: 30rpx;
  332. border-bottom: 1rpx solid #e5e5e5;
  333. background: #fff;
  334. .popup-title {
  335. font-size: 36rpx;
  336. font-weight: bold;
  337. color: #333;
  338. }
  339. .close-btn {
  340. width: 60rpx;
  341. height: 60rpx;
  342. display: flex;
  343. align-items: center;
  344. justify-content: center;
  345. font-size: 60rpx;
  346. color: #999;
  347. line-height: 1;
  348. }
  349. }
  350. .popup-body {
  351. flex: 1;
  352. overflow-y: auto;
  353. padding: 28rpx;
  354. }
  355. .page {
  356. font-family:
  357. system-ui,
  358. -apple-system,
  359. "Segoe UI",
  360. Roboto,
  361. Helvetica,
  362. 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. .a-header {
  371. background: linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%);
  372. padding: 40rpx 32rpx 24rpx;
  373. text-align: center;
  374. .a-main-title {
  375. font-size: 36rpx;
  376. font-weight: 800;
  377. color: #2e7d32;
  378. }
  379. }
  380. .card-section {
  381. padding: 30rpx 32rpx;
  382. border-bottom: 2rpx solid #f0f2f5;
  383. &:last-child {
  384. border-bottom: none;
  385. }
  386. }
  387. .section-title {
  388. font-size: 30rpx;
  389. font-weight: 700;
  390. color: #1f2a44;
  391. margin-bottom: 24rpx;
  392. padding-left: 16rpx;
  393. border-left: 6rpx solid #4caf50;
  394. }
  395. .info-list {
  396. display: flex;
  397. flex-direction: column;
  398. gap: 28rpx;
  399. }
  400. .info-item {
  401. display: flex;
  402. flex-direction: column;
  403. gap: 8rpx;
  404. }
  405. .info-label {
  406. font-size: 26rpx;
  407. font-weight: 600;
  408. color: #6c7a91;
  409. }
  410. .info-value {
  411. font-size: 28rpx;
  412. font-weight: 500;
  413. color: #1e2a3a;
  414. padding: 16rpx 20rpx;
  415. border-radius: 24rpx;
  416. border: 2rpx solid #e9edf2;
  417. background: #fff;
  418. &.readonly {
  419. color: #999;
  420. background: #f5f5f5;
  421. }
  422. }
  423. .info-input,
  424. .info-textarea {
  425. font-size: 28rpx;
  426. color: #1e2a3a;
  427. height: 60rpx;
  428. padding: 16rpx 20rpx;
  429. border-radius: 24rpx;
  430. border: 2rpx solid #e9edf2;
  431. background: #fff;
  432. }
  433. .info-textarea {
  434. min-height: 160rpx;
  435. }
  436. .image-list {
  437. display: flex;
  438. flex-wrap: wrap;
  439. gap: 16rpx;
  440. image {
  441. width: 160rpx;
  442. height: 160rpx;
  443. border-radius: 16rpx;
  444. background: #f5f5f5;
  445. object-fit: cover;
  446. }
  447. }
  448. .status-pending {
  449. color: #e6a23c;
  450. background: #fff3e0;
  451. padding: 8rpx 20rpx;
  452. border-radius: 32rpx;
  453. font-size: 26rpx;
  454. display: inline-block;
  455. }
  456. .status-done {
  457. color: #67c23a;
  458. background: #e0f3e8;
  459. padding: 8rpx 20rpx;
  460. border-radius: 32rpx;
  461. font-size: 26rpx;
  462. display: inline-block;
  463. }
  464. .status-discard {
  465. color: #f56c6c;
  466. background: #fef0f0;
  467. padding: 8rpx 20rpx;
  468. border-radius: 32rpx;
  469. font-size: 26rpx;
  470. display: inline-block;
  471. }
  472. .status-link {
  473. text-decoration: none;
  474. }
  475. .popup-footer {
  476. display: flex;
  477. padding: 20rpx 30rpx;
  478. border-top: 1rpx solid #e5e5e5;
  479. background: #fff;
  480. gap: 20rpx;
  481. /deep/ .u-button {
  482. flex: 1;
  483. }
  484. }
  485. </style>