2 Commity 95c11281ba ... f81365fcb9

Autor SHA1 Wiadomość Data
  doi f81365fcb9 feat: 对接接口 2 miesięcy temu
  doi 7584080cb2 feat: 对接接口 用户 客服 剪辑师 财务 3 miesięcy temu

+ 1 - 1
.env.development

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

+ 1 - 1
.env.production

@@ -5,4 +5,4 @@ NODE_ENV = production
 VUE_APP_TITLE = 蓝微帧工场
 
 # 接口地址
-VUE_APP_API_BASEURL = /api
+VUE_APP_API_BASEURL = https://cut.bluevchat.cn/adminapi

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

@@ -0,0 +1,68 @@
+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);
+        }
+    },
+    orderList: {
+        url: `${config.API_URL}/cutter/order/list`,
+        name: "剪辑订单列表",
+        get: async function (params = {}) {
+            return await http.get(this.url, params);
+        }
+    },
+    orderInfo: {
+        url: `${config.API_URL}/cutter/order/info`,
+        name: "剪辑订单详情",
+        get: async function (id) {
+            return await http.get(`${this.url}/${id}`);
+        }
+    },
+    orderAffirm: {
+        url: `${config.API_URL}/cutter/order/affirm`,
+        name: "剪辑订单通过",
+        post: async function (id, data = {}) {
+            return await http.post(`${this.url}/${id}`, data);
+        }
+    },
+    orderReject: {
+        url: `${config.API_URL}/cutter/order/reject`,
+        name: "剪辑订单拒绝",
+        post: async function (id, data = {}) {
+            return await http.post(`${this.url}/${id}`, data);
+        }
+    },
+    orderSettle: {
+        url: `${config.API_URL}/cutter/order/settle`,
+        name: "订单结算",
+        post: async function (id, data = {}) {
+            return await http.post(`${this.url}/${id}`, data);
+        }
+    }
+};

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

@@ -0,0 +1,37 @@
+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);
+            }
+        },
+        agree: {
+            url: `${config.API_URL}/finance/extract/adopt`,
+            name: "用户提现同意",
+            post: async function (id, data = {}) {
+                return await http.post(`${this.url}/${id}`, data);
+            }
+        },
+        refuse: {
+            url: `${config.API_URL}/finance/extract/refuse`,
+            name: "用户提现拒绝",
+            post: async function (id, data = {}) {
+                return await http.post(`${this.url}/${id}`, data);
+            }
+        }
+    }
+};

+ 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: "-",

+ 12 - 0
src/config/index.js

@@ -1,3 +1,12 @@
+/** 上传接口根地址(不走 /adminapi,由域名根路径提供 /upload/...) */
+function resolveUploadApiUrl() {
+	if (process.env.NODE_ENV === 'development' && process.env.VUE_APP_PROXY === 'true') {
+		return ''
+	}
+	const base = (process.env.VUE_APP_API_BASEURL || '').replace(/\/+$/, '')
+	return base.replace(/\/adminapi$/i, '')
+}
+
 const DEFAULT_CONFIG = {
 	//标题
 	APP_NAME: process.env.VUE_APP_TITLE,
@@ -14,6 +23,9 @@ const DEFAULT_CONFIG = {
 	//接口地址
 	API_URL: process.env.NODE_ENV === 'development' && process.env.VUE_APP_PROXY === 'true' ? "/adminapi" : process.env.VUE_APP_API_BASEURL,
 
+	//上传接口地址(不含 adminapi)
+	UPLOAD_API_URL: resolveUploadApiUrl(),
+
 	//请求超时
 	TIMEOUT: 10000,
 

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

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

@@ -0,0 +1,182 @@
+<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 label="头像" width="72" align="center">
+                    <template #default="{ row }">
+                        <el-avatar v-if="row.avatar" :size="36" :src="row.avatar" />
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
+                <el-table-column label="真实姓名" min-width="100" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ applyField(row, "real_name") }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="手机号" width="130">
+                    <template #default="{ row }">
+                        {{ displayPhone(row) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="微信号" min-width="120" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ applyField(row, "wechat") }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="作品链接" min-width="100">
+                    <template #default="{ row }">
+                        <el-link v-if="row.cutter_apply && row.cutter_apply.works" :href="row.cutter_apply.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 v-if="row.cutter_apply" size="small" :type="applyStatusType(row.cutter_apply.status)">
+                            {{ applyStatusText(row.cutter_apply.status) }}
+                        </el-tag>
+                        <span v-else class="muted">无资料</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="账号状态" width="100">
+                    <template #default="{ row }">
+                        <el-tag v-if="row.status !== undefined && row.status !== null" size="small" :type="Number(row.status) === 1 ? 'success' : 'danger'">
+                            {{ formatAccountStatus(row.status) }}
+                        </el-tag>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="brokerage_price" label="佣金余额" width="100" align="right" />
+                <el-table-column prop="login_ip" label="登录IP" width="130" show-overflow-tooltip />
+                <el-table-column label="最后登录" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatDispTime(row.login_at) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="注册时间" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatDispTime(row.created_at) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="更新时间" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatDispTime(row.updated_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: "CutterList",
+    data() {
+        return {
+            loading: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        applyField(row, key) {
+            const a = row.cutter_apply;
+            if (!a || a[key] === undefined || a[key] === null || a[key] === "") return "-";
+            return a[key];
+        },
+        displayPhone(row) {
+            const fromApply = row.cutter_apply && row.cutter_apply.phone;
+            const v = fromApply || row.phone;
+            if (v === undefined || v === null || v === "") return "-";
+            return v;
+        },
+        formatAccountStatus(v) {
+            const n = Number(v);
+            const map = { 0: "禁用", 1: "正常" };
+            return map[n] !== undefined ? map[n] : String(v);
+        },
+        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";
+        },
+        /** 后端为日期字符串 "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");
+        },
+        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.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();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+.muted {
+    color: var(--el-text-color-secondary);
+    font-size: 12px;
+}
+</style>

+ 322 - 0
src/views/cutter/order/index.vue

@@ -0,0 +1,322 @@
+<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="90" />
+                <el-table-column label="任务ID" width="90">
+                    <template #default="{ row }">
+                        {{ row.task_id ?? row.tid ?? "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="用户" min-width="140" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.user?.nickname || row.uid || row.user_id || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="任务标题" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.task?.title || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="任务金额" width="100">
+                    <template #default="{ row }">
+                        {{ row.task?.price ?? "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="审核地址" min-width="180" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        <el-link v-if="row.audit_url" :href="row.audit_url" type="primary" target="_blank">
+                            打开
+                        </el-link>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="成品地址" min-width="180" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        <el-link v-if="row.finish_url" :href="row.finish_url" type="primary" target="_blank">
+                            打开
+                        </el-link>
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="120">
+                    <template #default="{ row }">
+                        <el-tag :type="statusType(row.status)" size="small">
+                            {{ statusText(row.status) }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column label="备注" min-width="140" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ row.remark || row.reject_remark || "-" }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="创建时间" min-width="170" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatTime(row.created_at || row.add_time || row.create_time) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="更新时间" min-width="170" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ formatTime(row.updated_at || row.update_time) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="190" fixed="right">
+                    <template #default="{ row }">
+                        <el-button type="primary" link @click="showDetail(row)">详情</el-button>
+                        <el-button v-if="canAudit(row)" type="success" link @click="affirmOrder(row)">通过</el-button>
+                        <el-button v-if="canAudit(row)" type="danger" link @click="openRemark(row, 'reject')">拒绝</el-button>
+                        <el-button v-if="canSettle(row)" type="warning" link @click="settleOrder(row)">结算</el-button>
+                        <!-- <span v-if="!canAudit(row) && !canSettle(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="detailDialog" title="订单详情" width="680px" destroy-on-close>
+            <el-descriptions :column="2" border>
+                <el-descriptions-item label="订单ID">{{ detail.id ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="任务ID">{{ detail.task_id ?? detail.tid ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="用户">{{ detail.user?.nickname || detail.uid || detail.user_id || "-" }}</el-descriptions-item>
+                <el-descriptions-item label="任务标题">{{ detail.task?.title || "-" }}</el-descriptions-item>
+                <el-descriptions-item label="状态">{{ statusText(detail.status) }}</el-descriptions-item>
+                <el-descriptions-item label="创建时间">{{ formatTime(detail.created_at || detail.add_time || detail.create_time) }}</el-descriptions-item>
+                <el-descriptions-item label="更新时间">{{ formatTime(detail.updated_at || detail.update_time) }}</el-descriptions-item>
+                <el-descriptions-item label="备注">{{ detail.remark || detail.reject_remark || "-" }}</el-descriptions-item>
+                <el-descriptions-item label="审核地址" :span="2">
+                    <el-link v-if="detail.audit_url" :href="detail.audit_url" type="primary" target="_blank">
+                        {{ detail.audit_url }}
+                    </el-link>
+                    <span v-else>-</span>
+                </el-descriptions-item>
+                <el-descriptions-item label="成品地址" :span="2">
+                    <el-link v-if="detail.finish_url" :href="detail.finish_url" type="primary" target="_blank">
+                        {{ detail.finish_url }}
+                    </el-link>
+                    <span v-else>-</span>
+                </el-descriptions-item>
+            </el-descriptions>
+        </el-dialog>
+
+        <el-dialog v-model="remarkDialog" :title="remarkTitle" width="420px" destroy-on-close @closed="resetRemark">
+            <el-input v-model="remark" type="textarea" :rows="4" :placeholder="remarkPlaceholder" 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>
+const STATUS_MAP = {
+    "-1": "未通过",
+    0: "待制作",
+    1: "待验收",
+    2: "待交付",
+    3: "待结算",
+    4: "已结算"
+};
+
+export default {
+    name: "CutterOrderList",
+    data() {
+        return {
+            loading: false,
+            submitting: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10 },
+            detailDialog: false,
+            detail: {},
+            remarkDialog: false,
+            remarkTitle: "",
+            remarkPlaceholder: "",
+            remark: "",
+            remarkAction: "",
+            currentRow: null
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        statusText(status) {
+            if (status == null || status === "") return "-";
+            const key = String(status);
+            return STATUS_MAP[key] ?? STATUS_MAP[status] ?? key;
+        },
+        statusType(status) {
+            const n = Number(status);
+            if (n === -1) return "danger";
+            if (n === 1 || n === 4) return "success";
+            return "warning";
+        },
+        canAudit(row) {
+            return Number(row.status) === 1;
+        },
+        canSettle(row) {
+            return Number(row.status) === 3;
+        },
+        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.cutter.orderList.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 showDetail(row) {
+            const id = row.id;
+            if (id == null) return;
+            const res = await this.$API.cutter.orderInfo.get(id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "获取详情失败");
+                return;
+            }
+            this.detail = res.data || {};
+            this.detailDialog = true;
+        },
+        async affirmOrder(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.orderAffirm.post(id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "操作失败");
+                return;
+            }
+            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 = "请输入拒绝理由(必填)";
+            this.remarkDialog = true;
+        },
+        resetRemark() {
+            this.currentRow = null;
+            this.remarkAction = "";
+            this.remark = "";
+            this.remarkTitle = "";
+            this.remarkPlaceholder = "";
+        },
+        async submitRemark() {
+            if (!this.currentRow) return;
+            const id = this.currentRow.id;
+            if (id == null) {
+                this.$message.error("缺少订单ID");
+                return;
+            }
+            const remark = (this.remark || "").trim();
+            if (!remark) {
+                this.$message.warning("请填写拒绝理由");
+                return;
+            }
+            this.submitting = true;
+            try {
+                const 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.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>

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

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

@@ -0,0 +1,217 @@
+<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 }">
+                        <el-tag :type="statusType(row.status)" size="small">
+                            {{ formatStatus(row.status) }}
+                        </el-tag>
+                    </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-column label="操作" width="220" 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="remarkPlaceholder"
+                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>
+const STATUS_MAP = {
+    "-1": "已拒绝",
+    0: "审核中",
+    1: "已通过",
+    2: "已打款"
+};
+
+export default {
+    name: "ExtractList",
+    data() {
+        return {
+            loading: false,
+            submitting: false,
+            list: [],
+            total: 0,
+            query: { page: 1, limit: 10, status: 0 },
+            remarkDialog: false,
+            remarkTitle: "",
+            remarkPlaceholder: "",
+            remark: "",
+            remarkAction: "",
+            currentRow: null
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        formatStatus(status) {
+            if (status == null || status === "") return "-";
+            const key = String(status);
+            return STATUS_MAP[key] ?? STATUS_MAP[status] ?? key;
+        },
+        statusType(status) {
+            const n = Number(status);
+            if (n === -1) return "danger";
+            if (n === 1 || n === 2) return "success";
+            return "warning";
+        },
+        isPending(row) {
+            return Number(row.status) === 0;
+        },
+        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();
+        },
+        openRemark(row, action) {
+            this.currentRow = row;
+            this.remarkAction = action;
+            this.remark = "";
+            this.remarkTitle = action === "agree" ? "同意提现" : "拒绝提现";
+            this.remarkPlaceholder = action === "agree" ? "请输入通过备注(选填)" : "请输入拒绝理由(必填)";
+            this.remarkDialog = true;
+        },
+        resetRemark() {
+            this.currentRow = null;
+            this.remarkAction = "";
+            this.remark = "";
+            this.remarkTitle = "";
+            this.remarkPlaceholder = "";
+        },
+        async submitRemark() {
+            if (!this.currentRow) return;
+            const id = this.currentRow.id;
+            if (id == null) {
+                this.$message.error("缺少提现记录ID");
+                return;
+            }
+            const failMsg = (this.remark || "").trim();
+            if (this.remarkAction === "refuse" && !failMsg) {
+                this.$message.warning("请填写拒绝理由");
+                return;
+            }
+            this.submitting = true;
+            try {
+                const payload = { fail_msg: failMsg || "通过" };
+                const res =
+                    this.remarkAction === "agree"
+                        ? await this.$API.finance.extract.agree.post(id, payload)
+                        : await this.$API.finance.extract.refuse.post(id, payload);
+                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>

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

+ 37 - 1
src/views/manage/setting/basic/index.vue

@@ -32,6 +32,10 @@
                                 <sc-editor v-model="sys.agreements" placeholder="请输入" :height="400"></sc-editor>
                                 <div class="el-form-item-msg"></div>
                             </el-form-item>
+                            <el-form-item label="规则协议" prop="cutter_rule" class="label-item">
+                                <sc-editor v-model="sys.cutter_rule" placeholder="请输入" :height="400"></sc-editor>
+                                <div class="el-form-item-msg"></div>
+                            </el-form-item>
                         </el-form>
                     </el-tab-pane>
                     <el-tab-pane label="微信设置" name="wechat" v-loading="isLoading">
@@ -98,6 +102,22 @@
                             </el-row>
                         </el-form>
                     </el-tab-pane>
+                    <el-tab-pane label="提现设置" name="extract" v-loading="isLoading">
+                        <el-form :model="sys" :rules="rulesExtract" ref="form" label-position="top" @keyup.enter="saveForm" v-if="activeName=='extract'">
+                            <el-form-item label="提现最低金额(元)" prop="extract_min_price" class="label-item">
+                                <el-input-number class="input-number-text-left" v-model="sys.extract_min_price" :min="0" :precision="2" :controls="false" placeholder="请输入提现最低金额(元)" style="width: 100%;" />
+                                <div class="el-form-item-msg">单笔提现允许的最低金额</div>
+                            </el-form-item>
+                            <el-form-item label="提现银行卡" prop="extract_bank" class="label-item">
+                                <el-input v-model="sys.extract_bank" type="textarea" :rows="4" placeholder="请输入提现银行卡" clearable />
+                                <div class="el-form-item-msg"></div>
+                            </el-form-item>
+                            <el-form-item label="手续费" prop="extract_charge" class="label-item">
+                                <el-input v-model="sys.extract_charge" placeholder="请输入手续费" clearable />
+                                <div class="el-form-item-msg"></div>
+                            </el-form-item>
+                        </el-form>
+                    </el-tab-pane>
                     
                 </el-tabs>
             </el-card>
@@ -163,6 +183,14 @@ export default {
                     { required: true, message: '请输入' }
                 ]
             },
+            rulesExtract: {
+                extract_min_price: [
+                    { required: true, message: '请输入提现最低金额', trigger: 'blur' }
+                ],
+                extract_charge: [
+                    { required: true, message: '请输入手续费', trigger: 'blur' }
+                ]
+            },
             isLoading:false
         }
     },
@@ -211,4 +239,12 @@ export default {
         },
     }
 }
-</script>
+</script>
+<style scoped>
+.input-number-text-left :deep(.el-input__wrapper) {
+    justify-content: flex-start;
+}
+.input-number-text-left :deep(.el-input__inner) {
+    text-align: left;
+}
+</style>

+ 103 - 22
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 />
@@ -27,7 +27,7 @@
                         <span v-else>-</span>
                     </template>
                 </el-table-column>
-                <el-table-column prop="service_id" label="客服ID" width="100" />
+                <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 }">
@@ -79,14 +79,35 @@
                 <el-form-item label="说明" prop="explain">
                     <el-input v-model="form.explain" type="textarea" :rows="4" />
                 </el-form-item>
-                <el-form-item label="素材" prop="materialText">
-                    <el-input v-model="form.materialText" placeholder="多个素材ID用英文逗号分隔,如 11,12,13" />
+                <el-form-item label="素材" prop="materialFiles">
+                    <scUploadFile
+                        v-model="form.materialFiles"
+                        :apiObj="$API.common.uploadFile"
+                        :multiple="true"
+                        :limit="20"
+                        tip="请上传素材文件,支持多文件,上传后自动写入素材链接"
+                        btnTxt="上传素材"
+                    />
                 </el-form-item>
                 <el-form-item label="价格" prop="price">
                     <el-input v-model="form.price" />
                 </el-form-item>
-                <el-form-item label="客服ID" prop="service_id">
-                    <el-input v-model="form.service_id" />
+                <el-form-item label="客服ID" prop="kefu_id">
+                    <el-select
+                        v-model="form.kefu_id"
+                        placeholder="请选择客服"
+                        clearable
+                        filterable
+                        :loading="kefuLoading"
+                        style="width: 100%;"
+                    >
+                        <el-option
+                            v-for="item in kefuOptions"
+                            :key="item.id"
+                            :label="item.label"
+                            :value="item.value"
+                        />
+                    </el-select>
                 </el-form-item>
                 <el-form-item label="截止时间" prop="endDate">
                     <el-date-picker
@@ -119,6 +140,36 @@
 </template>
 
 <script>
+function formatMaterialLabels(material) {
+    if (!Array.isArray(material) || !material.length) return [];
+    return material
+        .map((m) => {
+            if (m == null) return "";
+            if (typeof m === "string") return m.trim();
+            return String(m.fileName || m.name || m.src || m.url || "").trim();
+        })
+        .filter(Boolean);
+}
+
+function normalizeMaterialFiles(material) {
+    if (!Array.isArray(material) || !material.length) return [];
+    return material
+        .map((m) => {
+            if (typeof m === "string") {
+                const url = m.trim();
+                if (!url) return null;
+                const parts = url.split(/[/\\]/);
+                return { name: parts[parts.length - 1] || url, url };
+            }
+            const src = String(m.src || m.url || "").trim();
+            if (!src) return null;
+            const fileName = String(m.fileName || m.name || "").trim();
+            const name = fileName || src.split(/[/\\]/).pop() || src;
+            return { name, url: src };
+        })
+        .filter(Boolean);
+}
+
 export default {
     name: "taskList",
     data() {
@@ -127,6 +178,8 @@ export default {
             saving: false,
             list: [],
             total: 0,
+            kefuLoading: false,
+            kefuOptions: [],
             query: {
                 page: 1,
                 limit: 15
@@ -137,14 +190,23 @@ export default {
             rules: {
                 title: [{ required: true, message: "请输入标题", trigger: "blur" }],
                 explain: [{ required: true, message: "请输入说明", trigger: "blur" }],
-                materialText: [{ required: true, message: "请输入素材ID", trigger: "blur" }],
+                materialFiles: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (Array.isArray(value) && value.length) callback();
+                            else callback(new Error("请上传素材文件"));
+                        },
+                        trigger: "change"
+                    }
+                ],
                 price: [{ required: true, message: "请输入价格", trigger: "blur" }],
-                service_id: [{ required: true, message: "请输入客服ID", trigger: "blur" }],
+                kefu_id: [{ required: true, message: "请选择客服", trigger: "change" }],
                 endDate: [{ required: true, message: "请选择截止时间", trigger: "change" }]
             }
         };
     },
     created() {
+        this.getKefuOptions();
         this.getList();
     },
     methods: {
@@ -153,10 +215,9 @@ export default {
                 id: "",
                 title: "",
                 explain: "",
-                material: [],
-                materialText: "",
+                materialFiles: [],
                 price: "",
-                service_id: "",
+                kefu_id: "",
                 endDate: "",
                 end_time: "",
                 status: 1
@@ -180,7 +241,7 @@ export default {
                 const rawList = data.data || data.list || [];
                 this.list = rawList.map((item) => ({
                     ...item,
-                    materialDisplay: Array.isArray(item.material) ? item.material : [],
+                    materialDisplay: formatMaterialLabels(item.material),
                     // 后端字段为 pirce,这里统一映射给表格展示
                     price: item.price || item.pirce || ""
                 }));
@@ -189,6 +250,25 @@ export default {
                 this.loading = false;
             }
         },
+        async getKefuOptions() {
+            this.kefuLoading = true;
+            try {
+                const res = await this.$API.kefu.list.get({ page: 1, limit: 200 });
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取客服列表失败");
+                    return;
+                }
+                const data = res.data || {};
+                const rawList = data.data || data.list || [];
+                this.kefuOptions = rawList.map((item) => ({
+                    id: item.id,
+                    value: String(item.id),
+                    label: item.name ? `${item.name}(id:${item.id})` : String(item.id)
+                }));
+            } finally {
+                this.kefuLoading = false;
+            }
+        },
         handlePageChange(page) {
             this.query.page = page;
             this.getList();
@@ -213,16 +293,17 @@ export default {
                 return;
             }
             const info = res.data || {};
-            const material = Array.isArray(info.material) ? info.material : [];
+            const materialFiles = normalizeMaterialFiles(
+                Array.isArray(info.material) ? info.material : []
+            );
             const endTime = info.end_time ? Number(info.end_time) * 1000 : "";
             this.form = {
                 id: info.id || row.id,
                 title: info.title || "",
                 explain: info.explain || "",
-                material,
-                materialText: material.join(","),
+                materialFiles,
                 price: info.price || info.pirce || "",
-                service_id: String(info.service_id || ""),
+                kefu_id: String(info.kefu_id || ""),
                 endDate: endTime ? String(endTime) : "",
                 end_time: String(info.end_time || ""),
                 status: Number(info.status ?? 1)
@@ -233,18 +314,18 @@ export default {
                 if (!valid) return false;
                 this.saving = true;
                 try {
-                    const material = (this.form.materialText || "")
-                        .split(",")
-                        .map((item) => item.trim())
-                        .filter(Boolean);
+                    const material = (this.form.materialFiles || []).map((f) => ({
+                        fileName: f.name || "",
+                        src: f.url || ""
+                    })).filter((m) => m.src);
                     const endTimeSec = this.form.endDate ? Math.floor(Number(this.form.endDate) / 1000) : "";
                     const payload = {
                         title: this.form.title,
                         explain: this.form.explain,
                         material,
-                        pirce: this.form.price,
+                        price: this.form.price,
                         end_time: String(endTimeSec),
-                        service_id: this.form.service_id,
+                        kefu_id: this.form.kefu_id,
                         status: String(this.form.status)
                     };
                     if (this.form.id) payload.id = this.form.id;

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

+ 10 - 0
vue.config.js

@@ -1,5 +1,10 @@
 const { defineConfig } = require('@vue/cli-service')
 
+function uploadProxyTarget() {
+	const base = (process.env.VUE_APP_API_BASEURL || '').replace(/\/+$/, '')
+	return base.replace(/\/adminapi$/i, '') || base
+}
+
 module.exports = defineConfig({
 	//设置为空打包后不分更目录还是多级目录
 	publicPath:'',
@@ -21,6 +26,11 @@ module.exports = defineConfig({
 				pathRewrite: {
 					'^/adminapi': '/'
 				}
+			},
+			'/upload': {
+				target: uploadProxyTarget(),
+				ws: true,
+				changeOrigin: true
 			}
 		}
 	},