修复代理商卡密管理页面:增加服务端筛选、批量导出、复制功能
This commit is contained in:
@@ -5,6 +5,7 @@ import { h } from 'vue'
|
||||
|
||||
import type { Card } from '../data/schema'
|
||||
|
||||
import { Copy } from '@/components/sva-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
|
||||
export function getColumns(t: Composer['t']): ColumnDef<Card>[] {
|
||||
@@ -14,7 +15,11 @@ export function getColumns(t: Composer['t']): ColumnDef<Card>[] {
|
||||
header: () => t('agent.cards.columns.code'),
|
||||
cell: ({ row }) => {
|
||||
const code = row.getValue('code') as string
|
||||
return h('span', { class: 'font-mono text-sm' }, code)
|
||||
if (!code) return '-'
|
||||
return h('div', { class: 'flex items-center space-x-2' }, [
|
||||
h('code', { class: 'text-xs bg-muted px-2 py-1 rounded font-mono' }, code),
|
||||
h(Copy, { class: 'h-4 w-4', size: 'sm', content: code }),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -22,7 +27,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Card>[] {
|
||||
header: () => t('agent.cards.columns.appName'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('app_name') as string
|
||||
return h(Badge, { variant: 'secondary' }, () => appName)
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -30,7 +35,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Card>[] {
|
||||
header: () => t('agent.cards.columns.cardTypeName'),
|
||||
cell: ({ row }) => {
|
||||
const cardTypeName = row.getValue('card_type_name') as string
|
||||
return h('span', { class: 'font-medium' }, cardTypeName)
|
||||
return cardTypeName ? h(Badge, { variant: 'outline' }, () => cardTypeName) : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -57,7 +62,13 @@ export function getColumns(t: Composer['t']): ColumnDef<Card>[] {
|
||||
try {
|
||||
const date = new Date(createdAt)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
catch {
|
||||
return '-'
|
||||
@@ -73,7 +84,13 @@ export function getColumns(t: Composer['t']): ColumnDef<Card>[] {
|
||||
try {
|
||||
const date = new Date(usedAt)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
catch {
|
||||
return '-'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import type { Card } from '@/pages/agent/cards/data/schema'
|
||||
|
||||
@@ -18,18 +20,30 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const searchModel = computed({
|
||||
get: () => props.searchFilter,
|
||||
set: (value: string) => emit('update:searchFilter', value),
|
||||
})
|
||||
const isFiltered = computed(() => !!props.searchFilter)
|
||||
|
||||
function resetFilters() {
|
||||
emit('update:searchFilter', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiInput
|
||||
v-model="searchModel"
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('agent.cards.searchPlaceholder')"
|
||||
class="h-8 w-[200px] lg:w-[250px]"
|
||||
:model-value="searchFilter"
|
||||
class="h-8 w-[200px] lg:w-[300px]"
|
||||
@update:model-value="emit('update:searchFilter', $event as string)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
v-if="isFiltered"
|
||||
variant="ghost"
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('agent.cards.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export interface Card {
|
||||
id: number
|
||||
code: string
|
||||
application_id: number
|
||||
card_type_id: number
|
||||
app_name: string
|
||||
card_type_name: string
|
||||
status: string
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Key, Loader2, Plus } from 'lucide-vue-next'
|
||||
import { CheckCircle, Key, Plus, Users } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { Card } from '@/pages/agent/cards/data/schema'
|
||||
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/agent/cards/components/data-table.vue'
|
||||
import api, { BASE_URL } from '@/services/api'
|
||||
@@ -13,31 +15,67 @@ import api, { BASE_URL } from '@/services/api'
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CardType {
|
||||
id: number
|
||||
name: string
|
||||
application_id: number
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const cards = ref<Card[]>([])
|
||||
const total = ref(0)
|
||||
const unusedCount = ref(0)
|
||||
const usedCount = ref(0)
|
||||
const expiredCount = ref(0)
|
||||
const disabledCount = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const tableRef = ref()
|
||||
const searchFilter = ref('')
|
||||
|
||||
const filteredCards = computed(() => {
|
||||
let result = cards.value.filter(c => c)
|
||||
if (searchFilter.value) {
|
||||
const query = searchFilter.value.toLowerCase()
|
||||
result = result.filter(c =>
|
||||
c.code?.toLowerCase().includes(query)
|
||||
|| c.app_name?.toLowerCase().includes(query)
|
||||
|| c.card_type_name?.toLowerCase().includes(query),
|
||||
const applications = ref<Application[]>([])
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
|
||||
const appFilter = ref<string>('')
|
||||
const cardTypeFilter = ref<string>('')
|
||||
const statusFilter = ref<string>('')
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const searchFilter = ref<string>('')
|
||||
|
||||
const filteredCardTypes = computed(() => {
|
||||
if (appFilter.value) {
|
||||
return cardTypes.value.filter(
|
||||
ct => String(ct.application_id) === appFilter.value,
|
||||
)
|
||||
}
|
||||
return result
|
||||
return cardTypes.value
|
||||
})
|
||||
|
||||
const unusedCount = computed(() => cards.value.filter(card => card.status === 'unused').length)
|
||||
const usedCount = computed(() => cards.value.filter(card => card.status === 'used').length)
|
||||
const expiredCount = computed(() => cards.value.filter(card => card.status === 'expired').length)
|
||||
const disabledCount = computed(() => cards.value.filter(card => card.status === 'disabled').length)
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: String(app.id),
|
||||
}))
|
||||
})
|
||||
|
||||
const cardTypeOptions = computed(() => {
|
||||
return filteredCardTypes.value.map(ct => ({
|
||||
label: ct.name,
|
||||
value: String(ct.id),
|
||||
}))
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('agent.cards.status.unused'), value: 'unused' },
|
||||
{ label: t('agent.cards.status.used'), value: 'used' },
|
||||
{ label: t('agent.cards.status.expired'), value: 'expired' },
|
||||
{ label: t('agent.cards.status.disabled'), value: 'disabled' },
|
||||
])
|
||||
|
||||
const serverPagination = computed(() => ({
|
||||
page: currentPage.value,
|
||||
@@ -54,6 +92,33 @@ const serverPagination = computed(() => ({
|
||||
},
|
||||
}))
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ apps: Application[] }>('/agent/apps')
|
||||
applications.value = data?.apps || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCardTypes() {
|
||||
try {
|
||||
for (const app of applications.value) {
|
||||
const data = await api.get<{ cardTypes: CardType[] }>(`/agent/apps/${app.id}`)
|
||||
const types = data?.cardTypes || []
|
||||
for (const ct of types) {
|
||||
if (!cardTypes.value.some(existing => existing.id === ct.id)) {
|
||||
cardTypes.value.push({ ...ct, application_id: app.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取卡类列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCards() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -61,16 +126,36 @@ async function fetchCards() {
|
||||
params.append('page', String(currentPage.value))
|
||||
params.append('page_size', String(pageSize.value))
|
||||
|
||||
if (appFilter.value) {
|
||||
params.append('application_id', appFilter.value)
|
||||
}
|
||||
if (cardTypeFilter.value) {
|
||||
params.append('card_type_id', cardTypeFilter.value)
|
||||
}
|
||||
if (statusFilter.value) {
|
||||
params.append('status', statusFilter.value)
|
||||
}
|
||||
if (startDate.value) {
|
||||
params.append('start_date', startDate.value.split('T')[0])
|
||||
}
|
||||
if (endDate.value) {
|
||||
params.append('end_date', endDate.value.split('T')[0])
|
||||
}
|
||||
if (searchFilter.value) {
|
||||
params.append('search', searchFilter.value.trim())
|
||||
}
|
||||
|
||||
const data = await api.get<any>(`/agent/cards?${params.toString()}`)
|
||||
if (Array.isArray(data)) {
|
||||
if (data?.cards) {
|
||||
cards.value = data.cards
|
||||
total.value = data.filtered_total || data.total || data.cards.length
|
||||
unusedCount.value = data.unused_count || 0
|
||||
usedCount.value = data.used_count || 0
|
||||
expiredCount.value = data.expired_count || 0
|
||||
disabledCount.value = data.disabled_count || 0
|
||||
} else if (Array.isArray(data)) {
|
||||
cards.value = data
|
||||
total.value = data.length
|
||||
} else if (data?.cards) {
|
||||
cards.value = data.cards
|
||||
total.value = data.total || data.cards.length
|
||||
} else if (data?.data) {
|
||||
cards.value = Array.isArray(data.data) ? data.data : []
|
||||
total.value = data.total || cards.value.length
|
||||
} else {
|
||||
cards.value = []
|
||||
total.value = 0
|
||||
@@ -92,7 +177,24 @@ function goToCreate() {
|
||||
|
||||
function handleExport() {
|
||||
const token = localStorage.getItem('token')
|
||||
const url = `${BASE_URL}/agent/cards/export?token=${token}`
|
||||
const params = new URLSearchParams()
|
||||
if (appFilter.value)
|
||||
params.append('application_id', appFilter.value)
|
||||
if (cardTypeFilter.value)
|
||||
params.append('card_type_id', cardTypeFilter.value)
|
||||
if (statusFilter.value)
|
||||
params.append('status', statusFilter.value)
|
||||
if (startDate.value) {
|
||||
const startDateOnly = startDate.value.includes('T') ? startDate.value.split('T')[0] : startDate.value
|
||||
params.append('start_date', startDateOnly)
|
||||
}
|
||||
if (endDate.value) {
|
||||
const endDateOnly = endDate.value.includes('T') ? endDate.value.split('T')[0] : endDate.value
|
||||
params.append('end_date', endDateOnly)
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${BASE_URL}/agent/cards/export?${queryString}&token=${token}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
@@ -105,7 +207,18 @@ function handleBatchExport(ids: (string | number)[]) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch(() => appFilter.value, () => {
|
||||
cardTypeFilter.value = ''
|
||||
})
|
||||
|
||||
watch([appFilter, cardTypeFilter, statusFilter, startDate, endDate, searchFilter], () => {
|
||||
currentPage.value = 1
|
||||
fetchCards()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchApplications()
|
||||
await fetchCardTypes()
|
||||
fetchCards()
|
||||
})
|
||||
</script>
|
||||
@@ -148,7 +261,7 @@ onMounted(() => {
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('agent.cards.unused') }}
|
||||
</UiCardTitle>
|
||||
<Key class="size-4 text-muted-foreground" />
|
||||
<CheckCircle class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
@@ -162,7 +275,7 @@ onMounted(() => {
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('agent.cards.used') }}
|
||||
</UiCardTitle>
|
||||
<Key class="size-4 text-muted-foreground" />
|
||||
<Users class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
@@ -188,22 +301,39 @@ onMounted(() => {
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
v-else
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="filteredCards"
|
||||
:data="cards"
|
||||
:server-pagination="serverPagination"
|
||||
:search-filter="searchFilter"
|
||||
@refresh="fetchCards"
|
||||
@batch-export="handleBatchExport(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
@export="handleExport"
|
||||
@update:search-filter="searchFilter = $event"
|
||||
/>
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('agent.cards.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="cardTypeFilter"
|
||||
:title="t('agent.cards.cardType')"
|
||||
:options="cardTypeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('agent.cards.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="startDate"
|
||||
v-model:end-model-value="endDate"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
@@ -2824,6 +2824,9 @@
|
||||
"searchPlaceholder": "Search card code, app or card type...",
|
||||
"exportBtn": "Export",
|
||||
"batchExportBtn": "Batch Export",
|
||||
"application": "Application",
|
||||
"cardType": "Card Type",
|
||||
"reset": "Reset",
|
||||
"select": "Select",
|
||||
"columns": {
|
||||
"code": "Card Code",
|
||||
|
||||
@@ -2825,6 +2825,9 @@
|
||||
"searchPlaceholder": "搜索卡号、应用或卡类...",
|
||||
"exportBtn": "导出",
|
||||
"batchExportBtn": "批量导出",
|
||||
"application": "应用",
|
||||
"cardType": "卡类",
|
||||
"reset": "重置",
|
||||
"select": "选择",
|
||||
"columns": {
|
||||
"code": "卡号",
|
||||
|
||||
Reference in New Issue
Block a user