Commit c6c33024847bac4c98008ce13575f4fe2941df20
1 parent
8b6413ac
feat: 增加问题合集
Showing
6 changed files
with
1373 additions
and
1 deletions
src/api/project/quest.ts
0 → 100644
1 | +import { defHttp } from '/@/utils/http/axios'; | ||
2 | + | ||
3 | +enum Api { | ||
4 | + QUEST_LIST = '/order/erp/quest/list_by_page', | ||
5 | + QUEST_CREATE = '/order/erp/quest/add', | ||
6 | + QUEST_DELETE = '/order/erp/quest/delete_by_id', | ||
7 | + QUEST_UPDATE = '/order/erp/quest/edit', | ||
8 | +} | ||
9 | + | ||
10 | +export const questUpdate=async(data:any)=>{ | ||
11 | + const res=await defHttp.post<any>({ | ||
12 | + url: Api.QUEST_UPDATE, | ||
13 | + data},{message:'操作成功'}); | ||
14 | + return res; | ||
15 | +} | ||
16 | + | ||
17 | +export const questDelete = async (ids: number[]) => { | ||
18 | + console.log('ids',ids); | ||
19 | + const res = await defHttp.post<any>({ url: Api.QUEST_DELETE, data: {ids} }, { message: '删除成功' }); | ||
20 | + return res; | ||
21 | +}; | ||
22 | + | ||
23 | +export const questCreate = async (data: any) => { | ||
24 | + const res = await defHttp.post<any>({ url: Api.QUEST_CREATE, data }, { message: '创建成功' }); | ||
25 | + return res; | ||
26 | +}; | ||
27 | + | ||
28 | +export const getQuestList = async (params: any) => { | ||
29 | + console.log('params',params); | ||
30 | + const res=await defHttp.post<any>({ | ||
31 | + url: Api.QUEST_LIST, | ||
32 | + params, | ||
33 | + }); | ||
34 | + const result={ | ||
35 | + items: res.records, | ||
36 | + total: res.total, | ||
37 | + }; | ||
38 | + return result; | ||
39 | +}; |
src/router/routes/modules/project/quest.ts
0 → 100644
1 | +import type { AppRouteModule } from '/@/router/types'; | ||
2 | + | ||
3 | +import { LAYOUT } from '/@/router/constant'; | ||
4 | +import { t } from '/@/hooks/web/useI18n'; | ||
5 | + | ||
6 | +const quest: AppRouteModule = { | ||
7 | + path: '/questList', | ||
8 | + name: 'QuestList', | ||
9 | + component: LAYOUT, | ||
10 | + redirect: '/questList', | ||
11 | + meta: { | ||
12 | + hideChildrenInMenu: true, // 代表隐藏子菜单选项,适合于底部没有子菜单选项列表。 | ||
13 | + orderNo: 5, | ||
14 | + icon: 'ion:help-outline', | ||
15 | + title: '问题合集', | ||
16 | + }, | ||
17 | + children: [ | ||
18 | + { | ||
19 | + path: '', | ||
20 | + name: 'QuestList', | ||
21 | + component: () => import('/@/views/project/quest/index.vue'), | ||
22 | + meta: { | ||
23 | + // affix: true, | ||
24 | + title: '问题合集', | ||
25 | + }, | ||
26 | + }, | ||
27 | + // { | ||
28 | + // path: 'workbench', | ||
29 | + // name: 'Workbench', | ||
30 | + // component: () => import('/@/views/dashboard/workbench/index.vue'), | ||
31 | + // meta: { | ||
32 | + // title: t('routes.dashboard.workbench'), | ||
33 | + // }, | ||
34 | + // }, | ||
35 | + ], | ||
36 | +}; | ||
37 | + | ||
38 | +export default quest; |
src/views/project/approve/FieldPanel.vue
@@ -24,7 +24,7 @@ | @@ -24,7 +24,7 @@ | ||
24 | </BasicTable> | 24 | </BasicTable> |
25 | <BasicDrawer | 25 | <BasicDrawer |
26 | width="500" | 26 | width="500" |
27 | - :showFooter="!isApproved && (role === ROLE.ADMIN || role === ROLE.BUSINESS)" | 27 | + :showFooter="!isApproved && (role === ROLE.ADMIN || (role === ROLE.BUSINESS && !shouldHideApproveButton))" |
28 | @register="registerDrawer" | 28 | @register="registerDrawer" |
29 | title="申请信息" | 29 | title="申请信息" |
30 | okText="通过" | 30 | okText="通过" |
@@ -278,6 +278,10 @@ | @@ -278,6 +278,10 @@ | ||
278 | const role = computed(() => { | 278 | const role = computed(() => { |
279 | return userStore.getUserInfo?.roleSmallVO?.code; | 279 | return userStore.getUserInfo?.roleSmallVO?.code; |
280 | }); | 280 | }); |
281 | + const shouldHideApproveButton = computed(() => { | ||
282 | + if (role.value !== ROLE.BUSINESS) return false; | ||
283 | + return fieldInfos.value.baseFields.some(field => field?.includes('数量') || field?.includes('生产科')); | ||
284 | + }); | ||
281 | 285 | ||
282 | // 定义MsgModalClose的事件,方便子组件调用 | 286 | // 定义MsgModalClose的事件,方便子组件调用 |
283 | const handleMsgModalClose = async (data) => { | 287 | const handleMsgModalClose = async (data) => { |
@@ -313,6 +317,7 @@ | @@ -313,6 +317,7 @@ | ||
313 | msgVisible, | 317 | msgVisible, |
314 | handleMsgModalClose, | 318 | handleMsgModalClose, |
315 | handlePreview, | 319 | handlePreview, |
320 | + shouldHideApproveButton, | ||
316 | }; | 321 | }; |
317 | }, | 322 | }, |
318 | }); | 323 | }); |
src/views/project/quest/QuestDrawer.vue
0 → 100644
1 | +<template> | ||
2 | + <BasicDrawer | ||
3 | + v-bind="$attrs" | ||
4 | + @register="registerDrawer" | ||
5 | + showFooter | ||
6 | + :title="getTitle" | ||
7 | + width="70%" | ||
8 | + @ok="handleSubmit" | ||
9 | + :okButtonProps="{ disabled: isView }" | ||
10 | + > | ||
11 | + <BasicForm @register="registerForm" /> | ||
12 | + | ||
13 | + <!-- 自定义图片上传区域 --> | ||
14 | + <div class="custom-image-upload" v-if="!isView"> | ||
15 | + <div class="upload-title">图片上传</div> | ||
16 | + <div class="upload-content"> | ||
17 | + <div | ||
18 | + class="upload-preview" | ||
19 | + v-for="(img, index) in uploadedImages" | ||
20 | + :key="index" | ||
21 | + > | ||
22 | + <img | ||
23 | + :src="img.url" | ||
24 | + @error="handleImageError($event, index)" | ||
25 | + :alt="img.name || '图片'" | ||
26 | + /> | ||
27 | + <div class="upload-preview-action"> | ||
28 | + <div class="action-buttons"> | ||
29 | + <EyeOutlined class="action-icon" @click="previewImage(img)" title="预览" /> | ||
30 | + <a-popconfirm title="确定删除吗?" ok-text="是" cancel-text="否" @confirm="removeImage(index)"> | ||
31 | + <DeleteOutlined class="action-icon" title="删除" /> | ||
32 | + </a-popconfirm> | ||
33 | + </div> | ||
34 | + </div> | ||
35 | + </div> | ||
36 | + <div class="upload-button" @click="handleCustomImageUpload" v-if="uploadedImages.length < 5"> | ||
37 | + <div class="upload-button-text"> | ||
38 | + <PlusOutlined /> | ||
39 | + <div>上传图片</div> | ||
40 | + </div> | ||
41 | + </div> | ||
42 | + </div> | ||
43 | + <input | ||
44 | + type="file" | ||
45 | + ref="imageFileInput" | ||
46 | + accept=".jpg,.jpeg,.png,.gif" | ||
47 | + style="display: none;" | ||
48 | + @change="uploadSelectedImage" | ||
49 | + /> | ||
50 | + </div> | ||
51 | + | ||
52 | + <!-- 查看模式下的图片显示 --> | ||
53 | + <div class="custom-image-view" v-if="isView && uploadedImages.length > 0"> | ||
54 | + <div class="upload-title">图片附件</div> | ||
55 | + <div class="upload-content"> | ||
56 | + <div | ||
57 | + class="upload-preview" | ||
58 | + v-for="(img, index) in uploadedImages" | ||
59 | + :key="index" | ||
60 | + @click="previewImage(img)" | ||
61 | + > | ||
62 | + <img | ||
63 | + :src="img.url" | ||
64 | + @error="handleImageError($event, index)" | ||
65 | + | ||
66 | + /> | ||
67 | + <div class="view-preview-hint"> | ||
68 | + <EyeOutlined /> | ||
69 | + <span>点击查看</span> | ||
70 | + </div> | ||
71 | + </div> | ||
72 | + </div> | ||
73 | + </div> | ||
74 | + | ||
75 | + <!-- 自定义附件上传区域 --> | ||
76 | + <div class="custom-file-upload" v-if="!isView"> | ||
77 | + <div class="upload-title">附件上传</div> | ||
78 | + <div class="file-list"> | ||
79 | + <!-- 附件列表 --> | ||
80 | + <div class="file-item" v-for="(file, index) in uploadedFiles" :key="index"> | ||
81 | + <div class="file-info"> | ||
82 | + <div class="file-name">{{ file.name }}</div> | ||
83 | + </div> | ||
84 | + <div class="file-actions"> | ||
85 | + <a @click="previewFile(file)" class="action-btn">预览</a> | ||
86 | + <a @click="downloadFile(file)" class="action-btn">下载</a> | ||
87 | + <a-popconfirm title="确定删除吗?" ok-text="是" cancel-text="否" @confirm="removeFile(index)"> | ||
88 | + <a class="action-btn delete">删除</a> | ||
89 | + </a-popconfirm> | ||
90 | + </div> | ||
91 | + </div> | ||
92 | + | ||
93 | + <!-- 上传按钮 --> | ||
94 | + <div class="file-upload-button" @click="handleCustomFileUpload"> | ||
95 | + <div class="upload-button-text"> | ||
96 | + <PlusOutlined /> | ||
97 | + <div>上传附件</div> | ||
98 | + </div> | ||
99 | + </div> | ||
100 | + </div> | ||
101 | + <input | ||
102 | + type="file" | ||
103 | + ref="fileInput" | ||
104 | + multiple | ||
105 | + style="display: none;" | ||
106 | + @change="uploadSelectedFile" | ||
107 | + /> | ||
108 | + </div> | ||
109 | + | ||
110 | + <!-- 查看模式下的附件显示 --> | ||
111 | + <div class="custom-file-view" v-if="isView && uploadedFiles.length > 0"> | ||
112 | + <div class="upload-title">文件附件</div> | ||
113 | + <div class="file-list"> | ||
114 | + <!-- 附件列表 --> | ||
115 | + <div class="file-item" v-for="(file, index) in uploadedFiles" :key="index"> | ||
116 | + <div class="file-info"> | ||
117 | + <div class="file-name">{{ file.name }}</div> | ||
118 | + </div> | ||
119 | + <div class="file-actions"> | ||
120 | + <a @click="previewFile(file)" class="action-btn">预览</a> | ||
121 | + <a @click="downloadFile(file)" class="action-btn">下载</a> | ||
122 | + </div> | ||
123 | + </div> | ||
124 | + </div> | ||
125 | + </div> | ||
126 | + </BasicDrawer> | ||
127 | +</template> | ||
128 | + | ||
129 | +<script lang="ts" setup> | ||
130 | + import { ref, computed, unref, onMounted } from 'vue'; | ||
131 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
132 | + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; | ||
133 | + import { useMessage } from '/@/hooks/web/useMessage'; | ||
134 | + import { Tinymce } from '/@/components/Tinymce'; | ||
135 | + import { h } from 'vue'; | ||
136 | + import { questCreate, questUpdate } from '/@/api/project/quest'; | ||
137 | + import { PlusOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons-vue'; | ||
138 | + | ||
139 | + const emit = defineEmits(['success', 'register']); | ||
140 | + const isUpdate = ref(true); | ||
141 | + const isView = ref(false); // 是否为查看模式 | ||
142 | + // 修改类型,使其包含id和其他必要属性 | ||
143 | + interface RecordType { | ||
144 | + id?: number; | ||
145 | + title?: string; | ||
146 | + contentText?: string; | ||
147 | + contentImages?: string; | ||
148 | + files?: string; | ||
149 | + [key: string]: any; | ||
150 | + } | ||
151 | + const record = ref<RecordType>({}); | ||
152 | + const { createMessage } = useMessage(); | ||
153 | + const uploadUrlFile=ref('http://47.104.8.35:80/api/localStorage/upload_file_oss?name='); | ||
154 | + const imageUploadUrl = ref('/api/localStorage/upload_oss'); // 使用相对路径避免跨域问题 | ||
155 | + const apiBaseDomain = 'http://47.104.8.35:80'; // API基础域名 | ||
156 | + | ||
157 | + // 自定义图片上传相关 | ||
158 | + const imageFileInput = ref<HTMLInputElement | null>(null); | ||
159 | + const uploadedImages = ref<Array<{ | ||
160 | + url: string; | ||
161 | + name?: string; | ||
162 | + }>>([]); | ||
163 | + const isUploading = ref(false); | ||
164 | + | ||
165 | + // 自定义附件上传相关 | ||
166 | + const fileInput = ref<HTMLInputElement | null>(null); | ||
167 | + const uploadedFiles = ref<Array<{ | ||
168 | + name: string; | ||
169 | + fileUrl: string; | ||
170 | + size?: number; | ||
171 | + type?: string; | ||
172 | + uploadTime?: number; | ||
173 | + }>>([]); | ||
174 | + const isFileUploading = ref(false); | ||
175 | + | ||
176 | + // 确保URL是完整路径 | ||
177 | + function ensureFullUrl(url) { | ||
178 | + if (!url) return ''; | ||
179 | + | ||
180 | + // 如果已经是完整URL,直接返回 | ||
181 | + if (url.startsWith('http://') || url.startsWith('https://')) { | ||
182 | + return url; | ||
183 | + } | ||
184 | + | ||
185 | + // 处理不同的URL格式 | ||
186 | + if (url.startsWith('//')) { | ||
187 | + return 'http:' + url; | ||
188 | + } else if (url.startsWith('/')) { | ||
189 | + return apiBaseDomain + url; | ||
190 | + } else { | ||
191 | + return apiBaseDomain + '/' + url; | ||
192 | + } | ||
193 | + } | ||
194 | + // 组件加载完成时执行 | ||
195 | + onMounted(() => { | ||
196 | + console.log('QuestDrawer组件已加载,自定义文件上传和预览功能已配置'); | ||
197 | + }); | ||
198 | + | ||
199 | + // 点击上传按钮 | ||
200 | + function handleCustomImageUpload() { | ||
201 | + if (isUploading.value) return; | ||
202 | + imageFileInput.value?.click(); | ||
203 | + } | ||
204 | + | ||
205 | + // 点击上传按钮 - 文件 | ||
206 | + function handleCustomFileUpload() { | ||
207 | + if (isFileUploading.value) return; | ||
208 | + fileInput.value?.click(); | ||
209 | + } | ||
210 | + | ||
211 | + // 上传选择的文件 | ||
212 | + async function uploadSelectedFile(e: Event) { | ||
213 | + const target = e.target as HTMLInputElement; | ||
214 | + if (!target.files || target.files.length === 0) return; | ||
215 | + | ||
216 | + isFileUploading.value = true; | ||
217 | + | ||
218 | + // 处理所有选中的文件 | ||
219 | + const files = Array.from(target.files); | ||
220 | + const uploadPromises = files.map(async (file) => { | ||
221 | + createMessage.loading(`正在上传: ${file.name}`); | ||
222 | + | ||
223 | + try { | ||
224 | + const formData = new FormData(); | ||
225 | + formData.append('file', file); | ||
226 | + | ||
227 | + const response = await fetch(uploadUrlFile.value + file.name, { | ||
228 | + method: 'POST', | ||
229 | + body: formData, | ||
230 | + }); | ||
231 | + | ||
232 | + if (!response.ok) { | ||
233 | + throw new Error(`上传失败: ${response.statusText}`); | ||
234 | + } | ||
235 | + | ||
236 | + const data = await response.json(); | ||
237 | + console.log('附件上传返回数据:', data); | ||
238 | + | ||
239 | + // 查找fileUrl字段 | ||
240 | + let fileUrl = null; | ||
241 | + if (data.data && data.data.fileUrl) { | ||
242 | + fileUrl = data.data.fileUrl; | ||
243 | + console.log("在data.data中找到fileUrl:", fileUrl); | ||
244 | + } else { | ||
245 | + console.error("未找到fileUrl字段! 数据:", data); | ||
246 | + } | ||
247 | + | ||
248 | + if (!fileUrl) { | ||
249 | + throw new Error('无法获取文件URL'); | ||
250 | + } | ||
251 | + | ||
252 | + // 确保URL是完整路径 | ||
253 | + const fullUrl = ensureFullUrl(fileUrl); | ||
254 | + console.log('完整文件URL:', fullUrl); | ||
255 | + | ||
256 | + // 从URL中提取文件名 | ||
257 | + const fileName = extractFileNameFromUrl(fullUrl); | ||
258 | + | ||
259 | + // 添加到上传文件列表,只存储fileUrl | ||
260 | + uploadedFiles.value.push({ | ||
261 | + name: fileName, | ||
262 | + fileUrl: fullUrl, | ||
263 | + }); | ||
264 | + | ||
265 | + createMessage.success(`${fileName} 上传成功`); | ||
266 | + return true; | ||
267 | + } catch (error) { | ||
268 | + console.error(`上传文件 ${file.name} 失败:`, error); | ||
269 | + createMessage.error(`${file.name} 上传失败: ${error.message || '未知错误'}`); | ||
270 | + return false; | ||
271 | + } | ||
272 | + }); | ||
273 | + | ||
274 | + // 等待所有文件上传完成 | ||
275 | + await Promise.all(uploadPromises); | ||
276 | + isFileUploading.value = false; | ||
277 | + | ||
278 | + // 清空input,以便于再次选择同一文件 | ||
279 | + if (fileInput.value) { | ||
280 | + fileInput.value.value = ''; | ||
281 | + } | ||
282 | + } | ||
283 | + | ||
284 | + // 从URL中提取文件名 | ||
285 | + function extractFileNameFromUrl(url) { | ||
286 | + if (!url) return '未知文件'; | ||
287 | + | ||
288 | + try { | ||
289 | + // 尝试从URL中提取文件名 | ||
290 | + // 移除查询参数 | ||
291 | + const urlWithoutParams = url.split('?')[0]; | ||
292 | + console.log('移除查询参数后的URL:', urlWithoutParams); | ||
293 | + | ||
294 | + // 获取最后一个路径部分 | ||
295 | + let fileName = urlWithoutParams.split('/').pop(); | ||
296 | + console.log('提取的文件名:', fileName); | ||
297 | + | ||
298 | + // 如果有编码的部分,尝试解码 | ||
299 | + if (fileName && (fileName.includes('%') || fileName.includes('+'))) { | ||
300 | + try { | ||
301 | + fileName = decodeURIComponent(fileName); | ||
302 | + console.log('解码后的文件名:', fileName); | ||
303 | + } catch (e) { | ||
304 | + console.error('解码文件名失败:', e); | ||
305 | + } | ||
306 | + } | ||
307 | + | ||
308 | + return fileName || '未知文件'; | ||
309 | + } catch (error) { | ||
310 | + console.error('提取文件名失败:', error); | ||
311 | + return '未知文件'; | ||
312 | + } | ||
313 | + } | ||
314 | + | ||
315 | + // 预览文件 | ||
316 | + function previewFile(file) { | ||
317 | + console.log('预览文件:', file); | ||
318 | + if (!file.fileUrl) { | ||
319 | + createMessage.error('无法预览:找不到文件URL'); | ||
320 | + return; | ||
321 | + } | ||
322 | + | ||
323 | + // 打开新窗口显示文件 | ||
324 | + const win = window.open(file.fileUrl, '_blank'); | ||
325 | + if (!win) { | ||
326 | + createMessage.error('浏览器阻止了弹出窗口,请允许弹出窗口或使用下载功能'); | ||
327 | + } | ||
328 | + } | ||
329 | + | ||
330 | + // 下载文件 | ||
331 | + function downloadFile(file) { | ||
332 | + console.log('下载文件:', file); | ||
333 | + if (!file.fileUrl) { | ||
334 | + createMessage.error('无法下载:找不到文件URL'); | ||
335 | + return; | ||
336 | + } | ||
337 | + | ||
338 | + // 创建一个隐藏的a标签,触发下载 | ||
339 | + const a = document.createElement('a'); | ||
340 | + a.href = file.fileUrl; | ||
341 | + a.download = file.name; | ||
342 | + a.target = '_blank'; | ||
343 | + document.body.appendChild(a); | ||
344 | + a.click(); | ||
345 | + document.body.removeChild(a); | ||
346 | + } | ||
347 | + | ||
348 | + // 删除已上传的文件 | ||
349 | + function removeFile(index: number) { | ||
350 | + uploadedFiles.value.splice(index, 1); | ||
351 | + } | ||
352 | + | ||
353 | + // 上传选择的图片 | ||
354 | + async function uploadSelectedImage(e: Event) { | ||
355 | + const target = e.target as HTMLInputElement; | ||
356 | + if (!target.files || target.files.length === 0) return; | ||
357 | + | ||
358 | + const file = target.files[0]; | ||
359 | + if (!file) return; | ||
360 | + | ||
361 | + isUploading.value = true; | ||
362 | + createMessage.loading('正在上传图片...'); | ||
363 | + | ||
364 | + try { | ||
365 | + const formData = new FormData(); | ||
366 | + formData.append('file', file); | ||
367 | + formData.append('name', file.name); // 显式添加文件名 | ||
368 | + console.log('formData',formData); | ||
369 | + console.log('file',file); | ||
370 | + console.log('name',file.name); | ||
371 | + const response = await fetch(imageUploadUrl.value, { | ||
372 | + method: 'POST', | ||
373 | + body: formData, | ||
374 | + }); | ||
375 | + | ||
376 | + if (!response.ok) { | ||
377 | + throw new Error(`上传失败: ${response.statusText}`); | ||
378 | + } | ||
379 | + | ||
380 | + const data = await response.json(); | ||
381 | + console.log('图片上传返回数据', data); | ||
382 | + | ||
383 | + // 根据接口返回格式获取URL | ||
384 | + let imageUrl = ''; | ||
385 | + | ||
386 | + // 检查嵌套的data对象 | ||
387 | + if (data.data) { | ||
388 | + if (data.data.picUrl) { | ||
389 | + imageUrl = data.data.picUrl; | ||
390 | + }else if (data.data.smallPicUrl) { | ||
391 | + imageUrl = data.data.smallPicUrl; | ||
392 | + } | ||
393 | + } | ||
394 | + | ||
395 | + | ||
396 | + // 确保URL是完整路径 | ||
397 | + if (imageUrl && !imageUrl.startsWith('http')) { | ||
398 | + // 如果URL包含双斜杠但不是http开头,可能是协议相对URL | ||
399 | + if (imageUrl.startsWith('//')) { | ||
400 | + imageUrl = 'http:' + imageUrl; | ||
401 | + } | ||
402 | + // 是相对路径,需要添加域名 | ||
403 | + else if (imageUrl.startsWith('/')) { | ||
404 | + // 使用服务器域名而不是前端域名 | ||
405 | + imageUrl = 'http://47.104.8.35:80' + imageUrl; | ||
406 | + } else { | ||
407 | + // 其他情况,添加完整路径前缀 | ||
408 | + imageUrl = 'http://47.104.8.35:80/' + imageUrl; | ||
409 | + } | ||
410 | + } | ||
411 | + | ||
412 | + console.log('处理后的图片URL:', imageUrl); | ||
413 | + | ||
414 | + // 只添加URL,不添加name | ||
415 | + uploadedImages.value.push({ | ||
416 | + url: imageUrl | ||
417 | + }); | ||
418 | + | ||
419 | + createMessage.success('图片上传成功'); | ||
420 | + } catch (error) { | ||
421 | + console.error('图片上传失败:', error); | ||
422 | + createMessage.error(`上传失败: ${error.message || '未知错误'}`); | ||
423 | + } finally { | ||
424 | + isUploading.value = false; | ||
425 | + // 清空input,以便于再次选择同一文件 | ||
426 | + if (imageFileInput.value) { | ||
427 | + imageFileInput.value.value = ''; | ||
428 | + } | ||
429 | + } | ||
430 | + } | ||
431 | + | ||
432 | + // 删除已上传的图片 | ||
433 | + function removeImage(index: number) { | ||
434 | + uploadedImages.value.splice(index, 1); | ||
435 | + } | ||
436 | + | ||
437 | + // 处理图片加载错误 | ||
438 | + function handleImageError(event: Event, index: number) { | ||
439 | + const target = event.target as HTMLImageElement; | ||
440 | + console.error('图片加载失败:', uploadedImages.value[index].url); | ||
441 | + | ||
442 | + // 尝试使用不同格式的URL | ||
443 | + const currentUrl = uploadedImages.value[index].url; | ||
444 | + let newUrl = ''; | ||
445 | + | ||
446 | + // 尝试各种可能的URL格式 | ||
447 | + if (currentUrl.includes('picUrl')) { | ||
448 | + // 如果是大图失败,尝试使用小图 | ||
449 | + newUrl = currentUrl.replace('picUrl', 'smallPicUrl'); | ||
450 | + } | ||
451 | + | ||
452 | + console.log('尝试新的URL:', newUrl); | ||
453 | + | ||
454 | + // 使用新URL | ||
455 | + uploadedImages.value[index].url = newUrl; | ||
456 | + target.src = newUrl; | ||
457 | + | ||
458 | + // 添加再次失败的处理 | ||
459 | + target.onerror = () => { | ||
460 | + console.error('备选URL也加载失败:', newUrl); | ||
461 | + createMessage.error('图片无法加载,请重新上传'); | ||
462 | + // 移除无法加载的图片 | ||
463 | + removeImage(index); | ||
464 | + }; | ||
465 | + } | ||
466 | + | ||
467 | + // 表单配置 | ||
468 | + const getFormSchema = computed(() => { | ||
469 | + return [ | ||
470 | + { | ||
471 | + field: 'id', | ||
472 | + label: 'ID', | ||
473 | + component: 'Input' as any, | ||
474 | + show: false, // 隐藏此字段 | ||
475 | + }, | ||
476 | + { | ||
477 | + field: 'title', | ||
478 | + label: '标题', | ||
479 | + component: 'Input' as any, | ||
480 | + required: true, | ||
481 | + componentProps: { | ||
482 | + disabled: unref(isView), // 查看模式下禁用 | ||
483 | + }, | ||
484 | + }, | ||
485 | + { | ||
486 | + field: 'contentText', | ||
487 | + label: '内容', | ||
488 | + component: 'Input' as any, | ||
489 | + required: true, | ||
490 | + render: ({ model, field }) => { | ||
491 | + return h(Tinymce, { | ||
492 | + value: model[field], | ||
493 | + height: 400, | ||
494 | + onChange: (value: string) => { | ||
495 | + model[field] = value; | ||
496 | + }, | ||
497 | + disabled: unref(isView), // 查看模式下禁用 | ||
498 | + readonly: unref(isView), // 查看模式下只读 | ||
499 | + }); | ||
500 | + }, | ||
501 | + }, | ||
502 | + ] as any; | ||
503 | + }); | ||
504 | + | ||
505 | + // 注册表单 | ||
506 | + const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({ | ||
507 | + labelWidth: 100, | ||
508 | + schemas: getFormSchema, //这就是数据。 | ||
509 | + showActionButtonGroup: false, | ||
510 | + baseColProps: { span: 24 }, | ||
511 | + }); | ||
512 | + | ||
513 | + // 注册抽屉 | ||
514 | + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { | ||
515 | + resetFields(); | ||
516 | + setDrawerProps({ confirmLoading: false }); | ||
517 | + uploadedImages.value = []; // 清空已上传图片 | ||
518 | + uploadedFiles.value = []; // 清空已上传文件 | ||
519 | + | ||
520 | + isUpdate.value = !!data?.isUpdate; | ||
521 | + isView.value = !!data?.isView; // 设置查看模式 | ||
522 | + | ||
523 | + if (unref(isUpdate)) { | ||
524 | + record.value = { ...data.record }; | ||
525 | + console.log('加载记录数据:', data.record); | ||
526 | + console.log('记录ID:', data.record?.id); | ||
527 | + | ||
528 | + // 确保ID字段被正确保存,即使它不在表单字段中 | ||
529 | + if (data.record && data.record.id) { | ||
530 | + record.value.id = data.record.id; | ||
531 | + console.log('保存记录ID到record:', record.value.id); | ||
532 | + } | ||
533 | + | ||
534 | + await setFieldsValue({ | ||
535 | + ...data.record, | ||
536 | + }); | ||
537 | + | ||
538 | + // 处理图片数据 - 现在contentImages是一个URL数组 | ||
539 | + try { | ||
540 | + if (data.record.contentImages) { | ||
541 | + console.log('原始图片数据:', data.record.contentImages); | ||
542 | + | ||
543 | + // 处理contentImages数组 | ||
544 | + if (Array.isArray(data.record.contentImages)) { | ||
545 | + // 直接使用数组 | ||
546 | + uploadedImages.value = data.record.contentImages.map(url => ({ | ||
547 | + url: ensureFullUrl(url) | ||
548 | + })); | ||
549 | + console.log('数组格式的图片URL:', uploadedImages.value); | ||
550 | + } | ||
551 | + // 向下兼容旧格式,如果是字符串,尝试解析 | ||
552 | + else if (typeof data.record.contentImages === 'string') { | ||
553 | + try { | ||
554 | + // 尝试解析为JSON | ||
555 | + const images = JSON.parse(data.record.contentImages); | ||
556 | + if (Array.isArray(images)) { | ||
557 | + // 如果解析成功并且是数组,处理每个图片对象 | ||
558 | + uploadedImages.value = images.map(img => ({ | ||
559 | + url: ensureFullUrl(typeof img === 'string' ? img : (img.url || '')) | ||
560 | + })); | ||
561 | + } else { | ||
562 | + // 如果不是数组,可能是以逗号分隔的字符串 | ||
563 | + const urls = data.record.contentImages.split(',').filter(url => url.trim()); | ||
564 | + uploadedImages.value = urls.map(url => ({ | ||
565 | + url: ensureFullUrl(url.trim()) | ||
566 | + })); | ||
567 | + } | ||
568 | + } catch (e) { | ||
569 | + // 解析失败,尝试作为逗号分隔的URL列表处理 | ||
570 | + const urls = data.record.contentImages.split(',').filter(url => url.trim()); | ||
571 | + uploadedImages.value = urls.map(url => ({ | ||
572 | + url: ensureFullUrl(url.trim()) | ||
573 | + })); | ||
574 | + } | ||
575 | + } | ||
576 | + | ||
577 | + console.log('处理后的图片数据:', uploadedImages.value); | ||
578 | + } | ||
579 | + | ||
580 | + // 处理附件数据 - 现在files是一个URL数组 | ||
581 | + if (data.record.files) { | ||
582 | + console.log('原始文件数据:', data.record.files); | ||
583 | + | ||
584 | + // 处理files数组 | ||
585 | + if (Array.isArray(data.record.files)) { | ||
586 | + // 直接使用数组 | ||
587 | + uploadedFiles.value = data.record.files.map(url => { | ||
588 | + const fileUrl = ensureFullUrl(url); | ||
589 | + return { | ||
590 | + name: extractFileNameFromUrl(fileUrl), | ||
591 | + fileUrl: fileUrl | ||
592 | + }; | ||
593 | + }); | ||
594 | + console.log('数组格式的文件URL:', uploadedFiles.value); | ||
595 | + } | ||
596 | + // 向下兼容旧格式,如果是字符串,尝试解析 | ||
597 | + else if (typeof data.record.files === 'string') { | ||
598 | + // 处理可能的额外引号 | ||
599 | + let filesStr = data.record.files; | ||
600 | + if (filesStr.startsWith('"') && filesStr.endsWith('"')) { | ||
601 | + filesStr = filesStr.substring(1, filesStr.length - 1); | ||
602 | + } | ||
603 | + | ||
604 | + try { | ||
605 | + // 尝试解析为JSON | ||
606 | + const files = JSON.parse(filesStr); | ||
607 | + if (Array.isArray(files)) { | ||
608 | + uploadedFiles.value = files.map(file => { | ||
609 | + if (typeof file === 'string') { | ||
610 | + // 如果文件项是字符串,当作URL处理 | ||
611 | + const fileUrl = ensureFullUrl(file); | ||
612 | + return { | ||
613 | + name: extractFileNameFromUrl(fileUrl), | ||
614 | + fileUrl: fileUrl | ||
615 | + }; | ||
616 | + } else { | ||
617 | + // 如果文件项是对象,提取fileUrl | ||
618 | + const fileUrl = ensureFullUrl(file.fileUrl || ''); | ||
619 | + return { | ||
620 | + name: file.name || extractFileNameFromUrl(fileUrl), | ||
621 | + fileUrl: fileUrl | ||
622 | + }; | ||
623 | + } | ||
624 | + }); | ||
625 | + } else { | ||
626 | + // 如果不是数组,可能是以逗号分隔的字符串 | ||
627 | + const urls = filesStr.split(',').filter(url => url.trim()); | ||
628 | + uploadedFiles.value = urls.map(url => { | ||
629 | + const fileUrl = ensureFullUrl(url.trim()); | ||
630 | + return { | ||
631 | + name: extractFileNameFromUrl(fileUrl), | ||
632 | + fileUrl: fileUrl | ||
633 | + }; | ||
634 | + }); | ||
635 | + } | ||
636 | + } catch (e) { | ||
637 | + // 解析失败,尝试作为逗号分隔的URL列表处理 | ||
638 | + const urls = filesStr.split(',').filter(url => url.trim()); | ||
639 | + uploadedFiles.value = urls.map(url => { | ||
640 | + const fileUrl = ensureFullUrl(url.trim()); | ||
641 | + return { | ||
642 | + name: extractFileNameFromUrl(fileUrl), | ||
643 | + fileUrl: fileUrl | ||
644 | + }; | ||
645 | + }); | ||
646 | + } | ||
647 | + } | ||
648 | + | ||
649 | + console.log('处理后的文件数据:', uploadedFiles.value); | ||
650 | + } | ||
651 | + } catch (error) { | ||
652 | + console.error('处理附件和图片数据时出错:', error); | ||
653 | + } | ||
654 | + } | ||
655 | + }); | ||
656 | + | ||
657 | + // 计算抽屉标题 | ||
658 | + const getTitle = computed(() => { | ||
659 | + if (unref(isView)) { | ||
660 | + return '查看问题'; | ||
661 | + } | ||
662 | + return !unref(isUpdate) ? '新建问题' : '编辑问题'; | ||
663 | + }); | ||
664 | + | ||
665 | + // 提交表单 | ||
666 | + async function handleSubmit() { | ||
667 | + // 查看模式下不允许提交 | ||
668 | + if (unref(isView)) return; | ||
669 | + | ||
670 | + try { | ||
671 | + const values = await validate(); | ||
672 | + console.log('values的值',values); | ||
673 | + setDrawerProps({ confirmLoading: true }); | ||
674 | + | ||
675 | + // 添加上传的图片到表单数据中 - 直接提交URL数组 | ||
676 | + if (uploadedImages.value.length > 0) { | ||
677 | + // 直接将URL数组作为contentImages | ||
678 | + values.contentImages = uploadedImages.value.map(img => img.url); | ||
679 | + console.log('图片数据(数组格式):', values.contentImages); | ||
680 | + } | ||
681 | + | ||
682 | + // 添加上传的附件到表单数据中 - 直接提交URL数组 | ||
683 | + if (uploadedFiles.value.length > 0) { | ||
684 | + // 直接将fileUrl数组作为files | ||
685 | + values.files = uploadedFiles.value.map(file => file.fileUrl); | ||
686 | + console.log('附件数据(数组格式):', values.files); | ||
687 | + } | ||
688 | + | ||
689 | + // 如果是更新模式,确保添加ID | ||
690 | + const isUpdateValue = unref(isUpdate); | ||
691 | + if (isUpdateValue) { | ||
692 | + // 从record中获取ID | ||
693 | + if (record.value && record.value.id) { | ||
694 | + values.id = record.value.id; | ||
695 | + console.log('从record中添加记录ID:', values.id); | ||
696 | + } else { | ||
697 | + console.warn('警告:更新模式下未找到记录ID!'); | ||
698 | + } | ||
699 | + } | ||
700 | + | ||
701 | + // 确保表单提交前检查ID | ||
702 | + if (isUpdateValue && !values.id) { | ||
703 | + console.error('错误:更新模式下缺少ID字段!'); | ||
704 | + createMessage.error('更新失败:缺少记录ID'); | ||
705 | + setDrawerProps({ confirmLoading: false }); | ||
706 | + return; | ||
707 | + } | ||
708 | + | ||
709 | + // 提交数据 | ||
710 | + console.log('表单数据', values); | ||
711 | + | ||
712 | + // 调用后端API保存数据 | ||
713 | + const requestApi = isUpdateValue | ||
714 | + ? questUpdate // 更新接口 | ||
715 | + : questCreate; // 创建接口 | ||
716 | + console.log('requestApi调用',requestApi); | ||
717 | + const res=await requestApi(values); | ||
718 | + console.log('res返回数据',res); | ||
719 | + // 关闭抽屉 | ||
720 | + closeDrawer(); | ||
721 | + // 通知父组件刷新数据 | ||
722 | + emit('success'); | ||
723 | + setDrawerProps({ confirmLoading: false }); | ||
724 | + } catch (error) { | ||
725 | + console.error('API请求失败:', error); | ||
726 | + createMessage.error(`保存失败: ${error.message || '未知错误'}`); | ||
727 | + } | ||
728 | + } | ||
729 | + | ||
730 | + // 预览图片 | ||
731 | + function previewImage(image) { | ||
732 | + console.log('预览图片:', image); | ||
733 | + if (!image.url) { | ||
734 | + createMessage.error('无法预览:找不到图片URL'); | ||
735 | + return; | ||
736 | + } | ||
737 | + | ||
738 | + // 创建一个新窗口安全地显示图片 | ||
739 | + const win = window.open('', '_blank'); | ||
740 | + if (!win) { | ||
741 | + createMessage.error('浏览器阻止了弹出窗口,请允许弹出窗口'); | ||
742 | + return; | ||
743 | + } | ||
744 | + | ||
745 | + // 写入HTML内容 | ||
746 | + win.document.write(`<!DOCTYPE html> | ||
747 | + <html> | ||
748 | + <head> | ||
749 | + <title>图片预览</title> | ||
750 | + <style> | ||
751 | + body { | ||
752 | + margin: 0; | ||
753 | + padding: 0; | ||
754 | + display: flex; | ||
755 | + justify-content: center; | ||
756 | + align-items: center; | ||
757 | + min-height: 100vh; | ||
758 | + background-color: #f0f0f0; | ||
759 | + } | ||
760 | + img { | ||
761 | + max-width: 90%; | ||
762 | + max-height: 90vh; | ||
763 | + box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); | ||
764 | + } | ||
765 | + .img-container { | ||
766 | + background-color: white; | ||
767 | + padding: 20px; | ||
768 | + border-radius: 5px; | ||
769 | + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | ||
770 | + } | ||
771 | + </style> | ||
772 | + </head> | ||
773 | + <body> | ||
774 | + <div class="img-container"> | ||
775 | + <img src="${image.url}" alt="${image.name || '图片'}" /> | ||
776 | + </div> | ||
777 | + </body> | ||
778 | + </html>`); | ||
779 | + win.document.close(); | ||
780 | + } | ||
781 | +</script> | ||
782 | + | ||
783 | +<style lang="less" scoped> | ||
784 | +.custom-image-upload { | ||
785 | + margin-top: 20px; | ||
786 | + padding: 20px; | ||
787 | + border-top: 1px solid #f0f0f0; | ||
788 | + | ||
789 | + .upload-title { | ||
790 | + font-size: 16px; | ||
791 | + font-weight: bold; | ||
792 | + margin-bottom: 16px; | ||
793 | + } | ||
794 | + | ||
795 | + .upload-content { | ||
796 | + display: flex; | ||
797 | + flex-wrap: wrap; | ||
798 | + gap: 12px; | ||
799 | + } | ||
800 | + | ||
801 | + .upload-preview { | ||
802 | + width: 104px; | ||
803 | + height: 104px; | ||
804 | + border: 1px solid #d9d9d9; | ||
805 | + border-radius: 4px; | ||
806 | + position: relative; | ||
807 | + overflow: hidden; | ||
808 | + | ||
809 | + img { | ||
810 | + width: 100%; | ||
811 | + height: 100%; | ||
812 | + object-fit: cover; | ||
813 | + } | ||
814 | + | ||
815 | + .upload-preview-action { | ||
816 | + position: absolute; | ||
817 | + top: 0; | ||
818 | + right: 0; | ||
819 | + bottom: 0; | ||
820 | + left: 0; | ||
821 | + background: rgba(0, 0, 0, 0.5); | ||
822 | + display: flex; | ||
823 | + align-items: center; | ||
824 | + justify-content: center; | ||
825 | + color: white; | ||
826 | + opacity: 0; | ||
827 | + transition: opacity 0.3s; | ||
828 | + | ||
829 | + &:hover { | ||
830 | + opacity: 1; | ||
831 | + } | ||
832 | + | ||
833 | + .action-buttons { | ||
834 | + display: flex; | ||
835 | + gap: 15px; | ||
836 | + | ||
837 | + .action-icon { | ||
838 | + font-size: 18px; | ||
839 | + cursor: pointer; | ||
840 | + | ||
841 | + &:hover { | ||
842 | + color: #1890ff; | ||
843 | + transform: scale(1.2); | ||
844 | + transition: all 0.2s; | ||
845 | + } | ||
846 | + } | ||
847 | + } | ||
848 | + } | ||
849 | + } | ||
850 | +.ant-message-notice .ant-message-loading { | ||
851 | + color: red !important; | ||
852 | +} | ||
853 | + .upload-button { | ||
854 | + width: 104px; | ||
855 | + height: 104px; | ||
856 | + border: 1px dashed #d9d9d9; | ||
857 | + border-radius: 4px; | ||
858 | + display: flex; | ||
859 | + justify-content: center; | ||
860 | + align-items: center; | ||
861 | + cursor: pointer; | ||
862 | + transition: border-color 0.3s; | ||
863 | + | ||
864 | + &:hover { | ||
865 | + border-color: #1890ff; | ||
866 | + } | ||
867 | + | ||
868 | + .upload-button-text { | ||
869 | + text-align: center; | ||
870 | + color: #666; | ||
871 | + | ||
872 | + .anticon { | ||
873 | + font-size: 24px; | ||
874 | + margin-bottom: 8px; | ||
875 | + } | ||
876 | + } | ||
877 | + } | ||
878 | +} | ||
879 | + | ||
880 | +/* 自定义文件上传样式 */ | ||
881 | +.custom-file-upload { | ||
882 | + margin-top: 20px; | ||
883 | + padding: 20px; | ||
884 | + border-top: 1px solid #f0f0f0; | ||
885 | + | ||
886 | + .upload-title { | ||
887 | + font-size: 16px; | ||
888 | + font-weight: bold; | ||
889 | + margin-bottom: 16px; | ||
890 | + } | ||
891 | + | ||
892 | + .file-list { | ||
893 | + width: 100%; | ||
894 | + } | ||
895 | + | ||
896 | + .file-item { | ||
897 | + display: flex; | ||
898 | + justify-content: space-between; | ||
899 | + align-items: center; | ||
900 | + padding: 12px 16px; | ||
901 | + margin-bottom: 8px; | ||
902 | + border: 1px solid #e8e8e8; | ||
903 | + border-radius: 4px; | ||
904 | + background-color: #fafafa; | ||
905 | + | ||
906 | + &:hover { | ||
907 | + background-color: #f0f0f0; | ||
908 | + } | ||
909 | + | ||
910 | + .file-info { | ||
911 | + flex: 1; | ||
912 | + | ||
913 | + .file-name { | ||
914 | + font-weight: 500; | ||
915 | + margin-bottom: 4px; | ||
916 | + word-break: break-all; | ||
917 | + } | ||
918 | + | ||
919 | + .file-size { | ||
920 | + color: #8c8c8c; | ||
921 | + font-size: 12px; | ||
922 | + } | ||
923 | + } | ||
924 | + | ||
925 | + .file-actions { | ||
926 | + display: flex; | ||
927 | + gap: 12px; | ||
928 | + | ||
929 | + .action-btn { | ||
930 | + color: #1890ff; | ||
931 | + cursor: pointer; | ||
932 | + | ||
933 | + &:hover { | ||
934 | + color: #40a9ff; | ||
935 | + text-decoration: underline; | ||
936 | + } | ||
937 | + | ||
938 | + &.delete { | ||
939 | + color: #ff4d4f; | ||
940 | + | ||
941 | + &:hover { | ||
942 | + color: #ff7875; | ||
943 | + } | ||
944 | + } | ||
945 | + } | ||
946 | + } | ||
947 | + } | ||
948 | + .file-upload-button { | ||
949 | + width: 100%; | ||
950 | + height: 80px; | ||
951 | + border: 1px dashed #d9d9d9; | ||
952 | + border-radius: 4px; | ||
953 | + display: flex; | ||
954 | + justify-content: center; | ||
955 | + align-items: center; | ||
956 | + cursor: pointer; | ||
957 | + transition: border-color 0.3s; | ||
958 | + margin-top: 16px; | ||
959 | + | ||
960 | + &:hover { | ||
961 | + border-color: #1890ff; | ||
962 | + background-color: #f6f6f6; | ||
963 | + } | ||
964 | + | ||
965 | + .upload-button-text { | ||
966 | + text-align: center; | ||
967 | + color: #666; | ||
968 | + | ||
969 | + .anticon { | ||
970 | + font-size: 24px; | ||
971 | + margin-bottom: 8px; | ||
972 | + } | ||
973 | + } | ||
974 | + } | ||
975 | +} | ||
976 | + | ||
977 | +/* 查看模式下的图片显示样式 */ | ||
978 | +.custom-image-view { | ||
979 | + margin-top: 20px; | ||
980 | + padding: 20px; | ||
981 | + border-top: 1px solid #f0f0f0; | ||
982 | + | ||
983 | + .upload-title { | ||
984 | + font-size: 16px; | ||
985 | + font-weight: bold; | ||
986 | + margin-bottom: 16px; | ||
987 | + } | ||
988 | + | ||
989 | + .upload-content { | ||
990 | + display: flex; | ||
991 | + flex-wrap: wrap; | ||
992 | + gap: 12px; | ||
993 | + } | ||
994 | + | ||
995 | + .upload-preview { | ||
996 | + width: 104px; | ||
997 | + height: 104px; | ||
998 | + border: 1px solid #d9d9d9; | ||
999 | + border-radius: 4px; | ||
1000 | + position: relative; | ||
1001 | + overflow: hidden; | ||
1002 | + | ||
1003 | + img { | ||
1004 | + width: 100%; | ||
1005 | + height: 100%; | ||
1006 | + object-fit: cover; | ||
1007 | + } | ||
1008 | + | ||
1009 | + .view-preview-hint { | ||
1010 | + position: absolute; | ||
1011 | + top: 0; | ||
1012 | + right: 0; | ||
1013 | + bottom: 0; | ||
1014 | + left: 0; | ||
1015 | + background: rgba(0, 0, 0, 0.5); | ||
1016 | + display: flex; | ||
1017 | + align-items: center; | ||
1018 | + justify-content: center; | ||
1019 | + color: white; | ||
1020 | + opacity: 0; | ||
1021 | + transition: opacity 0.3s; | ||
1022 | + | ||
1023 | + &:hover { | ||
1024 | + opacity: 1; | ||
1025 | + } | ||
1026 | + | ||
1027 | + .anticon { | ||
1028 | + font-size: 18px; | ||
1029 | + margin-right: 8px; | ||
1030 | + } | ||
1031 | + } | ||
1032 | + } | ||
1033 | +} | ||
1034 | + | ||
1035 | +/* 查看模式下的附件显示样式 */ | ||
1036 | +.custom-file-view { | ||
1037 | + margin-top: 20px; | ||
1038 | + padding: 20px; | ||
1039 | + border-top: 1px solid #f0f0f0; | ||
1040 | + | ||
1041 | + .upload-title { | ||
1042 | + font-size: 16px; | ||
1043 | + font-weight: bold; | ||
1044 | + margin-bottom: 16px; | ||
1045 | + } | ||
1046 | + | ||
1047 | + .file-list { | ||
1048 | + width: 100%; | ||
1049 | + } | ||
1050 | + | ||
1051 | + .file-item { | ||
1052 | + display: flex; | ||
1053 | + justify-content: space-between; | ||
1054 | + align-items: center; | ||
1055 | + padding: 12px 16px; | ||
1056 | + margin-bottom: 8px; | ||
1057 | + border: 1px solid #e8e8e8; | ||
1058 | + border-radius: 4px; | ||
1059 | + background-color: #fafafa; | ||
1060 | + | ||
1061 | + &:hover { | ||
1062 | + background-color: #f0f0f0; | ||
1063 | + } | ||
1064 | + | ||
1065 | + .file-info { | ||
1066 | + flex: 1; | ||
1067 | + | ||
1068 | + .file-name { | ||
1069 | + font-weight: 500; | ||
1070 | + margin-bottom: 4px; | ||
1071 | + word-break: break-all; | ||
1072 | + } | ||
1073 | + | ||
1074 | + .file-size { | ||
1075 | + color: #8c8c8c; | ||
1076 | + font-size: 12px; | ||
1077 | + } | ||
1078 | + } | ||
1079 | + | ||
1080 | + .file-actions { | ||
1081 | + display: flex; | ||
1082 | + gap: 12px; | ||
1083 | + | ||
1084 | + .action-btn { | ||
1085 | + color: #1890ff; | ||
1086 | + cursor: pointer; | ||
1087 | + | ||
1088 | + &:hover { | ||
1089 | + color: #40a9ff; | ||
1090 | + text-decoration: underline; | ||
1091 | + } | ||
1092 | + } | ||
1093 | + } | ||
1094 | + } | ||
1095 | +} | ||
1096 | +</style> | ||
0 | \ No newline at end of file | 1097 | \ No newline at end of file |
src/views/project/quest/index.vue
0 → 100644
1 | +<template> | ||
2 | + <div class="quest-container"> | ||
3 | + <!-- 搜索区域 --> | ||
4 | + <!-- <div class="bg-white p-4 mb-4"> | ||
5 | + <BasicForm @register="registerForm" /> | ||
6 | + </div> --> | ||
7 | + | ||
8 | + <!-- 表格区域 --> | ||
9 | + <div class="bg-white"> | ||
10 | + <BasicTable @register="registerTable"> | ||
11 | + <template #toolbar> | ||
12 | + <a-button type="primary" @click="handleCreate"> | ||
13 | + 新建 | ||
14 | + </a-button> | ||
15 | + </template> | ||
16 | + | ||
17 | + <template #bodyCell="{ column, record }"> | ||
18 | + <template v-if="column.key === 'action'"> | ||
19 | + <TableAction | ||
20 | + :actions="[ | ||
21 | + { | ||
22 | + icon: 'ant-design:eye-outlined', | ||
23 | + tooltip: '查看', | ||
24 | + onClick: handleView.bind(null, record), | ||
25 | + }, | ||
26 | + { | ||
27 | + icon: 'clarity:note-edit-line', | ||
28 | + tooltip: '编辑', | ||
29 | + onClick: handleEdit.bind(null, record), | ||
30 | + }, | ||
31 | + { | ||
32 | + icon: 'ant-design:delete-outlined', | ||
33 | + color: 'error', | ||
34 | + tooltip: '删除', | ||
35 | + popConfirm: { | ||
36 | + title: '是否确认删除?', | ||
37 | + confirm: handleDelete.bind(null, record), | ||
38 | + }, | ||
39 | + }, | ||
40 | + ]" | ||
41 | + /> | ||
42 | + </template> | ||
43 | + </template> | ||
44 | + </BasicTable> | ||
45 | + </div> | ||
46 | + | ||
47 | + <!-- 编辑弹窗 --> | ||
48 | + <QuestDrawer @register="registerDrawer" @success="handleSuccess" /> | ||
49 | + </div> | ||
50 | +</template> | ||
51 | + | ||
52 | +<script lang="ts" setup name="QuestList"> | ||
53 | + import { onMounted, ref } from 'vue'; | ||
54 | + import { BasicTable, useTable, TableAction } from '/@/components/Table'; | ||
55 | + import { useDrawer } from '/@/components/Drawer'; | ||
56 | + import QuestDrawer from './QuestDrawer.vue'; | ||
57 | + import { getQuestList, questDelete } from '/@/api/project/quest'; | ||
58 | + import { columns, searchFormSchema } from './quest.data'; | ||
59 | + | ||
60 | + // 注册表格 | ||
61 | + const [registerTable, { reload }] = useTable({ | ||
62 | + title: '问题列表', | ||
63 | + api: getQuestList, | ||
64 | + columns, | ||
65 | + formConfig: { | ||
66 | + labelWidth: 120, | ||
67 | + schemas: searchFormSchema, | ||
68 | + autoSubmitOnEnter: true, | ||
69 | + }, | ||
70 | + bordered: true, | ||
71 | + showIndexColumn: true, | ||
72 | + useSearchForm: true,//启用表单搜索 | ||
73 | + }); | ||
74 | + // 注册抽屉 | ||
75 | + const [registerDrawer, { openDrawer }] = useDrawer(); | ||
76 | + | ||
77 | + // 表单提交 | ||
78 | + // async function handleSubmit(values) { | ||
79 | + // console.log('表单提交', values); | ||
80 | + // // 假设 contentText 是需要转换为数组的字段 | ||
81 | + // if (typeof values.contentText === 'string') { | ||
82 | + // values.contentText = values.contentText.split(',').map(item => item.trim()); | ||
83 | + // } | ||
84 | + // await reload(values); | ||
85 | + // return Promise.resolve(); | ||
86 | + // } | ||
87 | + | ||
88 | + // 新建 | ||
89 | + function handleCreate() { | ||
90 | + openDrawer(true, { | ||
91 | + isUpdate: false, | ||
92 | + }); | ||
93 | + } | ||
94 | + | ||
95 | + // 查看(只读模式) | ||
96 | + function handleView(record) { | ||
97 | + openDrawer(true, { | ||
98 | + record, | ||
99 | + isUpdate: true, | ||
100 | + isView: true, // 标记为查看模式 | ||
101 | + }); | ||
102 | + } | ||
103 | + | ||
104 | + // 编辑 | ||
105 | + function handleEdit(record) { | ||
106 | + console.log('编辑记录:', record); | ||
107 | + console.log('记录ID:', record.id); | ||
108 | + | ||
109 | + openDrawer(true, { | ||
110 | + record, | ||
111 | + isUpdate: true, | ||
112 | + isView: false, // 显式标记为编辑模式 | ||
113 | + }); | ||
114 | + } | ||
115 | + | ||
116 | + // 删除 | ||
117 | + async function handleDelete(record) { | ||
118 | + await questDelete([record.id]); | ||
119 | + await reload(); | ||
120 | + } | ||
121 | + | ||
122 | + // 刷新表格 | ||
123 | + function handleSuccess() { | ||
124 | + reload(); | ||
125 | + } | ||
126 | + | ||
127 | + // 页面加载完成后请求数据 | ||
128 | + onMounted(async () => { | ||
129 | + | ||
130 | + }); | ||
131 | +</script> | ||
132 | + | ||
133 | +<style lang="less" scoped> | ||
134 | + .quest-container { | ||
135 | + padding: 16px; | ||
136 | + | ||
137 | + :deep(.ant-table-wrapper) { | ||
138 | + padding: 16px; | ||
139 | + } | ||
140 | + } | ||
141 | +</style> |
src/views/project/quest/quest.data.tsx
0 → 100644
1 | +import { BasicColumn, FormSchema } from '/@/components/Table'; | ||
2 | + // 表格列定义 | ||
3 | + export const columns: BasicColumn[] = [ | ||
4 | + { | ||
5 | + title: '标题', | ||
6 | + dataIndex: 'title', | ||
7 | + key: 'title', | ||
8 | + width: 600, | ||
9 | + customHeaderCell: () => ({ style: { color: 'red' ,fontSize:'16px'} }), // 让表头变红 | ||
10 | + }, | ||
11 | + { | ||
12 | + title: '内容', | ||
13 | + dataIndex: 'contentText', | ||
14 | + key: 'contentText', | ||
15 | + format: (text) => { | ||
16 | + if (!text) return ''; | ||
17 | + const div = document.createElement('div'); | ||
18 | + div.innerHTML = text; | ||
19 | + const plainText = div.textContent || div.innerText || ''; | ||
20 | + return plainText.length > 30 ? plainText.substring(0, 30) + '...' : plainText; | ||
21 | + }, | ||
22 | + customHeaderCell: () => ({ style: { color: 'red',fontSize:'16px' } }), // 让表头变红 | ||
23 | + }, | ||
24 | + { | ||
25 | + title: '操作', | ||
26 | + dataIndex: 'action', | ||
27 | + width: 160, | ||
28 | + key: 'action', | ||
29 | + }, | ||
30 | + ]; | ||
31 | + | ||
32 | + | ||
33 | + // 表单配置 | ||
34 | + export const searchFormSchema: FormSchema[] = [ | ||
35 | + { | ||
36 | + field: 'title', | ||
37 | + label: '标题', | ||
38 | + component: 'Input', | ||
39 | + colProps: { span: 8 }, | ||
40 | + }, | ||
41 | + { | ||
42 | + field: 'contentText', | ||
43 | + label: '内容关键字', | ||
44 | + component: 'Input', | ||
45 | + colProps: { span: 8 }, | ||
46 | + }, | ||
47 | + { | ||
48 | + field: 'createBy', | ||
49 | + label: '创建人', | ||
50 | + component: 'Input', | ||
51 | + colProps: { span: 8 }, | ||
52 | + }, | ||
53 | + ]; | ||
0 | \ No newline at end of file | 54 | \ No newline at end of file |