fix: 按照developer版本修复admin应用管理页面布局
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -8,7 +8,6 @@ import { toast } from 'vue-sonner'
|
|||||||
import type { App } from './applications/data/schema'
|
import type { App } from './applications/data/schema'
|
||||||
|
|
||||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
|
||||||
import { BasicPage } from '@/components/global-layout'
|
import { BasicPage } from '@/components/global-layout'
|
||||||
import DataTable from '@/pages/admin/applications/components/data-table.vue'
|
import DataTable from '@/pages/admin/applications/components/data-table.vue'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
@@ -18,9 +17,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const apps = ref<App[]>([])
|
const apps = ref<App[]>([])
|
||||||
const tableRef = ref()
|
const searchFilter = ref('')
|
||||||
const searchQuery = ref('')
|
|
||||||
const statusFilter = ref<string>('')
|
|
||||||
|
|
||||||
const deleteDialogOpen = ref(false)
|
const deleteDialogOpen = ref(false)
|
||||||
const deleteTarget = ref<App | null>(null)
|
const deleteTarget = ref<App | null>(null)
|
||||||
@@ -30,62 +27,53 @@ const totalUsers = computed(() => apps.value.reduce((sum, app) => sum + (app.use
|
|||||||
const totalVerifyCount = computed(() => apps.value.reduce((sum, app) => sum + (app.verify_count || 0), 0))
|
const totalVerifyCount = computed(() => apps.value.reduce((sum, app) => sum + (app.verify_count || 0), 0))
|
||||||
|
|
||||||
const filteredApps = computed(() => {
|
const filteredApps = computed(() => {
|
||||||
let result = [...apps.value]
|
if (!searchFilter.value)
|
||||||
|
return apps.value
|
||||||
|
|
||||||
if (searchQuery.value) {
|
const query = searchFilter.value.toLowerCase()
|
||||||
const query = searchQuery.value.toLowerCase()
|
return apps.value.filter(app =>
|
||||||
result = result.filter(app =>
|
app.name.toLowerCase().includes(query)
|
||||||
app.name.toLowerCase().includes(query)
|
|| app.app_key?.toLowerCase().includes(query)
|
||||||
|| app.app_key?.toLowerCase().includes(query)
|
|| app.description?.toLowerCase().includes(query),
|
||||||
|| app.description?.toLowerCase().includes(query),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusFilter.value) {
|
|
||||||
result = result.filter(app => app.status === statusFilter.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchApps() {
|
async function fetchApps() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get('/dev/applications')
|
const data = await api.get<{ applications: App[] }>('/dev/applications')
|
||||||
if (data.code === 200) {
|
const applications = data?.applications || []
|
||||||
const applications = Array.isArray(data.data) ? data.data : data.data.applications || []
|
apps.value = applications.map((app: any) => ({
|
||||||
apps.value = applications.map((app: any) => ({
|
id: app.id,
|
||||||
id: app.id,
|
name: app.name,
|
||||||
name: app.name,
|
description: app.description,
|
||||||
description: app.description || '',
|
app_key: app.app_key || '',
|
||||||
app_key: app.app_key || '',
|
billing_type: app.billing_type || 'free',
|
||||||
billing_type: app.billing_type || 'free',
|
login_policy: app.login_policy || 'loose',
|
||||||
login_policy: app.login_policy || 'loose',
|
enable_trial: app.enable_trial || false,
|
||||||
enable_trial: app.enable_trial || false,
|
trial_balance: app.trial_balance || 0,
|
||||||
trial_balance: app.trial_balance || 0,
|
enable_free_period: app.enable_free_period || false,
|
||||||
enable_free_period: app.enable_free_period || false,
|
free_period_start: app.free_period_start || '',
|
||||||
free_period_start: app.free_period_start || '',
|
free_period_end: app.free_period_end || '',
|
||||||
free_period_end: app.free_period_end || '',
|
status: app.status || 'active',
|
||||||
status: app.status || 'active',
|
users: app.users || 0,
|
||||||
users: app.users || 0,
|
devices: app.devices || 0,
|
||||||
devices: app.devices || 0,
|
verify_count: app.verify_count || 0,
|
||||||
verify_count: app.verify_count || 0,
|
created_at: app.created_at || new Date(),
|
||||||
created_at: app.created_at || new Date(),
|
icon_url: app.icon_url || '',
|
||||||
icon_url: app.icon_url || '',
|
encrypt_type: app.encrypt_type || 'none',
|
||||||
encrypt_type: app.encrypt_type || 'none',
|
secret_key: app.secret_key || '',
|
||||||
secret_key: app.secret_key || '',
|
bind_type: app.bind_type || 'device',
|
||||||
bind_type: app.bind_type || 'device',
|
max_devices: app.max_devices || 1,
|
||||||
max_devices: app.max_devices || 1,
|
multi_open: app.multi_open || false,
|
||||||
multi_open: app.multi_open || false,
|
deduction_cycle: app.deduction_cycle || 'per_use',
|
||||||
deduction_cycle: app.deduction_cycle || 'per_use',
|
deduction_amount: app.deduction_amount || 1,
|
||||||
deduction_amount: app.deduction_amount || 1,
|
heartbeat_interval: app.heartbeat_interval || 60,
|
||||||
heartbeat_interval: app.heartbeat_interval || 60,
|
heartbeat_timeout: app.heartbeat_timeout || 300,
|
||||||
heartbeat_timeout: app.heartbeat_timeout || 300,
|
max_attempts: app.max_attempts || 5,
|
||||||
max_attempts: app.max_attempts || 5,
|
lock_duration: app.lock_duration || 30,
|
||||||
lock_duration: app.lock_duration || 30,
|
disabled_by_package: app.disabled_by_package || false,
|
||||||
disabled_by_package: app.disabled_by_package || false,
|
}))
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('获取应用列表失败:', error)
|
console.error('获取应用列表失败:', error)
|
||||||
@@ -95,40 +83,8 @@ async function fetchApps() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleStatus(app: App) {
|
function goToCreate() {
|
||||||
try {
|
router.push('/admin/applications/create')
|
||||||
const newStatus = app.status === 'active' ? 'inactive' : 'active'
|
|
||||||
const { data } = await api.put(`/dev/applications/${app.id}`, { status: newStatus })
|
|
||||||
if (data.code === 200) {
|
|
||||||
toast.success(t('admin.applications.statusUpdateSuccess'))
|
|
||||||
await fetchApps()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error(data.message || t('admin.applications.statusUpdateFailed'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error: any) {
|
|
||||||
toast.error(error.message || t('admin.applications.statusUpdateFailed'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteApp() {
|
|
||||||
if (!deleteTarget.value) return
|
|
||||||
try {
|
|
||||||
const { data } = await api.delete(`/dev/applications/${deleteTarget.value.id}`)
|
|
||||||
if (data.code === 200) {
|
|
||||||
toast.success(t('admin.applications.deleteSuccess'))
|
|
||||||
deleteDialogOpen.value = false
|
|
||||||
deleteTarget.value = null
|
|
||||||
await fetchApps()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error(data.message || t('admin.applications.deleteFailed'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error: any) {
|
|
||||||
toast.error(error.message || t('admin.applications.deleteFailed'))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToSettings(app: App) {
|
function goToSettings(app: App) {
|
||||||
@@ -143,11 +99,43 @@ function goToEmail(app: App) {
|
|||||||
router.push(`/admin/applications/${app.id}/email`)
|
router.push(`/admin/applications/${app.id}/email`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
async function toggleStatus(app: App) {
|
||||||
fetchApps()
|
try {
|
||||||
})
|
const newStatus = app.status === 'active' ? 'inactive' : 'active'
|
||||||
|
await api.put(`/dev/applications/${app.id}`, { status: newStatus })
|
||||||
|
toast.success(t('admin.applications.statusUpdateSuccess'))
|
||||||
|
fetchApps()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('切换应用状态失败:', error)
|
||||||
|
toast.error(error.message || t('admin.applications.statusUpdateFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onActivated(() => {
|
function confirmDeleteApp(app: App) {
|
||||||
|
deleteTarget.value = app
|
||||||
|
deleteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteApp() {
|
||||||
|
if (!deleteTarget.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/dev/applications/${deleteTarget.value.id}`)
|
||||||
|
toast.success(t('admin.applications.deleteSuccess'))
|
||||||
|
fetchApps()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('删除应用失败:', error)
|
||||||
|
toast.error(error.message || t('admin.applications.deleteFailed'))
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
deleteTarget.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
fetchApps()
|
fetchApps()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -159,104 +147,102 @@ onActivated(() => {
|
|||||||
sticky
|
sticky
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<UiButton size="sm" @click="router.push('/admin/applications/create')">
|
<UiButton @click="goToCreate">
|
||||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||||
{{ t('admin.applications.createApp') }}
|
{{ t('admin.applications.createApp') }}
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
<div class="space-y-6">
|
||||||
<UiCard>
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCard>
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
{{ t('admin.applications.totalApps') }}
|
<UiCardTitle class="text-sm font-medium">
|
||||||
</UiCardTitle>
|
{{ t('admin.applications.totalApps') }}
|
||||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
</UiCardTitle>
|
||||||
</UiCardHeader>
|
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||||
<UiCardContent>
|
</UiCardHeader>
|
||||||
<div class="text-2xl font-bold">
|
<UiCardContent>
|
||||||
{{ apps.length }}
|
<div class="text-2xl font-bold">
|
||||||
</div>
|
{{ apps.length }}
|
||||||
</UiCardContent>
|
</div>
|
||||||
</UiCard>
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('admin.applications.activeApps') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ activeAppsCount }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('admin.applications.totalUsers') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ totalUsers }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('admin.applications.totalVerifyCount') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ totalVerifyCount }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardContent class="p-6">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<DataTable
|
||||||
{{ t('admin.applications.activeApps') }}
|
:loading
|
||||||
</UiCardTitle>
|
:data="filteredApps"
|
||||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
:search-filter="searchFilter"
|
||||||
</UiCardHeader>
|
@refresh="fetchApps"
|
||||||
<UiCardContent>
|
@go-to-settings="goToSettings"
|
||||||
<div class="text-2xl font-bold">
|
@go-to-security="goToSecurity"
|
||||||
{{ activeAppsCount }}
|
@go-to-email="goToEmail"
|
||||||
</div>
|
@toggle-status="toggleStatus"
|
||||||
</UiCardContent>
|
@delete="confirmDeleteApp"
|
||||||
</UiCard>
|
@update:search-filter="searchFilter = $event"
|
||||||
|
/>
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
|
||||||
<UiCardTitle class="text-sm font-medium">
|
|
||||||
{{ t('admin.applications.totalUsers') }}
|
|
||||||
</UiCardTitle>
|
|
||||||
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<div class="text-2xl font-bold">
|
|
||||||
{{ totalUsers }}
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
|
||||||
<UiCardTitle class="text-sm font-medium">
|
|
||||||
{{ t('admin.applications.totalVerifyCount') }}
|
|
||||||
</UiCardTitle>
|
|
||||||
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<div class="text-2xl font-bold">
|
|
||||||
{{ totalVerifyCount }}
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 mb-4">
|
|
||||||
<SingleFilter
|
|
||||||
v-model="statusFilter"
|
|
||||||
:options="[
|
|
||||||
{ label: t('admin.applications.allStatus'), value: '' },
|
|
||||||
{ label: t('admin.applications.active'), value: 'active' },
|
|
||||||
{ label: t('admin.applications.inactive'), value: 'inactive' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
ref="tableRef"
|
|
||||||
:data="filteredApps"
|
|
||||||
:loading="loading"
|
|
||||||
:search-filter="searchQuery"
|
|
||||||
@refresh="fetchApps"
|
|
||||||
@go-to-settings="goToSettings"
|
|
||||||
@go-to-security="goToSecurity"
|
|
||||||
@go-to-email="goToEmail"
|
|
||||||
@toggle-status="toggleStatus"
|
|
||||||
@delete="(app: App) => { deleteTarget = app; deleteDialogOpen = true }"
|
|
||||||
@update:search-filter="searchQuery = $event"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
v-model:open="deleteDialogOpen"
|
v-model:open="deleteDialogOpen"
|
||||||
|
destructive
|
||||||
:confirm-button-text="t('admin.applications.delete')"
|
:confirm-button-text="t('admin.applications.delete')"
|
||||||
@confirm="deleteApp"
|
:cancel-button-text="t('admin.applications.create.cancelBtn')"
|
||||||
|
@confirm="handleDeleteApp"
|
||||||
>
|
>
|
||||||
<UiAlertDialogTitle>{{ t('admin.applications.deleteApp') }}</UiAlertDialogTitle>
|
<template #title>
|
||||||
<UiAlertDialogDescription>
|
{{ t('admin.applications.deleteApp') }}
|
||||||
{{ t('admin.applications.deleteAppConfirm') }}
|
</template>
|
||||||
</UiAlertDialogDescription>
|
<template #description>
|
||||||
|
{{ t('admin.applications.deleteAppConfirm', { name: deleteTarget?.name }) }}
|
||||||
|
</template>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</BasicPage>
|
</BasicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user