feat: 用户创建充值优化、侧边栏导航修复、在线实例统计修复

This commit is contained in:
2026-05-03 06:07:11 +08:00
parent fa521de836
commit ecf5690505
8 changed files with 331 additions and 84 deletions
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, Mail, Megaphone, MessageSquare, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next'
import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next'
import { onMounted, onUnmounted, reactive } from 'vue'
import NavTeam from '@/components/app-sidebar/nav-team.vue'
@@ -119,6 +119,16 @@ const navMain = [
url: '/admin/users',
icon: Users,
},
{
title: '设备管理',
url: '/admin/devices',
icon: Smartphone,
},
{
title: '在线实例',
url: '/admin/sessions',
icon: Monitor,
},
{
title: '代理管理',
url: '/admin/agents',
@@ -23,13 +23,13 @@ function getAssetUrl(path: string) {
<UiSidebarMenuItem>
<UiSidebarMenuButton size="lg" class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<div
class="flex items-center justify-center rounded-lg aspect-square size-8 overflow-hidden"
class="flex items-center justify-center rounded-lg aspect-square size-8 overflow-hidden shrink-0"
:class="typeof activeTeam.logo === 'string' && activeTeam.logo ? '' : 'bg-sidebar-primary text-sidebar-primary-foreground'"
>
<img v-if="typeof activeTeam.logo === 'string' && activeTeam.logo" :src="getAssetUrl(activeTeam.logo)" class="size-full object-contain" alt="" />
<component v-else-if="typeof activeTeam.logo === 'function'" :is="activeTeam.logo" class="size-4" />
</div>
<div class="grid flex-1 text-sm leading-tight">
<div class="grid flex-1 text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span class="font-semibold truncate">{{ activeTeam.name }}</span>
</div>
</UiSidebarMenuButton>
+5 -4
View File
@@ -72,8 +72,9 @@ const filteredSessions = computed(() => {
})
const totalSessions = computed(() => filteredSessions.value.length)
const onlineDevices = computed(() => new Set(filteredSessions.value.map(s => s.device_identifier)).size)
const onlineUsers = computed(() => new Set(filteredSessions.value.map(s => s.username)).size)
const onlineApps = computed(() => new Set(filteredSessions.value.map(s => s.application_id)).size)
const onlineDevices = computed(() => new Set(filteredSessions.value.filter(s => s.device_identifier).map(s => s.device_identifier)).size)
const onlineUsers = computed(() => new Set(filteredSessions.value.filter(s => s.username).map(s => s.username)).size)
async function fetchApplications() {
try {
@@ -159,13 +160,13 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
应用数
在线应用数
</UiCardTitle>
<Boxes class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ applications.length }}
{{ onlineApps }}
</div>
</UiCardContent>
</UiCard>
+131 -12
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Eye, Loader2, UserPlus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { CreditCard, Eye, Loader2, UserPlus } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -16,14 +16,27 @@ interface Application {
name: string
}
interface CardType {
id: number
name: string
recharge_type: string
value: number
value_unit: string
price: number
description: string
}
const saving = ref(false)
const applications = ref<Application[]>([])
const cardTypes = ref<CardType[]>([])
const form = ref({
username: '',
email: '',
password: '',
application_id: '',
card_type_id: 'none',
card_quantity: 1,
})
async function fetchApplications() {
@@ -36,6 +49,26 @@ async function fetchApplications() {
}
}
async function fetchCardTypes(applicationId: string) {
if (!applicationId) {
cardTypes.value = []
return
}
try {
const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${applicationId}`)
cardTypes.value = Array.isArray(data?.card_types) ? data.card_types : []
}
catch (error) {
console.error('获取卡密类型失败:', error)
cardTypes.value = []
}
}
watch(() => form.value.application_id, (newVal) => {
form.value.card_type_id = 'none'
fetchCardTypes(newVal)
})
const selectedApplication = computed(() => {
if (form.value.application_id) {
return applications.value.find(app => String(app.id) === form.value.application_id)
@@ -43,8 +76,31 @@ const selectedApplication = computed(() => {
return null
})
const selectedCardType = computed(() => {
if (form.value.card_type_id && form.value.card_type_id !== 'none') {
return cardTypes.value.find(ct => String(ct.id) === form.value.card_type_id)
}
return null
})
function formatCardTypeValue(ct: CardType) {
if (ct.value === -1)
return t('admin.users.create.permanent')
if (ct.recharge_type === 'subscription') {
const unitMap: Record<string, string> = {
minute: t('admin.users.create.minutes'),
hour: t('admin.users.create.hours'),
day: t('admin.users.create.days'),
month: t('admin.users.create.months'),
year: t('admin.users.create.years'),
}
return `${ct.value} ${unitMap[ct.value_unit] || ct.value_unit}`
}
return `${ct.value} ${t('admin.users.create.points')}`
}
const isFormValid = computed(() => {
return form.value.username && form.value.email && form.value.password && form.value.application_id
return form.value.username && form.value.password && form.value.application_id
})
async function handleSave() {
@@ -52,10 +108,6 @@ async function handleSave() {
toast.error(t('admin.users.create.usernameRequired'))
return
}
if (!form.value.email) {
toast.error(t('admin.users.create.emailRequired'))
return
}
if (!form.value.password) {
toast.error(t('admin.users.create.passwordRequired'))
return
@@ -67,12 +119,17 @@ async function handleSave() {
saving.value = true
try {
await api.post('/dev/app-users', {
const payload: any = {
username: form.value.username,
email: form.value.email,
password: form.value.password,
application_id: Number(form.value.application_id),
})
}
if (form.value.card_type_id && form.value.card_type_id !== 'none') {
payload.card_type_id = Number(form.value.card_type_id)
payload.card_quantity = form.value.card_quantity || 1
}
await api.post('/dev/app-users', payload)
toast.success(t('admin.users.create.success'))
router.push('/admin/users')
}
@@ -135,18 +192,76 @@ onMounted(() => {
<UiInput id="username" v-model="form.username" :placeholder="t('admin.users.create.usernamePlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel for="password">
{{ t('admin.users.create.password') }}
</UiLabel>
<UiInput id="password" v-model="form.password" type="password" :placeholder="t('admin.users.create.passwordPlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel for="email">
{{ t('admin.users.create.email') }}
</UiLabel>
<UiInput id="email" v-model="form.email" type="email" :placeholder="t('admin.users.create.emailPlaceholder')" />
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<CreditCard class="size-5" />
{{ t('admin.users.create.rechargeCard') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.users.create.rechargeCardDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="password">
{{ t('admin.users.create.password') }}
<UiLabel for="card_type">
{{ t('admin.users.create.cardType') }}
</UiLabel>
<UiInput id="password" v-model="form.password" type="password" :placeholder="t('admin.users.create.passwordPlaceholder')" />
<UiSelect v-model="form.card_type_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="cardTypes.length > 0 ? t('admin.users.create.selectCardType') : t('admin.users.create.selectApplicationFirst')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="none">
{{ t('admin.users.create.noRecharge') }}
</UiSelectItem>
<UiSelectItem v-for="ct in cardTypes" :key="ct.id" :value="String(ct.id)">
{{ ct.name }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div v-if="selectedCardType" class="space-y-2">
<UiLabel for="card_quantity">
{{ t('admin.users.create.cardQuantity') }}
</UiLabel>
<UiNumberField v-model="form.card_quantity" :min="1" :max="100" class="max-w-[200px]">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
</div>
<div v-if="selectedCardType" class="rounded-lg border bg-muted/50 p-3 space-y-2">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.cardType') }}</span>
<span>{{ selectedCardType.name }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.cardValue') }}</span>
<span>{{ formatCardTypeValue(selectedCardType) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.cardQuantity') }}</span>
<span>{{ form.card_quantity }}</span>
</div>
</div>
</UiCardContent>
</UiCard>
@@ -174,6 +289,10 @@ onMounted(() => {
<span class="text-muted-foreground">{{ t('admin.users.create.emailLabel') }}</span>
<span>{{ form.email || '-' }}</span>
</div>
<div v-if="selectedCardType" class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.rechargeCard') }}</span>
<span class="text-primary">{{ selectedCardType.name }} x{{ form.card_quantity }}</span>
</div>
</div>
</UiCardContent>
</UiCard>
+19 -30
View File
@@ -34,7 +34,8 @@
"previous": "Previous",
"next": "Next",
"yes": "Yes",
"no": "No"
"no": "No",
"optional": "Optional"
},
"premium": {
"premium": "premium",
@@ -294,7 +295,23 @@
"applicationRequired": "Please select application",
"success": "User created successfully",
"failed": "Failed to create user",
"saveBtn": "Save Changes"
"saveBtn": "Save Changes",
"rechargeCard": "Recharge Card",
"rechargeCardDesc": "Optional, generate cards and auto-recharge when creating user",
"cardType": "Card Type",
"selectCardType": "Select card type",
"selectApplicationFirst": "Select application first",
"noRecharge": "No recharge",
"cardQuantity": "Quantity",
"cardQuantityPlaceholder": "Enter quantity",
"cardValue": "Recharge Value",
"permanent": "Permanent",
"points": "Points",
"minutes": "Minutes",
"hours": "Hours",
"days": "Days",
"months": "Months",
"years": "Years"
},
"editFailed": "Failed to load user info",
"editSuccess": "User updated successfully",
@@ -370,34 +387,6 @@
"banned": "Banned"
},
"lastLoginTime": "Last Login",
"create": {
"title": "Add User",
"basicInfo": "Basic Information",
"basicInfoDesc": "Fill in the user's basic information",
"application": "Application",
"selectApplication": "Select application",
"username": "Username",
"usernamePlaceholder": "Enter username",
"email": "Email",
"emailPlaceholder": "Enter email",
"password": "Password",
"passwordPlaceholder": "Enter password",
"deviceId": "Device ID",
"deviceIdPlaceholder": "Device ID (optional)",
"preview": "Preview",
"app": "Application",
"usernameLabel": "Username",
"emailLabel": "Email",
"deviceIdLabel": "Device ID",
"submit": "Add User",
"cancel": "Cancel",
"usernameRequired": "Please enter username",
"emailRequired": "Please enter email",
"passwordRequired": "Please enter password",
"applicationRequired": "Please select application",
"success": "User created successfully",
"failed": "Failed to create user"
},
"expiry": {
"permanent": "Unlimited",
"unlimitedTime": "Unlimited Time",
+19 -30
View File
@@ -34,7 +34,8 @@
"previous": "上一页",
"next": "下一页",
"yes": "是",
"no": "否"
"no": "否",
"optional": "可选"
},
"premium": {
"premium": "会员计划",
@@ -294,7 +295,23 @@
"applicationRequired": "请选择应用",
"success": "创建成功",
"failed": "创建失败",
"saveBtn": "保存修改"
"saveBtn": "保存修改",
"rechargeCard": "充值卡密",
"rechargeCardDesc": "可选,创建用户时同时生成卡密并自动充值",
"cardType": "卡密类型",
"selectCardType": "选择卡密类型",
"selectApplicationFirst": "请先选择应用",
"noRecharge": "不充值",
"cardQuantity": "卡密数量",
"cardQuantityPlaceholder": "输入数量",
"cardValue": "充值内容",
"permanent": "永久",
"points": "点",
"minutes": "分钟",
"hours": "小时",
"days": "天",
"months": "月",
"years": "年"
},
"editFailed": "获取用户信息失败",
"editSuccess": "更新用户成功",
@@ -371,34 +388,6 @@
"banned": "已封禁"
},
"lastLoginTime": "最后登录",
"create": {
"title": "添加用户",
"basicInfo": "基本信息",
"basicInfoDesc": "填写用户的基本信息",
"application": "所属应用",
"selectApplication": "选择应用",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱",
"password": "密码",
"passwordPlaceholder": "请输入密码",
"deviceId": "设备指纹",
"deviceIdPlaceholder": "设备指纹(可选)",
"preview": "预览",
"app": "应用",
"usernameLabel": "用户名",
"emailLabel": "邮箱",
"deviceIdLabel": "设备指纹",
"submit": "添加用户",
"cancel": "取消",
"usernameRequired": "请填写用户名",
"emailRequired": "请填写邮箱",
"passwordRequired": "请填写密码",
"applicationRequired": "请选择所属应用",
"success": "创建成功",
"failed": "创建失败"
},
"expiry": {
"permanent": "无限制",
"unlimitedTime": "无限时长",