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
+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>