Forráskód Böngészése

feat: 标签页面

doi 2 hónapja
szülő
commit
fb907035bc

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

@@ -64,5 +64,33 @@ export default {
         post: async function (id, data = {}) {
             return await http.post(`${this.url}/${id}`, data);
         }
+    },
+    labelList: {
+        url: `${config.API_URL}/cutter/label/list`,
+        name: "标签列表",
+        get: async function (type) {
+            return await http.get(`${this.url}/${type}`);
+        }
+    },
+    labelSave: {
+        url: `${config.API_URL}/cutter/label/save`,
+        name: "标签保存/编辑",
+        post: async function (data = {}) {
+            return await http.post(this.url, data);
+        }
+    },
+    labelDel: {
+        url: `${config.API_URL}/cutter/label/del`,
+        name: "标签删除",
+        delete: async function (id) {
+            return await http.delete(`${this.url}/${id}`);
+        }
+    },
+    labelInfo: {
+        url: `${config.API_URL}/cutter/label/info`,
+        name: "标签详情",
+        get: async function (id) {
+            return await http.get(`${this.url}/${id}`);
+        }
     }
 };

+ 32 - 0
src/style/app.scss

@@ -616,6 +616,38 @@ fieldset legend {
 	background-color: var(--el-fill-color-light) !important;
 }
 
+/* 表格「操作」列:实心按钮、左对齐、不换行 */
+.table-row-actions {
+	display: flex;
+	flex-wrap: nowrap;
+	justify-content: flex-start;
+	align-items: center;
+	gap: 8px;
+	width: 100%;
+	box-sizing: border-box;
+	min-height: 32px;
+}
+
+.table-row-actions .el-button {
+	margin-left: 0 !important;
+	flex-shrink: 0;
+	text-align: center;
+}
+
+.table-row-actions > .muted {
+	flex-shrink: 0;
+	white-space: nowrap;
+	padding: 4px 2px;
+	color: var(--el-text-color-placeholder);
+	font-size: 12px;
+	font-weight: normal;
+	text-align: left;
+}
+
+.el-table .el-table__cell .cell:has(> .table-row-actions) {
+	white-space: nowrap;
+}
+
 .hide-text {
 	white-space: nowrap;
 	text-overflow: ellipsis;

+ 65 - 0
src/utils/extractBaiduPan.js

@@ -0,0 +1,65 @@
+/**
+ * 从混合格式文案(含文件名、说明、提取码等)中解析百度网盘标准分享链接。
+ */
+
+const PAN_S_URL_RE =
+    /https?:\/\/pan\.baidu\.com\/s\/[A-Za-z0-9_-]+(?:\?(?:[A-Za-z0-9_=&%.-]*))?/i;
+
+const PWD_LABEL_RE = /(?:提取码|密码|提取\s*码)[::\s]*([A-Za-z0-9]{4,8})\b/i;
+
+const FIRST_HTTP_RE =
+    /https?:\/\/[^\s\u4e00-\u9fff\u3000-\u303f]+/i;
+
+/**
+ * @param {string|null|undefined} text
+ * @returns {{ url: string|null, pwd: string|null }}
+ */
+export function parseBaiduPanShare(text) {
+    if (text == null || text === "") {
+        return { url: null, pwd: null };
+    }
+    const s = String(text);
+    const urlMatch = s.match(PAN_S_URL_RE);
+    let url = urlMatch ? urlMatch[0] : null;
+
+    let pwd = null;
+    if (url) {
+        try {
+            const u = new URL(url);
+            pwd = u.searchParams.get("pwd") || u.searchParams.get("PWD");
+        } catch {
+            /* ignore */
+        }
+    }
+
+    const pwdLabel = s.match(PWD_LABEL_RE);
+    if (!pwd && pwdLabel) {
+        pwd = pwdLabel[1];
+    }
+
+    if (url && pwd && !/pwd=/i.test(url)) {
+        url += url.includes("?") ? `&pwd=${encodeURIComponent(pwd)}` : `?pwd=${encodeURIComponent(pwd)}`;
+    }
+
+    return { url, pwd };
+}
+
+/**
+ * 表格/外链打开用:优先解析百度分享;否则取文案中首个 http(s) URL(去掉末尾中英文标点粘连)。
+ * @param {string|null|undefined} raw
+ * @returns {string|null}
+ */
+export function resolveExternalShareUrl(raw) {
+    const { url } = parseBaiduPanShare(raw);
+    if (url) return url;
+    if (raw == null || raw === "") return null;
+    const s = String(raw).trim();
+    const any = s.match(FIRST_HTTP_RE);
+    if (any) {
+        return any[0].replace(/[,.;,。、;]+$/, "");
+    }
+    if (/^https?:\/\//i.test(s)) {
+        return s.replace(/[,.;,。、;]+$/, "");
+    }
+    return null;
+}

+ 61 - 19
src/views/cutter/apply/index.vue

@@ -8,6 +8,16 @@
             <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 label="行业" min-width="140" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ idsToLabelText(row.trade, tradeLabelMap) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="风格" min-width="140" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        {{ idsToLabelText(row.style, styleLabelMap) }}
+                    </template>
+                </el-table-column>
                 <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 />
@@ -37,24 +47,13 @@
                 </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>
+                        <div class="table-row-actions">
+                            <template v-if="isPending(row)">
+                                <el-button type="success" size="small" @click="openRemark(row, 'agree')">同意</el-button>
+                                <el-button type="danger" size="small" plain @click="openRemark(row, 'refuse')">拒绝</el-button>
+                            </template>
+                            <span v-else class="muted">已处理</span>
+                        </div>
                     </template>
                 </el-table-column>
             </el-table>
@@ -97,13 +96,56 @@ export default {
             remarkTitle: "",
             remark: "",
             remarkAction: "",
-            currentRow: null
+            currentRow: null,
+            /** 行业标签 id -> 名称(type=1) */
+            tradeLabelMap: {},
+            /** 风格标签 id -> 名称(type=2) */
+            styleLabelMap: {}
         };
     },
     created() {
+        this.loadLabelMaps();
         this.getList();
     },
     methods: {
+        buildLabelMap(raw) {
+            const arr = Array.isArray(raw) ? raw : raw?.list || raw?.data || [];
+            const map = {};
+            for (const item of arr) {
+                const id = item?.id;
+                if (id == null) continue;
+                map[Number(id)] = item.label_name ?? item.name ?? String(id);
+            }
+            return map;
+        },
+        async loadLabelMaps() {
+            try {
+                const [tradeRes, styleRes] = await Promise.all([
+                    this.$API.cutter.labelList.get(1),
+                    this.$API.cutter.labelList.get(2)
+                ]);
+                if (tradeRes.code === 1) {
+                    this.tradeLabelMap = this.buildLabelMap(tradeRes.data);
+                }
+                if (styleRes.code === 1) {
+                    this.styleLabelMap = this.buildLabelMap(styleRes.data);
+                }
+            } catch (e) {
+                console.error(e);
+            }
+        },
+        idsToLabelText(ids, map) {
+            if (ids == null) return "-";
+            const list = Array.isArray(ids) ? ids : [ids];
+            if (!list.length) return "-";
+            return list
+                .map((id) => {
+                    const n = Number(id);
+                    const name = map[n];
+                    return name != null && name !== "" ? name : String(id);
+                })
+                .join("、");
+        },
         /** 后端为日期字符串 "2026-04-29 12:17:09" 或时间戳 */
         formatDispTime(val) {
             if (val === undefined || val === null || val === "") return "-";

+ 250 - 0
src/views/cutter/label/list/index.vue

@@ -0,0 +1,250 @@
+<template>
+    <el-main>
+        <el-card shadow="never">
+            <template #header>
+                <div class="header-row">
+                    <span class="card-title">标签管理</span>
+                    <div class="header-actions">
+                        <el-radio-group v-model="labelType" size="default" @change="onTypeChange">
+                            <el-radio-button :label="1">行业标签</el-radio-button>
+                            <el-radio-button :label="2">风格标签</el-radio-button>
+                        </el-radio-group>
+                        <el-button type="primary" round @click="openCreate">
+                            新增标签
+                        </el-button>
+                    </div>
+                </div>
+            </template>
+
+            <el-table v-loading="loading" :data="list" stripe class="label-table">
+                <el-table-column prop="id" label="ID" width="90" />
+                <el-table-column label="标签名称" min-width="160" show-overflow-tooltip>
+                    <template #default="{ row }">
+                        <span class="label-name">{{ row.label_name ?? row.name ?? "-" }}</span>
+                    </template>
+                </el-table-column>
+                <!-- <el-table-column label="类型" width="110">
+                    <template #default="{ row }">
+                        {{ typeText(row.type ?? labelType) }}
+                    </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="操作" width="248" fixed="right">
+                    <template #default="{ row }">
+                        <div class="table-row-actions">
+                            <el-button type="info" size="small" @click="showDetail(row)">详情</el-button>
+                            <el-button type="primary" size="small" @click="openEdit(row)">编辑</el-button>
+                            <el-button type="danger" size="small" @click="removeRow(row)">删除</el-button>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </el-card>
+
+        <el-dialog v-model="detailDialog" title="标签详情" width="520px" destroy-on-close>
+            <el-descriptions :column="1" border>
+                <el-descriptions-item label="ID">{{ detail.id ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="标签名称">{{ detail.label_name ?? detail.name ?? "-" }}</el-descriptions-item>
+                <el-descriptions-item label="类型">{{ typeText(detail.type) }}</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>
+        </el-dialog>
+
+        <el-dialog v-model="editDialog" :title="editTitle" width="440px" destroy-on-close @closed="resetEditForm">
+            <el-form ref="formRef" :model="form" :rules="rules" label-width="96px">
+                <el-form-item label="标签名称" prop="label_name">
+                    <el-input v-model="form.label_name" clearable maxlength="64" placeholder="请输入标签名称" />
+                </el-form-item>
+                <el-form-item label="类型">
+                    <el-tag size="small">{{ typeText(labelType) }}</el-tag>
+                    <span class="hint">与当前列表分类一致</span>
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="editDialog = false">取消</el-button>
+                <el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+            </template>
+        </el-dialog>
+    </el-main>
+</template>
+
+<script>
+const TYPE_MAP = {
+    1: "行业标签",
+    2: "风格标签"
+};
+
+export default {
+    name: "LabelList",
+    data() {
+        return {
+            loading: false,
+            saving: false,
+            labelType: 1,
+            list: [],
+            detailDialog: false,
+            detail: {},
+            editDialog: false,
+            editTitle: "新增标签",
+            form: {
+                id: "",
+                label_name: ""
+            },
+            rules: {
+                label_name: [{ required: true, message: "请输入标签名称", trigger: "blur" }]
+            }
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        typeText(type) {
+            if (type == null || type === "") return "-";
+            const n = Number(type);
+            return TYPE_MAP[n] ?? String(type);
+        },
+        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");
+        },
+        onTypeChange() {
+            this.getList();
+        },
+        async getList() {
+            this.loading = true;
+            try {
+                const res = await this.$API.cutter.labelList.get(this.labelType);
+                if (res.code !== 1) {
+                    this.$message.error(res.msg || "获取标签列表失败");
+                    return;
+                }
+                const raw = res.data;
+                this.list = Array.isArray(raw) ? raw : raw?.list || raw?.data || [];
+            } finally {
+                this.loading = false;
+            }
+        },
+        async showDetail(row) {
+            const id = row?.id;
+            if (id == null) return;
+            const res = await this.$API.cutter.labelInfo.get(id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "获取详情失败");
+                return;
+            }
+            this.detail = res.data || {};
+            this.detailDialog = true;
+        },
+        openCreate() {
+            this.editTitle = "新增标签";
+            this.form = { id: "", label_name: "" };
+            this.editDialog = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        openEdit(row) {
+            this.editTitle = "编辑标签";
+            this.form = {
+                id: row.id != null ? String(row.id) : "",
+                label_name: row.label_name ?? row.name ?? ""
+            };
+            this.editDialog = true;
+            this.$nextTick(() => this.$refs.formRef?.clearValidate());
+        },
+        resetEditForm() {
+            this.form = { id: "", label_name: "" };
+        },
+        submitForm() {
+            this.$refs.formRef.validate(async (valid) => {
+                if (!valid) return false;
+                this.saving = true;
+                try {
+                    const payload = {
+                        label_name: (this.form.label_name || "").trim(),
+                        type: String(this.labelType)
+                    };
+                    if (this.form.id) payload.id = String(this.form.id);
+                    const res = await this.$API.cutter.labelSave.post(payload);
+                    if (res.code !== 1) {
+                        this.$message.error(res.msg || "保存失败");
+                        return false;
+                    }
+                    this.$message.success(res.msg || "保存成功");
+                    this.editDialog = false;
+                    this.getList();
+                } finally {
+                    this.saving = false;
+                }
+            });
+        },
+        async removeRow(row) {
+            const id = row?.id;
+            if (id == null) return;
+            try {
+                await this.$confirm("确认删除该标签吗?", "提示", {
+                    type: "warning",
+                    confirmButtonText: "删除",
+                    cancelButtonText: "取消"
+                });
+            } catch (e) {
+                return;
+            }
+            const res = await this.$API.cutter.labelDel.delete(id);
+            if (res.code !== 1) {
+                this.$message.error(res.msg || "删除失败");
+                return;
+            }
+            this.$message.success(res.msg || "删除成功");
+            this.getList();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.card-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    letter-spacing: 0.02em;
+}
+.header-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-wrap: wrap;
+    gap: 12px;
+}
+.header-actions {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+.label-table :deep(.el-table__header th.el-table__cell) {
+    font-weight: 600;
+    color: var(--el-text-color-regular);
+}
+.label-name {
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+}
+.hint {
+    margin-left: 8px;
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+}
+.pager {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 20 - 14
src/views/cutter/order/index.vue

@@ -29,7 +29,7 @@
                 </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 v-if="shareHref(row.audit_url)" :href="shareHref(row.audit_url)" type="primary" target="_blank">
                             打开
                         </el-link>
                         <span v-else>-</span>
@@ -37,7 +37,7 @@
                 </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 v-if="shareHref(row.finish_url)" :href="shareHref(row.finish_url)" type="primary" target="_blank">
                             打开
                         </el-link>
                         <span v-else>-</span>
@@ -65,13 +65,14 @@
                         {{ formatTime(row.updated_at || row.update_time) }}
                     </template>
                 </el-table-column>
-                <el-table-column label="操作" width="190" fixed="right">
+                <el-table-column label="操作" width="288" 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> -->
+                        <div class="table-row-actions">
+                            <el-button type="info" size="small" @click="showDetail(row)">详情</el-button>
+                            <el-button v-if="canAudit(row)" type="success" size="small" @click="affirmOrder(row)">通过</el-button>
+                            <el-button v-if="canAudit(row)" type="danger" size="small" @click="openRemark(row, 'reject')">拒绝</el-button>
+                            <el-button v-if="canSettle(row)" type="warning" size="small" @click="settleOrder(row)">结算</el-button>
+                        </div>
                     </template>
                 </el-table-column>
             </el-table>
@@ -101,16 +102,16 @@
                 <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 v-if="shareHref(detail.audit_url)" :href="shareHref(detail.audit_url)" type="primary" target="_blank">
+                        {{ shareHref(detail.audit_url) }}
                     </el-link>
-                    <span v-else>-</span>
+                    <span v-else>{{ detail.audit_url || "-" }}</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 v-if="shareHref(detail.finish_url)" :href="shareHref(detail.finish_url)" type="primary" target="_blank">
+                        {{ shareHref(detail.finish_url) }}
                     </el-link>
-                    <span v-else>-</span>
+                    <span v-else>{{ detail.finish_url || "-" }}</span>
                 </el-descriptions-item>
             </el-descriptions>
         </el-dialog>
@@ -126,6 +127,8 @@
 </template>
 
 <script>
+import { resolveExternalShareUrl } from "@/utils/extractBaiduPan";
+
 const STATUS_MAP = {
     "-1": "未通过",
     0: "待制作",
@@ -158,6 +161,9 @@ export default {
         this.getList();
     },
     methods: {
+        shareHref(raw) {
+            return resolveExternalShareUrl(raw);
+        },
         statusText(status) {
             if (status == null || status === "") return "-";
             const key = String(status);

+ 8 - 8
src/views/finance/extract/list/index.vue

@@ -34,15 +34,15 @@
                         {{ formatTime(row.add_time || row.create_time || row.created_at) }}
                     </template>
                 </el-table-column>
-                <el-table-column label="操作" width="220" fixed="right">
+                <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>
+                        <div class="table-row-actions">
+                            <template v-if="isPending(row)">
+                                <el-button type="success" size="small" @click="openRemark(row, 'agree')">同意</el-button>
+                                <el-button type="danger" size="small" plain @click="openRemark(row, 'refuse')">拒绝</el-button>
+                            </template>
+                            <span v-else class="muted">已处理</span>
+                        </div>
                     </template>
                 </el-table-column>
             </el-table>

+ 5 - 3
src/views/kefu/list/index.vue

@@ -41,10 +41,12 @@
                         />
                     </template>
                 </el-table-column>
-                <el-table-column label="操作" width="160" fixed="right">
+                <el-table-column label="操作" width="200" 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>
+                        <div class="table-row-actions">
+                            <el-button type="primary" size="small" @click="openEdit(row)">编辑</el-button>
+                            <el-button type="danger" size="small" plain @click="removeRow(row)">删除</el-button>
+                        </div>
                     </template>
                 </el-table-column>
             </el-table>

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

@@ -49,10 +49,12 @@
                         {{ formatTime(row.end_time) }}
                     </template>
                 </el-table-column>
-                <el-table-column label="操作" width="140" fixed="right">
+                <el-table-column label="操作" width="200" fixed="right">
                     <template #default="{ row }">
-                        <el-button size="small" @click="openEdit(row)">编辑</el-button>
-                        <el-button size="small" type="danger" @click="removeTask(row)">删除</el-button>
+                        <div class="table-row-actions">
+                            <el-button type="primary" size="small" @click="openEdit(row)">编辑</el-button>
+                            <el-button type="danger" size="small" plain @click="removeTask(row)">删除</el-button>
+                        </div>
                     </template>
                 </el-table-column>
             </el-table>