Implement pagination and loading states for Prices and Providers views

This commit is contained in:
wood chen 2025-02-09 18:56:56 +08:00
parent b5faf07c6c
commit e852b885c0
3 changed files with 170 additions and 16 deletions

View File

@ -3,6 +3,7 @@ package handlers
import ( import (
"database/sql" "database/sql"
"net/http" "net/http"
"strconv"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -12,12 +13,36 @@ import (
func GetPrices(c *gin.Context) { func GetPrices(c *gin.Context) {
db := c.MustGet("db").(*sql.DB) db := c.MustGet("db").(*sql.DB)
// 获取分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 获取总数
var total int
err := db.QueryRow("SELECT COUNT(*) FROM price").Scan(&total)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count prices"})
return
}
// 使用分页查询
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, model, billing_type, channel_type, currency, input_price, output_price, SELECT id, model, billing_type, channel_type, currency, input_price, output_price,
price_source, status, created_at, updated_at, created_by, price_source, status, created_at, updated_at, created_by,
temp_model, temp_billing_type, temp_channel_type, temp_currency, temp_model, temp_billing_type, temp_channel_type, temp_currency,
temp_input_price, temp_output_price, temp_price_source, updated_by temp_input_price, temp_output_price, temp_price_source, updated_by
FROM price ORDER BY created_at DESC`) FROM price
ORDER BY created_at DESC
LIMIT ? OFFSET ?`, pageSize, offset)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch prices"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch prices"})
return return
@ -39,7 +64,10 @@ func GetPrices(c *gin.Context) {
prices = append(prices, price) prices = append(prices, price)
} }
c.JSON(http.StatusOK, prices) c.JSON(http.StatusOK, gin.H{
"total": total,
"prices": prices,
})
} }
func CreatePrice(c *gin.Context) { func CreatePrice(c *gin.Context) {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="prices"> <div class="prices">
<el-card> <el-card v-loading="loading" element-loading-text="加载中...">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<div class="header-left"> <div class="header-left">
@ -43,10 +43,20 @@
</div> </div>
</div> </div>
<!-- 添加骨架屏 -->
<template v-if="loading">
<div v-for="i in 5" :key="i" class="skeleton-row">
<el-skeleton :rows="1" animated />
</div>
</template>
<el-table <el-table
v-else
:data="filteredPrices" :data="filteredPrices"
style="width: 100%" style="width: 100%"
@selection-change="handlePriceSelectionChange" @selection-change="handlePriceSelectionChange"
v-loading="tableLoading"
element-loading-text="加载中..."
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column label="模型"> <el-table-column label="模型">
@ -169,6 +179,19 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 添加分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card> </el-card>
<!-- 批量添加对话框 --> <!-- 批量添加对话框 -->
@ -337,7 +360,7 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import axios from 'axios' import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -392,13 +415,61 @@ const filteredPrices = computed(() => {
const editingPrice = ref(null) const editingPrice = ref(null)
const loading = ref(true)
const tableLoading = ref(true)
//
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const cachedPrices = ref(new Map()) //
const loadPrices = async () => { const loadPrices = async () => {
tableLoading.value = true
//
const cacheKey = `${currentPage.value}-${pageSize.value}-${selectedProvider.value}`
if (cachedPrices.value.has(cacheKey)) {
const cached = cachedPrices.value.get(cacheKey)
prices.value = cached.prices
total.value = cached.total
tableLoading.value = false
loading.value = false
return
}
try { try {
const { data } = await axios.get('/api/prices') const [pricesRes, providersRes] = await Promise.all([
prices.value = data axios.get('/api/prices', {
params: {
page: currentPage.value,
pageSize: pageSize.value
}
}),
axios.get('/api/providers')
])
prices.value = pricesRes.data.prices
total.value = pricesRes.data.total
providers.value = providersRes.data
//
cachedPrices.value.set(cacheKey, {
prices: pricesRes.data.prices,
total: pricesRes.data.total
})
//
if (cachedPrices.value.size > 10) {
const firstKey = cachedPrices.value.keys().next().value
cachedPrices.value.delete(firstKey)
}
} catch (error) { } catch (error) {
console.error('Failed to load prices:', error) console.error('Failed to load data:', error)
ElMessage.error('加载数据失败') ElMessage.error('加载数据失败')
} finally {
loading.value = false
tableLoading.value = false
} }
} }
@ -732,15 +803,26 @@ const batchUpdateStatus = async (status) => {
} }
} }
//
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
loadPrices()
}
const handleCurrentChange = (val) => {
currentPage.value = val
loadPrices()
}
//
watch(selectedProvider, () => {
currentPage.value = 1
loadPrices()
})
onMounted(async () => { onMounted(async () => {
await loadPrices() await loadPrices()
try {
const { data } = await axios.get('/api/providers')
providers.value = data
} catch (error) {
console.error('Failed to load providers:', error)
ElMessage.error('加载模型厂商数据失败')
}
}) })
</script> </script>
@ -900,4 +982,31 @@ onMounted(async () => {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
:deep(.el-loading-spinner) {
.el-loading-text {
color: #409EFF;
}
.path {
stroke: #409EFF;
}
}
.skeleton-row {
padding: 10px;
border-bottom: 1px solid #EBEEF5;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 添加表格行动画 */
:deep(.el-table__body-wrapper) {
.el-table__row {
transition: all 0.3s ease;
}
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="providers"> <div class="providers">
<el-card> <el-card v-loading="loading" element-loading-text="加载中...">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>模型厂商</span> <span>模型厂商</span>
@ -8,7 +8,7 @@
</div> </div>
</template> </template>
<el-table :data="sortedProviders" style="width: 100%"> <el-table :data="sortedProviders" style="width: 100%" v-loading="tableLoading" element-loading-text="加载中...">
<el-table-column prop="id" label="ID" /> <el-table-column prop="id" label="ID" />
<el-table-column label="名称"> <el-table-column label="名称">
<template #default="{ row }"> <template #default="{ row }">
@ -82,18 +82,26 @@ const router = useRouter()
const isAdmin = computed(() => props.user?.role === 'admin') const isAdmin = computed(() => props.user?.role === 'admin')
//
const loading = ref(true)
const tableLoading = ref(true)
// ID // ID
const sortedProviders = computed(() => { const sortedProviders = computed(() => {
return [...providers.value].sort((a, b) => a.id - b.id) return [...providers.value].sort((a, b) => a.id - b.id)
}) })
const loadProviders = async () => { const loadProviders = async () => {
tableLoading.value = true
try { try {
const { data } = await axios.get('/api/providers') const { data } = await axios.get('/api/providers')
providers.value = Array.isArray(data) ? data : [] providers.value = Array.isArray(data) ? data : []
} catch (error) { } catch (error) {
console.error('Failed to load providers:', error) console.error('Failed to load providers:', error)
ElMessage.error('加载数据失败') ElMessage.error('加载数据失败')
} finally {
loading.value = false
tableLoading.value = false
} }
} }
@ -214,4 +222,13 @@ const submitForm = async () => {
justify-content: flex-end; justify-content: flex-end;
gap: 1rem; gap: 1rem;
} }
:deep(.el-loading-spinner) {
.el-loading-text {
color: #409EFF;
}
.path {
stroke: #409EFF;
}
}
</style> </style>