feat: 将推广费页签迁移到营销账户账单

This commit is contained in:
qiuxiang 2026-10-28 11:09:46 +08:00
parent 8c38175318
commit ad9d09aafe
9 changed files with 165 additions and 38 deletions

View File

@ -914,7 +914,7 @@ class Pages extends Controller
* 支持的营销账户平台列表
*/
protected const MARKETING_PLATFORMS = [
'taobao-alipay', 'taobao-wechat', 'pinduoduo',
'promotion-marketing', 'taobao-alipay', 'taobao-wechat', 'pinduoduo',
'jd-wallet', 'douyin', 'dewu', 'vipshop',
'jd-promotion', 'douyin-promotion', 'dewu-promotion', 'vip-promotion',
];
@ -9341,6 +9341,11 @@ class Pages extends Controller
return null;
}
// 推广费页面由 pages.js 挂载共享 AccountBills 组件,服务端不重复查询。
if ($tab === 'promotion-marketing') {
return null;
}
if (in_array($tab, self::PROMOTION_MARKETING_TABS, true)) {
return app(PlatformPromotionAccountPageData::class)->content($tab);
}

View File

@ -554,13 +554,13 @@ $navigation = [
['slug' => 'dewu', 'title' => '得物账户'],
['slug' => 'vipshop', 'title' => '唯品会MP账户'],
['slug' => 'vipshop-jx', 'title' => '唯品会JX账户'],
['slug' => 'promotion-marketing', 'title' => '推广费(营销账户)'],
],
],
[
'slug' => 'marketing-account-bills',
'title' => '店铺营销账户账单',
'tabs' => [
['slug' => 'promotion-marketing', 'title' => '推广费(营销账户)'],
['slug' => 'taobao-alipay', 'title' => '淘系支付宝账户'],
['slug' => 'taobao-wechat', 'title' => '淘系微信账户'],
['slug' => 'pinduoduo', 'title' => '拼多多账户'],

View File

@ -1,6 +1,12 @@
<template>
<div class="table-workspace">
<div class="tw-stats" :class="{ 'tw-stats--promotion-only': isPromotionMarketing }">
<div class="table-workspace" :class="{ 'table-workspace--compact': compact }">
<div
class="tw-stats"
:class="{
'tw-stats--promotion-only': isPromotionMarketing && !compact,
'tw-stats--promotion-compact': compact,
}"
>
<div
v-for="stat in stats"
:key="stat.label"
@ -15,6 +21,7 @@
type="button"
class="tw-promotion-link"
aria-label="查看店铺明细"
:disabled="compact && (loadError || loading)"
@click="openPromotionDetails"
>查看明细 →</button>
</div>
@ -133,7 +140,7 @@
</div>
</div>
</div>
<form class="tw-filter" @submit.prevent="applyFilters">
<form v-if="!compact" class="tw-filter" @submit.prevent="applyFilters">
<label>店铺
<el-select v-if="usesMirrorOptions && !filterOptionsError" v-model="filters.store_name" filterable clearable :loading="filterOptionsLoading" placeholder="全部店铺" aria-label="店铺筛选" popper-class="account-bill-filter-options" no-match-text="没有匹配的店铺" no-data-text="暂无店铺" @change="applyFilters">
<el-option v-for="shop in filterOptions.shops" :key="shop" :label="shop" :value="shop" />
@ -174,7 +181,7 @@
<p v-if="filterOptionsError" class="tw-filter-error" role="alert">筛选选项加载失败,可输入名称查询。<button type="button" class="tw-button" @click="fetchFilterOptions">重试选项</button></p>
<p v-if="filtersDirty && !loading" class="tw-note" role="status">筛选条件已修改,查询后可导出对应结果。</p>
</form>
<section class="tw-panel" :aria-label="`${columnsTitle || '账户'}账单明细`">
<section v-if="!compact" class="tw-panel" :aria-label="`${columnsTitle || '账户'}账单明细`">
<div v-if="isJdBillTab && sync" class="tw-note">来源:{{ sync.source }} · 已接入月份:{{ (sync.periods || []).join('、') }} · 导入时间:{{ sync.last_succeeded_at }}</div>
<div v-if="loadError" class="tw-alert" role="alert"><span>账单加载失败,请重试</span><button type="button" @click="fetch">重新加载</button></div>
<div class="tw-toolbar">
@ -185,7 +192,7 @@
<button type="button" class="tw-button" :disabled="!canExport || exporting" @click="exportRows"><span class="material-icons-outlined" aria-hidden="true">download</span>{{ exporting ? '提交中…' : (exportMode === 'page-visible' ? '导出当前页' : '导出全部筛选结果') }}</button>
<button type="button" class="tw-button" @click="openExportModal"><span class="material-icons-outlined" aria-hidden="true">assignment</span>导出进度</button>
</template>
<table-column-settings :columns="sourceColumns" :user-key="userKey" page-key="store-receiving-bills" :tab-key="billType" table-key="bill-details" @change="configuredColumns = $event" />
<table-column-settings :columns="sourceColumns" :user-key="userKey" :page-key="pageKey" :tab-key="billType" table-key="bill-details" @change="configuredColumns = $event" />
</div>
</div>
@ -262,11 +269,13 @@ const DATE_FILTER_LABELS = {'jd-transactions':'费用结算时间','jingxi':'费
const dateConfig = {dateFormat:'Y-m-d',locale:Mandarin,disableMobile:true,allowInput:false,onReady:(_,value,picker)=>picker.calendarContainer.classList.add('account-bill-calendar')};
// 默认账期=本月:不填日期时查全表(几百万行)会很慢,默认落到当月既快又符合日常用法。
const currentMonthRange = () => { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const pad = value => String(value).padStart(2, '0'); const lastDay = new Date(year, month, 0).getDate(); return { start: `${year}-${pad(month)}-01`, end: `${year}-${pad(month)}-${pad(lastDay)}` }; };
const emptyFilters = billType => ({store_name:'',ledger_remark:'',bill_item:'',...currentMonthRange(),keyword:'',keyword_match:'contains'});
const filtersForRange=(start,end)=>{const range=currentMonthRange();return {store_name:'',ledger_remark:'',bill_item:'',start:start||range.start,end:end||range.end,keyword:'',keyword_match:'contains'};};
export default {
name:'AccountBills', components:{ TableColumnSettings, flatPicker, [Select.name]:Select, [Option.name]:Option }, props:{ apiBase:{type:String,required:true}, billType:{type:String,required:true}, progressBase:{type:String,default:''}, userKey:{type:[String,Number],required:true} },
data(){return{loading:false,loadError:false,sync:null,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:emptyFilters(this.billType),queryFilters:emptyFilters(this.billType),resultFilters:null,filterOptions:{shops:[],bill_items:[],ledger_remarks:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,promotionRequestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible',exporting:false,exportModalOpen:false,exportTasks:[],exportHint:'导出在后台生成,成功后点「下载」;期间可以继续查看其它页面。',exportTimer:null,promotionModalOpen:false,promotionExpenseTab:'platform',promotionExpenseLoading:false,promotionExpenseError:false,promotionStoreSummary:[],promotionExpenseSummary:{expense:0,count:0},promotionExpenseMessage:'',promotionPlatformFilter:'',promotionStoreFilter:'',promotionNoteFilter:'',promotionNoteTimer:null};},
data(){return{loading:false,loadError:false,sync:null,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:emptyFilters(this.billType),queryFilters:emptyFilters(this.billType),resultFilters:null,filterOptions:{shops:[],bill_items:[],ledger_remarks:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,promotionRequestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible',exporting:false,exportModalOpen:false,exportTasks:[],exportHint:'导出在后台生成,成功后点「下载」;期间可以继续查看其它页面。',exportTimer:null,promotionModalOpen:false,promotionExpenseTab:'platform',promotionExpenseLoading:false,promotionExpenseError:false,promotionStoreSummary:[],promotionExpenseSummary:{expense:0,count:0},promotionExpenseMessage:'',promotionPlatformFilter:'',promotionStoreFilter:''};},
name:'AccountBills', components:{ TableColumnSettings, flatPicker, [Select.name]:Select, [Option.name]:Option }, props:{ apiBase:{type:String,required:true}, billType:{type:String,required:true}, progressBase:{type:String,default:''}, userKey:{type:[String,Number],required:true}, pageKey:{type:String,default:'store-receiving-bills'}, compact:{type:Boolean,default:false}, initialStart:{type:String,default:''}, initialEnd:{type:String,default:''} },
data(){
const filters=filtersForRange(this.initialStart,this.initialEnd);
return{loading:false,loadError:false,sync:null,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:this.compact?1:20,filters,queryFilters:{...filters},resultFilters:null,filterOptions:{shops:[],bill_items:[],ledger_remarks:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,promotionRequestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible',exporting:false,exportModalOpen:false,exportTasks:[],exportHint:'导出在后台生成,成功后点「下载」;期间可以继续查看其它页面。',exportTimer:null,promotionModalOpen:false,promotionExpenseTab:'platform',promotionExpenseLoading:false,promotionExpenseError:false,promotionStoreSummary:[],promotionExpenseSummary:{expense:0,count:0},promotionExpenseMessage:'',promotionPlatformFilter:'',promotionStoreFilter:'',promotionNoteFilter:'',promotionNoteTimer:null};
},
computed:{
isDouyin(){return this.billType==='douyin';},
isPromotionMarketing(){return this.billType==='promotion-marketing';},
@ -289,7 +298,7 @@ export default {
visibleGroups(){return this.visibleColumns.reduce((groups,column)=>{if(!column.group)return groups;const last=groups[groups.length-1];if(last&&last.label===column.group)last.span+=1;else groups.push({label:column.group,span:1});return groups;},[]);},
stats(){
if(this.isPromotionMarketing){
return [{label:'推广费(营销账户)',value:this.moneyWan(this.promotionExpenseSummary.expense),note:`${this.promotionExpenseSummary.count} 笔支出 · 按当前筛选`,className:'is-expense',action:true}];
return [{label:'推广费(营销账户)',value:this.loadError?'—':this.moneyWan(this.promotionExpenseSummary.expense),note:this.loadError?'加载失败,请刷新页面重试':`${this.promotionExpenseSummary.count} 笔支出 · 按当前筛选`,className:'is-expense',action:true}];
}
const netLabel=JD_BILL_TABS.concat(['vipshop-jx']).indexOf(this.billType)!==-1;
const items=[
@ -307,13 +316,13 @@ export default {
promotionFilteredExpenseTotal(){return this.promotionFilteredStoreRows.reduce((sum,row)=>sum+Number(row.amount||0),0);},
rangeStart(){return this.total===0?0:(this.currentPage-1)*this.perPage+1;},rangeEnd(){return Math.min(this.currentPage*this.perPage,this.total);}
},
mounted(){if(this.usesMirrorOptions)this.fetchFilterOptions();this.fetch();},
mounted(){if(this.usesMirrorOptions&&!this.compact)this.fetchFilterOptions();this.fetch();},
beforeDestroy(){if(this.promotionNoteTimer)window.clearTimeout(this.promotionNoteTimer);},
methods:{
async fetchFilterOptions(){this.filterOptionsLoading=true;this.filterOptionsError=false;try{const response=await window.axios.get(`${this.apiBase}/filter-options`,{params:{start:this.filters.start,end:this.filters.end}});const body=response.data||{};this.filterOptions={shops:Array.isArray(body.shops)?body.shops:[],bill_items:Array.isArray(body.bill_items)?body.bill_items:[],ledger_remarks:Array.isArray(body.ledger_remarks)?body.ledger_remarks:[]};}catch(error){this.filterOptionsError=true;}finally{this.filterOptionsLoading=false;}},
async fetch(){const requestId=++this.requestSerial;const requestedFilters={...this.queryFilters};this.loading=true;this.loadError=false;try{const response=await window.axios.get(this.apiBase,{params:{...requestedFilters,sort:this.sort.key,dir:this.sort.dir,limit:this.perPage,page:this.currentPage}});if(requestId!==this.requestSerial)return;const body=response.data||{};this.resultFilters=requestedFilters;this.sync=body.sync||null;this.rows=body.data||[];this.total=body.total||0;this.currentPage=body.current_page||1;this.lastPage=body.last_page||1;this.perPage=body.per_page||this.perPage;this.summary=body.summary||{income:0,expense:0,balance:0,count:0};if(this.isPromotionMarketing){this.promotionExpenseSummary={expense:Number(this.summary.expense||0),count:Number(this.summary.count||0)};}if(body.columns){this.columnsTitle=body.columns.title||'';this.sourceGroups=body.columns.groups||[];this.sourceColumns=this.attachGroups(body.columns.columns||body.columns||[],this.sourceGroups);}}catch(error){if(requestId!==this.requestSerial)return;this.rows=[];this.total=0;this.sync=null;this.summary={income:0,expense:0,balance:0,count:0};this.loadError=true;}finally{if(requestId===this.requestSerial)this.loading=false;}},
attachGroups(columns,groups){let index=0;return columns.map((column,columnIndex)=>{while(groups[index]&&columnIndex>=groups.slice(0,index+1).reduce((sum,group)=>sum+Number(group.span||0),0))index+=1;return{...column,key:column.field||`column-${columnIndex}`,group:groups[index]?groups[index].label:''};});},
applyFilters(){if(this.invalidDateRange)return;this.queryFilters={...this.filters};this.currentPage=1;return this.fetch();},resetFilters(){this.filters=emptyFilters(this.billType);return this.applyFilters();},changePage(page){this.currentPage=page;this.fetch();this.$nextTick(()=>{if(this.$refs.scroll)this.$refs.scroll.scrollTop=0;});},
applyFilters(){if(this.invalidDateRange)return;this.queryFilters={...this.filters};this.currentPage=1;return this.fetch();},resetFilters(){this.filters=filtersForRange();return this.applyFilters();},changePage(page){this.currentPage=page;this.fetch();this.$nextTick(()=>{if(this.$refs.scroll)this.$refs.scroll.scrollTop=0;});},
openDatePicker(refName){const picker=this.$refs[refName];if(picker&&picker.fp)picker.fp.open();},
async fetchPromotionExpenseDetails(filters=null){
const requestId=++this.promotionRequestSerial;
@ -326,15 +335,10 @@ export default {
if(requestId!==this.promotionRequestSerial)return;
const body=response.data||{};
this.promotionStoreSummary=Array.isArray(body.store_summary)?body.store_summary:[];
this.promotionExpenseSummary={
expense:Number((body.summary||{}).expense||0),
count:Number((body.summary||{}).count||0),
};
this.promotionExpenseMessage=body.message||'';
}catch(error){
if(requestId!==this.promotionRequestSerial)return;
this.promotionStoreSummary=[];
this.promotionExpenseSummary={expense:0,count:0};
this.promotionExpenseError=true;
}finally{
if(requestId===this.promotionRequestSerial)this.promotionExpenseLoading=false;
@ -525,9 +529,15 @@ export default {
<style scoped>
.tw-stats--promotion-only{grid-template-columns:minmax(0,1fr)}
.table-workspace--compact,.tw-stats--promotion-compact{display:contents}
.tw-stats--promotion-compact .tw-stat{padding:.75rem 1rem;border-color:#e5e7eb;border-radius:.5rem}
.tw-stats--promotion-compact .tw-label{color:#6b7280;font-size:.75rem;font-weight:500}
.tw-stats--promotion-compact .tw-value{margin-top:.25rem;font-size:1.125rem;font-weight:600}
.tw-stats--promotion-compact .tw-note{margin-top:.125rem;font-size:.625rem}
.tw-stat--promotion{display:flex;min-height:5.25rem;flex-direction:column}
.tw-promotion-link{margin-top:auto;align-self:flex-end;padding:0;border:0;background:transparent;color:#856632;font:500 .75rem/1.5 inherit;text-decoration:underline;text-underline-offset:2px;cursor:pointer}
.tw-promotion-link:hover{color:#6a5129}
.tw-promotion-link:disabled{cursor:not-allowed;opacity:.5}
.tw-promotion-link:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(133,102,50,.2);border-radius:.25rem}
.tw-promotion-modal{width:min(980px,100%)}
.tw-promotion-modal .tw-modal__head{gap:1rem}

View File

@ -8,6 +8,8 @@
* - taobao-wechat / pinduoduo / jd-wallet / jd-transactions / jingxi / dewu / vipshop / vipshop-jx / promotion-marketing
* → 通用实现 AccountBills(表 ec_account_bills,按 bill_type 区分)
* - douyin → AccountBills 展示共享原始账单镜像中的头条放心购渠道
* 店铺营销账户账单(marketing-account-bills)的推广费页签复用 AccountBills;
* 淘系支付宝页签同时挂载其紧凑汇总卡和同一个店铺明细弹窗。
*/
require('./../../bootstrap');
@ -17,7 +19,7 @@ import TaobaoAlipayBills from './../../components/business/TaobaoAlipayBills.vue
import AccountBills from './../../components/business/AccountBills.vue';
const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'jingxi', 'douyin', 'dewu', 'vipshop', 'vipshop-jx', 'promotion-marketing'];
const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'jingxi', 'douyin', 'dewu', 'vipshop', 'vipshop-jx', 'promotion-marketing'];
const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'jingxi', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];
function pageContext(pathname = window.location.pathname) {
const parts = (pathname || '').split('/').filter(Boolean);
@ -53,12 +55,16 @@ function resolveComponent(context) {
return { Component: TaobaoAlipayBills, props: { apiBase: alipayBase, exportBase, progressBase } };
}
// 店铺收款账户账单 › 其它账户类型(通用实现)
if (context.page === 'store-receiving-bills' && GENERIC_TABS.indexOf(context.tab) !== -1) {
// 店铺收款账户账单 › 其它账户类型,以及店铺营销账户账单 › 推广费(通用实现)
if (
(context.page === 'store-receiving-bills' || context.page === 'marketing-account-bills')
&& GENERIC_TABS.indexOf(context.tab) !== -1
&& (context.page === 'store-receiving-bills' || context.tab === 'promotion-marketing')
) {
const progressBase = context.companyId
? `/${context.companyId}/ecommerce-finance/account-bills/export-progress`
: '/ecommerce-finance/account-bills/export-progress';
return { Component: AccountBills, props: { apiBase: accountBase, billType: context.tab, progressBase } };
return { Component: AccountBills, props: { apiBase: accountBase, billType: context.tab, progressBase, pageKey: context.page } };
}
return null;
@ -172,3 +178,26 @@ if (mount) {
updateActiveTab(initialContext.tab);
}
document.querySelectorAll('[data-promotion-expense-summary]').forEach((mount) => {
const userKey = mount.dataset.userKey || 'anonymous';
/* eslint-disable no-new */
new Vue({
el: mount,
render(h) {
return h(AccountBills, {
props: {
apiBase: mount.dataset.apiBase,
billType: 'promotion-marketing',
userKey,
pageKey: 'marketing-account-bills',
compact: true,
initialStart: mount.dataset.start || '',
initialEnd: mount.dataset.end || '',
},
});
},
});
});

View File

@ -40,8 +40,19 @@
<div class="w-full">
{{-- 汇总卡:当前筛选条件下的收入/支出/净额/笔数,金额单位为万元 --}}
@if ($hasSummary)
<div class="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div class="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-5">
<div
class="rounded-lg border border-gray-200 bg-white px-4 py-3"
data-promotion-expense-summary
data-api-base="{{ route('ecommerce-finance.account-bills.index', ['bill_type' => 'promotion-marketing']) }}"
data-start="{{ $filters['from'] ?? '' }}"
data-end="{{ $filters['to'] ?? '' }}"
data-user-key="{{ user()->id }}"
>
<div class="text-xs font-medium text-gray-500">推广费(营销账户)</div>
<div class="mt-1 text-lg font-semibold tabular-nums text-gray-500">加载中…</div>
</div>
@if ($hasSummary)
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3">
<div class="text-xs font-medium text-gray-500">收入合计(万元)</div>
<div class="mt-1 text-lg font-semibold text-green-600 tabular-nums">
@ -66,8 +77,8 @@
{{ number_format((int) ($summary['count'] ?? 0)) }}
</div>
</div>
</div>
@endif
@endif
</div>
{{-- 筛选器:按店铺 + 收支类型 + 交易日期范围 --}}
@php

View File

@ -0,0 +1,6 @@
<section class="business-content-section" aria-label="推广费(营销账户)">
<div
class="business-blank-page"
data-table-preferences-user="{{ user()->id }}"
></div>
</section>

View File

@ -1,6 +1,10 @@
@php
$isMarketingAccountBillsPage = ($section['slug'] ?? null) === 'billing-transactions'
&& ($page['slug'] ?? null) === 'marketing-account-bills';
$isPromotionMarketingBillsTab = $isMarketingAccountBillsPage
&& ($activeTab['slug'] ?? null) === 'promotion-marketing';
$isPromotionMarketingSummaryTab = $isMarketingAccountBillsPage
&& ($activeTab['slug'] ?? null) === 'taobao-alipay';
$isMarketingAccountBalancePage = ($section['slug'] ?? null) === 'billing-transactions'
&& ($page['slug'] ?? null) === 'marketing-account-balance';
$isStoreReceivingAccountsPage = ($section['slug'] ?? null) === 'fund-management'
@ -981,6 +985,9 @@
'business-tab-link',
'is-active' => $activeTab['slug'] === $tab['slug'],
])
@if ($isMarketingAccountBillsPage && $tab['slug'] === 'promotion-marketing')
data-marketing-account-bills-full-page
@endif
@if ($activeTab['slug'] === $tab['slug']) aria-current="page" @endif
>
<span class="material-icons-outlined text-xl" aria-hidden="true">
@ -1461,6 +1468,9 @@
@if ($isMarketingAccountBillsPage)
@push('scripts_start')
<script src="{{ asset('public/js/business/marketing-account-bills.min.js') }}?id={{ md5_file(public_path('js/business/marketing-account-bills.min.js')) }}"></script>
@if ($isPromotionMarketingBillsTab || $isPromotionMarketingSummaryTab)
<script src="{{ asset('public/js/business/pages.min.js') }}?id={{ md5_file(public_path('js/business/pages.min.js')) }}"></script>
@endif
@endpush
@elseif (($page['slug'] ?? null) === 'store-receiving-bills')
@push('scripts_start')

View File

@ -4,30 +4,32 @@ namespace Tests\Feature\Business;
use Mockery;
use Modules\EcommerceFinance\Repositories\PromotionMarketingExpenseRepository;
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
use Tests\Feature\FeatureTestCase;
class PromotionMarketingAccountBillsTest extends FeatureTestCase
{
public function test_promotion_marketing_tab_is_next_to_vipshop_jx(): void
public function test_promotion_marketing_tab_is_first_inside_marketing_account_bills(): void
{
$pages = collect(config('business_navigation'))
->firstWhere('slug', 'billing-transactions')['pages'];
$tabs = collect($pages)->firstWhere('slug', 'store-receiving-bills')['tabs'];
$slugs = array_column($tabs, 'slug');
$vipshopJxIndex = array_search('vipshop-jx', $slugs, true);
$receivingTabs = collect($pages)->firstWhere('slug', 'store-receiving-bills')['tabs'];
$marketingTabs = collect($pages)->firstWhere('slug', 'marketing-account-bills')['tabs'];
$this->assertNotFalse($vipshopJxIndex);
$this->assertSame('promotion-marketing', $slugs[$vipshopJxIndex + 1] ?? null);
$this->assertSame('推广费(营销账户)', $tabs[$vipshopJxIndex + 1]['title'] ?? null);
$this->assertNotContains('promotion-marketing', array_column($receivingTabs, 'slug'));
$this->assertSame('promotion-marketing', $marketingTabs[0]['slug'] ?? null);
$this->assertSame('推广费(营销账户)', $marketingTabs[0]['title'] ?? null);
$this->assertSame('taobao-alipay', $marketingTabs[1]['slug'] ?? null);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'store-receiving-bills',
'page' => 'marketing-account-bills',
'tab' => 'promotion-marketing',
]))
->assertOk()
->assertSeeText('推广费(营销账户)');
->assertSeeText('推广费(营销账户)')
->assertSee('class="business-blank-page"', false);
}
public function test_promotion_marketing_page_and_details_share_contract(): void
@ -101,6 +103,40 @@ class PromotionMarketingAccountBillsTest extends FeatureTestCase
->assertJsonPath('store_summary.0.amount', 12.34);
}
public function test_taobao_alipay_tab_mounts_the_promotion_card_before_income(): void
{
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn(['rows' => [], 'total' => 0]);
$repository->shouldReceive('summary')->once()->andReturn([
'income_total' => 100,
'expense_total' => 0,
'net' => 100,
'count' => 1,
]);
$repository->shouldReceive('storeOptions')->once()->andReturn([]);
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-bills',
'tab' => 'taobao-alipay',
]))->assertOk();
$html = $response->getContent();
$promotionPosition = strpos($html, 'data-promotion-expense-summary');
$incomePosition = strpos($html, '收入合计(万元)');
$this->assertNotFalse($promotionPosition);
$this->assertNotFalse($incomePosition);
$this->assertLessThan($incomePosition, $promotionPosition);
$response
->assertSee('public/js/business/pages.min.js', false)
->assertSee(route('ecommerce-finance.account-bills.index', [
'bill_type' => 'promotion-marketing',
]), false);
}
public function test_promotion_marketing_store_options_come_from_the_same_date_range(): void
{
$repository = Mockery::mock(PromotionMarketingExpenseRepository::class);

View File

@ -16,7 +16,7 @@ class AccountBillsTableTest extends TestCase
$this->assertStringContainsString('.tw-date.is-stable-date .tw-date-control', $source);
$this->assertStringContainsString("['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jingxi','jd-wallet','vipshop','vipshop-jx','promotion-marketing'].indexOf(this.billType)!==-1", $source);
$this->assertStringContainsString('v-if="usesMirrorOptions && !filterOptionsError"', $source);
$this->assertStringContainsString('page-key="store-receiving-bills"', $source);
$this->assertStringContainsString(':page-key="pageKey"', $source);
$this->assertStringContainsString(':tab-key="billType"', $source);
$this->assertStringContainsString("value=\"page-visible\"", $source);
$this->assertStringContainsString("value=\"filtered-visible\"", $source);
@ -34,7 +34,11 @@ class AccountBillsTableTest extends TestCase
$source,
);
$this->assertStringContainsString(
"const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'jingxi', 'douyin', 'dewu', 'vipshop', 'vipshop-jx', 'promotion-marketing'];",
"const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'jingxi', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];",
$source,
);
$this->assertStringContainsString(
"context.page === 'store-receiving-bills' || context.tab === 'promotion-marketing'",
$source,
);
}
@ -73,7 +77,23 @@ class AccountBillsTableTest extends TestCase
$this->assertStringContainsString('promotionFilteredStoreRows', $source);
$this->assertStringContainsString('tw-stats--promotion-only', $source);
$this->assertStringContainsString('.tw-stats--promotion-only{grid-template-columns:minmax(0,1fr)}', $source);
$this->assertStringContainsString('tw-stats--promotion-compact', $source);
$this->assertStringContainsString('table-workspace--compact', $source);
$this->assertStringContainsString('class="tw-date-icon"', $source);
$this->assertStringContainsString("if(this.isPromotionMarketing){\n return [{label:'推广费(营销账户)'", $source);
}
public function test_taobao_marketing_tab_places_the_promotion_card_before_income(): void
{
$source = file_get_contents(__DIR__ . '/../../../resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php');
$promotionPosition = strpos($source, 'data-promotion-expense-summary');
$incomePosition = strpos($source, '收入合计(万元)');
$this->assertNotFalse($promotionPosition);
$this->assertNotFalse($incomePosition);
$this->assertLessThan($incomePosition, $promotionPosition);
$this->assertStringContainsString('sm:grid-cols-5', $source);
$this->assertStringContainsString('data-api-base=', $source);
}
}