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