Explorar el Código

feat: 对接接口 用户 客服 剪辑师 财务

doi hace 3 meses
padre
commit
7584080cb2

+ 1 - 1
src/api/model/common.js

@@ -3,7 +3,7 @@ import http from "@/utils/request"
 
 export default {
 	upload: {
-		url: `${config.API_URL}/upload/data`,
+		url: `upload/data`,
 		name: "文件上传",
 		post: async function(data, config={}){
 			return await http.post(this.url, data, config);

+ 33 - 0
src/api/model/cutter.js

@@ -0,0 +1,33 @@
+import config from "@/config";
+import http from "@/utils/request";
+
+export default {
+    list: {
+        url: `${config.API_URL}/cutter/list`,
+        name: "剪辑师列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        }
+    },
+    applyList: {
+        url: `${config.API_URL}/cutter/apply/list`,
+        name: "剪辑师申请列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        }
+    },
+    applyAgreed: {
+        url: `${config.API_URL}/cutter/apply/agreed`,
+        name: "剪辑师同意申请",
+        post: async function (id, data = {}) {
+            return await http.post(`${this.url}/${id}`, data);
+        }
+    },
+    applyRefuse: {
+        url: `${config.API_URL}/cutter/apply/refuse`,
+        name: "剪辑师拒绝申请",
+        post: async function (id, data = {}) {
+            return await http.post(`${this.url}/${id}`, data);
+        }
+    }
+};

+ 23 - 0
src/api/model/finance.js

@@ -0,0 +1,23 @@
+import config from "@/config";
+import http from "@/utils/request";
+
+export default {
+    brokerage: {
+        list: {
+            url: `${config.API_URL}/finance/brokerage/list`,
+            name: "佣金明细",
+            get: async function (params = {}) {
+                return await http.get(this.url, params);
+            }
+        }
+    },
+    extract: {
+        list: {
+            url: `${config.API_URL}/finance/extract/list`,
+            name: "用户提现",
+            get: async function (params = {}) {
+                return await http.get(this.url, params);
+            }
+        }
+    }
+};

+ 33 - 0
src/api/model/kefu.js

@@ -0,0 +1,33 @@
+import config from "@/config";
+import http from "@/utils/request";
+
+export default {
+    list: {
+        url: `${config.API_URL}/kefu/list`,
+        name: "客服列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        }
+    },
+    save: {
+        url: `${config.API_URL}/kefu/save`,
+        name: "客服添加/保存",
+        post: async function (data = {}) {
+            return await http.post(this.url, data);
+        }
+    },
+    del: {
+        url: `${config.API_URL}/kefu/del`,
+        name: "客服删除",
+        delete: async function (id) {
+            return await http.delete(`${this.url}/${id}`);
+        }
+    },
+    setStatus: {
+        url: `${config.API_URL}/kefu/set_status`,
+        name: "客服状态设置",
+        get: async function (id, status) {
+            return await http.get(`${this.url}/${id}/${status}`);
+        }
+    }
+};

+ 5 - 5
src/api/model/task.js

@@ -3,35 +3,35 @@ import http from "@/utils/request";
 
 export default {
     list: {
-        url: `${config.API_URL}/task/list`,
+        url: `${config.API_URL}/cutter/task/list`,
         name: "任务列表",
         get: async function(params = {}){
             return await http.get(this.url, params);
         }
     },
     save: {
-        url: `${config.API_URL}/task/save`,
+        url: `${config.API_URL}/cutter/task/save`,
         name: "任务新增/编辑",
         post: async function(data = {}){
             return await http.post(this.url, data);
         }
     },
     del: {
-        url: `${config.API_URL}/task/del`,
+        url: `${config.API_URL}/cutter/task/del`,
         name: "任务删除",
         delete: async function(id){
             return await http.delete(`${this.url}/${id}`);
         }
     },
     setStatus: {
-        url: `${config.API_URL}/task/set_status`,
+        url: `${config.API_URL}/cutter/task/set_status`,
         name: "任务状态设置",
         get: async function(id, status){
             return await http.get(`${this.url}/${id}/${status}`);
         }
     },
     info: {
-        url: `${config.API_URL}/task/info`,
+        url: `${config.API_URL}/cutter/task/info`,
         name: "任务详情",
         get: async function(id){
             return await http.get(`${this.url}/${id}`);

+ 11 - 3
src/api/model/user.js

@@ -2,13 +2,21 @@ import config from "@/config";
 import http from "@/utils/request";
 
 export default {
+    /** 小程序用户列表 adminapi/users/list */
     list: {
-        url: `${config.API_URL}/user/list`,
-        name: "-",
-        get: async function (params) {
+        url: `${config.API_URL}/users/list`,
+        name: "用户列表",
+        get: async function (params = {}) {
             return await http.get(this.url, params);
         },
     },
+    setStatus: {
+        url: `${config.API_URL}/users/set_status`,
+        name: "用户状态",
+        get: async function (id, status) {
+            return await http.get(`${this.url}/${id}/${status}`);
+        },
+    },
     pwd: {
         url: `${config.API_URL}/user/passwd`,
         name: "-",

+ 209 - 0
src/views/cutter/apply/index.vue

@@ -0,0 +1,209 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <span>剪辑师申请审核</span>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column prop="real_name" label="真实姓名" min-width="100" show-overflow-tooltip />
+                <el-table-column prop="card" label="身份证" min-width="160" show-overflow-tooltip />
+                <el-table-column prop="phone" label="手机号" width="130" />
+                <el-table-column prop="wechat" label="微信号" min-width="120" show-overflow-tooltip />
+                <el-table-column label="作品链接" min-width="160">
+                    <template #default="{ row }">
+                        <el-link v-if="row.works" :href="row.works" target="_blank" type="primary">打开</el-link>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="100">
+                    <template #default="{ row }">
+                        <el-tag size="small" :type="applyStatusType(row.status)">
+                            {{ applyStatusText(row.status) }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="remark" label="审核备注" min-width="120" show-overflow-tooltip />
+                <el-table-column label="申请时间" min-width="170">
+                    <template #default="{ row }">
+                        {{ formatDispTime(row.created_at) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="更新时间" min-width="170">
+                    <template #default="{ row }">
+                        {{ formatDispTime(row.updated_at) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="200" fixed="right">
+                    <template #default="{ row }">
+                        <el-button
+                            v-if="isPending(row)"
+                            type="success"
+                            size="small"
+                            @click="openRemark(row, 'agree')"
+                        >
+                            同意
+                        </el-button>
+                        <el-button
+                            v-if="isPending(row)"
+                            type="danger"
+                            size="small"
+                            plain
+                            @click="openRemark(row, 'refuse')"
+                        >
+                            拒绝
+                        </el-button>
+                        <span v-if="!isPending(row)" class="muted">已处理</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+
+        <el-dialog v-model="remarkDialog" :title="remarkTitle" width="420px" destroy-on-close @closed="resetRemark">
+            <el-input v-model="remark" type="textarea" :rows="4" placeholder="请输入备注(必填)" maxlength="200" show-word-limit />
+            <template #footer>
+                <el-button @click="remarkDialog = false">取消</el-button>
+                <el-button type="primary" :loading="submitting" @click="submitRemark">确定</el-button>
+            </template>
+        </el-dialog>
+    </el-main>
+</template>
+
+<script>
+export default {
+    name: "CutterApply",
+    data() {
+        return {
+            loading: false,
+            submitting: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 },
+            remarkDialog: false,
+            remarkTitle: "",
+            remark: "",
+            remarkAction: "",
+            currentRow: null
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        /** 后端为日期字符串 "2026-04-29 12:17:09" 或时间戳 */
+        formatDispTime(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");
+        },
+        applyStatusText(status) {
+            const n = Number(status);
+            const map = { 0: "待审核", 1: "已通过", 2: "已拒绝" };
+            return map[n] !== undefined ? map[n] : status === undefined || status === null ? "-" : String(status);
+        },
+        applyStatusType(status) {
+            const n = Number(status);
+            if (n === 1) return "success";
+            if (n === 2) return "danger";
+            return "warning";
+        },
+        isPending(row) {
+            const s = row.status;
+            return s === undefined || s === null || Number(s) === 0;
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.cutter.applyList.get(this.query);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取申请列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.list || data.data || [];
+                this.total = Number(data.count ?? data.total ?? this.list.length ?? 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        },
+        openRemark(row, action) {
+            this.currentRow = row;
+            this.remarkAction = action;
+            this.remarkTitle = action === "agree" ? "同意申请" : "拒绝申请";
+            this.remark = "";
+            this.remarkDialog = true;
+        },
+        resetRemark() {
+            this.currentRow = null;
+            this.remarkAction = "";
+            this.remark = "";
+        },
+        async submitRemark() {
+            const text = (this.remark || "").trim();
+            if (!text) {
+                this.$message.warning("请填写备注");
+                return;
+            }
+            if (!this.currentRow) return;
+            this.submitting = true;
+            try {
+                const id = this.currentRow.id;
+                const body = { remark: text };
+                const res =
+                    this.remarkAction === "agree"
+                        ? await this.$API.cutter.applyAgreed.post(id, body)
+                        : await this.$API.cutter.applyRefuse.post(id, body);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "操作失败");
+                    return;
+                }
+                this.$message.success(res.msg || "操作成功");
+                this.remarkDialog = false;
+                this.getList();
+            } finally {
+                this.submitting = false;
+            }
+        }
+    }
+};
+</script>
+
+<style scoped>
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+.muted {
+    color: var(--el-text-color-secondary);
+    font-size: 12px;
+}
+</style>

+ 106 - 0
src/views/cutter/list/index.vue

@@ -0,0 +1,106 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <span>剪辑师列表</span>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column prop="real_name" label="姓名" min-width="100" show-overflow-tooltip />
+                <el-table-column prop="nickname" label="昵称" min-width="100" show-overflow-tooltip />
+                <el-table-column prop="phone" label="手机号" width="130" />
+                <el-table-column prop="wechat" label="微信号" min-width="120" show-overflow-tooltip />
+                <el-table-column label="状态" width="100">
+                    <template #default="{ row }">
+                        <el-tag v-if="row.status !== undefined && row.status !== null" size="small">
+                            {{ formatStatus(row.status) }}
+                        </el-tag>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="add_time" label="时间" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatCellTime(row.add_time || row.create_time) }}
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+    </el-main>
+</template>
+
+<script>
+export default {
+    name: "CutterList",
+    data() {
+        return {
+            loading: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        formatStatus(v) {
+            const n = Number(v);
+            const map = { 0: "禁用", 1: "正常" };
+            return map[n] !== undefined ? map[n] : String(v);
+        },
+        formatCellTime(val) {
+            if (val === undefined || val === null || val === "") return "-";
+            const num = Number(val);
+            if (!num) 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.cutter.list.get(this.query);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取剪辑师列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.data || data.list || [];
+                this.total = Number(data.count || data.total || this.list.length || 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 107 - 0
src/views/finance/brokerage/list/index.vue

@@ -0,0 +1,107 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <span>佣金明细</span>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column label="用户" min-width="100" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.uid || row.user_id || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="标题/关联" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.title || row.mark || row.link_id || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="金额" width="120">
+                    <template #default="{ row }">
+                        {{ row.number ?? row.price ?? row.brokerage ?? row.amount ?? "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="时间" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatTime(row.add_time || row.create_time || row.created_at) }}
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+    </el-main>
+</template>
+
+<script>
+export default {
+    name: "BrokerageList",
+    data() {
+        return {
+            loading: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        formatTime(val) {
+            if (val == null || val === "") return "-";
+            const n = Number(val);
+            if (!Number.isNaN(n) && String(val).length <= 12) {
+                const ms = n < 1e12 ? n * 1000 : n;
+                const d = new Date(ms);
+                if (!Number.isNaN(d.getTime())) return d.toLocaleString();
+            }
+            return String(val);
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.finance.brokerage.list.get(this.query);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取佣金明细失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.data || data.list || [];
+                this.total = Number(data.count || data.total || this.list.length || 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 124 - 0
src/views/finance/extract/list/index.vue

@@ -0,0 +1,124 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <span>用户提现</span>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column label="用户" min-width="100" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.uid || row.user_id || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="提现金额" width="120">
+                    <template #default="{ row }">
+                        {{ row.extract_price ?? row.price ?? row.amount ?? "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="120">
+                    <template #default="{ row }">
+                        {{ formatStatus(row.status) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="备注" min-width="140" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.mark || row.remark || row.fail_msg || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="申请时间" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatTime(row.add_time || row.create_time || row.created_at) }}
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+    </el-main>
+</template>
+
+<script>
+const STATUS_MAP = {
+    "-1": "已拒绝",
+    0: "审核中",
+    1: "已通过",
+    2: "已打款"
+};
+
+export default {
+    name: "ExtractList",
+    data() {
+        return {
+            loading: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        formatStatus(status) {
+            if (status == null || status === "") return "-";
+            const key = String(status);
+            return STATUS_MAP[key] ?? STATUS_MAP[status] ?? key;
+        },
+        formatTime(val) {
+            if (val == null || val === "") return "-";
+            const n = Number(val);
+            if (!Number.isNaN(n) && String(val).length <= 12) {
+                const ms = n < 1e12 ? n * 1000 : n;
+                const d = new Date(ms);
+                if (!Number.isNaN(d.getTime())) return d.toLocaleString();
+            }
+            return String(val);
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.finance.extract.list.get(this.query);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取提现列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.data || data.list || [];
+                this.total = Number(data.count || data.total || this.list.length || 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 241 - 0
src/views/kefu/list/index.vue

@@ -0,0 +1,241 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <div class="header-row">
+                    <span>客服管理</span>
+                    <el-button type="primary" @click="openCreate">添加客服</el-button>
+                </div>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="id" label="ID" width="80" />
+                <el-table-column prop="name" label="姓名" min-width="100" show-overflow-tooltip />
+                <el-table-column prop="wechat" label="微信号" min-width="120" show-overflow-tooltip />
+                <el-table-column prop="phone" label="手机号" width="130" />
+                <el-table-column label="二维码" width="100">
+                    <template #default="{ row }">
+                        <el-image
+                            v-if="row.qrcode"
+                            :src="row.qrcode"
+                            :preview-src-list="[row.qrcode]"
+                            fit="cover"
+                            style="width: 48px; height: 48px; border-radius: 4px;"
+                            preview-teleported
+                        />
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="120">
+                    <template #default="{ row }">
+                        <el-switch
+                            :model-value="Number(row.status)"
+                            :active-value="1"
+                            :inactive-value="0"
+                            inline-prompt
+                            active-text="启用"
+                            inactive-text="停用"
+                            active-color="#13ce66"
+                            inactive-color="#ff4949"
+                            @change="(val) => changeStatus(row, val)"
+                        />
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="160" fixed="right">
+                    <template #default="{ row }">
+                        <el-button size="small" @click="openEdit(row)">编辑</el-button>
+                        <el-button size="small" type="danger" @click="removeRow(row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+
+        <el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" destroy-on-close>
+            <el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
+                <el-form-item label="姓名" prop="name">
+                    <el-input v-model="form.name" clearable maxlength="40" />
+                </el-form-item>
+                <el-form-item label="微信号" prop="wechat">
+                    <el-input v-model="form.wechat" clearable maxlength="60" />
+                </el-form-item>
+                <el-form-item label="手机号" prop="phone">
+                    <el-input v-model="form.phone" clearable maxlength="20" />
+                </el-form-item>
+                <el-form-item label="二维码" prop="qrcode">
+                    <div class="qrcode-row">
+                        <sc-upload v-model="form.qrcode" title="上传" :api-obj="$API.common.upload" />
+                        <el-input v-model="form.qrcode" placeholder="或填写图片地址" clearable class="qrcode-input" />
+                    </div>
+                </el-form-item>
+                <el-form-item label="备注" prop="aa">
+                    <el-input v-model="form.aa" placeholder="与接口字段 aa 对应" maxlength="120" />
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+            </template>
+        </el-dialog>
+    </el-main>
+</template>
+
+<script>
+export default {
+    name: "KefuList",
+    data() {
+        return {
+            loading: false,
+            saving: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 },
+            dialogVisible: false,
+            dialogTitle: "添加客服",
+            form: this.getDefaultForm(),
+            rules: {
+                name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
+                wechat: [{ required: true, message: "请输入微信号", trigger: "blur" }],
+                phone: [{ required: true, message: "请输入手机号", trigger: "blur" }],
+                qrcode: [{ required: true, message: "请上传或填写二维码", trigger: "blur" }]
+            }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        getDefaultForm() {
+            return { id: "", name: "", wechat: "", phone: "", qrcode: "", aa: "-" };
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.kefu.list.get(this.query);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取客服列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.data || data.list || [];
+                this.total = Number(data.count || data.total || this.list.length || 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        },
+        openCreate() {
+            this.dialogTitle = "添加客服";
+            this.form = this.getDefaultForm();
+            this.dialogVisible = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        openEdit(row) {
+            this.dialogTitle = "编辑客服";
+            this.form = {
+                id: row.id,
+                name: row.name || "",
+                wechat: row.wechat || "",
+                phone: row.phone || "",
+                qrcode: row.qrcode || "",
+                aa: row.aa != null && row.aa !== "" ? String(row.aa) : "-"
+            };
+            this.dialogVisible = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        submitForm() {
+            this.$refs.formRef.validate(async (valid) => {
+                if (!valid) return false;
+                this.saving = true;
+                try {
+                    const payload = {
+                        name: this.form.name,
+                        wechat: this.form.wechat,
+                        phone: this.form.phone,
+                        qrcode: this.form.qrcode,
+                        aa: this.form.aa || "-"
+                    };
+                    if (this.form.id) payload.id = String(this.form.id);
+                    const res = await this.$API.kefu.save.post(payload);
+                    if (res.code !== 1) {
+                        this.$message.error(res.msg || "保存失败");
+                        return false;
+                    }
+                    this.$message.success(res.msg || "保存成功");
+                    this.dialogVisible = false;
+                    this.getList();
+                } finally {
+                    this.saving = false;
+                }
+            });
+        },
+        async removeRow(row) {
+            const confirm = await this.$confirm("确认删除该客服吗?", "提示", {
+                type: "warning",
+                confirmButtonText: "删除",
+                confirmButtonClass: "el-button--danger"
+            }).catch(() => {});
+            if (confirm !== "confirm") return;
+            const res = await this.$API.kefu.del.delete(row.id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "删除失败");
+                return;
+            }
+            this.$message.success(res.msg || "删除成功");
+            this.getList();
+        },
+        async changeStatus(row, status) {
+            const res = await this.$API.kefu.setStatus.get(row.id, status);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "状态更新失败");
+                this.getList();
+                return;
+            }
+            this.$message.success(res.msg || "状态更新成功");
+            row.status = status;
+        }
+    }
+};
+</script>
+
+<style scoped>
+.header-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+.qrcode-row {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    width: 100%;
+}
+.qrcode-input {
+    max-width: 360px;
+}
+</style>

+ 1 - 1
src/views/task/manage/list/index.vue

@@ -8,7 +8,7 @@
                 </div>
             </template>
 
-            <el-table v-loading="loading" :data="list" border>
+            <el-table v-loading="loading" :data="list">
                 <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 />

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

@@ -0,0 +1,129 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <span>用户列表</span>
+            </template>
+
+            <el-table v-loading="loading" :data="list">
+                <el-table-column prop="uid" label="UID" width="72" />
+                <el-table-column prop="nickname" label="昵称" min-width="140" show-overflow-tooltip />
+                <!-- <el-table-column prop="openid" label="OpenID" min-width="200" show-overflow-tooltip /> -->
+                <el-table-column label="手机号" width="120">
+                    <template #default="{ row }">
+                        {{ row.phone ?? "-" }}
+                    </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 }">
+                        <el-tag :type="row.is_cutter ? 'success' : 'info'" size="small">
+                            {{ row.is_cutter ? "是" : "否" }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="120">
+                    <template #default="{ row }">
+                        <el-switch
+                            :model-value="Number(row.status)"
+                            :active-value="1"
+                            :inactive-value="0"
+                            inline-prompt
+                            active-text="正常"
+                            inactive-text="禁用"
+                            active-color="#13ce66"
+                            inactive-color="#ff4949"
+                            @change="(val) => changeStatus(row, val)"
+                        />
+                    </template>
+                </el-table-column>
+                <el-table-column prop="login_ip" label="登录IP" width="130" show-overflow-tooltip />
+                <el-table-column prop="login_at" label="最后登录" min-width="160" show-overflow-tooltip />
+                <el-table-column prop="created_at" label="注册时间" min-width="160" show-overflow-tooltip />
+                <el-table-column prop="updated_at" label="更新时间" min-width="160" show-overflow-tooltip />
+            </el-table>
+
+            <div class="pager">
+                <el-pagination
+                    background
+                    layout="total, prev, pager, next, sizes"
+                    :total="total"
+                    :current-page="query.page"
+                    :page-size="query.limit"
+                    :page-sizes="[5, 10, 20, 50]"
+                    @current-change="handlePageChange"
+                    @size-change="handleSizeChange"
+                />
+            </div>
+        </el-card>
+    </el-main>
+</template>
+
+<script>
+export default {
+    name: "UserList",
+    data() {
+        return {
+            loading: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        userPk(row) {
+            return row.uid ?? row.id;
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.user.list.get(this.query);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取用户列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                this.list = data.list || data.data || [];
+                this.total = Number(data.count ?? data.total ?? this.list.length ?? 0);
+            } finally {
+                this.loading = false;
+            }
+        },
+        handlePageChange(page) {
+            this.query.page = page;
+            this.getList();
+        },
+        handleSizeChange(limit) {
+            this.query.limit = limit;
+            this.query.page = 1;
+            this.getList();
+        },
+        async changeStatus(row, status) {
+            const id = this.userPk(row);
+            if (id == null) {
+                this.$message.error("缺少用户标识");
+                return;
+            }
+            const res = await this.$API.user.setStatus.get(id, status);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "状态更新失败");
+                this.getList();
+                return;
+            }
+            this.$message.success(res.msg || "状态更新成功");
+            row.status = status;
+        }
+    }
+};
+</script>
+
+<style scoped>
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>