indexDc.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <template>
  2. <div class="pblm-detail-wrapper">
  3. <van-form @submit="onSubmit">
  4. <div class="pblm-detail-label">
  5. <span>问题详情</span>
  6. <!-- <van-button size="mini" type="primary" @click="showTacObjPblmstbPopup()">选择违规事项</van-button> -->
  7. </div>
  8. <van-cell-group inset>
  9. <van-field v-model="tacObjPblmstb.nm" placeholder="请输入对象名称" label="对象名称" />
  10. <van-field
  11. v-model="tacObjPblmstb.inspPblmsName"
  12. is-link
  13. readonly
  14. label="问题类别"
  15. placeholder="选择对象"
  16. @click="showPicker = true"
  17. />
  18. <van-popup v-model:show="showPicker" destroy-on-close round position="bottom">
  19. <van-picker
  20. :columns="columns"
  21. @change="changeC1"
  22. @cancel="showPicker = false"
  23. @confirm="onConfirm"
  24. />
  25. </van-popup>
  26. <van-field
  27. v-model="tacObjPblmstb.checkPoint"
  28. is-link
  29. readonly
  30. label="问题项目"
  31. placeholder="选择对象"
  32. @click="showPicker1 = true"
  33. />
  34. <van-popup v-model:show="showPicker1" destroy-on-close round position="bottom">
  35. <van-picker
  36. :columns="columns1"
  37. @change="changeC2"
  38. @cancel="showPicker1 = false"
  39. @confirm="onConfirm1"
  40. />
  41. </van-popup>
  42. <van-field
  43. v-model="tacObjPblmstb.pblmDesc"
  44. is-link
  45. readonly
  46. label="问题描述"
  47. placeholder="选择对象"
  48. @click="showPicker2 = true"
  49. />
  50. <van-popup v-model:show="showPicker2" destroy-on-close round position="bottom">
  51. <van-picker
  52. :columns="columns2"
  53. @cancel="showPicker2 = false"
  54. @confirm="onConfirm2"
  55. />
  56. </van-popup>
  57. <van-field v-model="tacObjPblmstb.inspPblmOrgName" placeholder="请输入对象名称" label="问题责任单位" />
  58. <van-field v-model="tacObjPblmstb.inspPblmDesc" placeholder="请输入对象名称" label="详细描述" rows="1" type="textarea"/>
  59. <van-field
  60. v-model="tacObjPblmstb.ifCasePblm"
  61. is-link
  62. readonly
  63. label="是否典型问题"
  64. placeholder=""
  65. @click="showPicker3 = true"
  66. />
  67. <van-popup v-model:show="showPicker3" destroy-on-close round position="bottom">
  68. <van-picker
  69. :columns="columns3"
  70. :model-value="pickerValue3"
  71. @cancel="showPicker3 = false"
  72. @confirm="onConfirm3"
  73. />
  74. </van-popup>
  75. <van-field v-model="tacObjPblmstb.score" readonly label="扣分值" />
  76. <van-field v-model="tacObjPblmstb.persName" readonly label="上报人" />
  77. <van-field v-model="tacObjPblmstb.collTime" readonly label="上报时间" />
  78. <div style="display: flex;align-items: center;margin-top: -1%;">
  79. <div style="width: 20%;text-align: center;font-weight: 700;font-size: 14px;">上传照片</div>
  80. <van-uploader v-model="fileList" :after-read="afterRead" style=""/>
  81. </div>
  82. </van-cell-group>
  83. <div style="margin: 16px;">
  84. <van-button v-if="objData.state !== '2'" @click="save" block native-type="submit" round type="primary">
  85. 保存问题
  86. </van-button>
  87. </div>
  88. </van-form>
  89. <van-popup v-model:show="tacObjPblmstbShow" :style="{ width: '80%', height: '100%' }" position="left">
  90. <tacObjPblmstbList :listType="pblm.listType" style="z-index:3000;"
  91. @change="changeTacObjPblmstb"></tacObjPblmstbList>
  92. </van-popup>
  93. </div>
  94. </template>
  95. <script setup>
  96. import { showSuccessToast, showFailToast } from 'vant';
  97. import {computed, onMounted, ref, watch} from "vue";
  98. import {useRoute,useRouter} from "vue-router";
  99. import { getToken } from "@/utils/auth";
  100. import { useAppStore } from '@/stores/app';
  101. import {useUserStore} from "@/stores/user";
  102. import tacObjPblmstbList from './TacObjPblmstbList.vue';
  103. import {getTacQuestionById} from "@/api/inspect";
  104. import { showConfirmDialog } from 'vant';
  105. import {addTacQuestion, getIllegalActById, getTacUnitList} from "@/api/questions";
  106. import { jumpPage } from "@/utils/page";
  107. import request from "@/utils/request";
  108. const route = useRoute();
  109. const router1 = useRouter();
  110. const userStore = useUserStore();
  111. const fileList = ref([]);
  112. const listType = ref(route.query.inspectType || '1');
  113. const pblm = ref({});
  114. const tacObjPblmstb = ref({
  115. });
  116. const url = process.env.VUE_APP_BASE_API
  117. const columns = ref([]);
  118. const columns1 = ref([]);
  119. const columns2 = ref([]);
  120. const columns3 = ref([
  121. { text: '是', value: '1' },
  122. { text: '否', value: '0' }
  123. ]);
  124. const showPicker = ref(false);
  125. const showPicker1 = ref(false);
  126. const showPicker2 = ref(false);
  127. const showPicker3 = ref(false);
  128. const pickerValue3 = ref([])
  129. const pickerValue = ref([])
  130. const pickerValue1 = ref([])
  131. const pickerValue2 = ref([])
  132. const router = useRoute();
  133. const parSectino = ref([]);
  134. const subjectList = ref([]);
  135. const tacObjPblmstbShow = ref(false);
  136. const accessToken = localStorage.getItem('accessToken') || '';
  137. function onConfirm3(selectedValues, selectedOptions) {
  138. tacObjPblmstb.value.ifCasePblm = selectedValues.selectedOptions[0].text
  139. showPicker3.value = false;
  140. }
  141. function onConfirm(selectedValues, selectedOptions) {
  142. tacObjPblmstb.value.inspPblmsName = selectedValues.selectedOptions[0].text
  143. showPicker.value = false;
  144. }
  145. function onConfirm1(selectedValues, selectedOptions) {
  146. tacObjPblmstb.value.checkPoint = selectedValues.selectedOptions[0].text
  147. showPicker1.value = false;
  148. }
  149. function onConfirm2(selectedValues, selectedOptions) {
  150. tacObjPblmstb.value.pblmDesc = selectedValues.selectedOptions[0].text
  151. showPicker2.value = false;
  152. }
  153. function changeC1(selectedValues, selectedOptions) {
  154. columns1.value = []
  155. columns2.value = []
  156. parSectino.value.forEach(item => {
  157. if(item.inspPblmsName === selectedValues.selectedValues[0]){
  158. var par = {
  159. text: item.checkPoint,
  160. value: item.checkPoint
  161. }
  162. columns1.value.push(par);
  163. }
  164. columns1.value = [...new Set(columns1.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  165. })
  166. }
  167. function changeC2(selectedValues, selectedOptions) {
  168. columns2.value = []
  169. parSectino.value.forEach(item => {
  170. if(item.checkPoint === selectedValues.selectedValues[0]){
  171. var par = {
  172. text: item.pblmDesc,
  173. value: item.pblmDesc
  174. }
  175. columns2.value.push(par);
  176. }
  177. columns2.value = [...new Set(columns2.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  178. })
  179. console.log(columns1.value)
  180. }
  181. function formatTime(timestamp) {
  182. const date = new Date(timestamp);
  183. const year = date.getFullYear();
  184. const month = ('0' + (date.getMonth() + 1)).slice(-2);
  185. const day = ('0' + date.getDate()).slice(-2);
  186. const hours = ('0' + date.getHours()).slice(-2);
  187. const minutes = ('0' + date.getMinutes()).slice(-2);
  188. const seconds = ('0' + date.getSeconds()).slice(-2);
  189. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  190. }
  191. async function getSelection(){
  192. await request.get(`/dc/insp/pblms/listByObjId/${pblm.value.objId}`).then(res=>{
  193. parSectino.value = res.data
  194. const uniqueInspPblmsName = [...new Set(res.data.map(item => item.inspPblmsName))];
  195. const uniquePblmDesc = [...new Set(res.data.map(item => item.pblmDesc))];
  196. const uniqueCheckPoint = [...new Set(res.data.map(item => item.checkPoint))];
  197. // columns1.value = uniqueCheckPoint.map(item => ({ text: item, value: item }));
  198. // columns2.value = uniquePblmDesc.map(item => ({ text: item, value: item }));
  199. columns.value = uniqueInspPblmsName.map(item => ({ text: item, value: item }));
  200. })
  201. }
  202. const parData = ref({});
  203. function getDe(){
  204. request.get(`/dc/insp/pblm/${pblm.value.pblmId}`).then(res=>{
  205. if(res.success){
  206. parData.value = res.data
  207. tacObjPblmstb.value = res.data;
  208. tacObjPblmstb.value.ifCasePblm = tacObjPblmstb.value.ifCasePblm === '1' ? '是' : '否';
  209. tacObjPblmstb.value.collTime = formatTime(tacObjPblmstb.value.collTime);
  210. columns1.value = []
  211. columns2.value = []
  212. parSectino.value.forEach(item => {
  213. if(item.inspPblmsName === tacObjPblmstb.value.inspPblmsName){
  214. var par = {
  215. text: item.checkPoint,
  216. value: item.checkPoint
  217. }
  218. columns1.value.push(par);
  219. }
  220. columns1.value = [...new Set(columns1.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  221. })
  222. parSectino.value.forEach(item => {
  223. if(item.checkPoint === tacObjPblmstb.value.checkPoint){
  224. var par = {
  225. text: item.pblmDesc,
  226. value: item.pblmDesc
  227. }
  228. columns2.value.push(par);
  229. }
  230. columns2.value = [...new Set(columns2.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  231. })
  232. res.data.gwComFiles.forEach(item => {
  233. item.filePath = item.filePath.replace(/\\/g, '/');
  234. fileList.value.push({
  235. url: url + item.filePath,
  236. name: item.fileName,
  237. isImage: true,
  238. id: item.id
  239. })
  240. console.log(fileList.value);
  241. })
  242. }
  243. })
  244. }
  245. async function save(){
  246. console.log(tacObjPblmstb.value);
  247. tacObjPblmstb.value.ifCasePblm = tacObjPblmstb.value.ifCasePblm === '是' ? '1' : '0';
  248. // tacObjPblmstb.value.collTime = parData.value.collTime;
  249. delete tacObjPblmstb.value.inspPblmCode;
  250. delete tacObjPblmstb.value.pblmsTypeId;
  251. delete tacObjPblmstb.value.collTime;
  252. // uploadFileToServer(newFile.value, parData.value.pblmId)
  253. await request.post('/dc/insp/pblm/update', tacObjPblmstb.value).then((res) => {
  254. if (res.success) {
  255. fileList.value.forEach(item => {
  256. console.log(item)
  257. if(!item.id){
  258. uploadFileToServer(item, parData.value.pblmId)
  259. }
  260. })
  261. showSuccessToast('保存成功!');
  262. var par = JSON.parse(router.query.object);
  263. jumpPage(`/InspectDC/${par.parObj.planId}/object/${par.parObj.objId}/problems`, {
  264. object: JSON.stringify(par.parObj),
  265. })
  266. // fileList.value = []
  267. }
  268. });
  269. // getDe();
  270. }
  271. watch(() => tacObjPblmstb.value.inspPblmsName, (newVal, oldVal) => {
  272. if (newVal !== oldVal) {
  273. columns1.value = []
  274. columns2.value = []
  275. parSectino.value.forEach(item => {
  276. if(item.inspPblmsName === newVal){
  277. var par = {
  278. text: item.checkPoint,
  279. value: item.checkPoint
  280. }
  281. columns1.value.push(par);
  282. }
  283. columns1.value = [...new Set(columns1.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  284. })
  285. }
  286. });
  287. watch(() => tacObjPblmstb.value.checkPoint, (newVal, oldVal) => {
  288. if (newVal !== oldVal) {
  289. columns2.value = []
  290. parSectino.value.forEach(item => {
  291. if(item.checkPoint === newVal){
  292. var par = {
  293. text: item.pblmDesc,
  294. value: item.pblmDesc
  295. }
  296. columns2.value.push(par);
  297. }
  298. columns2.value = [...new Set(columns2.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  299. })
  300. }
  301. });
  302. function getObjectArrayDiff(arr1, arr2, key) {
  303. if (!key) throw new Error('必须指定对比的唯一字段(如id)');
  304. // 提取两个数组的key值集合
  305. const keySet1 = new Set(arr1.map(item => item[key]));
  306. const keySet2 = new Set(arr2.map(item => item[key]));
  307. // 仅在arr1中存在的对象
  308. const onlyInArr1 = arr1.filter(item => !keySet2.has(item[key]));
  309. // 仅在arr2中存在的对象
  310. const onlyInArr2 = arr2.filter(item => !keySet1.has(item[key]));
  311. // 所有差异对象(合并)
  312. const allDiff = [...onlyInArr1, ...onlyInArr2];
  313. return {
  314. onlyInArr1,
  315. onlyInArr2,
  316. allDiff
  317. };
  318. }
  319. const dataURLtoBlob = (dataURL) => {
  320. try {
  321. const arr = dataURL.split(',');
  322. const mime = arr[0].match(/:(.*?);/)[1];
  323. const bstr = atob(arr[1]);
  324. let n = bstr.length;
  325. const u8arr = new Uint8Array(n);
  326. while (n--) {
  327. u8arr[n] = bstr.charCodeAt(n);
  328. }
  329. return new Blob([u8arr], { type: mime });
  330. } catch (e) {
  331. console.error('base64转Blob失败:', e);
  332. return new Blob([], { type: 'application/octet-stream' });
  333. }
  334. };
  335. // 2. 手动构建multipart/form-data(保留之前验证正确的逻辑)
  336. const buildMultipartFormData = async (file) => {
  337. if (!file) throw new Error('文件为空');
  338. console.log(file.content)
  339. // 生成标准boundary
  340. const boundary = `----WebKitFormBoundary${Math.random().toString(36).substr(2, 16)}`;
  341. // 处理文件为Blob
  342. let fileBlob;
  343. if (file.content instanceof Blob || file.content instanceof File) {
  344. fileBlob = file.content;
  345. } else if (typeof file.content === 'string' && file.content.startsWith('data:')) {
  346. fileBlob = dataURLtoBlob(file.content);
  347. } else {
  348. throw new Error(`不支持的文件格式: ${typeof file.content}`);
  349. }
  350. // 读取文件为ArrayBuffer
  351. const fileBuffer = await new Promise((resolve, reject) => {
  352. const reader = new FileReader();
  353. reader.onload = () => resolve(reader.result);
  354. reader.onerror = reject;
  355. reader.readAsArrayBuffer(fileBlob);
  356. });
  357. // 手动拼接multipart格式(字符串转Uint8Array,兼容App端)
  358. const stringToUint8Array = (str) => {
  359. const arr = [];
  360. for (let i = 0; i < str.length; i++) {
  361. arr.push(str.charCodeAt(i));
  362. }
  363. return new Uint8Array(arr);
  364. };
  365. const parts = [];
  366. // 拼接分隔符+元信息
  367. parts.push(stringToUint8Array(`--${boundary}\r\n`));
  368. parts.push(stringToUint8Array(`Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(file.name || 'upload.png')}"\r\n`));
  369. parts.push(stringToUint8Array(`Content-Type: ${fileBlob.type || 'application/octet-stream'}\r\n\r\n`));
  370. // 拼接文件二进制数据
  371. parts.push(new Uint8Array(fileBuffer));
  372. // 拼接结束分隔符
  373. parts.push(stringToUint8Array(`\r\n--${boundary}--\r\n`));
  374. // 合并所有片段
  375. const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
  376. const resultBuffer = new Uint8Array(totalLength);
  377. let offset = 0;
  378. parts.forEach(part => {
  379. resultBuffer.set(part, offset);
  380. offset += part.length;
  381. });
  382. return {
  383. body: resultBuffer,
  384. contentType: `multipart/form-data; boundary=${boundary}`
  385. };
  386. };
  387. // 3. 核心:改用Fetch API上传文件(替换Axios)
  388. const uploadFileToServer = async (file, bizId) => {
  389. if (!file || !bizId) {
  390. showFailToast('文件或业务ID为空');
  391. return null;
  392. }
  393. try {
  394. // 构建标准multipart请求体
  395. const { body, contentType } = await buildMultipartFormData(file);
  396. // 获取token和自定义头(和Axios拦截器一致)
  397. const appStore = useAppStore();
  398. const userStore = useUserStore();
  399. const token = getToken();
  400. // 拼接完整接口地址(补充baseURL,和你的request封装一致)
  401. const baseURL = process.env.VUE_APP_BASE_API;
  402. const fullUrl = `${baseURL}/file/insert?bizId=${bizId}`;
  403. // 使用Fetch API发送请求(原生支持二进制,无兼容性问题)
  404. const formData = new FormData();
  405. // formData.append('file', fileList.value[0].content); // 文件参数
  406. const response = await fetch(fullUrl, {
  407. method: 'POST',
  408. headers: {
  409. 'Content-Type': contentType, // 带boundary的标准格式
  410. 'Accept': 'application/json',
  411. 'Persid': userStore.userId,
  412. 'Accesstoken': token,
  413. 'Cache-Control': 'no-cache'
  414. },
  415. body: new Blob([body], { type: contentType }), // 二进制请求体
  416. timeout: 30000,
  417. credentials: 'include' // 保持cookie/token会话(和Axios一致)
  418. });
  419. // 解析响应
  420. if (!response.ok) {
  421. throw new Error(`请求失败: ${response.status}`);
  422. }
  423. const resData = await response.json();
  424. console.log('文件上传成功:', resData);
  425. return resData;
  426. } catch (error) {
  427. console.error('文件上传失败:', error);
  428. showFailToast('文件上传失败');
  429. throw error;
  430. }
  431. };
  432. // /pdcApi/file
  433. const newFile = ref({})
  434. const isAdding = ref(false);
  435. watch(() => fileList.value, (newVal, oldVal) => {
  436. if (newVal.length>oldVal.length && objData.value.state !== '2') {
  437. isAdding.value = true;
  438. newFile.value = getObjectArrayDiff(newVal, oldVal, 'id').onlyInArr1[0]
  439. console.log(newFile.value);
  440. }
  441. else if(newVal.length<oldVal.length && objData.value.state !== '2'){
  442. isAdding.value = false;
  443. newFile.value = getObjectArrayDiff(newVal, oldVal, 'id').onlyInArr2[0];
  444. showConfirmDialog({
  445. title: '删除',
  446. message:
  447. '是否确认删除',
  448. })
  449. .then(() => {
  450. request.post(`/file/${newFile.value.id}`).then(res=>{
  451. if(res.success){
  452. showSuccessToast('删除成功!');
  453. }
  454. })
  455. })
  456. .catch(() => {
  457. fileList.value = oldVal;
  458. });
  459. }
  460. });
  461. const objData = ref({});
  462. onMounted(() => {
  463. var par = JSON.parse(router.query.object);
  464. objData.value = par;
  465. objData.value.state = route.params.id
  466. console.log(objData.value);
  467. pblm.value.objId = par.objId
  468. pblm.value.pblmId = par.pblmId;
  469. getSelection().then(() => {
  470. getDe();
  471. })
  472. })
  473. </script>
  474. <style lang="scss" scoped>
  475. .pblm-detail-wrapper {
  476. height: 120%;
  477. padding: 10px 0;
  478. overflow: auto;
  479. background-color: #f5f7fa;
  480. .pblm-detail-label {
  481. padding: 10px 16px;
  482. color: #606266;
  483. display: flex;
  484. align-items: center;
  485. justify-content: space-between;
  486. }
  487. }
  488. </style>
  489. <style lang="scss">
  490. .van-cell-group--inset {
  491. margin: 0 10px;
  492. }
  493. </style>