Bladeren bron

feat: 对接接口

doi 2 maanden geleden
bovenliggende
commit
f81365fcb9

+ 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

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

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

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

@@ -29,5 +29,40 @@ export default {
         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);
+        }
     }
 };

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

@@ -18,6 +18,20 @@ export default {
             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);
+            }
         }
     }
 };

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

+ 91 - 15
src/views/cutter/list/index.vue

@@ -6,22 +6,68 @@
             </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">
+                <el-table-column prop="uid" label="UID" width="72" />
+                <el-table-column label="头像" width="72" align="center">
                     <template #default="{ row }">
-                        <el-tag v-if="row.status !== undefined && row.status !== null" size="small">
-                            {{ formatStatus(row.status) }}
+                        <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="add_time" label="时间" min-width="160" show-overflow-tooltip>
+                <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 }">
-                        {{ formatCellTime(row.add_time || row.create_time) }}
+                        {{ formatDispTime(row.updated_at) }}
                     </template>
                 </el-table-column>
             </el-table>
@@ -57,15 +103,41 @@ export default {
         this.getList();
     },
     methods: {
-        formatStatus(v) {
+        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);
         },
-        formatCellTime(val) {
+        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 (!num) return String(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");
         },
@@ -78,8 +150,8 @@ export default {
                     return;
                 }
                 const data = res.data || {};
-                this.list = data.data || data.list || [];
-                this.total = Number(data.count || data.total || this.list.length || 0);
+                this.list = data.list || data.data || [];
+                this.total = Number(data.count ?? data.total ?? this.list.length ?? 0);
             } finally {
                 this.loading = false;
             }
@@ -103,4 +175,8 @@ export default {
     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>

+ 95 - 2
src/views/finance/extract/list/index.vue

@@ -19,7 +19,9 @@
                 </el-table-column>
                 <el-table-column label="状态" width="120">
                     <template #default="{ row }">
-                        {{ formatStatus(row.status) }}
+                        <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>
@@ -32,6 +34,17 @@
                         {{ 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">
@@ -47,6 +60,21 @@
                 />
             </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>
 
@@ -63,9 +91,16 @@ export default {
     data() {
         return {
             loading: false,
+            submitting: false,
             list: [],
             total: 0,
-            query: { page: 1, limit: 10 }
+            query: { page: 1, limit: 10, status: 0 },
+            remarkDialog: false,
+            remarkTitle: "",
+            remarkPlaceholder: "",
+            remark: "",
+            remarkAction: "",
+            currentRow: null
         };
     },
     created() {
@@ -77,6 +112,15 @@ export default {
             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);
@@ -110,6 +154,51 @@ export default {
             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;
+            }
         }
     }
 };
@@ -121,4 +210,8 @@ export default {
     display: flex;
     justify-content: flex-end;
 }
+.muted {
+    color: var(--el-text-color-secondary);
+    font-size: 12px;
+}
 </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>

+ 102 - 21
src/views/task/manage/list/index.vue

@@ -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;

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