urbanu 1 bulan lalu
induk
melakukan
e8a8264017

+ 80 - 4
src/components/MaterialPicker/index.vue

@@ -48,15 +48,28 @@
           </el-input>
           <!-- 如果有类型限制且只有一种类型,显示固定文字;否则显示选择器 -->
           <template v-if="acceptTypes.length === 1">
-            <el-tag type="info" style="margin-left: 10px">仅{{ typeNameMap[acceptTypes[0]] }}</el-tag>
+            <el-tag type="info">仅{{ typeNameMap[acceptTypes[0]] }}</el-tag>
           </template>
-          <el-select v-else v-model="filterType" placeholder="素材类型" clearable style="width: 120px; margin-left: 10px" @change="getMaterialList">
+          <el-select v-else v-model="filterType" placeholder="素材类型" clearable style="width: 120px" @change="getMaterialList">
             <el-option label="全部" value="" />
             <el-option v-if="!acceptTypes.length || acceptTypes.includes('image')" label="图片" value="image" />
             <el-option v-if="!acceptTypes.length || acceptTypes.includes('video')" label="视频" value="video" />
             <el-option v-if="!acceptTypes.length || acceptTypes.includes('audio')" label="音频" value="audio" />
             <el-option v-if="!acceptTypes.length || acceptTypes.includes('other')" label="其他" value="other" />
           </el-select>
+          <el-upload
+            class="upload-btn"
+            action="#"
+            :show-file-list="false"
+            :accept="uploadAccept"
+            :before-upload="beforeUpload"
+            :http-request="handleUpload"
+          >
+            <el-button type="primary" :loading="uploading">
+              <el-icon><Upload /></el-icon>
+              上传素材
+            </el-button>
+          </el-upload>
         </div>
 
         <div v-loading="loading" class="material-grid">
@@ -132,9 +145,11 @@
 </template>
 
 <script setup>
-import { ref, reactive, watch } from "vue";
-import { Picture, VideoPlay, Headset, Document, Check } from "@element-plus/icons-vue";
+import { ref, reactive, watch, computed } from "vue";
+import { Picture, VideoPlay, Headset, Document, Check, Upload } from "@element-plus/icons-vue";
+import { ElNotification } from "element-plus";
 import { getMaterialGroupList, getMaterialList as fetchMaterialList } from "@/api/modules/daytask.js";
+import { uploadImg } from "@/api/modules/upload.js";
 
 const props = defineProps({
   modelValue: {
@@ -257,6 +272,61 @@ const handleConfirm = () => {
     visible.value = false;
   }
 };
+
+// ==================== 上传相关 ====================
+const uploading = ref(false);
+
+// 根据 acceptTypes 计算上传接受的文件类型
+const uploadAccept = computed(() => {
+  if (props.acceptTypes.length === 0) {
+    return "*";
+  }
+  const acceptMap = {
+    image: "image/*",
+    video: "video/*",
+    audio: "audio/*",
+    other: "*"
+  };
+  return props.acceptTypes.map(t => acceptMap[t] || "*").join(",");
+});
+
+const beforeUpload = (file) => {
+  uploading.value = true;
+  // 文件大小限制 50MB
+  const isLt50M = file.size / 1024 / 1024 < 50;
+  if (!isLt50M) {
+    ElNotification.error("上传文件大小不能超过 50MB");
+    uploading.value = false;
+    return false;
+  }
+  return true;
+};
+
+// 自定义上传处理,使用后端自动保存素材功能
+const handleUpload = async (options) => {
+  const formData = new FormData();
+  formData.append("file", options.file);
+  // 传入当前选中的分组ID,后端会自动保存到素材库
+  if (selectedGroupId.value) {
+    formData.append("groupId", selectedGroupId.value);
+  }
+
+  try {
+    const res = await uploadImg(formData);
+    if (res.code === 200) {
+      ElNotification.success("上传成功");
+      // 刷新素材列表
+      getMaterialList();
+    } else {
+      ElNotification.error(res.msg || "上传失败");
+    }
+  } catch (error) {
+    console.error("上传失败", error);
+    ElNotification.error("上传失败");
+  } finally {
+    uploading.value = false;
+  }
+};
 </script>
 
 <style scoped>
@@ -316,6 +386,12 @@ const handleConfirm = () => {
   border-bottom: 1px solid #e4e7ed;
   display: flex;
   align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.upload-btn {
+  display: inline-block;
 }
 
 .material-grid {

+ 9 - 1
src/views/daytask/content/banner.vue

@@ -78,7 +78,10 @@
             <el-button v-if="formData.image" type="danger" size="small" @click="formData.image = ''">删除</el-button>
           </div>
         </div>
-        <div class="upload-tip">建议尺寸: 750 x 350 px,支持 jpg/png/gif 格式,可上传或从素材库选择</div>
+        <div class="image-url-input">
+          <el-input v-model="formData.image" placeholder="或直接输入图片URL" size="small" clearable />
+        </div>
+        <div class="upload-tip">建议尺寸: 750 x 350 px,支持 jpg/png/gif 格式</div>
       </el-form-item>
       <el-form-item label="位置" prop="position">
         <el-select v-model="formData.position" placeholder="请选择位置" style="width: 100%">
@@ -372,6 +375,11 @@ const handleBannerUpload = async (options) => {
   color: #8c939d;
 }
 
+.image-url-input {
+  margin-top: 10px;
+  width: 100%;
+}
+
 .upload-tip {
   font-size: 12px;
   color: #909399;

+ 135 - 12
src/views/daytask/task/taskList.vue

@@ -104,14 +104,27 @@
         </el-select>
       </el-form-item>
       <el-form-item label="图标" prop="icon">
-        <div class="icon-input-wrapper">
-          <el-input v-model="formData.icon" placeholder="请输入图标URL" style="flex: 1" />
-          <el-button type="primary" @click="openMaterialPicker">选择素材</el-button>
+        <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 v-if="formData.icon" class="icon-preview">
-          <el-image :src="formData.icon" style="width: 60px; height: 60px" fit="contain" />
-          <el-button type="danger" size="small" @click="formData.icon = ''">删除</el-button>
+        <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="description">
         <el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入描述" />
@@ -305,20 +318,23 @@
 <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 MaterialPicker from "@/components/MaterialPicker/index.vue";
 import { ElNotification, ElMessageBox } from "element-plus";
 import Pagination from "@/components/Pangination/Pagination.vue";
 import {
   getTaskList, createTask, updateTask, deleteTask,
   getTaskCategoryList, getTaskStepList, createTaskStep, updateTaskStep, deleteTaskStep,
-  getTaskExampleList, createTaskExample, updateTaskExample, deleteTaskExample
+  getTaskExampleList, createTaskExample, updateTaskExample, deleteTaskExample,
+  createMaterial, getMaterialGroupList
 } from "@/api/modules/daytask.js";
+import { uploadImg } from "@/api/modules/upload.js";
 
 const searchForm = ref({ title: null, category_id: null, status: null });
 const tableData = ref([]);
 const pageable = reactive({ pageNum: 1, pageSize: 30, total: 0 });
 const categoryOptions = ref([]);
+const taskIconGroupId = ref(0); // task_icon 分组ID
 
 const dialogVisible = ref(false);
 const isEdit = ref(false);
@@ -410,8 +426,21 @@ const getCategoryName = (id) => {
 onMounted(() => {
   getList();
   getCategoryList();
+  getTaskIconGroupId();
 });
 
+// 获取 task_icon 分组ID
+const getTaskIconGroupId = async () => {
+  try {
+    const res = await getMaterialGroupList({ code: "task_icon", pageSize: 1 });
+    if (res.code === 200 && res.data.list?.length > 0) {
+      taskIconGroupId.value = res.data.list[0].id;
+    }
+  } catch (e) {
+    console.error("获取task_icon分组失败", e);
+  }
+};
+
 const getCategoryList = async () => {
   try {
     const res = await getTaskCategoryList({ pageSize: 100 });
@@ -575,6 +604,59 @@ 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;
+      // 保存到素材库,使用 task_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: taskIconGroupId.value || 0,
+            status: 1
+          }
+        });
+        ElNotification.success("上传成功并已保存到素材库");
+      } catch (e) {
+        ElNotification.success("图片上传成功");
+      }
+    } else {
+      ElNotification.error(res.msg || "图片上传失败");
+    }
+  } catch (error) {
+    console.error("上传失败", error);
+    ElNotification.error("图片上传失败");
+  }
+};
+
 const handleDelete = async (row) => {
   try {
     await ElMessageBox.confirm("确认删除该任务吗?", "提示", {
@@ -810,16 +892,57 @@ const handleDeleteExample = async (row) => {
 .step-header {
   margin-bottom: 16px;
 }
-.icon-input-wrapper {
+.icon-upload-wrapper {
   display: flex;
+  align-items: flex-start;
   gap: 10px;
-  width: 100%;
 }
-.icon-preview {
+
+.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;
-  gap: 10px;
+  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;
 }
 .step-image-wrapper {
   display: flex;

+ 24 - 24
src/views/daytask/task/userTask.vue

@@ -3,10 +3,10 @@
     <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="用户ID">
-          <el-input v-model="searchForm.user_id" placeholder="用户ID" style="width:100px"></el-input>
+          <el-input v-model="searchForm.userId" placeholder="用户ID" style="width:100px"></el-input>
         </el-form-item>
         <el-form-item label="任务ID">
-          <el-input v-model="searchForm.task_id" placeholder="任务ID" style="width:100px"></el-input>
+          <el-input v-model="searchForm.taskId" placeholder="任务ID" style="width:100px"></el-input>
         </el-form-item>
         <el-form-item label="状态">
           <el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
@@ -36,12 +36,12 @@
 
       <el-table ref="myTable" :data="tableData" border size="small">
         <el-table-column prop="id" label="ID" align="center" width="80" />
-        <el-table-column prop="user_id" label="用户ID" align="center" width="100" />
-        <el-table-column prop="task_id" label="任务ID" align="center" width="100" />
-        <el-table-column prop="task_title" label="任务名称" align="center" min-width="150" />
-        <el-table-column prop="reward_amount" label="奖励金额(USDT)" align="center" min-width="130">
+        <el-table-column prop="userId" label="用户ID" align="center" width="100" />
+        <el-table-column prop="taskId" label="任务ID" align="center" width="100" />
+        <el-table-column prop="taskTitle" label="任务名称" align="center" min-width="150" />
+        <el-table-column prop="rewardAmount" label="奖励金额(USDT)" align="center" min-width="130">
           <template #default="{ row }">
-            <span class="text-success">{{ formatAmount(row.reward_amount) }}</span>
+            <span class="text-success">{{ formatAmount(row.rewardAmount) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="status" label="状态" align="center" width="100">
@@ -49,28 +49,28 @@
             <el-tag :type="getStatusType(row.status)">{{ statusMap[row.status] }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="submit_data" label="提交数据" align="center" min-width="150">
+        <el-table-column prop="screenshots" label="提交数据" align="center" min-width="150">
           <template #default="{ row }">
-            <el-button v-if="row.submit_data" type="primary" link @click="viewSubmitData(row)">查看</el-button>
+            <el-button v-if="row.screenshots" type="primary" link @click="viewSubmitData(row)">查看</el-button>
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column prop="audit_remark" label="审核备注" align="center" min-width="150" show-overflow-tooltip />
-        <el-table-column prop="submitted_at" label="提交时间" align="center" min-width="160">
+        <el-table-column prop="auditRemark" label="审核备注" align="center" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="submitTime" label="提交时间" align="center" min-width="160">
           <template #default="{ row }">
-            <span v-if="row.submitted_at">{{ formatUnix(row.submitted_at) }}</span>
+            <span v-if="row.submitTime">{{ formatUnix(row.submitTime) }}</span>
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column prop="audited_at" label="审核时间" align="center" min-width="160">
+        <el-table-column prop="auditTime" label="审核时间" align="center" min-width="160">
           <template #default="{ row }">
-            <span v-if="row.audited_at">{{ formatUnix(row.audited_at) }}</span>
+            <span v-if="row.auditTime">{{ formatUnix(row.auditTime) }}</span>
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column prop="created_at" label="创建时间" align="center" min-width="160">
+        <el-table-column prop="createdAt" label="创建时间" align="center" min-width="160">
           <template #default="{ row }">
-            <span v-if="row.created_at">{{ formatUnix(row.created_at) }}</span>
+            <span v-if="row.createdAt">{{ formatUnix(row.createdAt) }}</span>
           </template>
         </el-table-column>
         <el-table-column fixed="right" label="操作" align="center" width="150">
@@ -119,7 +119,7 @@
   <el-dialog v-model="auditDialogVisible" :title="auditStatus === 2 ? '审核通过' : '审核拒绝'" width="400px" center>
     <el-form ref="auditFormRef" :model="auditFormData" label-width="80px">
       <el-form-item label="审核备注">
-        <el-input v-model="auditFormData.audit_remark" type="textarea" :rows="3" placeholder="请输入审核备注" />
+        <el-input v-model="auditFormData.auditRemark" type="textarea" :rows="3" placeholder="请输入审核备注" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -138,7 +138,7 @@ import Pagination from "@/components/Pangination/Pagination.vue";
 import { getUserTaskList, auditUserTask } from "@/api/modules/daytask.js";
 
 const timeValue = ref([]);
-const searchForm = ref({ user_id: null, task_id: null, status: null });
+const searchForm = ref({ userId: null, taskId: null, status: null });
 const tableData = ref([]);
 const pageable = reactive({ pageNum: 1, pageSize: 30, total: 0 });
 
@@ -165,9 +165,9 @@ const currentSubmitData = ref(null);
 
 const viewSubmitData = (row) => {
   try {
-    currentSubmitData.value = typeof row.submit_data === "string" ? JSON.parse(row.submit_data) : row.submit_data;
+    currentSubmitData.value = typeof row.screenshots === "string" ? JSON.parse(row.screenshots) : row.screenshots;
   } catch (e) {
-    currentSubmitData.value = [{ value: row.submit_data }];
+    currentSubmitData.value = [{ value: row.screenshots }];
   }
   dataDialogVisible.value = true;
 };
@@ -178,12 +178,12 @@ const auditLoading = ref(false);
 const auditStatus = ref(2);
 const currentAuditRow = ref(null);
 const auditFormRef = ref(null);
-const auditFormData = ref({ audit_remark: "" });
+const auditFormData = ref({ auditRemark: "" });
 
 const handleAudit = (row, status) => {
   currentAuditRow.value = row;
   auditStatus.value = status;
-  auditFormData.value = { audit_remark: "" };
+  auditFormData.value = { auditRemark: "" };
   auditDialogVisible.value = true;
 };
 
@@ -193,7 +193,7 @@ const submitAudit = async () => {
     const params = {
       id: currentAuditRow.value.id,
       status: auditStatus.value,
-      audit_remark: auditFormData.value.audit_remark
+      auditRemark: auditFormData.value.auditRemark
     };
     const res = await auditUserTask(params);
     if (res.code === 200) {
@@ -243,7 +243,7 @@ const onSubmit = () => {
 const refresh = () => getList();
 
 const onResetSearch = () => {
-  searchForm.value = { user_id: null, task_id: null, status: null };
+  searchForm.value = { userId: null, taskId: null, status: null };
   timeValue.value = [];
   getList();
 };