فهرست منبع

任务分类,上传图标保存到素材

urbanu 1 ماه پیش
والد
کامیت
f403bd48c3
2فایلهای تغییر یافته به همراه176 افزوده شده و 8 حذف شده
  1. 3 3
      src/views/daytask/material/materialList.vue
  2. 173 5
      src/views/daytask/task/taskCategory.vue

+ 3 - 3
src/views/daytask/material/materialList.vue

@@ -3,7 +3,7 @@
     <div class="card table-search">
       <el-form ref="elSearchFormRef" :inline="true" size="small" :model="searchForm" class="demo-form-inline" @keyup.enter="onSubmit">
         <el-form-item label="分组">
-          <el-select v-model="searchForm.group_id" placeholder="分组" clearable style="width:150px">
+          <el-select v-model="searchForm.groupId" placeholder="分组" clearable style="width:150px">
             <el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
           </el-select>
         </el-form-item>
@@ -213,7 +213,7 @@ import Pagination from "@/components/Pangination/Pagination.vue";
 import { getMaterialList, createMaterial, updateMaterial, deleteMaterial, getMaterialGroupList } from "@/api/modules/daytask.js";
 import { uploadImg } from "@/api/modules/upload.js";
 
-const searchForm = ref({ group_id: null, name: null, type: null });
+const searchForm = ref({ groupId: null, name: null, type: null });
 const tableData = ref([]);
 const pageable = reactive({ pageNum: 1, pageSize: 30, total: 0 });
 const groupOptions = ref([]);
@@ -328,7 +328,7 @@ const onSubmit = () => {
 const refresh = () => getList();
 
 const onResetSearch = () => {
-  searchForm.value = { group_id: null, name: null, type: null };
+  searchForm.value = { groupId: null, name: null, type: null };
   getList();
 };
 

+ 173 - 5
src/views/daytask/task/taskCategory.vue

@@ -51,7 +51,27 @@
         <el-input v-model="formData.name" placeholder="请输入分类名称" />
       </el-form-item>
       <el-form-item label="图标" prop="icon">
-        <el-input v-model="formData.icon" placeholder="请输入图标URL" />
+        <div class="icon-upload-wrapper">
+          <el-upload
+            class="icon-uploader"
+            action="#"
+            :show-file-list="false"
+            :http-request="handleIconUpload"
+            :before-upload="beforeIconUpload"
+            accept="image/*"
+          >
+            <el-image v-if="formData.icon" :src="formData.icon" class="icon-preview-img" fit="contain" />
+            <el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
+          </el-upload>
+          <div class="icon-actions">
+            <el-button type="primary" size="small" @click="openMaterialPicker">从素材库选择</el-button>
+            <el-button v-if="formData.icon" type="danger" size="small" @click="formData.icon = ''">删除</el-button>
+          </div>
+        </div>
+        <div class="icon-url-input">
+          <el-input v-model="formData.icon" placeholder="或直接输入图标URL" size="small" clearable />
+        </div>
+        <div class="upload-tip">建议尺寸: 100 x 100 px,支持 jpg/png/gif 格式</div>
       </el-form-item>
       <el-form-item label="排序" prop="sort">
         <el-input-number v-model="formData.sort" :min="0" :max="9999" style="width: 100%" />
@@ -71,20 +91,31 @@
       <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
     </template>
   </el-dialog>
+
+  <!-- 素材选择器 -->
+  <MaterialPicker
+    v-model="materialPickerVisible"
+    :accept-types="['image']"
+    @confirm="handleMaterialSelect"
+  />
 </template>
 
 <script setup>
 import { ref, reactive, onMounted } from "vue";
 import dayjs from "dayjs";
-import { Refresh } from "@element-plus/icons-vue";
+import { Refresh, Plus } from "@element-plus/icons-vue";
 import { ElNotification, ElMessageBox } from "element-plus";
 import Pagination from "@/components/Pangination/Pagination.vue";
-import { getTaskCategoryList, createTaskCategory, updateTaskCategory, deleteTaskCategory } from "@/api/modules/daytask.js";
+import MaterialPicker from "@/components/MaterialPicker/index.vue";
+import { getTaskCategoryList, createTaskCategory, updateTaskCategory, deleteTaskCategory, createMaterial, getMaterialGroupList } from "@/api/modules/daytask.js";
+import { uploadImg } from "@/api/modules/upload.js";
 
 const tableData = ref([]);
 const pageable = reactive({ pageNum: 1, pageSize: 30, total: 0 });
+const categoryIconGroupId = ref(0); // category_icon 分组ID
 
 const dialogVisible = ref(false);
+const materialPickerVisible = ref(false);
 const isEdit = ref(false);
 const formRef = ref(null);
 const submitLoading = ref(false);
@@ -108,8 +139,21 @@ const formatUnix = (val) => {
 
 onMounted(() => {
   getList();
+  getCategoryIconGroupId();
 });
 
+// 获取 category_icon 分组ID
+const getCategoryIconGroupId = async () => {
+  try {
+    const res = await getMaterialGroupList({ code: "category_icon", pageSize: 1 });
+    if (res.code === 200 && res.data.list?.length > 0) {
+      categoryIconGroupId.value = res.data.list[0].id;
+    }
+  } catch (e) {
+    console.error("获取category_icon分组失败", e);
+  }
+};
+
 const getList = async () => {
   tableData.value = [];
   const params = {
@@ -162,8 +206,15 @@ const handleSubmit = async () => {
     if (!valid) return;
     submitLoading.value = true;
     try {
-      const api = isEdit.value ? updateTaskCategory : createTaskCategory;
-      const res = await api(formData.value);
+      let res;
+      if (isEdit.value) {
+        // 更新时,后端期望格式: { id: xxx, data: { ...fields } }
+        const { id, ...updateFields } = formData.value;
+        res = await updateTaskCategory({ id, data: updateFields });
+      } else {
+        // 创建时,后端期望格式: { data: { ...fields } }
+        res = await createTaskCategory({ data: formData.value });
+      }
       if (res.code === 200) {
         ElNotification.success(isEdit.value ? "更新成功" : "创建成功");
         dialogVisible.value = false;
@@ -199,4 +250,121 @@ const handleDelete = async (row) => {
     }
   }
 };
+
+// ==================== 素材选择器 ====================
+const openMaterialPicker = () => {
+  materialPickerVisible.value = true;
+};
+
+const handleMaterialSelect = (material) => {
+  formData.value.icon = material.url;
+};
+
+// ==================== 图标上传 ====================
+const beforeIconUpload = (file) => {
+  const isImage = file.type.startsWith("image/");
+  const isLt5M = file.size / 1024 / 1024 < 5;
+
+  if (!isImage) {
+    ElNotification.warning("只能上传图片文件");
+    return false;
+  }
+  if (!isLt5M) {
+    ElNotification.warning("图片大小不能超过 5MB");
+    return false;
+  }
+  return true;
+};
+
+const handleIconUpload = async (options) => {
+  const uploadFormData = new FormData();
+  uploadFormData.append("file", options.file);
+  uploadFormData.append("autoSave", "false"); // 禁止后端自动保存,由前端控制
+
+  try {
+    const res = await uploadImg(uploadFormData);
+    if (res.code === 200) {
+      formData.value.icon = res.data.path;
+      // 保存到素材库,使用 category_icon 分组ID
+      const fileName = options.file.name;
+      const file = options.file;
+      try {
+        await createMaterial({
+          data: {
+            name: fileName,
+            type: "image",
+            url: res.data.path,
+            size: file.size || 0,
+            mimeType: file.type || "",
+            groupId: categoryIconGroupId.value || 0,
+            status: 1
+          }
+        });
+        ElNotification.success("上传成功并已保存到素材库");
+      } catch (e) {
+        ElNotification.success("图片上传成功");
+      }
+    } else {
+      ElNotification.error(res.msg || "图片上传失败");
+    }
+  } catch (error) {
+    console.error("上传失败", error);
+    ElNotification.error("图片上传失败");
+  }
+};
 </script>
+
+<style scoped>
+.icon-upload-wrapper {
+  display: flex;
+  align-items: flex-start;
+  gap: 10px;
+}
+
+.icon-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.icon-uploader {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  overflow: hidden;
+  transition: border-color 0.3s;
+}
+
+.icon-uploader:hover {
+  border-color: #409eff;
+}
+
+.icon-uploader :deep(.el-upload) {
+  width: 80px;
+  height: 80px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.icon-preview-img {
+  width: 80px;
+  height: 80px;
+}
+
+.icon-uploader-icon {
+  font-size: 24px;
+  color: #8c939d;
+}
+
+.icon-url-input {
+  margin-top: 10px;
+  width: 100%;
+}
+
+.upload-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 5px;
+}
+</style>