优化价格处理逻辑,整合价格创建和更新功能

- 在 prices.go 中新增 ProcessPrice 函数,统一处理价格的创建和更新逻辑
- 更新 FetchAndSavePrices 和 UpdateOtherPrices 函数,使用 ProcessPrice 进行价格记录的处理
- 在 GetPrices 函数中添加状态筛选参数,优化价格查询功能
- 前端 Prices.vue 中调整搜索框和筛选功能的样式,提升用户体验
- 修复部分代码格式和样式问题,增强代码可读性
This commit is contained in:
wood chen 2025-03-18 02:18:57 +08:00
parent dce4815654
commit 75d62d978a
5 changed files with 542 additions and 410 deletions

View File

@ -8,9 +8,9 @@ import (
"math" "math"
"net/http" "net/http"
"strconv" "strconv"
"time"
"aimodels-prices/database" "aimodels-prices/database"
"aimodels-prices/handlers"
"aimodels-prices/models" "aimodels-prices/models"
) )
@ -74,6 +74,8 @@ func FetchAndSavePrices() error {
} }
// 处理每个模型的价格数据 // 处理每个模型的价格数据
processedCount := 0
skippedCount := 0
for _, modelData := range openRouterResp.Data { for _, modelData := range openRouterResp.Data {
// 确定模型类型 // 确定模型类型
modelType := determineModelType(modelData.Modality) modelType := determineModelType(modelData.Modality)
@ -87,6 +89,7 @@ func FetchAndSavePrices() error {
inputPrice, err = parsePrice(modelData.Endpoint.Pricing.Prompt) inputPrice, err = parsePrice(modelData.Endpoint.Pricing.Prompt)
if err != nil { if err != nil {
log.Printf("解析endpoint输入价格失败 %s: %v", modelData.Slug, err) log.Printf("解析endpoint输入价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue continue
} }
} else if modelData.Pricing.Prompt != "" { } else if modelData.Pricing.Prompt != "" {
@ -94,6 +97,7 @@ func FetchAndSavePrices() error {
inputPrice, err = parsePrice(modelData.Pricing.Prompt) inputPrice, err = parsePrice(modelData.Pricing.Prompt)
if err != nil { if err != nil {
log.Printf("解析输入价格失败 %s: %v", modelData.Slug, err) log.Printf("解析输入价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue continue
} }
} }
@ -102,60 +106,72 @@ func FetchAndSavePrices() error {
outputPrice, err = parsePrice(modelData.Endpoint.Pricing.Completion) outputPrice, err = parsePrice(modelData.Endpoint.Pricing.Completion)
if err != nil { if err != nil {
log.Printf("解析endpoint输出价格失败 %s: %v", modelData.Slug, err) log.Printf("解析endpoint输出价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue continue
} }
} else if modelData.Pricing.Completion != "" { } else if modelData.Pricing.Completion != "" {
outputPrice, err = parsePrice(modelData.Pricing.Completion) outputPrice, err = parsePrice(modelData.Pricing.Completion)
if err != nil { if err != nil {
log.Printf("解析输出价格失败 %s: %v", modelData.Slug, err) log.Printf("解析输出价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue continue
} }
} }
// 创建价格对象
price := models.Price{
Model: modelData.Slug,
ModelType: modelType,
BillingType: BillingType,
ChannelType: ChannelType,
Currency: Currency,
InputPrice: inputPrice,
OutputPrice: outputPrice,
PriceSource: PriceSource,
Status: Status,
CreatedBy: CreatedBy,
}
// 检查是否已存在相同模型的价格记录 // 检查是否已存在相同模型的价格记录
var existingPrice models.Price var existingPrice models.Price
result := db.Where("model = ? AND channel_type = ?", modelData.Slug, ChannelType).First(&existingPrice) result := db.Where("model = ? AND channel_type = ?", modelData.Slug, ChannelType).First(&existingPrice)
if result.Error == nil { if result.Error == nil {
// 更新现有记录 // 使用processPrice函数处理更新
existingPrice.ModelType = modelType _, changed, err := handlers.ProcessPrice(price, &existingPrice, true, CreatedBy)
existingPrice.BillingType = BillingType if err != nil {
existingPrice.Currency = Currency
existingPrice.InputPrice = inputPrice
existingPrice.OutputPrice = outputPrice
existingPrice.PriceSource = PriceSource
existingPrice.Status = Status
existingPrice.UpdatedAt = time.Now()
if err := db.Save(&existingPrice).Error; err != nil {
log.Printf("更新价格记录失败 %s: %v", modelData.Slug, err) log.Printf("更新价格记录失败 %s: %v", modelData.Slug, err)
skippedCount++
continue continue
} }
log.Printf("更新价格记录: %s", modelData.Slug)
} else {
// 创建新记录
newPrice := models.Price{
Model: modelData.Slug,
ModelType: modelType,
BillingType: BillingType,
ChannelType: ChannelType,
Currency: Currency,
InputPrice: inputPrice,
OutputPrice: outputPrice,
PriceSource: PriceSource,
Status: Status,
CreatedBy: CreatedBy,
}
if err := db.Create(&newPrice).Error; err != nil { if changed {
log.Printf("更新价格记录: %s", modelData.Slug)
processedCount++
} else {
log.Printf("价格无变化,跳过更新: %s", modelData.Slug)
skippedCount++
}
} else {
// 使用processPrice函数处理创建
_, changed, err := handlers.ProcessPrice(price, nil, true, CreatedBy)
if err != nil {
log.Printf("创建价格记录失败 %s: %v", modelData.Slug, err) log.Printf("创建价格记录失败 %s: %v", modelData.Slug, err)
skippedCount++
continue continue
} }
log.Printf("创建新价格记录: %s", modelData.Slug)
if changed {
log.Printf("创建新价格记录: %s", modelData.Slug)
processedCount++
} else {
log.Printf("价格创建失败: %s", modelData.Slug)
skippedCount++
}
} }
} }
log.Println("OpenRouter价格数据处理完成") log.Printf("OpenRouter价格数据处理完成,成功处理: %d, 跳过: %d", processedCount, skippedCount)
return nil return nil
} }

View File

@ -8,9 +8,9 @@ import (
"encoding/json" "encoding/json"
"strings" "strings"
"time"
"aimodels-prices/database" "aimodels-prices/database"
"aimodels-prices/handlers"
"aimodels-prices/models" "aimodels-prices/models"
) )
@ -28,6 +28,11 @@ var blacklist = []string{
"shap-e", "shap-e",
"palm-2", "palm-2",
"o3-mini-high", "o3-mini-high",
"claude-instant",
"claude-1",
"claude-3-haiku",
"claude-3-opus",
"claude-3-sonnet",
} }
const ( const (
@ -94,6 +99,20 @@ func UpdateOtherPrices() error {
log.Printf("修正Google模型名称: %s -> %s", parts[1], modelName) log.Printf("修正Google模型名称: %s -> %s", parts[1], modelName)
} }
} }
if author == "anthropic" {
// 处理claude-3.5-sonnet系列模型名称
if strings.HasPrefix(modelName, "claude-3.5") {
suffix := strings.TrimPrefix(modelName, "claude-3.5")
modelName = "claude-3-5" + suffix
log.Printf("修正Claude模型名称: %s -> %s", parts[1], modelName)
}
if strings.HasPrefix(modelName, "claude-3.7") {
suffix := strings.TrimPrefix(modelName, "claude-3.7")
modelName = "claude-3-7" + suffix
log.Printf("修正Claude模型名称: %s -> %s", parts[1], modelName)
}
}
// 确定模型类型 // 确定模型类型
modelType := determineModelType(modelData.Modality) modelType := determineModelType(modelData.Modality)
@ -102,7 +121,14 @@ func UpdateOtherPrices() error {
var inputPrice, outputPrice float64 var inputPrice, outputPrice float64
var parseErr error var parseErr error
// 优先使用endpoint中的pricing // 如果输入或输出价格为空,直接跳过
if modelData.Endpoint.Pricing.Prompt == "" || modelData.Endpoint.Pricing.Completion == "" {
log.Printf("跳过价格数据不完整的模型: %s", modelData.Slug)
skippedCount++
continue
}
// 使用endpoint中的pricing
if modelData.Endpoint.Pricing.Prompt != "" { if modelData.Endpoint.Pricing.Prompt != "" {
inputPrice, parseErr = parsePrice(modelData.Endpoint.Pricing.Prompt) inputPrice, parseErr = parsePrice(modelData.Endpoint.Pricing.Prompt)
if parseErr != nil { if parseErr != nil {
@ -110,14 +136,6 @@ func UpdateOtherPrices() error {
skippedCount++ skippedCount++
continue continue
} }
} else if modelData.Pricing.Prompt != "" {
// 如果endpoint中没有则使用顶层pricing
inputPrice, parseErr = parsePrice(modelData.Pricing.Prompt)
if parseErr != nil {
log.Printf("解析输入价格失败 %s: %v", modelData.Slug, parseErr)
skippedCount++
continue
}
} }
if modelData.Endpoint.Pricing.Completion != "" { if modelData.Endpoint.Pricing.Completion != "" {
@ -127,13 +145,20 @@ func UpdateOtherPrices() error {
skippedCount++ skippedCount++
continue continue
} }
} else if modelData.Pricing.Completion != "" { }
outputPrice, parseErr = parsePrice(modelData.Pricing.Completion)
if parseErr != nil { // 创建价格对象
log.Printf("解析输出价格失败 %s: %v", modelData.Slug, parseErr) price := models.Price{
skippedCount++ Model: modelName,
continue ModelType: modelType,
} BillingType: BillingType,
ChannelType: channelType,
Currency: Currency,
InputPrice: inputPrice,
OutputPrice: outputPrice,
PriceSource: OtherPriceSource,
Status: OtherStatus,
CreatedBy: CreatedBy,
} }
// 检查是否已存在相同模型的价格记录 // 检查是否已存在相同模型的价格记录
@ -141,45 +166,37 @@ func UpdateOtherPrices() error {
result := db.Where("model = ? AND channel_type = ?", modelName, channelType).First(&existingPrice) result := db.Where("model = ? AND channel_type = ?", modelName, channelType).First(&existingPrice)
if result.Error == nil { if result.Error == nil {
// 更新现有记录 // 使用processPrice函数处理更新
existingPrice.ModelType = modelType _, changed, err := handlers.ProcessPrice(price, &existingPrice, false, CreatedBy)
existingPrice.BillingType = BillingType if err != nil {
existingPrice.Currency = Currency
existingPrice.InputPrice = inputPrice
existingPrice.OutputPrice = outputPrice
existingPrice.PriceSource = OtherPriceSource
existingPrice.Status = OtherStatus
existingPrice.UpdatedAt = time.Now()
if err := db.Save(&existingPrice).Error; err != nil {
log.Printf("更新价格记录失败 %s: %v", modelName, err) log.Printf("更新价格记录失败 %s: %v", modelName, err)
skippedCount++ skippedCount++
continue continue
} }
log.Printf("更新价格记录: %s (厂商: %s)", modelName, author)
processedCount++
} else {
// 创建新记录
newPrice := models.Price{
Model: modelName,
ModelType: modelType,
BillingType: BillingType,
ChannelType: channelType,
Currency: Currency,
InputPrice: inputPrice,
OutputPrice: outputPrice,
PriceSource: OtherPriceSource,
Status: OtherStatus,
CreatedBy: CreatedBy,
}
if err := db.Create(&newPrice).Error; err != nil { if changed {
log.Printf("更新价格记录: %s (厂商: %s)", modelName, author)
processedCount++
} else {
log.Printf("价格无变化,跳过更新: %s (厂商: %s)", modelName, author)
skippedCount++
}
} else {
// 使用processPrice函数处理创建
_, changed, err := handlers.ProcessPrice(price, nil, false, CreatedBy)
if err != nil {
log.Printf("创建价格记录失败 %s: %v", modelName, err) log.Printf("创建价格记录失败 %s: %v", modelName, err)
skippedCount++ skippedCount++
continue continue
} }
log.Printf("创建新价格记录: %s (厂商: %s)", modelName, author)
processedCount++ if changed {
log.Printf("创建新价格记录: %s (厂商: %s)", modelName, author)
processedCount++
} else {
log.Printf("价格创建失败: %s (厂商: %s)", modelName, author)
skippedCount++
}
} }
} }

View File

@ -20,6 +20,7 @@ func GetPrices(c *gin.Context) {
channelType := c.Query("channel_type") // 厂商筛选参数 channelType := c.Query("channel_type") // 厂商筛选参数
modelType := c.Query("model_type") // 模型类型筛选参数 modelType := c.Query("model_type") // 模型类型筛选参数
searchQuery := c.Query("search") // 搜索查询参数 searchQuery := c.Query("search") // 搜索查询参数
status := c.Query("status") // 状态筛选参数
if page < 1 { if page < 1 {
page = 1 page = 1
@ -31,8 +32,8 @@ func GetPrices(c *gin.Context) {
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
// 构建缓存键 // 构建缓存键
cacheKey := fmt.Sprintf("prices_page_%d_size_%d_channel_%s_type_%s_search_%s", cacheKey := fmt.Sprintf("prices_page_%d_size_%d_channel_%s_type_%s_search_%s_status_%s",
page, pageSize, channelType, modelType, searchQuery) page, pageSize, channelType, modelType, searchQuery, status)
// 尝试从缓存获取 // 尝试从缓存获取
if cachedData, found := database.GlobalCache.Get(cacheKey); found { if cachedData, found := database.GlobalCache.Get(cacheKey); found {
@ -56,10 +57,15 @@ func GetPrices(c *gin.Context) {
if searchQuery != "" { if searchQuery != "" {
query = query.Where("model LIKE ?", "%"+searchQuery+"%") query = query.Where("model LIKE ?", "%"+searchQuery+"%")
} }
// 添加状态筛选条件
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数 - 使用缓存优化 // 获取总数 - 使用缓存优化
var total int64 var total int64
totalCacheKey := fmt.Sprintf("prices_count_channel_%s_type_%s_search_%s", channelType, modelType, searchQuery) totalCacheKey := fmt.Sprintf("prices_count_channel_%s_type_%s_search_%s_status_%s",
channelType, modelType, searchQuery, status)
if cachedTotal, found := database.GlobalCache.Get(totalCacheKey); found { if cachedTotal, found := database.GlobalCache.Get(totalCacheKey); found {
if t, ok := cachedTotal.(int64); ok { if t, ok := cachedTotal.(int64); ok {
@ -97,6 +103,118 @@ func GetPrices(c *gin.Context) {
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
} }
// processPrice 处理价格的创建和更新逻辑
func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, username string) (models.Price, bool, error) {
// 如果是更新操作且存在现有记录
if existingPrice != nil {
// 检查价格是否有变化
if isAdmin {
// 管理员直接更新主字段,检查是否有实际变化
if existingPrice.Model == price.Model &&
existingPrice.ModelType == price.ModelType &&
existingPrice.BillingType == price.BillingType &&
existingPrice.ChannelType == price.ChannelType &&
existingPrice.Currency == price.Currency &&
existingPrice.InputPrice == price.InputPrice &&
existingPrice.OutputPrice == price.OutputPrice &&
existingPrice.PriceSource == price.PriceSource {
// 没有变化,不需要更新
return *existingPrice, false, nil
}
// 有变化,更新字段
existingPrice.Model = price.Model
existingPrice.ModelType = price.ModelType
existingPrice.BillingType = price.BillingType
existingPrice.ChannelType = price.ChannelType
existingPrice.Currency = price.Currency
existingPrice.InputPrice = price.InputPrice
existingPrice.OutputPrice = price.OutputPrice
existingPrice.PriceSource = price.PriceSource
existingPrice.Status = "approved"
existingPrice.UpdatedBy = &username
existingPrice.TempModel = nil
existingPrice.TempModelType = nil
existingPrice.TempBillingType = nil
existingPrice.TempChannelType = nil
existingPrice.TempCurrency = nil
existingPrice.TempInputPrice = nil
existingPrice.TempOutputPrice = nil
existingPrice.TempPriceSource = nil
// 保存更新
if err := database.DB.Save(existingPrice).Error; err != nil {
return *existingPrice, false, err
}
return *existingPrice, true, nil
} else {
// 普通用户更新临时字段,检查是否有实际变化
// 创建临时值的指针
modelPtr := &price.Model
modelTypePtr := &price.ModelType
billingTypePtr := &price.BillingType
channelTypePtr := &price.ChannelType
currencyPtr := &price.Currency
inputPricePtr := &price.InputPrice
outputPricePtr := &price.OutputPrice
priceSourcePtr := &price.PriceSource
// 检查临时字段与现有主字段是否相同
if (existingPrice.Model == price.Model &&
existingPrice.ModelType == price.ModelType &&
existingPrice.BillingType == price.BillingType &&
existingPrice.ChannelType == price.ChannelType &&
existingPrice.Currency == price.Currency &&
existingPrice.InputPrice == price.InputPrice &&
existingPrice.OutputPrice == price.OutputPrice &&
existingPrice.PriceSource == price.PriceSource) ||
// 或者检查临时字段与现有临时字段是否相同
(existingPrice.TempModel != nil && *existingPrice.TempModel == price.Model &&
existingPrice.TempModelType != nil && *existingPrice.TempModelType == price.ModelType &&
existingPrice.TempBillingType != nil && *existingPrice.TempBillingType == price.BillingType &&
existingPrice.TempChannelType != nil && *existingPrice.TempChannelType == price.ChannelType &&
existingPrice.TempCurrency != nil && *existingPrice.TempCurrency == price.Currency &&
existingPrice.TempInputPrice != nil && *existingPrice.TempInputPrice == price.InputPrice &&
existingPrice.TempOutputPrice != nil && *existingPrice.TempOutputPrice == price.OutputPrice &&
existingPrice.TempPriceSource != nil && *existingPrice.TempPriceSource == price.PriceSource) {
// 没有变化,不需要更新
return *existingPrice, false, nil
}
// 有变化,更新临时字段
existingPrice.TempModel = modelPtr
existingPrice.TempModelType = modelTypePtr
existingPrice.TempBillingType = billingTypePtr
existingPrice.TempChannelType = channelTypePtr
existingPrice.TempCurrency = currencyPtr
existingPrice.TempInputPrice = inputPricePtr
existingPrice.TempOutputPrice = outputPricePtr
existingPrice.TempPriceSource = priceSourcePtr
existingPrice.Status = "pending"
existingPrice.UpdatedBy = &username
// 保存更新
if err := database.DB.Save(existingPrice).Error; err != nil {
return *existingPrice, false, err
}
return *existingPrice, true, nil
}
} else {
// 创建新记录
price.Status = "pending"
if isAdmin {
price.Status = "approved"
}
price.CreatedBy = username
// 保存新记录
if err := database.DB.Create(&price).Error; err != nil {
return price, false, err
}
return price, true, nil
}
}
func CreatePrice(c *gin.Context) { func CreatePrice(c *gin.Context) {
var price models.Price var price models.Price
if err := c.ShouldBindJSON(&price); err != nil { if err := c.ShouldBindJSON(&price); err != nil {
@ -123,19 +241,27 @@ func CreatePrice(c *gin.Context) {
return return
} }
// 设置状态和创建者 // 获取当前用户
price.Status = "pending" user, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
return
}
currentUser := user.(*models.User)
// 创建记录 // 处理价格创建
if err := database.DB.Create(&price).Error; err != nil { result, changed, err := ProcessPrice(price, nil, currentUser.Role == "admin", currentUser.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create price"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create price"})
return return
} }
// 清除所有价格相关缓存 // 清除所有价格相关缓存
clearPriceCache() if changed {
clearPriceCache()
}
c.JSON(http.StatusCreated, price) c.JSON(http.StatusCreated, result)
} }
func UpdatePriceStatus(c *gin.Context) { func UpdatePriceStatus(c *gin.Context) {
@ -209,23 +335,36 @@ func UpdatePriceStatus(c *gin.Context) {
return return
} }
} else { } else {
// 如果是拒绝,清除临时字段 // 如果是拒绝
if err := tx.Model(&price).Updates(map[string]interface{}{ // 检查是否是新创建的价格(没有原始价格)
"status": input.Status, isNewPrice := price.Model == "" || (price.TempModel != nil && price.Model == *price.TempModel)
"updated_at": time.Now(),
"temp_model": nil, if isNewPrice {
"temp_model_type": nil, // 如果是新创建的价格,直接删除
"temp_billing_type": nil, if err := tx.Delete(&price).Error; err != nil {
"temp_channel_type": nil, tx.Rollback()
"temp_currency": nil, c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete rejected price"})
"temp_input_price": nil, return
"temp_output_price": nil, }
"temp_price_source": nil, } else {
"updated_by": nil, // 如果是更新的价格恢复到原始状态清除临时字段并设置状态为approved
}).Error; err != nil { if err := tx.Model(&price).Updates(map[string]interface{}{
tx.Rollback() "status": "approved", // 恢复为已批准状态
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price status"}) "updated_at": time.Now(),
return "temp_model": nil,
"temp_model_type": nil,
"temp_billing_type": nil,
"temp_channel_type": nil,
"temp_currency": nil,
"temp_input_price": nil,
"temp_output_price": nil,
"temp_price_source": nil,
"updated_by": nil,
}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price status"})
return
}
} }
} }
@ -239,11 +378,20 @@ func UpdatePriceStatus(c *gin.Context) {
// 清除所有价格相关缓存 // 清除所有价格相关缓存
clearPriceCache() clearPriceCache()
c.JSON(http.StatusOK, gin.H{ // 根据操作类型返回不同的消息
"message": "Status updated successfully", if input.Status == "rejected" && (price.Model == "" || (price.TempModel != nil && price.Model == *price.TempModel)) {
"status": input.Status, c.JSON(http.StatusOK, gin.H{
"updated_at": time.Now(), "message": "Price rejected and deleted successfully",
}) "status": input.Status,
"updated_at": time.Now(),
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "Status updated successfully",
"status": input.Status,
"updated_at": time.Now(),
})
}
} }
func UpdatePrice(c *gin.Context) { func UpdatePrice(c *gin.Context) {
@ -288,55 +436,19 @@ func UpdatePrice(c *gin.Context) {
return return
} }
// 根据用户角色决定更新方式 // 处理价格更新
if currentUser.Role == "admin" { result, changed, err := ProcessPrice(price, &existingPrice, currentUser.Role == "admin", currentUser.Username)
// 管理员直接更新主字段 if err != nil {
existingPrice.Model = price.Model c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price"})
existingPrice.ModelType = price.ModelType return
existingPrice.BillingType = price.BillingType
existingPrice.ChannelType = price.ChannelType
existingPrice.Currency = price.Currency
existingPrice.InputPrice = price.InputPrice
existingPrice.OutputPrice = price.OutputPrice
existingPrice.PriceSource = price.PriceSource
existingPrice.Status = "approved"
existingPrice.UpdatedBy = &currentUser.Username
existingPrice.TempModel = nil
existingPrice.TempModelType = nil
existingPrice.TempBillingType = nil
existingPrice.TempChannelType = nil
existingPrice.TempCurrency = nil
existingPrice.TempInputPrice = nil
existingPrice.TempOutputPrice = nil
existingPrice.TempPriceSource = nil
if err := database.DB.Save(&existingPrice).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price"})
return
}
} else {
// 普通用户更新临时字段
existingPrice.TempModel = &price.Model
existingPrice.TempModelType = &price.ModelType
existingPrice.TempBillingType = &price.BillingType
existingPrice.TempChannelType = &price.ChannelType
existingPrice.TempCurrency = &price.Currency
existingPrice.TempInputPrice = &price.InputPrice
existingPrice.TempOutputPrice = &price.OutputPrice
existingPrice.TempPriceSource = &price.PriceSource
existingPrice.Status = "pending"
existingPrice.UpdatedBy = &currentUser.Username
if err := database.DB.Save(&existingPrice).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price"})
return
}
} }
// 清除所有价格相关缓存 // 清除所有价格相关缓存
clearPriceCache() if changed {
clearPriceCache()
}
c.JSON(http.StatusOK, existingPrice) c.JSON(http.StatusOK, result)
} }
func DeletePrice(c *gin.Context) { func DeletePrice(c *gin.Context) {
@ -362,6 +474,16 @@ func DeletePrice(c *gin.Context) {
} }
func ApproveAllPrices(c *gin.Context) { func ApproveAllPrices(c *gin.Context) {
// 获取操作类型(批准或拒绝)
var input struct {
Action string `json:"action" binding:"required,oneof=approve reject"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action, must be 'approve' or 'reject'"})
return
}
// 查找所有待审核的价格 // 查找所有待审核的价格
var pendingPrices []models.Price var pendingPrices []models.Price
if err := database.DB.Where("status = 'pending'").Find(&pendingPrices).Error; err != nil { if err := database.DB.Where("status = 'pending'").Find(&pendingPrices).Error; err != nil {
@ -371,54 +493,94 @@ func ApproveAllPrices(c *gin.Context) {
// 开始事务 // 开始事务
tx := database.DB.Begin() tx := database.DB.Begin()
processedCount := 0
deletedCount := 0
for _, price := range pendingPrices { for _, price := range pendingPrices {
updateMap := map[string]interface{}{ if input.Action == "approve" {
"status": "approved", // 批准操作
"updated_at": time.Now(), updateMap := map[string]interface{}{
} "status": "approved",
"updated_at": time.Now(),
}
// 如果临时字段有值,则更新主字段 // 如果临时字段有值,则更新主字段
if price.TempModel != nil { if price.TempModel != nil {
updateMap["model"] = *price.TempModel updateMap["model"] = *price.TempModel
} }
if price.TempModelType != nil { if price.TempModelType != nil {
updateMap["model_type"] = *price.TempModelType updateMap["model_type"] = *price.TempModelType
} }
if price.TempBillingType != nil { if price.TempBillingType != nil {
updateMap["billing_type"] = *price.TempBillingType updateMap["billing_type"] = *price.TempBillingType
} }
if price.TempChannelType != nil { if price.TempChannelType != nil {
updateMap["channel_type"] = *price.TempChannelType updateMap["channel_type"] = *price.TempChannelType
} }
if price.TempCurrency != nil { if price.TempCurrency != nil {
updateMap["currency"] = *price.TempCurrency updateMap["currency"] = *price.TempCurrency
} }
if price.TempInputPrice != nil { if price.TempInputPrice != nil {
updateMap["input_price"] = *price.TempInputPrice updateMap["input_price"] = *price.TempInputPrice
} }
if price.TempOutputPrice != nil { if price.TempOutputPrice != nil {
updateMap["output_price"] = *price.TempOutputPrice updateMap["output_price"] = *price.TempOutputPrice
} }
if price.TempPriceSource != nil { if price.TempPriceSource != nil {
updateMap["price_source"] = *price.TempPriceSource updateMap["price_source"] = *price.TempPriceSource
} }
// 清除所有临时字段 // 清除所有临时字段
updateMap["temp_model"] = nil updateMap["temp_model"] = nil
updateMap["temp_model_type"] = nil updateMap["temp_model_type"] = nil
updateMap["temp_billing_type"] = nil updateMap["temp_billing_type"] = nil
updateMap["temp_channel_type"] = nil updateMap["temp_channel_type"] = nil
updateMap["temp_currency"] = nil updateMap["temp_currency"] = nil
updateMap["temp_input_price"] = nil updateMap["temp_input_price"] = nil
updateMap["temp_output_price"] = nil updateMap["temp_output_price"] = nil
updateMap["temp_price_source"] = nil updateMap["temp_price_source"] = nil
updateMap["updated_by"] = nil updateMap["updated_by"] = nil
if err := tx.Model(&price).Updates(updateMap).Error; err != nil { if err := tx.Model(&price).Updates(updateMap).Error; err != nil {
tx.Rollback() tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve prices"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve prices"})
return return
}
processedCount++
} else {
// 拒绝操作
// 检查是否是新创建的价格(没有原始价格)
isNewPrice := price.Model == "" || (price.TempModel != nil && price.Model == *price.TempModel)
if isNewPrice {
// 如果是新创建的价格,直接删除
if err := tx.Delete(&price).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete rejected price"})
return
}
deletedCount++
} else {
// 如果是更新的价格恢复到原始状态清除临时字段并设置状态为approved
if err := tx.Model(&price).Updates(map[string]interface{}{
"status": "approved", // 恢复为已批准状态
"updated_at": time.Now(),
"temp_model": nil,
"temp_model_type": nil,
"temp_billing_type": nil,
"temp_channel_type": nil,
"temp_currency": nil,
"temp_input_price": nil,
"temp_output_price": nil,
"temp_price_source": nil,
"updated_by": nil,
}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject prices"})
return
}
processedCount++
}
} }
} }
@ -432,10 +594,20 @@ func ApproveAllPrices(c *gin.Context) {
// 清除所有价格相关缓存 // 清除所有价格相关缓存
clearPriceCache() clearPriceCache()
c.JSON(http.StatusOK, gin.H{ // 根据操作类型返回不同的消息
"message": "All pending prices approved successfully", if input.Action == "approve" {
"count": len(pendingPrices), c.JSON(http.StatusOK, gin.H{
}) "message": "All pending prices approved successfully",
"count": processedCount,
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "All pending prices rejected successfully",
"processed": processedCount,
"deleted": deletedCount,
"total": processedCount + deletedCount,
})
}
} }
// clearPriceCache 清除所有价格相关的缓存 // clearPriceCache 清除所有价格相关的缓存

View File

@ -23,39 +23,28 @@
</template> </template>
<!-- 添加搜索框 --> <!-- 添加搜索框 -->
<div class="search-section"> <div class="filter-section">
<el-input <div class="filter-label" style="min-width:80px;">搜索模型:</div>
v-model="searchQuery" <div>
placeholder="搜索模型名称" <el-input v-model="searchQuery" placeholder="搜索模型名称" clearable prefix-icon="Search" @input="handleSearch">
clearable <template #prefix>
prefix-icon="Search" <el-icon>
@input="handleSearch" <Search />
> </el-icon>
<template #prefix> </template>
<el-icon><Search /></el-icon> </el-input>
</template> </div>
</el-input>
</div> </div>
<div class="filter-section"> <div class="filter-section">
<div class="filter-label">厂商筛选:</div> <div class="filter-label" style="min-width:80px;">厂商筛选:</div>
<div class="provider-filters"> <div class="provider-filters">
<el-button <el-button :type="!selectedProvider ? 'primary' : ''" @click="selectedProvider = ''">全部</el-button>
:type="!selectedProvider ? 'primary' : ''" <el-button v-for="provider in providers" :key="provider.id"
@click="selectedProvider = ''"
>全部</el-button>
<el-button
v-for="provider in providers"
:key="provider.id"
:type="selectedProvider === provider.id.toString() ? 'primary' : ''" :type="selectedProvider === provider.id.toString() ? 'primary' : ''"
@click="selectedProvider = provider.id.toString()" @click="selectedProvider = provider.id.toString()">
>
<div style="display: flex; align-items: center; gap: 8px"> <div style="display: flex; align-items: center; gap: 8px">
<el-image <el-image v-if="provider.icon" :src="provider.icon" style="width: 16px; height: 16px" />
v-if="provider.icon"
:src="provider.icon"
style="width: 16px; height: 16px"
/>
<span>{{ provider.name }}</span> <span>{{ provider.name }}</span>
</div> </div>
</el-button> </el-button>
@ -63,18 +52,11 @@
</div> </div>
<div class="filter-section"> <div class="filter-section">
<div class="filter-label">模型类别:</div> <div class="filter-label" style="min-width:80px;">模型类别:</div>
<div class="model-type-filters"> <div class="model-type-filters">
<el-button <el-button :type="!selectedModelType ? 'primary' : ''" @click="selectedModelType = ''">全部</el-button>
:type="!selectedModelType ? 'primary' : ''" <el-button v-for="(label, key) in modelTypeMap" :key="key" :type="selectedModelType === key ? 'primary' : ''"
@click="selectedModelType = ''" @click="selectedModelType = key">
>全部</el-button>
<el-button
v-for="(label, key) in modelTypeMap"
:key="key"
:type="selectedModelType === key ? 'primary' : ''"
@click="selectedModelType = key"
>
{{ label }} {{ label }}
</el-button> </el-button>
</div> </div>
@ -86,14 +68,9 @@
<el-skeleton :rows="1" animated /> <el-skeleton :rows="1" animated />
</div> </div>
</template> </template>
<el-table <el-table :data="prices" style="width: 100%" @selection-change="handlePriceSelectionChange"
:data="prices" v-loading="tableLoading" element-loading-text="加载中...">
style="width: 100%"
@selection-change="handlePriceSelectionChange"
v-loading="tableLoading"
element-loading-text="加载中..."
>
<el-table-column v-if="isAdmin" type="selection" width="55" /> <el-table-column v-if="isAdmin" type="selection" width="55" />
<el-table-column label="模型"> <el-table-column label="模型">
<template #default="{ row }"> <template #default="{ row }">
@ -109,7 +86,8 @@
<template #default="{ row }"> <template #default="{ row }">
<div class="value-container"> <div class="value-container">
<span>{{ getModelType(row.model_type) }}</span> <span>{{ getModelType(row.model_type) }}</span>
<el-tag v-if="row.temp_model_type && row.temp_model_type !== 'NULL'" type="warning" size="small" effect="light"> <el-tag v-if="row.temp_model_type && row.temp_model_type !== 'NULL'" type="warning" size="small"
effect="light">
待审核: {{ getModelType(row.temp_model_type) }} 待审核: {{ getModelType(row.temp_model_type) }}
</el-tag> </el-tag>
</div> </div>
@ -119,7 +97,8 @@
<template #default="{ row }"> <template #default="{ row }">
<div class="value-container"> <div class="value-container">
<span>{{ getBillingType(row.billing_type) }}</span> <span>{{ getBillingType(row.billing_type) }}</span>
<el-tag v-if="row.temp_billing_type && row.temp_billing_type !== 'NULL'" type="warning" size="small" effect="light"> <el-tag v-if="row.temp_billing_type && row.temp_billing_type !== 'NULL'" type="warning" size="small"
effect="light">
待审核: {{ getBillingType(row.temp_billing_type) }} 待审核: {{ getBillingType(row.temp_billing_type) }}
</el-tag> </el-tag>
</div> </div>
@ -129,14 +108,12 @@
<template #default="{ row }"> <template #default="{ row }">
<div class="value-container"> <div class="value-container">
<div style="display: flex; align-items: center; gap: 8px"> <div style="display: flex; align-items: center; gap: 8px">
<el-image <el-image v-if="getProvider(row.channel_type)?.icon" :src="getProvider(row.channel_type)?.icon"
v-if="getProvider(row.channel_type)?.icon" style="width: 24px; height: 24px" />
:src="getProvider(row.channel_type)?.icon"
style="width: 24px; height: 24px"
/>
<span>{{ getProvider(row.channel_type)?.name || row.channel_type }}</span> <span>{{ getProvider(row.channel_type)?.name || row.channel_type }}</span>
</div> </div>
<el-tag v-if="row.temp_channel_type && row.temp_channel_type !== 'NULL'" type="warning" size="small" effect="light"> <el-tag v-if="row.temp_channel_type && row.temp_channel_type !== 'NULL'" type="warning" size="small"
effect="light">
待审核: {{ getProvider(row.temp_channel_type)?.name || row.temp_channel_type }} 待审核: {{ getProvider(row.temp_channel_type)?.name || row.temp_channel_type }}
</el-tag> </el-tag>
</div> </div>
@ -146,7 +123,8 @@
<template #default="{ row }"> <template #default="{ row }">
<div class="value-container"> <div class="value-container">
<span>{{ row.currency }}</span> <span>{{ row.currency }}</span>
<el-tag v-if="row.temp_currency && row.temp_currency !== 'NULL'" type="warning" size="small" effect="light"> <el-tag v-if="row.temp_currency && row.temp_currency !== 'NULL'" type="warning" size="small"
effect="light">
待审核: {{ row.temp_currency }} 待审核: {{ row.temp_currency }}
</el-tag> </el-tag>
</div> </div>
@ -156,7 +134,9 @@
<template #default="{ row }"> <template #default="{ row }">
<div class="value-container"> <div class="value-container">
<span>{{ row.input_price === 0 ? '免费' : row.input_price }}</span> <span>{{ row.input_price === 0 ? '免费' : row.input_price }}</span>
<el-tag v-if="row.temp_input_price !== null && row.temp_input_price !== undefined && row.temp_input_price !== 'NULL'" type="warning" size="small" effect="light"> <el-tag
v-if="row.temp_input_price !== null && row.temp_input_price !== undefined && row.temp_input_price !== 'NULL'"
type="warning" size="small" effect="light">
待审核: {{ row.temp_input_price === 0 ? '免费' : row.temp_input_price }} 待审核: {{ row.temp_input_price === 0 ? '免费' : row.temp_input_price }}
</el-tag> </el-tag>
</div> </div>
@ -166,7 +146,9 @@
<template #default="{ row }"> <template #default="{ row }">
<div class="value-container"> <div class="value-container">
<span>{{ row.output_price === 0 ? '免费' : row.output_price }}</span> <span>{{ row.output_price === 0 ? '免费' : row.output_price }}</span>
<el-tag v-if="row.temp_output_price !== null && row.temp_output_price !== undefined && row.temp_output_price !== 'NULL'" type="warning" size="small" effect="light"> <el-tag
v-if="row.temp_output_price !== null && row.temp_output_price !== undefined && row.temp_output_price !== 'NULL'"
type="warning" size="small" effect="light">
待审核: {{ row.temp_output_price === 0 ? '免费' : row.temp_output_price }} 待审核: {{ row.temp_output_price === 0 ? '免费' : row.temp_output_price }}
</el-tag> </el-tag>
</div> </div>
@ -174,11 +156,7 @@
</el-table-column> </el-table-column>
<el-table-column width="80"> <el-table-column width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-popover <el-popover placement="left" :width="200" trigger="hover">
placement="left"
:width="200"
trigger="hover"
>
<template #reference> <template #reference>
<el-button link type="primary">详情</el-button> <el-button link type="primary">详情</el-button>
</template> </template>
@ -191,7 +169,8 @@
<span class="detail-label">价格来源:</span> <span class="detail-label">价格来源:</span>
<div class="detail-value"> <div class="detail-value">
<span>{{ row.price_source }}</span> <span>{{ row.price_source }}</span>
<el-tag v-if="row.temp_price_source && row.temp_price_source !== 'NULL'" type="warning" size="small" effect="light"> <el-tag v-if="row.temp_price_source && row.temp_price_source !== 'NULL'" type="warning" size="small"
effect="light">
待审核: {{ row.temp_price_source }} 待审核: {{ row.temp_price_source }}
</el-tag> </el-tag>
</div> </div>
@ -210,44 +189,41 @@
<template v-if="isAdmin"> <template v-if="isAdmin">
<el-tooltip content="编辑" placement="top"> <el-tooltip content="编辑" placement="top">
<el-button type="primary" link @click="handleEdit(row)"> <el-button type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon> <el-icon>
<Edit />
</el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<el-button type="danger" link @click="handleDelete(row)"> <el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon> <el-icon>
<Delete />
</el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip :content="row.status === 'pending' ? '通过审核' : '已审核'" placement="top"> <el-tooltip :content="row.status === 'pending' ? '通过审核' : '已审核'" placement="top">
<el-button <el-button type="success" link @click="updateStatus(row.id, 'approved')"
type="success" :disabled="row.status !== 'pending'">
link <el-icon>
@click="updateStatus(row.id, 'approved')" <Check />
:disabled="row.status !== 'pending'" </el-icon>
>
<el-icon><Check /></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip :content="row.status === 'pending' ? '拒绝审核' : '已审核'" placement="top"> <el-tooltip :content="row.status === 'pending' ? '拒绝审核' : '已审核'" placement="top">
<el-button <el-button type="danger" link @click="updateStatus(row.id, 'rejected')"
type="danger" :disabled="row.status !== 'pending'">
link <el-icon>
@click="updateStatus(row.id, 'rejected')" <Close />
:disabled="row.status !== 'pending'" </el-icon>
>
<el-icon><Close /></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</template> </template>
<template v-else> <template v-else>
<el-tooltip :content="row.status === 'pending' ? '等待审核中' : '提交修改'" placement="top"> <el-tooltip :content="row.status === 'pending' ? '等待审核中' : '提交修改'" placement="top">
<el-button <el-button type="primary" link @click="handleQuickEdit(row)" :disabled="row.status === 'pending'">
type="primary" <el-icon>
link <Edit />
@click="handleQuickEdit(row)" </el-icon>
:disabled="row.status === 'pending'"
>
<el-icon><Edit /></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</template> </template>
@ -255,21 +231,15 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 修改分页组件 --> <!-- 修改分页组件 -->
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
v-model:current-page="currentPage" :total="total" layout="total, sizes, prev, pager, next" :small="false" @size-change="handleSizeChange"
v-model:page-size="pageSize" @current-change="handleCurrentChange">
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next"
:small="false"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #sizes> <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> <template #prefix>每页</template>
</el-select> </el-select>
</template> </template>
@ -283,48 +253,39 @@
<div class="batch-toolbar"> <div class="batch-toolbar">
<el-button type="primary" @click="addRow">添加行</el-button> <el-button type="primary" @click="addRow">添加行</el-button>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
<el-popover <el-popover placement="bottom" :width="400" trigger="click">
placement="bottom"
:width="400"
trigger="click"
>
<template #reference> <template #reference>
<el-button type="success">从表格导入</el-button> <el-button type="success">从表格导入</el-button>
</template> </template>
<div class="import-popover"> <div class="import-popover">
<p class="import-tip">请粘贴表格数据支持从Excel复制每行格式为</p> <p class="import-tip">请粘贴表格数据支持从Excel复制每行格式为</p>
<p class="import-format">模型名称 计费类型 厂商 货币 输入价格 输出价格</p> <p class="import-format">模型名称 计费类型 厂商 货币 输入价格 输出价格</p>
<el-input <el-input v-model="importText" type="textarea" :rows="8" placeholder="例如
v-model="importText"
type="textarea"
:rows="8"
placeholder="例如
dall-e-2 按Token收费 OpenAI 美元 16.000000 16.000000 dall-e-2 按Token收费 OpenAI 美元 16.000000 16.000000
dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000" dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000" />
/>
<div class="import-actions"> <div class="import-actions">
<el-button type="primary" @click="handleImport">导入</el-button> <el-button type="primary" @click="handleImport">导入</el-button>
</div> </div>
</div> </div>
</el-popover> </el-popover>
</div> </div>
<el-table <el-table :data="batchForms" style="width: 100%" height="400">
:data="batchForms"
style="width: 100%"
height="400"
>
<el-table-column label="操作" width="100"> <el-table-column label="操作" width="100">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<div class="row-actions"> <div class="row-actions">
<el-tooltip content="复制" placement="top"> <el-tooltip content="复制" placement="top">
<el-button type="primary" link @click="duplicateRow($index)"> <el-button type="primary" link @click="duplicateRow($index)">
<el-icon><Document /></el-icon> <el-icon>
<Document />
</el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<el-button type="danger" link @click="removeRow($index)"> <el-button type="danger" link @click="removeRow($index)">
<el-icon><Delete /></el-icon> <el-icon>
<Delete />
</el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</div> </div>
@ -337,19 +298,9 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-table-column> </el-table-column>
<el-table-column label="模型类型" width="120"> <el-table-column label="模型类型" width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-select <el-select v-model="row.model_type" placeholder="请选择或输入" allow-create filterable
v-model="row.model_type" @create="handleModelTypeCreate">
placeholder="请选择或输入" <el-option v-for="(label, value) in modelTypeMap" :key="value" :label="label" :value="value" />
allow-create
filterable
@create="handleModelTypeCreate"
>
<el-option
v-for="(label, value) in modelTypeMap"
:key="value"
:label="label"
:value="value"
/>
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
@ -364,18 +315,10 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
<el-table-column label="模型厂商" width="180"> <el-table-column label="模型厂商" width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.channel_type" placeholder="请选择"> <el-select v-model="row.channel_type" placeholder="请选择">
<el-option <el-option v-for="provider in providers" :key="provider.id" :label="provider.name"
v-for="provider in providers" :value="provider.id.toString()">
:key="provider.id"
:label="provider.name"
:value="provider.id.toString()"
>
<div style="display: flex; align-items: center; gap: 8px"> <div style="display: flex; align-items: center; gap: 8px">
<el-image <el-image v-if="provider.icon" :src="provider.icon" style="width: 24px; height: 24px" />
v-if="provider.icon"
:src="provider.icon"
style="width: 24px; height: 24px"
/>
<span>{{ provider.name }}</span> <span>{{ provider.name }}</span>
</div> </div>
</el-option> </el-option>
@ -392,12 +335,14 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-table-column> </el-table-column>
<el-table-column label="输入价格(M)" width="150"> <el-table-column label="输入价格(M)" width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-input-number v-model="row.input_price" :precision="4" :step="0.0001" style="width: 100%" :controls="false" placeholder="请输入价格" /> <el-input-number v-model="row.input_price" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入价格" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="输出价格(M)" width="150"> <el-table-column label="输出价格(M)" width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-input-number v-model="row.output_price" :precision="4" :step="0.0001" style="width: 100%" :controls="false" placeholder="请输入价格" /> <el-input-number v-model="row.output_price" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入价格" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="价格来源" min-width="200" width="200"> <el-table-column label="价格来源" min-width="200" width="200">
@ -418,11 +363,7 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-dialog> </el-dialog>
<!-- 现有的单个添加对话框 --> <!-- 现有的单个添加对话框 -->
<el-dialog <el-dialog v-model="dialogVisible" :title="editingPrice ? (isAdmin ? '编辑价格' : '提交价格修改') : '提交价格'" width="700px">
v-model="dialogVisible"
:title="editingPrice ? (isAdmin ? '编辑价格' : '提交价格修改') : '提交价格'"
width="700px"
>
<el-form :model="form" label-width="100px"> <el-form :model="form" label-width="100px">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
@ -432,19 +373,9 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="模型类型"> <el-form-item label="模型类型">
<el-select <el-select v-model="form.model_type" placeholder="请选择或输入" allow-create filterable
v-model="form.model_type" @create="handleModelTypeCreate">
placeholder="请选择或输入" <el-option v-for="(label, value) in modelTypeMap" :key="value" :label="label" :value="value" />
allow-create
filterable
@create="handleModelTypeCreate"
>
<el-option
v-for="(label, value) in modelTypeMap"
:key="value"
:label="label"
:value="value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -459,18 +390,10 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
<el-col :span="12"> <el-col :span="12">
<el-form-item label="模型厂商"> <el-form-item label="模型厂商">
<el-select v-model="form.channel_type" placeholder="请选择"> <el-select v-model="form.channel_type" placeholder="请选择">
<el-option <el-option v-for="provider in providers" :key="provider.id" :label="provider.name"
v-for="provider in providers" :value="provider.id.toString()">
:key="provider.id"
:label="provider.name"
:value="provider.id.toString()"
>
<div style="display: flex; align-items: center; gap: 8px"> <div style="display: flex; align-items: center; gap: 8px">
<el-image <el-image v-if="provider.icon" :src="provider.icon" style="width: 24px; height: 24px" />
v-if="provider.icon"
:src="provider.icon"
style="width: 24px; height: 24px"
/>
<span>{{ provider.name }}</span> <span>{{ provider.name }}</span>
</div> </div>
</el-option> </el-option>
@ -487,12 +410,14 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="输入价格(M)"> <el-form-item label="输入价格(M)">
<el-input-number v-model="form.input_price" :precision="4" :step="0.0001" style="width: 100%" :controls="false" placeholder="请输入价格" /> <el-input-number v-model="form.input_price" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入价格" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="输出价格(M)"> <el-form-item label="输出价格(M)">
<el-input-number v-model="form.output_price" :precision="4" :step="0.0001" style="width: 100%" :controls="false" placeholder="请输入价格" /> <el-input-number v-model="form.output_price" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入价格" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
@ -590,13 +515,13 @@ const cachedPrices = ref(new Map()) // 用于缓存数据
const loadPrices = async () => { const loadPrices = async () => {
tableLoading.value = true tableLoading.value = true
// //
const params = { const params = {
page: currentPage.value, page: currentPage.value,
pageSize: pageSize.value pageSize: pageSize.value
} }
// //
if (selectedProvider.value) { if (selectedProvider.value) {
params.channel_type = selectedProvider.value params.channel_type = selectedProvider.value
@ -608,24 +533,24 @@ const loadPrices = async () => {
if (searchQuery.value) { if (searchQuery.value) {
params.search = searchQuery.value params.search = searchQuery.value
} }
try { try {
const [pricesRes, providersRes] = await Promise.all([ const [pricesRes, providersRes] = await Promise.all([
axios.get('/api/prices', { params }), axios.get('/api/prices', { params }),
axios.get('/api/providers') axios.get('/api/providers')
]) ])
prices.value = pricesRes.data.data prices.value = pricesRes.data.data
total.value = pricesRes.data.total total.value = pricesRes.data.total
providers.value = providersRes.data providers.value = providersRes.data
// //
const cacheKey = `${currentPage.value}-${pageSize.value}-${selectedProvider.value}-${selectedModelType.value}-${searchQuery.value}` const cacheKey = `${currentPage.value}-${pageSize.value}-${selectedProvider.value}-${selectedModelType.value}-${searchQuery.value}`
cachedPrices.value.set(cacheKey, { cachedPrices.value.set(cacheKey, {
prices: pricesRes.data.data, prices: pricesRes.data.data,
total: pricesRes.data.total total: pricesRes.data.total
}) })
// //
if (cachedPrices.value.size > 10) { if (cachedPrices.value.size > 10) {
const firstKey = cachedPrices.value.keys().next().value const firstKey = cachedPrices.value.keys().next().value
@ -700,7 +625,7 @@ const handleQuickEdit = (row) => {
} }
editingPrice.value = row editingPrice.value = row
// //
form.value = { form.value = {
model: row.model, model: row.model,
model_type: row.model_type, model_type: row.model_type,
billing_type: row.billing_type, billing_type: row.billing_type,
@ -717,21 +642,21 @@ const handleQuickEdit = (row) => {
const submitForm = async () => { const submitForm = async () => {
try { try {
form.value.created_by = props.user.username form.value.created_by = props.user.username
// channel_type // channel_type
const formToSubmit = { ...form.value } const formToSubmit = { ...form.value }
if (formToSubmit.channel_type) { if (formToSubmit.channel_type) {
formToSubmit.channel_type = parseInt(formToSubmit.channel_type, 10) formToSubmit.channel_type = parseInt(formToSubmit.channel_type, 10)
} }
let response let response
if (editingPrice.value) { if (editingPrice.value) {
// //
response = await axios.put(`/api/prices/${editingPrice.value.id}`, formToSubmit) response = await axios.put(`/api/prices/${editingPrice.value.id}`, formToSubmit)
} else { } else {
// //
const existingPrice = prices.value?.find(p => const existingPrice = prices.value?.find(p =>
p.model === form.value.model && p.model === form.value.model &&
p.channel_type === form.value.channel_type p.channel_type === form.value.channel_type
) )
if (existingPrice) { if (existingPrice) {
@ -840,7 +765,7 @@ const handleModelTypeCreate = async (value) => {
if (existingKey) { if (existingKey) {
return existingKey return existingKey
} }
// key使 // key使
let type_key = value let type_key = value
let type_label = value let type_label = value
@ -910,10 +835,10 @@ const submitBatchForms = async () => {
} }
// //
const invalidForms = batchForms.value.filter(form => const invalidForms = batchForms.value.filter(form =>
!form.model || !form.channel_type || !form.price_source !form.model || !form.channel_type || !form.price_source
) )
if (invalidForms.length) { if (invalidForms.length) {
ElMessage.error('请填写完整所有必填字段') ElMessage.error('请填写完整所有必填字段')
return return
@ -930,7 +855,7 @@ const submitBatchForms = async () => {
} }
await axios.post('/api/prices', formToSubmit) await axios.post('/api/prices', formToSubmit)
} }
await loadPrices() await loadPrices()
batchDialogVisible.value = false batchDialogVisible.value = false
ElMessage.success('批量添加成功') ElMessage.success('批量添加成功')
@ -966,7 +891,7 @@ const handleImport = () => {
} }
const [model, billingType, providerName, currency, inputPrice, outputPrice] = parts const [model, billingType, providerName, currency, inputPrice, outputPrice] = parts
// ID // ID
const provider = providers.value.find(p => p.name === providerName) const provider = providers.value.find(p => p.name === providerName)
if (!provider) { if (!provider) {
@ -1109,14 +1034,19 @@ const removeRow = (index) => {
const approveAllPending = async () => { const approveAllPending = async () => {
try { try {
// //
const { data } = await axios.get('/api/prices', { params: { status: 'pending', pageSize: 1 } }) const { data } = await axios.get('/api/prices', {
params: {
status: 'pending',
pageSize: 1
}
})
const pendingCount = data.total const pendingCount = data.total
if (pendingCount === 0) { if (pendingCount === 0) {
ElMessage.info('当前没有待审核的价格') ElMessage.info('当前没有待审核的价格')
return return
} }
// //
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确定要通过所有 ${pendingCount} 条待审核价格吗?`, `确定要通过所有 ${pendingCount} 条待审核价格吗?`,
@ -1127,12 +1057,13 @@ const approveAllPending = async () => {
type: 'success' type: 'success'
} }
) )
// //
await axios.put('/api/prices/approve-all', { status: 'approved' }) const response = await axios.put('/api/prices/approve-all', { status: 'approved' })
await loadPrices() await loadPrices()
ElMessage.success('已通过所有待审核价格') // 使
ElMessage.success(`已通过 ${response.data.count} 条待审核价格`)
} catch (error) { } catch (error) {
if (error === 'cancel') return if (error === 'cancel') return
console.error('Failed to approve all pending prices:', error) console.error('Failed to approve all pending prices:', error)
@ -1176,11 +1107,6 @@ onMounted(() => {
color: #606266; color: #606266;
} }
.search-section {
margin: 16px 0;
max-width: 400px;
}
.provider-filters { .provider-filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1325,6 +1251,7 @@ onMounted(() => {
.el-loading-text { .el-loading-text {
color: #409EFF; color: #409EFF;
} }
.path { .path {
stroke: #409EFF; stroke: #409EFF;
} }
@ -1355,25 +1282,25 @@ onMounted(() => {
width: auto !important; width: auto !important;
margin: 0 8px; margin: 0 8px;
} }
.el-select .el-input { .el-select .el-input {
width: 140px !important; width: 140px !important;
} }
.el-select-dropdown__item { .el-select-dropdown__item {
padding-right: 15px; padding-right: 15px;
} }
.el-pagination__sizes { .el-pagination__sizes {
margin-right: 15px; margin-right: 15px;
} }
/* 修复选择框宽度问题 */ /* 修复选择框宽度问题 */
.el-select__wrapper { .el-select__wrapper {
min-width: 140px !important; min-width: 140px !important;
width: auto !important; width: auto !important;
} }
/* 确保下拉菜单也足够宽 */ /* 确保下拉菜单也足够宽 */
.el-select__popper { .el-select__popper {
min-width: 140px !important; min-width: 140px !important;
@ -1425,4 +1352,4 @@ onMounted(() => {
.el-select-dropdown { .el-select-dropdown {
min-width: 140px !important; min-width: 140px !important;
} }
</style> </style>

View File

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