auditOpinion.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <template>
  2. <div class="opinions-content">
  3. <!-- 初步审核意见 -->
  4. <div class="opinion-section">
  5. <div class="opinion-header">
  6. <h3>成本审核初步意见</h3>
  7. <el-button
  8. class="ml10"
  9. type="primary"
  10. size="small"
  11. :disabled="isPreliminaryDisabled"
  12. @click="handleSavePreliminaryOpinion"
  13. >
  14. 保存
  15. </el-button>
  16. </div>
  17. <el-form
  18. :model="preliminaryOpinionForm"
  19. label-width="180px"
  20. :class="{ 'disabled-section': isPreliminaryDisabled }"
  21. >
  22. <el-form-item label="被监审单位基本情况及主要财务状况:">
  23. <el-input
  24. v-model="preliminaryOpinionForm.basicSituation"
  25. type="textarea"
  26. :rows="3"
  27. maxlength="500"
  28. show-word-limit
  29. :disabled="isPreliminaryDisabled"
  30. ></el-input>
  31. </el-form-item>
  32. <el-form-item label="监审项目现行执行的价格标准:">
  33. <el-input
  34. v-model="preliminaryOpinionForm.currentPriceStandard"
  35. type="textarea"
  36. :rows="3"
  37. maxlength="500"
  38. show-word-limit
  39. :disabled="isPreliminaryDisabled"
  40. ></el-input>
  41. </el-form-item>
  42. <el-form-item
  43. label="监审项目的成本构成、数据核增核减情况、依据及理由:"
  44. >
  45. <el-input
  46. v-model="preliminaryOpinionForm.costComposition"
  47. type="textarea"
  48. :rows="3"
  49. maxlength="500"
  50. show-word-limit
  51. :disabled="isPreliminaryDisabled"
  52. ></el-input>
  53. </el-form-item>
  54. <el-form-item label="成本审核初步意见:">
  55. <el-input
  56. v-model="preliminaryOpinionForm.preliminaryOpinion"
  57. type="textarea"
  58. :rows="3"
  59. maxlength="500"
  60. show-word-limit
  61. :disabled="isPreliminaryDisabled"
  62. ></el-input>
  63. </el-form-item>
  64. </el-form>
  65. </div>
  66. <!-- 被审核单位反馈意见 -->
  67. <div v-if="isConclusionEditable" class="opinion-section">
  68. <div class="opinion-header">
  69. <h3>监审单位反馈意见:</h3>
  70. </div>
  71. <el-form :model="feedbackForm" label-width="180px">
  72. <el-form-item label="被监审单位反馈的意见:">
  73. <el-input
  74. v-model="feedbackForm.feedbackOpinion"
  75. disabled
  76. type="textarea"
  77. :rows="3"
  78. maxlength="500"
  79. show-word-limit
  80. ></el-input>
  81. </el-form-item>
  82. <el-form-item label="被监审单位反馈资料:">
  83. <div v-if="feedbackForm.feedbackMaterial" class="file-item">
  84. <i class="el-icon-document"></i>
  85. <span
  86. class="file-link"
  87. @click="handleFileView(feedbackForm.feedbackMaterial)"
  88. >
  89. {{ getFileName(feedbackForm.feedbackMaterial) }}
  90. </span>
  91. </div>
  92. <div v-else class="no-file">
  93. <span style="color: #909399">暂无附件</span>
  94. </div>
  95. </el-form-item>
  96. </el-form>
  97. </div>
  98. <!-- 成本审核结论意见 -->
  99. <div v-if="isConclusionEditable" class="opinion-section">
  100. <div class="opinion-header">
  101. <h3>成本审核结论意见</h3>
  102. <el-button
  103. class="ml10"
  104. type="primary"
  105. size="small"
  106. :disabled="!isConclusionEditable"
  107. @click="handleSaveConclusionOpinion"
  108. >
  109. 保存
  110. </el-button>
  111. </div>
  112. <el-form :model="conclusionOpinionForm" label-width="180px">
  113. <el-form-item label="成本审核结论意见:">
  114. <el-input
  115. v-model="conclusionOpinionForm.conclusionOpinion"
  116. type="textarea"
  117. :rows="3"
  118. maxlength="500"
  119. show-word-limit
  120. :disabled="!isConclusionEditable"
  121. ></el-input>
  122. </el-form-item>
  123. <el-form-item label="其他需要说明的事项:">
  124. <el-input
  125. v-model="conclusionOpinionForm.otherExplanations"
  126. type="textarea"
  127. :rows="3"
  128. maxlength="500"
  129. show-word-limit
  130. :disabled="!isConclusionEditable"
  131. ></el-input>
  132. </el-form-item>
  133. <el-form-item label="备注:">
  134. <el-input
  135. v-model="conclusionOpinionForm.remark"
  136. type="textarea"
  137. :rows="3"
  138. maxlength="500"
  139. show-word-limit
  140. :disabled="!isConclusionEditable"
  141. ></el-input>
  142. </el-form-item>
  143. </el-form>
  144. </div>
  145. </div>
  146. </template>
  147. <script>
  148. import {
  149. getPreliminaryOpinion,
  150. addPreliminaryOpinion,
  151. } from '@/api/audit/preliminaryOpinion'
  152. export default {
  153. name: 'AuditOpinion',
  154. props: {
  155. id: {
  156. type: [String, Number],
  157. default: null,
  158. },
  159. currentNode: {
  160. type: String,
  161. default: '',
  162. },
  163. currentStatus: {
  164. type: String,
  165. default: '',
  166. },
  167. },
  168. data() {
  169. return {
  170. // 审核意见表单数据
  171. preliminaryOpinionForm: {
  172. id: '',
  173. basicSituation: '',
  174. currentPriceStandard: '',
  175. costComposition: '',
  176. preliminaryOpinion: '',
  177. },
  178. feedbackForm: {
  179. feedbackOpinion: '',
  180. feedbackMaterial: '', // 反馈附件(单个URL字符串)
  181. },
  182. conclusionOpinionForm: {
  183. id: '',
  184. conclusionOpinion: '',
  185. otherExplanations: '',
  186. remark: '',
  187. },
  188. }
  189. },
  190. computed: {
  191. // 判断初步意见是否需要置灰(currentNode === 'yjfk' && status === '已反馈')
  192. isPreliminaryDisabled() {
  193. return this.currentNode === 'yjfk' && this.currentStatus === '260'
  194. },
  195. // 判断结论意见是否可编辑(currentNode === 'yjfk' && status === '已反馈')
  196. isConclusionEditable() {
  197. return this.currentNode === 'yjfk' && this.currentStatus === '260'
  198. },
  199. },
  200. watch: {
  201. id(newVal) {
  202. // 当 id 变化时,重新获取数据
  203. if (newVal) {
  204. this.getPreliminaryOpinionData()
  205. }
  206. },
  207. },
  208. mounted() {
  209. console.log(this.currentStatus, '意见反馈this.currentStatus')
  210. // 组件挂载时,如果有 id,获取数据
  211. if (this.id) {
  212. this.getPreliminaryOpinionData()
  213. }
  214. },
  215. methods: {
  216. // 获取初步审核意见数据
  217. async getPreliminaryOpinionData() {
  218. if (!this.id) {
  219. return
  220. }
  221. try {
  222. const res = await getPreliminaryOpinion({ taskId: this.id })
  223. if (res && res.value) {
  224. const data = res.value
  225. // 回显初步审核意见数据
  226. if (data.id) {
  227. this.preliminaryOpinionForm = {
  228. id: data.id,
  229. basicSituation: data.basicSituation || '',
  230. currentPriceStandard: data.currentPriceStandard || '',
  231. costComposition: data.costComposition || '',
  232. preliminaryOpinion: data.preliminaryOpinion || '',
  233. }
  234. }
  235. // 回显结论意见数据(如果接口返回了结论意见)
  236. if (
  237. data.conclusionOpinion !== undefined ||
  238. data.otherExplanations !== undefined ||
  239. data.remark !== undefined
  240. ) {
  241. this.conclusionOpinionForm = {
  242. id: data.conclusionId || data.id || '',
  243. conclusionOpinion: data.conclusionOpinion || '',
  244. otherExplanations: data.otherExplanations || '',
  245. remark: data.remark || '',
  246. }
  247. }
  248. // 回显反馈意见数据(如果接口返回了反馈意见)
  249. if (
  250. data.feedbackOpinion !== undefined ||
  251. data.feedbackMaterials !== undefined
  252. ) {
  253. // 处理单个附件(可能是字符串或数组的第一个元素)
  254. let feedbackMaterial = ''
  255. if (data.feedbackMaterials) {
  256. if (typeof data.feedbackMaterials === 'string') {
  257. // 如果是字符串,直接使用
  258. feedbackMaterial = data.feedbackMaterials
  259. } else if (
  260. Array.isArray(data.feedbackMaterials) &&
  261. data.feedbackMaterials.length > 0
  262. ) {
  263. // 如果是数组,取第一个元素
  264. const firstItem = data.feedbackMaterials[0]
  265. if (typeof firstItem === 'string') {
  266. feedbackMaterial = firstItem
  267. } else if (firstItem && firstItem.url) {
  268. feedbackMaterial = firstItem.url
  269. } else if (
  270. firstItem &&
  271. firstItem.response &&
  272. firstItem.response.savePath
  273. ) {
  274. feedbackMaterial = firstItem.response.savePath
  275. }
  276. }
  277. }
  278. this.feedbackForm = {
  279. feedbackOpinion: data.feedbackOpinion || '',
  280. feedbackMaterial: feedbackMaterial,
  281. }
  282. }
  283. }
  284. } catch (error) {
  285. console.error('获取初步审核意见失败:', error)
  286. // 获取失败不影响页面显示,静默处理
  287. }
  288. },
  289. // 成本审核意见操作
  290. handleSavePreliminaryOpinion() {
  291. if (!this.id) {
  292. this.$message.error('缺少任务ID')
  293. return
  294. }
  295. // 判断是新增还是编辑(根据是否有 id)
  296. const formData = {
  297. basicSituation: this.preliminaryOpinionForm.basicSituation || '',
  298. currentPriceStandard:
  299. this.preliminaryOpinionForm.currentPriceStandard || '',
  300. costComposition: this.preliminaryOpinionForm.costComposition || '',
  301. preliminaryOpinion:
  302. this.preliminaryOpinionForm.preliminaryOpinion || '',
  303. taskId: this.id,
  304. }
  305. // 如果有 id,则是编辑,需要添加 id 字段
  306. if (this.preliminaryOpinionForm.id) {
  307. formData.id = this.preliminaryOpinionForm.id
  308. }
  309. addPreliminaryOpinion(formData)
  310. .then((res) => {
  311. if (res.code === 200) {
  312. this.$message({ type: 'success', message: '初步审核意见已保存' })
  313. // 保存成功后,如果有返回 id,更新表单中的 id
  314. if (res.value && res.value.id) {
  315. this.preliminaryOpinionForm.id = res.value.id
  316. }
  317. // 通知父组件刷新列表并关闭弹窗,要求分页回到第一页
  318. // this.$emit('refresh', { resetToFirst: true })
  319. // this.$emit('close')
  320. } else {
  321. this.$message({ type: 'error', message: res.message })
  322. }
  323. })
  324. .catch((error) => {
  325. // this.$message({ type: 'error', message: '保存失败,请稍后重试' })
  326. console.error('保存初步审核意见失败:', error)
  327. })
  328. },
  329. handleSaveConclusionOpinion() {
  330. if (!this.id) {
  331. this.$message.error('缺少任务ID')
  332. return
  333. }
  334. // 判断是新增还是编辑(根据是否有 id)
  335. // 传递所有字段(包括初步意见、结论意见和反馈意见)
  336. const formData = {
  337. // 成本监审意见数据(初步意见)
  338. basicSituation: this.preliminaryOpinionForm.basicSituation || '',
  339. currentPriceStandard:
  340. this.preliminaryOpinionForm.currentPriceStandard || '',
  341. costComposition: this.preliminaryOpinionForm.costComposition || '',
  342. preliminaryOpinion:
  343. this.preliminaryOpinionForm.preliminaryOpinion || '',
  344. // 成本审核结论意见数据
  345. conclusionOpinion: this.conclusionOpinionForm.conclusionOpinion || '',
  346. otherExplanations: this.conclusionOpinionForm.otherExplanations || '',
  347. remark: this.conclusionOpinionForm.remark || '',
  348. // 监审单位反馈意见数据
  349. feedbackOpinion: this.feedbackForm.feedbackOpinion || '',
  350. feedbackMaterials: this.feedbackForm.feedbackMaterial || '', // 反馈附件(单个URL字符串)
  351. taskId: this.id,
  352. }
  353. // 如果有初步意见的 id,则添加 id 字段(用于编辑)
  354. if (this.preliminaryOpinionForm.id) {
  355. formData.id = this.preliminaryOpinionForm.id
  356. }
  357. // 如果有结论意见的 id,也添加(可能是 conclusionId)
  358. if (
  359. this.conclusionOpinionForm.id &&
  360. this.conclusionOpinionForm.id !== this.preliminaryOpinionForm.id
  361. ) {
  362. formData.conclusionId = this.conclusionOpinionForm.id
  363. }
  364. addPreliminaryOpinion(formData)
  365. .then((res) => {
  366. if (res.code === 200) {
  367. this.$message({ type: 'success', message: '审核结论意见已保存' })
  368. // 保存成功后,如果有返回 id,更新表单中的 id
  369. if (res.value && res.value.id) {
  370. this.preliminaryOpinionForm.id = res.value.id
  371. }
  372. if (res.value && res.value.conclusionId) {
  373. this.conclusionOpinionForm.id = res.value.conclusionId
  374. }
  375. // 通知父组件刷新列表并关闭弹窗,要求分页回到第一页
  376. this.$emit('refresh', { resetToFirst: true })
  377. this.$emit('close')
  378. } else {
  379. this.$message({ type: 'error', message: res.message })
  380. }
  381. })
  382. .catch((error) => {
  383. // this.$message({ type: 'error', message: '保存失败,请稍后重试' })
  384. console.error('保存审核结论意见失败:', error)
  385. })
  386. },
  387. // 从URL获取文件名
  388. getFileNameFromUrl(url) {
  389. if (!url) return '未知文件'
  390. try {
  391. const urlParts = url.split('/')
  392. const fileName = urlParts[urlParts.length - 1]
  393. // 如果有查询参数,去除
  394. return fileName.split('?')[0] || '未知文件'
  395. } catch (e) {
  396. return '未知文件'
  397. }
  398. },
  399. // 获取文件名(支持对象和字符串)
  400. getFileName(file) {
  401. if (typeof file === 'string') {
  402. return this.getFileNameFromUrl(file)
  403. }
  404. if (file && file.name) {
  405. return file.name
  406. }
  407. if (file && file.url) {
  408. return this.getFileNameFromUrl(file.url)
  409. }
  410. return '未知文件'
  411. },
  412. // 查看文件
  413. handleFileView(file) {
  414. let fileUrl = ''
  415. if (typeof file === 'string') {
  416. fileUrl = file
  417. } else if (file && file.url) {
  418. fileUrl = file.url
  419. } else if (file && file.response) {
  420. fileUrl = file.response.savePath || file.response.url || ''
  421. } else if (file && file.savePath) {
  422. fileUrl = file.savePath
  423. }
  424. if (!fileUrl) {
  425. this.$message &&
  426. this.$message.warning &&
  427. this.$message.warning('文件地址无效')
  428. return
  429. }
  430. const normalized = this.normalizeUrl(fileUrl)
  431. const suggestName =
  432. (file && (file.name || file.fileName)) ||
  433. this.getFileName(file) ||
  434. '下载文件'
  435. this.downloadByFetch(normalized, suggestName).catch((e) => {
  436. console.error('文件下载失败: ', e)
  437. })
  438. },
  439. normalizeUrl(u) {
  440. if (!u) return ''
  441. if (/^https?:\/\//i.test(u)) return u
  442. const base = (window.context && window.context.form) || ''
  443. if (!base) return u
  444. if (u.startsWith('/')) return base + u
  445. return base.replace(/\/$/, '') + '/' + u
  446. },
  447. extractFileName(contentDisposition) {
  448. if (!contentDisposition) return ''
  449. const match = /filename[^;=\n]*=((['"])?.*?\2|[^;\n]*)/i.exec(
  450. contentDisposition
  451. )
  452. if (match && match[1]) {
  453. try {
  454. return decodeURIComponent(match[1].replace(/['"]/g, ''))
  455. } catch (e) {
  456. return match[1].replace(/['"]/g, '')
  457. }
  458. }
  459. return ''
  460. },
  461. async downloadByFetch(rawUrl, fallbackName) {
  462. const url = this.normalizeUrl(rawUrl)
  463. let loading
  464. try {
  465. loading = this.$baseLoading
  466. ? this.$baseLoading(1, '文件下载中...')
  467. : this.$loading({
  468. lock: true,
  469. text: '文件下载中...',
  470. spinner: 'el-icon-loading',
  471. background: 'rgba(0,0,0,0.7)',
  472. })
  473. const res = await fetch(url, { method: 'GET' })
  474. if (!res.ok) throw new Error('下载失败')
  475. const blob = await res.blob()
  476. let fileName =
  477. this.extractFileName(res.headers.get('content-disposition')) ||
  478. fallbackName ||
  479. '下载文件'
  480. if (!/\.[a-zA-Z0-9]+$/.test(fileName)) {
  481. const extFromUrl = (
  482. url.split('?')[0].split('#')[0].split('.').pop() || ''
  483. ).toLowerCase()
  484. fileName = extFromUrl ? `${fileName}.${extFromUrl}` : fileName
  485. }
  486. const objectUrl = window.URL.createObjectURL(blob)
  487. const link = document.createElement('a')
  488. link.style.display = 'none'
  489. link.href = objectUrl
  490. link.download = fileName
  491. document.body.appendChild(link)
  492. link.click()
  493. document.body.removeChild(link)
  494. window.URL.revokeObjectURL(objectUrl)
  495. this.$message &&
  496. this.$message.success &&
  497. this.$message.success('开始下载文件')
  498. } catch (e) {
  499. console.log(e.message || '文件下载失败')
  500. } finally {
  501. if (loading && loading.close) loading.close()
  502. }
  503. },
  504. },
  505. }
  506. </script>
  507. <style scoped>
  508. .opinions-content {
  509. display: flex;
  510. flex-direction: column;
  511. gap: 20px;
  512. margin-top: -20px;
  513. }
  514. .opinion-section {
  515. border: 1px solid #ccc;
  516. border-radius: 4px;
  517. padding: 20px;
  518. margin-top: 20px;
  519. }
  520. .disabled-section {
  521. opacity: 0.7;
  522. background-color: #f5f5f5;
  523. }
  524. .opinion-header {
  525. display: flex;
  526. align-items: center;
  527. margin-bottom: 20px;
  528. }
  529. .opinion-header h3 {
  530. margin: 0;
  531. font-size: 14px;
  532. font-weight: 500;
  533. }
  534. .opinion-section .el-form-item {
  535. margin-bottom: 15px;
  536. }
  537. .ml10 {
  538. margin-left: 10px;
  539. }
  540. .file-item {
  541. display: flex;
  542. align-items: center;
  543. padding: 8px 12px;
  544. background-color: #f5f7fa;
  545. border-radius: 4px;
  546. cursor: pointer;
  547. transition: background-color 0.3s;
  548. }
  549. .file-item:hover {
  550. background-color: #e4e7ed;
  551. }
  552. .file-item i {
  553. margin-right: 8px;
  554. color: #409eff;
  555. font-size: 16px;
  556. }
  557. .file-link {
  558. color: #409eff;
  559. text-decoration: none;
  560. cursor: pointer;
  561. }
  562. .file-link:hover {
  563. text-decoration: underline;
  564. }
  565. .no-file {
  566. color: #909399;
  567. padding: 8px 0;
  568. }
  569. </style>