Procházet zdrojové kódy

feat: 用户等级列表

doi před 2 měsíci
rodič
revize
11151037d3

+ 6 - 0
src/api/model/user.js

@@ -74,12 +74,18 @@ export default {
         name: "用户等级保存",
         post: async function (data = {}) {
             const id = data.id;
+            const colorRaw = data.color;
+            const color = Array.isArray(colorRaw)
+                ? colorRaw.map((x) => String(x ?? "").trim()).filter(Boolean)
+                : [];
             const payload = {
                 name: String(data.name ?? ""),
                 grade: String(data.grade ?? ""),
                 icon: String(data.icon ?? ""),
                 explain: String(data.explain ?? ""),
                 exp_num: String(data.exp_num ?? ""),
+                background: String(data.background ?? ""),
+                color,
             };
             const url = id != null && String(id) !== "" ? `${this.url}/${id}` : this.url;
             return await http.post(url, payload);

+ 16 - 29
src/views/cutter/order/index.vue

@@ -71,7 +71,7 @@
                             <el-button type="info" size="small" @click="showDetail(row)">详情</el-button>
                             <el-button v-if="canAudit(row)" type="success" size="small" @click="affirmOrder(row)">通过</el-button>
                             <el-button v-if="canAudit(row)" type="danger" size="small" @click="openRemark(row, 'reject')">拒绝</el-button>
-                            <el-button v-if="canSettle(row)" type="warning" size="small" @click="settleOrder(row)">结算</el-button>
+                            <el-button v-if="canSettle(row)" type="warning" size="small" @click="openRemark(row, 'settle')">结算</el-button>
                         </div>
                     </template>
                 </el-table-column>
@@ -247,35 +247,17 @@ export default {
             this.$message.success(res.msg || "操作成功");
             this.getList();
         },
-        async settleOrder(row) {
-            const id = row?.id;
-            if (id == null) {
-                this.$message.error("缺少订单ID");
-                return;
-            }
-            try {
-                await this.$confirm("确认将该订单结算吗?", "提示", {
-                    type: "warning",
-                    confirmButtonText: "确认",
-                    cancelButtonText: "取消"
-                });
-            } catch (e) {
-                return;
-            }
-            const res = await this.$API.cutter.orderSettle.post(id);
-            if (res.code !== 1) {
-                this.$message.error(res.msg || "操作失败");
-                return;
-            }
-            this.$message.success(res.msg || "结算成功");
-            this.getList();
-        },
         openRemark(row, action) {
             this.currentRow = row;
             this.remarkAction = action;
             this.remark = "";
-            this.remarkTitle = "订单拒绝";
-            this.remarkPlaceholder = "请输入拒绝理由(必填)";
+            if (action === "settle") {
+                this.remarkTitle = "订单结算";
+                this.remarkPlaceholder = "请输入结算备注(必填)";
+            } else {
+                this.remarkTitle = "订单拒绝";
+                this.remarkPlaceholder = "请输入拒绝理由(必填)";
+            }
             this.remarkDialog = true;
         },
         resetRemark() {
@@ -294,17 +276,22 @@ export default {
             }
             const remark = (this.remark || "").trim();
             if (!remark) {
-                this.$message.warning("请填写拒绝理由");
+                this.$message.warning(this.remarkAction === "settle" ? "请填写结算备注" : "请填写拒绝理由");
                 return;
             }
             this.submitting = true;
             try {
-                const res = await this.$API.cutter.orderReject.post(id, { remark });
+                let res;
+                if (this.remarkAction === "settle") {
+                    res = await this.$API.cutter.orderSettle.post(id, { remark });
+                } else {
+                    res = await this.$API.cutter.orderReject.post(id, { remark });
+                }
                 if (res.code !== 1) {
                     this.$message.error(res.msg || "操作失败");
                     return;
                 }
-                this.$message.success(res.msg || "操作成功");
+                this.$message.success(res.msg || (this.remarkAction === "settle" ? "结算成功" : "操作成功"));
                 this.remarkDialog = false;
                 this.getList();
             } finally {

+ 85 - 0
src/views/task/manage/list/index.vue

@@ -15,6 +15,11 @@
               
                 <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">
+                    <template #default="{ row }">
+                        {{ taskGradeNameLabel(row.grade) }}
+                    </template>
+                </el-table-column>
                 <el-table-column prop="kefu_id" label="客服ID" width="100" align="center" />
                 <el-table-column prop="price" label="价格" width="110" />
                 <el-table-column label="素材" min-width="180">
@@ -84,6 +89,29 @@
                 <el-form-item label="说明" prop="explain">
                     <el-input v-model="form.explain" type="textarea" :rows="4" />
                 </el-form-item>
+                <el-form-item label="经验值" prop="exp">
+                    <el-input v-model="form.exp" placeholder="完成任务可获得的经验值" clearable />
+                </el-form-item>
+                <el-form-item label="总量" prop="taskTotal">
+                    <el-input v-model="form.taskTotal" placeholder="任务可领取或完成的总名额" clearable />
+                </el-form-item>
+                <el-form-item label="等级" prop="grade">
+                    <el-select
+                        v-model="form.grade"
+                        placeholder="请选择用户等级"
+                        clearable
+                        filterable
+                        :loading="levelLoading"
+                        style="width: 100%;"
+                    >
+                        <el-option
+                            v-for="item in levelOptions"
+                            :key="item.id"
+                            :label="item.label"
+                            :value="item.value"
+                        />
+                    </el-select>
+                </el-form-item>
                 <el-form-item label="素材" prop="materialFiles">
                     <scUploadFile
                         v-model="form.materialFiles"
@@ -175,6 +203,25 @@ function normalizeMaterialFiles(material) {
         .filter(Boolean);
 }
 
+/** 列表接口 grade 为 null 或 { name, grade, icon, color } */
+function taskGradeNameLabel(grade) {
+    if (grade != null && typeof grade === "object") {
+        const name = grade.name != null ? String(grade.name).trim() : "";
+        return name || "-";
+    }
+    return "-";
+}
+
+/** 详情/保存表单:接口可能返回嵌套对象或纯等级数字 */
+function normalizeTaskGradeForForm(grade) {
+    if (grade == null || grade === "") return "";
+    if (typeof grade === "object") {
+        if (grade.grade != null && grade.grade !== "") return String(grade.grade);
+        return "";
+    }
+    return String(grade);
+}
+
 export default {
     name: "taskList",
     data() {
@@ -185,6 +232,8 @@ export default {
             total: 0,
             kefuLoading: false,
             kefuOptions: [],
+            levelLoading: false,
+            levelOptions: [],
             query: {
                 page: 1,
                 limit: 15
@@ -205,6 +254,9 @@ export default {
                     }
                 ],
                 price: [{ required: true, message: "请输入价格", trigger: "blur" }],
+                exp: [{ required: true, message: "请输入经验值", trigger: "blur" }],
+                taskTotal: [{ required: true, message: "请输入总量", trigger: "blur" }],
+                grade: [{ required: true, message: "请选择等级", trigger: "change" }],
                 kefu_id: [{ required: true, message: "请选择客服", trigger: "change" }],
                 endDate: [{ required: true, message: "请选择截止时间", trigger: "change" }]
             }
@@ -212,6 +264,7 @@ export default {
     },
     created() {
         this.getKefuOptions();
+        this.getLevelOptions();
         this.getList();
     },
     methods: {
@@ -222,6 +275,9 @@ export default {
                 explain: "",
                 materialFiles: [],
                 price: "",
+                exp: "",
+                taskTotal: "",
+                grade: "",
                 kefu_id: "",
                 endDate: "",
                 end_time: "",
@@ -234,6 +290,7 @@ export default {
             if (!num) return ts;
             return this.$TOOL.getTime(Math.floor(num / 1000), "YYYY-MM-DD HH:mm:ss");
         },
+        taskGradeNameLabel,
         async getList() {
             this.loading = true;
             try {
@@ -274,6 +331,28 @@ export default {
                 this.kefuLoading = false;
             }
         },
+        async getLevelOptions() {
+            this.levelLoading = true;
+            try {
+                const res = await this.$API.user.levelList.get();
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取用户等级列表失败");
+                    return;
+                }
+                const raw = res.data || {};
+                const rawList = Array.isArray(raw) ? raw : raw.list || raw.data || [];
+                const sorted = rawList.slice().sort((a, b) => Number(a.grade) - Number(b.grade));
+                this.levelOptions = sorted.map((item) => ({
+                    id: item.id,
+                    value: String(item.grade ?? ""),
+                    label: item.name
+                        ? `${item.name}(等级 ${item.grade})`
+                        : `等级 ${item.grade}`
+                }));
+            } finally {
+                this.levelLoading = false;
+            }
+        },
         handlePageChange(page) {
             this.query.page = page;
             this.getList();
@@ -308,6 +387,9 @@ export default {
                 explain: info.explain || "",
                 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 || ""),
                 endDate: endTime ? String(endTime) : "",
                 end_time: String(info.end_time || ""),
@@ -329,6 +411,9 @@ export default {
                         explain: this.form.explain,
                         material,
                         price: this.form.price,
+                        exp: this.form.exp,
+                        total: this.form.taskTotal,
+                        grade: this.form.grade,
                         end_time: String(endTimeSec),
                         kefu_id: this.form.kefu_id,
                         status: String(this.form.status)

+ 151 - 2
src/views/user/level/list/index.vue

@@ -21,13 +21,31 @@
                             v-if="row.icon"
                             class="level-icon"
                             :src="row.icon"
-                            fit="cover"
+                            fit="contain"
                             :preview-src-list="[row.icon]"
                             preview-teleported
                         />
                         <span v-else>-</span>
                     </template>
                 </el-table-column>
+                <el-table-column label="背景图" width="88" align="center">
+                    <template #default="{ row }">
+                        <el-image
+                            v-if="row.background"
+                            class="level-icon"
+                            :src="row.background"
+                            fit="contain"
+                            :preview-src-list="[row.background]"
+                            preview-teleported
+                        />
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="颜色" min-width="140" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ rowColorSummary(row) }}
+                    </template>
+                </el-table-column>
                 <el-table-column prop="explain" label="说明" min-width="200" show-overflow-tooltip />
                 <el-table-column label="显示" width="120" align="center">
                     <template #default="{ row }">
@@ -89,12 +107,29 @@
                     />
                     <span v-else>-</span>
                 </el-descriptions-item>
+                <el-descriptions-item label="背景图">
+                    <el-image
+                        v-if="detail.background"
+                        class="detail-bg"
+                        :src="detail.background"
+                        fit="cover"
+                        :preview-src-list="[detail.background]"
+                        preview-teleported
+                    />
+                    <span v-else>-</span>
+                </el-descriptions-item>
+                <el-descriptions-item label="颜色">
+                    <template v-if="detailColorTags.length">
+                        <el-tag v-for="(c, i) in detailColorTags" :key="i" size="small" class="color-tag">{{ c }}</el-tag>
+                    </template>
+                    <span v-else>-</span>
+                </el-descriptions-item>
                 <el-descriptions-item label="创建时间">{{ formatTime(detail.created_at) }}</el-descriptions-item>
                 <el-descriptions-item label="更新时间">{{ formatTime(detail.updated_at) }}</el-descriptions-item>
             </el-descriptions>
         </el-dialog>
 
-        <el-dialog v-model="editDialog" :title="editTitle" width="520px" destroy-on-close @closed="resetEditForm">
+        <el-dialog v-model="editDialog" :title="editTitle" width="580px" destroy-on-close @closed="resetEditForm">
             <el-form ref="formRef" :model="form" :rules="rules" label-width="96px">
                 <el-form-item label="名称" prop="name">
                     <el-input v-model="form.name" clearable maxlength="64" placeholder="如 Lv1" />
@@ -111,6 +146,28 @@
                         <el-input v-model="form.icon" placeholder="或填写图片地址" clearable />
                     </div>
                 </el-form-item>
+                <el-form-item label="背景图" prop="background">
+                    <div class="icon-field">
+                        <sc-upload v-model="form.background" title="上传" :api-obj="$API.common.upload" />
+                        <el-input v-model="form.background" placeholder="或填写背景图地址" clearable />
+                    </div>
+                </el-form-item>
+                <el-form-item label="颜色" prop="colorList">
+                    <div class="color-palette-field">
+                        <p v-if="!form.colorList.length" class="palette-hint">点击「添加颜色」,用取色器选择</p>
+                        <div v-for="(_, index) in form.colorList" :key="'palette-row-' + index" class="palette-row">
+                            <el-color-picker
+                                :model-value="form.colorList[index]"
+                                color-format="hex"
+                                :show-alpha="false"
+                                @update:model-value="(v) => updatePaletteColor(index, v)"
+                            />
+                            <span class="palette-hex">{{ form.colorList[index] }}</span>
+                            <el-button type="danger" link size="small" @click="removePaletteColor(index)">移除</el-button>
+                        </div>
+                        <el-button size="small" type="primary" plain @click="addPaletteColor">添加颜色</el-button>
+                    </div>
+                </el-form-item>
                 <el-form-item label="说明" prop="explain">
                     <el-input v-model="form.explain" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="等级说明" />
                 </el-form-item>
@@ -131,12 +188,50 @@ function defaultLevelForm() {
         grade: 1,
         exp_num: 0,
         icon: "",
+        background: "",
+        colorList: [],
         explain: ""
     };
 }
 
+/** 规范为六位小写 #rrggbb,供接口 ["#000000"] 格式 */
+function normalizeHex(val) {
+    if (val == null || val === "") return "#000000";
+    let s = String(val).trim();
+    if (!s.startsWith("#")) s = `#${s}`;
+    if (s.length === 9 && /^#[0-9a-fA-F]{8}$/.test(s)) s = s.slice(0, 7);
+    if (s.length === 4 && /^#[0-9a-fA-F]{3}$/.test(s)) {
+        s = `#${s[1]}${s[1]}${s[2]}${s[2]}${s[3]}${s[3]}`;
+    }
+    if (!/^#[0-9a-fA-F]{6}$/.test(s)) return "#000000";
+    return s.toLowerCase();
+}
+
+/** 将接口里的 color(一维字符串数组;兼容历史 JSON 字符串或逗号分隔)解析为字符串数组 */
+function parseColorList(val) {
+    if (val == null || val === "") return [];
+    if (Array.isArray(val)) return val.map(String).filter(Boolean);
+    if (typeof val === "string") {
+        const s = val.trim();
+        if (!s) return [];
+        try {
+            const parsed = JSON.parse(s);
+            if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
+        } catch (_) {
+            /* 非 JSON 时按分隔符拆分 */
+        }
+        return s.split(/[,,]/).map((x) => x.trim()).filter(Boolean);
+    }
+    return [];
+}
+
 export default {
     name: "UserLevelList",
+    computed: {
+        detailColorTags() {
+            return parseColorList(this.detail?.color);
+        }
+    },
     data() {
         return {
             loading: false,
@@ -160,6 +255,25 @@ export default {
         this.getList();
     },
     methods: {
+        rowColorSummary(row) {
+            const arr = parseColorList(row?.color);
+            if (!arr.length) return "-";
+            return arr.join("、");
+        },
+        addPaletteColor() {
+            this.form.colorList.push("#409eff");
+        },
+        removePaletteColor(index) {
+            this.form.colorList.splice(index, 1);
+        },
+        updatePaletteColor(index, val) {
+            if (val == null || val === "") {
+                this.form.colorList.splice(index, 1);
+                return;
+            }
+            const hex = normalizeHex(val);
+            this.form.colorList.splice(index, 1, hex);
+        },
         formatTime(val) {
             if (val === undefined || val === null || val === "") return "-";
             if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}/.test(val.trim())) return val.trim();
@@ -207,6 +321,8 @@ export default {
                 grade: row.grade != null ? Number(row.grade) : 1,
                 exp_num: row.exp_num != null ? Number(row.exp_num) : 0,
                 icon: row.icon || "",
+                background: row.background || "",
+                colorList: parseColorList(row.color).map((c) => normalizeHex(c)),
                 explain: row.explain || ""
             };
             this.editDialog = true;
@@ -220,11 +336,15 @@ export default {
                 if (!valid) return false;
                 this.saving = true;
                 try {
+                    /** 接口约定:color 为一维数组,元素为颜色字符串,如 ["#000000","#ffffff"] */
+                    const color = (this.form.colorList || []).map((x) => normalizeHex(x));
                     const payload = {
                         name: (this.form.name || "").trim(),
                         grade: this.form.grade,
                         exp_num: this.form.exp_num,
                         icon: (this.form.icon || "").trim(),
+                        background: (this.form.background || "").trim(),
+                        color,
                         explain: (this.form.explain || "").trim()
                     };
                     if (this.form.id) payload.id = String(this.form.id);
@@ -325,4 +445,33 @@ export default {
 .w-full-num :deep(.el-input__inner) {
     text-align: left;
 }
+.color-palette-field {
+    width: 100%;
+}
+.palette-hint {
+    margin: 0 0 10px;
+    font-size: 12px;
+    color: var(--el-text-color-placeholder);
+}
+.palette-row {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 10px;
+}
+.palette-hex {
+    font-family: ui-monospace, monospace;
+    font-size: 13px;
+    color: var(--el-text-color-regular);
+    min-width: 7.5em;
+}
+.detail-bg {
+    max-width: 240px;
+    max-height: 120px;
+    border-radius: 8px;
+}
+.color-tag {
+    margin-right: 6px;
+    margin-bottom: 4px;
+}
 </style>

+ 5 - 0
src/views/user/list/index.vue

@@ -14,6 +14,11 @@
                         {{ row.phone ?? "-" }}
                     </template>
                 </el-table-column>
+                <el-table-column prop="nickname" label="等级" min-width="140" ><template #default="{ row }">
+                        {{ row.level?.name ?? "-" }}
+                    </template>
+                </el-table-column>
+
                 <el-table-column prop="brokerage_price" label="佣金余额" width="100" align="right" />
                 <el-table-column label="剪辑师" width="88" align="center">
                     <template #default="{ row }">