Commit c6c33024847bac4c98008ce13575f4fe2941df20

Authored by chenhang4442024
1 parent 8b6413ac

feat: 增加问题合集

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 24 </BasicTable>
25 25 <BasicDrawer
26 26 width="500"
27   - :showFooter="!isApproved && (role === ROLE.ADMIN || role === ROLE.BUSINESS)"
  27 + :showFooter="!isApproved && (role === ROLE.ADMIN || (role === ROLE.BUSINESS && !shouldHideApproveButton))"
28 28 @register="registerDrawer"
29 29 title="申请信息"
30 30 okText="通过"
... ... @@ -278,6 +278,10 @@
278 278 const role = computed(() => {
279 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 286 // 定义MsgModalClose的事件,方便子组件调用
283 287 const handleMsgModalClose = async (data) => {
... ... @@ -313,6 +317,7 @@
313 317 msgVisible,
314 318 handleMsgModalClose,
315 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 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 54 \ No newline at end of file
... ...