fix: 应用管理页面从卡片布局改为表格布局
This commit is contained in:
@@ -1,27 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { TSort } from '@/components/sort-select/types'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import SortSelect from '@/components/sort-select/index.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { App } from './applications/data/schema'
|
||||
|
||||
import AppCard from './applications/components/app-card.vue'
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/admin/applications/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const API_BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
const loading = ref(true)
|
||||
const apps = ref<App[]>([])
|
||||
const tableRef = ref()
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref<'all' | 'active' | 'inactive'>('all')
|
||||
const sort = ref<TSort>('asc')
|
||||
const statusFilter = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<App | null>(null)
|
||||
|
||||
const activeAppsCount = computed(() => apps.value.filter(app => app.status === 'active').length)
|
||||
const totalUsers = computed(() => apps.value.reduce((sum, app) => sum + (app.users || 0), 0))
|
||||
@@ -39,43 +41,23 @@ const filteredApps = computed(() => {
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value !== 'all') {
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(app => app.status === statusFilter.value)
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
if (a.disabled_by_package !== b.disabled_by_package) {
|
||||
return a.disabled_by_package ? 1 : -1
|
||||
}
|
||||
const timeA = new Date(a.created_at || 0).getTime()
|
||||
const timeB = new Date(b.created_at || 0).getTime()
|
||||
if (sort.value === 'asc') {
|
||||
return timeA - timeB
|
||||
}
|
||||
else {
|
||||
return timeB - timeA
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
async function fetchApps() {
|
||||
loading.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch(`${API_BASE}/dev/applications`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const data = await response.json()
|
||||
const { data } = await api.get('/dev/applications')
|
||||
if (data.code === 200) {
|
||||
const applications = Array.isArray(data.data) ? data.data : data.data.applications || []
|
||||
apps.value = applications.map((app: any) => ({
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
description: app.description || '',
|
||||
app_key: app.app_key || '',
|
||||
billing_type: app.billing_type || 'free',
|
||||
login_policy: app.login_policy || 'loose',
|
||||
@@ -113,9 +95,61 @@ async function fetchApps() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleStatus(app: App) {
|
||||
try {
|
||||
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) {
|
||||
router.push(`/admin/applications/${app.id}/settings`)
|
||||
}
|
||||
|
||||
function goToSecurity(app: App) {
|
||||
router.push(`/admin/applications/${app.id}/security`)
|
||||
}
|
||||
|
||||
function goToEmail(app: App) {
|
||||
router.push(`/admin/applications/${app.id}/email`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchApps()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
fetchApps()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -189,62 +223,40 @@ onMounted(() => {
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between sm:items-center mb-4">
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<UiInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="t('admin.applications.searchPlaceholder')"
|
||||
class="h-9 w-40 lg:w-[250px]"
|
||||
/>
|
||||
|
||||
<UiSelect v-model="statusFilter">
|
||||
<UiSelectTrigger class="w-32">
|
||||
<UiSelectValue :placeholder="t('admin.applications.statusFilter')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="all">
|
||||
{{ t('admin.applications.allStatus') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="active">
|
||||
{{ t('admin.applications.active') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="inactive">
|
||||
{{ t('admin.applications.inactive') }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<SortSelect v-model:sort="sort" />
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredApps.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="size-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||
<Icon icon="lucide:layout-grid" class="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ t('admin.applications.noApps') }}
|
||||
</h3>
|
||||
<p class="text-muted-foreground mt-1 mb-4">
|
||||
{{ searchQuery || statusFilter !== 'all' ? t('admin.applications.noMatch') : t('admin.applications.createFirst') }}
|
||||
</p>
|
||||
<UiButton v-if="!searchQuery && statusFilter === 'all'" @click="router.push('/admin/applications/create')">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.applications.createApp') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<main v-else class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<AppCard
|
||||
v-for="app in filteredApps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
@refresh="fetchApps"
|
||||
<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' },
|
||||
]"
|
||||
/>
|
||||
</main>
|
||||
</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
|
||||
v-model:open="deleteDialogOpen"
|
||||
:confirm-button-text="t('admin.applications.delete')"
|
||||
@confirm="deleteApp"
|
||||
>
|
||||
<UiAlertDialogTitle>{{ t('admin.applications.deleteApp') }}</UiAlertDialogTitle>
|
||||
<UiAlertDialogDescription>
|
||||
{{ t('admin.applications.deleteAppConfirm') }}
|
||||
</UiAlertDialogDescription>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function createRouterGuard(router: Router) {
|
||||
}
|
||||
}
|
||||
|
||||
const requiresAuth = to.path.startsWith('/admin') || to.path.startsWith('/agent')
|
||||
const requiresAuth = to.path.startsWith('/developer') || to.path.startsWith('/agent')
|
||||
const isAuthPage = to.path === '/login' || to.path === '/register'
|
||||
|
||||
if (requiresAuth && !token) {
|
||||
@@ -30,7 +30,7 @@ export function createRouterGuard(router: Router) {
|
||||
if (user.role === 'agent' || user.parent_agent_id) {
|
||||
return '/agent'
|
||||
}
|
||||
return '/admin'
|
||||
return '/developer'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user