Browse Source

feat: 视频标签

doi 2 months ago
parent
commit
c591ecac73
4 changed files with 139 additions and 4 deletions
  1. 1 1
      .env.development
  2. BIN
      public/favicon.ico
  3. 3 1
      src/views/cutter/label/list/index.vue
  4. 135 2
      src/views/task/manage/list/index.vue

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ NODE_ENV = development
 VUE_APP_TITLE = 蓝微帧工场
 
 # 接口地址
-VUE_APP_API_BASEURL = https://cut.bluevchat.cn/adminapi/
+VUE_APP_API_BASEURL = http://192.168.2.64:5890/adminapi
 
 # 本地端口
 VUE_APP_PORT = 2808

BIN
public/favicon.ico


+ 3 - 1
src/views/cutter/label/list/index.vue

@@ -8,6 +8,7 @@
                         <el-radio-group v-model="labelType" size="default" @change="onTypeChange">
                             <el-radio-button :label="1">行业标签</el-radio-button>
                             <el-radio-button :label="2">风格标签</el-radio-button>
+                            <el-radio-button :label="3">视频标签</el-radio-button>
                         </el-radio-group>
                         <el-button type="primary" round @click="openCreate">
                             新增标签
@@ -76,7 +77,8 @@
 <script>
 const TYPE_MAP = {
     1: "行业标签",
-    2: "风格标签"
+    2: "风格标签",
+    3: "视频标签"
 };
 
 export default {

+ 135 - 2
src/views/task/manage/list/index.vue

@@ -12,7 +12,17 @@
                 <el-table-column prop="id" label="ID" width="80" />
                 <el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
                 <el-table-column prop="explain" label="说明" min-width="220" show-overflow-tooltip />
-              
+                <el-table-column label="类型" width="110" align="center">
+                    <template #default="{ row }">
+                        {{ taskTypeLabel(row.type) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="标签" width="130" align="center">
+                    <template #default="{ row }">
+                        <el-tag v-if="taskLabelName(row)" size="small" type="info">{{ taskLabelName(row) }}</el-tag>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
                 <el-table-column prop="exp" label="经验值" width="100" align="center" />
                 <el-table-column prop="total" label="总量" width="100" align="center" />
                 <el-table-column label="等级" min-width="120" show-overflow-tooltip align="center">
@@ -83,15 +93,49 @@
 
         <el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close>
             <el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
+                <el-form-item label="类型" prop="type">
+                    <el-radio-group v-model="form.type">
+                        <el-radio label="0">普通剪辑</el-radio>
+                        <el-radio label="1">剪同款</el-radio>
+                    </el-radio-group>
+                </el-form-item>
                 <el-form-item label="标题" prop="title">
                     <el-input v-model="form.title" clearable maxlength="80" />
                 </el-form-item>
                 <el-form-item label="说明" prop="explain">
                     <el-input v-model="form.explain" type="textarea" :rows="4" />
                 </el-form-item>
+                <el-form-item label="标签" prop="label_id">
+                    <el-select
+                        v-model="form.label_id"
+                        placeholder="请选择视频标签"
+                        clearable
+                        filterable
+                        :loading="taskLabelLoading"
+                        style="width: 100%;"
+                    >
+                        <el-option
+                            v-for="item in taskLabelOptions"
+                            :key="item.id"
+                            :label="item.label"
+                            :value="item.value"
+                        />
+                    </el-select>
+                </el-form-item>
                 <el-form-item label="经验值" prop="exp">
                     <el-input v-model="form.exp" placeholder="完成任务可获得的经验值" clearable />
                 </el-form-item>
+                <el-form-item v-if="String(form.type) === '1'" label="封面视频" prop="coverVideoFiles">
+                    <scUploadFile
+                        v-model="form.coverVideoFiles"
+                        :apiObj="$API.common.upload"
+                        :multiple="false"
+                        :limit="1"
+                        accept="video/*"
+                        tip="请上传封面视频,限制1个文件"
+                        btnTxt="上传视频"
+                    />
+                </el-form-item>
                 <el-form-item label="总量" prop="taskTotal">
                     <el-input v-model="form.taskTotal" placeholder="任务可领取或完成的总名额" clearable />
                 </el-form-item>
@@ -115,7 +159,7 @@
                 <el-form-item label="素材" prop="materialFiles">
                     <scUploadFile
                         v-model="form.materialFiles"
-                        :apiObj="$API.common.uploadFile"
+                        :apiObj="$API.common.upload"
                         :multiple="true"
                         :limit="20"
                         tip="请上传素材文件,支持多文件,上传后自动写入素材链接"
@@ -173,6 +217,9 @@
 </template>
 
 <script>
+/** 与标签管理「视频标签」一致,对应接口 GET /cutter/label/list/3 */
+const TASK_LABEL_LIST_TYPE = 3;
+
 function formatMaterialLabels(material) {
     if (!Array.isArray(material) || !material.length) return [];
     return material
@@ -222,6 +269,38 @@ function normalizeTaskGradeForForm(grade) {
     return String(grade);
 }
 
+function normalizeSingleUploadFile(fileValue) {
+    if (!fileValue) return [];
+    if (typeof fileValue === "string") {
+        const url = fileValue.trim();
+        if (!url) return [];
+        const name = url.split(/[/\\]/).pop() || url;
+        return [{ name, url }];
+    }
+    if (typeof fileValue === "object") {
+        const url = String(fileValue.src || fileValue.url || "").trim();
+        if (!url) return [];
+        const name = String(fileValue.fileName || fileValue.name || "").trim() || url.split(/[/\\]/).pop() || url;
+        return [{ name, url }];
+    }
+    return [];
+}
+
+function taskTypeLabel(type) {
+    const val = String(type ?? "0");
+    return val === "1" ? "剪同款" : "普通剪辑";
+}
+
+/** 列表接口 label 可能为 { id, label_name } */
+function taskLabelName(row) {
+    const label = row?.label;
+    if (label != null && typeof label === "object") {
+        const name = label.label_name != null ? String(label.label_name).trim() : "";
+        return name || "";
+    }
+    return "";
+}
+
 export default {
     name: "taskList",
     data() {
@@ -234,6 +313,8 @@ export default {
             kefuOptions: [],
             levelLoading: false,
             levelOptions: [],
+            taskLabelLoading: false,
+            taskLabelOptions: [],
             query: {
                 page: 1,
                 limit: 15
@@ -244,6 +325,7 @@ export default {
             rules: {
                 title: [{ required: true, message: "请输入标题", trigger: "blur" }],
                 explain: [{ required: true, message: "请输入说明", trigger: "blur" }],
+                type: [{ required: true, message: "请选择类型", trigger: "change" }],
                 materialFiles: [
                     {
                         validator: (rule, value, callback) => {
@@ -253,10 +335,24 @@ export default {
                         trigger: "change"
                     }
                 ],
+                coverVideoFiles: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (String(this.form.type) !== "1") {
+                                callback();
+                                return;
+                            }
+                            if (Array.isArray(value) && value.length && value[0]?.url) callback();
+                            else callback(new Error("请上传封面视频"));
+                        },
+                        trigger: "change"
+                    }
+                ],
                 price: [{ required: true, message: "请输入价格", trigger: "blur" }],
                 exp: [{ required: true, message: "请输入经验值", trigger: "blur" }],
                 taskTotal: [{ required: true, message: "请输入总量", trigger: "blur" }],
                 grade: [{ required: true, message: "请选择等级", trigger: "change" }],
+                label_id: [{ required: true, message: "请选择标签", trigger: "change" }],
                 kefu_id: [{ required: true, message: "请选择客服", trigger: "change" }],
                 endDate: [{ required: true, message: "请选择截止时间", trigger: "change" }]
             }
@@ -265,6 +361,7 @@ export default {
     created() {
         this.getKefuOptions();
         this.getLevelOptions();
+        this.getTaskLabelOptions();
         this.getList();
     },
     methods: {
@@ -273,12 +370,16 @@ export default {
                 id: "",
                 title: "",
                 explain: "",
+                type: "0",
+                coverVideoFiles: [],
+                cover_video: "",
                 materialFiles: [],
                 price: "",
                 exp: "",
                 taskTotal: "",
                 grade: "",
                 kefu_id: "",
+                label_id: "",
                 endDate: "",
                 end_time: "",
                 status: 1
@@ -291,6 +392,8 @@ export default {
             return this.$TOOL.getTime(Math.floor(num / 1000), "YYYY-MM-DD HH:mm:ss");
         },
         taskGradeNameLabel,
+        taskTypeLabel,
+        taskLabelName,
         async getList() {
             this.loading = true;
             try {
@@ -353,6 +456,25 @@ export default {
                 this.levelLoading = false;
             }
         },
+        async getTaskLabelOptions() {
+            this.taskLabelLoading = true;
+            try {
+                const res = await this.$API.cutter.labelList.get(TASK_LABEL_LIST_TYPE);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取标签列表失败");
+                    return;
+                }
+                const raw = res.data;
+                const rawList = Array.isArray(raw) ? raw : raw?.list || raw?.data || [];
+                this.taskLabelOptions = rawList.map((item) => ({
+                    id: item.id,
+                    value: String(item.id),
+                    label: item.label_name ?? item.name ?? String(item.id)
+                }));
+            } finally {
+                this.taskLabelLoading = false;
+            }
+        },
         handlePageChange(page) {
             this.query.page = page;
             this.getList();
@@ -385,12 +507,19 @@ export default {
                 id: info.id || row.id,
                 title: info.title || "",
                 explain: info.explain || "",
+                type: String(info.type ?? "0"),
+                coverVideoFiles: normalizeSingleUploadFile(info.cover_video),
+                cover_video: String(info.cover_video || ""),
                 materialFiles,
                 price: info.price || info.pirce || "",
                 exp: info.exp != null && info.exp !== "" ? String(info.exp) : "",
                 taskTotal: info.total != null && info.total !== "" ? String(info.total) : "",
                 grade: normalizeTaskGradeForForm(info.grade),
                 kefu_id: String(info.kefu_id || ""),
+                label_id:
+                    info.label_id != null && info.label_id !== ""
+                        ? String(info.label_id)
+                        : "",
                 endDate: endTime ? String(endTime) : "",
                 end_time: String(info.end_time || ""),
                 status: Number(info.status ?? 1)
@@ -405,15 +534,19 @@ export default {
                         fileName: f.name || "",
                         src: f.url || ""
                     })).filter((m) => m.src);
+                    const coverVideo = (this.form.coverVideoFiles || [])[0]?.url || "";
                     const endTimeSec = this.form.endDate ? Math.floor(Number(this.form.endDate) / 1000) : "";
                     const payload = {
                         title: this.form.title,
                         explain: this.form.explain,
+                        type: String(this.form.type),
+                        cover_video: coverVideo,
                         material,
                         price: this.form.price,
                         exp: this.form.exp,
                         total: this.form.taskTotal,
                         grade: this.form.grade,
+                        label_id: this.form.label_id,
                         end_time: String(endTimeSec),
                         kefu_id: this.form.kefu_id,
                         status: String(this.form.status)