Selaa lähdekoodia

feat: 用户等级列表

doi 2 kuukautta sitten
vanhempi
sitoutus
81a4041282
3 muutettua tiedostoa jossa 382 lisäystä ja 2 poistoa
  1. 49 0
      src/api/model/user.js
  2. 5 2
      src/views/task/manage/list/index.vue
  3. 328 0
      src/views/user/level/list/index.vue

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

@@ -52,4 +52,53 @@ export default {
             return await http.post(this.url, params);
         },
     },
+    /** 用户等级列表 adminapi/user/level/list */
+    levelList: {
+        url: `${config.API_URL}/user/level/list`,
+        name: "用户等级列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        },
+    },
+    /** 用户等级详情 adminapi/user/level/info/:id */
+    levelInfo: {
+        url: `${config.API_URL}/user/level/info`,
+        name: "用户等级详情",
+        get: async function (id) {
+            return await http.get(`${this.url}/${id}`);
+        },
+    },
+    /** 用户等级新增/编辑:新增 POST .../save;编辑 POST .../save/:id */
+    levelSave: {
+        url: `${config.API_URL}/user/level/save`,
+        name: "用户等级保存",
+        post: async function (data = {}) {
+            const id = data.id;
+            const payload = {
+                name: String(data.name ?? ""),
+                grade: String(data.grade ?? ""),
+                icon: String(data.icon ?? ""),
+                explain: String(data.explain ?? ""),
+                exp_num: String(data.exp_num ?? ""),
+            };
+            const url = id != null && String(id) !== "" ? `${this.url}/${id}` : this.url;
+            return await http.post(url, payload);
+        },
+    },
+    /** 用户等级删除 adminapi/user/level/del/:id */
+    levelDel: {
+        url: `${config.API_URL}/user/level/del`,
+        name: "用户等级删除",
+        delete: async function (id) {
+            return await http.delete(`${this.url}/${id}`);
+        },
+    },
+    /** 显示开关 adminapi/user/level/set_show/:id/:show */
+    levelSetShow: {
+        url: `${config.API_URL}/user/level/set_show`,
+        name: "用户等级显示设置",
+        get: async function (id, show) {
+            return await http.get(`${this.url}/${id}/${show}`);
+        },
+    },
 }

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

@@ -12,6 +12,11 @@
                 <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 prop="exp" label="经验值" width="100" align="center" />
+                <el-table-column prop="total" label="总量" width="100" align="center" />
+                <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">
                     <template #default="{ row }">
                         <div v-if="row.materialDisplay.length">
@@ -27,8 +32,6 @@
                         <span v-else>-</span>
                     </template>
                 </el-table-column>
-                <el-table-column prop="kefu_id" label="客服ID" width="100" />
-                <el-table-column prop="price" label="价格" width="110" />
                 <el-table-column label="状态" width="120">
                     <template #default="{ row }">
                         <el-switch

+ 328 - 0
src/views/user/level/list/index.vue

@@ -0,0 +1,328 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <div class="header-row">
+                    <span class="card-title">用户等级</span>
+                    <div class="header-actions">
+                        <el-button type="primary"  @click="openCreate">添加等级</el-button>
+                    </div>
+                </div>
+            </template>
+
+            <el-table v-loading="loading" :data="list" stripe class="level-table">
+                <el-table-column prop="id" label="ID" width="88" />
+                <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
+                <el-table-column prop="grade" label="等级" width="88" align="center" />
+                <el-table-column prop="exp_num" label="所需经验" width="110" align="right" />
+                <el-table-column label="图标" width="88" align="center">
+                    <template #default="{ row }">
+                        <el-image
+                            v-if="row.icon"
+                            class="level-icon"
+                            :src="row.icon"
+                            fit="cover"
+                            :preview-src-list="[row.icon]"
+                            preview-teleported
+                        />
+                        <span v-else>-</span>
+                    </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 }">
+                        <el-switch
+                            :model-value="Number(row.is_show)"
+                            :active-value="1"
+                            :inactive-value="0"
+                            inline-prompt
+                            active-text="显示"
+                            inactive-text="隐藏"
+                            active-color="#13ce66"
+                            inactive-color="#909399"
+                            @change="(val) => changeShow(row, val)"
+                        />
+                    </template>
+                </el-table-column>
+                <el-table-column label="创建时间" min-width="170" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatTime(row.created_at) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="更新时间" min-width="170" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatTime(row.updated_at) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="248" fixed="right">
+                    <template #default="{ row }">
+                        <div class="table-row-actions">
+                            <el-button type="info" size="small" @click="showDetail(row)">详情</el-button>
+                            <el-button type="primary" size="small" @click="openEdit(row)">编辑</el-button>
+                            <el-button type="danger" size="small" @click="removeRow(row)">删除</el-button>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </el-card>
+
+        <el-dialog v-model="detailDialog" title="等级详情" width="560px" destroy-on-close>
+            <el-descriptions :column="1" border>
+                <el-descriptions-item label="ID">{{ detail.id ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="名称">{{ detail.name ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="等级">{{ detail.grade ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="所需经验">{{ detail.exp_num ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="说明">{{ detail.explain ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="显示">
+                    <el-tag :type="Number(detail.is_show) === 1 ? 'success' : 'info'" size="small">
+                        {{ Number(detail.is_show) === 1 ? "显示" : "隐藏" }}
+                    </el-tag>
+                </el-descriptions-item>
+                <el-descriptions-item label="图标">
+                    <el-image
+                        v-if="detail.icon"
+                        class="detail-icon"
+                        :src="detail.icon"
+                        fit="cover"
+                        :preview-src-list="[detail.icon]"
+                        preview-teleported
+                    />
+                    <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-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" />
+                </el-form-item>
+                <el-form-item label="等级" prop="grade">
+                    <el-input-number v-model="form.grade" :min="0" :max="9999" :step="1" controls-position="right" class="w-full-num" />
+                </el-form-item>
+                <el-form-item label="所需经验" prop="exp_num">
+                    <el-input-number v-model="form.exp_num" :min="0" :max="999999999" :step="1" controls-position="right" class="w-full-num" />
+                </el-form-item>
+                <el-form-item label="图标" prop="icon">
+                    <div class="icon-field">
+                        <sc-upload v-model="form.icon" title="上传" :api-obj="$API.common.upload" />
+                        <el-input v-model="form.icon" placeholder="或填写图片地址" clearable />
+                    </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>
+            </el-form>
+            <template #footer>
+                <el-button @click="editDialog = false">取消</el-button>
+                <el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+            </template>
+        </el-dialog>
+    </el-main>
+</template>
+
+<script>
+function defaultLevelForm() {
+    return {
+        id: "",
+        name: "",
+        grade: 1,
+        exp_num: 0,
+        icon: "",
+        explain: ""
+    };
+}
+
+export default {
+    name: "UserLevelList",
+    data() {
+        return {
+            loading: false,
+            saving: false,
+            list: [],
+            detailDialog: false,
+            detail: {},
+            editDialog: false,
+            editTitle: "添加等级",
+            form: defaultLevelForm(),
+            rules: {
+                name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+                grade: [{ required: true, message: "请输入等级", trigger: "change" }],
+                exp_num: [{ required: true, message: "请输入所需经验", trigger: "change" }],
+                icon: [{ required: true, message: "请上传或填写图标地址", trigger: "blur" }],
+                explain: [{ required: true, message: "请输入说明", trigger: "blur" }]
+            }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        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();
+            const num = Number(val);
+            if (!Number.isFinite(num) || num <= 0) return String(val);
+            const sec = num > 1e12 ? Math.floor(num / 1000) : num;
+            return this.$TOOL.getTime(sec, "YYYY-MM-DD HH:mm:ss");
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.user.levelList.get();
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取用户等级列表失败");
+                    return;
+                }
+                const raw = res.data || {};
+                this.list = Array.isArray(raw) ? raw : raw.list || raw.data || [];
+            } finally {
+                this.loading = false;
+            }
+        },
+        async showDetail(row) {
+            const id = row?.id;
+            if (id == null) return;
+            const res = await this.$API.user.levelInfo.get(id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "获取详情失败");
+                return;
+            }
+            this.detail = res.data || {};
+            this.detailDialog = true;
+        },
+        openCreate() {
+            this.editTitle = "添加等级";
+            this.form = defaultLevelForm();
+            this.editDialog = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        openEdit(row) {
+            this.editTitle = "编辑等级";
+            this.form = {
+                id: row.id != null ? String(row.id) : "",
+                name: row.name || "",
+                grade: row.grade != null ? Number(row.grade) : 1,
+                exp_num: row.exp_num != null ? Number(row.exp_num) : 0,
+                icon: row.icon || "",
+                explain: row.explain || ""
+            };
+            this.editDialog = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        resetEditForm() {
+            this.form = defaultLevelForm();
+        },
+        submitForm() {
+            this.$refs.formRef.validate(async (valid) => {
+                if (!valid) return false;
+                this.saving = true;
+                try {
+                    const payload = {
+                        name: (this.form.name || "").trim(),
+                        grade: this.form.grade,
+                        exp_num: this.form.exp_num,
+                        icon: (this.form.icon || "").trim(),
+                        explain: (this.form.explain || "").trim()
+                    };
+                    if (this.form.id) payload.id = String(this.form.id);
+                    const res = await this.$API.user.levelSave.post(payload);
+                    if (res.code !== 1) {
+                        this.$message.error(res.msg || "保存失败");
+                        return false;
+                    }
+                    this.$message.success(res.msg || "保存成功");
+                    this.editDialog = false;
+                    this.getList();
+                } finally {
+                    this.saving = false;
+                }
+            });
+        },
+        async removeRow(row) {
+            const id = row?.id;
+            if (id == null) return;
+            try {
+                await this.$confirm("确认删除该等级吗?", "提示", {
+                    type: "warning",
+                    confirmButtonText: "删除",
+                    cancelButtonText: "取消"
+                });
+            } catch (e) {
+                return;
+            }
+            const res = await this.$API.user.levelDel.delete(id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "删除失败");
+                return;
+            }
+            this.$message.success(res.msg || "删除成功");
+            this.getList();
+        },
+        async changeShow(row, show) {
+            const id = row?.id;
+            if (id == null) {
+                this.$message.error("缺少等级 ID");
+                return;
+            }
+            const res = await this.$API.user.levelSetShow.get(id, show);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "显示状态更新失败");
+                this.getList();
+                return;
+            }
+            this.$message.success(res.msg || "已更新");
+            row.is_show = show;
+        }
+    }
+};
+</script>
+
+<style scoped>
+.card-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    letter-spacing: 0.02em;
+}
+.header-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-wrap: wrap;
+    gap: 12px;
+}
+.header-actions {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+.level-table :deep(.el-table__header th.el-table__cell) {
+    font-weight: 600;
+    color: var(--el-text-color-regular);
+}
+.level-icon {
+    width: 40px;
+    height: 40px;
+    border-radius: 6px;
+}
+.detail-icon {
+    width: 72px;
+    height: 72px;
+    border-radius: 8px;
+}
+.icon-field {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    width: 100%;
+}
+.w-full-num {
+    width: 100%;
+}
+.w-full-num :deep(.el-input__inner) {
+    text-align: left;
+}
+</style>