feat:表格优化
This commit is contained in:
1 parent
d14e5b00e1
commit
ba88fb6bf0
15 files changed
+363
-256
No files matched your search
@@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [page: number]
|
||||
change: [page: number]
|
||||
}>()
|
||||
|
||||
const jumpPage = ref('')
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
|
||||
|
||||
const pageItems = computed(() => {
|
||||
const count = totalPages.value
|
||||
if (count <= 7) {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
key: `page-${index + 1}`,
|
||||
page: index + 1,
|
||||
label: String(index + 1),
|
||||
}))
|
||||
}
|
||||
|
||||
const pages = new Set([1, count, props.modelValue - 1, props.modelValue, props.modelValue + 1])
|
||||
const visiblePages = [...pages].filter(page => page >= 1 && page <= count).sort((a, b) => a - b)
|
||||
const items: Array<{ key: string; page?: number; label: string }> = []
|
||||
|
||||
visiblePages.forEach((page, index) => {
|
||||
const previous = visiblePages[index - 1]
|
||||
if (previous && page - previous > 1) {
|
||||
items.push({ key: `ellipsis-${previous}`, label: '...' })
|
||||
}
|
||||
items.push({ key: `page-${page}`, page, label: String(page) })
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
function goToPage(page: number) {
|
||||
const target = Math.min(totalPages.value, Math.max(1, Math.trunc(page)))
|
||||
if (target === props.modelValue) return
|
||||
emit('update:modelValue', target)
|
||||
emit('change', target)
|
||||
}
|
||||
|
||||
function submitJump() {
|
||||
const target = Number(jumpPage.value)
|
||||
if (!Number.isFinite(target)) return
|
||||
goToPage(target)
|
||||
jumpPage.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="total > 0" class="table-pagination">
|
||||
<span>共 {{ total }} 条记录</span>
|
||||
|
||||
<div class="pagination-controls">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="modelValue <= 1"
|
||||
@click="goToPage(modelValue - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<nav class="pagination-pages" aria-label="分页">
|
||||
<template v-for="item in pageItems" :key="item.key">
|
||||
<button
|
||||
v-if="item.page"
|
||||
type="button"
|
||||
class="pagination-page"
|
||||
:class="{ 'pagination-page-active': item.page === modelValue }"
|
||||
:aria-current="item.page === modelValue ? 'page' : undefined"
|
||||
@click="goToPage(item.page)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
<span v-else class="pagination-ellipsis">{{ item.label }}</span>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="modelValue >= totalPages"
|
||||
@click="goToPage(modelValue + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
|
||||
<form class="pagination-jump" @submit.prevent="submitJump">
|
||||
<label for="pagination-jump-input">跳至</label>
|
||||
<input
|
||||
id="pagination-jump-input"
|
||||
v-model="jumpPage"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:min="1"
|
||||
:max="totalPages"
|
||||
class="pagination-jump-input"
|
||||
aria-label="跳转页码"
|
||||
/>
|
||||
<span>页</span>
|
||||
<button type="submit" class="btn btn-secondary btn-sm">跳转</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pagination-controls,
|
||||
.pagination-pages,
|
||||
.pagination-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pagination-pages {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pagination-page {
|
||||
display: inline-flex;
|
||||
height: 2rem;
|
||||
min-width: 2rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(226, 232, 240, 0.82);
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.pagination-page:hover {
|
||||
border-color: rgba(117, 104, 248, 0.45);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #6558ea;
|
||||
}
|
||||
|
||||
.pagination-page-active {
|
||||
border-color: #7568f8;
|
||||
background: #7568f8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination-page-active:hover {
|
||||
background: #6558ea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination-ellipsis {
|
||||
min-width: 1.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.pagination-jump {
|
||||
gap: 0.375rem;
|
||||
margin-left: 0.25rem;
|
||||
white-space: nowrap;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pagination-jump-input {
|
||||
height: 2rem;
|
||||
width: 3.5rem;
|
||||
border: 1px solid rgba(203, 213, 225, 0.82);
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
padding: 0 0.375rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pagination-jump-input:focus {
|
||||
border-color: #7568f8;
|
||||
box-shadow: 0 0 0 3px rgba(117, 104, 248, 0.12);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pagination-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-pages {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.pagination-controls,
|
||||
.pagination-jump {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-controls,
|
||||
.pagination-pages,
|
||||
.pagination-jump {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@ declare module 'vue-router' {
|
||||
icon?: string
|
||||
sort?: number
|
||||
roles?: string[]
|
||||
viewportTable?: boolean
|
||||
description?: string
|
||||
keywords?: string
|
||||
}
|
||||
@@ -65,13 +66,13 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
path: 'members',
|
||||
name: 'TenantMembers',
|
||||
component: () => import('@/views/team/MembersPage.vue'),
|
||||
meta: { title: '成员管理', icon: 'users', sort: 20, roles: ['owner', 'admin'] },
|
||||
meta: { title: '成员管理', icon: 'users', sort: 20, roles: ['owner', 'admin'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'members/invitations',
|
||||
name: 'TenantInvitations',
|
||||
component: () => import('@/views/team/InvitationsPage.vue'),
|
||||
meta: { title: '邀请记录', roles: ['owner', 'admin'] },
|
||||
meta: { title: '邀请记录', roles: ['owner', 'admin'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'members/:id',
|
||||
@@ -83,7 +84,7 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
path: 'projects',
|
||||
name: 'TenantProjects',
|
||||
component: () => import('@/views/team/ProjectsPage.vue'),
|
||||
meta: { title: '项目管理', icon: 'project', sort: 21, roles: ['owner', 'admin'] },
|
||||
meta: { title: '项目管理', icon: 'project', sort: 21, roles: ['owner', 'admin'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'projects/:id',
|
||||
@@ -108,25 +109,25 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
path: 'api-keys',
|
||||
name: 'TenantApiKeys',
|
||||
component: () => import('@/views/service/ApiKeysPage.vue'),
|
||||
meta: { title: 'API 密钥', icon: 'key', sort: 32, roles: ['owner', 'admin', 'member'] },
|
||||
meta: { title: 'API 密钥', icon: 'key', sort: 32, roles: ['owner', 'admin', 'member'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'usage-logs',
|
||||
name: 'TenantUsageLogs',
|
||||
component: () => import('@/views/service/UsageLogsPage.vue'),
|
||||
meta: { title: '用量日志', icon: 'chart', sort: 33, roles: ['owner', 'admin', 'member'] },
|
||||
meta: { title: '用量日志', icon: 'chart', sort: 33, roles: ['owner', 'admin', 'member'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'request-audit-logs',
|
||||
name: 'TenantRequestAuditLogs',
|
||||
component: () => import('@/views/service/RequestAuditLogsPage.vue'),
|
||||
meta: { title: '请求审计日志', icon: 'clipboard', sort: 34, roles: ['owner', 'admin'] },
|
||||
meta: { title: '请求审计日志', icon: 'clipboard', sort: 34, roles: ['owner', 'admin'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'task-logs',
|
||||
name: 'TenantTaskLogs',
|
||||
component: () => import('@/views/service/TaskLogsPage.vue'),
|
||||
meta: { title: '任务日志', icon: 'clock', sort: 35, roles: ['owner', 'admin', 'member'] },
|
||||
meta: { title: '任务日志', icon: 'clock', sort: 35, roles: ['owner', 'admin', 'member'], viewportTable: true },
|
||||
},
|
||||
// 财务 (40-49)
|
||||
{
|
||||
@@ -139,13 +140,13 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
path: 'transactions',
|
||||
name: 'TenantTransactions',
|
||||
component: () => import('@/views/finance/TransactionsPage.vue'),
|
||||
meta: { title: '交易记录', icon: 'document', sort: 41, roles: ['owner', 'admin'] },
|
||||
meta: { title: '交易记录', icon: 'document', sort: 41, roles: ['owner', 'admin'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'TenantOrders',
|
||||
component: () => import('@/views/finance/OrdersPage.vue'),
|
||||
meta: { title: '订单记录', icon: 'receipt', sort: 42, roles: ['owner', 'admin'] },
|
||||
meta: { title: '订单记录', icon: 'receipt', sort: 42, roles: ['owner', 'admin'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'redeem',
|
||||
@@ -176,7 +177,7 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
path: 'tickets',
|
||||
name: 'TenantTickets',
|
||||
component: () => import('@/views/support/TicketsPage.vue'),
|
||||
meta: { title: '工单中心', icon: 'ticket', sort: 51, roles: ['owner', 'admin', 'member'] },
|
||||
meta: { title: '工单中心', icon: 'ticket', sort: 51, roles: ['owner', 'admin', 'member'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'feedback',
|
||||
@@ -207,7 +208,7 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
path: 'login-history',
|
||||
name: 'TenantLoginHistory',
|
||||
component: () => import('@/views/settings/LoginHistoryPage.vue'),
|
||||
meta: { title: '登录历史', roles: ['owner', 'admin', 'member'] },
|
||||
meta: { title: '登录历史', roles: ['owner', 'admin', 'member'], viewportTable: true },
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
|
||||
@@ -384,6 +384,7 @@ body.modal-open {
|
||||
scrollbar-color: rgba(117, 104, 248, 0.28) transparent;
|
||||
}
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
@@ -395,6 +396,65 @@ body.modal-open {
|
||||
background: linear-gradient(90deg, rgba(65, 168, 244, 0.4), rgba(117, 104, 248, 0.42));
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Desktop list pages fill the viewport; only their table rows scroll. */
|
||||
@media (min-width: 1024px) {
|
||||
.tenant-layout {
|
||||
display: flex;
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tenant-shell {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tenant-main {
|
||||
min-height: 0;
|
||||
flex: 1 1 0%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.tenant-table-main {
|
||||
overflow: hidden;
|
||||
}
|
||||
.tenant-table-main > .viewport-table-page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.viewport-table-page,
|
||||
.viewport-table-panel,
|
||||
.viewport-table-content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
.viewport-table-panel {
|
||||
flex: 1 1 0%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.viewport-table-content {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.viewport-table-scroll {
|
||||
min-height: 0;
|
||||
flex: 1 1 0%;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.viewport-table-scroll .table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.viewport-table-panel > .table-pagination,
|
||||
.viewport-table-content > .table-pagination {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
.table-container-flush {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
|
||||
@@ -242,7 +242,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen overflow-x-hidden">
|
||||
<div class="tenant-layout min-h-screen overflow-x-hidden">
|
||||
<!-- Maintenance Banner -->
|
||||
<MaintenanceBanner />
|
||||
<AnnouncementBanner v-if="consoleAnnouncements.length" :announcements="consoleAnnouncements" />
|
||||
@@ -323,7 +323,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div
|
||||
class="relative min-h-screen transition-all duration-300"
|
||||
class="tenant-shell relative min-h-screen transition-all duration-300"
|
||||
:class="[sidebarCollapsed ? 'lg:ml-[116px]' : 'lg:ml-[296px]']"
|
||||
>
|
||||
<!-- Header -->
|
||||
@@ -519,7 +519,10 @@ onBeforeUnmount(() => {
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="px-3 pb-5 pt-4 md:px-5 md:pb-6 lg:pt-5">
|
||||
<main
|
||||
class="tenant-main px-3 pb-5 pt-4 md:px-5 md:pb-6 lg:pt-5"
|
||||
:class="{ 'tenant-table-main': route.meta.viewportTable }"
|
||||
>
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -90,24 +91,13 @@ async function handleCancel(order: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
const totalPages = ref(0)
|
||||
|
||||
function updateTotalPages() {
|
||||
totalPages.value = Math.ceil(total.value / pageSize)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="viewport-table-page">
|
||||
<div class="page-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -156,8 +146,8 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div v-else class="card p-0 overflow-hidden">
|
||||
<div class="table-container">
|
||||
<div v-else class="viewport-table-panel card p-0 overflow-hidden">
|
||||
<div class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -209,27 +199,7 @@ onMounted(() => {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="totalPages > 1" class="table-pagination">
|
||||
<span class="text-xs text-gray-500">共 {{ total }} 条记录</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="handlePageChange(page - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="text-sm text-gray-600">{{ page }} / {{ totalPages }}</span>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="page >= totalPages"
|
||||
@click="handlePageChange(page + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchOrders" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseSelect from '@/components/common/BaseSelect.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -115,25 +116,11 @@ function resetFilters() {
|
||||
fetchTransactions()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
fetchTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value * pageSize < total.value) {
|
||||
page.value++
|
||||
fetchTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchTransactions)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
@@ -198,12 +185,12 @@ onMounted(fetchTransactions)
|
||||
</div>
|
||||
|
||||
<!-- Transactions -->
|
||||
<div class="card">
|
||||
<div class="viewport-table-panel card">
|
||||
<div v-if="loading" class="p-8 flex justify-center">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="transactions.length > 0" class="table-container table-container-flush">
|
||||
<div v-else-if="transactions.length > 0" class="viewport-table-scroll table-container table-container-flush">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -244,16 +231,7 @@ onMounted(fetchTransactions)
|
||||
<p class="empty-state-description">交易记录将在 API 调用和充值后展示</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="total > pageSize" class="table-pagination">
|
||||
<p class="text-sm text-gray-500">
|
||||
第 {{ page }} / {{ Math.ceil(total / pageSize) }} 页
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-secondary btn-sm" :disabled="page <= 1" @click="prevPage">上一页</button>
|
||||
<button class="btn btn-secondary btn-sm" :disabled="page * pageSize >= total" @click="nextPage">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchTransactions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ApiKeyEditModal from '@/components/common/ApiKeyEditModal.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import type { ApiKeyData } from '@/components/common/ApiKeyEditModal.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import request from '@/utils/request'
|
||||
@@ -146,7 +147,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
@@ -174,12 +175,12 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Keys Table -->
|
||||
<div class="card">
|
||||
<div class="viewport-table-panel card">
|
||||
<div v-if="loading" class="p-8 flex justify-center">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="keys.length > 0" class="table-container">
|
||||
<div v-else-if="keys.length > 0" class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -255,14 +256,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">创建第一个密钥以开始使用 AI 模型</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="total > pageSize" class="table-pagination card-footer flex justify-end">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="page <= 1" @click="page--; fetchKeys()">上一页</button>
|
||||
<span class="text-sm text-gray-500">{{ page }} / {{ Math.ceil(total / pageSize) }}</span>
|
||||
<button class="btn btn-ghost btn-sm" :disabled="page * pageSize >= total" @click="page++; fetchKeys()">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchKeys" />
|
||||
</div>
|
||||
|
||||
<!-- Create Modal -->
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
interface RequestLog {
|
||||
@@ -26,7 +27,6 @@ const logsLoading = ref(false)
|
||||
const logPage = ref(1)
|
||||
const logPageSize = 20
|
||||
const logTotal = ref(0)
|
||||
const logTotalPages = computed(() => Math.ceil(logTotal.value / logPageSize))
|
||||
|
||||
const logFilter = reactive({
|
||||
username: '',
|
||||
@@ -153,11 +153,6 @@ async function fetchDetail(id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogPageChange(newPage: number) {
|
||||
logPage.value = newPage
|
||||
fetchRequestLogs()
|
||||
}
|
||||
|
||||
function handleFilter() {
|
||||
logPage.value = 1
|
||||
fetchRequestLogs()
|
||||
@@ -188,7 +183,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
@@ -238,7 +233,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Table -->
|
||||
<div class="card p-0 overflow-hidden">
|
||||
<div class="viewport-table-panel card p-0 overflow-hidden">
|
||||
<!-- Loading -->
|
||||
<div v-if="logsLoading" class="p-8 text-center">
|
||||
<div class="spinner mx-auto mb-3"></div>
|
||||
@@ -253,8 +248,8 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div v-else>
|
||||
<div class="table-container table-container-flush request-audit-table">
|
||||
<div v-else class="viewport-table-content">
|
||||
<div class="viewport-table-scroll table-container table-container-flush request-audit-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -318,27 +313,7 @@ onMounted(() => {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="logTotalPages > 1" class="table-pagination">
|
||||
<span class="text-xs text-gray-500">共 {{ logTotal }} 条记录</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="logPage <= 1"
|
||||
@click="handleLogPageChange(logPage - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="text-sm text-gray-600">{{ logPage }} / {{ logTotalPages }}</span>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="logPage >= logTotalPages"
|
||||
@click="handleLogPageChange(logPage + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="logPage" :page-size="logPageSize" :total="logTotal" @change="fetchRequestLogs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
@@ -32,7 +33,6 @@ const tasks = ref<TaskItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const total = ref(0)
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize))
|
||||
|
||||
const filterStatus = ref('')
|
||||
const filterPlatform = ref('')
|
||||
@@ -141,14 +141,6 @@ function resetFilters() {
|
||||
fetchTasks()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) { page.value--; fetchTasks() }
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value * pageSize < total.value) { page.value++; fetchTasks() }
|
||||
}
|
||||
|
||||
function isImageResult(url: string | undefined): boolean {
|
||||
if (!url) return false
|
||||
return /\.(jpg|jpeg|png|gif|webp)(\?|$)/i.test(url)
|
||||
@@ -164,7 +156,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
@@ -207,7 +199,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card p-0 overflow-hidden">
|
||||
<div class="viewport-table-panel card p-0 overflow-hidden">
|
||||
<div v-if="loading" class="p-8 text-center">
|
||||
<div class="spinner mx-auto mb-3"></div>
|
||||
<p class="text-sm text-gray-500">加载中...</p>
|
||||
@@ -219,8 +211,8 @@ onMounted(() => {
|
||||
<p class="empty-state-description">异步生成任务的执行记录将显示在这里</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="table-container">
|
||||
<div v-else class="viewport-table-content">
|
||||
<div class="viewport-table-scroll table-container">
|
||||
<table class="table table-fixed w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -271,15 +263,7 @@ onMounted(() => {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="totalPages > 1" class="table-pagination">
|
||||
<span class="text-xs text-gray-500">共 {{ total }} 条记录</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="page <= 1" @click="prevPage">上一页</button>
|
||||
<span class="text-sm text-gray-600">{{ page }} / {{ totalPages }}</span>
|
||||
<button class="btn btn-ghost btn-sm" :disabled="page >= totalPages" @click="nextPage">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchTasks" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
@@ -137,14 +138,6 @@ function resetFilters() {
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) { page.value--; fetchLogs() }
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value * pageSize < total.value) { page.value++; fetchLogs() }
|
||||
}
|
||||
|
||||
function openDetail(log: any) {
|
||||
detailLog.value = log
|
||||
detailModal.value = true
|
||||
@@ -232,7 +225,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
@@ -289,12 +282,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Logs Table -->
|
||||
<div class="card overflow-hidden">
|
||||
<div class="viewport-table-panel card overflow-hidden">
|
||||
<div v-if="loading" class="p-8 flex justify-center">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="logs.length > 0" class="table-container table-container-flush usage-log-table">
|
||||
<div v-else-if="logs.length > 0" class="viewport-table-scroll table-container table-container-flush usage-log-table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -447,16 +440,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">日志将在 API 调用后展示</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="total > pageSize" class="table-pagination">
|
||||
<p class="text-sm text-gray-500">
|
||||
第 {{ page }} / {{ Math.ceil(total / pageSize) }} 页,共 {{ total }} 条
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-secondary btn-sm" :disabled="page <= 1" @click="prevPage">上一页</button>
|
||||
<button class="btn btn-secondary btn-sm" :disabled="page * pageSize >= total" @click="nextPage">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchLogs" />
|
||||
</div>
|
||||
|
||||
<!-- Token Tooltip -->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import request from '@/utils/request'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
|
||||
@@ -76,7 +77,7 @@ const methodLabel: Record<string, string> = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="viewport-table-page">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">登录历史</h1>
|
||||
<p class="page-description">查看组织成员的登录记录,包括成功和失败的登录尝试</p>
|
||||
@@ -136,13 +137,13 @@ const methodLabel: Record<string, string> = {
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="viewport-table-panel card">
|
||||
<div v-if="loading" class="p-8 flex justify-center">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="list.length > 0">
|
||||
<div class="table-container">
|
||||
<div v-else-if="list.length > 0" class="viewport-table-content">
|
||||
<div class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -179,29 +180,7 @@ const methodLabel: Record<string, string> = {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="total > pageSize" class="table-pagination card-footer">
|
||||
<span class="text-sm text-gray-500">共 {{ total }} 条记录</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="page--; fetchData()"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ page }} / {{ Math.ceil(total / pageSize) }}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="page * pageSize >= total"
|
||||
@click="page++; fetchData()"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchData" />
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import request from '@/utils/request'
|
||||
@@ -105,7 +106,6 @@ const urgencyBadgeClass: Record<string, string> = {
|
||||
urgent: 'badge-danger',
|
||||
}
|
||||
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize))
|
||||
|
||||
async function fetchTickets() {
|
||||
loading.value = true
|
||||
@@ -204,18 +204,13 @@ async function reopenTicket() {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
fetchTickets()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTickets()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
@@ -260,8 +255,8 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div v-else class="card p-0 overflow-hidden">
|
||||
<div class="table-container">
|
||||
<div v-else class="viewport-table-panel card p-0 overflow-hidden">
|
||||
<div class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -311,27 +306,7 @@ onMounted(() => {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="totalPages > 1" class="table-pagination">
|
||||
<span class="text-xs text-gray-500">共 {{ total }} 条记录</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="handlePageChange(page - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="text-sm text-gray-600">{{ page }} / {{ totalPages }}</span>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="page >= totalPages"
|
||||
@click="handlePageChange(page + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchTickets" />
|
||||
</div>
|
||||
|
||||
<!-- Create Ticket Modal -->
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTenantAuthStore } from '@/stores/tenant-auth'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import TeamLockedBanner from '@/components/common/TeamLockedBanner.vue'
|
||||
import request from '@/utils/request'
|
||||
import { toast } from '@/utils/toast'
|
||||
@@ -29,7 +30,6 @@ const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize))
|
||||
|
||||
const roleBadgeClass: Record<string, string> = {
|
||||
owner: 'badge-primary',
|
||||
@@ -97,7 +97,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<TeamLockedBanner v-if="!teamEnabled" />
|
||||
<div v-else class="space-y-6">
|
||||
<div v-else class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
@@ -116,12 +116,12 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="viewport-table-panel card">
|
||||
<div v-if="loading" class="p-8 flex justify-center">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="invitations.length > 0" class="table-container">
|
||||
<div v-else-if="invitations.length > 0" class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -183,23 +183,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">生成邀请链接后,记录会显示在这里</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="totalPages > 1" class="table-pagination">
|
||||
<p class="text-xs text-gray-500">共 {{ total }} 条记录</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="page--; fetchInvitations()"
|
||||
>上一页</button>
|
||||
<span class="text-sm text-gray-600">{{ page }} / {{ totalPages }}</span>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
:disabled="page >= totalPages"
|
||||
@click="page++; fetchInvitations()"
|
||||
>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchInvitations" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTenantAuthStore } from '@/stores/tenant-auth'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import TeamLockedBanner from '@/components/common/TeamLockedBanner.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
@@ -214,7 +215,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<TeamLockedBanner v-if="!teamEnabled" />
|
||||
<div v-else class="space-y-6">
|
||||
<div v-else class="viewport-table-page space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
@@ -258,12 +259,12 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Members Table -->
|
||||
<div class="card">
|
||||
<div class="viewport-table-panel card">
|
||||
<div v-if="loading" class="p-8 flex justify-center">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="members.length > 0" class="table-container">
|
||||
<div v-else-if="members.length > 0" class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -339,6 +340,8 @@ onMounted(() => {
|
||||
<p class="empty-state-title">暂无成员</p>
|
||||
<p class="empty-state-description">邀请第一位团队成员吧</p>
|
||||
</div>
|
||||
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchMembers" />
|
||||
</div>
|
||||
|
||||
<!-- Invite Modal -->
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
@@ -104,7 +105,7 @@ function goToDetail(item: any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="viewport-table-page">
|
||||
<div class="page-header flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="page-title">项目管理</h1>
|
||||
@@ -116,7 +117,7 @@ function goToDetail(item: any) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="viewport-table-panel card">
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="spinner h-6 w-6 text-primary-500"></div>
|
||||
</div>
|
||||
@@ -125,7 +126,7 @@ function goToDetail(item: any) {
|
||||
<h3 class="empty-state-title">暂无项目</h3>
|
||||
<p class="empty-state-description">创建项目来组织你的 API Key 和资源</p>
|
||||
</div>
|
||||
<div v-else class="table-container">
|
||||
<div v-else class="viewport-table-scroll table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -169,14 +170,7 @@ function goToDetail(item: any) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="total > pageSize" class="table-pagination card-footer flex justify-end">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="page <= 1" @click="page--; fetchProjects()">上一页</button>
|
||||
<span class="text-sm text-gray-500">{{ page }} / {{ Math.ceil(total / pageSize) }}</span>
|
||||
<button class="btn btn-ghost btn-sm" :disabled="page * pageSize >= total" @click="page++; fetchProjects()">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<BasePagination v-model="page" :page-size="pageSize" :total="total" @change="fetchProjects" />
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
|
||||
Reference in new issue
Block a user