mirror of
https://github.com/woodchen-ink/aimodels-prices.git
synced 2025-07-18 13:41:59 +08:00
Implement pagination and loading states for Prices and Providers views
This commit is contained in:
parent
b5faf07c6c
commit
e852b885c0
@ -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) {
|
||||||
|
@ -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>
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user