实现内存缓存机制,优化数据库查询性能

- 新增内存缓存接口和实现,支持设置过期时间
- 在数据库初始化时创建全局缓存实例
- 为模型类型、提供商和价格查询添加缓存层
- 实现定期缓存常用数据的后台任务
- 优化数据库查询,减少重复查询和不必要的数据库访问
- 为价格查询添加索引,提高查询效率
This commit is contained in:
wood chen 2025-03-07 00:28:36 +08:00
parent 449f95d1b5
commit 9f51ac602e
9 changed files with 618 additions and 90 deletions

View File

@ -3,6 +3,8 @@ package database
import (
"fmt"
"log"
"sync"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
@ -15,6 +17,116 @@ import (
// DB 是数据库连接的全局实例
var DB *gorm.DB
// Cache 接口定义了缓存的基本操作
type Cache interface {
Get(key string) (interface{}, bool)
Set(key string, value interface{}, expiration time.Duration)
Delete(key string)
Clear()
}
// MemoryCache 是一个简单的内存缓存实现
type MemoryCache struct {
items map[string]cacheItem
mu sync.RWMutex
}
type cacheItem struct {
value interface{}
expiration int64
}
// 全局缓存实例
var GlobalCache Cache
// NewMemoryCache 创建一个新的内存缓存
func NewMemoryCache() *MemoryCache {
cache := &MemoryCache{
items: make(map[string]cacheItem),
}
// 启动一个后台协程定期清理过期项
go cache.janitor()
return cache
}
// Get 从缓存中获取值
func (c *MemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[key]
if !found {
return nil, false
}
// 检查是否过期
if item.expiration > 0 && item.expiration < time.Now().UnixNano() {
return nil, false
}
return item.value, true
}
// Set 设置缓存值
func (c *MemoryCache) Set(key string, value interface{}, expiration time.Duration) {
var exp int64
if expiration > 0 {
exp = time.Now().Add(expiration).UnixNano()
}
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = cacheItem{
value: value,
expiration: exp,
}
}
// Delete 删除缓存项
func (c *MemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
// Clear 清空所有缓存
func (c *MemoryCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = make(map[string]cacheItem)
}
// janitor 定期清理过期的缓存项
func (c *MemoryCache) janitor() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
<-ticker.C
c.deleteExpired()
}
}
// deleteExpired 删除所有过期的项
func (c *MemoryCache) deleteExpired() {
now := time.Now().UnixNano()
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if v.expiration > 0 && v.expiration < now {
delete(c.items, k)
}
}
}
// InitDB 初始化数据库连接
func InitDB(cfg *config.Config) error {
var err error
@ -43,8 +155,15 @@ func InitDB(cfg *config.Config) error {
}
// 设置连接池参数
sqlDB.SetMaxOpenConns(10)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetMaxOpenConns(20) // 增加最大连接数
sqlDB.SetMaxIdleConns(10) // 增加空闲连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 设置连接最大生命周期
// 初始化缓存
GlobalCache = NewMemoryCache()
// 启动定期缓存任务
go startCacheJobs()
// 自动迁移表结构
if err = migrateModels(); err != nil {
@ -54,6 +173,215 @@ func InitDB(cfg *config.Config) error {
return nil
}
// startCacheJobs 启动定期缓存任务
func startCacheJobs() {
// 每5分钟执行一次
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
// 立即执行一次
cacheCommonData()
for {
<-ticker.C
cacheCommonData()
}
}
// cacheCommonData 缓存常用数据
func cacheCommonData() {
log.Println("开始自动缓存常用数据...")
// 缓存所有模型类型
cacheModelTypes()
// 缓存所有提供商
cacheProviders()
// 缓存价格倍率
cachePriceRates()
log.Println("自动缓存常用数据完成")
}
// cacheModelTypes 缓存所有模型类型
func cacheModelTypes() {
var types []models.ModelType
if err := DB.Order("sort_order ASC, type_key ASC").Find(&types).Error; err != nil {
log.Printf("缓存模型类型失败: %v", err)
return
}
GlobalCache.Set("model_types", types, 30*time.Minute)
log.Printf("已缓存 %d 个模型类型", len(types))
}
// cacheProviders 缓存所有提供商
func cacheProviders() {
var providers []models.Provider
if err := DB.Order("id").Find(&providers).Error; err != nil {
log.Printf("缓存提供商失败: %v", err)
return
}
GlobalCache.Set("providers", providers, 30*time.Minute)
log.Printf("已缓存 %d 个提供商", len(providers))
}
// cachePriceRates 缓存价格倍率
func cachePriceRates() {
// 获取所有已批准的价格
var prices []models.Price
if err := DB.Where("status = 'approved'").Find(&prices).Error; err != nil {
log.Printf("缓存价格倍率失败: %v", err)
return
}
// 按模型分组
modelMap := make(map[string]map[uint]models.Price)
for _, price := range prices {
if _, exists := modelMap[price.Model]; !exists {
modelMap[price.Model] = make(map[uint]models.Price)
}
modelMap[price.Model][price.ChannelType] = price
}
// 计算倍率
type PriceRate struct {
Model string `json:"model"`
ModelType string `json:"model_type"`
Type string `json:"type"`
ChannelType uint `json:"channel_type"`
Input float64 `json:"input"`
Output float64 `json:"output"`
}
var rates []PriceRate
for model, providers := range modelMap {
// 找出基准价格通常是OpenAI的价格
var basePrice models.Price
var found bool
for _, price := range providers {
if price.ChannelType == 1 { // 假设OpenAI的ID是1
basePrice = price
found = true
break
}
}
if !found {
continue
}
// 计算其他厂商相对于基准价格的倍率
for channelType, price := range providers {
if channelType == 1 {
continue // 跳过基准价格
}
// 计算输入和输出的倍率
inputRate := 0.0
if basePrice.InputPrice > 0 {
inputRate = price.InputPrice / basePrice.InputPrice
}
outputRate := 0.0
if basePrice.OutputPrice > 0 {
outputRate = price.OutputPrice / basePrice.OutputPrice
}
rates = append(rates, PriceRate{
Model: model,
ModelType: price.ModelType,
Type: price.BillingType,
ChannelType: channelType,
Input: inputRate,
Output: outputRate,
})
}
}
GlobalCache.Set("price_rates", rates, 10*time.Minute)
log.Printf("已缓存 %d 个价格倍率", len(rates))
// 缓存常用的价格查询
cachePriceQueries()
}
// cachePriceQueries 缓存常用的价格查询
func cachePriceQueries() {
// 缓存第一页数据(无筛选条件)
cachePricePage(1, 20, "", "")
// 获取所有模型类型
var modelTypes []models.ModelType
if err := DB.Find(&modelTypes).Error; err != nil {
log.Printf("获取模型类型失败: %v", err)
return
}
// 获取所有提供商
var providers []models.Provider
if err := DB.Find(&providers).Error; err != nil {
log.Printf("获取提供商失败: %v", err)
return
}
// 为每种模型类型缓存第一页数据
for _, mt := range modelTypes {
cachePricePage(1, 20, "", mt.TypeKey)
}
// 为每个提供商缓存第一页数据
for _, p := range providers {
channelType := fmt.Sprintf("%d", p.ID)
cachePricePage(1, 20, channelType, "")
}
}
// cachePricePage 缓存特定页的价格数据
func cachePricePage(page, pageSize int, channelType, modelType string) {
offset := (page - 1) * pageSize
// 构建查询
query := DB.Model(&models.Price{})
// 添加筛选条件
if channelType != "" {
query = query.Where("channel_type = ?", channelType)
}
if modelType != "" {
query = query.Where("model_type = ?", modelType)
}
// 获取总数
var total int64
if err := query.Count(&total).Error; err != nil {
log.Printf("计算价格总数失败: %v", err)
return
}
// 获取分页数据
var prices []models.Price
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&prices).Error; err != nil {
log.Printf("获取价格数据失败: %v", err)
return
}
result := map[string]interface{}{
"total": total,
"data": prices,
}
// 构建缓存键
cacheKey := fmt.Sprintf("prices_page_%d_size_%d_channel_%s_type_%s",
page, pageSize, channelType, modelType)
// 存入缓存有效期5分钟
GlobalCache.Set(cacheKey, result, 5*time.Minute)
log.Printf("已缓存价格查询: %s", cacheKey)
}
// migrateModels 自动迁移模型到数据库表
func migrateModels() error {
// 自动迁移模型
@ -68,8 +396,5 @@ func migrateModels() error {
return err
}
// 这里可以添加其他模型的迁移
// 例如DB.AutoMigrate(&models.User{})
return nil
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
@ -11,6 +12,16 @@ import (
// GetModelTypes 获取所有模型类型
func GetModelTypes(c *gin.Context) {
cacheKey := "model_types"
// 尝试从缓存获取
if cachedData, found := database.GlobalCache.Get(cacheKey); found {
if types, ok := cachedData.([]models.ModelType); ok {
c.JSON(http.StatusOK, types)
return
}
}
var types []models.ModelType
// 使用GORM查询所有模型类型按排序字段和键值排序
@ -19,6 +30,9 @@ func GetModelTypes(c *gin.Context) {
return
}
// 存入缓存有效期30分钟
database.GlobalCache.Set(cacheKey, types, 30*time.Minute)
c.JSON(http.StatusOK, types)
}
@ -36,6 +50,9 @@ func CreateModelType(c *gin.Context) {
return
}
// 清除缓存
database.GlobalCache.Delete("model_types")
c.JSON(http.StatusCreated, newType)
}
@ -55,44 +72,19 @@ func UpdateModelType(c *gin.Context) {
return
}
// 如果key发生变化需要删除旧记录并创建新记录
if typeKey != updateType.TypeKey {
// 开始事务
tx := database.DB.Begin()
// 更新记录
existingType.TypeLabel = updateType.TypeLabel
existingType.SortOrder = updateType.SortOrder
// 删除旧记录
if err := tx.Delete(&existingType).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete old model type"})
return
}
// 创建新记录
if err := tx.Create(&updateType).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create new model type"})
return
}
// 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
return
}
} else {
// 直接更新,只更新特定字段
if err := database.DB.Model(&existingType).Updates(map[string]interface{}{
"type_label": updateType.TypeLabel,
"sort_order": updateType.SortOrder,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update model type"})
return
}
updateType = existingType
if err := database.DB.Save(&existingType).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, updateType)
// 清除缓存
database.GlobalCache.Delete("model_types")
c.JSON(http.StatusOK, existingType)
}
// DeleteModelType 删除模型类型
@ -120,9 +112,12 @@ func DeleteModelType(c *gin.Context) {
// 删除记录
if err := database.DB.Delete(&existingType).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete model type"})
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 清除缓存
database.GlobalCache.Delete("model_types")
c.JSON(http.StatusOK, gin.H{"message": "Model type deleted successfully"})
}

View File

@ -1,6 +1,7 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
@ -27,8 +28,20 @@ func GetPrices(c *gin.Context) {
offset := (page - 1) * pageSize
// 构建查询
query := database.DB.Model(&models.Price{})
// 构建缓存键
cacheKey := fmt.Sprintf("prices_page_%d_size_%d_channel_%s_type_%s",
page, pageSize, channelType, modelType)
// 尝试从缓存获取
if cachedData, found := database.GlobalCache.Get(cacheKey); found {
if result, ok := cachedData.(gin.H); ok {
c.JSON(http.StatusOK, result)
return
}
}
// 构建查询 - 使用索引优化
query := database.DB.Model(&models.Price{}).Select("*")
// 添加筛选条件
if channelType != "" {
@ -38,24 +51,44 @@ func GetPrices(c *gin.Context) {
query = query.Where("model_type = ?", modelType)
}
// 获取总数
// 获取总数 - 使用缓存优化
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count prices"})
return
totalCacheKey := fmt.Sprintf("prices_count_channel_%s_type_%s", channelType, modelType)
if cachedTotal, found := database.GlobalCache.Get(totalCacheKey); found {
if t, ok := cachedTotal.(int64); ok {
total = t
} else {
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count prices"})
return
}
database.GlobalCache.Set(totalCacheKey, total, 5*time.Minute)
}
} else {
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count prices"})
return
}
database.GlobalCache.Set(totalCacheKey, total, 5*time.Minute)
}
// 获取分页数据
// 获取分页数据 - 使用索引优化
var prices []models.Price
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&prices).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch prices"})
return
}
c.JSON(http.StatusOK, gin.H{
result := gin.H{
"total": total,
"data": prices,
})
}
// 存入缓存有效期5分钟
database.GlobalCache.Set(cacheKey, result, 5*time.Minute)
c.JSON(http.StatusOK, result)
}
func CreatePrice(c *gin.Context) {
@ -93,6 +126,9 @@ func CreatePrice(c *gin.Context) {
return
}
// 清除所有价格相关缓存
clearPriceCache()
c.JSON(http.StatusCreated, price)
}
@ -194,6 +230,9 @@ func UpdatePriceStatus(c *gin.Context) {
return
}
// 清除所有价格相关缓存
clearPriceCache()
c.JSON(http.StatusOK, gin.H{
"message": "Status updated successfully",
"status": input.Status,
@ -288,6 +327,9 @@ func UpdatePrice(c *gin.Context) {
}
}
// 清除所有价格相关缓存
clearPriceCache()
c.JSON(http.StatusOK, existingPrice)
}
@ -307,6 +349,9 @@ func DeletePrice(c *gin.Context) {
return
}
// 清除所有价格相关缓存
clearPriceCache()
c.JSON(http.StatusOK, gin.H{"message": "Price deleted successfully"})
}
@ -322,33 +367,45 @@ type PriceRate struct {
// GetPriceRates 获取价格倍率
func GetPriceRates(c *gin.Context) {
cacheKey := "price_rates"
// 尝试从缓存获取
if cachedData, found := database.GlobalCache.Get(cacheKey); found {
if rates, ok := cachedData.([]PriceRate); ok {
c.JSON(http.StatusOK, rates)
return
}
}
// 使用索引优化查询,只查询需要的字段
var prices []models.Price
if err := database.DB.Where("status = 'approved'").Find(&prices).Error; err != nil {
if err := database.DB.Select("model, model_type, billing_type, channel_type, input_price, output_price").
Where("status = 'approved'").
Find(&prices).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch prices"})
return
}
// 按模型分组
modelMap := make(map[string]map[uint]models.Price)
// 按模型分组 - 使用map优化
modelMap := make(map[string]map[uint]models.Price, len(prices)/2) // 预分配合理大小
for _, price := range prices {
if _, exists := modelMap[price.Model]; !exists {
modelMap[price.Model] = make(map[uint]models.Price)
modelMap[price.Model] = make(map[uint]models.Price, 5) // 假设每个模型有5个提供商
}
modelMap[price.Model][price.ChannelType] = price
}
// 预分配rates切片减少内存分配
rates := make([]PriceRate, 0, len(prices))
// 计算倍率
var rates []PriceRate
for model, providers := range modelMap {
// 找出基准价格通常是OpenAI的价格
var basePrice models.Price
var found bool
for _, price := range providers {
if price.ChannelType == 1 { // 假设OpenAI的ID是1
basePrice = price
found = true
break
}
if baseProvider, exists := providers[1]; exists { // 直接检查ID为1的提供商
basePrice = baseProvider
found = true
}
if !found {
@ -383,6 +440,9 @@ func GetPriceRates(c *gin.Context) {
}
}
// 存入缓存有效期10分钟
database.GlobalCache.Set(cacheKey, rates, 10*time.Minute)
c.JSON(http.StatusOK, rates)
}
@ -454,8 +514,17 @@ func ApproveAllPrices(c *gin.Context) {
return
}
// 清除所有价格相关缓存
clearPriceCache()
c.JSON(http.StatusOK, gin.H{
"message": "All pending prices approved successfully",
"count": len(pendingPrices),
})
}
// clearPriceCache 清除所有价格相关的缓存
func clearPriceCache() {
// 由于我们无法精确知道哪些缓存键与价格相关,所以清除所有缓存
database.GlobalCache.Clear()
}

View File

@ -13,6 +13,16 @@ import (
// GetProviders 获取所有模型厂商
func GetProviders(c *gin.Context) {
cacheKey := "providers"
// 尝试从缓存获取
if cachedData, found := database.GlobalCache.Get(cacheKey); found {
if providers, ok := cachedData.([]models.Provider); ok {
c.JSON(http.StatusOK, providers)
return
}
}
var providers []models.Provider
if err := database.DB.Order("id").Find(&providers).Error; err != nil {
@ -20,6 +30,9 @@ func GetProviders(c *gin.Context) {
return
}
// 存入缓存有效期30分钟
database.GlobalCache.Set(cacheKey, providers, 30*time.Minute)
c.JSON(http.StatusOK, providers)
}
@ -56,6 +69,9 @@ func CreateProvider(c *gin.Context) {
return
}
// 清除缓存
database.GlobalCache.Delete("providers")
c.JSON(http.StatusCreated, provider)
}
@ -127,6 +143,9 @@ func UpdateProvider(c *gin.Context) {
provider = existingProvider
}
// 清除缓存
database.GlobalCache.Delete("providers")
c.JSON(http.StatusOK, provider)
}
@ -212,5 +231,8 @@ func DeleteProvider(c *gin.Context) {
return
}
// 清除缓存
database.GlobalCache.Delete("providers")
c.JSON(http.StatusOK, gin.H{"message": "Provider deleted successfully"})
}

View File

@ -8,16 +8,16 @@ import (
type Price struct {
ID uint `json:"id" gorm:"primaryKey"`
Model string `json:"model" gorm:"not null"`
ModelType string `json:"model_type" gorm:"not null"` // text2text, text2image, etc.
BillingType string `json:"billing_type" gorm:"not null"` // tokens or times
ChannelType uint `json:"channel_type" gorm:"not null"`
Model string `json:"model" gorm:"not null;index:idx_model_channel"`
ModelType string `json:"model_type" gorm:"not null;index:idx_model_type"` // text2text, text2image, etc.
BillingType string `json:"billing_type" gorm:"not null"` // tokens or times
ChannelType uint `json:"channel_type" gorm:"not null;index:idx_model_channel"`
Currency string `json:"currency" gorm:"not null"` // USD or CNY
InputPrice float64 `json:"input_price" gorm:"not null"`
OutputPrice float64 `json:"output_price" gorm:"not null"`
PriceSource string `json:"price_source" gorm:"not null"`
Status string `json:"status" gorm:"not null;default:pending"` // pending, approved, rejected
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
Status string `json:"status" gorm:"not null;default:pending;index:idx_status"` // pending, approved, rejected
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
CreatedBy string `json:"created_by" gorm:"not null"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`

View File

@ -1,24 +1,25 @@
<template>
<div class="model-types">
<el-card v-loading="loading" element-loading-text="加载中...">
<div class="model-types-container">
<el-card v-loading="loading" element-loading-text="加载中..." class="model-types-card">
<template #header>
<div class="card-header">
<span>模型类别</span>
<span class="title">模型类别</span>
<el-button v-if="isAdmin" type="primary" @click="handleAdd">添加模型类别</el-button>
</div>
</template>
<el-table
:data="modelTypes"
style="width: 100%"
v-loading="tableLoading"
element-loading-text="加载中..."
row-key="key"
class="model-types-table"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<el-table-column prop="key" label="类别键值" width="180" />
<el-table-column prop="label" label="类别名称" width="180" />
<el-table-column prop="sort_order" label="排序" width="100" />
<el-table-column v-if="isAdmin" label="操作" width="200">
<el-table-column prop="key" label="类别键值" min-width="200" />
<el-table-column prop="label" label="类别名称" min-width="200" />
<el-table-column prop="sort_order" label="排序" min-width="120" align="center" />
<el-table-column v-if="isAdmin" label="操作" min-width="150" align="center">
<template #default="{ row }">
<el-button-group>
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
@ -168,14 +169,49 @@ onMounted(() => {
</script>
<style scoped>
.model-types-container {
padding: 20px;
display: flex;
justify-content: center;
}
.model-types-card {
width: 100%;
max-width: 1200px;
margin: 0 auto;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.model-types-table {
width: 100%;
margin-top: 10px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.model-types-container {
padding: 10px;
}
.model-types-card {
max-width: 100%;
}
}
</style>

View File

@ -252,10 +252,9 @@
:small="false"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
size-change-label="条/页"
>
<template #sizes>
<el-select v-model="pageSize" :options="[10, 20, 50, 100].map(item => ({ value: item, label: item + ' 条/页' }))">
<el-select v-model="pageSize" :options="[10, 20, 50, 100].map(item => ({ value: item, label: `${item} 条/页` }))">
<template #prefix>每页</template>
</el-select>
</template>
@ -529,7 +528,11 @@ const selectedModelType = ref('')
const isAdmin = computed(() => props.user?.role === 'admin')
const providers = ref([])
const getProvider = (id) => providers.value.find(p => p.id.toString() === id)
const getProvider = (id) => {
// id
const idStr = id?.toString()
return providers.value.find(p => p.id.toString() === idStr)
}
const statusMap = {
'pending': '待审核',
@ -592,14 +595,14 @@ const loadPrices = async () => {
axios.get('/api/providers')
])
prices.value = pricesRes.data.prices
prices.value = pricesRes.data.data
total.value = pricesRes.data.total
providers.value = providersRes.data
//
const cacheKey = `${currentPage.value}-${pageSize.value}-${selectedProvider.value}-${selectedModelType.value}`
cachedPrices.value.set(cacheKey, {
prices: pricesRes.data.prices,
prices: pricesRes.data.data,
total: pricesRes.data.total
})
@ -1280,6 +1283,7 @@ onMounted(() => {
margin-top: 20px;
display: flex;
justify-content: flex-end;
padding: 0 10px;
}
/* 添加表格行动画 */
@ -1297,12 +1301,27 @@ onMounted(() => {
}
.el-select .el-input {
width: 110px !important;
width: 140px !important;
}
.el-select-dropdown__item {
padding-right: 15px;
}
.el-pagination__sizes {
margin-right: 15px;
}
/* 修复选择框宽度问题 */
.el-select__wrapper {
min-width: 140px !important;
width: auto !important;
}
/* 确保下拉菜单也足够宽 */
.el-select__popper {
min-width: 140px !important;
}
}
.action-buttons {
@ -1318,4 +1337,36 @@ onMounted(() => {
.action-buttons :deep(.el-icon) {
font-size: 16px;
}
/* 添加全局样式覆盖 */
:global(.el-pagination .el-select__wrapper) {
min-width: 140px !important;
width: auto !important;
}
:global(.el-pagination .el-select-dropdown__wrap) {
min-width: 140px !important;
}
:global(.el-pagination .el-select .el-input__wrapper) {
width: auto !important;
min-width: 140px !important;
}
</style>
<style>
/* 全局样式,确保分页选择框宽度足够 */
.el-pagination .el-select__wrapper {
min-width: 140px !important;
width: auto !important;
}
.el-pagination .el-select .el-input__wrapper {
width: auto !important;
min-width: 140px !important;
}
.el-select-dropdown {
min-width: 140px !important;
}
</style>

View File

@ -1,21 +1,21 @@
<template>
<div class="providers">
<div class="providers-container">
<el-card v-loading="loading" element-loading-text="加载中...">
<template #header>
<div class="card-header">
<span>模型厂商</span>
<span class="title">模型厂商</span>
<el-button type="primary" @click="handleAdd">添加模型厂商</el-button>
</div>
</template>
<el-table :data="sortedProviders" style="width: 100%" v-loading="tableLoading" element-loading-text="加载中...">
<el-table-column prop="id" label="ID" />
<el-table-column label="名称">
<el-table :data="sortedProviders" style="width: 100%" v-loading="tableLoading" element-loading-text="加载中..." class="providers-table" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
<el-table-column prop="id" label="ID" min-width="200"/>
<el-table-column label="名称" min-width="200">
<template #default="{ row }">
{{ row.name }}
</template>
</el-table-column>
<el-table-column label="图标">
<el-table-column label="图标" min-width="200">
<template #default="{ row }">
<el-image
v-if="row.icon"
@ -26,8 +26,8 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="created_by" label="创建者" />
<el-table-column v-if="isAdmin" label="操作" width="200">
<!-- <el-table-column prop="created_by" label="创建者" min-width="100"/> -->
<el-table-column v-if="isAdmin" label="操作" min-width="150">
<template #default="{ row }">
<el-button-group>
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
@ -233,4 +233,34 @@ const submitForm = async () => {
stroke: #409EFF;
}
}
.providers-container {
padding: 20px;
display: flex;
justify-content: center;
}
.providers-card {
width: 100%;
max-width: 1200px;
margin: 0 auto;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.providers-table {
width: 100%;
margin-top: 10px;
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.providers-container {
padding: 10px;
}
.providers-card {
max-width: 100%;
}
}
</style>

View File

@ -7,7 +7,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
target: 'https://aimodels-prices.q58.club',
changeOrigin: true
}
}