improve(ui):优化移动端显示效果

This commit is contained in:
chengqian committed 2026-08-13 16:44:59 +08:00
1 parent be8275a8c5
commit cbffa01689
33 files changed
+474 -470

No files matched your search

+6 -2
View File
@@ -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 判断。
-120
View File
@@ -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>
+12 -1
View File
@@ -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() }" />
+12 -1
View File
@@ -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() }" />
+12 -1
View File
@@ -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" />
+12 -1
View File
@@ -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" />
+8 -3
View File
@@ -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" />
+31 -2
View File
@@ -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() }" />
+12 -1
View File
@@ -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() }" />
+30 -1
View File
@@ -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() }" />
+11 -4
View File
@@ -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 -2
View File
@@ -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 -2
View File
@@ -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>
+6 -2
View File
@@ -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" />