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

style: rebalance scheduling overview and filters

彭峰 4 дней назад
Родитель
Сommit
840ef13d4c

+ 131 - 0
projects/xiaoshu-admin/src/app/pages/operations-schedule-layout.scss

@@ -0,0 +1,131 @@
+/* Page-level layout kept in the global bundle to respect the schedule component style budget. */
+app-operations-schedule.schedule-ui {
+/* Overview first: the range count anchors the page, while the other figures stay quiet. */
+.page-header { margin-bottom: 18px; }
+.page-header p { margin: 0; color: #347a64; font-size: 12px; font-weight: 800; letter-spacing: .03em; }
+.summary-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; }
+.summary-card {
+  min-width: 0;
+  min-height: 112px;
+  align-content: space-between;
+  padding: 16px 18px;
+  border-color: #dbe8e2;
+  box-shadow: 0 6px 18px rgba(31, 67, 55, .035);
+}
+.summary-card span { color: #657b70; font-weight: 750; }
+.summary-card strong { color: #21493c; font-size: clamp(26px, 2.2vw, 34px); line-height: 1; font-variant-numeric: tabular-nums; }
+.summary-card small { min-height: 16px; line-height: 1.35; }
+.summary-card-primary {
+  position: relative;
+  isolation: isolate;
+  overflow: hidden;
+  border-color: #176c55;
+  background: linear-gradient(118deg, #145944, #1d7860);
+  box-shadow: 0 10px 24px rgba(23, 108, 85, .16);
+}
+.summary-card-primary::after {
+  content: '';
+  position: absolute;
+  z-index: -1;
+  width: 176px;
+  height: 176px;
+  right: -48px;
+  top: -99px;
+  border: 27px solid rgba(255, 255, 255, .1);
+  border-radius: 50%;
+}
+.summary-card-primary.summary-card-primary span, .summary-card-primary.summary-card-primary strong { color: #fff; }
+.summary-card-primary.summary-card-primary small { color: #c9e5d9; }
+
+.schedule-planner {
+  margin-bottom: 14px;
+  overflow: visible;
+  border: 1px solid #dce9e2;
+  border-radius: 18px;
+  background: #fff;
+  box-shadow: 0 8px 26px rgba(31, 67, 55, .045);
+}
+.planner-heading {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 10px 18px;
+  padding: 15px 18px 0;
+}
+.planner-heading > div:first-child { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px 12px; }
+.planner-heading strong { color: #224b3e; font-size: 16px; }
+.planner-heading span, .planner-heading small { color: #74867e; font-size: 12px; }
+.planner-heading small { white-space: nowrap; font-variant-numeric: tabular-nums; }
+.schedule-planner .toolbar {
+  flex-wrap: wrap;
+  gap: 10px;
+  margin: 0;
+  padding: 14px 18px 16px;
+  border: 0;
+  border-radius: 0;
+  box-shadow: none;
+}
+.schedule-planner .toolbar > .segmented { width: 190px; }
+.schedule-planner .toolbar .date-filter { flex: 0 0 200px; width: 200px; }
+.schedule-planner .toolbar .coach-filter { flex: 0 1 300px; width: 300px; min-width: 230px; max-width: 300px; margin-left: auto; }
+.schedule-planner .schedule-filterbar {
+  flex-wrap: wrap;
+  justify-content: space-between;
+  gap: 10px 12px;
+  margin: 0;
+  padding: 14px 18px 16px;
+  border: 0;
+  border-top: 1px solid #edf2ef;
+  border-radius: 0 0 18px 18px;
+  background: #fbfdfc;
+}
+.schedule-planner .schedule-search { flex: 0 1 430px; width: min(100%, 430px); max-width: 430px; }
+.schedule-planner .status-filter { flex: 0 0 220px; }
+.schedule-planner .filter-result { margin-left: 0; }
+.schedule-planner .schedule-loading { margin: 0 18px 14px; }
+
+.schedule-workflow { margin-bottom: 14px; }
+.schedule-workflow > summary {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  min-height: 52px;
+  padding: 11px 18px;
+  cursor: pointer;
+  list-style: none;
+  background: #f7fbf9;
+}
+.schedule-workflow > summary::-webkit-details-marker { display: none; }
+.schedule-workflow > summary > span:first-child { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
+.schedule-workflow > summary strong { color: #244c3e; font-size: 14px; }
+.schedule-workflow > summary small { color: #6b8177; font-size: 12px; }
+.workflow-toggle { flex: none; color: #176c55; font-size: 12px; font-weight: 750; }
+.workflow-toggle::after { content: '⌄'; display: inline-block; margin-left: 7px; font-size: 17px; line-height: .7; }
+.schedule-workflow[open] .workflow-toggle::after { transform: rotate(180deg); }
+.schedule-workflow[open] > summary { border-bottom: 1px solid #e8efec; }
+.schedule-workflow > summary:focus-visible { outline: 3px solid rgba(31, 130, 98, .25); outline-offset: -3px; }
+
+@media (max-width: 1300px) {
+  .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (max-width: 700px) {
+  .summary-grid { gap: 10px; margin-bottom: 12px; }
+  .summary-card { min-height: 96px; padding: 14px; }
+  .summary-card:first-child { grid-column: 1 / -1; }
+  .summary-card:last-child { min-height: 82px; }
+  .summary-card:last-child strong { grid-column: auto; grid-row: auto; font-size: 26px; }
+  .summary-card:last-child span, .summary-card:last-child small { grid-column: auto; }
+  .planner-heading { align-items: flex-start; flex-wrap: wrap; padding: 13px 14px 0; }
+  .planner-heading > div:first-child { display: grid; gap: 3px; }
+  .schedule-planner .toolbar { padding: 12px 14px; }
+  .schedule-planner .toolbar > .segmented { width: 100%; }
+  .schedule-planner .toolbar .date-filter, .schedule-planner .toolbar .coach-filter { width: 100%; min-width: 0; max-width: none; margin-left: 0; }
+  .schedule-planner .schedule-filterbar { grid-template-columns: minmax(0, 1fr); padding: 12px 14px 14px; }
+  .schedule-planner .schedule-search, .schedule-planner .status-filter { width: 100%; max-width: none; min-width: 0; }
+  .schedule-planner .filter-result { margin-left: 0; }
+  .schedule-workflow > summary { align-items: flex-start; padding: 12px 14px; }
+  .schedule-workflow > summary > span:first-child { display: grid; gap: 3px; }
+}
+}

+ 14 - 15
projects/xiaoshu-admin/src/app/pages/operations-schedule.component.html

@@ -4,13 +4,22 @@
   </section>
 }
 <header class="page-header">
-  <div><p>SCHEDULING CENTER</p><h1>排课中心</h1><span>按陪练或时间快速排课,并在提交前发现冲突</span></div>
+  <div><p>课程安排</p><h1>排课中心</h1><span>查看课表、定位待处理预约,或为学员安排新课</span></div>
   <div class="header-actions">
     <button type="button" (click)="load()" [disabled]="loading()"><lucide-icon [img]="loading()?icons.LoaderCircle:icons.RefreshCw" [size]="17" [class.spin]="loading()" />{{loading()?'正在加载…':'刷新'}}</button>
     <button class="primary" type="button" (click)="openCreate()"><lucide-icon [img]="icons.Plus" [size]="17" />新增排课</button>
   </div>
 </header>
 
+<section class="summary-grid" aria-label="排课概况">
+  <article class="summary-card summary-card-primary"><span>当前范围课程</span><strong>{{data()?activeAppointments():'—'}}</strong><small>{{range().from}} 至 {{range().to}}</small></article>
+  <article class="summary-card"><span>待分配</span><strong>{{data()?unassignedCount():'—'}}</strong><small>未分配老师时不能开始</small></article>
+  <article class="summary-card"><span>在岗陪练</span><strong>{{data()?.coaches?.length??'—'}}</strong><small>未配置可用时间会提示</small></article>
+  <article class="summary-card"><span>历史排课总量</span><strong>{{data()?.totalAppointments??'—'}}</strong><small>{{data()?.dataDateFrom||'—'}} 至 {{data()?.dataDateTo||'—'}}</small></article>
+</section>
+
+<section class="schedule-planner" aria-label="课表日期与筛选">
+  <div class="planner-heading"><div><strong>查看课表</strong><span>先选日期与陪练,再按学员或状态定位预约</span></div><small>数据库更新 {{(data()?.refreshedAt|date:'MM-dd HH:mm:ss')||'—'}}</small></div>
 <section class="toolbar">
   <div class="segmented">
     <button type="button" [class.active]="view()==='day'" (click)="view.set('day');load()">日</button>
@@ -44,32 +53,22 @@
   </div>
   <div class="filter-result" role="status"><strong>{{visibleAppointments().length}}</strong><span>条筛选结果</span><small>当前范围共 {{data()?.appointments?.length||0}} 条</small></div>
 </section>
+</section>
 
 @if(message()){<p class="notice" role="status">{{message()}}</p>}
 @if(error()){<p class="notice error" role="alert">{{error()}}</p>}
 @if(data()&&!data()?.liveSourceAvailable){<p class="notice error" role="status">{{data()?.liveSourceMessage}}</p>}
 @if(data()?.source!=='database'&&data()?.sync?.state!=='synced'&&data()){<p class="notice error" role="status">旧系统持久化同步尚未追平。{{data()?.liveSourceAvailable?'本页已读取旧系统当前排课':'本页暂时无法核对旧系统当前排课'}};刷新页面不等于新数据库已同步,未核实记录不能据此认定缺课或结算工资。</p>}
 
-<section class="schedule-workflow" aria-label="预约到完课业务流程">
-  <header>
-    <div><strong>预约到完课流程</strong><span>排课、上课、结课与评价</span></div>
-    <small>老师一次结课,学生随后评价</small>
-  </header>
+<details class="schedule-workflow">
+  <summary><span><strong>预约到完课</strong><small>创建预约 → 开始上课 → 结课生成报告 → 学生评价</small></span><span class="workflow-toggle">查看流程</span></summary>
   <ol>
     <li><i>1</i><div><strong>运营创建预约</strong><span>选择学员已绑定课程、时间和陪练</span></div><em>状态 0</em></li>
     <li><i>2</i><div><strong>陪练开始上课</strong><span>预约当天均可开始,记录实际上课时间</span></div><em>上课中</em></li>
     <li><i>3</i><div><strong>老师结课并生成报告</strong><span>课堂结果、反馈与课时扣减一次完成</span></div><em>已结课</em></li>
     <li><i>4</i><div><strong>学生评价</strong><span>学生查看报告后评分;未评价不阻塞结课</span></div><em>评价独立</em></li>
   </ol>
-</section>
-
-<section class="summary-grid">
-  <article class="summary-card"><span>当前范围课程</span><strong>{{data()?activeAppointments():'—'}}</strong><small>{{range().from}} 至 {{range().to}}</small></article>
-  <article class="summary-card"><span>待分配</span><strong>{{data()?unassignedCount():'—'}}</strong><small>未分配老师时不能开始</small></article>
-  <article class="summary-card"><span>在岗陪练</span><strong>{{data()?.coaches?.length??'—'}}</strong><small>未配置可用时间会提示</small></article>
-  <article class="summary-card"><span>历史排课总量</span><strong>{{data()?.totalAppointments??'—'}}</strong><small>{{data()?.dataDateFrom||'—'}} 至 {{data()?.dataDateTo||'—'}}</small></article>
-  <article class="summary-card"><span>数据库更新</span><strong>{{data()?.refreshedAt|date:'MM-dd HH:mm:ss'}}</strong><small>新排课程、教学状态以新库为准</small></article>
-</section>
+</details>
 
 <section class="content-card" [class.is-loading]="loading()" [attr.aria-busy]="loading()">
   <header class="schedule-content-header">

+ 1 - 1
projects/xiaoshu-admin/src/app/pages/operations-schedule.component.ts

@@ -126,7 +126,7 @@ export function scheduleCoachBusy(
   });
 }
 
-@Component({ selector:'app-operations-schedule', standalone:true, imports: [LessonRecordDetailComponent,AnchoredMenuDirective, DropdownDirective, CommonModule,FormsModule,LucideAngularModule,OperationsDateFilterComponent], templateUrl:'./operations-schedule.component.html', styleUrls:['./operations-professional.scss','./operations-schedule.component.scss'], changeDetection:ChangeDetectionStrategy.OnPush })
+@Component({ selector:'app-operations-schedule', host:{class:'schedule-ui'}, standalone:true, imports: [LessonRecordDetailComponent,AnchoredMenuDirective, DropdownDirective, CommonModule,FormsModule,LucideAngularModule,OperationsDateFilterComponent], templateUrl:'./operations-schedule.component.html', styleUrls:['./operations-professional.scss','./operations-schedule.component.scss'], changeDetection:ChangeDetectionStrategy.OnPush })
 export class OperationsScheduleComponent implements OnInit, OnDestroy {
   readonly data=signal<ScheduleCalendarData|null>(null); readonly loading=signal(true); readonly saving=signal(false); readonly error=signal(''); readonly message=signal(''); readonly drawerOpen=signal(false); readonly lessonAppointmentId=signal(''); readonly feedback=signal<Record<string,unknown>|null>(null); readonly detail=signal<ScheduleAppointment|null>(null); readonly cancelReason=signal(''); readonly detailError=signal(''); readonly editing=signal<ScheduleAppointment|null>(null); readonly mode=signal<'coach'|'time'>('coach'); readonly view=signal<'day'|'week'|'list'>('day'); readonly date=signal(localDateKey(new Date())); readonly coachFilter=signal(0);
   readonly bindingId=signal(''); private readonly route=inject(ActivatedRoute); readonly studentId=signal(0); readonly courseId=signal(0); readonly coachId=signal(0); readonly classType=signal(1); readonly durationMinutes=signal(30); readonly deliveryMethod=signal('线上'); readonly startTime=signal('19:00'); readonly recurrence=signal('once'); readonly occurrences=signal(1); readonly reason=signal('运营排课'); readonly preview=signal<Record<string,unknown>|null>(null); readonly checking=signal(false);

+ 1 - 0
projects/xiaoshu-admin/src/styles.scss

@@ -1,4 +1,5 @@
 @use '../../shared/ui/dropdown';
+@use './app/pages/operations-schedule-layout';
 :root {
   --brand: #2f9b82;
   --brand-strong: #1f7b68;