improve(ui):优化移动端显示效果
This commit is contained in:
1 parent
be8275a8c5
commit
cbffa01689
33 files changed
+474
-470
No files matched your search
@@ -72,6 +72,7 @@ const props = withDefaults(
|
||||
cardFields?: (CardField | string)[] // 卡片网格字段;缺省取除 标题/徽章/副标题/操作 外的全部列
|
||||
cardActionsKey?: string // 操作列 dataIndex,渲染在卡片底部(默认 'actions')
|
||||
rowClick?: (row: any) => void // 提供后整张卡片可点击(仅移动端;桌面端用 row-click 事件)
|
||||
rowClass?: (row: any) => string // 自定义行类名(桌面端 ATable row-class,如高亮当前会话)
|
||||
breakpoint?: number // 卡片切换断点(默认 768 = md)
|
||||
}>(),
|
||||
{
|
||||
@@ -105,8 +106,11 @@ const badgeCol = computed(() => props.columns.find((c) => c.dataIndex === props.
|
||||
const subtitleCol = computed(() => props.columns.find((c) => c.dataIndex === props.cardSubtitleKey))
|
||||
const actionsCol = computed(() => props.columns.find((c) => c.dataIndex === props.cardActionsKey))
|
||||
|
||||
// 桌面端行类名:可点击时显示手型
|
||||
const rowClassHandler = () => (props.rowClick ? 'cursor-pointer' : '')
|
||||
// 桌面端行类名:合并页面自定义 rowClass + 可点击时的手型
|
||||
const rowClassHandler = (record: any) => {
|
||||
const cls = typeof props.rowClass === 'function' ? props.rowClass(record) || '' : ''
|
||||
return [cls, props.rowClick ? 'cursor-pointer' : ''].filter(Boolean).join(' ') || undefined
|
||||
}
|
||||
|
||||
// 网格字段:按 cardFields 顺序取;未传 cardFields(undefined)时用除 标题/徽章/副标题/操作 外的全部列。
|
||||
// 注意:显式传 :card-fields="[]" 表示「不要字段网格」,必须与「未传」区分,不能按 truthy 判断。
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import {
|
||||
Tag, Message,
|
||||
} from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
const billLoading = ref(false)
|
||||
const billData = ref<any[]>([])
|
||||
const billPagination = reactive({
|
||||
current: 1, pageSize: 20, total: 0, showPageSize: true, pageSizeOptions: [10, 20, 50],
|
||||
})
|
||||
const billFilterTenantId = ref('')
|
||||
|
||||
const billStatusTagColor: Record<string, string> = {
|
||||
settled: 'green', pending: 'orangered', failed: 'red',
|
||||
}
|
||||
const billStatusLabel: Record<string, string> = {
|
||||
settled: '已结算', pending: '待结算', failed: '失败',
|
||||
}
|
||||
|
||||
const billColumns: TableColumnData[] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '租户', dataIndex: 'tenant_name', width: 120, ellipsis: true, tooltip: true },
|
||||
{ title: '用户', dataIndex: 'user_name', width: 120, ellipsis: true, tooltip: true },
|
||||
{ title: '渠道', dataIndex: 'channel_name', width: 120, ellipsis: true, tooltip: true },
|
||||
{ title: '模型', dataIndex: 'model_name', width: 140, ellipsis: true },
|
||||
{ title: '模式', dataIndex: 'relay_mode', width: 100 },
|
||||
{ title: '输入Token', dataIndex: 'input_tokens', width: 100 },
|
||||
{ title: '输出Token', dataIndex: 'output_tokens', width: 100 },
|
||||
{
|
||||
title: '总费用', dataIndex: 'total_cost', width: 100,
|
||||
render({ record }) { return `$${(record.total_cost || 0).toFixed(6)}` },
|
||||
},
|
||||
{
|
||||
title: '扣费来源', dataIndex: 'deduction_source', width: 120,
|
||||
render({ record }) {
|
||||
const tags = []
|
||||
if (record.plan_deduction > 0) tags.push(h(Tag, { size: 'small', type: 'info' }, () => '套餐抵扣'))
|
||||
if (record.wallet_deduction > 0) tags.push(h(Tag, { size: 'small' }, () => '钱包'))
|
||||
return tags.length > 0 ? h('div', { class: 'flex gap-1' }, tags) : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 80,
|
||||
render({ record }) {
|
||||
return h(Tag, { color: billStatusTagColor[record.status], size: 'small' }, () => billStatusLabel[record.status] || record.status)
|
||||
},
|
||||
},
|
||||
{ title: '结算时间', dataIndex: 'settled_at', width: 170 },
|
||||
]
|
||||
|
||||
async function fetchBillingRecords() {
|
||||
billLoading.value = true
|
||||
try {
|
||||
const params: Record<string, any> = { page: billPagination.current, page_size: billPagination.pageSize }
|
||||
if (billFilterTenantId.value) params.tenant_id = parseInt(billFilterTenantId.value)
|
||||
const res: any = await request.get('/admin/billing-records', { params })
|
||||
const raw = res.data?.data
|
||||
billData.value = raw?.list || []
|
||||
billPagination.total = raw?.total || 0
|
||||
} catch {
|
||||
billData.value = []
|
||||
billPagination.total = 0
|
||||
} finally { billLoading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchBillingRecords() })
|
||||
|
||||
const { exporting, exportFile } = useExport({
|
||||
url: '/admin/billing-records/export',
|
||||
getFilters: () => ({
|
||||
tenant_id: billFilterTenantId.value,
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-table">
|
||||
<PageHeader title="账单记录" description="查看计费记录">
|
||||
<template #actions>
|
||||
<ADropdown trigger="hover">
|
||||
<AButton :loading="exporting">导出</AButton>
|
||||
<template #content>
|
||||
<ADoption @click="exportFile('csv')">导出 CSV</ADoption>
|
||||
<ADoption @click="exportFile('xlsx')">导出 Excel</ADoption>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<ACard :bordered="false" class="mt-4">
|
||||
<div class="mb-4">
|
||||
<ASpace>
|
||||
<AInput v-model="billFilterTenantId" placeholder="租户ID" allow-clear style="width: 120px" @keydown.enter="fetchBillingRecords" />
|
||||
<AButton type="primary" @click="fetchBillingRecords">搜索</AButton>
|
||||
</ASpace>
|
||||
</div>
|
||||
<ATable :columns="billColumns" :data="billData" :loading="billLoading" :scroll="{ x: 1800 }" :bordered="false" :stripe="true" size="small" :pagination="false" row-key="id" />
|
||||
<div class="table-footer">
|
||||
<TableStats :total="billPagination.total" />
|
||||
<APagination v-model:current="billPagination.current" v-model:page-size="billPagination.pageSize" :total="billPagination.total" :page-size-options="billPagination.pageSizeOptions" show-page-size @change="fetchBillingRecords" @page-size-change="(s: number) => { billPagination.pageSize = s; billPagination.current = 1; fetchBillingRecords() }" />
|
||||
</div>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--ta-border-light);
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -139,7 +140,17 @@ const { exporting, exportFile } = useExport({
|
||||
</ASpace>
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="orders" :loading="loading" row-key="id" :scroll="{ x: 1100 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="orders"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1100 }"
|
||||
card-title-key="order_no"
|
||||
card-badge-key="status"
|
||||
card-subtitle-key="created_at"
|
||||
:card-fields="['order_type', 'final_amount', 'payment_channel', 'tenant_id']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchOrders" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchOrders() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData, FormInstance } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -141,7 +142,17 @@ const { exporting, exportFile } = useExport({
|
||||
<ASelect v-model="statusFilter" :options="statusOptions" style="width: 120px" allow-clear @change="() => { pagination.current = 1; fetchPlans() }" />
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="plans" :loading="loading" row-key="id" :scroll="{ x: 1200 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="plans"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
card-title-key="name"
|
||||
card-subtitle-key="identifier"
|
||||
card-badge-key="status"
|
||||
:card-fields="['monthly_price', 'yearly_price', 'is_recommended', 'sort_order']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchPlans" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchPlans() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -123,7 +124,17 @@ const { exporting, exportFile } = useExport({
|
||||
</PageHeader>
|
||||
|
||||
<ACard :bordered="false">
|
||||
<ATable :columns="columns" :data="promoCodes" :loading="loading" row-key="id" :scroll="{ x: 1200 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="promoCodes"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
card-title-key="name"
|
||||
card-subtitle-key="code"
|
||||
card-badge-key="status"
|
||||
:card-fields="['type', 'discount_value', 'usage', 'valid_to']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -145,7 +146,17 @@ const { exporting, exportFile } = useExport({
|
||||
{ label: '已禁用', value: 'disabled' }, { label: '已过期', value: 'expired' },
|
||||
]" style="width: 120px" allow-clear @change="() => { pagination.current = 1; fetchRedemptions() }" />
|
||||
</div>
|
||||
<ATable :columns="columns" :data="redemptions" :loading="loading" row-key="id" :scroll="{ x: 1100 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="redemptions"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1100 }"
|
||||
card-title-key="code"
|
||||
card-subtitle-key="type"
|
||||
card-badge-key="status"
|
||||
:card-fields="['value', 'usage', 'batch_no', 'duration_days', 'plan_id']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchRedemptions" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchRedemptions() }" />
|
||||
@@ -153,7 +164,17 @@ const { exporting, exportFile } = useExport({
|
||||
</ATabPane>
|
||||
|
||||
<ATabPane key="usages" title="使用记录">
|
||||
<ATable :columns="usageColumns" :data="usages" :loading="usageLoading" row-key="id" :scroll="{ x: 900 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="usageColumns"
|
||||
:data="usages"
|
||||
:loading="usageLoading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 900 }"
|
||||
card-title-key="code"
|
||||
card-subtitle-key="tenant_name"
|
||||
card-badge-key="type"
|
||||
:card-fields="['value', 'username', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="usagePagination.total" />
|
||||
<APagination v-model:current="usagePagination.current" v-model:page-size="usagePagination.pageSize" :total="usagePagination.total" :page-size-options="usagePagination.pageSizeOptions" show-page-size @change="fetchUsages" @page-size-change="(s: number) => { usagePagination.pageSize = s; usagePagination.current = 1; fetchUsages() }" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -104,7 +105,26 @@ onMounted(fetchData)
|
||||
<AButton type="primary" @click="resetAndFetch">搜索</AButton>
|
||||
</ASpace>
|
||||
</div>
|
||||
<ATable :columns="columns" :data="data" :loading="loading" :scroll="{ x: 1200 }" :bordered="false" :stripe="true" size="small" :pagination="false" row-key="id" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:scroll="{ x: 1200 }"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
row-key="id"
|
||||
card-title-key="username"
|
||||
card-subtitle-key="tenant_name"
|
||||
card-badge-key="type"
|
||||
:card-fields="[
|
||||
{ key: 'amount' },
|
||||
{ key: 'balance_after' },
|
||||
{ key: 'created_at' },
|
||||
{ key: 'description', full: true },
|
||||
{ key: 'model_name' },
|
||||
]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchData" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchData() }" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Message, Modal, Tag, Button, Space } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
@@ -201,7 +202,22 @@ onUnmounted(() => {
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table :data="data" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
|
||||
<ResponsiveTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
card-title-key="rule_name"
|
||||
card-subtitle-key="metric_type"
|
||||
card-badge-key="status"
|
||||
:card-fields="[
|
||||
'level',
|
||||
'trigger_value',
|
||||
'threshold_value',
|
||||
{ key: 'trigger_message', full: true },
|
||||
'created_at',
|
||||
]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="total" />
|
||||
<a-pagination :current="pagination.current" :page-size="pagination.pageSize" :total="total" @change="handlePageChange" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Message, Tag, Button, Space, Popconfirm } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -46,6 +47,7 @@ const columns: TableColumnData[] = [
|
||||
{ title: '指标', dataIndex: 'metric_type_label', width: 160 },
|
||||
{
|
||||
title: '条件',
|
||||
dataIndex: 'condition',
|
||||
width: 150,
|
||||
render({ record }: { record: any }) {
|
||||
const cond = record.condition_label || record.condition
|
||||
@@ -237,7 +239,16 @@ onMounted(() => {
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table :data="data" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
|
||||
<ResponsiveTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
card-title-key="name"
|
||||
card-subtitle-key="metric_type_label"
|
||||
card-badge-key="level"
|
||||
:card-fields="['condition', 'is_enabled', 'last_triggered_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="total" />
|
||||
<a-pagination :current="pagination.current" :page-size="pagination.pageSize" :total="total" @change="handlePageChange" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag, Button, Select, Input, DatePicker } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -283,13 +284,23 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1400 }"
|
||||
card-title-key="channel_name"
|
||||
card-subtitle-key="model_name"
|
||||
card-badge-key="error_category"
|
||||
:card-fields="[
|
||||
'provider',
|
||||
'status_code',
|
||||
'latency_ms',
|
||||
'attempt',
|
||||
'created_at',
|
||||
{ key: 'error_message', full: true },
|
||||
]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="total" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -263,12 +264,22 @@ onMounted(() => {
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
<ResponsiveTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="user_name"
|
||||
card-subtitle-key="tenant_name"
|
||||
card-badge-key="blocked"
|
||||
:card-fields="[
|
||||
'filter_mode',
|
||||
'method',
|
||||
'client_ip',
|
||||
'created_at',
|
||||
{ key: 'path', full: true },
|
||||
{ key: 'matched_words', full: true },
|
||||
]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="total" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Message, Tag, Button, Space } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -79,6 +80,7 @@ const columns: TableColumnData[] = [
|
||||
},
|
||||
{
|
||||
title: '统计',
|
||||
dataIndex: 'stats',
|
||||
width: 160,
|
||||
render({ record }: { record: any }) {
|
||||
if (!record.total_executions) return '-'
|
||||
@@ -228,15 +230,18 @@ onMounted(() => {
|
||||
</PageHeader>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
<ResponsiveTable
|
||||
:data="jobs"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
row-key="name"
|
||||
:expanded-keys="expandedKeys"
|
||||
card-title-key="name"
|
||||
card-subtitle-key="schedule"
|
||||
card-badge-key="is_running"
|
||||
:card-fields="['last_status', 'last_duration_ms', 'last_started_at', { key: 'stats', full: true }]"
|
||||
@expanded-change="handleExpand"
|
||||
>
|
||||
<template #expand-row="{ record }">
|
||||
@@ -261,7 +266,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</ResponsiveTable>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="jobs.length" />
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Message, Modal, Tag, Button, Space } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
@@ -310,15 +311,20 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
<ResponsiveTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
row-key="id"
|
||||
:row-selection="{ type: 'checkbox', showCheckedAll: true, selectedRowKeys: selectedKeys, onSelect: handleSelection }"
|
||||
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
|
||||
:selected-keys="selectedKeys"
|
||||
card-title-key="error_message"
|
||||
card-subtitle-key="request_path"
|
||||
card-badge-key="resolved"
|
||||
:card-fields="['source', 'error_code', 'created_at']"
|
||||
@selection-change="handleSelection"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="total" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -220,21 +221,24 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
row-key="model_name"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 1200 }"
|
||||
size="large"
|
||||
stripe
|
||||
:border="{ wrapper: true, headerCell: true }"
|
||||
card-title-key="model_name"
|
||||
card-subtitle-key="request_count"
|
||||
card-badge-key="success_rate"
|
||||
:card-fields="['avg_latency_ms', 'avg_first_token_ms', 'tps', 'total_tokens', 'total_cost']"
|
||||
>
|
||||
<template #empty>
|
||||
<a-empty description="所选区间暂无模型性能数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</ResponsiveTable>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="data.length" />
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { marked } from 'marked'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -267,7 +268,17 @@ onMounted(fetchList)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="data" :loading="loading" row-key="id" :scroll="{ x: 1400 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1400 }"
|
||||
card-title-key="title"
|
||||
card-subtitle-key="code"
|
||||
card-badge-key="status"
|
||||
:card-fields="['version', 'force_accept', 'published_at', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
@@ -318,7 +329,15 @@ onMounted(fetchList)
|
||||
|
||||
<!-- Acceptances Drawer -->
|
||||
<ADrawer v-model:visible="showAcceptances" :title="acceptancesTitle" :width="640" unmount-on-close>
|
||||
<ATable :columns="acceptanceColumns" :data="acceptancesData" :loading="acceptancesLoading" row-key="id" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="acceptanceColumns"
|
||||
:data="acceptancesData"
|
||||
:loading="acceptancesLoading"
|
||||
row-key="id"
|
||||
card-title-key="user_type"
|
||||
card-subtitle-key="user_id"
|
||||
:card-fields="['ip_address', 'created_at']"
|
||||
/>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<APagination v-model:current="acceptancesPagination.current" :total="acceptancesPagination.total" :page-size="acceptancesPagination.pageSize" @change="fetchAcceptances" />
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -212,7 +213,17 @@ onMounted(fetchList)
|
||||
<ASelect v-model="statusFilter" :options="statusOptions" style="width: 120px" allow-clear placeholder="状态筛选" @change="() => { pagination.current = 1; fetchList() }" />
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="announcements" :loading="loading" row-key="id" :scroll="{ x: 1400 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="announcements"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1400 }"
|
||||
card-title-key="title"
|
||||
card-subtitle-key="display_position"
|
||||
card-badge-key="status"
|
||||
:card-fields="['type', 'is_pinned', 'effective_at', 'expires_at', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -183,7 +184,17 @@ onMounted(fetchList)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="data" :loading="loading" row-key="id" :scroll="{ x: 1200 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
card-title-key="title"
|
||||
card-subtitle-key="version"
|
||||
card-badge-key="status"
|
||||
:card-fields="['type', 'published_at', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag, Button } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const activeTab = ref<'send-logs' | 'verify-codes'>('send-logs')
|
||||
@@ -200,7 +201,7 @@ onMounted(() => {
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
<ResponsiveTable
|
||||
:columns="sendColumns"
|
||||
:data="sendData"
|
||||
:loading="sendLoading"
|
||||
@@ -208,8 +209,11 @@ onMounted(() => {
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="recipient"
|
||||
card-subtitle-key="subject"
|
||||
card-badge-key="status"
|
||||
:card-fields="['template_code', 'retry_count', 'sent_at', { key: 'error_message', full: true }]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="sendPagination.total" />
|
||||
@@ -246,7 +250,7 @@ onMounted(() => {
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
<ResponsiveTable
|
||||
:columns="codeColumns"
|
||||
:data="codeData"
|
||||
:loading="codeLoading"
|
||||
@@ -254,8 +258,11 @@ onMounted(() => {
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="email"
|
||||
card-subtitle-key="code"
|
||||
card-badge-key="purpose"
|
||||
:card-fields="['expires_at', 'used_at', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="codePagination.total" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ============================================================
|
||||
@@ -208,7 +209,7 @@ onMounted(() => { fetchStats(); fetchList() })
|
||||
<PageHeader title="反馈管理" description="查看和管理租户用户提交的反馈" />
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-5 gap-4 mb-4">
|
||||
<div class="grid grid-cols-5 gap-4 mb-4 max-md:grid-cols-3 max-md:gap-3">
|
||||
<ACard v-for="card in statCards" :key="card.key" :bordered="false" class="text-center">
|
||||
<div class="text-3xl font-bold" :class="card.color ? `text-${card.color}-500` : 'text-gray-700'">
|
||||
{{ (stats as any)[card.key] || 0 }}
|
||||
@@ -228,7 +229,35 @@ onMounted(() => { fetchStats(); fetchList() })
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="data" :loading="loading" row-key="id" :scroll="{ x: 1300 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1300 }"
|
||||
:card-fields="['category', 'user_display_name', 'created_at']"
|
||||
>
|
||||
<template #card-header="{ row }">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-base font-semibold text-[var(--color-text-1)]">
|
||||
{{ row.title }}
|
||||
</div>
|
||||
<div v-if="row.tenant_name" class="mt-0.5 truncate text-xs text-[var(--color-text-3)]">
|
||||
{{ row.tenant_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 flex-col items-end gap-1">
|
||||
<ATag :color="statusColorMap[row.status]" size="small">
|
||||
{{ statusLabelMap[row.status] || row.status }}
|
||||
</ATag>
|
||||
<ATag :color="priorityColorMap[row.priority]" size="small">
|
||||
{{ priorityLabelMap[row.priority] || row.priority }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ResponsiveTable>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -326,7 +327,17 @@ onMounted(fetchList)
|
||||
<ASelect v-model="typeFilter" :options="typeOptions" style="width: 120px" allow-clear placeholder="类型筛选" @change="() => { pagination.current = 1; fetchList() }" />
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="messages" :loading="loading" row-key="id" :scroll="{ x: 1100 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="messages"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1100 }"
|
||||
card-title-key="title"
|
||||
card-subtitle-key="tenant_name"
|
||||
card-badge-key="type"
|
||||
:card-fields="['user_name', 'channel', 'is_read', 'is_broadcast', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -151,7 +152,17 @@ onMounted(fetchTemplates)
|
||||
</PageHeader>
|
||||
|
||||
<ACard :bordered="false">
|
||||
<ATable :columns="columns" :data="templates" :loading="loading" row-key="code" :scroll="{ x: 1000 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="templates"
|
||||
:loading="loading"
|
||||
row-key="code"
|
||||
:scroll="{ x: 1000 }"
|
||||
card-title-key="code"
|
||||
card-subtitle-key="subject"
|
||||
card-badge-key="channel"
|
||||
:card-fields="['status', 'updated_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchTemplates" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchTemplates() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -288,7 +289,35 @@ onMounted(fetchTickets)
|
||||
</ASpace>
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="tickets" :loading="loading" row-key="id" :scroll="{ x: 1300 }" :pagination="false" />
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="tickets"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1300 }"
|
||||
:card-fields="['category', 'user_name', 'assigned_admin_name', 'created_at']"
|
||||
>
|
||||
<template #card-header="{ row }">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-base font-semibold text-[var(--color-text-1)]">
|
||||
{{ row.title }}
|
||||
</div>
|
||||
<div v-if="row.tenant_name" class="mt-0.5 truncate text-xs text-[var(--color-text-3)]">
|
||||
{{ row.tenant_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 flex-col items-end gap-1">
|
||||
<ATag :color="statusColor[row.status]" size="small">
|
||||
{{ statusLabel[row.status] || row.status }}
|
||||
</ATag>
|
||||
<ATag :color="urgencyColor[row.urgency]" size="small">
|
||||
{{ urgencyLabel[row.urgency] || row.urgency }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ResponsiveTable>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-size-options="pagination.pageSizeOptions" show-page-size @change="fetchTickets" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchTickets() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -242,7 +243,7 @@ const { exporting, exportFile } = useExport({
|
||||
</ASpace>
|
||||
</div>
|
||||
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="opColumns"
|
||||
:data="opData"
|
||||
:loading="opLoading"
|
||||
@@ -250,8 +251,11 @@ const { exporting, exportFile } = useExport({
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="action"
|
||||
card-subtitle-key="ip_address"
|
||||
card-badge-key="user_type"
|
||||
:card-fields="['user_id', 'resource_type', 'detail', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="opPagination.total" />
|
||||
@@ -295,7 +299,7 @@ const { exporting, exportFile } = useExport({
|
||||
</ASpace>
|
||||
</div>
|
||||
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="sensColumns"
|
||||
:data="sensData"
|
||||
:loading="sensLoading"
|
||||
@@ -303,8 +307,11 @@ const { exporting, exportFile } = useExport({
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="action"
|
||||
card-subtitle-key="resource_type"
|
||||
card-badge-key="user_type"
|
||||
:card-fields="['user_id', 'reason', 'ip_address', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="sensPagination.total" />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { Message, Tag } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const list = ref<any[]>([])
|
||||
@@ -113,6 +115,56 @@ function onDateChange(dateString: string | undefined, type: 'start' | 'end') {
|
||||
searchForm.end_time = dateString || ''
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '时间', dataIndex: 'created_at', width: 180 },
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'username',
|
||||
width: 160,
|
||||
render({ record }: any) {
|
||||
return h('span', [
|
||||
record.display_name || record.username,
|
||||
record.display_name ? h('span', { style: 'color: var(--ta-text-tertiary); margin-left: 4px' }, `(${record.username})`) : null,
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录方式',
|
||||
dataIndex: 'login_method',
|
||||
width: 110,
|
||||
render({ record }: any) {
|
||||
return h(Tag, { color: methodColor(record.login_method), size: 'small' }, () => methodLabel(record.login_method))
|
||||
},
|
||||
},
|
||||
{ title: 'IP 地址', dataIndex: 'ip_address', width: 150 },
|
||||
{ title: '设备', dataIndex: 'user_agent', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'success',
|
||||
width: 80,
|
||||
render({ record }: any) {
|
||||
return h(Tag, { color: record.success ? 'green' : 'red', size: 'small' }, () => record.success ? '成功' : '失败')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '新设备',
|
||||
dataIndex: 'is_new_device',
|
||||
width: 80,
|
||||
render({ record }: any) {
|
||||
return record.is_new_device ? h(Tag, { color: 'orangered', size: 'small' }, () => '新设备') : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
dataIndex: 'fail_reason',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render({ record }: any) {
|
||||
return record.fail_reason || '-'
|
||||
},
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -149,48 +201,18 @@ function onDateChange(dateString: string | undefined, type: 'start' | 'end') {
|
||||
</ACard>
|
||||
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #columns>
|
||||
<ATableColumn title="时间" data-index="created_at" :width="180" />
|
||||
<ATableColumn title="用户" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ record.display_name || record.username }}</span>
|
||||
<span v-if="record.display_name" style="color: var(--ta-text-tertiary); margin-left: 4px">({{ record.username }})</span>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="登录方式" data-index="login_method" :width="110">
|
||||
<template #cell="{ record }">
|
||||
<ATag :color="methodColor(record.login_method)" size="small">{{ methodLabel(record.login_method) }}</ATag>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="IP 地址" data-index="ip_address" :width="150" />
|
||||
<ATableColumn title="设备" data-index="user_agent" :width="200" ellipsis />
|
||||
<ATableColumn title="状态" :width="80">
|
||||
<template #cell="{ record }">
|
||||
<ATag :color="record.success ? 'green' : 'red'" size="small">{{ record.success ? '成功' : '失败' }}</ATag>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="新设备" :width="80">
|
||||
<template #cell="{ record }">
|
||||
<ATag v-if="record.is_new_device" color="orangered" size="small">新设备</ATag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="失败原因" data-index="fail_reason" :width="180" ellipsis>
|
||||
<template #cell="{ record }">
|
||||
<span v-if="record.fail_reason">{{ record.fail_reason }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
</template>
|
||||
</ATable>
|
||||
card-title-key="username"
|
||||
card-subtitle-key="ip_address"
|
||||
card-badge-key="success"
|
||||
:card-fields="['login_method', 'is_new_device', 'created_at', { key: 'fail_reason', full: true }]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -58,14 +59,16 @@ onMounted(fetchPermissions)
|
||||
</PageHeader>
|
||||
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="groups"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="name"
|
||||
card-subtitle-key="label"
|
||||
:card-fields="['count', { key: 'permissions', full: true }]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="groups.length">
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<template>
|
||||
<div class="security-settings-page">
|
||||
<a-card title="双因素认证(2FA)" :bordered="false" class="mb-4">
|
||||
<template v-if="!totpEnabled">
|
||||
<a-alert type="info" class="mb-4">
|
||||
启用双因素认证后,登录时需要输入动态验证码,大幅提升账号安全性。
|
||||
</a-alert>
|
||||
<a-button type="primary" @click="handleSetup" :loading="setupLoading">
|
||||
开启 2FA
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-alert type="success" class="mb-4">已启用双因素认证</a-alert>
|
||||
<a-space>
|
||||
<a-button @click="showRegenerateModal = true">重新生成恢复码</a-button>
|
||||
<a-button status="danger" @click="showDisableModal = true">禁用 2FA</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-card>
|
||||
|
||||
<!-- Setup Modal -->
|
||||
<a-modal v-model:visible="showSetupModal" title="设置双因素认证" :maskClosable="false" :footer="false" width="480px">
|
||||
<div v-if="setupStep === 1">
|
||||
<a-alert type="info" class="mb-4">请使用身份验证器 App(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码。</a-alert>
|
||||
<div class="qr-container">
|
||||
<img :src="qrCodeUrl" alt="QR Code" v-if="qrCodeUrl" class="qr-image" />
|
||||
</div>
|
||||
<div class="secret-key">
|
||||
<span class="label">手动输入密钥:</span>
|
||||
<a-typography-text copyable code>{{ setupSecret }}</a-typography-text>
|
||||
</div>
|
||||
<a-button type="primary" long @click="setupStep = 2" class="mt-4">我已扫描,下一步</a-button>
|
||||
</div>
|
||||
<div v-if="setupStep === 2">
|
||||
<a-form :model="enableForm" @submit-success="handleEnable" layout="vertical">
|
||||
<a-form-item label="输入验证码" field="code" :rules="[{ required: true, message: '请输入6位验证码' }, { length: 6, message: '验证码为6位' }]">
|
||||
<a-input v-model="enableForm.code" placeholder="6位数字验证码" maxlength="6" />
|
||||
</a-form-item>
|
||||
<a-form-item label="当前密码" field="password" :rules="[{ required: true, message: '请输入密码' }]">
|
||||
<a-input-password v-model="enableForm.password" placeholder="请输入当前密码以确认" />
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit" long :loading="enableLoading">确认启用</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- Backup Codes Modal -->
|
||||
<a-modal v-model:visible="showBackupCodesModal" title="备用恢复码" :maskClosable="false" width="400px">
|
||||
<a-alert type="warning" class="mb-4">请妥善保存以下恢复码。每个恢复码只能使用一次。关闭后无法再次查看。</a-alert>
|
||||
<div class="backup-codes-grid">
|
||||
<a-tag v-for="code in backupCodes" :key="code" size="large" class="backup-code">{{ code }}</a-tag>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button type="primary" @click="showBackupCodesModal = false">我已保存</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- Disable Modal -->
|
||||
<a-modal v-model:visible="showDisableModal" title="禁用双因素认证" @ok="handleDisable" :maskClosable="false">
|
||||
<a-form :model="disableForm" layout="vertical">
|
||||
<a-form-item label="输入验证码或恢复码">
|
||||
<a-input v-model="disableForm.code" placeholder="6位验证码或8位恢复码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- Regenerate Backup Codes Modal -->
|
||||
<a-modal v-model:visible="showRegenerateModal" title="重新生成恢复码" @ok="handleRegenerate" :maskClosable="false">
|
||||
<a-form :model="{ code: regenerateCode }" layout="vertical">
|
||||
<a-form-item label="输入验证码">
|
||||
<a-input v-model="regenerateCode" placeholder="6位数字验证码" maxlength="6" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const totpEnabled = ref(false)
|
||||
const setupLoading = ref(false)
|
||||
const enableLoading = ref(false)
|
||||
const showSetupModal = ref(false)
|
||||
const showBackupCodesModal = ref(false)
|
||||
const showDisableModal = ref(false)
|
||||
const showRegenerateModal = ref(false)
|
||||
const setupStep = ref(1)
|
||||
const setupSecret = ref('')
|
||||
const qrCodeUrl = ref('')
|
||||
const backupCodes = ref<string[]>([])
|
||||
|
||||
const enableForm = reactive({ code: '', password: '' })
|
||||
const disableForm = reactive({ code: '' })
|
||||
const regenerateCode = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
check2FAStatus()
|
||||
})
|
||||
|
||||
async function check2FAStatus() {
|
||||
try {
|
||||
const res = await request.get('/api/admin/auth/sessions')
|
||||
// If we can make authenticated requests, check profile for 2FA status
|
||||
// For now, we'll get this from a separate endpoint
|
||||
const profileRes = await request.get('/api/admin/users/profile')
|
||||
if (profileRes.data?.code === 0) {
|
||||
totpEnabled.value = profileRes.data.data?.totp_enabled || false
|
||||
}
|
||||
} catch {
|
||||
// Ignore - might not have a profile endpoint
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetup() {
|
||||
setupLoading.value = true
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/setup')
|
||||
setupSecret.value = res.data.data.secret
|
||||
// Generate QR code URL using a chart API
|
||||
qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(res.data.data.uri)}`
|
||||
setupStep.value = 1
|
||||
showSetupModal.value = true
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
} finally {
|
||||
setupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEnable() {
|
||||
enableLoading.value = true
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/enable', enableForm)
|
||||
totpEnabled.value = true
|
||||
showSetupModal.value = false
|
||||
backupCodes.value = res.data.data.backup_codes || []
|
||||
showBackupCodesModal.value = true
|
||||
Message.success('双因素认证已启用')
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
} finally {
|
||||
enableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable() {
|
||||
if (!disableForm.code) {
|
||||
Message.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/disable', disableForm)
|
||||
totpEnabled.value = false
|
||||
showDisableModal.value = false
|
||||
disableForm.code = ''
|
||||
Message.success('双因素认证已禁用')
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegenerate() {
|
||||
if (!regenerateCode.value) {
|
||||
Message.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/backup-codes', { code: regenerateCode.value })
|
||||
regenerateCode.value = ''
|
||||
showRegenerateModal.value = false
|
||||
backupCodes.value = res.data.data.backup_codes || []
|
||||
showBackupCodesModal.value = true
|
||||
Message.success('恢复码已重新生成')
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.security-settings-page {
|
||||
padding: 0;
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.qr-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
.qr-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
.secret-key {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.secret-key .label {
|
||||
color: var(--color-text-2);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.backup-codes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.backup-code {
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -158,7 +159,7 @@ onMounted(() => {
|
||||
</ACard>
|
||||
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
@@ -166,7 +167,10 @@ onMounted(() => {
|
||||
:stripe="true"
|
||||
row-key="id"
|
||||
:row-class="(record: any) => record?.is_current ? 'current-session-row' : ''"
|
||||
:pagination="false"
|
||||
card-title-key="username"
|
||||
card-subtitle-key="ip_address"
|
||||
card-badge-key="is_current"
|
||||
:card-fields="['device_info', 'created_at', 'expires_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { Tag } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const list = ref<any[]>([])
|
||||
@@ -131,6 +134,57 @@ function onDateChange(dateString: string | undefined, type: 'start' | 'end') {
|
||||
searchForm.end_time = dateString || ''
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '时间', dataIndex: 'created_at', width: 180 },
|
||||
{ title: '租户ID', dataIndex: 'tenant_id', width: 90 },
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'username',
|
||||
width: 160,
|
||||
render({ record }: any) {
|
||||
return h('span', [
|
||||
record.display_name || record.username,
|
||||
record.display_name ? h('span', { style: 'color: var(--ta-text-tertiary); margin-left: 4px' }, `(${record.username})`) : null,
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录方式',
|
||||
dataIndex: 'login_method',
|
||||
width: 110,
|
||||
render({ record }: any) {
|
||||
return h(Tag, { color: methodColor(record.login_method), size: 'small' }, () => methodLabel(record.login_method))
|
||||
},
|
||||
},
|
||||
{ title: 'IP 地址', dataIndex: 'ip_address', width: 150 },
|
||||
{ title: '设备', dataIndex: 'user_agent', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'success',
|
||||
width: 80,
|
||||
render({ record }: any) {
|
||||
return h(Tag, { color: record.success ? 'green' : 'red', size: 'small' }, () => record.success ? '成功' : '失败')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '新设备',
|
||||
dataIndex: 'is_new_device',
|
||||
width: 80,
|
||||
render({ record }: any) {
|
||||
return record.is_new_device ? h(Tag, { color: 'orangered', size: 'small' }, () => '新设备') : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
dataIndex: 'fail_reason',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render({ record }: any) {
|
||||
return record.fail_reason || '-'
|
||||
},
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -179,49 +233,18 @@ function onDateChange(dateString: string | undefined, type: 'start' | 'end') {
|
||||
</ACard>
|
||||
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #columns>
|
||||
<ATableColumn title="时间" data-index="created_at" :width="180" />
|
||||
<ATableColumn title="租户ID" data-index="tenant_id" :width="90" />
|
||||
<ATableColumn title="用户" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<span>{{ record.display_name || record.username }}</span>
|
||||
<span v-if="record.display_name" style="color: var(--ta-text-tertiary); margin-left: 4px">({{ record.username }})</span>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="登录方式" data-index="login_method" :width="110">
|
||||
<template #cell="{ record }">
|
||||
<ATag :color="methodColor(record.login_method)" size="small">{{ methodLabel(record.login_method) }}</ATag>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="IP 地址" data-index="ip_address" :width="150" />
|
||||
<ATableColumn title="设备" data-index="user_agent" :width="200" ellipsis />
|
||||
<ATableColumn title="状态" :width="80">
|
||||
<template #cell="{ record }">
|
||||
<ATag :color="record.success ? 'green' : 'red'" size="small">{{ record.success ? '成功' : '失败' }}</ATag>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="新设备" :width="80">
|
||||
<template #cell="{ record }">
|
||||
<ATag v-if="record.is_new_device" color="orangered" size="small">新设备</ATag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
<ATableColumn title="失败原因" data-index="fail_reason" :width="180" ellipsis>
|
||||
<template #cell="{ record }">
|
||||
<span v-if="record.fail_reason">{{ record.fail_reason }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ATableColumn>
|
||||
</template>
|
||||
</ATable>
|
||||
card-title-key="username"
|
||||
card-subtitle-key="ip_address"
|
||||
card-badge-key="success"
|
||||
:card-fields="['tenant_id', 'login_method', 'is_new_device', 'created_at', { key: 'fail_reason', full: true }]"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
@@ -476,10 +477,14 @@ onMounted(() => {
|
||||
<AButton @click="resetFilter">重置</AButton>
|
||||
</ASpace>
|
||||
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="columns" :data="data" :loading="loading"
|
||||
:scroll="{ x: 1200 }" :bordered="false" :stripe="true" size="small"
|
||||
:pagination="false" row-key="id"
|
||||
row-key="id"
|
||||
card-title-key="original_name"
|
||||
card-subtitle-key="size"
|
||||
card-badge-key="category"
|
||||
:card-fields="['tenant_id', 'user_id', 'mime_type', 'storage_provider', 'created_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag, Button, Space, Message, Modal } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -159,7 +160,7 @@ onMounted(() => { fetchCategories() })
|
||||
</Space>
|
||||
</div>
|
||||
</template>
|
||||
<ATable :columns="columns" :data="articles" :loading="loading" row-key="id" :scroll="{ x: 1300 }" :pagination="false" />
|
||||
<ResponsiveTable :columns="columns" :data="articles" :loading="loading" row-key="id" :scroll="{ x: 1300 }" card-title-key="title" card-subtitle-key="category_id" card-badge-key="status" :card-fields="['slug', 'view_count', 'sort_order', 'published_at', 'created_at']" />
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tag, Button, Space, Message, Modal } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -86,7 +87,7 @@ onMounted(fetchList)
|
||||
</template>
|
||||
</PageHeader>
|
||||
<ACard :bordered="false">
|
||||
<ATable :columns="columns" :data="categories" :loading="loading" row-key="id" :scroll="{ x: 1100 }" :pagination="false" />
|
||||
<ResponsiveTable :columns="columns" :data="categories" :loading="loading" row-key="id" :scroll="{ x: 1100 }" card-title-key="name" card-subtitle-key="slug" card-badge-key="is_visible" :card-fields="['description', 'sort_order', 'article_count', 'created_at']" />
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
<APagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize" :total="pagination.total" show-page-size @change="fetchList" @page-size-change="(s: number) => { pagination.pageSize = s; pagination.current = 1; fetchList() }" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -305,15 +306,18 @@ onMounted(() => {
|
||||
|
||||
<!-- Table -->
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:scroll="{ x: 1000 }"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
card-title-key="name"
|
||||
card-subtitle-key="description"
|
||||
card-badge-key="status"
|
||||
:card-fields="['category', 'version', 'author']"
|
||||
/>
|
||||
<AEmpty v-if="!loading && data.length === 0" style="margin-top: 40px">
|
||||
<template #description>暂无已注册的插件</template>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { TableColumnData, FormInstance } from '@arco-design/web-vue'
|
||||
import { IconSync } from '@arco-design/web-vue/es/icon'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import TableStats from '@/components/TableStats.vue'
|
||||
import ResponsiveTable from '@/components/ResponsiveTable.vue'
|
||||
import request from '@/utils/request'
|
||||
import { useExport } from '@/composables/useExport'
|
||||
|
||||
@@ -275,15 +276,18 @@ const { exporting, exportFile } = useExport({
|
||||
|
||||
<!-- Table Card -->
|
||||
<ACard :bordered="false">
|
||||
<ATable
|
||||
<ResponsiveTable
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:scroll="{ x: 1200 }"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
card-title-key="username"
|
||||
card-subtitle-key="email"
|
||||
card-badge-key="role"
|
||||
:card-fields="['status', 'last_login_at']"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<TableStats :total="pagination.total" />
|
||||
|
||||
Reference in new issue
Block a user