feat(ecommerce): 店铺收款账户账单·淘系支付宝账户页

按淘系支付宝账单表1(25字段)在现有系统落地:
- 迁移:新增 ec_alipay_bills 表(标识/账单/账号流水/商品/金额/时间分组)
- 模型:AlipayBill(App\Abstracts\Model,company 作用域)
- 控制器:AlipayBills(admin 路由组,筛选/排序/分页 + 收入/支出/余额/笔数汇总)
- 路由:modules/EcommerceFinance/Routes/admin.php 登记 alipay-bills
- 前端:views/business/pages.js 统一业务页入口(从路径取 company_id/page slug)
  + components/business/TaobaoAlipayBills.vue(25列分组表/筛选/汇总卡/分页)
- webpack.mix.js 增加入口;business.pages.show 末尾按 slug 引入脚本
- 种子:表1 真实3行 + 补充行(演示用)
This commit is contained in:
xuxiake 2026-07-21 17:52:18 +08:00
parent 57fcd366f3
commit c934f5f12c
9 changed files with 884 additions and 0 deletions

View File

@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 淘系(淘宝/天猫)支付宝账户账单。
* 字段对齐「淘系支付宝账单.xlsx」表1。
*/
public function up(): void
{
Schema::create('ec_alipay_bills', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('company_id');
// —— 标识与店铺 ——
$table->string('record_code', 64); // 记录编码
$table->string('store_name', 255); // 原始店铺
$table->string('custom_remark', 255)->nullable(); // 自定义备注
// —— 账单与业务 ——
$table->string('bill_item', 64); // 账单项目
$table->string('business_type', 32); // 业务类型
$table->string('business_description', 512)->nullable(); // 业务描述
$table->string('remark', 512)->nullable(); // 备注
// —— 账号与流水 ——
$table->string('alipay_account', 64); // 支付宝账号
$table->string('counterparty_account', 255); // 对方账号
$table->string('account_order_no', 64); // 账务订单号
$table->string('business_serial_no', 64); // 业务流水号
$table->string('merchant_order_no', 64)->nullable(); // 商户订单号
$table->string('business_base_order_no', 64)->nullable(); // 业务基础订单号
$table->string('online_order_no', 64)->nullable(); // 线上单号
$table->string('business_order_no', 64)->nullable(); // 业务订单号
$table->string('business_bill_source', 128)->nullable(); // 业务账单来源
// —— 商品 ——
$table->string('goods_name', 255); // 商品名称
$table->string('online_real_store', 255)->nullable(); // 线上单号对应实际店铺
// —— 金额(元) ——
$table->decimal('income', 20, 6)->default(0); // 收入
$table->decimal('expense', 20, 6)->default(0); // 支出
$table->decimal('balance', 20, 6)->default(0); // 余额
// —— 时间 ——
$table->dateTime('occurred_at')->nullable(); // 发生时间
$table->string('modifier', 64)->nullable(); // 修改人
$table->string('platform', 32)->default('taobao'); // 平台(淘系/京东...),预留
$table->unsignedInteger('created_by')->nullable();
$table->string('created_from', 100)->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['company_id', 'record_code', 'deleted_at']);
$table->index(['company_id', 'store_name']);
$table->index(['company_id', 'alipay_account']);
$table->index(['company_id', 'bill_item']);
$table->index(['company_id', 'business_type']);
$table->index(['company_id', 'occurred_at']);
});
}
public function down(): void
{
Schema::dropIfExists('ec_alipay_bills');
}
};

View File

@ -0,0 +1,134 @@
<?php
namespace Modules\EcommerceFinance\Database\Seeds;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class AlipayBillsTableSeeder extends Seeder
{
/**
* 演示数据:表1 真实 3 行 + 补充行,便于本地预览淘系支付宝账户页面。
* 运行:php artisan db:seed --class=Modules\\EcommerceFinance\\Database\\Seeds\\AlipayBillsTableSeeder
*/
public function run()
{
$companyId = (int) (DB::table('companies')->value('id') ?? 1);
DB::table('ec_alipay_bills')->where('company_id', $companyId)->delete();
$rows = [
// —— 表1 真实样例 ——
[
'record_code' => '2000000001114707218',
'store_name' => '淘宝-亿家达家居鞋柜电脑桌',
'bill_item' => '交易收款', 'business_type' => '交易付款',
'remark' => '实木腿窄书桌书架一体电脑桌床边桌卧室家用小户型学习桌写字桌子',
'alipay_account' => '2088680870590483', 'counterparty_account' => '**文',
'account_order_no' => '1570801495986359201',
'business_serial_no' => '2026062923001159201451054134',
'merchant_order_no' => 'T200P5121711939135003306',
'online_order_no' => '5121711939135003306',
'goods_name' => '实木腿窄书桌书架一体电脑桌床边桌卧室家用小户型学习桌写字桌子',
'online_real_store' => '淘宝-亿家达家居鞋柜电脑桌',
'income' => 130.93, 'expense' => 0.00, 'balance' => 13899.81,
'occurred_at' => '2026-07-01 23:57:11', 'modifier' => 'api',
],
[
'record_code' => '2000000001114782775',
'store_name' => '天猫-亿家达旗舰店',
'bill_item' => '以旧换新自补平台过桥', 'business_type' => '交易分账',
'remark' => '以旧换新自补平台过桥{3310629457741000253}',
'alipay_account' => '2088870779873392', 'counterparty_account' => '浙江天猫技术有限公司',
'account_order_no' => '1570778496992153730',
'business_serial_no' => '2026062823001153731421787379',
'merchant_order_no' => 'T200P3310629457741000253',
'online_order_no' => '3310629457741000253',
'goods_name' => '书柜储物柜一体靠墙置物柜客厅墙边小格子柜卧室床边柜子飘窗书架',
'online_real_store' => '天猫-亿家达旗舰店',
'income' => 0.00, 'expense' => 17.44, 'balance' => 253157.10,
'occurred_at' => '2026-07-01 23:55:56', 'modifier' => 'api',
],
[
'record_code' => '2000000001114787788',
'store_name' => '天猫-卓禾家具旗舰店',
'bill_item' => '以旧换新自补平台过桥', 'business_type' => '交易分账',
'remark' => '以旧换新自补平台过桥{3310023615943000698}',
'alipay_account' => '2088480341979438', 'counterparty_account' => '浙江天猫技术有限公司',
'account_order_no' => '1570765667613157810',
'business_serial_no' => '2026062623001157811400561188',
'merchant_order_no' => 'T200P3310023615943000698',
'online_order_no' => '3310023615943000698',
'goods_name' => '沙发边几可移动升降小茶几桌子中古风客厅C型床边桌电脑桌置物架',
'online_real_store' => '天猫-卓禾家具旗舰店',
'income' => 0.00, 'expense' => 32.35, 'balance' => 83929.40,
'occurred_at' => '2026-07-01 23:55:52', 'modifier' => 'api',
],
];
// —— 补充演示行(覆盖更多 账单项目 / 业务类型) ——
$shops = ['淘宝-亿家达家居鞋柜电脑桌', '天猫-亿家达旗舰店', '天猫-卓禾家具旗舰店'];
$items = [
['交易收款', '交易付款', 0],
['退款', '退款', 1],
['提现到银行卡', '提现', 1],
['服务费', '手续费', 1],
['红包补贴', '交易分账', 0],
['以旧换新自补平台过桥', '交易分账', 1],
];
$goods = [
'北欧实木餐桌椅组合小户型吃饭桌',
'懒人沙发卧室阳台休闲单椅',
'岩板茶几客厅现代简约小茶桌',
'儿童学习桌可升降书桌写字台',
'衣柜简易组装卧室衣橱',
'实木床头柜卧室收纳柜',
];
$seq = 2000000001114790000;
$balance = 90000.00;
for ($i = 0; $i < 14; $i++) {
[$item, $type, $isOut] = $items[$i % count($items)];
$shop = $shops[$i % count($shops)];
$amount = $isOut
? round(5 + ($i * 13 % 120), 2)
: round(80 + ($i * 37 % 900), 2);
$balance += $isOut ? -$amount : $amount;
$day = str_pad((string) (1 + ($i % 15)), 2, '0', STR_PAD_LEFT);
$hh = str_pad((string) (8 + ($i % 12)), 2, '0', STR_PAD_LEFT);
$mm = str_pad((string) (($i * 7) % 60), 2, '0', STR_PAD_LEFT);
$rows[] = [
'record_code' => (string) ($seq++),
'store_name' => $shop,
'bill_item' => $item, 'business_type' => $type,
'remark' => $item === '以旧换新自补平台过桥' ? ('以旧换新自补平台过桥{33' . (1000000000000000 + $i) . '}') : $item,
'alipay_account' => '2088' . (800000000000 + $i),
'counterparty_account' => $isOut ? '浙江天猫技术有限公司' : '**客',
'account_order_no' => (string) (1570700000000000000 + $i),
'business_serial_no' => '202606' . $day . '2300115' . (1000000 + $i),
'merchant_order_no' => 'T200P' . (3300000000000000 + $i),
'online_order_no' => (string) (3300000000000000 + $i),
'goods_name' => $goods[$i % count($goods)],
'online_real_store' => $shop,
'income' => $isOut ? 0.00 : $amount,
'expense' => $isOut ? $amount : 0.00,
'balance' => round($balance, 2),
'occurred_at' => '2026-07-' . $day . ' ' . $hh . ':' . $mm . ':11',
'modifier' => 'api',
];
}
$now = now();
foreach ($rows as &$row) {
$row['company_id'] = $companyId;
$row['platform'] = 'taobao';
$row['created_from'] = 'ecommerce-finance';
$row['created_at'] = $now;
$row['updated_at'] = $now;
}
unset($row);
DB::table('ec_alipay_bills')->insert($rows);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Modules\EcommerceFinance\Http\Controllers;
use App\Abstracts\Http\Controller;
use Illuminate\Http\Request;
use Modules\EcommerceFinance\Models\AlipayBill;
class AlipayBills extends Controller
{
/**
* 淘系支付宝账户账单列表(店铺收款账户账单 › 淘系支付宝账户)。
* 经由 admin 路由组(web 会话)暴露,前端带 CSRF 调用即可。
*/
public function index(Request $request)
{
$query = AlipayBill::query()->where('company_id', company_id());
if ($store = $request->get('store_name')) {
$query->where('store_name', $store);
}
if ($alipay = $request->get('alipay_account')) {
$query->where('alipay_account', 'like', '%' . $alipay . '%');
}
if ($item = $request->get('bill_item')) {
$query->where('bill_item', $item);
}
if ($type = $request->get('business_type')) {
$query->where('business_type', $type);
}
if ($start = $request->get('start')) {
$query->where('occurred_at', '>=', $start);
}
if ($end = $request->get('end')) {
$query->where('occurred_at', '<=', $end . ' 23:59:59');
}
if ($keyword = $request->get('keyword')) {
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->where('goods_name', 'like', $like)
->orWhere('record_code', 'like', $like)
->orWhere('account_order_no', 'like', $like)
->orWhere('business_serial_no', 'like', $like)
->orWhere('merchant_order_no', 'like', $like)
->orWhere('online_order_no', 'like', $like)
->orWhere('remark', 'like', $like);
});
}
$sortable = ['occurred_at', 'income', 'expense', 'balance', 'record_code'];
$sort = in_array($request->get('sort'), $sortable, true) ? $request->get('sort') : 'occurred_at';
$dir = $request->get('dir') === 'asc' ? 'asc' : 'desc';
$query->orderBy($sort, $dir);
$perPage = min(max((int) $request->get('limit', 10), 1), 100);
$rows = $query->paginate($perPage);
// 汇总:基于当前筛选结果(分页前)
$summaryRow = (clone $query)->selectRaw(
'COALESCE(SUM(income), 0) as income, ' .
'COALESCE(SUM(expense), 0) as expense, ' .
'COUNT(*) as count'
)->first();
$latestBalance = (clone $query)
->orderByDesc('occurred_at')
->orderByDesc('id')
->value('balance');
$data = $rows->toArray();
$data['summary'] = [
'income' => (float) ($summaryRow->income ?? 0),
'expense' => (float) ($summaryRow->expense ?? 0),
'count' => (int) ($summaryRow->count ?? 0),
'balance' => $latestBalance !== null ? (float) $latestBalance : 0,
];
return response()->json($data);
}
public function show(AlipayBill $bill)
{
return response()->json($bill);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Modules\EcommerceFinance\Models;
use App\Abstracts\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class AlipayBill extends Model
{
use SoftDeletes;
protected $table = 'ec_alipay_bills';
protected $fillable = [
'company_id',
'record_code',
'store_name',
'custom_remark',
'bill_item',
'business_type',
'business_description',
'remark',
'alipay_account',
'counterparty_account',
'account_order_no',
'business_serial_no',
'merchant_order_no',
'business_base_order_no',
'online_order_no',
'business_order_no',
'business_bill_source',
'goods_name',
'online_real_store',
'income',
'expense',
'balance',
'occurred_at',
'modifier',
'platform',
'created_by',
'created_from',
];
protected $casts = [
'occurred_at' => 'datetime',
'income' => 'decimal:6',
'expense' => 'decimal:6',
'balance' => 'decimal:6',
];
}

View File

@ -4,4 +4,8 @@ use Illuminate\Support\Facades\Route;
Route::admin('ecommerce-finance', function () {
Route::view('/', 'ecommerce-finance::index')->name('index');
// 店铺收款账户账单 › 淘系支付宝账户
Route::get('alipay-bills', 'AlipayBills@index')->name('alipay-bills.index');
Route::get('alipay-bills/{bill}', 'AlipayBills@show')->name('alipay-bills.show');
});

View File

@ -0,0 +1,473 @@
<template>
<div class="taobao-page">
<!-- 汇总卡片 -->
<div class="tb-stats">
<div class="tb-stat">
<div class="tb-stat-k">收入合计</div>
<div class="tb-stat-v income">{{ money(summary.income) }}</div>
<div class="tb-stat-s">{{ summary.count }} 笔交易</div>
</div>
<div class="tb-stat">
<div class="tb-stat-k">支出合计</div>
<div class="tb-stat-v expense">{{ money(summary.expense) }}</div>
<div class="tb-stat-s">按当前筛选</div>
</div>
<div class="tb-stat">
<div class="tb-stat-k">期末余额(最新)</div>
<div class="tb-stat-v">{{ money(summary.balance) }}</div>
<div class="tb-stat-s">最新一条记录</div>
</div>
<div class="tb-stat">
<div class="tb-stat-k">交易笔数</div>
<div class="tb-stat-v">{{ summary.count }}</div>
<div class="tb-stat-s">当前筛选结果</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="tb-filter">
<div class="tb-field">
<label>原始店铺</label>
<input v-model.trim="filters.store_name" placeholder="如 天猫-亿家达旗舰店" />
</div>
<div class="tb-field">
<label>支付宝账号</label>
<input v-model.trim="filters.alipay_account" placeholder="如 2088**********" />
</div>
<div class="tb-field">
<label>账单项目</label>
<select v-model="filters.bill_item">
<option value="">全部项目</option>
<option>交易收款</option>
<option>以旧换新自补平台过桥</option>
<option>退款</option>
<option>提现到银行卡</option>
<option>服务费</option>
<option>红包补贴</option>
</select>
</div>
<div class="tb-field">
<label>业务类型</label>
<select v-model="filters.business_type">
<option value="">全部类型</option>
<option>交易付款</option>
<option>交易分账</option>
<option>退款</option>
<option>提现</option>
<option>手续费</option>
</select>
</div>
<div class="tb-field tb-field-date">
<label>发生时间</label>
<div class="tb-date">
<input type="date" v-model="filters.start" /> <span>~</span>
<input type="date" v-model="filters.end" />
</div>
</div>
<div class="tb-field tb-field-kw">
<label>关键词(商品 / 单号 / 备注)</label>
<input v-model.trim="filters.keyword" placeholder="输入商品名称、订单号或备注" />
</div>
<div class="tb-actions">
<button class="tb-btn tb-btn-primary" @click="applyFilters">查询</button>
<button class="tb-btn" @click="resetFilters">重置</button>
</div>
</div>
<!-- 数据表 -->
<div class="tb-panel">
<div class="tb-phead">
<div class="tb-title">账单明细</div>
<div class="tb-count" v-if="!loading">{{ total }} 条</div>
</div>
<div class="tb-scroll">
<table class="tb-table" v-if="rows.length">
<thead>
<tr class="tb-group">
<th colspan="3">标识与店铺</th>
<th colspan="4">账单与业务</th>
<th colspan="9">账号与流水</th>
<th colspan="2">商品</th>
<th colspan="3">金额(元)</th>
<th colspan="4">时间</th>
</tr>
<tr class="tb-col">
<th @click="toggleSort('record_code')">记录编码 <span class="tb-sort">⇅</span></th>
<th>原始店铺</th>
<th>自定义备注</th>
<th>账单项目</th>
<th>业务类型</th>
<th>业务描述</th>
<th>备注</th>
<th>支付宝账号</th>
<th>对方账号</th>
<th>账务订单号</th>
<th>业务流水号</th>
<th>商户订单号</th>
<th>业务基础订单号</th>
<th>线上单号</th>
<th>业务订单号</th>
<th>业务账单来源</th>
<th>商品名称</th>
<th>线上单号对应实际店铺</th>
<th class="tb-num" @click="toggleSort('income')">收入 <span class="tb-sort">⇅</span></th>
<th class="tb-num" @click="toggleSort('expense')">支出 <span class="tb-sort">⇅</span></th>
<th class="tb-num" @click="toggleSort('balance')">余额 <span class="tb-sort">⇅</span></th>
<th @click="toggleSort('occurred_at')">发生时间 <span class="tb-sort">⇅</span></th>
<th>创建时间</th>
<th>修改时间</th>
<th>修改人</th>
</tr>
</thead>
<tbody>
<tr v-for="r in rows" :key="r.id">
<td class="tb-mono">{{ r.record_code }}</td>
<td>{{ r.store_name }}</td>
<td class="tb-muted">{{ r.custom_remark || '—' }}</td>
<td><span class="tb-tag">{{ r.bill_item }}</span></td>
<td>
<span class="tb-tag" :class="tagClass(r.business_type)">{{ r.business_type }}</span>
</td>
<td class="tb-muted tb-ellipsis">{{ r.business_description || '—' }}</td>
<td class="tb-muted tb-ellipsis">{{ r.remark || '—' }}</td>
<td class="tb-mono">{{ r.alipay_account }}</td>
<td>{{ r.counterparty_account }}</td>
<td class="tb-mono">{{ r.account_order_no }}</td>
<td class="tb-mono">{{ r.business_serial_no }}</td>
<td class="tb-mono">{{ r.merchant_order_no || '—' }}</td>
<td class="tb-mono tb-muted">{{ r.business_base_order_no || '—' }}</td>
<td class="tb-mono">{{ r.online_order_no || '—' }}</td>
<td class="tb-mono tb-muted">{{ r.business_order_no || '—' }}</td>
<td class="tb-muted">{{ r.business_bill_source || '—' }}</td>
<td class="tb-ellipsis">{{ r.goods_name }}</td>
<td>{{ r.online_real_store || '—' }}</td>
<td class="tb-num tb-in" v-if="r.income > 0">+{{ money(r.income) }}</td>
<td class="tb-num tb-in" v-else>—</td>
<td class="tb-num tb-out" v-if="r.expense > 0">−{{ money(r.expense) }}</td>
<td class="tb-num tb-out" v-else>—</td>
<td class="tb-num">{{ money(r.balance) }}</td>
<td>{{ r.occurred_at }}</td>
<td class="tb-muted">{{ r.created_at }}</td>
<td class="tb-muted">{{ r.updated_at }}</td>
<td class="tb-muted">{{ r.modifier || '—' }}</td>
</tr>
</tbody>
</table>
<div class="tb-empty" v-else-if="!loading">没有符合条件的账单记录</div>
<div class="tb-empty" v-else>加载中…</div>
</div>
<!-- 分页 -->
<div class="tb-pager" v-if="rows.length">
<div class="tb-page-info">
第 {{ rangeStart }} - {{ rangeEnd }} 条 / 共 {{ total }} 条
</div>
<div class="tb-page-right">
<span>每页</span>
<select v-model.number="perPage" @change="changePage(1)">
<option :value="10">10</option>
<option :value="20">20</option>
<option :value="50">50</option>
</select>
<button class="tb-page-btn" :disabled="currentPage <= 1" @click="changePage(1)">«</button>
<button class="tb-page-btn" :disabled="currentPage <= 1" @click="changePage(currentPage - 1)">‹</button>
<span class="tb-page-no">{{ currentPage }} / {{ lastPage }}</span>
<button class="tb-page-btn" :disabled="currentPage >= lastPage" @click="changePage(currentPage + 1)">›</button>
<button class="tb-page-btn" :disabled="currentPage >= lastPage" @click="changePage(lastPage)">»</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TaobaoAlipayBills',
props: {
apiBase: { type: String, required: true },
},
data() {
return {
loading: false,
rows: [],
summary: { income: 0, expense: 0, balance: 0, count: 0 },
total: 0,
currentPage: 1,
lastPage: 1,
perPage: 10,
filters: {
store_name: '',
alipay_account: '',
bill_item: '',
business_type: '',
start: '',
end: '',
keyword: '',
},
sort: { key: 'occurred_at', dir: 'desc' },
};
},
computed: {
rangeStart() {
return this.total === 0 ? 0 : (this.currentPage - 1) * this.perPage + 1;
},
rangeEnd() {
return Math.min(this.currentPage * this.perPage, this.total);
},
},
mounted() {
this.fetch();
},
methods: {
async fetch() {
this.loading = true;
const params = {
...this.filters,
sort: this.sort.key,
dir: this.sort.dir,
limit: this.perPage,
page: this.currentPage,
};
try {
const res = await window.axios.get(this.apiBase, { params });
const body = res.data || {};
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 };
} catch (e) {
console.error('加载淘系支付宝账单失败', e);
this.rows = [];
this.total = 0;
this.summary = { income: 0, expense: 0, balance: 0, count: 0 };
} finally {
this.loading = false;
}
},
applyFilters() {
this.currentPage = 1;
this.fetch();
},
resetFilters() {
this.filters = {
store_name: '',
alipay_account: '',
bill_item: '',
business_type: '',
start: '',
end: '',
keyword: '',
};
this.currentPage = 1;
this.fetch();
},
changePage(p) {
this.currentPage = p;
this.fetch();
},
toggleSort(key) {
if (this.sort.key === key) {
this.sort.dir = this.sort.dir === 'asc' ? 'desc' : 'asc';
} else {
this.sort.key = key;
this.sort.dir = 'desc';
}
this.currentPage = 1;
this.fetch();
},
tagClass(type) {
if (type === '交易分账') return 'tb-tag-amber';
if (type === '退款') return 'tb-tag-gray';
return '';
},
money(n) {
const v = Number(n || 0);
return '¥' + v.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
},
},
};
</script>
<style scoped>
.taobao-page {
padding: 1.25rem 1.5rem 1.5rem;
}
.tb-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.875rem;
margin-bottom: 0.875rem;
}
.tb-stat {
background: #ffffff;
border: 1px solid #e6e9f0;
border-radius: 0.75rem;
padding: 1rem 1.125rem;
box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
}
.tb-stat-k { font-size: 0.75rem; color: #6b7280; }
.tb-stat-v { font-size: 1.375rem; font-weight: 700; margin-top: 0.5rem; letter-spacing: .3px; }
.tb-stat-s { font-size: 0.75rem; color: #9aa3b2; margin-top: 0.25rem; }
.tb-stat-v.income { color: #f5222d; }
.tb-stat-v.expense { color: #16a34a; }
.tb-filter {
display: flex;
flex-wrap: wrap;
gap: 0.875rem;
background: #ffffff;
border: 1px solid #e6e9f0;
border-radius: 0.75rem;
padding: 1rem 1.125rem;
margin-bottom: 0.875rem;
}
.tb-field { display: flex; flex-direction: column; gap: 0.3125rem; }
.tb-field label { font-size: 0.75rem; color: #6b7280; font-weight: 500; }
.tb-field input,
.tb-field select {
height: 2.125rem;
min-width: 11.25rem;
padding: 0 0.625rem;
border: 1px solid #e6e9f0;
border-radius: 0.5rem;
background: #fff;
font-size: 0.8125rem;
color: #1f2937;
outline: none;
}
.tb-field input:focus,
.tb-field select:focus { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, .12); }
.tb-field-kw { flex: 1; min-width: 12.5rem; }
.tb-field-kw input { width: 100%; }
.tb-date { display: flex; align-items: center; gap: 0.375rem; }
.tb-date input { min-width: 8.75rem; }
.tb-actions { display: flex; gap: 0.625rem; align-items: flex-end; margin-left: auto; }
.tb-btn {
height: 2.125rem;
padding: 0 0.875rem;
border-radius: 0.5rem;
border: 1px solid #e6e9f0;
background: #fff;
color: #1f2937;
cursor: pointer;
font-size: 0.8125rem;
font-weight: 500;
}
.tb-btn:hover { border-color: #c7cdda; }
.tb-btn-primary { background: #4f46e5; border-color: #4f46e5; color: #fff; }
.tb-btn-primary:hover { background: #4338ca; }
.tb-panel {
background: #fff;
border: 1px solid #e6e9f0;
border-radius: 0.75rem;
box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
overflow: hidden;
}
.tb-phead { display: flex; align-items: center; gap: 0.625rem; padding: 0.875rem 1rem; border-bottom: 1px solid #e6e9f0; }
.tb-title { font-weight: 600; font-size: 0.875rem; }
.tb-count { color: #9aa3b2; font-size: 0.75rem; }
.tb-scroll { overflow-x: auto; }
.tb-table { border-collapse: collapse; width: 100%; min-width: 2200px; font-size: 0.78125rem; }
.tb-table thead .tb-group th {
background: #f0f3f9;
color: #6b7280;
font-weight: 600;
text-align: center;
border-bottom: 1px solid #e6e9f0;
padding: 0.5625rem 0.625rem;
font-size: 0.75rem;
white-space: nowrap;
}
.tb-table thead .tb-col th {
background: #fafbfd;
color: #1f2937;
font-weight: 600;
text-align: left;
border-bottom: 1px solid #e6e9f0;
padding: 0.5625rem 0.625rem;
white-space: nowrap;
cursor: default;
position: relative;
}
.tb-table thead .tb-col th.tb-num { text-align: right; }
.tb-sort { color: #9aa3b2; cursor: pointer; margin-left: 0.1875rem; }
.tb-sort:hover { color: #4f46e5; }
.tb-table tbody td {
padding: 0.5625rem 0.625rem;
border-bottom: 1px solid #f1f3f7;
white-space: nowrap;
color: #1f2937;
}
.tb-table tbody tr:hover { background: #f7f9ff; }
.tb-num { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, Menlo, Consolas, monospace; }
.tb-mono { font-family: ui-monospace, Menlo, Consolas, monospace; color: #6b7280; font-size: 0.71875rem; }
.tb-muted { color: #9aa3b2; }
.tb-ellipsis { max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
.tb-in { color: #f5222d; font-weight: 600; }
.tb-out { color: #16a34a; font-weight: 600; }
.tb-tag {
display: inline-block;
padding: 0.0625rem 0.5rem;
border-radius: 1rem;
font-size: 0.71875rem;
font-weight: 500;
background: #eef2ff;
color: #4f46e5;
}
.tb-tag-amber { background: #fff4e5; color: #b76e00; }
.tb-tag-gray { background: #eef1f6; color: #6b7280; }
.tb-empty { padding: 3rem; text-align: center; color: #9aa3b2; }
.tb-pager {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.875rem 1rem;
border-top: 1px solid #e6e9f0;
flex-wrap: wrap;
}
.tb-page-info { color: #6b7280; font-size: 0.78125rem; }
.tb-page-right { margin-left: auto; display: flex; align-items: center; gap: 0.375rem; font-size: 0.78125rem; }
.tb-page-right select { height: 1.875rem; border: 1px solid #e6e9f0; border-radius: 0.4375rem; padding: 0 0.375rem; }
.tb-page-btn {
height: 1.875rem;
min-width: 1.875rem;
padding: 0 0.5rem;
border: 1px solid #e6e9f0;
background: #fff;
border-radius: 0.4375rem;
cursor: pointer;
font-size: 0.78125rem;
color: #1f2937;
}
.tb-page-btn:hover:not(:disabled) { border-color: #4f46e5; color: #4f46e5; }
.tb-page-btn:disabled { opacity: .45; cursor: not-allowed; }
.tb-page-no { padding: 0 0.375rem; }
.business-page-empty {
padding: 3rem;
text-align: center;
color: #9aa3b2;
font-size: 0.875rem;
}
</style>

View File

@ -0,0 +1,51 @@
/**
* 业务工作台(店铺管理 › 各子功能)统一前端入口。
* 对应 Blade:resources/views/business/pages/show.blade.php
* 从当前 URL(/{company_id}/workspace/{section}/{page}/{tab})取出
* company_id 与 page slug,挂载 Vue 并按 slug 渲染对应组件。
*
* 淘系支付宝账户(store-receiving-bill › taobao-alipay)在此实现;
* 京东流水(jd-transactions)等后续页面复用同一框架,按 slug 增加分支即可。
*/
require('./../../bootstrap');
import Vue from 'vue';
import TaobaoAlipayBills from './../../components/business/TaobaoAlipayBills.vue';
function pageContext() {
const parts = (window.location.pathname || '').split('/').filter(Boolean);
const idx = parts.indexOf('workspace');
if (idx === -1) {
return { companyId: '', page: '' };
}
const companyId = idx > 0 ? parts[idx - 1] : '';
const page = parts[idx + 1] || '';
return { companyId, page };
}
const { companyId, page } = pageContext();
const apiBase = companyId
? `/${companyId}/ecommerce-finance/alipay-bills`
: '/ecommerce-finance/alipay-bills';
const mount = document.querySelector('.business-blank-page');
if (mount) {
/* eslint-disable no-new */
new Vue({
el: mount,
render(h) {
if (page === 'taobao-alipay') {
return h(TaobaoAlipayBills, { props: { apiBase } });
}
return h('div', { class: 'business-page-empty' }, '该页面尚未实现');
},
});
}

View File

@ -98,5 +98,9 @@
<section class="business-blank-page" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}"></section>
</div>
@if (in_array($page['slug'] ?? null, ['taobao-alipay', 'taobao-wechat', 'jd-wallet', 'jd-transactions']))
<script src="{{ asset('js/business/pages.min.js') }}"></script>
@endif
</x-slot>
</x-layouts.admin>

3
webpack.mix.js vendored
View File

@ -61,6 +61,9 @@ mix
// Modules
.js('resources/assets/js/views/modules/apps.js', 'public/js/modules/apps.min.js')
// Business workspace(店铺管理 › 各子功能)
.js('resources/assets/js/views/business/pages.js', 'public/js/business/pages.min.js')
// Portal
.js('resources/assets/js/views/portal/apps.js', 'public/js/portal/apps.min.js')