1
This commit is contained in:
@@ -132,8 +132,8 @@ export default {
|
|||||||
columns() {
|
columns() {
|
||||||
return [
|
return [
|
||||||
{ title: 'ID', key: 'id', width: 70 },
|
{ title: 'ID', key: 'id', width: 70 },
|
||||||
{ title: '用户', key: 'user_id', width: 90 },
|
{ title: '用户', key: 'user_name', minWidth: 160, ellipsis: true, tooltip: true },
|
||||||
{ title: '套餐', key: 'plan_id', width: 90 },
|
{ title: '套餐', key: 'plan_name', minWidth: 160, ellipsis: true, tooltip: true },
|
||||||
{ title: '状态', key: 'status', width: 100 },
|
{ title: '状态', key: 'status', width: 100 },
|
||||||
{ title: '开始', key: 'start_time', minWidth: 150 },
|
{ title: '开始', key: 'start_time', minWidth: 150 },
|
||||||
{ title: '结束', key: 'end_time', minWidth: 150 },
|
{ title: '结束', key: 'end_time', minWidth: 150 },
|
||||||
|
|||||||
@@ -70,79 +70,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal v-model="create_token_modal" title="生成 API Token" width="560" :loading="create_token_saving"
|
||||||
v-model="create_token_modal"
|
@on-ok="submit_create_token">
|
||||||
title="生成 API Token"
|
|
||||||
width="560"
|
|
||||||
:loading="create_token_saving"
|
|
||||||
@on-ok="submit_create_token"
|
|
||||||
>
|
|
||||||
<p v-if="create_token_target_user" class="text-muted mb12">
|
<p v-if="create_token_target_user" class="text-muted mb12">
|
||||||
用户:{{ create_token_target_user.name || ('#' + create_token_target_user.id) }}
|
用户:{{ create_token_target_user.name || ('#' + create_token_target_user.id) }}
|
||||||
· ID {{ create_token_target_user.id }}
|
· ID {{ create_token_target_user.id }}
|
||||||
</p>
|
</p>
|
||||||
<Form :label-width="100">
|
<Form :label-width="100">
|
||||||
<FormItem label="名称"><Input v-model="create_token_form.token_name" placeholder="default" /></FormItem>
|
<FormItem label="名称"><Input v-model="create_token_form.token_name" placeholder="default" /></FormItem>
|
||||||
<FormItem label="过期时间"><Input v-model="create_token_form.expire_at" placeholder="YYYY-MM-DD 23:59:59" /></FormItem>
|
<FormItem label="过期时间"><Input v-model="create_token_form.expire_at" placeholder="YYYY-MM-DD 23:59:59" />
|
||||||
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal v-model="detailVisible" title="用户详情" width="920" footer-hide>
|
<Modal v-model="detailVisible" title="用户详情" width="960" footer-hide>
|
||||||
<div v-if="detail && detail.user" class="detail-user-bar mb12">
|
<template v-if="detail && detail.user">
|
||||||
<span class="detail-user-item">ID {{ detail.user.id }}</span>
|
<p class="detail-one-line mb12">
|
||||||
<span class="detail-user-item">{{ detail.user.name || '—' }}</span>
|
<strong>{{ detail.user.name || '—' }}</strong>
|
||||||
<span class="detail-user-item">{{ detail.user.mobile || '—' }}</span>
|
<span class="text-muted"> · ID {{ detail.user.id }} · {{ detail.user.mobile || '—' }} · {{ detail.user.status
|
||||||
<Tag>{{ detail.user.status }}</Tag>
|
}}</span>
|
||||||
<Button
|
<Button type="primary" size="small" class="ml12" :disabled="detail.user.status !== 'active'"
|
||||||
type="primary"
|
@click="open_create_token_for_user(detail.user)">生成 Token</Button>
|
||||||
size="small"
|
</p>
|
||||||
class="ml12"
|
<p class="sub-title">Token({{ (detail.tokens && detail.tokens.length) || 0 }})</p>
|
||||||
:disabled="detail.user.status !== 'active'"
|
<Table :columns="tokenCols" :data="detail.tokens || []" size="small" border
|
||||||
@click="open_create_token_for_user(detail.user)"
|
class="mb16 token-table-in-detail" />
|
||||||
>生成 Token</Button>
|
<p class="sub-title">订阅</p>
|
||||||
</div>
|
<Table v-if="detail.subscriptions && detail.subscriptions.length" :columns="subCols"
|
||||||
<p v-if="detail" class="mb8">Token 数量:{{ detail.tokenCount }}</p>
|
:data="detail.subscriptions" size="small" border />
|
||||||
<p v-if="detail && detail.tokens && detail.tokens.length" class="sub-title">API Token</p>
|
<p v-else class="text-muted">暂无订阅</p>
|
||||||
<Table v-if="detail && detail.tokens && detail.tokens.length" :columns="tokenCols" :data="detail.tokens"
|
|
||||||
size="small" border highlight-row class="mb16" @on-row-click="onTokenRowClick" />
|
|
||||||
<p v-if="detail && detail.subscriptions && detail.subscriptions.length" class="sub-title">订阅记录</p>
|
|
||||||
<Table v-if="detail && detail.subscriptions" :columns="subCols" :data="detail.subscriptions" size="small"
|
|
||||||
border />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal v-model="tokenListVisible" :title="tokenListTitle" width="980" footer-hide>
|
|
||||||
<div v-if="tokenList_target_user" class="mb12">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:disabled="tokenList_target_user.status !== 'active'"
|
|
||||||
@click="open_create_token_for_user(tokenList_target_user)"
|
|
||||||
>生成 Token</Button>
|
|
||||||
<span class="text-muted ml10">点击行查看详情;列表「明文」来自服务端加密存储(吊销后不可查看)</span>
|
|
||||||
</div>
|
|
||||||
<Table v-if="tokenListRows.length" :columns="tokenCols" :data="tokenListRows" size="small" border highlight-row
|
|
||||||
@on-row-click="onTokenRowClick" />
|
|
||||||
<p v-else class="text-muted">暂无 Token</p>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal v-model="tokenDetailVisible" title="Token 详情" width="600" footer-hide>
|
|
||||||
<div v-if="selectedToken" class="token-detail">
|
|
||||||
<p><span class="label">ID</span>{{ selectedToken.id }}</p>
|
|
||||||
<p><span class="label">名称</span>{{ selectedToken.token_name }}</p>
|
|
||||||
<p><span class="label">用户</span>{{ selectedToken.user_id }}</p>
|
|
||||||
<p><span class="label">套餐</span>{{ selectedToken.plan_id != null ? selectedToken.plan_id : '—' }}</p>
|
|
||||||
<p><span class="label">状态</span>{{ selectedToken.status }}</p>
|
|
||||||
<p><span class="label">过期时间</span>{{ selectedToken.expire_at || '—' }}</p>
|
|
||||||
<p><span class="label">最后使用</span>{{ selectedToken.last_used_at || '—' }}</p>
|
|
||||||
<div class="token-plain-block">
|
|
||||||
<p><span class="label">明文</span></p>
|
|
||||||
<template v-if="selectedToken.plain_token">
|
|
||||||
<Input type="textarea" :rows="3" :value="selectedToken.plain_token" readonly class="token-plain-ta" />
|
|
||||||
<Button type="primary" size="small" class="mt8" @click="copy_text(selectedToken.plain_token)">复制全文</Button>
|
|
||||||
</template>
|
</template>
|
||||||
<p v-else class="text-muted mb0">无存储明文(旧数据或已吊销);可对 active 记录「重新生成」后查看</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -171,11 +128,6 @@ export default {
|
|||||||
},
|
},
|
||||||
detailVisible: false,
|
detailVisible: false,
|
||||||
detail: null,
|
detail: null,
|
||||||
tokenListVisible: false,
|
|
||||||
tokenListRows: [],
|
|
||||||
tokenListUserName: '',
|
|
||||||
tokenDetailVisible: false,
|
|
||||||
selectedToken: null,
|
|
||||||
token_plain_modal: false,
|
token_plain_modal: false,
|
||||||
token_plain_title: '',
|
token_plain_title: '',
|
||||||
token_plain_text: '',
|
token_plain_text: '',
|
||||||
@@ -183,7 +135,6 @@ export default {
|
|||||||
create_token_saving: false,
|
create_token_saving: false,
|
||||||
create_token_target_user: null,
|
create_token_target_user: null,
|
||||||
create_token_form: { token_name: 'default', expire_at: '' },
|
create_token_form: { token_name: 'default', expire_at: '' },
|
||||||
tokenList_target_user: null,
|
|
||||||
subCols: [
|
subCols: [
|
||||||
{ title: 'ID', key: 'id', width: 80 },
|
{ title: 'ID', key: 'id', width: 80 },
|
||||||
{ title: '套餐ID', key: 'plan_id', width: 90 },
|
{ title: '套餐ID', key: 'plan_id', width: 90 },
|
||||||
@@ -215,11 +166,11 @@ export default {
|
|||||||
on: {
|
on: {
|
||||||
click: (e) => {
|
click: (e) => {
|
||||||
e.preventDefault()
|
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() {
|
tokenCols() {
|
||||||
return [
|
return [
|
||||||
{ title: 'ID', key: 'id', width: 72 },
|
{ title: 'ID', key: 'id', width: 56 },
|
||||||
{ title: '名称', key: 'token_name', minWidth: 100 },
|
{ title: '名称', key: 'token_name', width: 88 },
|
||||||
{ title: '套餐', key: 'plan_id', width: 80 },
|
{ title: '套餐', key: 'plan_id', width: 64 },
|
||||||
{ title: '状态', key: 'status', width: 90 },
|
{ title: '状态', key: 'status', width: 72 },
|
||||||
{ title: '过期', key: 'expire_at', minWidth: 150 },
|
{ title: '过期', key: 'expire_at', minWidth: 128 },
|
||||||
{ title: '最后使用', key: 'last_used_at', minWidth: 150 },
|
{ title: '最后使用', key: 'last_used_at', minWidth: 128 },
|
||||||
{
|
{
|
||||||
title: '明文',
|
title: '明文',
|
||||||
key: 'plain_token',
|
key: 'plain_token',
|
||||||
minWidth: 200,
|
minWidth: 280,
|
||||||
render: (h, p) => {
|
render: (h, p) => {
|
||||||
const v = p.row.plain_token
|
const v = p.row.plain_token
|
||||||
if (!v) {
|
if (!v) {
|
||||||
return h('span', { class: { 'text-muted': true } }, '—')
|
return h('span', { class: { 'text-muted': true } }, '—')
|
||||||
}
|
}
|
||||||
const snip = v.length > 28 ? `${v.slice(0, 28)}…` : v
|
return h('div', { class: 'plain-cell' }, [
|
||||||
return h('div', { class: 'plain-token-row' }, [
|
h('Input', {
|
||||||
h('span', { class: 'plain-token-snip', attrs: { title: v } }, snip),
|
props: {
|
||||||
|
type: 'textarea',
|
||||||
|
value: v,
|
||||||
|
rows: 2,
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
class: 'plain-cell-input',
|
||||||
|
}),
|
||||||
h(
|
h(
|
||||||
'Button',
|
'Button',
|
||||||
{
|
{
|
||||||
@@ -331,7 +289,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'tok_op',
|
key: 'tok_op',
|
||||||
width: 120,
|
width: 96,
|
||||||
render: (h, p) => {
|
render: (h, p) => {
|
||||||
if (p.row.status !== 'active') {
|
if (p.row.status !== 'active') {
|
||||||
return h('span', { class: { 'text-muted': true } }, '—')
|
return h('span', { class: { 'text-muted': true } }, '—')
|
||||||
@@ -340,7 +298,12 @@ export default {
|
|||||||
'Button',
|
'Button',
|
||||||
{
|
{
|
||||||
props: { type: 'warning', size: 'small' },
|
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() {
|
mounted() {
|
||||||
this.load(1)
|
this.load(1)
|
||||||
@@ -503,21 +462,6 @@ export default {
|
|||||||
this.create_token_saving = false
|
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) {
|
show_token_plain(title, text) {
|
||||||
this.token_plain_title = title
|
this.token_plain_title = title
|
||||||
this.token_plain_text = text || ''
|
this.token_plain_text = text || ''
|
||||||
@@ -527,7 +471,6 @@ export default {
|
|||||||
const res = await userServer.detail(user_id)
|
const res = await userServer.detail(user_id)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
if (this.detailVisible) this.detail = res.data
|
if (this.detailVisible) this.detail = res.data
|
||||||
if (this.tokenListVisible) this.tokenListRows = res.data.tokens || []
|
|
||||||
this.load(1)
|
this.load(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -650,59 +593,25 @@ export default {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-detail .label {
|
.token-table-in-detail :deep(.plain-cell) {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
width: 88px;
|
flex-direction: column;
|
||||||
color: #808695;
|
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;
|
margin-top: 12px;
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid #e8eaec;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-plain-block .label {
|
.detail-one-line {
|
||||||
margin-bottom: 6px;
|
line-height: 32px;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,20 @@ const { build_search_where } = require("../utils/query_helpers");
|
|||||||
const logic = require("../service/biz_subscription_logic");
|
const logic = require("../service/biz_subscription_logic");
|
||||||
const audit = require("../utils/biz_audit");
|
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 = {
|
module.exports = {
|
||||||
"POST /biz_subscription/page": async (ctx) => {
|
"POST /biz_subscription/page": async (ctx) => {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
@@ -19,8 +33,13 @@ module.exports = {
|
|||||||
offset,
|
offset,
|
||||||
limit: page_size,
|
limit: page_size,
|
||||||
order: [["id", "DESC"]],
|
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) => {
|
"GET /biz_subscription/detail": async (ctx) => {
|
||||||
const q = ctx.query || {};
|
const q = ctx.query || {};
|
||||||
@@ -95,7 +114,11 @@ module.exports = {
|
|||||||
where,
|
where,
|
||||||
limit: 10000,
|
limit: 10000,
|
||||||
order: [["id", "DESC"]],
|
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) });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user