feat: add shipping template management and update favicon
This commit is contained in:
@@ -300,14 +300,80 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
break
|
||||
}
|
||||
|
||||
var shippingFeeFirstWeight, shippingFeePerGram, serviceFeeRate, taxRate, channelFeeRate float64
|
||||
// 获取收货地址省份
|
||||
var shippingAddress models.Address
|
||||
var province string
|
||||
if err := utils.DB.First(&shippingAddress, req.ShippingAddressID).Error; err == nil {
|
||||
province = shippingAddress.Province
|
||||
}
|
||||
|
||||
// 使用运费模板计算运费
|
||||
shippingFee := 0.0
|
||||
|
||||
// 按运费模板分组计算
|
||||
type TemplateGroup struct {
|
||||
TemplateID *uint
|
||||
Weight float64
|
||||
Quantity int
|
||||
Subtotal float64
|
||||
}
|
||||
templateGroups := make(map[uint]*TemplateGroup)
|
||||
noTemplateGroup := &TemplateGroup{Subtotal: 0, Weight: 0, Quantity: 0}
|
||||
|
||||
for _, cart := range carts {
|
||||
var templateID uint
|
||||
if cart.Product.ShippingTemplateID != nil {
|
||||
templateID = *cart.Product.ShippingTemplateID
|
||||
}
|
||||
|
||||
if templateID == 0 {
|
||||
// 没有运费模板,累加到无模板组
|
||||
noTemplateGroup.Weight += cart.Product.Weight * float64(cart.Quantity)
|
||||
noTemplateGroup.Quantity += cart.Quantity
|
||||
noTemplateGroup.Subtotal += cart.Product.Price * float64(cart.Quantity)
|
||||
} else {
|
||||
if _, ok := templateGroups[templateID]; !ok {
|
||||
templateGroups[templateID] = &TemplateGroup{
|
||||
TemplateID: &templateID,
|
||||
Weight: 0,
|
||||
Quantity: 0,
|
||||
Subtotal: 0,
|
||||
}
|
||||
}
|
||||
templateGroups[templateID].Weight += cart.Product.Weight * float64(cart.Quantity)
|
||||
templateGroups[templateID].Quantity += cart.Quantity
|
||||
templateGroups[templateID].Subtotal += cart.Product.Price * float64(cart.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算有模板的运费
|
||||
for _, group := range templateGroups {
|
||||
fee, _ := CalculateShippingFee(group.TemplateID, group.Weight, group.Quantity, group.Subtotal, province)
|
||||
shippingFee += fee
|
||||
}
|
||||
|
||||
// 计算无模板的运费(使用旧的系统设置)
|
||||
if noTemplateGroup.Subtotal > 0 {
|
||||
var shippingFeeFirstWeight, shippingFeePerGram float64
|
||||
var setting models.SystemSetting
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil {
|
||||
shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil {
|
||||
shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if noTemplateGroup.Subtotal < 99 && shippingFeeFirstWeight > 0 {
|
||||
fee := shippingFeeFirstWeight
|
||||
if noTemplateGroup.Weight > 500 {
|
||||
fee += shippingFeePerGram * (noTemplateGroup.Weight - 500)
|
||||
}
|
||||
shippingFee += fee
|
||||
}
|
||||
}
|
||||
|
||||
// 获取其他费率
|
||||
var serviceFeeRate, taxRate, channelFeeRate float64
|
||||
var setting models.SystemSetting
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil {
|
||||
shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil {
|
||||
shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if err := utils.DB.Where("`key` = ?", "service_fee_rate").First(&setting).Error; err == nil {
|
||||
serviceFeeRate, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
@@ -318,15 +384,6 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
channelFeeRate, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
|
||||
// 根据商品重量计算运费
|
||||
shippingFee := 0.0
|
||||
if subtotal < 99 && shippingFeeFirstWeight > 0 {
|
||||
shippingFee = shippingFeeFirstWeight
|
||||
if totalWeight > 500 {
|
||||
shippingFee += shippingFeePerGram * (totalWeight - 500)
|
||||
}
|
||||
}
|
||||
|
||||
serviceFee := subtotal * serviceFeeRate / 100
|
||||
tax := subtotal * taxRate / 100
|
||||
channelFee := subtotal * channelFeeRate / 100
|
||||
|
||||
@@ -225,7 +225,7 @@ func (h *ProductHandler) GetByID(c *gin.Context) {
|
||||
|
||||
func (h *ProductHandler) AdminList(c *gin.Context) {
|
||||
var products []models.Product
|
||||
utils.DB.Preload("Categories").Preload("Brand").Order("created_at DESC").Find(&products)
|
||||
utils.DB.Preload("Categories").Preload("Brand").Preload("ShippingTemplate").Order("created_at DESC").Find(&products)
|
||||
c.JSON(http.StatusOK, gin.H{"data": products})
|
||||
}
|
||||
|
||||
@@ -237,22 +237,23 @@ func (h *ProductHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
product := models.Product{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Price: req.Price,
|
||||
Weight: req.Weight,
|
||||
MinPurchase: req.MinPurchase,
|
||||
MaxPurchase: req.MaxPurchase,
|
||||
MinWeight: req.MinWeight,
|
||||
MaxWeight: req.MaxWeight,
|
||||
MinAmount: req.MinAmount,
|
||||
MaxAmount: req.MaxAmount,
|
||||
RequireCredit: req.RequireCredit,
|
||||
CreditCost: req.CreditCost,
|
||||
CreditReward: req.CreditReward,
|
||||
Images: req.Images,
|
||||
BrandID: req.BrandID,
|
||||
IsActive: true,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Price: req.Price,
|
||||
Weight: req.Weight,
|
||||
MinPurchase: req.MinPurchase,
|
||||
MaxPurchase: req.MaxPurchase,
|
||||
MinWeight: req.MinWeight,
|
||||
MaxWeight: req.MaxWeight,
|
||||
MinAmount: req.MinAmount,
|
||||
MaxAmount: req.MaxAmount,
|
||||
RequireCredit: req.RequireCredit,
|
||||
CreditCost: req.CreditCost,
|
||||
CreditReward: req.CreditReward,
|
||||
Images: req.Images,
|
||||
BrandID: req.BrandID,
|
||||
ShippingTemplateID: req.ShippingTemplateID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := utils.DB.Create(&product).Error; err != nil {
|
||||
@@ -276,7 +277,7 @@ func (h *ProductHandler) Create(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").Preload("ShippingTemplate").First(&product, product.ID)
|
||||
c.JSON(http.StatusCreated, gin.H{"data": product})
|
||||
}
|
||||
|
||||
@@ -343,6 +344,9 @@ func (h *ProductHandler) Update(c *gin.Context) {
|
||||
if req.BrandID != nil {
|
||||
updates["brand_id"] = *req.BrandID
|
||||
}
|
||||
if req.ShippingTemplateID != nil {
|
||||
updates["shipping_template_id"] = *req.ShippingTemplateID
|
||||
}
|
||||
|
||||
utils.DB.Model(&product).Updates(updates)
|
||||
|
||||
@@ -363,7 +367,7 @@ func (h *ProductHandler) Update(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").Preload("ShippingTemplate").First(&product, product.ID)
|
||||
c.JSON(http.StatusOK, gin.H{"data": product})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"sale/internal/models"
|
||||
"sale/internal/schemas"
|
||||
"sale/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ShippingTemplateHandler struct{}
|
||||
|
||||
func NewShippingTemplateHandler() *ShippingTemplateHandler {
|
||||
return &ShippingTemplateHandler{}
|
||||
}
|
||||
|
||||
// List 获取运费模板列表
|
||||
func (h *ShippingTemplateHandler) List(c *gin.Context) {
|
||||
var templates []models.ShippingTemplate
|
||||
utils.DB.Order("sort_order ASC, id ASC").Find(&templates)
|
||||
|
||||
// 解析 provinces JSON 为数组
|
||||
type TemplateResponse struct {
|
||||
models.ShippingTemplate
|
||||
ProvincesList []string `json:"provinces_list"`
|
||||
}
|
||||
|
||||
var response []TemplateResponse
|
||||
for _, t := range templates {
|
||||
var provinces []string
|
||||
if t.Provinces != "" {
|
||||
json.Unmarshal([]byte(t.Provinces), &provinces)
|
||||
}
|
||||
response = append(response, TemplateResponse{
|
||||
ShippingTemplate: t,
|
||||
ProvincesList: provinces,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": response})
|
||||
}
|
||||
|
||||
// GetByID 获取单个运费模板
|
||||
func (h *ShippingTemplateHandler) GetByID(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var template models.ShippingTemplate
|
||||
if err := utils.DB.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "运费模板不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var provinces []string
|
||||
if template.Provinces != "" {
|
||||
json.Unmarshal([]byte(template.Provinces), &provinces)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": gin.H{
|
||||
"ShippingTemplate": template,
|
||||
"provinces_list": provinces,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create 创建运费模板
|
||||
func (h *ShippingTemplateHandler) Create(c *gin.Context) {
|
||||
var req schemas.CreateShippingTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果设为默认,先取消其他默认模板
|
||||
if req.IsDefault {
|
||||
utils.DB.Model(&models.ShippingTemplate{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
}
|
||||
|
||||
// 序列化省份列表
|
||||
var provincesJSON string
|
||||
if len(req.Provinces) > 0 {
|
||||
data, _ := json.Marshal(req.Provinces)
|
||||
provincesJSON = string(data)
|
||||
}
|
||||
|
||||
template := models.ShippingTemplate{
|
||||
Name: req.Name,
|
||||
CalcType: req.CalcType,
|
||||
FirstUnit: req.FirstUnit,
|
||||
FirstFee: req.FirstFee,
|
||||
AdditionalUnit: req.AdditionalUnit,
|
||||
AdditionalFee: req.AdditionalFee,
|
||||
FreeAmount: req.FreeAmount,
|
||||
Provinces: provincesJSON,
|
||||
IsDefault: req.IsDefault,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
if err := utils.DB.Create(&template).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建运费模板失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": template})
|
||||
}
|
||||
|
||||
// Update 更新运费模板
|
||||
func (h *ShippingTemplateHandler) Update(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var template models.ShippingTemplate
|
||||
if err := utils.DB.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "运费模板不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req schemas.UpdateShippingTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if req.Name != nil {
|
||||
updates["name"] = *req.Name
|
||||
}
|
||||
if req.CalcType != nil {
|
||||
updates["calc_type"] = *req.CalcType
|
||||
}
|
||||
if req.FirstUnit != nil {
|
||||
updates["first_unit"] = *req.FirstUnit
|
||||
}
|
||||
if req.FirstFee != nil {
|
||||
updates["first_fee"] = *req.FirstFee
|
||||
}
|
||||
if req.AdditionalUnit != nil {
|
||||
updates["additional_unit"] = *req.AdditionalUnit
|
||||
}
|
||||
if req.AdditionalFee != nil {
|
||||
updates["additional_fee"] = *req.AdditionalFee
|
||||
}
|
||||
if req.FreeAmount != nil {
|
||||
updates["free_amount"] = *req.FreeAmount
|
||||
}
|
||||
if req.Provinces != nil {
|
||||
if len(req.Provinces) > 0 {
|
||||
data, _ := json.Marshal(req.Provinces)
|
||||
updates["provinces"] = string(data)
|
||||
} else {
|
||||
updates["provinces"] = ""
|
||||
}
|
||||
}
|
||||
if req.IsDefault != nil {
|
||||
if *req.IsDefault {
|
||||
utils.DB.Model(&models.ShippingTemplate{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
||||
}
|
||||
updates["is_default"] = *req.IsDefault
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
updates["sort_order"] = *req.SortOrder
|
||||
}
|
||||
|
||||
utils.DB.Model(&template).Updates(updates)
|
||||
utils.DB.First(&template, id)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": template})
|
||||
}
|
||||
|
||||
// Delete 删除运费模板
|
||||
func (h *ShippingTemplateHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var template models.ShippingTemplate
|
||||
if err := utils.DB.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "运费模板不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有关联的商品
|
||||
var count int64
|
||||
utils.DB.Model(&models.Product{}).Where("shipping_template_id = ?", id).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "该运费模板已被商品关联,无法删除"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.DB.Delete(&template).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除运费模板失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
// CalculateShippingFee 计算运费(供内部调用)
|
||||
func CalculateShippingFee(templateID *uint, weight float64, quantity int, subtotal float64, province string) (float64, error) {
|
||||
var template models.ShippingTemplate
|
||||
|
||||
// 如果没有指定模板,使用默认模板
|
||||
if templateID == nil || *templateID == 0 {
|
||||
if err := utils.DB.Where("is_default = ?", true).First(&template).Error; err != nil {
|
||||
// 没有默认模板,运费为0
|
||||
return 0, nil
|
||||
}
|
||||
} else {
|
||||
if err := utils.DB.First(&template, *templateID).Error; err != nil {
|
||||
// 模板不存在,使用默认模板
|
||||
if err := utils.DB.Where("is_default = ?", true).First(&template).Error; err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查适用地区
|
||||
if template.Provinces != "" && province != "" {
|
||||
var provinces []string
|
||||
json.Unmarshal([]byte(template.Provinces), &provinces)
|
||||
matched := false
|
||||
for _, p := range provinces {
|
||||
if p == province {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
// 地址不在适用范围内,使用默认模板
|
||||
var defaultTemplate models.ShippingTemplate
|
||||
if err := utils.DB.Where("is_default = ?", true).First(&defaultTemplate).Error; err == nil {
|
||||
template = defaultTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否满足包邮条件
|
||||
if template.FreeAmount > 0 && subtotal >= template.FreeAmount {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 根据计费方式计算运费
|
||||
var totalUnit float64
|
||||
if template.CalcType == models.CalcTypeWeight {
|
||||
totalUnit = weight // 重量(克)
|
||||
} else {
|
||||
totalUnit = float64(quantity) // 件数
|
||||
}
|
||||
|
||||
// 计算运费
|
||||
if totalUnit <= template.FirstUnit {
|
||||
return template.FirstFee, nil
|
||||
}
|
||||
|
||||
remainingUnit := totalUnit - template.FirstUnit
|
||||
additionalCount := math.Ceil(remainingUnit / template.AdditionalUnit)
|
||||
|
||||
return template.FirstFee + additionalCount*template.AdditionalFee, nil
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func SetupRoutes(r *gin.Engine) {
|
||||
paymentHandler := handlers.NewPaymentHandler()
|
||||
logHandler := handlers.NewLogHandler()
|
||||
purchaseGroupHandler := handlers.NewPurchaseGroupHandler()
|
||||
shippingTemplateHandler := handlers.NewShippingTemplateHandler()
|
||||
|
||||
r.Use(middlewares.CORSMiddleware())
|
||||
|
||||
@@ -250,6 +251,15 @@ func SetupRoutes(r *gin.Engine) {
|
||||
admin.GET("/logs", logHandler.GetLogs)
|
||||
admin.DELETE("/logs", logHandler.ClearLogs)
|
||||
admin.GET("/logs/info", logHandler.GetLogInfo)
|
||||
|
||||
adminShippingTemplates := admin.Group("/shipping-templates")
|
||||
{
|
||||
adminShippingTemplates.GET("", shippingTemplateHandler.List)
|
||||
adminShippingTemplates.GET("/:id", shippingTemplateHandler.GetByID)
|
||||
adminShippingTemplates.POST("", shippingTemplateHandler.Create)
|
||||
adminShippingTemplates.PUT("/:id", shippingTemplateHandler.Update)
|
||||
adminShippingTemplates.DELETE("/:id", shippingTemplateHandler.Delete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,29 +7,31 @@ import (
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
||||
Weight float64 `gorm:"type:decimal(10,2);default:0" json:"weight"` // 商品克重(克)
|
||||
MinPurchase int `gorm:"default:1" json:"min_purchase"`
|
||||
MaxPurchase *int `json:"max_purchase"`
|
||||
MinWeight *float64 `json:"min_weight"`
|
||||
MaxWeight *float64 `json:"max_weight"`
|
||||
MinAmount *float64 `json:"min_amount"`
|
||||
MaxAmount *float64 `json:"max_amount"`
|
||||
RequireCredit bool `gorm:"default:false" json:"require_credit"`
|
||||
CreditCost int `gorm:"default:0" json:"credit_cost"`
|
||||
CreditReward int `gorm:"default:0" json:"credit_reward"`
|
||||
Images string `gorm:"type:text" json:"images"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
BrandID *uint `json:"brand_id"`
|
||||
Brand *Brand `json:"brand,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
Categories []Category `gorm:"many2many:product_categories;" json:"categories,omitempty"`
|
||||
CustomFields []ProductCustomField `json:"custom_fields,omitempty"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
||||
Weight float64 `gorm:"type:decimal(10,2);default:0" json:"weight"` // 商品克重(克)
|
||||
MinPurchase int `gorm:"default:1" json:"min_purchase"`
|
||||
MaxPurchase *int `json:"max_purchase"`
|
||||
MinWeight *float64 `json:"min_weight"`
|
||||
MaxWeight *float64 `json:"max_weight"`
|
||||
MinAmount *float64 `json:"min_amount"`
|
||||
MaxAmount *float64 `json:"max_amount"`
|
||||
RequireCredit bool `gorm:"default:false" json:"require_credit"`
|
||||
CreditCost int `gorm:"default:0" json:"credit_cost"`
|
||||
CreditReward int `gorm:"default:0" json:"credit_reward"`
|
||||
Images string `gorm:"type:text" json:"images"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
BrandID *uint `json:"brand_id"`
|
||||
Brand *Brand `json:"brand,omitempty"`
|
||||
ShippingTemplateID *uint `json:"shipping_template_id"`
|
||||
ShippingTemplate *ShippingTemplate `json:"shipping_template,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
Categories []Category `gorm:"many2many:product_categories;" json:"categories,omitempty"`
|
||||
CustomFields []ProductCustomField `json:"custom_fields,omitempty"`
|
||||
}
|
||||
|
||||
func (Product) TableName() string {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShippingTemplate 运费模板
|
||||
type ShippingTemplate struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"` // 模板名称(如"江浙沪包邮"、"偏远地区")
|
||||
CalcType string `gorm:"size:20;not null;default:'weight'" json:"calc_type"` // 计费方式:weight=按重量, piece=按件数
|
||||
FirstUnit float64 `gorm:"type:decimal(10,2);not null" json:"first_unit"` // 首重/首件数量
|
||||
FirstFee float64 `gorm:"type:decimal(10,2);not null" json:"first_fee"` // 首重/首件价格
|
||||
AdditionalUnit float64 `gorm:"type:decimal(10,2);not null" json:"additional_unit"` // 续重/续件数量
|
||||
AdditionalFee float64 `gorm:"type:decimal(10,2);not null" json:"additional_fee"` // 续重/续件价格
|
||||
FreeAmount float64 `gorm:"type:decimal(10,2);default:0" json:"free_amount"` // 包邮金额(0表示不包邮)
|
||||
Provinces string `gorm:"type:text" json:"provinces"` // 适用省份列表(JSON数组,为空表示全国)
|
||||
IsDefault bool `gorm:"default:false" json:"is_default"` // 是否默认模板
|
||||
SortOrder int `gorm:"default:0" json:"sort_order"` // 排序
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (ShippingTemplate) TableName() string {
|
||||
return "shipping_templates"
|
||||
}
|
||||
|
||||
// 计费方式常量
|
||||
const (
|
||||
CalcTypeWeight = "weight" // 按重量计费
|
||||
CalcTypePiece = "piece" // 按件数计费
|
||||
)
|
||||
@@ -41,44 +41,46 @@ type UpdatePurchaseGroupRequest struct {
|
||||
}
|
||||
|
||||
type CreateProductRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `json:"price" binding:"gte=0"`
|
||||
Weight float64 `json:"weight"`
|
||||
MinPurchase int `json:"min_purchase"`
|
||||
MaxPurchase *int `json:"max_purchase"`
|
||||
MinWeight *float64 `json:"min_weight"`
|
||||
MaxWeight *float64 `json:"max_weight"`
|
||||
MinAmount *float64 `json:"min_amount"`
|
||||
MaxAmount *float64 `json:"max_amount"`
|
||||
RequireCredit bool `json:"require_credit"`
|
||||
CreditCost int `json:"credit_cost"`
|
||||
CreditReward int `json:"credit_reward"`
|
||||
Images string `json:"images"`
|
||||
BrandID *uint `json:"brand_id"`
|
||||
CategoryIDs []uint `json:"category_ids"`
|
||||
CustomFields []CustomFieldRequest `json:"custom_fields"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `json:"price" binding:"gte=0"`
|
||||
Weight float64 `json:"weight"`
|
||||
MinPurchase int `json:"min_purchase"`
|
||||
MaxPurchase *int `json:"max_purchase"`
|
||||
MinWeight *float64 `json:"min_weight"`
|
||||
MaxWeight *float64 `json:"max_weight"`
|
||||
MinAmount *float64 `json:"min_amount"`
|
||||
MaxAmount *float64 `json:"max_amount"`
|
||||
RequireCredit bool `json:"require_credit"`
|
||||
CreditCost int `json:"credit_cost"`
|
||||
CreditReward int `json:"credit_reward"`
|
||||
Images string `json:"images"`
|
||||
BrandID *uint `json:"brand_id"`
|
||||
ShippingTemplateID *uint `json:"shipping_template_id"`
|
||||
CategoryIDs []uint `json:"category_ids"`
|
||||
CustomFields []CustomFieldRequest `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type UpdateProductRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Price *float64 `json:"price"`
|
||||
Weight *float64 `json:"weight"`
|
||||
MinPurchase *int `json:"min_purchase"`
|
||||
MaxPurchase *int `json:"max_purchase"`
|
||||
MinWeight *float64 `json:"min_weight"`
|
||||
MaxWeight *float64 `json:"max_weight"`
|
||||
MinAmount *float64 `json:"min_amount"`
|
||||
MaxAmount *float64 `json:"max_amount"`
|
||||
RequireCredit *bool `json:"require_credit"`
|
||||
CreditCost *int `json:"credit_cost"`
|
||||
CreditReward *int `json:"credit_reward"`
|
||||
Images *string `json:"images"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
BrandID *uint `json:"brand_id"`
|
||||
CategoryIDs []uint `json:"category_ids"`
|
||||
CustomFields []CustomFieldRequest `json:"custom_fields"`
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Price *float64 `json:"price"`
|
||||
Weight *float64 `json:"weight"`
|
||||
MinPurchase *int `json:"min_purchase"`
|
||||
MaxPurchase *int `json:"max_purchase"`
|
||||
MinWeight *float64 `json:"min_weight"`
|
||||
MaxWeight *float64 `json:"max_weight"`
|
||||
MinAmount *float64 `json:"min_amount"`
|
||||
MaxAmount *float64 `json:"max_amount"`
|
||||
RequireCredit *bool `json:"require_credit"`
|
||||
CreditCost *int `json:"credit_cost"`
|
||||
CreditReward *int `json:"credit_reward"`
|
||||
Images *string `json:"images"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
BrandID *uint `json:"brand_id"`
|
||||
ShippingTemplateID *uint `json:"shipping_template_id"`
|
||||
CategoryIDs []uint `json:"category_ids"`
|
||||
CustomFields []CustomFieldRequest `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type CustomFieldRequest struct {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package schemas
|
||||
|
||||
// CreateShippingTemplateRequest 创建运费模板请求
|
||||
type CreateShippingTemplateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
CalcType string `json:"calc_type" binding:"required,oneof=weight piece"`
|
||||
FirstUnit float64 `json:"first_unit" binding:"required,gt=0"`
|
||||
FirstFee float64 `json:"first_fee" binding:"gte=0"`
|
||||
AdditionalUnit float64 `json:"additional_unit" binding:"required,gt=0"`
|
||||
AdditionalFee float64 `json:"additional_fee" binding:"gte=0"`
|
||||
FreeAmount float64 `json:"free_amount"`
|
||||
Provinces []string `json:"provinces"` // 省份列表
|
||||
IsDefault bool `json:"is_default"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// UpdateShippingTemplateRequest 更新运费模板请求
|
||||
type UpdateShippingTemplateRequest struct {
|
||||
Name *string `json:"name"`
|
||||
CalcType *string `json:"calc_type" binding:"omitempty,oneof=weight piece"`
|
||||
FirstUnit *float64 `json:"first_unit" binding:"omitempty,gt=0"`
|
||||
FirstFee *float64 `json:"first_fee" binding:"omitempty,gte=0"`
|
||||
AdditionalUnit *float64 `json:"additional_unit" binding:"omitempty,gt=0"`
|
||||
AdditionalFee *float64 `json:"additional_fee" binding:"omitempty,gte=0"`
|
||||
FreeAmount *float64 `json:"free_amount"`
|
||||
Provinces []string `json:"provinces"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sale</title>
|
||||
</head>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -190,4 +190,11 @@ export const adminApi = {
|
||||
getLogs: (lines?: number) => api.get('/admin/logs', { params: { lines: lines || 500 } }),
|
||||
clearLogs: () => api.delete('/admin/logs'),
|
||||
getLogInfo: () => api.get('/admin/logs/info'),
|
||||
|
||||
// 运费模板
|
||||
getShippingTemplates: () => api.get('/admin/shipping-templates'),
|
||||
getShippingTemplateById: (id: number) => api.get(`/admin/shipping-templates/${id}`),
|
||||
createShippingTemplate: (data: any) => api.post('/admin/shipping-templates', data),
|
||||
updateShippingTemplate: (id: number, data: any) => api.put(`/admin/shipping-templates/${id}`, data),
|
||||
deleteShippingTemplate: (id: number) => api.delete(`/admin/shipping-templates/${id}`),
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span class="nav-text">日志管理</span>
|
||||
</router-link>
|
||||
<router-link to="/admin/shipping-templates" class="nav-link">
|
||||
<el-icon><Box /></el-icon>
|
||||
<span class="nav-text">运费管理</span>
|
||||
</router-link>
|
||||
<router-link to="/admin/settings" class="nav-link">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span class="nav-text">{{ $t('admin.systemSettings') }}</span>
|
||||
@@ -168,7 +172,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { DataAnalysis, User, Folder, Grid, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet, Notebook } from '@element-plus/icons-vue'
|
||||
import { DataAnalysis, User, Folder, Grid, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet, Notebook, Box } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '../store/user'
|
||||
import { useCartStore } from '../store/cart'
|
||||
|
||||
@@ -197,6 +201,7 @@ const pageTitles: Record<string, string> = {
|
||||
'/admin/banners': '轮播图管理',
|
||||
'/admin/payment': '支付管理',
|
||||
'/admin/logs': '日志管理',
|
||||
'/admin/shipping-templates': '运费管理',
|
||||
'/admin/settings': '系统设置'
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const routes = [
|
||||
{ path: 'banners', name: 'AdminBanners', component: () => import('../views/admin/Banners.vue') },
|
||||
{ path: 'payment', name: 'AdminPayment', component: () => import('../views/admin/PaymentChannels.vue') },
|
||||
{ path: 'logs', name: 'AdminLogs', component: () => import('../views/admin/Logs.vue') },
|
||||
{ path: 'shipping-templates', name: 'AdminShippingTemplates', component: () => import('../views/admin/ShippingTemplates.vue') },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -133,6 +133,15 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="运费模板">
|
||||
<el-select v-model="form.shipping_template_id" placeholder="选择运费模板" clearable popper-class="dark-select-dropdown" style="width: 100%">
|
||||
<el-option v-for="t in shippingTemplates" :key="t.id" :label="t.name" :value="t.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
@@ -171,6 +180,7 @@ import { getImageUrl } from '../../utils/image'
|
||||
const products = ref<any[]>([])
|
||||
const categories = ref<any[]>([])
|
||||
const brands = ref<any[]>([])
|
||||
const shippingTemplates = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const showAdd = ref(false)
|
||||
@@ -202,7 +212,8 @@ const form = reactive({
|
||||
credit_reward: 0,
|
||||
is_active: true,
|
||||
category_ids: [] as number[],
|
||||
brand_id: undefined as number | undefined
|
||||
brand_id: undefined as number | undefined,
|
||||
shipping_template_id: undefined as number | undefined
|
||||
})
|
||||
|
||||
function getFirstImage(images: string) {
|
||||
@@ -268,13 +279,18 @@ async function fetchBrands() {
|
||||
brands.value = res.data || []
|
||||
}
|
||||
|
||||
async function fetchShippingTemplates() {
|
||||
const res: any = await adminApi.getShippingTemplates()
|
||||
shippingTemplates.value = res.data || []
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
editing.value = null
|
||||
Object.assign(form, {
|
||||
name: '', description: '', price: 0, weight: 0, stock: 0,
|
||||
min_purchase: 1, max_purchase: undefined,
|
||||
require_credit: false, credit_cost: 0, credit_reward: 0,
|
||||
is_active: true, category_ids: [], brand_id: undefined
|
||||
is_active: true, category_ids: [], brand_id: undefined, shipping_template_id: undefined
|
||||
})
|
||||
fileList.value = []
|
||||
uploadedUrls.value = []
|
||||
@@ -296,7 +312,8 @@ function editProd(row: any) {
|
||||
credit_reward: row.credit_reward || 0,
|
||||
is_active: row.is_active !== false,
|
||||
category_ids: (row.categories || []).map((c: any) => c.id),
|
||||
brand_id: row.brand_id
|
||||
brand_id: row.brand_id,
|
||||
shipping_template_id: row.shipping_template_id
|
||||
})
|
||||
const images = row.images ? row.images.split(',').map((s: string) => s.trim()).filter(Boolean) : []
|
||||
uploadedUrls.value = images
|
||||
@@ -350,6 +367,7 @@ onMounted(() => {
|
||||
fetchProducts()
|
||||
fetchCategories()
|
||||
fetchBrands()
|
||||
fetchShippingTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<div class="shipping-templates-page">
|
||||
<div class="page-header">
|
||||
<el-button type="primary" @click="openAdd">创建运费模板</el-button>
|
||||
</div>
|
||||
<div class="table-card">
|
||||
<el-table :data="templates" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="模板名称" min-width="150" />
|
||||
<el-table-column label="计费方式" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.calc_type === 'weight' ? 'primary' : 'success'" size="small">
|
||||
{{ row.calc_type === 'weight' ? '按重量' : '按件数' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="首重/首件" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.first_unit }}{{ row.calc_type === 'weight' ? '克' : '件' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="首费" width="100">
|
||||
<template #default="{ row }">¥{{ row.first_fee }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续重/续件" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.additional_unit }}{{ row.calc_type === 'weight' ? '克' : '件' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续费" width="100">
|
||||
<template #default="{ row }">¥{{ row.additional_fee }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="包邮金额" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.free_amount > 0 ? '¥' + row.free_amount : '不包邮' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用地区" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!row.provinces_list || row.provinces_list.length === 0">全国</span>
|
||||
<el-tag v-else v-for="p in row.provinces_list.slice(0, 3)" :key="p" size="small" style="margin-right: 4px;">{{ p }}</el-tag>
|
||||
<span v-if="row.provinces_list && row.provinces_list.length > 3">...</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_default" type="warning" size="small">默认</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="editTemplate(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteTemplate(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showAdd" :title="editing ? '编辑运费模板' : '创建运费模板'" width="600px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="模板名称" required>
|
||||
<el-input v-model="form.name" placeholder="如:江浙沪包邮、偏远地区" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计费方式" required>
|
||||
<el-radio-group v-model="form.calc_type">
|
||||
<el-radio value="weight">按重量</el-radio>
|
||||
<el-radio value="piece">按件数</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="首重/首件" required>
|
||||
<el-input-number v-model="form.first_unit" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="首费(元)" required>
|
||||
<el-input-number v-model="form.first_fee" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="续重/续件" required>
|
||||
<el-input-number v-model="form.additional_unit" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="续费(元)" required>
|
||||
<el-input-number v-model="form.additional_fee" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="包邮金额">
|
||||
<el-input-number v-model="form.free_amount" :min="0" :precision="2" style="width: 200px" />
|
||||
<span class="form-tip" style="margin-left: 8px; color: rgba(255,255,255,0.5);">0表示不包邮</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="适用地区">
|
||||
<el-select
|
||||
v-model="form.provinces"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="留空表示全国"
|
||||
popper-class="dark-select-dropdown"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="p in chinaProvinces" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设为默认">
|
||||
<el-switch v-model="form.is_default" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAdd = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveTemplate" :loading="saving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { adminApi } from '../../api'
|
||||
|
||||
const templates = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const showAdd = ref(false)
|
||||
const editing = ref<any>(null)
|
||||
|
||||
// 中国省份列表
|
||||
const chinaProvinces = [
|
||||
'北京市', '天津市', '上海市', '重庆市',
|
||||
'河北省', '山西省', '辽宁省', '吉林省', '黑龙江省',
|
||||
'江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省',
|
||||
'河南省', '湖北省', '湖南省', '广东省', '海南省',
|
||||
'四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省',
|
||||
'台湾省', '内蒙古自治区', '广西壮族自治区', '西藏自治区',
|
||||
'宁夏回族自治区', '新疆维吾尔自治区', '香港特别行政区', '澳门特别行政区'
|
||||
]
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
calc_type: 'weight',
|
||||
first_unit: 500,
|
||||
first_fee: 10,
|
||||
additional_unit: 100,
|
||||
additional_fee: 2,
|
||||
free_amount: 0,
|
||||
provinces: [] as string[],
|
||||
is_default: false,
|
||||
sort_order: 0
|
||||
})
|
||||
|
||||
async function fetchTemplates() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await adminApi.getShippingTemplates()
|
||||
templates.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
editing.value = null
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
calc_type: 'weight',
|
||||
first_unit: 500,
|
||||
first_fee: 10,
|
||||
additional_unit: 100,
|
||||
additional_fee: 2,
|
||||
free_amount: 0,
|
||||
provinces: [],
|
||||
is_default: false,
|
||||
sort_order: 0
|
||||
})
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
function editTemplate(row: any) {
|
||||
editing.value = row
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
calc_type: row.calc_type,
|
||||
first_unit: row.first_unit,
|
||||
first_fee: row.first_fee,
|
||||
additional_unit: row.additional_unit,
|
||||
additional_fee: row.additional_fee,
|
||||
free_amount: row.free_amount || 0,
|
||||
provinces: row.provinces_list || [],
|
||||
is_default: row.is_default,
|
||||
sort_order: row.sort_order || 0
|
||||
})
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!form.name) {
|
||||
ElMessage.warning('请输入模板名称')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const submitData = { ...form }
|
||||
if (editing.value) {
|
||||
await adminApi.updateShippingTemplate(editing.value.id, submitData)
|
||||
} else {
|
||||
await adminApi.createShippingTemplate(submitData)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
showAdd.value = false
|
||||
editing.value = null
|
||||
await fetchTemplates()
|
||||
} catch (error: any) {
|
||||
const msg = error.response?.data?.error || '保存失败'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(id: number) {
|
||||
await ElMessageBox.confirm('确定删除此运费模板?', '确认删除')
|
||||
try {
|
||||
await adminApi.deleteShippingTemplate(id)
|
||||
ElMessage.success('删除成功')
|
||||
await fetchTemplates()
|
||||
} catch (error: any) {
|
||||
const msg = error.response?.data?.error || '删除失败'
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchTemplates)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.shipping-templates-page { padding: 0; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
.table-card { background: #2d2d44; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 20px; }
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__inner) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #4e6ef2;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner),
|
||||
:deep(.el-textarea__inner) {
|
||||
color: #fff;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-input-number .el-input__wrapper) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
:deep(.el-radio-button__inner) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
||||
background: #4e6ef2;
|
||||
border-color: #4e6ef2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
background: #2d2d44;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 16px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__title) {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__headerbtn .el-dialog__close) {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user