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

- 在 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"
"net/http"
"strconv"
"time"
"aimodels-prices/database"
"aimodels-prices/handlers"
"aimodels-prices/models"
)
@ -74,6 +74,8 @@ func FetchAndSavePrices() error {
}
// 处理每个模型的价格数据
processedCount := 0
skippedCount := 0
for _, modelData := range openRouterResp.Data {
// 确定模型类型
modelType := determineModelType(modelData.Modality)
@ -87,6 +89,7 @@ func FetchAndSavePrices() error {
inputPrice, err = parsePrice(modelData.Endpoint.Pricing.Prompt)
if err != nil {
log.Printf("解析endpoint输入价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue
}
} else if modelData.Pricing.Prompt != "" {
@ -94,6 +97,7 @@ func FetchAndSavePrices() error {
inputPrice, err = parsePrice(modelData.Pricing.Prompt)
if err != nil {
log.Printf("解析输入价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue
}
}
@ -102,39 +106,20 @@ func FetchAndSavePrices() error {
outputPrice, err = parsePrice(modelData.Endpoint.Pricing.Completion)
if err != nil {
log.Printf("解析endpoint输出价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue
}
} else if modelData.Pricing.Completion != "" {
outputPrice, err = parsePrice(modelData.Pricing.Completion)
if err != nil {
log.Printf("解析输出价格失败 %s: %v", modelData.Slug, err)
skippedCount++
continue
}
}
// 检查是否已存在相同模型的价格记录
var existingPrice models.Price
result := db.Where("model = ? AND channel_type = ?", modelData.Slug, ChannelType).First(&existingPrice)
if result.Error == nil {
// 更新现有记录
existingPrice.ModelType = modelType
existingPrice.BillingType = BillingType
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)
continue
}
log.Printf("更新价格记录: %s", modelData.Slug)
} else {
// 创建新记录
newPrice := models.Price{
// 创建价格对象
price := models.Price{
Model: modelData.Slug,
ModelType: modelType,
BillingType: BillingType,
@ -147,15 +132,46 @@ func FetchAndSavePrices() error {
CreatedBy: CreatedBy,
}
if err := db.Create(&newPrice).Error; err != nil {
log.Printf("创建价格记录失败 %s: %v", modelData.Slug, err)
// 检查是否已存在相同模型的价格记录
var existingPrice models.Price
result := db.Where("model = ? AND channel_type = ?", modelData.Slug, ChannelType).First(&existingPrice)
if result.Error == nil {
// 使用processPrice函数处理更新
_, changed, err := handlers.ProcessPrice(price, &existingPrice, true, CreatedBy)
if err != nil {
log.Printf("更新价格记录失败 %s: %v", modelData.Slug, err)
skippedCount++
continue
}
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)
skippedCount++
continue
}
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
}

View File

@ -8,9 +8,9 @@ import (
"encoding/json"
"strings"
"time"
"aimodels-prices/database"
"aimodels-prices/handlers"
"aimodels-prices/models"
)
@ -28,6 +28,11 @@ var blacklist = []string{
"shap-e",
"palm-2",
"o3-mini-high",
"claude-instant",
"claude-1",
"claude-3-haiku",
"claude-3-opus",
"claude-3-sonnet",
}
const (
@ -94,6 +99,20 @@ func UpdateOtherPrices() error {
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)
@ -102,7 +121,14 @@ func UpdateOtherPrices() error {
var inputPrice, outputPrice float64
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 != "" {
inputPrice, parseErr = parsePrice(modelData.Endpoint.Pricing.Prompt)
if parseErr != nil {
@ -110,14 +136,6 @@ func UpdateOtherPrices() error {
skippedCount++
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 != "" {
@ -127,40 +145,10 @@ func UpdateOtherPrices() error {
skippedCount++
continue
}
} else if modelData.Pricing.Completion != "" {
outputPrice, parseErr = parsePrice(modelData.Pricing.Completion)
if parseErr != nil {
log.Printf("解析输出价格失败 %s: %v", modelData.Slug, parseErr)
skippedCount++
continue
}
}
// 检查是否已存在相同模型的价格记录
var existingPrice models.Price
result := db.Where("model = ? AND channel_type = ?", modelName, channelType).First(&existingPrice)
if result.Error == nil {
// 更新现有记录
existingPrice.ModelType = modelType
existingPrice.BillingType = BillingType
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)
skippedCount++
continue
}
log.Printf("更新价格记录: %s (厂商: %s)", modelName, author)
processedCount++
} else {
// 创建新记录
newPrice := models.Price{
// 创建价格对象
price := models.Price{
Model: modelName,
ModelType: modelType,
BillingType: BillingType,
@ -173,13 +161,42 @@ func UpdateOtherPrices() error {
CreatedBy: CreatedBy,
}
if err := db.Create(&newPrice).Error; err != nil {
// 检查是否已存在相同模型的价格记录
var existingPrice models.Price
result := db.Where("model = ? AND channel_type = ?", modelName, channelType).First(&existingPrice)
if result.Error == nil {
// 使用processPrice函数处理更新
_, changed, err := handlers.ProcessPrice(price, &existingPrice, false, CreatedBy)
if err != nil {
log.Printf("更新价格记录失败 %s: %v", modelName, err)
skippedCount++
continue
}
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)
skippedCount++
continue
}
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") // 厂商筛选参数
modelType := c.Query("model_type") // 模型类型筛选参数
searchQuery := c.Query("search") // 搜索查询参数
status := c.Query("status") // 状态筛选参数
if page < 1 {
page = 1
@ -31,8 +32,8 @@ func GetPrices(c *gin.Context) {
offset := (page - 1) * pageSize
// 构建缓存键
cacheKey := fmt.Sprintf("prices_page_%d_size_%d_channel_%s_type_%s_search_%s",
page, pageSize, channelType, modelType, searchQuery)
cacheKey := fmt.Sprintf("prices_page_%d_size_%d_channel_%s_type_%s_search_%s_status_%s",
page, pageSize, channelType, modelType, searchQuery, status)
// 尝试从缓存获取
if cachedData, found := database.GlobalCache.Get(cacheKey); found {
@ -56,10 +57,15 @@ func GetPrices(c *gin.Context) {
if searchQuery != "" {
query = query.Where("model LIKE ?", "%"+searchQuery+"%")
}
// 添加状态筛选条件
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数 - 使用缓存优化
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 t, ok := cachedTotal.(int64); ok {
@ -97,6 +103,118 @@ func GetPrices(c *gin.Context) {
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) {
var price models.Price
if err := c.ShouldBindJSON(&price); err != nil {
@ -123,19 +241,27 @@ func CreatePrice(c *gin.Context) {
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"})
return
}
// 清除所有价格相关缓存
if changed {
clearPriceCache()
}
c.JSON(http.StatusCreated, price)
c.JSON(http.StatusCreated, result)
}
func UpdatePriceStatus(c *gin.Context) {
@ -209,9 +335,21 @@ func UpdatePriceStatus(c *gin.Context) {
return
}
} 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
}
} else {
// 如果是更新的价格恢复到原始状态清除临时字段并设置状态为approved
if err := tx.Model(&price).Updates(map[string]interface{}{
"status": input.Status,
"status": "approved", // 恢复为已批准状态
"updated_at": time.Now(),
"temp_model": nil,
"temp_model_type": nil,
@ -228,6 +366,7 @@ func UpdatePriceStatus(c *gin.Context) {
return
}
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
@ -239,11 +378,20 @@ func UpdatePriceStatus(c *gin.Context) {
// 清除所有价格相关缓存
clearPriceCache()
// 根据操作类型返回不同的消息
if input.Status == "rejected" && (price.Model == "" || (price.TempModel != nil && price.Model == *price.TempModel)) {
c.JSON(http.StatusOK, gin.H{
"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) {
@ -288,55 +436,19 @@ func UpdatePrice(c *gin.Context) {
return
}
// 根据用户角色决定更新方式
if currentUser.Role == "admin" {
// 管理员直接更新主字段
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 = &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 {
// 处理价格更新
result, changed, err := ProcessPrice(price, &existingPrice, currentUser.Role == "admin", currentUser.Username)
if 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
}
}
// 清除所有价格相关缓存
if changed {
clearPriceCache()
}
c.JSON(http.StatusOK, existingPrice)
c.JSON(http.StatusOK, result)
}
func DeletePrice(c *gin.Context) {
@ -362,6 +474,16 @@ func DeletePrice(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
if err := database.DB.Where("status = 'pending'").Find(&pendingPrices).Error; err != nil {
@ -371,8 +493,12 @@ func ApproveAllPrices(c *gin.Context) {
// 开始事务
tx := database.DB.Begin()
processedCount := 0
deletedCount := 0
for _, price := range pendingPrices {
if input.Action == "approve" {
// 批准操作
updateMap := map[string]interface{}{
"status": "approved",
"updated_at": time.Now(),
@ -420,6 +546,42 @@ func ApproveAllPrices(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve prices"})
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()
// 根据操作类型返回不同的消息
if input.Action == "approve" {
c.JSON(http.StatusOK, gin.H{
"message": "All pending prices approved successfully",
"count": len(pendingPrices),
"count": processedCount,
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "All pending prices rejected successfully",
"processed": processedCount,
"deleted": deletedCount,
"total": processedCount + deletedCount,
})
}
}
// clearPriceCache 清除所有价格相关的缓存

View File

@ -23,39 +23,28 @@
</template>
<!-- 添加搜索框 -->
<div class="search-section">
<el-input
v-model="searchQuery"
placeholder="搜索模型名称"
clearable
prefix-icon="Search"
@input="handleSearch"
>
<div class="filter-section">
<div class="filter-label" style="min-width:80px;">搜索模型:</div>
<div>
<el-input v-model="searchQuery" placeholder="搜索模型名称" clearable prefix-icon="Search" @input="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
<div class="filter-section">
<div class="filter-label">厂商筛选:</div>
<div class="filter-label" style="min-width:80px;">厂商筛选:</div>
<div class="provider-filters">
<el-button
:type="!selectedProvider ? 'primary' : ''"
@click="selectedProvider = ''"
>全部</el-button>
<el-button
v-for="provider in providers"
:key="provider.id"
<el-button :type="!selectedProvider ? 'primary' : ''" @click="selectedProvider = ''">全部</el-button>
<el-button v-for="provider in providers" :key="provider.id"
: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">
<el-image
v-if="provider.icon"
:src="provider.icon"
style="width: 16px; height: 16px"
/>
<el-image v-if="provider.icon" :src="provider.icon" style="width: 16px; height: 16px" />
<span>{{ provider.name }}</span>
</div>
</el-button>
@ -63,18 +52,11 @@
</div>
<div class="filter-section">
<div class="filter-label">模型类别:</div>
<div class="filter-label" style="min-width:80px;">模型类别:</div>
<div class="model-type-filters">
<el-button
:type="!selectedModelType ? 'primary' : ''"
@click="selectedModelType = ''"
>全部</el-button>
<el-button
v-for="(label, key) in modelTypeMap"
:key="key"
:type="selectedModelType === key ? 'primary' : ''"
@click="selectedModelType = key"
>
<el-button :type="!selectedModelType ? 'primary' : ''" @click="selectedModelType = ''">全部</el-button>
<el-button v-for="(label, key) in modelTypeMap" :key="key" :type="selectedModelType === key ? 'primary' : ''"
@click="selectedModelType = key">
{{ label }}
</el-button>
</div>
@ -87,13 +69,8 @@
</div>
</template>
<el-table
:data="prices"
style="width: 100%"
@selection-change="handlePriceSelectionChange"
v-loading="tableLoading"
element-loading-text="加载中..."
>
<el-table :data="prices" 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 label="模型">
<template #default="{ row }">
@ -109,7 +86,8 @@
<template #default="{ row }">
<div class="value-container">
<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) }}
</el-tag>
</div>
@ -119,7 +97,8 @@
<template #default="{ row }">
<div class="value-container">
<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) }}
</el-tag>
</div>
@ -129,14 +108,12 @@
<template #default="{ row }">
<div class="value-container">
<div style="display: flex; align-items: center; gap: 8px">
<el-image
v-if="getProvider(row.channel_type)?.icon"
:src="getProvider(row.channel_type)?.icon"
style="width: 24px; height: 24px"
/>
<el-image v-if="getProvider(row.channel_type)?.icon" :src="getProvider(row.channel_type)?.icon"
style="width: 24px; height: 24px" />
<span>{{ getProvider(row.channel_type)?.name || row.channel_type }}</span>
</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 }}
</el-tag>
</div>
@ -146,7 +123,8 @@
<template #default="{ row }">
<div class="value-container">
<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 }}
</el-tag>
</div>
@ -156,7 +134,9 @@
<template #default="{ row }">
<div class="value-container">
<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 }}
</el-tag>
</div>
@ -166,7 +146,9 @@
<template #default="{ row }">
<div class="value-container">
<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 }}
</el-tag>
</div>
@ -174,11 +156,7 @@
</el-table-column>
<el-table-column width="80">
<template #default="{ row }">
<el-popover
placement="left"
:width="200"
trigger="hover"
>
<el-popover placement="left" :width="200" trigger="hover">
<template #reference>
<el-button link type="primary">详情</el-button>
</template>
@ -191,7 +169,8 @@
<span class="detail-label">价格来源:</span>
<div class="detail-value">
<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 }}
</el-tag>
</div>
@ -210,44 +189,41 @@
<template v-if="isAdmin">
<el-tooltip content="编辑" placement="top">
<el-button type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
<el-icon>
<Edit />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip :content="row.status === 'pending' ? '通过审核' : '已审核'" placement="top">
<el-button
type="success"
link
@click="updateStatus(row.id, 'approved')"
:disabled="row.status !== 'pending'"
>
<el-icon><Check /></el-icon>
<el-button type="success" link @click="updateStatus(row.id, 'approved')"
:disabled="row.status !== 'pending'">
<el-icon>
<Check />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip :content="row.status === 'pending' ? '拒绝审核' : '已审核'" placement="top">
<el-button
type="danger"
link
@click="updateStatus(row.id, 'rejected')"
:disabled="row.status !== 'pending'"
>
<el-icon><Close /></el-icon>
<el-button type="danger" link @click="updateStatus(row.id, 'rejected')"
:disabled="row.status !== 'pending'">
<el-icon>
<Close />
</el-icon>
</el-button>
</el-tooltip>
</template>
<template v-else>
<el-tooltip :content="row.status === 'pending' ? '等待审核中' : '提交修改'" placement="top">
<el-button
type="primary"
link
@click="handleQuickEdit(row)"
:disabled="row.status === 'pending'"
>
<el-icon><Edit /></el-icon>
<el-button type="primary" link @click="handleQuickEdit(row)" :disabled="row.status === 'pending'">
<el-icon>
<Edit />
</el-icon>
</el-button>
</el-tooltip>
</template>
@ -258,18 +234,12 @@
<!-- 修改分页组件 -->
<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"
:small="false"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<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" :small="false" @size-change="handleSizeChange"
@current-change="handleCurrentChange">
<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>
@ -283,25 +253,16 @@
<div class="batch-toolbar">
<el-button type="primary" @click="addRow">添加行</el-button>
<el-divider direction="vertical" />
<el-popover
placement="bottom"
:width="400"
trigger="click"
>
<el-popover placement="bottom" :width="400" trigger="click">
<template #reference>
<el-button type="success">从表格导入</el-button>
</template>
<div class="import-popover">
<p class="import-tip">请粘贴表格数据支持从Excel复制每行格式为</p>
<p class="import-format">模型名称 计费类型 厂商 货币 输入价格 输出价格</p>
<el-input
v-model="importText"
type="textarea"
:rows="8"
placeholder="例如
<el-input v-model="importText" type="textarea" :rows="8" placeholder="例如
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">
<el-button type="primary" @click="handleImport">导入</el-button>
</div>
@ -309,22 +270,22 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-popover>
</div>
<el-table
:data="batchForms"
style="width: 100%"
height="400"
>
<el-table :data="batchForms" style="width: 100%" height="400">
<el-table-column label="操作" width="100">
<template #default="{ row, $index }">
<div class="row-actions">
<el-tooltip content="复制" placement="top">
<el-button type="primary" link @click="duplicateRow($index)">
<el-icon><Document /></el-icon>
<el-icon>
<Document />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button type="danger" link @click="removeRow($index)">
<el-icon><Delete /></el-icon>
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
</div>
@ -337,19 +298,9 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-table-column>
<el-table-column label="模型类型" width="120">
<template #default="{ row }">
<el-select
v-model="row.model_type"
placeholder="请选择或输入"
allow-create
filterable
@create="handleModelTypeCreate"
>
<el-option
v-for="(label, value) in modelTypeMap"
:key="value"
:label="label"
:value="value"
/>
<el-select v-model="row.model_type" placeholder="请选择或输入" allow-create filterable
@create="handleModelTypeCreate">
<el-option v-for="(label, value) in modelTypeMap" :key="value" :label="label" :value="value" />
</el-select>
</template>
</el-table-column>
@ -364,18 +315,10 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
<el-table-column label="模型厂商" width="180">
<template #default="{ row }">
<el-select v-model="row.channel_type" placeholder="请选择">
<el-option
v-for="provider in providers"
:key="provider.id"
:label="provider.name"
:value="provider.id.toString()"
>
<el-option v-for="provider in providers" :key="provider.id" :label="provider.name"
:value="provider.id.toString()">
<div style="display: flex; align-items: center; gap: 8px">
<el-image
v-if="provider.icon"
:src="provider.icon"
style="width: 24px; height: 24px"
/>
<el-image v-if="provider.icon" :src="provider.icon" style="width: 24px; height: 24px" />
<span>{{ provider.name }}</span>
</div>
</el-option>
@ -392,12 +335,14 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-table-column>
<el-table-column label="输入价格(M)" width="150">
<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>
</el-table-column>
<el-table-column label="输出价格(M)" width="150">
<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>
</el-table-column>
<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
v-model="dialogVisible"
:title="editingPrice ? (isAdmin ? '编辑价格' : '提交价格修改') : '提交价格'"
width="700px"
>
<el-dialog v-model="dialogVisible" :title="editingPrice ? (isAdmin ? '编辑价格' : '提交价格修改') : '提交价格'" width="700px">
<el-form :model="form" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
@ -432,19 +373,9 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-col>
<el-col :span="12">
<el-form-item label="模型类型">
<el-select
v-model="form.model_type"
placeholder="请选择或输入"
allow-create
filterable
@create="handleModelTypeCreate"
>
<el-option
v-for="(label, value) in modelTypeMap"
:key="value"
:label="label"
:value="value"
/>
<el-select v-model="form.model_type" placeholder="请选择或输入" allow-create filterable
@create="handleModelTypeCreate">
<el-option v-for="(label, value) in modelTypeMap" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
</el-col>
@ -459,18 +390,10 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
<el-col :span="12">
<el-form-item label="模型厂商">
<el-select v-model="form.channel_type" placeholder="请选择">
<el-option
v-for="provider in providers"
:key="provider.id"
:label="provider.name"
:value="provider.id.toString()"
>
<el-option v-for="provider in providers" :key="provider.id" :label="provider.name"
:value="provider.id.toString()">
<div style="display: flex; align-items: center; gap: 8px">
<el-image
v-if="provider.icon"
:src="provider.icon"
style="width: 24px; height: 24px"
/>
<el-image v-if="provider.icon" :src="provider.icon" style="width: 24px; height: 24px" />
<span>{{ provider.name }}</span>
</div>
</el-option>
@ -487,12 +410,14 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
</el-col>
<el-col :span="12">
<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-col>
<el-col :span="12">
<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-col>
<el-col :span="24">
@ -1109,7 +1034,12 @@ const removeRow = (index) => {
const approveAllPending = async () => {
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
if (pendingCount === 0) {
@ -1129,10 +1059,11 @@ const approveAllPending = async () => {
)
//
await axios.put('/api/prices/approve-all', { status: 'approved' })
const response = await axios.put('/api/prices/approve-all', { status: 'approved' })
await loadPrices()
ElMessage.success('已通过所有待审核价格')
// 使
ElMessage.success(`已通过 ${response.data.count} 条待审核价格`)
} catch (error) {
if (error === 'cancel') return
console.error('Failed to approve all pending prices:', error)
@ -1176,11 +1107,6 @@ onMounted(() => {
color: #606266;
}
.search-section {
margin: 16px 0;
max-width: 400px;
}
.provider-filters {
display: flex;
flex-wrap: wrap;
@ -1325,6 +1251,7 @@ onMounted(() => {
.el-loading-text {
color: #409EFF;
}
.path {
stroke: #409EFF;
}

View File

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