feat: improve business directory filters and export
This commit is contained in:
parent
44374faa0b
commit
ea2d2f9ca1
@ -88,10 +88,12 @@ class UpstreamBusinessInformation
|
||||
|
||||
private function companyPage(int $page, int $perPage): array
|
||||
{
|
||||
$filters = $this->pageFilters('company-management', 'basic-information');
|
||||
return $this->page(
|
||||
'company-information.read',
|
||||
$page,
|
||||
$perPage,
|
||||
$filters,
|
||||
fn () => $this->snapshot('company-information.read', [
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
@ -133,10 +135,12 @@ SQL);
|
||||
|
||||
private function storePage(int $page, int $perPage): array
|
||||
{
|
||||
$filters = $this->pageFilters('store-management', 'basic-information');
|
||||
return $this->page(
|
||||
'store-information.read',
|
||||
$page,
|
||||
$perPage,
|
||||
$filters,
|
||||
fn () => $this->snapshot('store-information.read', [
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
@ -157,7 +161,11 @@ SQL);
|
||||
private function storeAccountPage(int $page, int $perPage): array
|
||||
{
|
||||
try {
|
||||
return ($this->storeAccounts ?? app(StoreAccountMirrorRepository::class))->paginate($page, $perPage);
|
||||
return ($this->storeAccounts ?? app(StoreAccountMirrorRepository::class))->paginate(
|
||||
$page,
|
||||
$perPage,
|
||||
$this->pageFilters('store-management', 'store-accounts'),
|
||||
);
|
||||
} catch (\RuntimeException $exception) {
|
||||
report($exception);
|
||||
throw new ServiceUnavailableHttpException(null, '店铺账号共享镜像尚未完成首次同步。');
|
||||
@ -469,10 +477,12 @@ SQL);
|
||||
|
||||
private function trademarkPage(int $page, int $perPage): array
|
||||
{
|
||||
$filters = $this->pageFilters('trademark-management', 'information');
|
||||
return $this->page(
|
||||
'trademark-information.read',
|
||||
$page,
|
||||
$perPage,
|
||||
$filters,
|
||||
fn () => $this->snapshot('trademark-information.read', [
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
@ -509,10 +519,14 @@ SQL);
|
||||
];
|
||||
}
|
||||
|
||||
private function page(string $scope, int $page, int $perPage, callable $resolver): array
|
||||
private function page(string $scope, int $page, int $perPage, array $filters, callable $resolver): array
|
||||
{
|
||||
if ($this->mirror->ready($scope)) {
|
||||
return $this->normalize($scope, $this->mirror->paginate($scope, $page, $perPage));
|
||||
return $this->normalize($scope, $this->mirror->paginate($scope, $page, $perPage, $filters));
|
||||
}
|
||||
|
||||
if ($filters !== []) {
|
||||
throw new ServiceUnavailableHttpException(null, '当前数据源暂不支持所选条件查询,请检查共享业务镜像状态。');
|
||||
}
|
||||
|
||||
if ((app(\App\Utilities\SystemDatabaseConfiguration::class)->current()['mode'] ?? '') === 'bootstrap'
|
||||
@ -527,6 +541,34 @@ SQL);
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function pageFilters(string $section, string $page): array
|
||||
{
|
||||
$fields = collect(config('business_navigation'))
|
||||
->firstWhere('slug', $section)['pages'] ?? [];
|
||||
$pageConfig = collect($fields)->flatMap(fn (array $item) => [$item, ...($item['children'] ?? [])])->firstWhere('slug', $page);
|
||||
$allowed = collect($pageConfig['directory']['fields'] ?? [])
|
||||
->reject(fn (array $field) => ($field['kind'] ?? '') === 'secret' && ($field['name'] ?? '') !== 'withdrawal_account')
|
||||
->pluck('name')->all();
|
||||
if ($section === 'company-management' && $page === 'basic-information') {
|
||||
$allowed = array_values(array_unique([...$allowed, 'company_type', 'related_people']));
|
||||
}
|
||||
$filters = [];
|
||||
foreach ($allowed as $name) {
|
||||
foreach ([$name, $name . '_from', $name . '_to'] as $key) {
|
||||
$value = trim((string) request($key, ''));
|
||||
if ($value !== '') $filters[$key] = mb_substr($value, 0, 191);
|
||||
}
|
||||
}
|
||||
$sort = trim((string) request('sort', ''));
|
||||
if (in_array($sort, $allowed, true)) {
|
||||
$filters['_sort'] = $sort;
|
||||
$filters['_direction'] = request('direction') === 'desc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/** @param array{data: array<int,array<string,mixed>>, total: int} $payload */
|
||||
private function normalize(string $scope, array $payload): array
|
||||
{
|
||||
@ -548,6 +590,7 @@ SQL);
|
||||
$row = $this->toUtf8($row);
|
||||
$row['establishment_date'] = $this->date($row['establishment_date'] ?? null);
|
||||
$row['subscribed_at'] = $this->date($row['subscribed_at'] ?? null);
|
||||
$row['city'] = RegisteredAddressCity::parse($row['registered_address'] ?? null);
|
||||
$row['company_status'] = match ((string) ($row['company_status'] ?? '')) {
|
||||
'0' => '正常',
|
||||
'1' => '已注销',
|
||||
|
||||
@ -76,7 +76,7 @@ class SidebarPermissions
|
||||
return $navigation;
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(function (array $section) use ($allowed) {
|
||||
$navigation = array_values(array_filter(array_map(function (array $section) use ($allowed) {
|
||||
$sectionKey = $section['slug'];
|
||||
|
||||
if (in_array($sectionKey, $allowed, true)) {
|
||||
@ -118,6 +118,29 @@ class SidebarPermissions
|
||||
|
||||
return null;
|
||||
}, $navigation)));
|
||||
|
||||
// 新增的公司成本安排与主体经营报表共用公司经营报表权限。
|
||||
// 对已经保存过精确权限的账号兼容旧配置,避免新增页面被旧权限集合隐藏。
|
||||
foreach ($navigation as &$section) {
|
||||
if (($section['slug'] ?? null) !== 'company-management') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasSubjectReport = collect($section['pages'] ?? [])->contains('slug', 'subject-operating-report');
|
||||
$hasCostArrangement = collect($section['pages'] ?? [])->contains('slug', 'company-cost-arrangement');
|
||||
|
||||
if ($hasSubjectReport && ! $hasCostArrangement) {
|
||||
$costPage = collect(BusinessNavigation::all())
|
||||
->firstWhere('slug', 'company-management')['pages'] ?? [];
|
||||
$costPage = collect($costPage)->firstWhere('slug', 'company-cost-arrangement');
|
||||
if ($costPage) {
|
||||
$section['pages'][] = $costPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($section);
|
||||
|
||||
return $navigation;
|
||||
}
|
||||
|
||||
public static function allKeys(): array
|
||||
|
||||
@ -26,6 +26,28 @@ $navigation = [
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'slug' => 'company-cost-arrangement',
|
||||
'title' => '公司成本安排',
|
||||
'directory' => [
|
||||
'icon' => 'account_balance_wallet',
|
||||
'status' => '报表待定义',
|
||||
'description' => '按公司主体查看当月收入、成本缺口及成本费用安排。',
|
||||
'empty' => '公司成本安排尚未同步数据。',
|
||||
'fields' => [
|
||||
['name' => 'company', 'label' => '公司'],
|
||||
['name' => 'company_type', 'label' => '公司类型'],
|
||||
['name' => 'invoice_type', 'label' => '发票类型'],
|
||||
['name' => 'current_month_income_forecast', 'label' => '当月收入预测', 'kind' => 'number'],
|
||||
['name' => 'historical_cost_gap', 'label' => '往期成本缺口', 'kind' => 'number'],
|
||||
['name' => 'supplier_cost_arrangement', 'label' => '供应商成本安排', 'kind' => 'number'],
|
||||
['name' => 'courier_cost_arrangement', 'label' => '快递成本安排', 'kind' => 'number'],
|
||||
['name' => 'current_month_tax_forecast', 'label' => '当月税金预测', 'kind' => 'number'],
|
||||
['name' => 'labor_cost_forecast', 'label' => '人工成本预测', 'kind' => 'number'],
|
||||
['name' => 'other_expense_forecast', 'label' => '其他费用预测', 'kind' => 'number'],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'slug' => 'basic-information',
|
||||
'title' => '公司基础信息',
|
||||
|
||||
@ -31,7 +31,7 @@ class BusinessInformationMirrorRepository
|
||||
}
|
||||
|
||||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||||
public function paginate(string $scope, int $page, int $perPage): array
|
||||
public function paginate(string $scope, int $page, int $perPage, array $filters = []): array
|
||||
{
|
||||
if (! $this->ready($scope)) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
@ -41,9 +41,9 @@ class BusinessInformationMirrorRepository
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
|
||||
return match ($scope) {
|
||||
'company-information.read' => $this->companies($page, $perPage),
|
||||
'store-information.read' => $this->stores($page, $perPage),
|
||||
'trademark-information.read' => $this->trademarks($page, $perPage),
|
||||
'company-information.read' => $this->companies($page, $perPage, $filters),
|
||||
'store-information.read' => $this->stores($page, $perPage, $filters),
|
||||
'trademark-information.read' => $this->trademarks($page, $perPage, $filters),
|
||||
default => throw new InvalidArgumentException('未知的业务资料镜像范围:' . $scope),
|
||||
};
|
||||
}
|
||||
@ -162,7 +162,7 @@ SQL)
|
||||
}
|
||||
|
||||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||||
private function companies(int $page, int $perPage): array
|
||||
private function companies(int $page, int $perPage, array $filters): array
|
||||
{
|
||||
$query = $this->connection->table('ops_fys_company_info')
|
||||
->selectRaw(<<<'SQL'
|
||||
@ -184,13 +184,77 @@ taxCategoryAndType AS taxpayer_category,
|
||||
NULL AS shareholders
|
||||
SQL);
|
||||
|
||||
return $this->page($query, $page, $perPage, 'Id');
|
||||
$this->applyFieldFilters($query, $filters, [
|
||||
'unified_social_credit_code' => 'unifiedSocialCreditCode', 'company_name' => 'businessLicenseName',
|
||||
'establishment_date' => 'IncorporationDate',
|
||||
'registered_at' => 'IncorporationDate',
|
||||
'legal_representative' => 'legalRepresentative', 'registered_address' => 'registeredAddress',
|
||||
'city' => 'registeredAddress',
|
||||
'business_scope' => 'businessScope', 'subscribed_at' => 'registeredCapitalVaildTime',
|
||||
'financial_officer' => 'financialManager',
|
||||
'billing_officer' => 'financialManager',
|
||||
'supervisor' => 'supervisor',
|
||||
], ['subscribed_capital' => 'registeredCapital'], [
|
||||
'company_type' => 'businessLicenseType',
|
||||
'taxpayer_type' => 'taxCategoryAndType',
|
||||
'taxpayer_category' => 'taxCategoryAndType',
|
||||
]);
|
||||
$companyStatus = trim((string) ($filters['company_status'] ?? ''));
|
||||
if ($companyStatus !== '') {
|
||||
$statusCode = ['正常' => '0', '已注销' => '1'][$companyStatus] ?? $companyStatus;
|
||||
$query->where('companyStatus', '=', $statusCode);
|
||||
}
|
||||
$relatedPeople = mb_substr(trim((string) ($filters['related_people'] ?? '')), 0, 191);
|
||||
if ($relatedPeople !== '') {
|
||||
$like = '%' . addcslashes($relatedPeople, '%_\\') . '%';
|
||||
$query->where(function ($query) use ($like): void {
|
||||
$query->where('legalRepresentative', 'like', $like)
|
||||
->orWhere('supervisor', 'like', $like)
|
||||
->orWhere('financialManager', 'like', $like);
|
||||
});
|
||||
}
|
||||
return $this->page($query, $page, $perPage, 'Id', $filters, [
|
||||
'unified_social_credit_code' => 'unifiedSocialCreditCode', 'company_name' => 'businessLicenseName',
|
||||
'taxpayer_type' => 'taxCategoryAndType', 'company_status' => 'companyStatus',
|
||||
'subscribed_capital' => 'registeredCapital', 'registered_at' => 'IncorporationDate',
|
||||
'legal_representative' => 'legalRepresentative', 'supervisor' => 'supervisor',
|
||||
'billing_officer' => 'financialManager', 'registered_address' => 'registeredAddress',
|
||||
'city' => 'registeredAddress', 'business_scope' => 'businessScope',
|
||||
'basic_deposit_account' => 'accountSerialNumber', 'subscribed_at' => 'registeredCapitalVaildTime',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||||
private function stores(int $page, int $perPage): array
|
||||
private function stores(int $page, int $perPage, array $filters): array
|
||||
{
|
||||
return $this->page($this->storeQuery(), $page, $perPage, 's.Id');
|
||||
$query = $this->storeQuery();
|
||||
$this->applyFieldFilters($query, $filters, [
|
||||
'store_id' => 's.sotreId', 'store_name' => 's.erpShopName', 'store_entity' => 'c.businessLicenseName',
|
||||
'trademark' => 't.trademarkName', 'trademark_number' => 's.trademarkNumber',
|
||||
'business_department' => 's.responsibleDepartment', 'store_manager' => 's.shopHead',
|
||||
'department_manager' => 's.customerDeptManager', 'opened_at' => 's.openTime', 'renewed_at' => 's.renewalTime',
|
||||
], [], ['platform' => 's.platform']);
|
||||
$storeStatus = trim((string) ($filters['store_status'] ?? ''));
|
||||
if ($storeStatus !== '') {
|
||||
$statusCode = ['正常' => '0', '关闭中' => '3', '已关闭' => '6'][$storeStatus] ?? $storeStatus;
|
||||
$query->where('s.shopState', '=', $statusCode);
|
||||
}
|
||||
$withdrawalAccount = trim((string) ($filters['withdrawal_account'] ?? ''));
|
||||
if ($withdrawalAccount === '已配置') {
|
||||
$query->whereNotNull('s.shopBankAccount')->where('s.shopBankAccount', '<>', '');
|
||||
} elseif ($withdrawalAccount === '未配置') {
|
||||
$query->where(function ($query): void {
|
||||
$query->whereNull('s.shopBankAccount')->orWhere('s.shopBankAccount', '=', '');
|
||||
});
|
||||
}
|
||||
return $this->page($query, $page, $perPage, 's.Id', $filters, [
|
||||
'platform' => 's.platform', 'store_id' => 's.sotreId', 'store_name' => 's.erpShopName',
|
||||
'store_entity' => 'c.businessLicenseName', 'store_status' => 's.shopState',
|
||||
'trademark' => 't.trademarkName', 'trademark_number' => 's.trademarkNumber',
|
||||
'business_department' => 's.responsibleDepartment', 'store_manager' => 's.shopHead',
|
||||
'department_manager' => 's.customerDeptManager', 'opened_at' => 's.openTime',
|
||||
'renewed_at' => 's.renewalTime', 'withdrawal_account' => 's.shopBankAccount',
|
||||
]);
|
||||
}
|
||||
|
||||
private function storeQuery()
|
||||
@ -245,7 +309,7 @@ SQL);
|
||||
}
|
||||
|
||||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||||
private function trademarks(int $page, int $perPage): array
|
||||
private function trademarks(int $page, int $perPage, array $filters): array
|
||||
{
|
||||
$query = $this->connection->table('ops_fys_trademark_info')
|
||||
->where('isDelete', 0)
|
||||
@ -264,14 +328,69 @@ custodyDepartment AS custody_department,
|
||||
remark AS notes
|
||||
SQL);
|
||||
|
||||
return $this->page($query, $page, $perPage, 'Id');
|
||||
$this->applyFieldFilters($query, $filters, [
|
||||
'trademark_number' => 'trademarkNumber', 'trademark_name' => 'trademarkName',
|
||||
'registration_date' => 'registrationDate',
|
||||
'expiry_date' => 'termOfValidity', 'international_classification' => 'internationalClassification',
|
||||
'approved_goods' => 'approvedCommodities', 'final_holder' => 'transferenceUser',
|
||||
'registered_address' => 'registrationAddress', 'custody_department' => 'custodyDepartment',
|
||||
]);
|
||||
$status = trim((string) ($filters['trademark_status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$statusCode = ['未启用' => '0', '有效' => '1'][$status] ?? $status;
|
||||
$query->where('trademarkStatus', '=', $statusCode);
|
||||
}
|
||||
return $this->page($query, $page, $perPage, 'Id', $filters, [
|
||||
'trademark_name' => 'trademarkName', 'trademark_number' => 'trademarkNumber',
|
||||
'trademark_status' => 'trademarkStatus', 'registration_date' => 'registrationDate',
|
||||
'expiry_date' => 'termOfValidity', 'international_classification' => 'internationalClassification',
|
||||
'approved_goods' => 'approvedCommodities', 'final_holder' => 'transferenceUser',
|
||||
'registered_address' => 'registrationAddress', 'custody_department' => 'custodyDepartment',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<string,string> $textColumns @param array<string,string> $exactColumns @param array<string,string> $enumColumns */
|
||||
private function applyFieldFilters($query, array $filters, array $textColumns, array $exactColumns = [], array $enumColumns = []): void
|
||||
{
|
||||
foreach ($textColumns as $key => $column) {
|
||||
$value = mb_substr(trim((string) ($filters[$key] ?? '')), 0, 191);
|
||||
if ($value !== '') {
|
||||
$query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%');
|
||||
}
|
||||
}
|
||||
foreach ($exactColumns as $key => $column) {
|
||||
$value = trim((string) ($filters[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
$query->where($column, '=', $value);
|
||||
}
|
||||
}
|
||||
foreach ($enumColumns as $key => $column) {
|
||||
$value = trim((string) ($filters[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
$query->where($column, '=', $value);
|
||||
}
|
||||
}
|
||||
foreach (['opened_at' => 's.openTime', 'renewed_at' => 's.renewalTime', 'registration_date' => 'registrationDate', 'expiry_date' => 'termOfValidity'] as $key => $column) {
|
||||
if (! array_key_exists($key, $textColumns)) continue;
|
||||
foreach (['_from' => '>=', '_to' => '<='] as $suffix => $operator) {
|
||||
$date = trim((string) ($filters[$key . $suffix] ?? ''));
|
||||
if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/D', $date)) $query->where($column, $operator, $date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||||
private function page($query, int $page, int $perPage, string $orderColumn): array
|
||||
private function page($query, int $page, int $perPage, string $orderColumn, array $filters = [], array $sortColumns = []): array
|
||||
{
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query->orderByDesc($orderColumn)
|
||||
$sortColumn = $sortColumns[$filters['_sort'] ?? ''] ?? null;
|
||||
$direction = ($filters['_direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
|
||||
if ($sortColumn !== null) {
|
||||
$query->orderBy($sortColumn, $direction)->orderByDesc($orderColumn);
|
||||
} else {
|
||||
$query->orderByDesc($orderColumn);
|
||||
}
|
||||
$rows = $query
|
||||
->forPage($page, $perPage)
|
||||
->get()
|
||||
->map(static fn (object $row): array => (array) $row)
|
||||
|
||||
@ -15,7 +15,7 @@ class StoreAccountMirrorRepository
|
||||
) {}
|
||||
|
||||
/** @return array{data:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginate(int $page, int $perPage): array
|
||||
public function paginate(int $page, int $perPage, array $filters = []): array
|
||||
{
|
||||
$dataset = $this->datasets->get(self::DATASET);
|
||||
if (! $this->mirror->ready($dataset)) {
|
||||
@ -25,9 +25,46 @@ class StoreAccountMirrorRepository
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$query = $this->mirror->connection()->table($dataset->targetTable());
|
||||
$columns = [
|
||||
'store_name' => 'storeName', 'operation_owner' => 'operationOwner',
|
||||
'operation_department_head' => 'operationDepartmentHead', 'operation_director' => 'operationDirector',
|
||||
'profit_center' => 'profitCenter', 'account' => 'account', 'bound_phone' => 'boundPhone',
|
||||
'verification_contact' => 'verificationContact', 'account_group_name' => 'accountGroupName',
|
||||
];
|
||||
foreach ($columns as $key => $column) {
|
||||
$value = mb_substr(trim((string) ($filters[$key] ?? '')), 0, 191);
|
||||
if ($value !== '') $query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%');
|
||||
}
|
||||
$accountType = trim((string) ($filters['account_type'] ?? ''));
|
||||
if ($accountType !== '') $query->where('accountType', '=', $accountType === '主账号' ? 'MAIN' : ($accountType === '子账号' ? 'SUB' : $accountType));
|
||||
$passwordState = trim((string) ($filters['password_configured'] ?? ''));
|
||||
if (in_array($passwordState, ['已设置', '未设置', '状态未同步'], true)) {
|
||||
if ($passwordState === '状态未同步') $query->whereNull('passwordConfigured');
|
||||
else $query->where('passwordConfigured', $passwordState === '已设置');
|
||||
}
|
||||
foreach (['created_at' => 'createdAt', 'updated_at' => 'updatedAt'] as $key => $column) {
|
||||
foreach (['_from' => '>=', '_to' => '<='] as $suffix => $operator) {
|
||||
$date = trim((string) ($filters[$key . $suffix] ?? ''));
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date)) $query->whereDate($column, $operator, $date);
|
||||
}
|
||||
}
|
||||
$total = (clone $query)->count();
|
||||
$sortColumns = [
|
||||
'store_name' => 'storeName', 'operation_owner' => 'operationOwner',
|
||||
'operation_department_head' => 'operationDepartmentHead', 'operation_director' => 'operationDirector',
|
||||
'profit_center' => 'profitCenter', 'account_type' => 'accountType', 'account' => 'account',
|
||||
'password_configured' => 'passwordConfigured', 'bound_phone' => 'boundPhone',
|
||||
'verification_contact' => 'verificationContact', 'account_group_name' => 'accountGroupName',
|
||||
'created_at' => 'createdAt', 'updated_at' => 'updatedAt',
|
||||
];
|
||||
$sortColumn = $sortColumns[$filters['_sort'] ?? ''] ?? null;
|
||||
$direction = ($filters['_direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
|
||||
if ($sortColumn !== null) {
|
||||
$query->orderBy($sortColumn, $direction)->orderByDesc('id');
|
||||
} else {
|
||||
$query->orderByDesc('id');
|
||||
}
|
||||
$rows = $query
|
||||
->orderByDesc('id')
|
||||
->forPage($page, $perPage)
|
||||
->get()
|
||||
->map(fn (object $row): array => [
|
||||
|
||||
@ -23,6 +23,19 @@
|
||||
$directoryCurrentPage = $directoryIsPaginated ? $directoryRows->currentPage() : 1;
|
||||
$directoryLastPage = $directoryIsPaginated ? $directoryRows->lastPage() : 1;
|
||||
$directoryPerPage = $directoryIsPaginated ? $directoryRows->perPage() : 0;
|
||||
$directoryPageKey = ($section['slug'] ?? '') . '/' . ($page['slug'] ?? '');
|
||||
$directoryUsesFieldQuery = in_array($directoryPageKey, [
|
||||
'company-management/basic-information',
|
||||
'store-management/basic-information',
|
||||
'trademark-management/information',
|
||||
'store-management/store-accounts',
|
||||
], true);
|
||||
$directoryQueryPageDescriptions = [
|
||||
'company-management/basic-information' => '公司主体、状态和登记资料的查询与导出',
|
||||
'store-management/basic-information' => '店铺主体、平台和经营资料的查询与导出',
|
||||
'trademark-management/information' => '商标主体、状态和登记资料的查询与导出',
|
||||
'store-management/store-accounts' => '店铺账号资料的查询与安全状态导出',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
@ -35,6 +48,37 @@
|
||||
color: var(--vip-content-ink, #171717);
|
||||
}
|
||||
|
||||
.directory-table-page.has-field-query {
|
||||
margin-top: 1.5rem;
|
||||
overflow: visible;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.directory-query-page-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.directory-query-page-head strong {
|
||||
display: block;
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.directory-query-page-head p {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.directory-table-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@ -115,6 +159,60 @@
|
||||
background: var(--vip-content-bg, #f8f6f1);
|
||||
}
|
||||
|
||||
.directory-query-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(12rem, 1fr));
|
||||
gap: 1rem 1.25rem;
|
||||
margin: 0;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 0.5rem;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
background: #fff;
|
||||
}
|
||||
.directory-query-heading { display: flex; grid-column: 1 / -1; align-items: center; justify-content: space-between; gap: 1rem; padding-bottom: 0.125rem; color: var(--vip-content-accent-dark, #856632); }
|
||||
.directory-query-heading strong { font-size: 0.8125rem; }
|
||||
.directory-query-heading span { color: var(--vip-content-muted, #595959); font-size: 0.6875rem; font-weight: 400; }
|
||||
.directory-query-field { position: relative; display: flex; min-width: 0; flex-direction: column; align-items: stretch; gap: 0.45rem; color: var(--vip-content-muted, #595959); font-size: 0.75rem; }
|
||||
.directory-query-field > span:first-child { overflow: hidden; color: var(--vip-content-ink, #171717); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.directory-query-field input, .directory-query-field select { width: 100%; min-width: 0; height: 2.125rem; padding: 0 0.625rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.375rem; background: #fff; color: var(--vip-content-ink, #171717); font-size: 0.75rem; }
|
||||
.directory-query-field input:focus, .directory-query-field select:focus { border-color: var(--vip-content-accent, #b9954f); outline: 2px solid rgba(185, 149, 79, 0.16); }
|
||||
.directory-query-dates { display: grid; flex: 1 1 auto; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 0.25rem; width: 100%; min-width: 0; }
|
||||
.directory-query-dates input { min-width: 0; }
|
||||
.directory-query-suggestions { position: absolute; top: calc(100% + 0.25rem); right: 0; left: 0; z-index: 6; margin: 0; padding: 0.25rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.375rem; background: #fff; box-shadow: 0 5px 12px rgba(23,23,23,0.12); list-style: none; }
|
||||
.directory-query-suggestions[hidden] { display: none; }
|
||||
.directory-query-suggestions button { display: block; width: 100%; overflow: hidden; padding: 0.4rem 0.5rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--vip-content-ink, #171717); font: inherit; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
|
||||
.directory-query-suggestions button:hover, .directory-query-suggestions button:focus { background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
|
||||
.directory-query-actions { display: flex; grid-column: 1 / -1; flex-wrap: wrap; align-items: flex-start; justify-content: flex-end; gap: 0.5rem; padding-top: 0.125rem; }
|
||||
.directory-query-actions button { height: 2.1rem; padding: 0 0.8rem; border: 1px solid var(--vip-content-accent, #b9954f); border-radius: 0.375rem; background: #fff; color: var(--vip-content-accent-dark, #856632); cursor: pointer; }
|
||||
.directory-query-actions .is-primary { background: var(--vip-content-accent-dark, #856632); color: #fff; }
|
||||
.directory-query-picker, .directory-query-export { position: relative; }
|
||||
.directory-query-picker summary, .directory-query-export summary { display: flex; height: 2.1rem; align-items: center; padding: 0 0.8rem; border: 1px solid var(--vip-content-accent, #b9954f); border-radius: 0.375rem; background: #fff; color: var(--vip-content-accent-dark, #856632); cursor: pointer; list-style: none; }
|
||||
.directory-query-export summary { gap: 0.35rem; }
|
||||
.directory-query-export summary .material-icons-outlined { font-size: 1rem; }
|
||||
.directory-query-picker summary::-webkit-details-marker, .directory-query-export summary::-webkit-details-marker { display: none; }
|
||||
.directory-query-picker__panel, .directory-query-export__panel { position: absolute; top: calc(100% + 0.35rem); right: 0; z-index: 8; display: grid; grid-template-columns: repeat(2, minmax(10rem, 1fr)); gap: 0.35rem 0.75rem; width: max-content; min-width: 24rem; max-width: min(34rem, calc(100vw - 2rem)); max-height: 22rem; overflow: auto; padding: 0.75rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.5rem; background: #fff; box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10); }
|
||||
.directory-query-picker__hint { grid-column: 1 / -1; color: var(--vip-content-muted, #595959); font-size: 0.6875rem; }
|
||||
.directory-query-picker__option-form { min-width: 0; }
|
||||
.directory-query-picker__option { display: flex; align-items: center; gap: 0.4rem; width: 100%; min-width: 0; padding: 0.25rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--vip-content-ink, #171717); font: inherit; font-size: 0.75rem; text-align: left; cursor: pointer; }
|
||||
.directory-query-picker__option:hover, .directory-query-picker__option:focus { background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
|
||||
.directory-query-picker__option input { flex: 0 0 auto; width: 1rem; height: 1rem; margin: 0; accent-color: var(--vip-content-accent, #b9954f); }
|
||||
.directory-query-export__option { display: flex; align-items: center; gap: 0.4rem; min-width: 0; color: var(--vip-content-ink, #171717); font-size: 0.75rem; cursor: pointer; }
|
||||
.directory-query-export__option input { flex: 0 0 auto; width: 1rem; height: 1rem; margin: 0; accent-color: var(--vip-content-accent, #b9954f); }
|
||||
.directory-query-export__button { grid-column: 1 / -1; justify-self: end; }
|
||||
@media (max-width: 1100px) { .directory-query-bar { grid-template-columns: repeat(2, minmax(14rem, 1fr)); } }
|
||||
@media (max-width: 650px) {
|
||||
.directory-query-page-head { margin-bottom: 1rem; }
|
||||
.directory-query-bar { grid-template-columns: minmax(0, 1fr); padding: 1rem; }
|
||||
.directory-query-heading { align-items: flex-start; flex-direction: column; gap: 0.3rem; }
|
||||
.directory-query-actions { justify-content: flex-start; }
|
||||
.directory-query-actions > * { flex: 1 1 calc(50% - 0.25rem); }
|
||||
.directory-query-actions button,
|
||||
.directory-query-picker summary,
|
||||
.directory-query-export summary { justify-content: center; width: 100%; }
|
||||
.directory-query-picker__panel, .directory-query-export__panel { right: auto; left: 0; grid-template-columns: minmax(0, 1fr); width: min(18rem, calc(100vw - 2rem)); min-width: 0; }
|
||||
}
|
||||
|
||||
.directory-table-tools > [data-directory-search] {
|
||||
flex: 1 1 16rem;
|
||||
min-width: 12rem;
|
||||
@ -193,6 +291,7 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.directory-field-picker__option label { display: block; flex: 1 1 auto; min-width: 0; cursor: pointer; }
|
||||
.directory-table [data-column-key="sales_01"],
|
||||
.directory-table [data-column-key="sales_02"],
|
||||
.directory-table [data-column-key="sales_03"],
|
||||
@ -216,6 +315,18 @@
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.directory-table-page.has-field-query .directory-table-scroll {
|
||||
margin-top: 1.25rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
background: var(--vip-surface, #fff);
|
||||
}
|
||||
|
||||
.directory-table-page.has-field-query .directory-table-pagination {
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-top: 0;
|
||||
background: var(--vip-surface, #fff);
|
||||
}
|
||||
|
||||
.directory-table {
|
||||
width: 100%;
|
||||
min-width: 68rem;
|
||||
@ -257,6 +368,10 @@
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 0 var(--vip-border, #d8d2c7);
|
||||
}
|
||||
.directory-sort-link { display: inline-flex; align-items: center; gap: 0.25rem; color: inherit; text-decoration: none; }
|
||||
.directory-sort-link:hover { color: var(--vip-content-accent-dark, #856632); }
|
||||
.directory-sort-icon { width: 1rem; height: 1rem; color: var(--vip-content-muted, #595959); font-size: 1rem; line-height: 1; }
|
||||
.directory-sort-link[aria-current="true"] .directory-sort-icon { color: var(--vip-content-accent-dark, #856632); }
|
||||
|
||||
.directory-table tbody tr:hover {
|
||||
background: #fbf8f0;
|
||||
@ -348,7 +463,7 @@
|
||||
</style>
|
||||
|
||||
<section
|
||||
class="directory-table-page"
|
||||
class="directory-table-page {{ $directoryUsesFieldQuery ? 'has-field-query' : '' }}"
|
||||
data-directory-page="{{ $page['slug'] ?? '' }}"
|
||||
data-directory-last-page="{{ $directoryLastPage }}"
|
||||
data-directory-user="{{ auth()->id() ?? 'guest' }}"
|
||||
@ -356,6 +471,21 @@
|
||||
data-directory-tab="{{ $activeTab['slug'] ?? '' }}"
|
||||
aria-label="{{ $page['title'] ?? $section['title'] }}"
|
||||
>
|
||||
@if ($directoryUsesFieldQuery)
|
||||
<header class="directory-query-page-head">
|
||||
<div>
|
||||
<strong>{{ $page['title'] ?? $section['title'] }}</strong>
|
||||
<p>{{ $directoryQueryPageDescriptions[$directoryPageKey] ?? '基础资料的查询与导出' }}</p>
|
||||
</div>
|
||||
<span class="sr-only" data-directory-status>{{ $directory['status'] ?? '尚未同步' }}</span>
|
||||
</header>
|
||||
@if (!empty($upstreamError))
|
||||
<div class="directory-table-notice" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $upstreamError }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<header class="directory-table-head">
|
||||
<div class="directory-table-title">
|
||||
<span class="material-icons-outlined" aria-hidden="true">{{ $directory['icon'] ?? 'table_view' }}</span>
|
||||
@ -400,8 +530,140 @@
|
||||
<span>{{ $directory['empty'] ?? '数据尚未同步,当前只展示已确认的字段结构。' }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if (!empty($directoryFields))
|
||||
@php
|
||||
$directoryQueryFields = $directoryPageKey === 'company-management/basic-information'
|
||||
? [
|
||||
['name' => 'unified_social_credit_code', 'label' => '统一社会信用代码', 'placeholder' => '输入完整或部分代码'],
|
||||
['name' => 'company_name', 'label' => '公司名称', 'placeholder' => '输入公司名称关键词'],
|
||||
['name' => 'taxpayer_type', 'label' => '纳税人类型'],
|
||||
['name' => 'company_status', 'label' => '公司状态'],
|
||||
['name' => 'legal_representative', 'label' => '法人', 'placeholder' => '按姓名查询'],
|
||||
['name' => 'city', 'label' => '城市', 'placeholder' => '城市较多,使用搜索'],
|
||||
['name' => 'related_people', 'label' => '相关人员', 'placeholder' => '输入人员姓名,模糊匹配'],
|
||||
['name' => 'company_type', 'label' => '公司类型'],
|
||||
]
|
||||
: $directoryFields;
|
||||
$directoryExcludedQueryFields = match ($directoryPageKey) {
|
||||
'store-management/basic-information' => ['opened_at', 'renewed_at'],
|
||||
'trademark-management/information' => ['registration_date', 'expiry_date'],
|
||||
default => [],
|
||||
};
|
||||
if (!empty($directoryExcludedQueryFields)) {
|
||||
$directoryQueryFields = collect($directoryQueryFields)
|
||||
->reject(fn (array $field): bool => in_array(
|
||||
$field['name'],
|
||||
$directoryExcludedQueryFields,
|
||||
true,
|
||||
))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
$directorySelectFields = match (($section['slug'] ?? '') . '/' . ($page['slug'] ?? '')) {
|
||||
'company-management/basic-information' => ['taxpayer_type', 'company_status', 'company_type'],
|
||||
'store-management/basic-information' => ['platform', 'store_status', 'withdrawal_account'],
|
||||
'trademark-management/information' => ['trademark_status'],
|
||||
'store-management/store-accounts' => ['account_type', 'password_configured'],
|
||||
default => [],
|
||||
};
|
||||
$directoryColumnNames = collect($directoryFields)->pluck('name')->all();
|
||||
$requestedDirectoryColumns = array_values(array_intersect(
|
||||
array_filter(explode(',', (string) request('columns', ''))),
|
||||
$directoryColumnNames,
|
||||
));
|
||||
$visibleDirectoryColumns = request()->has('columns') ? $requestedDirectoryColumns : $directoryColumnNames;
|
||||
$directoryQueryFormId = 'directory-query-form-' . ($page['slug'] ?? 'page');
|
||||
@endphp
|
||||
@if ($directoryUsesFieldQuery)
|
||||
<div class="directory-query-bar" data-directory-query aria-label="{{ $page['title'] ?? '' }}查询条件">
|
||||
<div class="directory-query-heading">
|
||||
<strong>查询条件</strong>
|
||||
<span>类型较少的字段直接选择,内容较多的字段支持关键词搜索</span>
|
||||
</div>
|
||||
<form id="{{ $directoryQueryFormId }}" method="GET" action="{{ request()->url() }}"></form>
|
||||
@if (request()->has('columns'))
|
||||
<input type="hidden" form="{{ $directoryQueryFormId }}" name="columns" value="{{ implode(',', $visibleDirectoryColumns) }}">
|
||||
@endif
|
||||
@foreach ($directoryQueryFields as $field)
|
||||
@php $kind = $field['kind'] ?? 'text'; @endphp
|
||||
@continue($kind === 'secret' && $field['name'] !== 'withdrawal_account')
|
||||
@continue(($page['slug'] ?? '') === 'store-accounts' && in_array($field['name'], [
|
||||
'account',
|
||||
'password_configured',
|
||||
'bound_phone',
|
||||
'verification_contact',
|
||||
'account_group_name',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
], true))
|
||||
<label class="directory-query-field" data-directory-query-field="{{ $field['name'] }}">
|
||||
<span title="{{ $field['label'] }}">{{ $field['label'] }}</span>
|
||||
@if ($kind === 'date')
|
||||
<span class="directory-query-dates">
|
||||
<input type="date" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}_from" value="{{ request($field['name'] . '_from') }}" data-directory-query-from="{{ $field['name'] }}" aria-label="{{ $field['label'] }}开始">
|
||||
<em>至</em>
|
||||
<input type="date" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}_to" value="{{ request($field['name'] . '_to') }}" data-directory-query-to="{{ $field['name'] }}" aria-label="{{ $field['label'] }}结束">
|
||||
</span>
|
||||
@elseif (in_array($field['name'], $directorySelectFields, true))
|
||||
<select form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}" data-directory-query-select="{{ $field['name'] }}" aria-label="{{ $field['label'] }}">
|
||||
<option value="">全部</option>
|
||||
@php
|
||||
$enumOptions = match ($field['name']) {
|
||||
'platform' => ['淘宝', '淘宝分销', '阿里巴巴', '天猫', '唯品会', '京东', '京喜', '抖音', '拼多多', '淘工厂', '得物'],
|
||||
'store_status' => ['正常', '关闭中', '已关闭'],
|
||||
'withdrawal_account' => ['已配置', '未配置'],
|
||||
'trademark_status' => ['未启用', '有效'],
|
||||
'account_type' => ['主账号', '子账号'],
|
||||
'password_configured' => ['已设置', '未设置', '状态未同步'],
|
||||
'taxpayer_type' => ['一般纳税人', '小规模纳税人', '个体工商户', '无税务登记', 'CORP'],
|
||||
'company_status' => ['正常', '已注销'],
|
||||
'company_type' => ['个体工商户', '个体户', '二级公司', '人力资源公司', '仓储公司', '其他', '合同公司', '品牌公司', '店铺-国标公司', '店铺-国标商标', '店铺-大店公司', '店铺-普通公司', '母公司', '资产型'],
|
||||
default => [],
|
||||
};
|
||||
@endphp
|
||||
@foreach ($enumOptions as $option)
|
||||
<option value="{{ $option }}" @selected((string) request($field['name']) === $option)>{{ $option }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input type="search" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}" value="{{ request($field['name']) }}" autocomplete="off" placeholder="{{ $field['placeholder'] ?? ('搜索' . $field['label']) }}" data-directory-query-input="{{ $field['name'] }}" aria-label="搜索{{ $field['label'] }}">
|
||||
<ul class="directory-query-suggestions" data-directory-query-suggestions="{{ $field['name'] }}" hidden></ul>
|
||||
@endif
|
||||
</label>
|
||||
@endforeach
|
||||
<div class="directory-query-actions">
|
||||
<details class="directory-query-picker" data-directory-query-picker>
|
||||
<summary>勾选字段</summary>
|
||||
<div class="directory-query-picker__panel" role="group" aria-label="勾选展示字段">
|
||||
<span class="directory-query-picker__hint">勾选需要显示的表头,可重复调整。</span>
|
||||
@foreach ($directoryFields as $field)
|
||||
<label class="directory-query-picker__option">
|
||||
<input type="checkbox" value="{{ $field['name'] }}" data-directory-query-column @checked(in_array($field['name'], $visibleDirectoryColumns, true))>
|
||||
<span>{{ $field['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
<button type="button" data-directory-query-reset>重置</button>
|
||||
<button type="submit" form="{{ $directoryQueryFormId }}" class="is-primary">查询</button>
|
||||
<details class="directory-query-export" data-directory-query-export-picker>
|
||||
<summary><span class="material-icons-outlined" aria-hidden="true">file_download</span>导出 Excel</summary>
|
||||
<div class="directory-query-export__panel" role="group" aria-label="选择导出字段">
|
||||
<span class="directory-query-picker__hint">默认全选,可取消不需要导出的表头。</span>
|
||||
@foreach ($directoryFields as $field)
|
||||
<label class="directory-query-export__option">
|
||||
<input type="checkbox" value="{{ $field['name'] }}" checked data-directory-query-export-field>
|
||||
<span>{{ $field['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
<button type="button" class="directory-query-export__button" data-directory-query-export>导出选中字段</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if (($page['slug'] ?? '') === 'store-report')
|
||||
<form method="GET" action="{{ request()->url() }}" class="installation-filter-bar" aria-label="店铺维度报表筛选查询栏">
|
||||
@php
|
||||
@ -484,6 +746,7 @@
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
@if (!in_array(($section['slug'] ?? '') . '/' . ($page['slug'] ?? ''), ['company-management/basic-information', 'store-management/basic-information', 'trademark-management/information', 'store-management/store-accounts'], true))
|
||||
<div class="directory-table-tools" data-directory-tools>
|
||||
<label class="sr-only" for="directory-search-{{ $page['slug'] ?? 'page' }}">搜索当前表格</label>
|
||||
<input id="directory-search-{{ $page['slug'] ?? 'page' }}" type="search" placeholder="搜索当前页数据..." data-directory-search>
|
||||
@ -508,13 +771,28 @@
|
||||
<button type="button" data-directory-reset>恢复默认</button>
|
||||
<button type="button" data-directory-template>下载表头模板</button>
|
||||
</div>
|
||||
@endif
|
||||
<div class="directory-table-scroll">
|
||||
<table class="directory-table">
|
||||
<table class="directory-table" @if($directoryUsesFieldQuery) data-columns-ready="true" @endif>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
@foreach ($directoryFields as $field)
|
||||
<th scope="col" data-column-key="{{ $field['name'] }}" class="{{ in_array($field['kind'] ?? null, ['number', 'integer'], true) ? 'is-number' : '' }}">{{ $field['label'] }}</th>
|
||||
<th scope="col" data-column-key="{{ $field['name'] }}" class="{{ in_array($field['kind'] ?? null, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}">
|
||||
@if ($directoryUsesFieldQuery)
|
||||
@php
|
||||
$isSorted = request('sort') === $field['name'];
|
||||
$direction = $isSorted ? request('direction', 'asc') : null;
|
||||
$nextDirection = $isSorted && $direction === 'asc' ? 'desc' : 'asc';
|
||||
@endphp
|
||||
<a class="directory-sort-link" href="{{ request()->fullUrlWithQuery(['sort' => $field['name'], 'direction' => $nextDirection, 'page' => 1]) }}" aria-label="按{{ $field['label'] }}{{ $nextDirection === 'asc' ? '升序' : '降序' }}排列" aria-current="{{ $isSorted ? 'true' : 'false' }}">
|
||||
<span data-directory-header-label>{{ $field['label'] }}</span>
|
||||
<span class="material-icons-outlined directory-sort-icon" aria-hidden="true">{{ !$isSorted ? 'unfold_more' : ($direction === 'asc' ? 'arrow_upward' : 'arrow_downward') }}</span>
|
||||
</a>
|
||||
@else
|
||||
{{ $field['label'] }}
|
||||
@endif
|
||||
</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
@ -544,7 +822,7 @@
|
||||
$display = in_array($value, [true, 1, '1', '是', '✔'], true) ? '是' : '否';
|
||||
}
|
||||
@endphp
|
||||
<td data-column-key="{{ $field['name'] }}" class="{{ in_array($kind, ['number', 'integer'], true) ? 'is-number' : '' }}">
|
||||
<td data-column-key="{{ $field['name'] }}" class="{{ in_array($kind, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}">
|
||||
@if (($page['slug'] ?? '') === 'store-accounts' && $field['name'] === 'password_configured')
|
||||
<span class="sr-only" data-password-status>{{ filled($display) ? $display : '—' }}</span>
|
||||
@if ($value === false)
|
||||
@ -620,6 +898,126 @@
|
||||
<script>
|
||||
(() => {
|
||||
const csvEscape = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||
document.querySelectorAll('[data-directory-query]').forEach((queryBar) => {
|
||||
const section = queryBar.closest('[data-directory-page]');
|
||||
const table = section?.querySelector('.directory-table');
|
||||
if (!table) return;
|
||||
const fields = [...queryBar.querySelectorAll('[data-directory-query-field]')];
|
||||
const rows = () => [...table.querySelectorAll('tbody tr:not(.is-empty)')];
|
||||
const fieldValues = (row, key) => {
|
||||
if (key === 'related_people') {
|
||||
return ['legal_representative', 'shareholders', 'supervisor', 'billing_officer']
|
||||
.map((column) => (row.querySelector(`[data-column-key="${column}"]`)?.textContent || '').trim())
|
||||
.filter((value) => value && value !== '—')
|
||||
.join(' / ');
|
||||
}
|
||||
const cell = row.querySelector(`[data-column-key="${CSS.escape(key)}"]`);
|
||||
return (cell?.textContent || '').trim();
|
||||
};
|
||||
const refreshSuggestions = (field, value) => {
|
||||
const list = field.querySelector('[data-directory-query-suggestions]');
|
||||
if (!list) return;
|
||||
const key = field.dataset.directoryQueryField;
|
||||
const needle = value.trim().toLocaleLowerCase();
|
||||
const candidates = key === 'related_people'
|
||||
? rows().flatMap((row) => ['legal_representative', 'shareholders', 'supervisor', 'billing_officer']
|
||||
.map((column) => (row.querySelector(`[data-column-key="${column}"]`)?.textContent || '').trim()))
|
||||
: rows().map((row) => fieldValues(row, key));
|
||||
const values = [...new Set(candidates.filter((item) => item && item !== '—' && (!needle || item.toLocaleLowerCase().includes(needle))))].slice(0, 8);
|
||||
list.innerHTML = values.map((item) => `<li><button type="button" data-directory-suggestion-value="${item.replaceAll('&','&').replaceAll('\"','"').replaceAll('<','<').replaceAll('>','>')}">${item.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>')}</button></li>`).join('');
|
||||
list.hidden = values.length === 0;
|
||||
list.querySelectorAll('button').forEach((button) => button.addEventListener('click', () => {
|
||||
const input = field.querySelector('[data-directory-query-input]');
|
||||
if (input) { input.value = button.dataset.directorySuggestionValue || ''; input.dispatchEvent(new Event('input', { bubbles: true })); input.focus(); }
|
||||
list.hidden = true;
|
||||
}));
|
||||
};
|
||||
fields.forEach((field) => {
|
||||
const input = field.querySelector('[data-directory-query-input]');
|
||||
const select = field.querySelector('[data-directory-query-select]');
|
||||
const key = field.dataset.directoryQueryField;
|
||||
const values = [...new Set(rows().map((row) => fieldValues(row, key)).filter((item) => item && item !== '—'))].sort((a,b) => a.localeCompare(b, 'zh-Hans-CN'));
|
||||
if (select && select.options.length <= 1) { values.forEach((value) => { const option = document.createElement('option'); option.value = value; option.textContent = value; select.appendChild(option); }); }
|
||||
input?.addEventListener('input', () => refreshSuggestions(field, input.value));
|
||||
input?.addEventListener('focus', () => refreshSuggestions(field, input.value));
|
||||
input?.addEventListener('blur', () => window.setTimeout(() => { const list = field.querySelector('[data-directory-query-suggestions]'); if (list) list.hidden = true; }, 150));
|
||||
});
|
||||
const columnChecks = [...queryBar.querySelectorAll('[data-directory-query-column]')];
|
||||
const applyVisibleColumns = () => {
|
||||
const selected = columnChecks.filter((check) => check.checked).map((check) => check.value);
|
||||
table.querySelectorAll('[data-column-key]').forEach((cell) => {
|
||||
cell.classList.toggle('is-column-hidden', !selected.includes(cell.dataset.columnKey));
|
||||
});
|
||||
let hiddenColumns = queryBar.querySelector(`input[form="${CSS.escape(queryBar.querySelector('form[id]')?.id || '')}"][name="columns"]`);
|
||||
if (!hiddenColumns) {
|
||||
hiddenColumns = document.createElement('input');
|
||||
hiddenColumns.type = 'hidden';
|
||||
hiddenColumns.name = 'columns';
|
||||
hiddenColumns.setAttribute('form', queryBar.querySelector('form[id]')?.id || '');
|
||||
queryBar.appendChild(hiddenColumns);
|
||||
}
|
||||
hiddenColumns.value = selected.join(',');
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('columns', selected.join(','));
|
||||
url.searchParams.delete('page');
|
||||
window.history.replaceState({}, '', url);
|
||||
};
|
||||
columnChecks.forEach((check) => check.addEventListener('change', applyVisibleColumns));
|
||||
queryBar.querySelector('[data-directory-query-reset]')?.addEventListener('click', () => {
|
||||
const url = new URL(window.location.href);
|
||||
fields.forEach((field) => {
|
||||
const key = field.dataset.directoryQueryField;
|
||||
url.searchParams.delete(key); url.searchParams.delete(`${key}_from`); url.searchParams.delete(`${key}_to`);
|
||||
});
|
||||
url.searchParams.delete('page');
|
||||
window.location.assign(url);
|
||||
});
|
||||
queryBar.querySelector('[data-directory-query-export]')?.addEventListener('click', async (event) => {
|
||||
const button = event.currentTarget;
|
||||
const picker = button.closest('[data-directory-query-export-picker]');
|
||||
const selected = new Set([...picker.querySelectorAll('[data-directory-query-export-field]:checked')].map((input) => input.value));
|
||||
if (selected.size === 0) {
|
||||
window.alert('请至少勾选一个导出字段');
|
||||
return;
|
||||
}
|
||||
const headerCells = [...table.tHead.rows[0].querySelectorAll('[data-column-key]')]
|
||||
.filter((cell) => selected.has(cell.dataset.columnKey));
|
||||
const header = headerCells.map((cell) => csvEscape((cell.querySelector('[data-directory-header-label]') || cell).textContent.trim())).join(',');
|
||||
const lines = [];
|
||||
const lastPage = Number(section.dataset.directoryLastPage || 1);
|
||||
button.disabled = true;
|
||||
const originalLabel = button.textContent;
|
||||
try {
|
||||
for (let page = 1; page <= lastPage; page += 1) {
|
||||
button.textContent = `导出中 ${page}/${lastPage}`;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('page', String(page));
|
||||
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if (!response.ok) throw new Error(`第 ${page} 页读取失败`);
|
||||
const parsed = new DOMParser().parseFromString(await response.text(), 'text/html');
|
||||
parsed.querySelectorAll('.directory-table tbody tr:not(.is-empty)').forEach((row) => {
|
||||
const values = headerCells.map((headerCell) => {
|
||||
const cell = row.querySelector(`[data-column-key="${CSS.escape(headerCell.dataset.columnKey)}"]`);
|
||||
return csvEscape((cell?.querySelector('[data-password-status]') || cell)?.textContent?.trim() || '');
|
||||
});
|
||||
lines.push(values.join(','));
|
||||
});
|
||||
}
|
||||
const blob = new Blob([`\ufeff${[header, ...lines].join('\n')}`], { type: 'text/csv;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `${section.dataset.directoryPage || 'directory'}-export.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
picker.open = false;
|
||||
} catch (error) {
|
||||
window.alert(error.message || '导出失败,请稍后重试');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = originalLabel;
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-directory-tools]').forEach((tools) => {
|
||||
const section = tools.closest('[data-directory-page]');
|
||||
const table = section?.querySelector('.directory-table');
|
||||
|
||||
@ -44,6 +44,10 @@
|
||||
'title' => '主体维度经营报表', 'description' => '按公司主体汇总店铺经营表现',
|
||||
'columns' => ['市', '主体', '公司类型', 'ERP系统店铺名称', '店铺所属利润中心', '店铺负责人', '部门负责人', '开店时间', '状态', ...$recentCompletedMonthSalesColumns, '连续12个月店铺累计销量', '连续12个月主体累计销量', '能否变更主体', '规划', '新主体(变更流程同步新主体)'],
|
||||
],
|
||||
'company-cost-arrangement' => [
|
||||
'title' => '公司成本安排', 'description' => '按公司主体查看成本安排',
|
||||
'columns' => ['公司', '公司类型', '发票类型', '当月收入预测', '往期成本缺口', '供应商成本安排', '快递成本安排', '当月税金预测', '人工成本预测', '其他费用预测'],
|
||||
],
|
||||
'trademark-progress' => [
|
||||
'title' => '商标进度表', 'description' => '跟踪商标续展与变更业务',
|
||||
'columns' => ['商标名称', '商标号', '业务', '进度', '是否完成', '完成时间', '办理人'],
|
||||
|
||||
@ -149,6 +149,7 @@
|
||||
]],
|
||||
['title' => '经营报表', 'items' => [
|
||||
['company-management', 'subject-operating-report', '主体维度经营报表', 'domain'],
|
||||
['company-management', 'company-cost-arrangement', '公司成本安排', 'account_balance_wallet'],
|
||||
['store-management', 'store-report', '店铺维度报表', 'storefront'],
|
||||
['store-management', 'internal-flow-report', '内部流转报表', 'swap_horiz'],
|
||||
['company-management', 'personnel-inventory', '人员盘点表', 'groups'],
|
||||
|
||||
@ -294,11 +294,13 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
'shareholders' => null,
|
||||
]);
|
||||
|
||||
$this->loginAs()
|
||||
$response = $this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'company-management',
|
||||
'page' => 'basic-information',
|
||||
]))
|
||||
]));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSeeText('公司基础信息')
|
||||
->assertSeeText('认缴注册资本(万元)')
|
||||
@ -307,6 +309,13 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertSeeText('已配置')
|
||||
->assertSeeText('测试上游公司')
|
||||
->assertSeeText('既有只读上游')
|
||||
->assertSee('aria-label="公司基础信息查询条件"', false)
|
||||
->assertSeeText('相关人员')
|
||||
->assertSeeText('勾选字段')
|
||||
->assertSeeText('导出 Excel')
|
||||
->assertSee('data-directory-query-export-field', false)
|
||||
->assertDontSee('<div class="directory-table-tools" data-directory-tools>', false)
|
||||
->assertDontSee('name="registered_at"', false)
|
||||
->assertDontSeeText('导入')
|
||||
->assertDontSeeText('新增');
|
||||
}
|
||||
@ -339,6 +348,11 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertSeeText('店铺名称')
|
||||
->assertSeeText('店铺主体')
|
||||
->assertSeeText('业务部门')
|
||||
->assertSee('data-column-key="opened_at"', false)
|
||||
->assertSee('data-column-key="renewed_at"', false)
|
||||
->assertDontSee('name="opened_at_from"', false)
|
||||
->assertDontSee('name="opened_at_to"', false)
|
||||
->assertDontSee('name="renewed_at"', false)
|
||||
->assertSeeText('测试上游店铺')
|
||||
->assertSeeText('测试关联公司')
|
||||
->assertSeeText('既有只读上游')
|
||||
@ -414,6 +428,12 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertOk()
|
||||
->assertSeeText('商标名称')
|
||||
->assertSeeText('注册时间')
|
||||
->assertSee('data-column-key="registration_date"', false)
|
||||
->assertSee('data-column-key="expiry_date"', false)
|
||||
->assertDontSee('name="registration_date_from"', false)
|
||||
->assertDontSee('name="registration_date_to"', false)
|
||||
->assertDontSee('name="expiry_date_from"', false)
|
||||
->assertDontSee('name="expiry_date_to"', false)
|
||||
->assertSeeText('测试上游商标')
|
||||
->assertSeeText('既有只读上游')
|
||||
->assertDontSeeText('导入')
|
||||
@ -426,7 +446,7 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
$sections = collect($navigation)->keyBy('slug');
|
||||
|
||||
$this->assertSame(
|
||||
['subject-operating-report', 'basic-information', 'personnel-inventory', 'company-progress'],
|
||||
['subject-operating-report', 'company-cost-arrangement', 'basic-information', 'personnel-inventory', 'company-progress'],
|
||||
array_column($sections['company-management']['pages'], 'slug')
|
||||
);
|
||||
$this->assertSame(
|
||||
@ -512,6 +532,7 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
|
||||
$expectedHeaders = [
|
||||
['section' => 'company-management', 'page' => 'subject-operating-report', 'headers' => ['市', '主体', '最近12个月累计收入', '1月收入', '12月收入']],
|
||||
['section' => 'company-management', 'page' => 'company-cost-arrangement', 'headers' => ['公司', '公司类型', '发票类型', '当月收入预测', '往期成本缺口', '供应商成本安排', '快递成本安排', '当月税金预测', '人工成本预测', '其他费用预测']],
|
||||
['section' => 'company-management', 'page' => 'personnel-inventory', 'headers' => ['人员名称', '税务局密码', '可担任公司数量']],
|
||||
['section' => 'company-management', 'page' => 'company-progress', 'headers' => ['公司进度表字段尚未定义']],
|
||||
['section' => 'trademark-management', 'page' => 'trademark-progress', 'headers' => ['商标号', '是否完成', '办理人']],
|
||||
|
||||
@ -58,6 +58,12 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
]);
|
||||
$this->markReady();
|
||||
$connection->table('ops_fys_company_info')->where('Id', 1)->update(['registeredCapitalVaildTime' => '2027-01-01']);
|
||||
$connection->table('ops_fys_company_info')->where('Id', 2)->update([
|
||||
'businessLicenseType' => '店铺-普通公司',
|
||||
'taxCategoryAndType' => '小规模纳税人',
|
||||
'legalRepresentative' => '张三',
|
||||
'registeredAddress' => '上海市浦东新区测试路1号',
|
||||
]);
|
||||
$connection->table('ops_fys_shop_info')->where('Id', 8)->update(['sotreId' => 'STORE-8', 'renewalTime' => '2027-08-14', 'shopBankAccount' => 'fixture-account', 'trademarkNumber' => 'TM-5']);
|
||||
$connection->table('ops_fys_trademark_info')->where('Id', 5)->update(['trademarkNumber' => 'TM-5', 'approvedCommodities' => '家具', 'transferenceUser' => '测试持有人', 'registrationAddress' => '测试地址', 'custodyDepartment' => '资料部']);
|
||||
|
||||
@ -69,6 +75,18 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
'store_name' => '上游店铺名',
|
||||
'store_id' => 'STORE-8',
|
||||
]);
|
||||
$filteredStore = $this->repository->paginate('store-information.read', 1, 20, [
|
||||
'platform' => '天猫', 'store_status' => '0', 'opened_at_from' => '2026-08-14',
|
||||
]);
|
||||
$filteredTrademark = $this->repository->paginate('trademark-information.read', 1, 20, [
|
||||
'trademark_status' => '1', 'registration_date_from' => '2020-01-01',
|
||||
]);
|
||||
$filteredCompany = $this->repository->paginate('company-information.read', 1, 20, ['company_name' => '新公司']);
|
||||
$relatedCompany = $this->repository->paginate('company-information.read', 1, 20, ['related_people' => '张']);
|
||||
$typedCompany = $this->repository->paginate('company-information.read', 1, 20, [
|
||||
'taxpayer_type' => '小规模纳税人', 'company_status' => '正常', 'company_type' => '店铺-普通公司',
|
||||
]);
|
||||
$cityCompany = $this->repository->paginate('company-information.read', 1, 20, ['city' => '上海']);
|
||||
|
||||
$this->assertSame(3, $companies['total']);
|
||||
$this->assertSame(1, $companies['data'][0]['id']);
|
||||
@ -90,6 +108,12 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame('TM-5', $trademarks['data'][0]['trademark_number']);
|
||||
$this->assertSame('家具', $trademarks['data'][0]['approved_goods']);
|
||||
$this->assertSame('测试持有人', $trademarks['data'][0]['final_holder']);
|
||||
$this->assertSame(1, $filteredStore['total']);
|
||||
$this->assertSame(1, $filteredTrademark['total']);
|
||||
$this->assertSame('新公司', $filteredCompany['data'][0]['company_name']);
|
||||
$this->assertSame('新公司', $relatedCompany['data'][0]['company_name']);
|
||||
$this->assertSame(1, $typedCompany['total']);
|
||||
$this->assertSame(1, $cityCompany['total']);
|
||||
}
|
||||
|
||||
public function test_dataset_is_not_ready_until_initial_full_sync_is_marked_complete(): void
|
||||
|
||||
@ -64,7 +64,7 @@ class StoreAccountMirrorRepositoryTest extends TestCase
|
||||
foreach (['MAIN' => '主账号', 'SUB' => '子账号', '/' => null] as $type => $expected) {
|
||||
DB::connection('business_mirror')->table('store_accounts')->insert([
|
||||
'id' => 'account-' . md5($type),
|
||||
'storeName' => '测试店铺',
|
||||
'storeName' => ['MAIN' => 'B店铺', 'SUB' => 'A店铺', '/' => 'C店铺'][$type],
|
||||
'accountType' => $type,
|
||||
'account' => 'test-account',
|
||||
'passwordConfigured' => $type === 'MAIN' ? true : ($type === 'SUB' ? false : null),
|
||||
@ -79,5 +79,8 @@ class StoreAccountMirrorRepositoryTest extends TestCase
|
||||
$this->assertTrue($rows['account-' . md5('MAIN')]['password_configured']);
|
||||
$this->assertFalse($rows['account-' . md5('SUB')]['password_configured']);
|
||||
$this->assertNull($rows['account-' . md5('/')]['password_configured']);
|
||||
|
||||
$sorted = $this->repository->paginate(1, 50, ['_sort' => 'store_name', '_direction' => 'asc']);
|
||||
$this->assertSame(['A店铺', 'B店铺', 'C店铺'], array_column($sorted['data'], 'store_name'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
};
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->once()->with($scope)->andReturnTrue();
|
||||
$mirror->shouldReceive('paginate')->once()->with($scope, 2, 25)->andReturn([
|
||||
$mirror->shouldReceive('paginate')->once()->with($scope, 2, 25, [])->andReturn([
|
||||
'data' => [['id' => 10]],
|
||||
'total' => 51,
|
||||
]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user