合并:完善店铺收款账户账单字段与加载状态

This commit is contained in:
WuYongJie 2026-07-22 13:43:38 +08:00
parent b008ff602a
commit 015c5de2a9
8 changed files with 120 additions and 40 deletions

View File

@ -72,9 +72,9 @@ return [
['slug' => 'taobao-wechat', 'title' => '淘系微信账户'],
['slug' => 'pinduoduo', 'title' => '拼多多账户'],
['slug' => 'jd-wallet', 'title' => '京东网银钱包账户'],
['slug' => 'jd-transactions', 'title' => '京东流水'],
['slug' => 'douyin', 'title' => '抖音账户'],
['slug' => 'dewu', 'title' => '得物账户'],
['slug' => 'vipshop', 'title' => '唯品会账户'],
],
],
['slug' => 'marketing-account-balance', 'title' => '店铺营销账户余额'],

View File

@ -8,6 +8,7 @@ namespace Modules\EcommerceFinance;
* 列定义来自「账单抬头.xlsx」的各子表(sheet 名 = 账户类型)。
* sheet 名 ↔ tab slug ↔ 列 三者在此一一对应,避免错乱。
*
* 账户可选配置分组表头 groups,字段列统一由 columns 定义。
* 每列: [label, field, kind]
* field: 真实列名(如 record_code / income / occurred_at / updated_at),
* 或 'payload.xxx' 表示存在 payload JSON 中。
@ -18,13 +19,20 @@ namespace Modules\EcommerceFinance;
class AccountBillColumns
{
/**
* @return array<string,array{title:string,columns:array<int,array{0:string,1:string,2:string}>}>
* @return array<string,array{title:string,groups?:array<int,array{0:string,1:int}>,columns:array<int,array{0:string,1:string,2:string}>}>
*/
public static function map(): array
{
return [
'taobao-wechat' => [
'title' => '淘系微信账户',
'groups' => [
['基础信息', 7],
['账户与交易', 5],
['金额', 2],
['业务关联', 2],
['维护信息', 2],
],
'columns' => [
['记录编码', 'record_code', 'num'],
['店铺', 'store_name', 'text'],
@ -214,7 +222,7 @@ class AccountBillColumns
}
/**
* @return array{title:string,columns:array<int,array{label:string,field:string,kind:string}>}
* @return array{title:string,groups:array<int,array{label:string,span:int}>,columns:array<int,array{label:string,field:string,kind:string}>}
*/
public static function for(string $type): ?array
{
@ -228,8 +236,13 @@ class AccountBillColumns
return ['label' => $c[0], 'field' => $c[1], 'kind' => $c[2]];
}, $map[$type]['columns']);
$groups = array_map(function ($group) {
return ['label' => $group[0], 'span' => $group[1]];
}, $map[$type]['groups'] ?? []);
return [
'title' => $map[$type]['title'],
'groups' => $groups,
'columns' => $columns,
];
}

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('ec_alipay_bills', function (Blueprint $table) {
$table->string('finance_remark', 255)->nullable()->after('store_name');
$table->string('order_no', 64)->nullable()->after('finance_remark');
$table->decimal('amount', 20, 6)->nullable()->after('order_no');
});
}
public function down(): void
{
Schema::table('ec_alipay_bills', function (Blueprint $table) {
$table->dropColumn(['finance_remark', 'order_no', 'amount']);
});
}
};

View File

@ -35,6 +35,14 @@ class AlipayBills extends Controller
$query->where('business_type', $type);
}
if ($orderNo = $request->get('order_no')) {
$query->where('order_no', 'like', '%' . $orderNo . '%');
}
if ($financeRemark = $request->get('finance_remark')) {
$query->where('finance_remark', 'like', '%' . $financeRemark . '%');
}
if ($start = $request->get('start')) {
$query->where('occurred_at', '>=', $start);
}
@ -48,10 +56,12 @@ class AlipayBills extends Controller
$like = '%' . $keyword . '%';
$q->where('goods_name', 'like', $like)
->orWhere('record_code', 'like', $like)
->orWhere('order_no', '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('finance_remark', 'like', $like)
->orWhere('remark', 'like', $like);
});
}

View File

@ -15,6 +15,9 @@ class AlipayBill extends Model
'company_id',
'record_code',
'store_name',
'finance_remark',
'order_no',
'amount',
'custom_remark',
'bill_item',
'business_type',
@ -43,6 +46,7 @@ class AlipayBill extends Model
protected $casts = [
'occurred_at' => 'datetime',
'amount' => 'decimal:6',
'income' => 'decimal:6',
'expense' => 'decimal:6',
'balance' => 'decimal:6',

View File

@ -54,15 +54,20 @@
<!-- 数据表(列由后端配置动态渲染) -->
<div class="ab-panel">
<div class="ab-phead">
<div class="ab-title">{{ columnsTitle }}</div>
<div class="ab-title">{{ columnGroups.length ? '账单明细' : columnsTitle }}</div>
<div class="ab-count" v-if="!loading">{{ total }} 条</div>
</div>
<div class="ab-scroll">
<table class="ab-table" v-if="rows.length && columns.length">
<table class="ab-table" :class="{ 'ab-table-grouped': columnGroups.length }" v-if="!loading && !loadError && columns.length">
<thead>
<tr>
<th v-for="(c, i) in columns" :key="i" :class="{ 'ab-num': isNumKind(c.kind) }"
<tr class="ab-group" v-if="columnGroups.length">
<th v-for="(group, i) in columnGroups" :key="i" :colspan="group.span" scope="colgroup">
{{ group.label }}
</th>
</tr>
<tr class="ab-col">
<th v-for="(c, i) in columns" :key="i" :class="{ 'ab-num': isNumKind(c.kind) }" scope="col"
@click="maybeSort(c)">
{{ c.label }}
<span class="ab-sort" v-if="sortableField(c.field)">⇅</span>
@ -76,11 +81,18 @@
<template v-else>{{ cellDisplay(r, c) }}</template>
</td>
</tr>
<tr v-if="!rows.length" class="ab-empty-row">
<td :colspan="columns.length" class="ab-empty">没有符合条件的账单记录</td>
</tr>
</tbody>
</table>
<div class="ab-empty" v-else-if="!loading">没有符合条件的账单记录</div>
<div class="ab-empty" v-else>加载中…</div>
<div v-else-if="loading" class="ab-empty">加载中…</div>
<div v-else-if="loadError" class="ab-empty ab-error">
<div>账单加载失败,请重试</div>
<button class="ab-btn" @click="fetch">重新加载</button>
</div>
<div v-else class="ab-empty">暂无账单字段</div>
</div>
<!-- 分页 -->
@ -118,9 +130,11 @@ export default {
data() {
return {
loading: false,
loadError: false,
rows: [],
columns: [],
columnsTitle: '',
columnGroups: [],
summary: { income: 0, expense: 0, balance: 0, count: 0 },
total: 0,
currentPage: 1,
@ -147,6 +161,7 @@ export default {
methods: {
async fetch() {
this.loading = true;
this.loadError = false;
const params = {
bill_type: this.billType,
...this.filters,
@ -168,12 +183,17 @@ export default {
if (body.columns) {
this.columns = body.columns.columns || body.columns || [];
this.columnsTitle = body.columns.title || '';
this.columnGroups = body.columns.groups || [];
}
} catch (e) {
console.error('加载账单失败', e);
this.rows = [];
this.columns = [];
this.columnsTitle = '';
this.columnGroups = [];
this.total = 0;
this.summary = { income: 0, expense: 0, balance: 0, count: 0 };
this.loadError = true;
} finally {
this.loading = false;
}
@ -295,8 +315,10 @@ export default {
.ab-scroll { overflow-x: auto; }
.ab-table { border-collapse: collapse; width: 100%; font-size: 0.78125rem; }
.ab-table thead th { background: #fafbfd; color: #1f2937; font-weight: 600; text-align: left; border-bottom: 1px solid #e6e9f0; padding: 0.5625rem 0.625rem; white-space: nowrap; }
.ab-table thead th.ab-num { text-align: right; }
.ab-table-grouped { min-width: 2200px; }
.ab-table thead .ab-group th { background: #f0f3f9; color: #6b7280; font-size: 0.75rem; font-weight: 600; text-align: center; border-bottom: 1px solid #dfe4ec; padding: 0.625rem; white-space: nowrap; }
.ab-table thead .ab-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; }
.ab-table thead .ab-col th.ab-num { text-align: right; }
.ab-sort { color: #9aa3b2; margin-left: 0.1875rem; }
.ab-table tbody td { padding: 0.5625rem 0.625rem; border-bottom: 1px solid #f1f3f7; white-space: nowrap; color: #1f2937; }
.ab-table tbody tr:hover { background: #f7f9ff; }
@ -307,6 +329,8 @@ export default {
.ab-tag { display: inline-block; padding: 0.0625rem 0.5rem; border-radius: 1rem; font-size: 0.71875rem; font-weight: 500; background: var(--vip-content-accent-soft); color: var(--vip-content-accent-dark); }
.ab-empty { padding: 3rem; text-align: center; color: #9aa3b2; }
.ab-error { color: #dc2626; }
.ab-error .ab-btn { margin-top: 0.75rem; color: #1f2937; }
.ab-pager { display: flex; align-items: center; gap: 0.625rem; padding: 0.875rem 1rem; border-top: 1px solid #e6e9f0; flex-wrap: wrap; }
.ab-page-info { color: #6b7280; font-size: 0.78125rem; }

View File

@ -82,39 +82,41 @@
</div>
<div class="tb-scroll">
<table class="tb-table" v-if="rows.length">
<table class="tb-table" v-if="!loading">
<thead>
<tr class="tb-group">
<th colspan="3">标识与店铺</th>
<th colspan="4">账单与业务</th>
<th colspan="9">账号与流水</th>
<th colspan="2">商品</th>
<th colspan="7">基础信息</th>
<th colspan="8">账户与交易</th>
<th colspan="3">金额(元)</th>
<th colspan="4">时间</th>
<th colspan="6">业务关联</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 class="tb-num">金额</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 @click="toggleSort('occurred_at')">发生时间 <span class="tb-sort">⇅</span></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>
<th>线上单号对应实际店铺</th>
<th>业务描述</th>
<th>业务账单来源</th>
<th>业务订单号</th>
<th>创建时间</th>
<th>修改时间</th>
<th>修改人</th>
@ -124,39 +126,42 @@
<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.finance_remark || '—' }}</td>
<td class="tb-mono">{{ r.order_no || '—' }}</td>
<td class="tb-num">{{ r.amount === null || r.amount === undefined ? '—' : money(r.amount) }}</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>{{ r.counterparty_account || '—' }}</td>
<td>{{ r.occurred_at || '—' }}</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><span class="tb-tag" :class="tagClass(r.business_type)">{{ r.business_type || '—' }}</span></td>
<td class="tb-muted tb-ellipsis">{{ r.remark || '—' }}</td>
<td class="tb-mono">{{ r.online_order_no || '—' }}</td>
<td>{{ r.online_real_store || '—' }}</td>
<td class="tb-muted tb-ellipsis">{{ r.business_description || '—' }}</td>
<td class="tb-muted">{{ r.business_bill_source || '—' }}</td>
<td class="tb-mono tb-muted">{{ r.business_order_no || '—' }}</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>
<tr v-if="!rows.length" class="tb-empty-row">
<td :colspan="28" class="tb-empty">没有符合条件的账单记录</td>
</tr>
</tbody>
</table>
<div class="tb-empty" v-else-if="!loading">没有符合条件的账单记录</div>
<div class="tb-empty" v-else>加载中…</div>
<div v-else class="tb-empty">加载中…</div>
</div>
<!-- 分页 -->

View File

@ -16,7 +16,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', 'douyin', 'dewu'];
const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'douyin', 'dewu', 'vipshop'];
const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu'];
function pageContext(pathname = window.location.pathname) {
const parts = (pathname || '').split('/').filter(Boolean);