Просмотр исходного кода

style(admin): refine store detail layout and month picker

彭峰 1 месяц назад
Родитель
Сommit
cfc2873890

+ 1 - 1
projects/xiaoshu-admin/src/app/pages/operations-store-detail.component.html

@@ -5,7 +5,7 @@
     <span>集中查看门店归属会员、陪练、排课、课程履约和陪练工资成本</span>
   </div>
   <div class="header-actions">
-    <label class="month-control"><span>经营月份</span><input type="month" [ngModel]="month()" (ngModelChange)="changeMonth($event)" [disabled]="loading()"/></label>
+    <div class="month-control"><span>经营月份</span><button class="month-picker-trigger" type="button" aria-label="选择经营月份" aria-haspopup="dialog" [attr.aria-expanded]="monthPickerOpen()" [class.active]="monthPickerOpen()" (click)="toggleMonthPicker($event)" [disabled]="loading()"><lucide-icon [img]="icons.CalendarDays" [size]="17"/><strong>{{monthLabel()}}</strong><lucide-icon class="picker-chevron" [img]="icons.ChevronDown" [size]="16"/></button>@if(monthPickerOpen()){<div class="month-calendar-popover" role="dialog" aria-label="选择经营月份" (click)="$event.stopPropagation()"><header><button type="button" aria-label="上一年" (click)="shiftMonthPickerYear(-1,$event)"><lucide-icon [img]="icons.ChevronLeft" [size]="17"/></button><strong>{{monthPickerYear()}}年</strong><button type="button" aria-label="下一年" (click)="shiftMonthPickerYear(1,$event)"><lucide-icon [img]="icons.ChevronRight" [size]="17"/></button></header><div class="month-grid">@for(name of monthNames;track $index){<button type="button" [class.selected]="month()===monthOptionValue($index)" [attr.aria-pressed]="month()===monthOptionValue($index)" (click)="selectMonth($index)">{{name}}</button>}</div><footer><button type="button" (click)="selectCurrentMonth()">回到本月</button></footer></div>}</div>
     <button class="ui-button secondary" type="button" (click)="load(true)" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="loading()"/>{{loading()?'读取中…':'刷新实时数据'}}</button>
   </div>
 </header>

+ 39 - 11
projects/xiaoshu-admin/src/app/pages/operations-store-detail.component.scss

@@ -1,11 +1,28 @@
 :host { display: block; color: #24382f; }
-.detail-header { display: flex; align-items: end; justify-content: space-between; gap: 24px; }
-.back-link { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 17px; color: #557068; font-size: 13px; font-weight: 750; text-decoration: none; }
-.month-control { display: grid; gap: 6px; color: #63746d; font-size: 11px; font-weight: 800; }
-.month-control input { min-width: 166px; min-height: 42px; padding: 8px 11px; border: 1px solid #d6e2dd; border-radius: 12px; color: #263b33; background: #fff; font-weight: 750; }
+.detail-header { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: start; gap: 20px 28px; margin-bottom: 18px; }
+.detail-header > div:first-child { min-width: 0; }
+.detail-header .header-actions { display: flex; align-items: end; gap: 10px; padding-top: 20px; }
+.detail-header .header-actions > .ui-button { min-height: 46px; }
+.month-control { position: relative; display: grid; gap: 6px; min-width: 190px; color: #63746d; font-size: 11px; font-weight: 800; }
+.month-picker-trigger { display: grid; grid-template-columns: auto minmax(0,1fr) auto; width: 100%; height: 46px; align-items: center; gap: 8px; padding: 9px 11px; border: 1px solid #d7e1dd; border-radius: 12px; color: #486058; background: #fff; text-align: left; cursor: pointer; }
+.month-picker-trigger strong { overflow: hidden; color: #2a3c36; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
+.month-picker-trigger:hover,.month-picker-trigger.active,.month-picker-trigger:focus-visible { border-color: #7fae9f; outline: 0; background: #fbfefd; box-shadow: 0 0 0 3px rgba(31,128,102,.11); }
+.month-picker-trigger:disabled { cursor: wait; opacity: .65; }
+.picker-chevron { color: #657972; }
+.month-calendar-popover { position: absolute; z-index: 85; top: calc(100% + 9px); right: 0; width: 310px; padding: 14px; border: 1px solid #d9e5e0; border-radius: 16px; background: #fff; box-shadow: 0 20px 55px rgba(24,62,49,.2); }
+.month-calendar-popover header { display: grid; grid-template-columns: 34px 1fr 34px; align-items: center; margin-bottom: 12px; }
+.month-calendar-popover header strong { color: #263b34; font-size: 14px; text-align: center; }
+.month-calendar-popover button { border: 0; border-radius: 9px; color: #415a51; background: transparent; cursor: pointer; }
+.month-calendar-popover button:hover,.month-calendar-popover button:focus-visible { outline: 0; color: #176c55; background: #edf7f3; }
+.month-calendar-popover header button { display: grid; width: 34px; height: 34px; place-items: center; }
+.month-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 7px; }
+.month-grid button { min-height: 39px; font-size: 13px; font-weight: 700; }
+.month-grid button.selected { color: #fff; background: #1f8066; box-shadow: 0 6px 14px rgba(31,128,102,.2); }
+.month-calendar-popover footer { display: flex; justify-content: flex-end; margin-top: 10px; padding-top: 10px; border-top: 1px solid #e6eeeb; }
+.month-calendar-popover footer button { min-height: 34px; padding: 0 10px; font-weight: 700; }
 .loading-card { display: grid; min-height: 260px; place-items: center; align-content: center; gap: 10px; border: 1px solid #dfe8e4; border-radius: 20px; color: #6e8079; background: #fff; }
 .loading-card strong { color: #2b443a; font-size: 17px; }
-.source-strip { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 16px; padding: 15px 18px; border: 1px solid #d7e7e0; border-radius: 16px; background: linear-gradient(100deg,#edf8f4,#f9fcfb); }
+.source-strip { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 14px; padding: 12px 16px; border: 1px solid #d7e7e0; border-radius: 14px; background: linear-gradient(100deg,#edf8f4,#f9fcfb); }
 .source-strip strong,.source-strip span,.source-strip small { display: block; }
 .source-strip strong { font-size: 16px; }
 .source-strip span,.source-strip small { margin-top: 3px; color: #6e8079; font-size: 12px; }
@@ -19,8 +36,12 @@
 .store-profile dl div { min-width: 0; padding: 11px 13px; border-radius: 12px; background: #f5f9f7; }
 .store-profile dt { color: #7a8c84; font-size: 11px; }
 .store-profile dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; }
-.summary-grid { grid-template-columns: repeat(4,minmax(0,1fr)); }
-.summary-grid article { position: relative; overflow: hidden; }
+.summary-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; margin-bottom: 18px; }
+.summary-grid article { position: relative; display: grid; min-width: 0; min-height: 112px; align-content: center; gap: 5px; overflow: hidden; padding: 18px 20px; border: 1px solid #dce7e3; border-radius: 16px; background: #fff; box-shadow: 0 8px 24px rgba(28,65,51,.045); }
+.summary-grid article > span,.summary-grid article > small { color: #74877f; }
+.summary-grid article > span { font-size: 12px; font-weight: 750; }
+.summary-grid article > strong { color: #20372e; font-size: 27px; line-height: 1.15; }
+.summary-grid article > small { font-size: 11px; }
 .summary-grid article::after { position: absolute; top: 0; right: 0; width: 46px; height: 4px; border-radius: 0 0 0 5px; background: #8cc6b3; content: ''; }
 .summary-grid .pay-card::after { background: #d79b3a; }
 .summary-grid .pay-card strong { color: #a55d10; }
@@ -28,8 +49,8 @@
 .commission-note span,.commission-note strong { display: block; }
 .commission-note strong { margin-bottom: 2px; color: #64481a; }
 .detail-card { min-height: 360px; }
-.detail-tabs { display: flex; gap: 4px; overflow-x: auto; padding: 10px 14px 0; border-bottom: 1px solid #e5ece9; }
-.detail-tabs button { display: inline-flex; min-height: 44px; align-items: center; gap: 7px; padding: 9px 13px; border: 0; border-bottom: 3px solid transparent; color: #61736b; background: transparent; font-weight: 750; white-space: nowrap; }
+.detail-tabs { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 4px; padding: 8px 10px 0; border-bottom: 1px solid #e5ece9; }
+.detail-tabs button { display: inline-flex; min-width: 0; min-height: 46px; align-items: center; justify-content: center; gap: 7px; padding: 9px 13px; border: 0; border-bottom: 3px solid transparent; color: #61736b; background: transparent; font-weight: 750; white-space: nowrap; }
 .detail-tabs button.active { border-color: #177456; color: #176d52; }
 .detail-tabs em { min-width: 23px; padding: 2px 6px; border-radius: 999px; color: #71827b; background: #edf3f0; font-size: 10px; font-style: normal; }
 .detail-tabs button.active em { color: #176d52; background: #e4f3ed; }
@@ -60,15 +81,22 @@
   .summary-grid { grid-template-columns: repeat(2,minmax(0,1fr)); }
 }
 @media (max-width: 720px) {
-  .detail-header,.source-strip,.section-heading { align-items: stretch; flex-direction: column; }
+  .detail-header { grid-template-columns: 1fr; gap: 12px; }
+  .detail-header .header-actions { padding-top: 0; }
+  .source-strip,.section-heading { align-items: stretch; flex-direction: column; }
   .header-actions { display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
-  .month-control input { width: 100%; }
+  .month-control { min-width: 0; }
+  .month-calendar-popover { position: fixed; top: 50%; right: auto; left: 50%; width: min(330px,calc(100vw - 28px)); transform: translate(-50%,-50%); }
   .source-meta { text-align: left; }
   .store-profile { grid-template-columns: auto 1fr; padding: 16px; }
   .store-profile dl { grid-template-columns: 1fr; }
   .summary-grid { gap: 10px; }
   .summary-grid article { padding: 15px; }
   .inline-search { width: 100%; }
+  .detail-tabs { grid-template-columns: repeat(2,minmax(0,1fr)); padding: 7px; }
+  .detail-tabs button { border: 0; border-radius: 9px; }
+  .detail-tabs button.active { color: #fff; background: #176d52; }
+  .detail-tabs button.active em { color: #176d52; background: #fff; }
 }
 @media (max-width: 440px) {
   .header-actions { grid-template-columns: 1fr; }

+ 17 - 0
projects/xiaoshu-admin/src/app/pages/operations-store-detail.component.spec.ts

@@ -53,6 +53,23 @@ describe('OperationsStoreDetailComponent', () => {
     expect(component.data()?.month).toBe('2026-08');
   });
 
+  it('uses the unified month picker and loads the selected month', async () => {
+    const component = fixture.componentInstance;
+    component.month.set('2026-09');
+    const event = new Event('click');
+    spyOn(event, 'stopPropagation');
+    component.toggleMonthPicker(event);
+    expect(component.monthPickerOpen()).toBeTrue();
+    expect(component.monthPickerYear()).toBe(2026);
+    component.shiftMonthPickerYear(1, event);
+    component.selectMonth(0);
+    await fixture.whenStable();
+    expect(component.month()).toBe('2027-01');
+    expect(component.monthLabel()).toBe('2027年1月');
+    expect(component.monthPickerOpen()).toBeFalse();
+    expect(operations.storeDetail).toHaveBeenCalledWith('store-1', '2027-01', false);
+  });
+
   it('keeps members, coaches, schedule and course fulfillment as business tabs', () => {
     const element: HTMLElement = fixture.nativeElement;
     expect(element.textContent).toContain('归属会员');

+ 43 - 3
projects/xiaoshu-admin/src/app/pages/operations-store-detail.component.ts

@@ -1,9 +1,9 @@
 import { MemberCreditAccountComponent } from '../components/member-credit-account.component';
 import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core';
+import { ChangeDetectionStrategy, Component, HostListener, OnInit, computed, inject, signal } from '@angular/core';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute, RouterLink } from '@angular/router';
-import { ArrowLeft, Building2, CalendarDays, Clock3, GraduationCap, RefreshCw, Search, UserRound, Users, WalletCards } from 'lucide-angular';
+import { Building2, CalendarDays, ChevronDown, ChevronLeft, ChevronRight, Clock3, GraduationCap, RefreshCw, Search, UserRound, Users, WalletCards } from 'lucide-angular';
 import { LucideAngularModule } from 'lucide-angular';
 import { ScheduleAppointment, StoreDetailData } from '../admin.models';
 import { OperationsService } from '../operations.service';
@@ -25,11 +25,14 @@ export class OperationsStoreDetailComponent implements OnInit {
   readonly loading = signal(true);
   readonly error = signal('');
   readonly month = signal(CURRENT_MONTH);
+  readonly monthPickerOpen = signal(false);
+  readonly monthPickerYear = signal(Number(CURRENT_MONTH.slice(0, 4)));
+  readonly monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
   readonly tab = signal<StoreDetailTab>('members');
   readonly memberSearch = signal('');
   readonly coachSearch = signal('');
   readonly scheduleSearch = signal('');
-  readonly icons = { ArrowLeft, Building2, CalendarDays, Clock3, GraduationCap, RefreshCw, Search, UserRound, Users, WalletCards };
+  readonly icons = { Building2, CalendarDays, ChevronDown, ChevronLeft, ChevronRight, Clock3, GraduationCap, RefreshCw, Search, UserRound, Users, WalletCards };
 
   readonly members = computed(() => {
     const keyword = this.memberSearch().trim().toLowerCase();
@@ -54,6 +57,43 @@ export class OperationsStoreDetailComponent implements OnInit {
 
   ngOnInit(): void { void this.load(); }
 
+  @HostListener('document:click')
+  closeMonthPicker(): void { this.monthPickerOpen.set(false); }
+
+  @HostListener('document:keydown.escape')
+  closeMonthPickerWithKeyboard(): void { this.closeMonthPicker(); }
+
+  toggleMonthPicker(event: Event): void {
+    event.stopPropagation();
+    if (this.monthPickerOpen()) { this.closeMonthPicker(); return; }
+    this.monthPickerYear.set(Number(this.month().slice(0, 4)) || Number(CURRENT_MONTH.slice(0, 4)));
+    this.monthPickerOpen.set(true);
+  }
+
+  shiftMonthPickerYear(delta: number, event: Event): void {
+    event.stopPropagation();
+    this.monthPickerYear.update((year) => year + delta);
+  }
+
+  selectMonth(monthIndex: number): void {
+    this.closeMonthPicker();
+    this.changeMonth(this.monthOptionValue(monthIndex));
+  }
+
+  selectCurrentMonth(): void {
+    this.closeMonthPicker();
+    this.changeMonth(CURRENT_MONTH);
+  }
+
+  monthLabel(): string {
+    const [year, month] = this.month().split('-');
+    return year && month ? `${year}年${Number(month)}月` : '选择月份';
+  }
+
+  monthOptionValue(monthIndex: number): string {
+    return `${this.monthPickerYear()}-${String(monthIndex + 1).padStart(2, '0')}`;
+  }
+
   async load(refresh = false): Promise<void> {
     this.loading.set(true);
     this.error.set('');