safetyCultureMaterialManagementDetail.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <template>
  2. <main class="safety-platform-container__main">
  3. <BasicForm
  4. ref="basicFormRef"
  5. :formData="ruleFormData"
  6. :formRules="isViewMode ? undefined : formRules"
  7. :formConfig="computedFormConfig"
  8. >
  9. <template #fileFormat>
  10. <el-radio-group v-model="ruleFormData.fileFormat" :disabled="isViewMode">
  11. <el-radio value="PDF">PDF</el-radio>
  12. <el-radio value="WORD">WORD</el-radio>
  13. </el-radio-group>
  14. </template>
  15. <template #fileUrl>
  16. <!-- <UploadFiles label="上传文件" :maxCount="1" :file-list="ruleFormData.fileUrlList" :disabled="isViewMode"
  17. :allow-all-file-types="true" @uploadSuccess="handleUploadSuccess" /> -->
  18. <UploadFiles
  19. v-if="!isViewMode"
  20. label="上传文件"
  21. :maxCount="1"
  22. :file-list="ruleFormData.attachmentUrl"
  23. :disabled="isViewMode"
  24. :allow-all-file-types="true"
  25. @uploadSuccess="(list: FileItem[]) => handleUploadSuccess(list)"
  26. />
  27. <div class="file-list" v-else>
  28. <div class="file-item" v-for="file in ruleFormData.attachmentUrl" :key="file.fileId">
  29. <span class="file-item--name">{{ file.fileName }}</span>
  30. <div class="file-item--footer">
  31. <el-button link type="primary" @click="previewOnline(file.fileUrl, file.fileType)">预览</el-button>
  32. <!-- <el-button link type="primary" @click.stop="downloadFile(file.fileUrl, file.fileName)"
  33. >下载</el-button
  34. > -->
  35. </div>
  36. </div>
  37. </div>
  38. </template>
  39. <template #content>
  40. <div class="editor-container">
  41. <Toolbar style="border-bottom: 1px solid #dcdfe6" :editor="editorRef" />
  42. <Editor
  43. style="height: 400px; overflow-y: auto"
  44. v-model="ruleFormData.content"
  45. mode="default"
  46. :defaultConfig="editorConfig"
  47. @on-created="handleEditorCreated"
  48. @on-change="handleEditorChange"
  49. />
  50. </div>
  51. </template>
  52. <template #imageFileUrl>
  53. <el-upload
  54. :key="approvalUploadKey"
  55. ref="approvalUploadRef"
  56. v-model:file-list="approvalImageFileList"
  57. :auto-upload="false"
  58. list-type="picture-card"
  59. :limit="1"
  60. :disabled="isViewMode"
  61. :on-change="handleApprovalImageChange"
  62. :on-exceed="handleApprovalImageExceed"
  63. :on-preview="handleApprovalPictureCardPreview"
  64. :on-remove="handleApprovalImageRemove"
  65. >
  66. <el-icon><Plus /></el-icon>
  67. <template #file="{ file }">
  68. <div>
  69. <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
  70. <span class="el-upload-list__item-actions" v-if="!isViewMode">
  71. <span class="el-upload-list__item-preview" @click="handleApprovalPictureCardPreview(file)">
  72. <el-icon><ZoomIn /></el-icon>
  73. </span>
  74. <span class="el-upload-list__item-delete" @click.stop="handleApprovalDeleteClick()">
  75. <el-icon><Delete /></el-icon>
  76. </span>
  77. </span>
  78. </div>
  79. </template>
  80. </el-upload>
  81. </template>
  82. <template #status>
  83. <el-radio-group v-model="ruleFormData.status" :disabled="isViewMode">
  84. <el-radio :value="1">启用</el-radio>
  85. <el-radio :value="0">禁用</el-radio>
  86. </el-radio-group>
  87. </template>
  88. </BasicForm>
  89. <PreviewOnline ref="previewOnlineRef" />
  90. </main>
  91. <el-dialog v-model="dialogVisible" width="30%">
  92. <div class="dialog-content">
  93. <img :src="dialogImageUrl" alt="Preview Image" />
  94. </div>
  95. </el-dialog>
  96. <footer class="safety-platform-container__footer">
  97. <el-button @click="router.back()">返回</el-button>
  98. <el-button v-if="!isViewMode" type="primary" @click="handleSubmit">
  99. {{ isCreateMode ? '提交' : '保存' }}
  100. </el-button>
  101. </footer>
  102. </template>
  103. <script setup lang="ts">
  104. import { computed, onMounted, ref, shallowRef, onBeforeUnmount } from 'vue';
  105. import { useRoute, useRouter } from 'vue-router';
  106. import { ElMessage } from 'element-plus';
  107. import BasicForm from '@/components/BasicForm.vue';
  108. import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
  109. import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
  110. import '@wangeditor/editor/dist/css/style.css';
  111. import { useFormConfigHook } from '@/hooks/useFormConfigHook';
  112. import { ACADEMY_FILE_FORM_CONFIG, ACADEMY_FILE_FORM_DATA, ACADEMY_FILE_FORM_RULES } from '../configs/form';
  113. import {
  114. queryAcademyFileById,
  115. saveAcademyFile,
  116. updateAcademyFile,
  117. type safetyCultureFile,
  118. } from '@/api/safety-culture';
  119. import { uploadFileApi, UPLOAD_BIZ_TYPE } from '@/api/minio';
  120. import type { FileItem } from '@/components/UploadFiles/types';
  121. import { formatAttachmentList } from '@/components/UploadFiles/utils';
  122. import { Plus, Delete, ZoomIn } from '@element-plus/icons-vue';
  123. import { genFileId, type UploadProps, type UploadUserFile, type UploadRawFile, type UploadInstance } from 'element-plus';
  124. import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
  125. const router = useRouter();
  126. const route = useRoute();
  127. const operate = computed(() => (route.query.operate as string) || 'safety-culture-material-create');
  128. const currentId = computed(() => Number(route.query.id));
  129. const isCreateMode = computed(() => operate.value === 'safety-culture-material-create');
  130. const isEditMode = computed(() => operate.value === 'safety-culture-material-edit');
  131. const isViewMode = computed(() => operate.value === 'safety-culture-material-view');
  132. const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
  133. const approvalUploadRef = ref<UploadInstance>();
  134. const approvalImageFileList = ref<UploadUserFile[]>([]);
  135. const approvalUploadKey = ref(0);
  136. const dialogImageUrl = ref('');
  137. const dialogVisible = ref(false);
  138. const { ruleFormData, formRules, ruleFormConfig, cloneRuleFormData, beforeRouteLeave } = useFormConfigHook(
  139. ACADEMY_FILE_FORM_CONFIG,
  140. ACADEMY_FILE_FORM_DATA,
  141. ACADEMY_FILE_FORM_RULES,
  142. );
  143. // 查看模式下,所有字段设为只读
  144. const viewFormConfig = ref(
  145. ACADEMY_FILE_FORM_CONFIG.map((item) => ({
  146. ...item,
  147. componentProps: {
  148. ...item.componentProps,
  149. disabled: true,
  150. },
  151. })),
  152. );
  153. const computedFormConfig = computed(() => {
  154. if (isViewMode.value) {
  155. return viewFormConfig.value;
  156. }
  157. return ruleFormConfig.value;
  158. });
  159. const basicFormRef = ref<InstanceType<typeof BasicForm>>();
  160. // 富文本编辑器
  161. const editorRef = shallowRef();
  162. const editorConfig = computed(() => ({
  163. placeholder: '请输入文档内容',
  164. MENU_CONF: {},
  165. }));
  166. const handleEditorCreated = (editor: any) => {
  167. editorRef.value = editor;
  168. if (isViewMode.value) {
  169. editor.disable();
  170. }
  171. };
  172. const handleEditorChange = () => {
  173. // 编辑器内容变化时的处理
  174. };
  175. // 文件上传
  176. const handleUploadSuccess = (files: FileItem[]) => {
  177. ruleFormData.attachmentUrl = files;
  178. ruleFormData.fileUrl = JSON.stringify(files);
  179. };
  180. const handleApprovalPictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
  181. dialogImageUrl.value = uploadFile.url || '';
  182. dialogVisible.value = true;
  183. };
  184. const resetApprovalImageUpload = () => {
  185. approvalUploadRef.value?.clearFiles();
  186. ruleFormData.imageUrls = [];
  187. approvalImageFileList.value = [];
  188. approvalUploadKey.value += 1;
  189. };
  190. const handleApprovalDeleteClick = () => {
  191. resetApprovalImageUpload();
  192. };
  193. const handleApprovalImageRemove: UploadProps['onRemove'] = () => {
  194. resetApprovalImageUpload();
  195. };
  196. const handleApprovalImageChange = (uploadFile: UploadUserFile) => {
  197. if (!uploadFile.raw) return;
  198. uploadFileApi({
  199. file: uploadFile.raw,
  200. bizType: UPLOAD_BIZ_TYPE['DICTIONARY'],
  201. fileName: uploadFile.raw.name,
  202. })
  203. .then((res) => {
  204. ruleFormData.imageUrls = [res.url];
  205. approvalImageFileList.value = [
  206. {
  207. name: uploadFile.name || res.url.split('/').pop() || 'image',
  208. url: res.url,
  209. },
  210. ];
  211. })
  212. .catch(() => {
  213. ElMessage.error('图片上传失败,请重试');
  214. });
  215. };
  216. const handleApprovalImageExceed: UploadProps['onExceed'] = (files) => {
  217. const uploadInstance = approvalUploadRef.value;
  218. if (!uploadInstance) return;
  219. uploadInstance.clearFiles();
  220. const file = files[0] as UploadRawFile;
  221. file.uid = genFileId();
  222. uploadInstance.handleStart(file);
  223. };
  224. // 将逗号分隔的URL字符串转换为FileItem数组
  225. const convertFileUrlToFileItems = (fileUrl: string): FileItem[] => {
  226. if (!fileUrl || !fileUrl.trim()) {
  227. return [];
  228. }
  229. // 按逗号分割URL
  230. const urls = fileUrl
  231. .split(',')
  232. .map((url) => url.trim())
  233. .filter((url) => url);
  234. return urls.map((url, index) => {
  235. // 从URL中提取文件名
  236. const urlParts = url.split('/');
  237. const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
  238. // 根据文件扩展名判断文件类型
  239. const extension = fileName.split('.').pop()?.toLowerCase() || '';
  240. let fileType = 'pdf';
  241. if (extension === 'doc' || extension === 'docx') {
  242. fileType = 'word';
  243. } else if (extension === 'xls' || extension === 'xlsx') {
  244. fileType = 'excel';
  245. } else if (extension === 'ppt' || extension === 'pptx') {
  246. fileType = 'ppt';
  247. }
  248. return {
  249. fileId: Date.now() + index,
  250. fileName,
  251. fileType,
  252. fileSize: '0',
  253. fileUrl: url,
  254. };
  255. });
  256. };
  257. const handleValidate = async () => {
  258. if (!basicFormRef.value) return;
  259. const res = await basicFormRef.value.validateForm();
  260. return res;
  261. };
  262. const getDetail = async () => {
  263. if (!currentId.value) return;
  264. try {
  265. const res = await queryAcademyFileById(currentId.value);
  266. if (res) {
  267. // 映射接口字段到表单字段
  268. ruleFormData.fileName = res.fileName || '';
  269. ruleFormData.categoryName = res.categoryName || '';
  270. ruleFormData.fileCode = res.fileCode || '';
  271. ruleFormData.fileVersion = res.fileVersion || '';
  272. ruleFormData.fileFormat = res.fileFormat === 1 ? 'PDF' : 'WORD';
  273. ruleFormData.publishDate = res.publishDate || '';
  274. ruleFormData.content = res.content || '';
  275. ruleFormData.status = res.status ?? 1;
  276. ruleFormData.imageUrls = JSON.parse(res.imageUrls || '[]');
  277. approvalImageFileList.value = (ruleFormData.imageUrls || []).map((url, index) => ({
  278. name: `图片${index + 1}`,
  279. url,
  280. }));
  281. ruleFormData.fileUrl = JSON.parse(res.attachmentUrl || res.fileUrl || '');
  282. ruleFormData.attachmentUrl = JSON.parse(res.attachmentUrl || res.fileUrl || '');
  283. }
  284. cloneRuleFormData();
  285. } catch (e) {
  286. console.error('获取院级文件详情失败:', e);
  287. ElMessage.error('获取详情失败');
  288. }
  289. };
  290. const handleSubmit = async () => {
  291. const res = await handleValidate();
  292. if (!res) return;
  293. // 验证文件上传(必填)
  294. if (!ruleFormData.attachmentUrl || ruleFormData.attachmentUrl.length === 0) {
  295. ElMessage.warning('请上传文件');
  296. return;
  297. }
  298. try {
  299. const uploadedFileList = await formatAttachmentList(ruleFormData.attachmentUrl);
  300. const basePayload: any = {
  301. fileName: ruleFormData.fileName,
  302. classifyName: ruleFormData.classifyName,
  303. categoryName: ruleFormData.categoryName,
  304. fileCode: ruleFormData.fileCode,
  305. fileVersion: ruleFormData.fileVersion,
  306. fileFormat: ruleFormData.fileFormat === 'PDF' ? 1 : 2,
  307. publishDate: ruleFormData.publishDate,
  308. attachmentUrl: JSON.stringify(uploadedFileList),
  309. content: ruleFormData.content || undefined,
  310. status: ruleFormData.status ?? 1,
  311. releaseDate: ruleFormData.publishDate,
  312. description: '',
  313. caseName: '',
  314. imageUrls: JSON.stringify(ruleFormData.imageUrls),
  315. };
  316. if (isCreateMode.value) {
  317. await saveAcademyFile(basePayload);
  318. ElMessage.success('创建成功');
  319. } else if (isEditMode.value && currentId.value) {
  320. await updateAcademyFile({
  321. id: currentId.value,
  322. ...basePayload,
  323. });
  324. ElMessage.success('保存成功');
  325. }
  326. router.back();
  327. } catch (e) {
  328. console.error('保存院级文件失败:', e);
  329. ElMessage.error('保存失败,请重试');
  330. }
  331. };
  332. const previewOnline = (url: string | undefined, type) => {
  333. if (url) {
  334. previewOnlineRef.value?.open(url, type);
  335. }
  336. };
  337. onMounted(() => {
  338. cloneRuleFormData();
  339. // beforeRouteLeave();
  340. if (isEditMode.value || isViewMode.value) {
  341. getDetail();
  342. }
  343. });
  344. onBeforeUnmount(() => {
  345. const editor = editorRef.value;
  346. if (editor == null) return;
  347. editor.destroy();
  348. });
  349. </script>
  350. <style scoped lang="scss">
  351. @use '@/styles/page-details-layout.scss' as *;
  352. .editor-container {
  353. width: 100%;
  354. border: 1px solid #dcdfe6;
  355. border-radius: 4px;
  356. overflow: hidden;
  357. }
  358. .content-display {
  359. min-height: 200px;
  360. padding: 12px;
  361. border: 1px solid #dcdfe6;
  362. border-radius: 4px;
  363. background-color: #f5f7fa;
  364. }
  365. .file-display {
  366. .file-link {
  367. color: #409eff;
  368. text-decoration: none;
  369. &:hover {
  370. text-decoration: underline;
  371. }
  372. }
  373. }
  374. .no-file {
  375. color: rgba(0, 0, 0, 0.65);
  376. }
  377. .image-uploader {
  378. :deep(.el-upload--picture-card) {
  379. width: 80px !important;
  380. height: 80px !important;
  381. line-height: 80px;
  382. }
  383. :deep(.el-upload-list--picture-card .el-upload-list__item) {
  384. width: 80px !important;
  385. height: 80px !important;
  386. }
  387. }
  388. .dialog-content {
  389. img {
  390. width: 100%;
  391. height: 100%;
  392. }
  393. }
  394. </style>
  395. <style lang="scss">
  396. .w-e-full-screen-container {
  397. inset: 0 !important;
  398. z-index: 3000 !important;
  399. }
  400. .w-e-full-screen-container .w-e-text-container {
  401. height: calc(100vh - 42px) !important;
  402. }
  403. </style>