feat(distribution): 支持分站发布群固定二维码
主站分销配置可为每个站点绑定可复用企微码,号主在该站上传账号后发到对应发布群,不再消耗主站码池。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ type DistributionSite struct {
|
||||
AnnouncementsJSON string `gorm:"type:text" json:"announcements_json"`
|
||||
FooterInfoJSON string `gorm:"type:text" json:"footer_info_json"`
|
||||
DefaultSupportGroupID *uint64 `gorm:"index" json:"default_support_group_id,omitempty"`
|
||||
ListingQrcodeURL string `gorm:"size:512;not null;default:''" json:"listing_qrcode_url"`
|
||||
TaxEnabled bool `gorm:"not null;default:false" json:"tax_enabled"`
|
||||
TaxRateBPS int64 `gorm:"not null;default:100" json:"tax_rate_bps"`
|
||||
SettlementHoldDays int `gorm:"not null;default:7" json:"settlement_hold_days"`
|
||||
|
||||
@@ -38,6 +38,11 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siteID, err := resolveListingDistributionSiteID(tx, listing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 建会话
|
||||
now := time.Now()
|
||||
conversation := model.ChatConversation{
|
||||
@@ -46,6 +51,9 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
|
||||
Title: listingConversationTitle(listing),
|
||||
Status: "active",
|
||||
}
|
||||
if siteID > 0 {
|
||||
conversation.DistributionSiteID = &siteID
|
||||
}
|
||||
if err := tx.Create(&conversation).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,17 +96,12 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 取二维码(带行锁)
|
||||
var qrcode *model.ChatQrCode
|
||||
var qrcodeImageURL string
|
||||
qrcodeObj, err := fetchUnusedQrCodeForListing(tx, listing.ID)
|
||||
// 4. 取二维码:分站优先用站点固定码(可复用);否则消耗对应码池。
|
||||
qrcode, qrcodeImageURL, err := resolveListingQrCode(tx, siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qrcodeObj != nil {
|
||||
qrcode = qrcodeObj
|
||||
qrcodeImageURL = qrcode.ImageURL
|
||||
// 标记二维码为已使用
|
||||
if qrcode != nil {
|
||||
if err := markQrCodeAsUsed(tx, qrcode.ID, conversation.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -309,22 +312,60 @@ func fetchUnusedQrCode(tx *gorm.DB) (*model.ChatQrCode, error) {
|
||||
return fetchUnusedQrCodeForSite(tx, nil)
|
||||
}
|
||||
|
||||
func fetchUnusedQrCodeForListing(tx *gorm.DB, listingID uint64) (*model.ChatQrCode, error) {
|
||||
func resolveListingDistributionSiteID(tx *gorm.DB, listing model.RentalListing) (uint64, error) {
|
||||
if listing.DistributionSiteID != nil && *listing.DistributionSiteID > 0 {
|
||||
return *listing.DistributionSiteID, nil
|
||||
}
|
||||
if listing.ID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var relation model.DistributionSiteAccount
|
||||
// 仅匹配启用中的分销站点,真实 DB 错误直接返回,避免误回退到全局二维码池。
|
||||
err := tx.Table("distribution_site_accounts AS relation").
|
||||
Select("relation.*").
|
||||
Joins("JOIN distribution_sites AS site ON site.id = relation.site_id AND site.status = 'active'").
|
||||
Where("relation.listing_id = ? AND relation.display_status = 'enabled'", listingID).
|
||||
Where("relation.listing_id = ? AND relation.display_status = 'enabled'", listing.ID).
|
||||
Order("relation.id ASC").
|
||||
Limit(1).
|
||||
Scan(&relation).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
if relation.ID != 0 {
|
||||
return fetchUnusedQrCodeForSite(tx, &relation.SiteID)
|
||||
return relation.SiteID, nil
|
||||
}
|
||||
return fetchUnusedQrCodeForSite(tx, nil)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func resolveListingQrCode(tx *gorm.DB, siteID uint64) (*model.ChatQrCode, string, error) {
|
||||
if siteID > 0 {
|
||||
var site model.DistributionSite
|
||||
err := tx.Select("id", "listing_qrcode_url").First(&site, siteID).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", err
|
||||
}
|
||||
if err == nil {
|
||||
if url := strings.TrimSpace(site.ListingQrcodeURL); url != "" {
|
||||
return nil, url, nil
|
||||
}
|
||||
}
|
||||
qrcode, err := fetchUnusedQrCodeForSite(tx, &siteID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if qrcode != nil {
|
||||
return qrcode, qrcode.ImageURL, nil
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
qrcode, err := fetchUnusedQrCodeForSite(tx, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if qrcode != nil {
|
||||
return qrcode, qrcode.ImageURL, nil
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
func fetchUnusedQrCodeForSite(tx *gorm.DB, siteID *uint64) (*model.ChatQrCode, error) {
|
||||
|
||||
@@ -479,3 +479,145 @@ func TestBatchCreateQrCodeDeliversPendingTask(t *testing.T) {
|
||||
t.Fatalf("未生成二维码图片消息: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureListingConversationUsesSiteFixedQrcode(t *testing.T) {
|
||||
db := setupQrCodeTestDB(t)
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "fx_qr_fixed",
|
||||
SiteName: "固定码分站",
|
||||
Domain: "fixed-qr.example.com",
|
||||
Status: "active",
|
||||
ListingQrcodeURL: "/api/files/object?key=qrcode/site-fixed.png",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatalf("创建分销站点失败: %v", err)
|
||||
}
|
||||
siteID := site.ID
|
||||
pool := []model.ChatQrCode{
|
||||
{ImageURL: "/api/files/object?key=qrcode/main-pool.png", Status: QrCodeStatusUnused},
|
||||
{ImageURL: "/api/files/object?key=qrcode/site-pool.png", Status: QrCodeStatusUnused, DistributionSiteID: &siteID},
|
||||
}
|
||||
if err := db.Create(&pool).Error; err != nil {
|
||||
t.Fatalf("创建码池失败: %v", err)
|
||||
}
|
||||
|
||||
listing := model.RentalListing{
|
||||
ID: 202,
|
||||
ListingNo: "L202609150001",
|
||||
OwnerID: 10,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
var conv *model.ChatConversation
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
created, err := EnsureListingConversation(tx, listing, 0)
|
||||
conv = created
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("建发布群失败: %v", err)
|
||||
}
|
||||
if conv == nil || conv.DistributionSiteID == nil || *conv.DistributionSiteID != siteID {
|
||||
t.Fatalf("发布群站点 = %v, want %d", conv.DistributionSiteID, siteID)
|
||||
}
|
||||
|
||||
var message model.ChatMessage
|
||||
if err := db.Where("conversation_id = ? AND content_type = ?", conv.ID, "image").First(&message).Error; err != nil {
|
||||
t.Fatalf("未发送固定二维码: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(message.AttachmentURLS), "site-fixed.png") {
|
||||
t.Fatalf("二维码附件 = %s, want 分站固定码", message.AttachmentURLS)
|
||||
}
|
||||
|
||||
var unusedCount int64
|
||||
if err := db.Model(&model.ChatQrCode{}).Where("status = ?", QrCodeStatusUnused).Count(&unusedCount).Error; err != nil {
|
||||
t.Fatalf("统计码池失败: %v", err)
|
||||
}
|
||||
if unusedCount != 2 {
|
||||
t.Fatalf("固定码仍应保留码池库存, unused = %d, want 2", unusedCount)
|
||||
}
|
||||
|
||||
var taskCount int64
|
||||
if err := db.Model(&model.ChatQrCodeDeliveryTask{}).Count(&taskCount).Error; err != nil {
|
||||
t.Fatalf("统计补发任务失败: %v", err)
|
||||
}
|
||||
if taskCount != 0 {
|
||||
t.Fatalf("固定码场景不应创建补发任务, got %d", taskCount)
|
||||
}
|
||||
|
||||
listing2 := model.RentalListing{
|
||||
ID: 203,
|
||||
ListingNo: "L202609150002",
|
||||
OwnerID: 11,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
_, err := EnsureListingConversation(tx, listing2, 0)
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("第二次建发布群失败: %v", err)
|
||||
}
|
||||
if err := db.Model(&model.ChatQrCode{}).Where("status = ?", QrCodeStatusUnused).Count(&unusedCount).Error; err != nil {
|
||||
t.Fatalf("再次统计码池失败: %v", err)
|
||||
}
|
||||
if unusedCount != 2 {
|
||||
t.Fatalf("同一分站固定码应可复用, unused = %d, want 2", unusedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureListingConversationSiteWithoutFixedQrUsesSitePool(t *testing.T) {
|
||||
db := setupQrCodeTestDB(t)
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "fx_qr_pool",
|
||||
SiteName: "码池分站",
|
||||
Domain: "pool-qr.example.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatalf("创建分销站点失败: %v", err)
|
||||
}
|
||||
siteID := site.ID
|
||||
pool := []model.ChatQrCode{
|
||||
{ImageURL: "/api/files/object?key=qrcode/main-pool.png", Status: QrCodeStatusUnused},
|
||||
{ImageURL: "/api/files/object?key=qrcode/site-pool.png", Status: QrCodeStatusUnused, DistributionSiteID: &siteID},
|
||||
}
|
||||
if err := db.Create(&pool).Error; err != nil {
|
||||
t.Fatalf("创建码池失败: %v", err)
|
||||
}
|
||||
|
||||
listing := model.RentalListing{
|
||||
ID: 204,
|
||||
ListingNo: "L202609150003",
|
||||
OwnerID: 12,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
var conv *model.ChatConversation
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
created, err := EnsureListingConversation(tx, listing, 0)
|
||||
conv = created
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("建发布群失败: %v", err)
|
||||
}
|
||||
|
||||
var message model.ChatMessage
|
||||
if err := db.Where("conversation_id = ? AND content_type = ?", conv.ID, "image").First(&message).Error; err != nil {
|
||||
t.Fatalf("未发送分站池二维码: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(message.AttachmentURLS), "site-pool.png") {
|
||||
t.Fatalf("二维码附件 = %s, want 分站码池", message.AttachmentURLS)
|
||||
}
|
||||
|
||||
var siteQR model.ChatQrCode
|
||||
if err := db.First(&siteQR, pool[1].ID).Error; err != nil {
|
||||
t.Fatalf("查询分站码失败: %v", err)
|
||||
}
|
||||
if siteQR.Status != QrCodeStatusUsed {
|
||||
t.Fatalf("未配置固定码时应消耗分站码池, status = %q", siteQR.Status)
|
||||
}
|
||||
var mainQR model.ChatQrCode
|
||||
if err := db.First(&mainQR, pool[0].ID).Error; err != nil {
|
||||
t.Fatalf("查询主站码失败: %v", err)
|
||||
}
|
||||
if mainQR.Status != QrCodeStatusUnused {
|
||||
t.Fatalf("分站上架不得消耗主站码池, status = %q", mainQR.Status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type SiteDTO struct {
|
||||
AnnouncementsJSON string `json:"announcements_json"`
|
||||
FooterInfoJSON string `json:"footer_info_json"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||
ListingQrcodeURL string `json:"listing_qrcode_url"`
|
||||
TaxEnabled bool `json:"tax_enabled"`
|
||||
TaxRateBPS int64 `json:"tax_rate_bps"`
|
||||
SettlementHoldDays int `json:"settlement_hold_days"`
|
||||
@@ -28,6 +29,7 @@ type CreateSiteRequest struct {
|
||||
Domain string `json:"domain" binding:"required,max=255"`
|
||||
DisplayAccountEnabled *bool `json:"display_account_enabled"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id"`
|
||||
ListingQrcodeURL *string `json:"listing_qrcode_url"`
|
||||
TaxEnabled *bool `json:"tax_enabled"`
|
||||
TaxRateBPS *int64 `json:"tax_rate_bps"`
|
||||
SettlementHoldDays *int `json:"settlement_hold_days"`
|
||||
@@ -39,6 +41,7 @@ type UpdateSiteRequest struct {
|
||||
Status *string `json:"status"`
|
||||
DisplayAccountEnabled *bool `json:"display_account_enabled"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id"`
|
||||
ListingQrcodeURL *string `json:"listing_qrcode_url"`
|
||||
TaxEnabled *bool `json:"tax_enabled"`
|
||||
TaxRateBPS *int64 `json:"tax_rate_bps"`
|
||||
SettlementHoldDays *int `json:"settlement_hold_days"`
|
||||
|
||||
@@ -253,6 +253,9 @@ func (r *Repository) CreateSite(ctx context.Context, req CreateSiteRequest) (*Si
|
||||
if req.DefaultSupportGroupID != nil {
|
||||
row.DefaultSupportGroupID = req.DefaultSupportGroupID
|
||||
}
|
||||
if req.ListingQrcodeURL != nil {
|
||||
row.ListingQrcodeURL = strings.TrimSpace(*req.ListingQrcodeURL)
|
||||
}
|
||||
if req.TaxEnabled != nil {
|
||||
row.TaxEnabled = *req.TaxEnabled
|
||||
}
|
||||
@@ -305,6 +308,9 @@ func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req Upd
|
||||
if req.DefaultSupportGroupID != nil {
|
||||
row.DefaultSupportGroupID = req.DefaultSupportGroupID
|
||||
}
|
||||
if req.ListingQrcodeURL != nil {
|
||||
row.ListingQrcodeURL = strings.TrimSpace(*req.ListingQrcodeURL)
|
||||
}
|
||||
if req.TaxEnabled != nil {
|
||||
row.TaxEnabled = *req.TaxEnabled
|
||||
}
|
||||
@@ -467,6 +473,7 @@ func siteDTO(row model.DistributionSite) SiteDTO {
|
||||
Status: row.Status,
|
||||
DisplayAccountEnabled: row.DisplayAccountEnabled,
|
||||
DefaultSupportGroupID: row.DefaultSupportGroupID,
|
||||
ListingQrcodeURL: row.ListingQrcodeURL,
|
||||
TaxEnabled: row.TaxEnabled,
|
||||
TaxRateBPS: row.TaxRateBPS,
|
||||
SettlementHoldDays: row.SettlementHoldDays,
|
||||
|
||||
@@ -649,6 +649,23 @@ func TestDistributionBrandingAndMarkup(t *testing.T) {
|
||||
t.Fatalf("unexpected branding: %+v", updated)
|
||||
}
|
||||
|
||||
qrURL := "/api/files/object?key=qrcode/brand-site.png"
|
||||
qrUpdated, err := repo.UpdateSite(t.Context(), 0, site.ID, UpdateSiteRequest{ListingQrcodeURL: &qrURL})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSite listing qrcode failed: %v", err)
|
||||
}
|
||||
if qrUpdated.ListingQrcodeURL != qrURL {
|
||||
t.Fatalf("listing_qrcode_url = %q, want %q", qrUpdated.ListingQrcodeURL, qrURL)
|
||||
}
|
||||
cleared := ""
|
||||
qrCleared, err := repo.UpdateSite(t.Context(), 0, site.ID, UpdateSiteRequest{ListingQrcodeURL: &cleared})
|
||||
if err != nil {
|
||||
t.Fatalf("clear listing qrcode failed: %v", err)
|
||||
}
|
||||
if qrCleared.ListingQrcodeURL != "" {
|
||||
t.Fatalf("cleared listing_qrcode_url = %q, want empty", qrCleared.ListingQrcodeURL)
|
||||
}
|
||||
|
||||
// Verify PublicSiteInfo contains branding
|
||||
pubInfo, err := repo.GetPublicSiteInfo(t.Context(), "brand_site", "")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 分销站点发布群固定二维码:号主在该站上传账号时发到对应发布群,可重复使用,不消耗主站二维码池。
|
||||
ALTER TABLE distribution_sites
|
||||
ADD COLUMN listing_qrcode_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '分站发布群固定企微二维码';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE distribution_sites
|
||||
DROP COLUMN listing_qrcode_url;
|
||||
@@ -11,6 +11,7 @@ export interface DistributionSite {
|
||||
tax_enabled: boolean
|
||||
tax_rate_bps: number
|
||||
settlement_hold_days: number
|
||||
listing_qrcode_url?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -150,6 +151,7 @@ export async function updateDistributionSite(
|
||||
| 'tax_enabled'
|
||||
| 'tax_rate_bps'
|
||||
| 'settlement_hold_days'
|
||||
| 'listing_qrcode_url'
|
||||
>
|
||||
>
|
||||
) {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus, Shop, Link, DocumentCopy, Refresh, Clock } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Plus,
|
||||
Shop,
|
||||
Link,
|
||||
DocumentCopy,
|
||||
Refresh,
|
||||
Clock,
|
||||
Picture,
|
||||
Upload,
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchDistributionSites,
|
||||
fetchDistributionSiteDefaults,
|
||||
@@ -12,6 +21,8 @@ import {
|
||||
type OnboardDistributionSitePayload,
|
||||
type OnboardDistributionSiteResult,
|
||||
} from '@/features/admin/api/adminDistribution'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
@@ -34,6 +45,15 @@ const settlementForm = reactive({
|
||||
settlement_hold_days: defaultSettlementHoldDays.value,
|
||||
})
|
||||
|
||||
const qrDialogVisible = ref(false)
|
||||
const qrSubmitting = ref(false)
|
||||
const qrUploading = ref(false)
|
||||
const qrForm = reactive({
|
||||
site_id: 0,
|
||||
site_name: '',
|
||||
listing_qrcode_url: '',
|
||||
})
|
||||
|
||||
function handleOpenSettlement(row: DistributionSite) {
|
||||
settlementForm.site_id = row.id
|
||||
settlementForm.site_name = row.site_name
|
||||
@@ -57,6 +77,51 @@ async function handleSettlementSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenQrcode(row: DistributionSite) {
|
||||
qrForm.site_id = row.id
|
||||
qrForm.site_name = row.site_name
|
||||
qrForm.listing_qrcode_url = row.listing_qrcode_url || ''
|
||||
qrDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function uploadSiteQrcode(file: File) {
|
||||
qrUploading.value = true
|
||||
try {
|
||||
const uploaded = await uploadAdminFile(file, 'qrcode')
|
||||
qrForm.listing_qrcode_url = uploaded.url
|
||||
ElMessage.success('二维码已上传,保存后生效')
|
||||
} catch (err) {
|
||||
ElMessage.error(readError(err, '上传二维码失败'))
|
||||
} finally {
|
||||
qrUploading.value = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function clearSiteQrcode() {
|
||||
qrForm.listing_qrcode_url = ''
|
||||
}
|
||||
|
||||
async function handleQrcodeSubmit() {
|
||||
qrSubmitting.value = true
|
||||
try {
|
||||
await updateDistributionSite(qrForm.site_id, {
|
||||
listing_qrcode_url: qrForm.listing_qrcode_url.trim(),
|
||||
})
|
||||
ElMessage.success(
|
||||
qrForm.listing_qrcode_url.trim()
|
||||
? `【${qrForm.site_name}】发布群固定二维码已更新`
|
||||
: `【${qrForm.site_name}】已清空发布群固定二维码`
|
||||
)
|
||||
qrDialogVisible.value = false
|
||||
await loadSites()
|
||||
} catch (err) {
|
||||
ElMessage.error(readError(err, '更新发布群二维码失败'))
|
||||
} finally {
|
||||
qrSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onboardForm = reactive({
|
||||
site_name: '',
|
||||
site_code: '',
|
||||
@@ -192,7 +257,8 @@ onMounted(() => {
|
||||
<div>
|
||||
<h2 class="page-title">分销站点管理</h2>
|
||||
<p class="page-subtitle">
|
||||
开通分销站、配置结算冷却期。利润按 35% 收号 / 40% 卖出 / 25% 服务费切分。
|
||||
开通分销站、配置结算冷却期与发布群固定二维码。利润按 35% 收号 / 40% 卖出 / 25%
|
||||
服务费切分。
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@@ -248,12 +314,24 @@ onMounted(() => {
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布群二维码" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<AuthImage
|
||||
v-if="row.listing_qrcode_url"
|
||||
:source="row.listing_qrcode_url"
|
||||
fit="cover"
|
||||
:image-style="{ width: '48px', height: '48px', borderRadius: '6px' }"
|
||||
:preview-src-list="[row.listing_qrcode_url]"
|
||||
/>
|
||||
<el-tag v-else type="info" size="small">未配置</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="created_at" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -267,6 +345,9 @@ onMounted(() => {
|
||||
<el-button type="primary" link :icon="Clock" @click="handleOpenSettlement(row)">
|
||||
结算设置
|
||||
</el-button>
|
||||
<el-button type="primary" link :icon="Picture" @click="handleOpenQrcode(row)">
|
||||
二维码
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -440,6 +521,58 @@ onMounted(() => {
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 发布群固定二维码 -->
|
||||
<el-dialog v-model="qrDialogVisible" title="发布群固定二维码" width="520px" destroy-on-close>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="号主在该分站上传账号后,会把这张固定二维码发到对应发布群。同一分站可重复使用,不消耗主站二维码池。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="分销站点">
|
||||
<el-input :model-value="qrForm.site_name" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="固定二维码">
|
||||
<div class="qr-row">
|
||||
<AuthImage
|
||||
v-if="qrForm.listing_qrcode_url"
|
||||
:source="qrForm.listing_qrcode_url"
|
||||
fit="contain"
|
||||
:image-style="{
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
borderRadius: '12px',
|
||||
background: '#f7f9fc',
|
||||
}"
|
||||
:preview-src-list="[qrForm.listing_qrcode_url]"
|
||||
/>
|
||||
<div v-else class="qr-empty">尚未配置</div>
|
||||
<div class="qr-actions">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:disabled="qrUploading"
|
||||
:before-upload="uploadSiteQrcode"
|
||||
>
|
||||
<el-button :icon="Upload" :loading="qrUploading">上传二维码</el-button>
|
||||
</el-upload>
|
||||
<el-button :disabled="!qrForm.listing_qrcode_url" @click="clearSiteQrcode">
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="qrDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="qrSubmitting" @click="handleQrcodeSubmit">
|
||||
保存设置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -508,4 +641,29 @@ export default {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.qr-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.qr-empty {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 12px;
|
||||
background: #f7f9fc;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.qr-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user