indexDc.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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 request from "@/utils/request";
  107. const route = useRoute();
  108. const router1 = useRouter();
  109. const userStore = useUserStore();
  110. const fileList = ref([]);
  111. const listType = ref(route.query.inspectType || '1');
  112. const pblm = ref({});
  113. const tacObjPblmstb = ref({
  114. });
  115. const url = process.env.VUE_APP_BASE_API
  116. const columns = ref([]);
  117. const columns1 = ref([]);
  118. const columns2 = ref([]);
  119. const columns3 = ref([
  120. { text: '是', value: '1' },
  121. { text: '否', value: '0' }
  122. ]);
  123. const showPicker = ref(false);
  124. const showPicker1 = ref(false);
  125. const showPicker2 = ref(false);
  126. const showPicker3 = ref(false);
  127. const pickerValue3 = ref([])
  128. const pickerValue = ref([])
  129. const pickerValue1 = ref([])
  130. const pickerValue2 = ref([])
  131. const router = useRoute();
  132. const parSectino = ref([]);
  133. const subjectList = ref([]);
  134. const tacObjPblmstbShow = ref(false);
  135. const accessToken = localStorage.getItem('accessToken') || '';
  136. function onConfirm3(selectedValues, selectedOptions) {
  137. tacObjPblmstb.value.ifCasePblm = selectedValues.selectedOptions[0].text
  138. showPicker3.value = false;
  139. }
  140. function onConfirm(selectedValues, selectedOptions) {
  141. tacObjPblmstb.value.inspPblmsName = selectedValues.selectedOptions[0].text
  142. showPicker.value = false;
  143. }
  144. function onConfirm1(selectedValues, selectedOptions) {
  145. tacObjPblmstb.value.checkPoint = selectedValues.selectedOptions[0].text
  146. showPicker1.value = false;
  147. }
  148. function onConfirm2(selectedValues, selectedOptions) {
  149. tacObjPblmstb.value.pblmDesc = selectedValues.selectedOptions[0].text
  150. showPicker2.value = false;
  151. }
  152. function changeC1(selectedValues, selectedOptions) {
  153. columns1.value = []
  154. columns2.value = []
  155. parSectino.value.forEach(item => {
  156. if(item.inspPblmsName === selectedValues.selectedValues[0]){
  157. var par = {
  158. text: item.checkPoint,
  159. value: item.checkPoint
  160. }
  161. columns1.value.push(par);
  162. }
  163. columns1.value = [...new Set(columns1.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  164. })
  165. }
  166. function changeC2(selectedValues, selectedOptions) {
  167. columns2.value = []
  168. parSectino.value.forEach(item => {
  169. if(item.checkPoint === selectedValues.selectedValues[0]){
  170. var par = {
  171. text: item.pblmDesc,
  172. value: item.pblmDesc
  173. }
  174. columns2.value.push(par);
  175. }
  176. columns2.value = [...new Set(columns2.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  177. })
  178. console.log(columns1.value)
  179. }
  180. function formatTime(timestamp) {
  181. const date = new Date(timestamp);
  182. const year = date.getFullYear();
  183. const month = ('0' + (date.getMonth() + 1)).slice(-2);
  184. const day = ('0' + date.getDate()).slice(-2);
  185. const hours = ('0' + date.getHours()).slice(-2);
  186. const minutes = ('0' + date.getMinutes()).slice(-2);
  187. const seconds = ('0' + date.getSeconds()).slice(-2);
  188. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  189. }
  190. async function getSelection(){
  191. await request.get(`/dc/insp/pblms/listByObjId/${pblm.value.objId}`).then(res=>{
  192. parSectino.value = res.data
  193. const uniqueInspPblmsName = [...new Set(res.data.map(item => item.inspPblmsName))];
  194. const uniquePblmDesc = [...new Set(res.data.map(item => item.pblmDesc))];
  195. const uniqueCheckPoint = [...new Set(res.data.map(item => item.checkPoint))];
  196. // columns1.value = uniqueCheckPoint.map(item => ({ text: item, value: item }));
  197. // columns2.value = uniquePblmDesc.map(item => ({ text: item, value: item }));
  198. columns.value = uniqueInspPblmsName.map(item => ({ text: item, value: item }));
  199. })
  200. }
  201. const parData = ref({});
  202. function getDe(){
  203. request.get(`/dc/insp/pblm/${pblm.value.pblmId}`).then(res=>{
  204. if(res.success){
  205. parData.value = res.data
  206. tacObjPblmstb.value = res.data;
  207. tacObjPblmstb.value.ifCasePblm = tacObjPblmstb.value.ifCasePblm === '1' ? '是' : '否';
  208. tacObjPblmstb.value.collTime = formatTime(tacObjPblmstb.value.collTime);
  209. columns1.value = []
  210. columns2.value = []
  211. parSectino.value.forEach(item => {
  212. if(item.inspPblmsName === tacObjPblmstb.value.inspPblmsName){
  213. console.log(item);
  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. console.log(item);
  225. var par = {
  226. text: item.pblmDesc,
  227. value: item.pblmDesc
  228. }
  229. columns2.value.push(par);
  230. }
  231. columns2.value = [...new Set(columns2.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  232. })
  233. res.data.gwComFiles.forEach(item => {
  234. item.filePath = item.filePath.replace(/\\/g, '/');
  235. fileList.value.push({
  236. url: url + item.filePath,
  237. name: item.fileName,
  238. isImage: true,
  239. id: item.id
  240. })
  241. console.log(fileList.value);
  242. })
  243. }
  244. })
  245. }
  246. // 获取需要认证的图片并转换为 base64
  247. async function getAuthImageAsBase64(imageUrl) {
  248. try {
  249. // 1. 使用 fetch 请求图片,携带 token
  250. const response = await fetch(imageUrl, {
  251. headers: {
  252. 'Authorization': `Bearer ${localStorage.getItem('token')}`,
  253. 'X-Token': localStorage.getItem('token') // 根据实际情况调整
  254. }
  255. });
  256. if (!response.ok) {
  257. throw new Error(`HTTP错误: ${response.status}`);
  258. }
  259. // 2. 获取图片的 ArrayBuffer
  260. const arrayBuffer = await response.arrayBuffer();
  261. // 3. 转换为 base64
  262. const base64 = btoa(
  263. new Uint8Array(arrayBuffer).reduce(
  264. (data, byte) => data + String.fromCharCode(byte),
  265. ''
  266. )
  267. );
  268. // 4. 根据图片类型生成 data URL
  269. const contentType = response.headers.get('content-type') || 'image/png';
  270. return `data:${contentType};base64,${base64}`;
  271. } catch (error) {
  272. console.error('获取认证图片失败:', error);
  273. throw error;
  274. }
  275. }
  276. async function save(){
  277. console.log(tacObjPblmstb.value);
  278. tacObjPblmstb.value.ifCasePblm = tacObjPblmstb.value.ifCasePblm === '是' ? '1' : '0';
  279. // tacObjPblmstb.value.collTime = parData.value.collTime;
  280. delete tacObjPblmstb.value.inspPblmCode;
  281. delete tacObjPblmstb.value.pblmsTypeId;
  282. delete tacObjPblmstb.value.collTime;
  283. // uploadFileToServer(newFile.value, parData.value.pblmId)
  284. await request.post('/dc/insp/pblm/update', tacObjPblmstb.value).then((res) => {
  285. if (res.success) {
  286. fileList.value.forEach(item => {
  287. console.log(item)
  288. if(!item.id){
  289. uploadFileToServer(item, parData.value.pblmId)
  290. }
  291. })
  292. showSuccessToast('保存成功!');
  293. router1.go(-1)
  294. fileList.value = []
  295. }
  296. });
  297. getDe();
  298. }
  299. watch(() => tacObjPblmstb.value.inspPblmsName, (newVal, oldVal) => {
  300. if (newVal !== oldVal) {
  301. columns1.value = []
  302. columns2.value = []
  303. parSectino.value.forEach(item => {
  304. if(item.inspPblmsName === newVal){
  305. var par = {
  306. text: item.checkPoint,
  307. value: item.checkPoint
  308. }
  309. columns1.value.push(par);
  310. }
  311. columns1.value = [...new Set(columns1.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  312. })
  313. }
  314. });
  315. watch(() => tacObjPblmstb.value.checkPoint, (newVal, oldVal) => {
  316. if (newVal !== oldVal) {
  317. columns2.value = []
  318. parSectino.value.forEach(item => {
  319. if(item.checkPoint === newVal){
  320. var par = {
  321. text: item.pblmDesc,
  322. value: item.pblmDesc
  323. }
  324. columns2.value.push(par);
  325. }
  326. columns2.value = [...new Set(columns2.value.map(item => JSON.stringify(item)))].map(str => JSON.parse(str))
  327. })
  328. }
  329. });
  330. function getObjectArrayDiff(arr1, arr2, key) {
  331. if (!key) throw new Error('必须指定对比的唯一字段(如id)');
  332. // 提取两个数组的key值集合
  333. const keySet1 = new Set(arr1.map(item => item[key]));
  334. const keySet2 = new Set(arr2.map(item => item[key]));
  335. // 仅在arr1中存在的对象
  336. const onlyInArr1 = arr1.filter(item => !keySet2.has(item[key]));
  337. // 仅在arr2中存在的对象
  338. const onlyInArr2 = arr2.filter(item => !keySet1.has(item[key]));
  339. // 所有差异对象(合并)
  340. const allDiff = [...onlyInArr1, ...onlyInArr2];
  341. return {
  342. onlyInArr1,
  343. onlyInArr2,
  344. allDiff
  345. };
  346. }
  347. const dataURLtoBlob = (dataURL) => {
  348. try {
  349. const arr = dataURL.split(',');
  350. const mime = arr[0].match(/:(.*?);/)[1];
  351. const bstr = atob(arr[1]);
  352. let n = bstr.length;
  353. const u8arr = new Uint8Array(n);
  354. while (n--) {
  355. u8arr[n] = bstr.charCodeAt(n);
  356. }
  357. return new Blob([u8arr], { type: mime });
  358. } catch (e) {
  359. console.error('base64转Blob失败:', e);
  360. return new Blob([], { type: 'application/octet-stream' });
  361. }
  362. };
  363. // 2. 手动构建multipart/form-data(保留之前验证正确的逻辑)
  364. const buildMultipartFormData = async (file) => {
  365. if (!file) throw new Error('文件为空');
  366. // 生成标准boundary
  367. const boundary = `----WebKitFormBoundary${Math.random().toString(36).substr(2, 16)}`;
  368. // 处理文件为Blob
  369. let fileBlob;
  370. if (file.content instanceof Blob || file.content instanceof File) {
  371. fileBlob = file.content;
  372. } else if (typeof file.content === 'string' && file.content.startsWith('data:')) {
  373. fileBlob = dataURLtoBlob(file.content);
  374. } else {
  375. throw new Error(`不支持的文件格式: ${typeof file.content}`);
  376. }
  377. // 读取文件为ArrayBuffer
  378. const fileBuffer = await new Promise((resolve, reject) => {
  379. const reader = new FileReader();
  380. reader.onload = () => resolve(reader.result);
  381. reader.onerror = reject;
  382. reader.readAsArrayBuffer(fileBlob);
  383. });
  384. // 手动拼接multipart格式(字符串转Uint8Array,兼容App端)
  385. const stringToUint8Array = (str) => {
  386. const arr = [];
  387. for (let i = 0; i < str.length; i++) {
  388. arr.push(str.charCodeAt(i));
  389. }
  390. return new Uint8Array(arr);
  391. };
  392. const parts = [];
  393. // 拼接分隔符+元信息
  394. parts.push(stringToUint8Array(`--${boundary}\r\n`));
  395. parts.push(stringToUint8Array(`Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(file.name || 'upload.png')}"\r\n`));
  396. parts.push(stringToUint8Array(`Content-Type: ${fileBlob.type || 'application/octet-stream'}\r\n\r\n`));
  397. // 拼接文件二进制数据
  398. parts.push(new Uint8Array(fileBuffer));
  399. // 拼接结束分隔符
  400. parts.push(stringToUint8Array(`\r\n--${boundary}--\r\n`));
  401. // 合并所有片段
  402. const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
  403. const resultBuffer = new Uint8Array(totalLength);
  404. let offset = 0;
  405. parts.forEach(part => {
  406. resultBuffer.set(part, offset);
  407. offset += part.length;
  408. });
  409. return {
  410. body: resultBuffer,
  411. contentType: `multipart/form-data; boundary=${boundary}`
  412. };
  413. };
  414. // 3. 核心:改用Fetch API上传文件(替换Axios)
  415. const uploadFileToServer = async (file, bizId) => {
  416. if (!file || !bizId) {
  417. showFailToast('文件或业务ID为空');
  418. return null;
  419. }
  420. try {
  421. // 构建标准multipart请求体
  422. const { body, contentType } = await buildMultipartFormData(file);
  423. // 获取token和自定义头(和Axios拦截器一致)
  424. const appStore = useAppStore();
  425. const userStore = useUserStore();
  426. const token = getToken();
  427. // 拼接完整接口地址(补充baseURL,和你的request封装一致)
  428. const baseURL = process.env.VUE_APP_BASE_API;
  429. const fullUrl = `${baseURL}/file/insert?bizId=${bizId}`;
  430. // 使用Fetch API发送请求(原生支持二进制,无兼容性问题)
  431. const formData = new FormData();
  432. formData.append('file', fileList.value[0].content); // 文件参数
  433. const response = await fetch(fullUrl, {
  434. method: 'POST',
  435. headers: {
  436. 'Content-Type': contentType, // 带boundary的标准格式
  437. 'Accept': 'application/json',
  438. 'Persid': userStore.userId,
  439. 'Accesstoken': token,
  440. 'Cache-Control': 'no-cache'
  441. },
  442. body: new Blob([body], { type: contentType }), // 二进制请求体
  443. timeout: 30000,
  444. credentials: 'include' // 保持cookie/token会话(和Axios一致)
  445. });
  446. // 解析响应
  447. if (!response.ok) {
  448. throw new Error(`请求失败: ${response.status}`);
  449. }
  450. const resData = await response.json();
  451. console.log('文件上传成功:', resData);
  452. return resData;
  453. } catch (error) {
  454. console.error('文件上传失败:', error);
  455. showFailToast('文件上传失败');
  456. throw error;
  457. }
  458. };
  459. // /pdcApi/file
  460. const newFile = ref({})
  461. const isAdding = ref(false);
  462. watch(() => fileList.value, (newVal, oldVal) => {
  463. if (newVal.length>oldVal.length && objData.value.state !== '2') {
  464. isAdding.value = true;
  465. newFile.value = getObjectArrayDiff(newVal, oldVal, 'id').onlyInArr1[0]
  466. console.log(newFile.value);
  467. }
  468. else if(newVal.length<oldVal.length&&newVal.length>0 && objData.value.state !== '2'){
  469. isAdding.value = false;
  470. newFile.value = getObjectArrayDiff(newVal, oldVal, 'id').onlyInArr2[0];
  471. showConfirmDialog({
  472. title: '删除',
  473. message:
  474. '是否确认删除',
  475. })
  476. .then(() => {
  477. request.post(`/file/${newFile.value.id}`).then(res=>{
  478. if(res.success){
  479. showSuccessToast('删除成功!');
  480. }
  481. })
  482. })
  483. .catch(() => {
  484. fileList.value = oldVal;
  485. });
  486. }
  487. });
  488. const objData = ref({});
  489. onMounted(() => {
  490. var par = JSON.parse(router.query.object);
  491. objData.value = par;
  492. objData.value.state = route.params.id
  493. console.log(objData.value);
  494. pblm.value.objId = par.objId
  495. pblm.value.pblmId = par.pblmId;
  496. getSelection().then(() => {
  497. getDe();
  498. })
  499. })
  500. </script>
  501. <style lang="scss" scoped>
  502. .pblm-detail-wrapper {
  503. height: 120%;
  504. padding: 10px 0;
  505. overflow: auto;
  506. .pblm-detail-label {
  507. padding: 10px 16px;
  508. color: #606266;
  509. display: flex;
  510. align-items: center;
  511. justify-content: space-between;
  512. }
  513. }
  514. </style>
  515. <style lang="scss">
  516. .van-cell-group--inset {
  517. margin: 0 10px;
  518. }
  519. </style>