diff --git a/admin/src/views/subscription/subscriptions.vue b/admin/src/views/subscription/subscriptions.vue index eeeeffb..d4f9f7a 100644 --- a/admin/src/views/subscription/subscriptions.vue +++ b/admin/src/views/subscription/subscriptions.vue @@ -132,8 +132,8 @@ export default { columns() { return [ { title: 'ID', key: 'id', width: 70 }, - { title: '用户', key: 'user_id', width: 90 }, - { title: '套餐', key: 'plan_id', width: 90 }, + { title: '用户', key: 'user_name', minWidth: 160, ellipsis: true, tooltip: true }, + { title: '套餐', key: 'plan_name', minWidth: 160, ellipsis: true, tooltip: true }, { title: '状态', key: 'status', width: 100 }, { title: '开始', key: 'start_time', minWidth: 150 }, { title: '结束', key: 'end_time', minWidth: 150 }, diff --git a/admin/src/views/subscription/users.vue b/admin/src/views/subscription/users.vue index adcf294..f48aa25 100644 --- a/admin/src/views/subscription/users.vue +++ b/admin/src/views/subscription/users.vue @@ -70,79 +70,36 @@ - +

用户:{{ create_token_target_user.name || ('#' + create_token_target_user.id) }} · ID {{ create_token_target_user.id }}

- + +
- -
- ID {{ detail.user.id }} - {{ detail.user.name || '—' }} - {{ detail.user.mobile || '—' }} - {{ detail.user.status }} - -
-

Token 数量:{{ detail.tokenCount }}

-

API Token

- -

订阅记录

-
- - - -
- - 点击行查看详情;列表「明文」来自服务端加密存储(吊销后不可查看) -
-
-

暂无 Token

- - - -
-

ID{{ selectedToken.id }}

-

名称{{ selectedToken.token_name }}

-

用户{{ selectedToken.user_id }}

-

套餐{{ selectedToken.plan_id != null ? selectedToken.plan_id : '—' }}

-

状态{{ selectedToken.status }}

-

过期时间{{ selectedToken.expire_at || '—' }}

-

最后使用{{ selectedToken.last_used_at || '—' }}

-
-

明文

- -

无存储明文(旧数据或已吊销);可对 active 记录「重新生成」后查看

-
-
+ + @@ -171,11 +128,6 @@ export default { }, detailVisible: false, detail: null, - tokenListVisible: false, - tokenListRows: [], - tokenListUserName: '', - tokenDetailVisible: false, - selectedToken: null, token_plain_modal: false, token_plain_title: '', token_plain_text: '', @@ -183,7 +135,6 @@ export default { create_token_saving: false, create_token_target_user: null, create_token_form: { token_name: 'default', expire_at: '' }, - tokenList_target_user: null, subCols: [ { title: 'ID', key: 'id', width: 80 }, { title: '套餐ID', key: 'plan_id', width: 90 }, @@ -215,11 +166,11 @@ export default { on: { click: (e) => { e.preventDefault() - this.openTokenList(p.row) + this.showDetail(p.row) }, }, }, - n > 0 ? `${n} · 列表` : String(n) + n > 0 ? `${n} 条` : '0' ) }, }, @@ -294,24 +245,31 @@ export default { }, tokenCols() { return [ - { title: 'ID', key: 'id', width: 72 }, - { title: '名称', key: 'token_name', minWidth: 100 }, - { title: '套餐', key: 'plan_id', width: 80 }, - { title: '状态', key: 'status', width: 90 }, - { title: '过期', key: 'expire_at', minWidth: 150 }, - { title: '最后使用', key: 'last_used_at', minWidth: 150 }, + { title: 'ID', key: 'id', width: 56 }, + { title: '名称', key: 'token_name', width: 88 }, + { title: '套餐', key: 'plan_id', width: 64 }, + { title: '状态', key: 'status', width: 72 }, + { title: '过期', key: 'expire_at', minWidth: 128 }, + { title: '最后使用', key: 'last_used_at', minWidth: 128 }, { title: '明文', key: 'plain_token', - minWidth: 200, + minWidth: 280, render: (h, p) => { const v = p.row.plain_token if (!v) { return h('span', { class: { 'text-muted': true } }, '—') } - const snip = v.length > 28 ? `${v.slice(0, 28)}…` : v - return h('div', { class: 'plain-token-row' }, [ - h('span', { class: 'plain-token-snip', attrs: { title: v } }, snip), + return h('div', { class: 'plain-cell' }, [ + h('Input', { + props: { + type: 'textarea', + value: v, + rows: 2, + readonly: true, + }, + class: 'plain-cell-input', + }), h( 'Button', { @@ -331,7 +289,7 @@ export default { { title: '操作', key: 'tok_op', - width: 120, + width: 96, render: (h, p) => { if (p.row.status !== 'active') { return h('span', { class: { 'text-muted': true } }, '—') @@ -340,7 +298,12 @@ export default { 'Button', { props: { type: 'warning', size: 'small' }, - on: { click: () => this.do_regenerate_token(p.row) }, + on: { + click: (e) => { + e.stopPropagation() + this.do_regenerate_token(p.row) + }, + }, }, '重新生成' ) @@ -348,10 +311,6 @@ export default { }, ] }, - tokenListTitle() { - const name = this.tokenListUserName || '' - return name ? `Token 列表 — ${name}` : 'Token 列表' - }, }, mounted() { this.load(1) @@ -503,21 +462,6 @@ export default { this.create_token_saving = false } }, - async openTokenList(row) { - const res = await userServer.detail(row.id) - if (res && res.code === 0) { - this.tokenListRows = res.data.tokens || [] - this.tokenListUserName = row.name || String(row.id) - this.tokenList_target_user = res.data.user || row - this.tokenListVisible = true - } else { - this.$Message.error((res && res.message) || '加载 Token 失败') - } - }, - onTokenRowClick(row) { - this.selectedToken = row - this.tokenDetailVisible = true - }, show_token_plain(title, text) { this.token_plain_title = title this.token_plain_text = text || '' @@ -527,7 +471,6 @@ export default { const res = await userServer.detail(user_id) if (res && res.code === 0) { if (this.detailVisible) this.detail = res.data - if (this.tokenListVisible) this.tokenListRows = res.data.tokens || [] this.load(1) } }, @@ -650,59 +593,25 @@ export default { text-decoration: underline; } -.token-detail .label { - display: inline-block; - width: 88px; - color: #808695; +.token-table-in-detail :deep(.plain-cell) { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; } -.token-plain-block { +.token-table-in-detail :deep(.plain-cell-input textarea) { + font-family: monospace; + font-size: 11px; + line-height: 1.4; + min-width: 260px; +} + +.mt12 { margin-top: 12px; - padding-top: 12px; - border-top: 1px solid #e8eaec; } -.token-plain-block .label { - margin-bottom: 6px; -} - -.token-plain-ta :deep(textarea) { - font-family: monospace; - font-size: 12px; -} - -.mb0 { - margin-bottom: 0; -} - -.mt8 { - margin-top: 8px; -} - -.plain-token-row { - display: flex; - align-items: center; - gap: 6px; -} - -.plain-token-snip { - flex: 1; - min-width: 0; - font-family: monospace; - font-size: 12px; - word-break: break-all; -} - -.detail-user-bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 12px; - padding-bottom: 8px; - border-bottom: 1px solid #e8eaec; -} - -.detail-user-item { - color: #515a6e; +.detail-one-line { + line-height: 32px; } diff --git a/api/controller_admin/biz_subscription.js b/api/controller_admin/biz_subscription.js index e796efe..91722b6 100644 --- a/api/controller_admin/biz_subscription.js +++ b/api/controller_admin/biz_subscription.js @@ -3,6 +3,20 @@ const { build_search_where } = require("../utils/query_helpers"); const logic = require("../service/biz_subscription_logic"); const audit = require("../utils/biz_audit"); +function subscription_rows_with_names(instances) { + return instances.map((r) => { + const j = r.toJSON(); + const u = j.biz_user; + const p = j.biz_plan; + const { biz_user, biz_plan, ...rest } = j; + return { + ...rest, + user_name: u ? [u.name, u.mobile].filter(Boolean).join(" ").trim() : "", + plan_name: p ? String(p.plan_name || p.plan_code || "").trim() : "", + }; + }); +} + module.exports = { "POST /biz_subscription/page": async (ctx) => { const body = ctx.getBody(); @@ -19,8 +33,13 @@ module.exports = { offset, limit: page_size, order: [["id", "DESC"]], + include: [ + { model: baseModel.biz_user, as: "biz_user", attributes: ["name", "mobile"] }, + { model: baseModel.biz_plan, as: "biz_plan", attributes: ["plan_name", "plan_code"] }, + ], + distinct: true, }); - ctx.success({ rows, count }); + ctx.success({ rows: subscription_rows_with_names(rows), count }); }, "GET /biz_subscription/detail": async (ctx) => { const q = ctx.query || {}; @@ -95,7 +114,11 @@ module.exports = { where, limit: 10000, order: [["id", "DESC"]], + include: [ + { model: baseModel.biz_user, as: "biz_user", attributes: ["name", "mobile"] }, + { model: baseModel.biz_plan, as: "biz_plan", attributes: ["plan_name", "plan_code"] }, + ], }); - ctx.success({ rows }); + ctx.success({ rows: subscription_rows_with_names(rows) }); }, };