feat:表格优化

This commit is contained in:
chengqian committed 2026-07-27 18:30:33 +08:00
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>
+12 -11
View File
@@ -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',
+60
View File
@@ -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;
+6 -3
View File
@@ -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>
+5 -35
View File
@@ -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>
+5 -11
View File
@@ -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>
+7 -23
View File
@@ -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>
+5 -21
View File
@@ -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 -->
+6 -31
View File
@@ -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 -->
+5 -21
View File
@@ -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>
+6 -3
View File
@@ -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 -->
+5 -11
View File
@@ -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 -->