improve(ui):优化表格移动端显示效果
This commit is contained in:
1 parent
0ec970a182
commit
16793c82ef
20 files changed
+474
-79
No files matched your search
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: number
|
||||
@@ -18,6 +19,8 @@ const emit = defineEmits<{
|
||||
change: [page: number]
|
||||
}>()
|
||||
|
||||
const { isMobile } = useIsMobile()
|
||||
|
||||
// 当前 pageSize 不在候选列表时兜底追加,避免 select 找不到选中项
|
||||
const sizeOptions = computed(() => {
|
||||
const opts = [...props.pageSizeOptions]
|
||||
@@ -40,14 +43,18 @@ function handlePageSize(size: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-pagination
|
||||
v-if="total > 0"
|
||||
:page="modelValue"
|
||||
:page-size="pageSize"
|
||||
:item-count="total"
|
||||
:page-sizes="sizeOptions"
|
||||
:show-size-picker="showSizeChanger"
|
||||
@update:page="handlePage"
|
||||
@update:page-size="handlePageSize"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="total > 0" class="flex w-full min-w-0 flex-col items-center justify-center gap-2 sm:flex-row">
|
||||
<span v-if="isMobile" class="text-xs text-gray-500">共 {{ total }} 条</span>
|
||||
<n-pagination
|
||||
class="max-w-full"
|
||||
:page="modelValue"
|
||||
:page-size="pageSize"
|
||||
:item-count="total"
|
||||
:page-sizes="sizeOptions"
|
||||
:simple="isMobile"
|
||||
:show-size-picker="showSizeChanger && !isMobile"
|
||||
@update:page="handlePage"
|
||||
@update:page-size="handlePageSize"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,254 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType, VNodeChild } from 'vue'
|
||||
import type { DataTableColumn, DataTableColumns, PaginationProps } from 'naive-ui'
|
||||
import BasePagination from '@/components/common/BasePagination.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
|
||||
/**
|
||||
* 响应式数据列表:桌面端(≥ breakpoint)渲染 NDataTable,移动端渲染卡片列表。
|
||||
* 卡片字段 / 标题 / 徽章 / 副标题 / 操作列全部从 columns 派生,并复用各列 render 函数,
|
||||
* 因此页面只需把 <n-data-table> 换成 <ResponsiveDataTable> 并配置几个卡片 key 即可。
|
||||
*
|
||||
* 页面原有 pagination / loading / #empty 插槽的用法保持不变:
|
||||
* <ResponsiveDataTable
|
||||
* remote v-model:page="page" v-model:page-size="pageSize"
|
||||
* :item-count="total" :columns="columns" :data="rows"
|
||||
* :row-key="(row) => row.id"
|
||||
* card-title-key="model" card-badge-key="status"
|
||||
* card-subtitle-key="created_at"
|
||||
* :card-fields="[{ key: 'token', full: true }, 'cost']"
|
||||
* card-actions-key="actions" :row-click="openDetail"
|
||||
* >
|
||||
* <template #empty>...</template>
|
||||
* </ResponsiveDataTable>
|
||||
*/
|
||||
|
||||
// 模板里无法直接输出 render 函数返回的 VNode,用一个小渲染组件承接
|
||||
const VNodeRenderer = defineComponent({
|
||||
name: 'VNodeRenderer',
|
||||
props: {
|
||||
node: {
|
||||
type: [Object, String, Number, Boolean, Array] as PropType<VNodeChild>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => props.node
|
||||
},
|
||||
})
|
||||
|
||||
/** 卡片网格字段:key 对应列,full=true 时占满整行 */
|
||||
interface CardField {
|
||||
key: string
|
||||
title?: string
|
||||
full?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// ---------- 透传给 NDataTable ----------
|
||||
columns: DataTableColumns<any>
|
||||
data: any[]
|
||||
loading?: boolean
|
||||
rowKey: (row: any) => string | number
|
||||
remote?: boolean
|
||||
itemCount?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
pageSizes?: number[]
|
||||
showSizePicker?: boolean
|
||||
scrollX?: number
|
||||
showPagination?: boolean // false 时桌面端和移动端均不显示分页(用于固定数量列表)
|
||||
// ---------- 卡片模式配置 ----------
|
||||
cardTitleKey?: string // 卡片主标题列 key
|
||||
cardBadgeKey?: string // 卡片头部右侧徽章列 key(如 status)
|
||||
cardSubtitleKey?: string // 主标题下副标题列 key(如 created_at)
|
||||
cardFields?: (CardField | string)[] // 卡片网格字段;缺省取除 title/badge/subtitle/actions 外全部列
|
||||
cardActionsKey?: string // 操作列 key,渲染在卡片底部(默认 'actions')
|
||||
rowClick?: (row: any) => void // 提供后整张卡片可点击
|
||||
breakpoint?: number // 卡片切换断点(默认 1024 = lg,与 viewport-table 桌面布局一致)
|
||||
fillHeight?: boolean // 桌面端让表格撑满父容器高度、body 内部滚动(配合 viewport-table-panel 使用)
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
remote: true,
|
||||
itemCount: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
pageSizes: () => [10, 20, 50, 100],
|
||||
showSizePicker: true,
|
||||
scrollX: 0,
|
||||
showPagination: true,
|
||||
cardActionsKey: 'actions',
|
||||
breakpoint: 1024,
|
||||
fillHeight: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:page': [page: number]
|
||||
'update:page-size': [size: number]
|
||||
}>()
|
||||
|
||||
const { isMobile } = useIsMobile(props.breakpoint)
|
||||
|
||||
// 各区域对应列
|
||||
const titleCol = computed(() => props.columns.find((c) => c.key === props.cardTitleKey))
|
||||
const badgeCol = computed(() => props.columns.find((c) => c.key === props.cardBadgeKey))
|
||||
const subtitleCol = computed(() => props.columns.find((c) => c.key === props.cardSubtitleKey))
|
||||
const actionsCol = computed(() => props.columns.find((c) => c.key === props.cardActionsKey))
|
||||
|
||||
// NDataTable 的分页状态必须完整放在 pagination 对象中,顶层同名属性不会生效。
|
||||
const paginationProp = computed<false | PaginationProps>(() => {
|
||||
if (!props.showPagination) return false
|
||||
return {
|
||||
page: props.page,
|
||||
pageSize: props.pageSize,
|
||||
itemCount: props.itemCount,
|
||||
pageSizes: props.pageSizes,
|
||||
showSizePicker: props.showSizePicker,
|
||||
prefix: ({ itemCount }) => `共 ${itemCount ?? 0} 条`,
|
||||
}
|
||||
})
|
||||
|
||||
function desktopRowProps(row: any) {
|
||||
if (!props.rowClick) return {}
|
||||
return {
|
||||
style: 'cursor: pointer',
|
||||
onClick: (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target?.closest('button, a, input, select, textarea, [role="button"]')) return
|
||||
props.rowClick?.(row)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 网格字段:按 cardFields 顺序取,未指定则用除标题/徽章/副标题/操作外的全部列
|
||||
const fields = computed(() => {
|
||||
const keys = props.cardFields?.length
|
||||
? props.cardFields.map((f) => (typeof f === 'string' ? { key: f } : f))
|
||||
: props.columns
|
||||
.filter((c) => ![props.cardTitleKey, props.cardBadgeKey, props.cardSubtitleKey, props.cardActionsKey].includes(c.key))
|
||||
.map((c) => ({ key: c.key }))
|
||||
|
||||
return keys
|
||||
.map((f) => {
|
||||
const col = props.columns.find((c) => c.key === f.key) || null
|
||||
return {
|
||||
key: f.key,
|
||||
full: !!f.full,
|
||||
title: f.title ?? col?.title ?? f.key,
|
||||
col,
|
||||
}
|
||||
})
|
||||
.filter((f) => f.col)
|
||||
})
|
||||
|
||||
// 优先用列 render 输出,否则取原始值
|
||||
function renderCell(row: any, col: DataTableColumn<any> | null): VNodeChild {
|
||||
if (col?.render) return col.render(row)
|
||||
const v = row?.[col?.key ?? '']
|
||||
if (v == null || v === '') return '--'
|
||||
return String(v)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 桌面端(≥ breakpoint):NDataTable 原样透传 -->
|
||||
<n-data-table
|
||||
v-if="!isMobile"
|
||||
:flex-height="fillHeight"
|
||||
:class="{ 'flex-1 min-h-0': fillHeight }"
|
||||
:remote="remote"
|
||||
:pagination="paginationProp"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:scroll-x="scrollX"
|
||||
:data="data"
|
||||
:row-key="rowKey"
|
||||
:row-props="desktopRowProps"
|
||||
@update:page="(p: number) => emit('update:page', p)"
|
||||
@update:page-size="(s: number) => emit('update:page-size', s)"
|
||||
>
|
||||
<template #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</n-data-table>
|
||||
|
||||
<!-- 移动端:每行 = 一张卡片 -->
|
||||
<div v-else class="space-y-3">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="flex justify-center py-6">
|
||||
<div class="spinner h-5 w-5 text-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="data.length === 0">
|
||||
<slot name="empty">
|
||||
<div class="empty-state">
|
||||
<p class="empty-state-title">暂无数据</p>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="row in data"
|
||||
:key="rowKey(row)"
|
||||
class="card p-4"
|
||||
:class="{ 'cursor-pointer transition-transform active:scale-[0.995]': rowClick }"
|
||||
@click="rowClick && rowClick(row)"
|
||||
>
|
||||
<!-- 卡片头部:主标题 + 副标题 + 状态徽章 + 可点击箭头 -->
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<VNodeRenderer v-if="titleCol" :node="renderCell(row, titleCol)" />
|
||||
<VNodeRenderer
|
||||
v-if="subtitleCol && subtitleCol.key !== titleCol?.key"
|
||||
:node="renderCell(row, subtitleCol)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="badgeCol" class="flex flex-shrink-0 items-center">
|
||||
<VNodeRenderer :node="renderCell(row, badgeCol)" />
|
||||
</div>
|
||||
<Icon
|
||||
v-if="rowClick"
|
||||
name="chevronRight"
|
||||
size="sm"
|
||||
class="mt-0.5 flex-shrink-0 text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 字段网格:label:value,full 列占整行 -->
|
||||
<dl v-if="fields.length" class="mt-3 grid grid-cols-2 gap-x-3 gap-y-2.5">
|
||||
<div v-for="f in fields" :key="f.key" class="min-w-0" :class="{ 'col-span-2': f.full }">
|
||||
<dt class="text-xs text-gray-400">{{ f.title }}</dt>
|
||||
<dd class="mt-0.5">
|
||||
<VNodeRenderer :node="renderCell(row, f.col)" />
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<!-- 操作列:卡片底部操作栏 -->
|
||||
<div v-if="actionsCol" class="mt-3 flex items-center justify-end gap-2 border-t border-gray-100 pt-3" @click.stop>
|
||||
<VNodeRenderer :node="renderCell(row, actionsCol)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="showPagination && itemCount > 0" class="flex justify-center pt-1">
|
||||
<BasePagination
|
||||
:model-value="page"
|
||||
:page-size="pageSize"
|
||||
:total="itemCount"
|
||||
:page-size-options="pageSizes"
|
||||
:show-size-changer="showSizePicker"
|
||||
@update:model-value="(p: number) => emit('update:page', p)"
|
||||
@update:page-size="(s: number) => emit('update:page-size', s)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
/**
|
||||
* 响应式断点检测:当前视口宽度 < breakpoint 时视为移动端。
|
||||
* 默认 768(对应 Tailwind 的 md 断点),超过则在桌面/移动之间实时切换。
|
||||
*
|
||||
* 用法:
|
||||
* const { isMobile } = useIsMobile()
|
||||
* watch(isMobile, v => { ... })
|
||||
*/
|
||||
export function useIsMobile(breakpoint = 768) {
|
||||
const isMobile = ref(false)
|
||||
let mql: MediaQueryList | null = null
|
||||
|
||||
function update(e: MediaQueryListEvent | MediaQueryList) {
|
||||
isMobile.value = e.matches
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
|
||||
update(mql)
|
||||
// Safari < 14 需要 addListener,现代浏览器用 addEventListener
|
||||
mql.addEventListener('change', update)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mql?.removeEventListener('change', update)
|
||||
mql = null
|
||||
})
|
||||
|
||||
return { isMobile }
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, computed, h } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton } from 'naive-ui'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import { renderBadge, formatMoney, tableScrollX } from '@/utils/renderUtils'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
@@ -190,8 +191,9 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<div class="viewport-table-panel p-0 overflow-hidden">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -202,6 +204,11 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="orders"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="order_no"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['order_type', 'final_amount']"
|
||||
card-actions-key="actions"
|
||||
@update:page="fetchOrders"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -212,7 +219,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">您的订单记录将显示在这里</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, computed, h } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NInput } from 'naive-ui'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import request from '@/utils/request'
|
||||
|
||||
@@ -145,12 +146,17 @@ onMounted(fetchHistory)
|
||||
<h3 class="text-base font-semibold text-gray-900">兑换记录</h3>
|
||||
</div>
|
||||
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
:show-pagination="false"
|
||||
:loading="historyLoading"
|
||||
:columns="columns"
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="recentRedemptions"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="code"
|
||||
card-badge-key="type"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['value']"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center px-4 py-12 text-center">
|
||||
@@ -160,7 +166,7 @@ onMounted(fetchHistory)
|
||||
<p class="text-sm text-gray-500">暂无兑换记录</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import type { DataTableColumns } from 'naive-ui'
|
||||
import { NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import DateRangePicker from '@/components/common/DateRangePicker.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
@@ -182,7 +183,7 @@ onMounted(fetchTransactions)
|
||||
<DateRangePicker
|
||||
v-model:start="filters.start_date"
|
||||
v-model:end="filters.end_date"
|
||||
@change="handleSearch"
|
||||
@change="applyFilters"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-500 whitespace-nowrap">类型</label>
|
||||
@@ -249,8 +250,9 @@ onMounted(fetchTransactions)
|
||||
|
||||
<!-- Transactions Table -->
|
||||
<div class="viewport-table-panel relative z-0">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -261,6 +263,10 @@ onMounted(fetchTransactions)
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="transactions"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="amount"
|
||||
card-badge-key="type"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['balance_after', 'username', 'model_name', { key: 'request_id', full: true }, { key: 'description', full: true }]"
|
||||
@update:page="fetchTransactions"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -271,7 +277,7 @@ onMounted(fetchTransactions)
|
||||
<p class="empty-state-description">交易记录将在 API 调用和充值后展示</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import type { DataTableColumns } from 'naive-ui'
|
||||
import { NInput, NInputNumber, NModal } from 'naive-ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import request from '@/utils/request'
|
||||
import { dispatchPayment } from '@/utils/payment'
|
||||
@@ -672,12 +673,17 @@ onBeforeUnmount(() => {
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-3">兑换记录</h4>
|
||||
|
||||
<!-- History table -->
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
:show-pagination="false"
|
||||
:loading="redeemHistoryLoading"
|
||||
:columns="redeemHistoryColumns"
|
||||
:scroll-x="tableScrollX(redeemHistoryColumns)"
|
||||
:data="redeemHistory"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="code"
|
||||
card-badge-key="type"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['value']"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center py-8 text-center">
|
||||
@@ -687,7 +693,7 @@ onBeforeUnmount(() => {
|
||||
<p class="text-sm text-gray-500">暂无兑换记录</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, computed, h } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NTag } from 'naive-ui'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import ApiKeyEditModal from '@/components/common/ApiKeyEditModal.vue'
|
||||
import type { ApiKeyData } from '@/components/common/ApiKeyEditModal.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
@@ -317,8 +318,9 @@ onMounted(() => {
|
||||
|
||||
<!-- Keys Table -->
|
||||
<div class="viewport-table-panel">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -329,6 +331,11 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="keys"
|
||||
:row-key="(row: ApiKey) => row.id"
|
||||
card-title-key="name"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="[{ key: 'key_prefix', full: true }, 'model_count', 'expires_at', { key: 'limits', full: true }]"
|
||||
card-actions-key="actions"
|
||||
@update:page="fetchKeys"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -339,7 +346,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">创建第一个密钥以开始使用 AI 模型</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Create Modal -->
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NTag } from 'naive-ui'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const activeSection = ref('quickstart')
|
||||
@@ -1333,11 +1334,15 @@ const errorColumns = computed<DataTableColumns<any>>(() => [
|
||||
<!-- Parameters Table -->
|
||||
<div v-if="ep.params.length > 0">
|
||||
<h5 class="text-sm font-semibold text-gray-800 mb-2">请求参数</h5>
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
:show-pagination="false"
|
||||
:columns="paramColumns"
|
||||
:scroll-x="tableScrollX(paramColumns)"
|
||||
:data="ep.params"
|
||||
:row-key="(row:any)=>row.field"
|
||||
card-title-key="field"
|
||||
card-badge-key="required"
|
||||
:card-fields="['type', { key: 'desc', full: true }]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1459,11 +1464,15 @@ const errorColumns = computed<DataTableColumns<any>>(() => [
|
||||
<h3 class="font-semibold text-gray-900">错误码一览</h3>
|
||||
</div>
|
||||
<div class="card-body !pt-0">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
:show-pagination="false"
|
||||
:columns="errorColumns"
|
||||
:scroll-x="tableScrollX(errorColumns)"
|
||||
:data="errorCodes"
|
||||
:row-key="(row:any)=>row.code"
|
||||
card-title-key="code"
|
||||
card-badge-key="status"
|
||||
:card-fields="[{ key: 'desc', full: true }, { key: 'example', full: true }]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DataTableColumns } from 'naive-ui'
|
||||
import request from '@/utils/request'
|
||||
import { tableScrollX } from '@/utils/renderUtils'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
|
||||
interface ModelItem { model_id: string; model_name: string; category: string }
|
||||
interface ComparisonItem {
|
||||
@@ -325,18 +326,22 @@ const columns = computed<DataTableColumns<ComparisonItem>>(() => [
|
||||
<h3 class="text-sm font-semibold text-gray-900">详细对比</h3>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
:show-pagination="false"
|
||||
:columns="columns"
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="items"
|
||||
:row-key="(row: ComparisonItem) => row.model_name"
|
||||
card-title-key="model_name"
|
||||
card-badge-key="score"
|
||||
:card-fields="['requests', 'success_rate', 'avg_latency_ms', 'p95_latency_ms', 'total_cost', 'avg_cost_per_request', { key: 'token', full: true }]"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<p class="text-sm text-gray-500">暂无对比数据</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NInput } from 'naive-ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { tableScrollX } from '@/utils/renderUtils'
|
||||
import DateTimeRangePicker from '@/components/common/DateTimeRangePicker.vue'
|
||||
@@ -328,7 +329,7 @@ onMounted(() => {
|
||||
<DateTimeRangePicker
|
||||
v-model:start="logFilter.start_date"
|
||||
v-model:end="logFilter.end_date"
|
||||
@change="handleSearchLogs"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-500 whitespace-nowrap">用户名</label>
|
||||
@@ -362,8 +363,9 @@ onMounted(() => {
|
||||
</div>
|
||||
<!-- Table -->
|
||||
<div class="viewport-table-panel p-0 overflow-hidden">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="logPage"
|
||||
v-model:page-size="logPageSize"
|
||||
:item-count="logTotal"
|
||||
@@ -374,6 +376,12 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="logs"
|
||||
:row-key="(row: RequestLog) => row.id"
|
||||
card-title-key="request_id"
|
||||
card-badge-key="status_code"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="[{ key: 'path', full: true }, 'user', 'method', 'latency_ms', 'first_token_ms', 'audit_level', 'task_id']"
|
||||
card-actions-key="actions"
|
||||
:row-click="(row: RequestLog) => fetchDetail(row.id)"
|
||||
@update:page="fetchRequestLogs"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -384,7 +392,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">API 调用的输入输出记录将显示在这里</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
@@ -501,4 +509,3 @@ onMounted(() => {
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NButton, NTag, NInput } from 'naive-ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import request from '@/utils/request'
|
||||
import { tableScrollX } from '@/utils/renderUtils'
|
||||
@@ -266,7 +267,7 @@ onMounted(() => {
|
||||
<DateTimeRangePicker
|
||||
v-model:start="filterStartDate"
|
||||
v-model:end="filterEndDate"
|
||||
@change="handleSearch"
|
||||
@change="applyFilters"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-500 whitespace-nowrap">任务 ID</label>
|
||||
@@ -306,8 +307,9 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<div class="viewport-table-panel relative z-0 p-0 overflow-hidden">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -318,6 +320,12 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="tasks"
|
||||
:row-key="(row: TaskItem) => row.id"
|
||||
card-title-key="public_task_id"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="submit_time"
|
||||
:card-fields="['platform', 'model_name', 'cost', 'finish_time']"
|
||||
card-actions-key="actions"
|
||||
:row-click="openDetail"
|
||||
@update:page="fetchTasks"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -328,7 +336,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">异步生成任务的执行记录将显示在这里</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import DateTimeRangePicker from '@/components/common/DateTimeRangePicker.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { tableScrollX } from '@/utils/renderUtils'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
@@ -469,7 +470,7 @@ onMounted(() => {
|
||||
<DateTimeRangePicker
|
||||
v-model:start="filters.start_date"
|
||||
v-model:end="filters.end_date"
|
||||
@change="handleSearch"
|
||||
@change="applyFilters"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-500 whitespace-nowrap">用户名</label>
|
||||
@@ -530,10 +531,11 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Table -->
|
||||
<!-- Logs Table / 移动端卡片 -->
|
||||
<div class="viewport-table-panel relative z-0 overflow-hidden">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -544,6 +546,19 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="logs"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="model"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="[
|
||||
{ key: 'user' },
|
||||
{ key: 'api_key' },
|
||||
{ key: 'type' },
|
||||
{ key: 'token', full: true },
|
||||
{ key: 'cost' },
|
||||
{ key: 'latency' },
|
||||
]"
|
||||
card-actions-key="actions"
|
||||
:row-click="openDetail"
|
||||
@update:page="fetchLogs"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -554,7 +569,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">日志将在 API 调用后展示</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Token Tooltip -->
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, watch, h } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
|
||||
export interface ParamDef {
|
||||
@@ -203,11 +204,15 @@ const paramTableColumns = computed<DataTableColumns<ParamDef>>(() => [
|
||||
|
||||
<!-- 参数表格 -->
|
||||
<div v-if="currentGroup">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
:show-pagination="false"
|
||||
:columns="paramTableColumns"
|
||||
:scroll-x="tableScrollX(paramTableColumns)"
|
||||
:data="currentGroup.params"
|
||||
:row-key="(row:any)=>row.name"
|
||||
card-title-key="name"
|
||||
card-badge-key="required"
|
||||
:card-fields="['type', 'default', { key: 'values', full: true }, { key: 'description', full: true }]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, computed, h } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NInput } from 'naive-ui'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
@@ -197,8 +198,9 @@ function handlePageSizeChange() {
|
||||
|
||||
<!-- Table -->
|
||||
<div class="viewport-table-panel">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -209,6 +211,10 @@ function handlePageSizeChange() {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="list"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="user"
|
||||
card-badge-key="success"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['login_method', 'ip_address', { key: 'user_agent', full: true }, { key: 'fail_reason', full: true }]"
|
||||
@update:page="fetchData"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -219,7 +225,7 @@ function handlePageSizeChange() {
|
||||
<p class="empty-state-description">当前没有任何登录历史记录</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import { ref, reactive, onMounted, computed, h } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NInput } from 'naive-ui'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
@@ -227,13 +228,6 @@ const columns = computed<DataTableColumns<Ticket>>(() => [
|
||||
},
|
||||
])
|
||||
|
||||
function handleRowProps(row: Ticket) {
|
||||
return {
|
||||
style: 'cursor: pointer',
|
||||
onClick: () => openDetail(row),
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange() {
|
||||
page.value = 1
|
||||
fetchTickets()
|
||||
@@ -274,8 +268,9 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<div class="viewport-table-panel p-0 overflow-hidden">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -286,7 +281,11 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="tickets"
|
||||
:row-key="(row: Ticket) => row.id"
|
||||
:row-props="handleRowProps"
|
||||
card-title-key="title"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['id', 'category', 'urgency', 'assigned_admin']"
|
||||
:row-click="openDetail"
|
||||
@update:page="fetchTickets"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -301,7 +300,7 @@ onMounted(() => {
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Create Ticket Modal -->
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useTenantAuthStore } from '@/stores/tenant-auth'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import TeamLockedBanner from '@/components/common/TeamLockedBanner.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import request from '@/utils/request'
|
||||
import { toast } from '@/utils/toast'
|
||||
@@ -163,8 +164,9 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<div class="viewport-table-panel">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -175,6 +177,11 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="invitations"
|
||||
:row-key="(row: InvitationItem) => row.id"
|
||||
card-title-key="code"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['role', 'use_count', 'creator_name', { key: 'expires_at', full: true }]"
|
||||
card-actions-key="actions"
|
||||
@update:page="fetchInvitations"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -185,7 +192,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">生成邀请链接后,记录会显示在这里</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { NButton, NTag } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTenantAuthStore } from '@/stores/tenant-auth'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import TeamLockedBanner from '@/components/common/TeamLockedBanner.vue'
|
||||
import BaseSelect from '../../components/common/BaseSelect.vue'
|
||||
import TableFilterForm, { type FilterField } from '@/components/common/TableFilterForm.vue'
|
||||
@@ -376,14 +377,6 @@ const columns = computed<DataTableColumns<Member>>(() => [
|
||||
},
|
||||
])
|
||||
|
||||
// 整行点击跳详情
|
||||
function handleRowProps(row: Member) {
|
||||
return {
|
||||
style: 'cursor: pointer',
|
||||
onClick: () => goDetail(row.id),
|
||||
}
|
||||
}
|
||||
|
||||
// pageSize 变化回第 1 页并刷新
|
||||
function handlePageSizeChange() {
|
||||
page.value = 1
|
||||
@@ -488,8 +481,9 @@ onMounted(() => {
|
||||
|
||||
<!-- Members Table -->
|
||||
<div class="viewport-table-panel">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -500,7 +494,12 @@ onMounted(() => {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="members"
|
||||
:row-key="(row: Member) => row.id"
|
||||
:row-props="handleRowProps"
|
||||
card-title-key="user"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['role', 'quota_type', 'model_count', 'month_cost', 'updated_at']"
|
||||
card-actions-key="actions"
|
||||
:row-click="(row: Member) => goDetail(row.id)"
|
||||
@update:page="fetchMembers"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -511,7 +510,7 @@ onMounted(() => {
|
||||
<p class="empty-state-description">邀请第一位团队成员吧</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Invite Modal -->
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NButton, NTag } from 'naive-ui'
|
||||
import { renderBadge, formatDate, formatTokens, formatMs, tableScrollX } from '@/utils/renderUtils'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import ApiKeyEditModal from '@/components/common/ApiKeyEditModal.vue'
|
||||
import type { ApiKeyData } from '@/components/common/ApiKeyEditModal.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
@@ -555,8 +556,8 @@ const usageLogsColumns = computed<DataTableColumns<any>>(() => [
|
||||
|
||||
<!-- Keys Tab -->
|
||||
<div v-if="activeTab === 'keys'" class="space-y-4">
|
||||
<div class="card">
|
||||
<n-data-table
|
||||
<div>
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
v-model:page="keysPage"
|
||||
v-model:page-size="keysPageSize"
|
||||
@@ -568,6 +569,11 @@ const usageLogsColumns = computed<DataTableColumns<any>>(() => [
|
||||
:scroll-x="tableScrollX(keysColumns)"
|
||||
:data="keys"
|
||||
:row-key="(row: ApiKey) => row.id"
|
||||
card-title-key="name"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="[{ key: 'key_prefix', full: true }, 'scope', { key: 'limit', full: true }, 'expires_at']"
|
||||
card-actions-key="actions"
|
||||
@update:page="fetchKeys"
|
||||
@update:page-size="handleKeysPageSizeChange"
|
||||
>
|
||||
@@ -578,7 +584,7 @@ const usageLogsColumns = computed<DataTableColumns<any>>(() => [
|
||||
<p class="empty-state-description">创建密钥以为此项目提供 AI 能力</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -630,11 +636,11 @@ const usageLogsColumns = computed<DataTableColumns<any>>(() => [
|
||||
</div>
|
||||
|
||||
<!-- Usage Logs -->
|
||||
<div class="card">
|
||||
<div class="space-y-3">
|
||||
<div class="card-header">
|
||||
<h3 class="text-base font-semibold text-gray-900">用量日志</h3>
|
||||
</div>
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
v-model:page="usageLogsPage"
|
||||
v-model:page-size="usageLogsPageSize"
|
||||
@@ -646,6 +652,10 @@ const usageLogsColumns = computed<DataTableColumns<any>>(() => [
|
||||
:scroll-x="tableScrollX(usageLogsColumns)"
|
||||
:data="usageLogs"
|
||||
:row-key="(row: any) => row.id"
|
||||
card-title-key="model_name"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['relay_mode', 'input_tokens', 'output_tokens', 'total_cost', 'latency_ms']"
|
||||
@update:page="fetchUsageLogs"
|
||||
@update:page-size="handleUsageLogsPageSizeChange"
|
||||
>
|
||||
@@ -656,7 +666,7 @@ const usageLogsColumns = computed<DataTableColumns<any>>(() => [
|
||||
<p class="empty-state-description">项目密钥调用后将在此显示用量数据</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NButton, NInput, NInputNumber } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import ResponsiveDataTable from '@/components/common/ResponsiveDataTable.vue'
|
||||
import { renderBadge, tableScrollX } from '@/utils/renderUtils'
|
||||
import request from '@/utils/request'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
@@ -171,13 +172,6 @@ const columns = computed<DataTableColumns<any>>(() => [
|
||||
},
|
||||
])
|
||||
|
||||
function handleRowProps(row: any) {
|
||||
return {
|
||||
style: 'cursor: pointer',
|
||||
onClick: () => goToDetail(row),
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange() {
|
||||
page.value = 1
|
||||
fetchProjects()
|
||||
@@ -198,8 +192,9 @@ function handlePageSizeChange() {
|
||||
</div>
|
||||
|
||||
<div class="viewport-table-panel">
|
||||
<n-data-table
|
||||
<ResponsiveDataTable
|
||||
remote
|
||||
fill-height
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
@@ -210,7 +205,12 @@ function handlePageSizeChange() {
|
||||
:scroll-x="tableScrollX(columns)"
|
||||
:data="projects"
|
||||
:row-key="(row: any) => row.id"
|
||||
:row-props="handleRowProps"
|
||||
card-title-key="name"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="[{ key: 'description', full: true }, 'budget']"
|
||||
card-actions-key="actions"
|
||||
:row-click="goToDetail"
|
||||
@update:page="fetchProjects"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
@@ -221,7 +221,7 @@ function handlePageSizeChange() {
|
||||
<p class="empty-state-description">创建项目来组织你的 API Key 和资源</p>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</ResponsiveDataTable>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
|
||||
Reference in new issue
Block a user