|
|
@@ -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>
|