@@ -12,6 +12,8 @@ import {
loadCommissionSettings ,
loadCommissionSettlements ,
loadCommissionSummary ,
loadCouponRedemptions ,
loadCouponReport ,
loadCoupons ,
loadCrmConfig ,
loadCrmQueue ,
@@ -20,6 +22,7 @@ import {
updateCommissionSettlementProofStatus ,
updateCommissionSettlementStatus ,
updateMemberCommissionRate ,
upsertCoupon ,
upsertCrmConfig ,
type CodeBatchItem ,
type CommissionOrderItem ,
@@ -28,6 +31,8 @@ import {
type CommissionSettingsItem ,
type CommissionSummaryItem ,
type CouponItem ,
type CouponRedemptionItem ,
type CouponReport ,
type CrmConfigItem ,
type CrmQueueItem ,
type TenantMemberItem ,
@@ -38,6 +43,24 @@ function money(cents: unknown) {
return ` ¥ ${ ( Number ( cents || 0 ) / 100 ) . toFixed ( 2 ) } ` ;
}
function yuanInput ( cents : unknown ) {
if ( cents === undefined || cents === null || cents === '' ) return '' ;
return String ( ( Number ( cents || 0 ) / 100 ) . toFixed ( 2 ) ) . replace ( /\.00$/ , '' ) ;
}
function yuanToCents ( value : string ) {
const parsed = Number ( value || 0 ) ;
return Number . isFinite ( parsed ) ? Math . max ( 0 , Math . round ( parsed * 100 ) ) : 0 ;
}
function csvValues ( value : string ) {
return value . split ( /[\n,, ]/ ) . map ( item = > item . trim ( ) ) . filter ( Boolean ) ;
}
function percentLabel ( value : unknown ) {
return ` ${ ( Number ( value || 0 ) * 100 ) . toFixed ( 1 ) . replace ( /\.0$/ , '' ) } % ` ;
}
function today() {
return new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
}
@@ -67,8 +90,52 @@ function downloadBase64File(filename: string, contentBase64: string, mimeType: s
return true ;
}
interface CouponFormState {
id : string ;
code : string ;
status : 'active' | 'disabled' | 'archived' ;
campaignName : string ;
planId : string ;
discountType : 'fixed' | 'percent' ;
discountValue : string ;
minOrderYuan : string ;
maxDiscountYuan : string ;
perUserLimit : string ;
maxUses : string ;
firstOrderOnly : boolean ;
allowedPlanIdsText : string ;
allowedRegionIdsText : string ;
validFrom : string ;
validTo : string ;
remark : string ;
}
function defaultCouponForm ( ) : CouponFormState {
return {
id : '' ,
code : '' ,
status : 'active' ,
campaignName : '' ,
planId : '' ,
discountType : 'fixed' ,
discountValue : '' ,
minOrderYuan : '0' ,
maxDiscountYuan : '' ,
perUserLimit : '1' ,
maxUses : '' ,
firstOrderOnly : false ,
allowedPlanIdsText : '' ,
allowedRegionIdsText : '' ,
validFrom : '' ,
validTo : '' ,
remark : '' ,
} ;
}
export default function TenantMarketingPage() {
const [ coupons , setCoupons ] = useState < CouponItem [ ] > ( [ ] ) ;
const [ couponReport , setCouponReport ] = useState < CouponReport | null > ( null ) ;
const [ couponRedemptions , setCouponRedemptions ] = useState < CouponRedemptionItem [ ] > ( [ ] ) ;
const [ batches , setBatches ] = useState < CodeBatchItem [ ] > ( [ ] ) ;
const [ codes , setCodes ] = useState < Record < string , unknown > [ ] > ( [ ] ) ;
const [ crmConfig , setCrmConfig ] = useState < CrmConfigItem | null > ( null ) ;
@@ -90,6 +157,15 @@ export default function TenantMarketingPage() {
assignmentMode : 'none' as 'none' | 'direct' | 'round_robin' | 'referrer' ,
assignmentPoolText : '' ,
} ) ;
const [ couponForm , setCouponForm ] = useState < CouponFormState > ( ( ) = > defaultCouponForm ( ) ) ;
const [ couponFilter , setCouponFilter ] = useState ( {
status : '' ,
campaignName : '' ,
selectedCouponId : '' ,
redemptionStatus : 'used' as 'claimed' | 'pending' | 'used' | 'cancelled' | 'expired' ,
startDate : monthStart ( ) ,
endDate : today ( ) ,
} ) ;
const [ commissionForm , setCommissionForm ] = useState ( { defaultRatePercent : '20' , minSettlementYuan : '0' , settlementCycle : 'monthly' } ) ;
const [ period , setPeriod ] = useState ( { startDate : monthStart ( ) , endDate : today ( ) , referrerUserId : '' } ) ;
const [ memberRate , setMemberRate ] = useState ( { userId : '' , ratePercent : '' } ) ;
@@ -107,8 +183,32 @@ export default function TenantMarketingPage() {
async function reloadAll() {
setError ( '' ) ;
try {
const [ couponPayload , batchPayload , codePayload , crmConfigPayload , crmPayload , commissionSettingsPayload , commissionPayload , orderPayload , settlementPayload , memberPayload ] = await Promise . all ( [
loadCoupons ( ) . catch ( ( ) = > ( { items : [ ] } ) ) ,
const [
couponPayload ,
couponReportPayload ,
couponRedemptionPayload ,
batchPayload ,
codePayload ,
crmConfigPayload ,
crmPayload ,
commissionSettingsPayload ,
commissionPayload ,
orderPayload ,
settlementPayload ,
memberPayload ,
] = await Promise . all ( [
loadCoupons ( { status : couponFilter.status || undefined , campaignName : couponFilter.campaignName || undefined } ) . catch ( ( ) = > ( { items : [ ] } ) ) ,
loadCouponReport ( {
startDate : couponFilter.startDate ,
endDate : couponFilter.endDate ,
couponId : couponFilter.selectedCouponId || undefined ,
campaignName : couponFilter.campaignName || undefined ,
} ) . catch ( ( ) = > ( { item : null } ) ) ,
loadCouponRedemptions ( {
couponId : couponFilter.selectedCouponId || undefined ,
status : couponFilter.redemptionStatus ,
limit : 20 ,
} ) . catch ( ( ) = > ( { items : [ ] } ) ) ,
loadCodeBatches ( ) . catch ( ( ) = > ( { items : [ ] } ) ) ,
loadActivationCodes ( 20 ) . catch ( ( ) = > ( { items : [ ] } ) ) ,
loadCrmConfig ( ) . catch ( ( ) = > ( { item : null } ) ) ,
@@ -122,6 +222,8 @@ export default function TenantMarketingPage() {
const nextCrm = crmConfigPayload . item || null ;
const nextSettings = commissionSettingsPayload . item || null ;
setCoupons ( couponPayload . items || [ ] ) ;
setCouponReport ( couponReportPayload . item || null ) ;
setCouponRedemptions ( couponRedemptionPayload . items || [ ] ) ;
setBatches ( batchPayload . items || [ ] ) ;
setCodes ( codePayload . items || [ ] ) ;
setCrmConfig ( nextCrm ) ;
@@ -160,6 +262,106 @@ export default function TenantMarketingPage() {
void reloadAll ( ) ;
} , [ ] ) ;
async function reloadCoupons ( override : Partial < typeof couponFilter > = { } ) {
const nextFilter = { . . . couponFilter , . . . override } ;
setBusy ( 'coupons' ) ;
setError ( '' ) ;
try {
const [ couponPayload , reportPayload , redemptionPayload ] = await Promise . all ( [
loadCoupons ( { status : nextFilter.status || undefined , campaignName : nextFilter.campaignName || undefined } ) ,
loadCouponReport ( {
startDate : nextFilter.startDate ,
endDate : nextFilter.endDate ,
couponId : nextFilter.selectedCouponId || undefined ,
campaignName : nextFilter.campaignName || undefined ,
} ) . catch ( ( ) = > ( { item : null } ) ) ,
loadCouponRedemptions ( {
couponId : nextFilter.selectedCouponId || undefined ,
status : nextFilter.redemptionStatus ,
limit : 30 ,
} ) . catch ( ( ) = > ( { items : [ ] } ) ) ,
] ) ;
setCoupons ( couponPayload . items || [ ] ) ;
setCouponReport ( reportPayload . item || null ) ;
setCouponRedemptions ( redemptionPayload . items || [ ] ) ;
} catch ( nextError ) {
setError ( nextError instanceof Error ? nextError . message : '优惠券数据加载失败' ) ;
} finally {
setBusy ( '' ) ;
}
}
function editCoupon ( item : CouponItem ) {
setCouponForm ( {
id : item.id ,
code : item.code ,
status : item.status === 'disabled' || item . status === 'archived' ? item . status : 'active' ,
campaignName : item.campaignName || '' ,
planId : item.planId || '' ,
discountType : item.discountType === 'percent' ? 'percent' : 'fixed' ,
discountValue : item.discountType === 'percent' ? String ( item . discountValue || '' ) : yuanInput ( item . discountValue ) ,
minOrderYuan : yuanInput ( item . minOrderAmountCents ) ,
maxDiscountYuan : yuanInput ( item . maxDiscountCents ) ,
perUserLimit : String ( item . perUserLimit || 1 ) ,
maxUses : item.maxUses === null || item . maxUses === undefined ? '' : String ( item . maxUses ) ,
firstOrderOnly : item.firstOrderOnly === true ,
allowedPlanIdsText : ( item . allowedPlanIds || [ ] ) . join ( ',' ) ,
allowedRegionIdsText : ( item . allowedRegionIds || [ ] ) . join ( ',' ) ,
validFrom : item.validFrom ? item . validFrom . slice ( 0 , 10 ) : '' ,
validTo : item.validTo ? item . validTo . slice ( 0 , 10 ) : '' ,
remark : item.remark || '' ,
} ) ;
setCouponFilter ( prev = > ( { . . . prev , selectedCouponId : item.id , campaignName : item.campaignName || prev . campaignName } ) ) ;
}
async function saveCoupon() {
if ( ! couponForm . code . trim ( ) ) {
Taro . showToast ( { title : '请填写优惠券码' , icon : 'none' } ) ;
return ;
}
setBusy ( 'coupon-save' ) ;
setError ( '' ) ;
try {
const discountValue = couponForm . discountType === 'fixed'
? yuanToCents ( couponForm . discountValue )
: Math . max ( 0 , Number ( couponForm . discountValue || 0 ) ) ;
const result = await upsertCoupon ( {
id : couponForm.id || undefined ,
code : couponForm.code.trim ( ) . toUpperCase ( ) ,
status : couponForm.status ,
campaignName : couponForm.campaignName.trim ( ) || null ,
planId : couponForm.planId.trim ( ) || null ,
discountType : couponForm.discountType ,
discountValue ,
minOrderAmountCents : yuanToCents ( couponForm . minOrderYuan ) ,
maxDiscountCents : couponForm.maxDiscountYuan.trim ( ) ? yuanToCents ( couponForm . maxDiscountYuan ) : null ,
perUserLimit : Math.max ( 1 , Math . min ( 100 , Math . trunc ( Number ( couponForm . perUserLimit || 1 ) ) ) ) ,
maxUses : couponForm.maxUses.trim ( ) ? Math . max ( 0 , Math . trunc ( Number ( couponForm . maxUses || 0 ) ) ) : null ,
firstOrderOnly : couponForm.firstOrderOnly ,
allowedPlanIds : csvValues ( couponForm . allowedPlanIdsText ) ,
allowedRegionIds : csvValues ( couponForm . allowedRegionIdsText ) ,
validFrom : couponForm.validFrom || null ,
validTo : couponForm.validTo || null ,
remark : couponForm.remark.trim ( ) || null ,
source : 'taro-tenant-admin' ,
metadata : { source : 'taro-tenant-admin' } ,
} ) ;
if ( result . item ? . id ) {
setCouponFilter ( prev = > ( {
. . . prev ,
selectedCouponId : result.item?.id || prev . selectedCouponId ,
campaignName : result.item?.campaignName || prev . campaignName ,
} ) ) ;
}
Taro . showToast ( { title : '优惠券已保存' , icon : 'success' } ) ;
await reloadCoupons ( ) ;
} catch ( nextError ) {
setError ( nextError instanceof Error ? nextError . message : '优惠券保存失败' ) ;
} finally {
setBusy ( '' ) ;
}
}
async function saveCrmConfig() {
setBusy ( 'crm' ) ;
setError ( '' ) ;
@@ -402,6 +604,69 @@ export default function TenantMarketingPage() {
< View className = 'admin-metric' > < Text className = 'admin-metric-label' > 预 计 佣 金 < / Text > < Text className = 'admin-metric-value' > { money ( commission ? . commissionAmountCents ) } < / Text > < / View >
< / View >
< View className = 'admin-section' >
< Text className = 'admin-section-title' > 优 惠 券 规 则 < / Text >
< View className = 'admin-form-grid' >
< Input className = 'admin-input' placeholder = '优惠券码,例如 SUMMER80' value = { couponForm . code } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , code : String ( event . detail . value || '' ) . toUpperCase ( ) } ) ) } / >
< Input className = 'admin-input' placeholder = '活动分组,例如 暑期活动' value = { couponForm . campaignName } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , campaignName : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '默认套餐 ID, 可留空' value = { couponForm . planId } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , planId : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' type = 'digit' placeholder = { couponForm . discountType === 'fixed' ? '优惠金额,单位元' : '优惠百分比,例如 80 表示 80%' } value = { couponForm . discountValue } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , discountValue : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' type = 'digit' placeholder = '最低订单金额,单位元' value = { couponForm . minOrderYuan } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , minOrderYuan : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' type = 'digit' placeholder = '最高优惠封顶,单位元,留空不限' value = { couponForm . maxDiscountYuan } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , maxDiscountYuan : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' type = 'number' placeholder = '单用户可用次数' value = { couponForm . perUserLimit } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , perUserLimit : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' type = 'number' placeholder = '总可用次数,留空不限' value = { couponForm . maxUses } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , maxUses : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '适用套餐 ID, 多个用逗号分隔' value = { couponForm . allowedPlanIdsText } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , allowedPlanIdsText : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '适用地区 ID, 多个用逗号分隔' value = { couponForm . allowedRegionIdsText } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , allowedRegionIdsText : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '开始日期 YYYY-MM-DD, 可留空' value = { couponForm . validFrom } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , validFrom : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '结束日期 YYYY-MM-DD, 可留空' value = { couponForm . validTo } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , validTo : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '内部备注' value = { couponForm . remark } onInput = { event = > setCouponForm ( prev = > ( { . . . prev , remark : String ( event . detail . value || '' ) } ) ) } / >
< / View >
< View className = 'admin-actions compact' >
{ ( [ 'fixed' , 'percent' ] as const ) . map ( type = > (
< Button key = { type } className = { ` admin-button ${ couponForm . discountType === type ? 'active' : '' } ` } onClick = { ( ) = > setCouponForm ( prev = > ( { . . . prev , discountType : type , discountValue : '' } ) ) } > { type === 'fixed' ? '固定金额' : '百分比' } < / Button >
) ) }
{ ( [ 'active' , 'disabled' , 'archived' ] as const ) . map ( status = > (
< Button key = { status } className = { ` admin-button ${ couponForm . status === status ? 'active' : '' } ` } onClick = { ( ) = > setCouponForm ( prev = > ( { . . . prev , status } ) ) } > { status === 'active' ? '启用' : status === 'disabled' ? '停用' : '归档' } < / Button >
) ) }
< Button className = { ` admin-button ${ couponForm . firstOrderOnly ? 'active' : '' } ` } onClick = { ( ) = > setCouponForm ( prev = > ( { . . . prev , firstOrderOnly : ! prev . firstOrderOnly } ) ) } > { couponForm . firstOrderOnly ? '仅首单' : '不限首单' } < / Button >
< Button className = 'admin-button primary' loading = { busy === 'coupon-save' } onClick = { saveCoupon } > 保 存 优 惠 券 < / Button >
< Button className = 'admin-button' onClick = { ( ) = > setCouponForm ( defaultCouponForm ( ) ) } > 新 建 空 表 单 < / Button >
< / View >
< / View >
< View className = 'admin-section' >
< Text className = 'admin-section-title' > 优 惠 券 核 销 报 表 < / Text >
< View className = 'admin-form-grid' >
< Input className = 'admin-input' placeholder = '报表开始日期 YYYY-MM-DD' value = { couponFilter . startDate } onInput = { event = > setCouponFilter ( prev = > ( { . . . prev , startDate : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '报表结束日期 YYYY-MM-DD' value = { couponFilter . endDate } onInput = { event = > setCouponFilter ( prev = > ( { . . . prev , endDate : String ( event . detail . value || '' ) } ) ) } / >
< Input className = 'admin-input' placeholder = '活动分组筛选' value = { couponFilter . campaignName } onInput = { event = > setCouponFilter ( prev = > ( { . . . prev , campaignName : String ( event . detail . value || '' ) } ) ) } / >
< / View >
< View className = 'admin-actions compact' >
< Button className = { ` admin-button ${ couponFilter . status ? '' : 'active' } ` } onClick = { ( ) = > setCouponFilter ( prev = > ( { . . . prev , status : '' } ) ) } > 全 部 券 < / Button >
{ [ 'active' , 'disabled' , 'archived' ] . map ( status = > (
< Button key = { status } className = { ` admin-button ${ couponFilter . status === status ? 'active' : '' } ` } onClick = { ( ) = > setCouponFilter ( prev = > ( { . . . prev , status } ) ) } > { status } < / Button >
) ) }
{ ( [ 'claimed' , 'pending' , 'used' , 'cancelled' , 'expired' ] as const ) . map ( status = > (
< Button key = { status } className = { ` admin-button ${ couponFilter . redemptionStatus === status ? 'active' : '' } ` } onClick = { ( ) = > setCouponFilter ( prev = > ( { . . . prev , redemptionStatus : status } ) ) } > { status } < / Button >
) ) }
< Button className = 'admin-button primary' loading = { busy === 'coupons' } onClick = { ( ) = > reloadCoupons ( ) } > 刷 新 报 表 < / Button >
< / View >
< View className = 'admin-grid' >
< View className = 'admin-metric' > < Text className = 'admin-metric-label' > 领 取 数 < / Text > < Text className = 'admin-metric-value' > { String ( couponReport ? . claimCount || 0 ) } < / Text > < / View >
< View className = 'admin-metric' > < Text className = 'admin-metric-label' > 核 销 数 < / Text > < Text className = 'admin-metric-value' > { String ( couponReport ? . usedCount || 0 ) } < / Text > < / View >
< View className = 'admin-metric' > < Text className = 'admin-metric-label' > 抵 扣 金 额 < / Text > < Text className = 'admin-metric-value' > { money ( couponReport ? . discountCents ) } < / Text > < / View >
< View className = 'admin-metric' > < Text className = 'admin-metric-label' > 转 化 率 < / Text > < Text className = 'admin-metric-value' > { percentLabel ( couponReport ? . conversionRate ) } < / Text > < / View >
< / View >
< View className = 'admin-list compact' >
{ ( couponReport ? . byCampaign || [ ] ) . slice ( 0 , 5 ) . map ( ( item , index ) = > (
< View className = 'admin-row' key = { ` ${ String ( item . campaignName || 'campaign' ) } : ${ index } ` } >
< Text className = 'admin-row-main' > { String ( item . campaignName || '未分组' ) } < / Text >
< Text className = 'admin-row-meta' > 领 取 { String ( item . claimCount || 0 ) } · 核 销 { String ( item . usedCount || 0 ) } · 抵 扣 { money ( item . discountCents ) } < / Text >
< / View >
) ) }
< / View >
< / View >
< View className = 'admin-section' >
< Text className = 'admin-section-title' > CRM 配 置 < / Text >
< View className = 'admin-form-grid' >
@@ -592,13 +857,48 @@ export default function TenantMarketingPage() {
{ coupons . map ( item = > (
< View className = 'admin-row' key = { item . id } >
< Text className = 'admin-row-main' > { item . code } < / Text >
< Text className = 'admin-row-meta' > { item . status || 'active' } · { item . discountType || 'discount' } { String ( item . discountValue ? ? '' ) } · 已 用 { item . usedCount || 0 } / { item . maxUses || '不限' } < / Text >
< Text className = 'admin-row-meta' >
{ item . status || 'active' } · { item . campaignName || '未分组' } · { item . discountType === 'fixed' ? money ( item . discountValue ) : ` ${ String ( item . discountValue ? ? 0 ) } % ` }
< / Text >
< Text className = 'admin-row-meta' >
已 用 { item . usedCount || 0 } / { item . maxUses || '不限' } · 满 { money ( item . minOrderAmountCents ) } 可 用 · 封 顶 { item . maxDiscountCents === null || item . maxDiscountCents === undefined ? '不限' : money ( item . maxDiscountCents ) } · 每 人 { item . perUserLimit || 1 } 次
< / Text >
< Text className = 'admin-row-meta break-line' >
套 餐 { item . allowedPlanIds ? . length ? item . allowedPlanIds . join ( ', ' ) : item . planId || '不限' } · 地 区 { item . allowedRegionIds ? . length ? item . allowedRegionIds . join ( ', ' ) : '不限' } { item . firstOrderOnly ? '· 仅首单' : '' }
< / Text >
< View className = 'admin-row-actions' >
< Button className = 'admin-mini-button primary' onClick = { ( ) = > editCoupon ( item ) } > 编 辑 规 则 < / Button >
< Button
className = { ` admin-mini-button ${ couponFilter . selectedCouponId === item . id ? 'primary' : '' } ` }
onClick = { ( ) = > {
const override = { selectedCouponId : item.id , campaignName : item.campaignName || couponFilter . campaignName } ;
setCouponFilter ( prev = > ( { . . . prev , . . . override } ) ) ;
void reloadCoupons ( override ) ;
} }
>
查 看 核 销
< / Button >
< / View >
< / View >
) ) }
< / View >
{ ! coupons . length ? < View className = 'admin-empty' > 暂 无 优 惠 券 。 < / View > : null }
< / View >
< View className = 'admin-section' >
< Text className = 'admin-section-title' > 核 销 明 细 < / Text >
< View className = 'admin-list' >
{ couponRedemptions . map ( item = > (
< View className = 'admin-row' key = { item . id } >
< Text className = 'admin-row-main' > { item . couponCode || '优惠券' } · { item . status || 'claimed' } · { money ( item . discountAppliedCents ) } < / Text >
< Text className = 'admin-row-meta' > { item . userName || item . userPhone || item . userId || '学生' } · { item . planName || item . planId || '套餐' } · { item . regionName || item . regionId || '未指定地区' } < / Text >
< Text className = 'admin-row-meta' > { item . orderNo || '未绑定订单' } · 领 取 { item . claimedAt || item . createdAt || '-' } · 核 销 { item . usedAt || '-' } < / Text >
< / View >
) ) }
< / View >
{ ! couponRedemptions . length ? < View className = 'admin-empty' > 当 前 筛 选 下 暂 无 核 销 明 细 , 或 当 前 角 色 没 有 ` coupons:redemptions:read ` 权 限 。 < / View > : null }
< / View >
< View className = 'admin-section' >
< Text className = 'admin-section-title' > 激 活 码 批 次 < / Text >
< View className = 'admin-list' >