feat: 用户创建充值优化、侧边栏导航修复、在线实例统计修复
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "无限时长",
|
||||
|
||||
Reference in New Issue
Block a user