| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- import { Component, OnInit, OnDestroy } from '@angular/core';
- import { CommonModule, DatePipe } from '@angular/common';
- import {
- FormsModule,
- ReactiveFormsModule,
- FormBuilder,
- FormGroup,
- Validators,
- } from '@angular/forms';
- import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
- import { Subscription } from 'rxjs';
- import { Router, ActivatedRoute } from '@angular/router';
- // NG-ZORRO 组件
- import { NzTableModule } from 'ng-zorro-antd/table';
- import { NzDividerModule } from 'ng-zorro-antd/divider';
- import { NzButtonModule } from 'ng-zorro-antd/button';
- import { NzIconModule } from 'ng-zorro-antd/icon';
- import { NzFormModule } from 'ng-zorro-antd/form';
- import { NzInputModule } from 'ng-zorro-antd/input';
- import { NzSelectModule } from 'ng-zorro-antd/select';
- import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
- import { NzPageHeaderModule } from 'ng-zorro-antd/page-header';
- import { NzCardModule } from 'ng-zorro-antd/card';
- import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
- import { NzTagModule } from 'ng-zorro-antd/tag';
- import { NzMessageService } from 'ng-zorro-antd/message';
- import { NzSpinModule } from 'ng-zorro-antd/spin';
- import { NzEmptyModule } from 'ng-zorro-antd/empty';
- import { NzPaginationModule } from 'ng-zorro-antd/pagination';
- import { NzModalModule, NzModalService } from 'ng-zorro-antd/modal';
- import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
- import { MessageService, IMessageRecord } from '../services/message.service';
- import { MessageRecordDetailComponent } from './message-record-detail.component';
- import { UserDetailModalComponent } from './user-detail-modal.component';
- @Component({
- selector: 'app-message-record',
- standalone: true,
- imports: [
- CommonModule,
- FormsModule,
- ReactiveFormsModule,
- NzTableModule,
- NzDividerModule,
- NzButtonModule,
- NzIconModule,
- NzFormModule,
- NzInputModule,
- NzSelectModule,
- NzDatePickerModule,
- NzPageHeaderModule,
- NzCardModule,
- NzPopconfirmModule,
- NzTagModule,
- NzSpinModule,
- NzEmptyModule,
- NzPaginationModule,
- NzDescriptionsModule,
- NzModalModule,
- DatePipe,
- ],
- template: `
- <nz-page-header [nzGhost]="false">
- <nz-page-header-title>消息推送记录</nz-page-header-title>
- <nz-page-header-content>
- 查看和管理所有消息推送记录
- </nz-page-header-content>
- </nz-page-header>
- <nz-card>
- <form nz-form [formGroup]="filterForm" class="filter-form">
- <div nz-row [nzGutter]="16">
- <!-- 基本筛选条件 -->
- <div nz-col [nzSpan]="4">
- <nz-form-item>
- <nz-input-group nzPrefixIcon="user">
- <input nz-input placeholder="用户ID" formControlName="uid" />
- </nz-input-group>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="4">
- <nz-form-item>
- <nz-input-group nzPrefixIcon="project">
- <input
- nz-input
- placeholder="活动"
- formControlName="activityName"
- />
- </nz-input-group>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="4">
- <nz-form-item>
- <nz-input-group nzPrefixIcon="ant-design">
- <input
- nz-input
- placeholder="策略"
- formControlName="strategyName"
- />
- </nz-input-group>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="4">
- <nz-form-item>
- <nz-input-group nzPrefixIcon="cluster">
- <input
- nz-input
- placeholder="模板"
- formControlName="templateName"
- />
- </nz-input-group>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="4">
- <nz-form-item>
- <nz-select
- nzPlaceHolder="状态"
- formControlName="status"
- nzAllowClear
- >
- <nz-option nzValue="0" nzLabel="未发送"></nz-option>
- <nz-option nzValue="1" nzLabel="发送成功"></nz-option>
- <nz-option nzValue="2" nzLabel="已送达"></nz-option>
- <nz-option nzValue="3" nzLabel="已打开"></nz-option>
- <nz-option nzValue="-1" nzLabel="发送失败"></nz-option>
- </nz-select>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="4" class="button-col">
- <button nz-button (click)="resetFilters()">重置</button>
- </div>
- </div>
- <!-- 日期筛选区域 -->
- <div nz-row [nzGutter]="16" style="margin-top: 16px;">
- <div nz-col [nzSpan]="6">
- <nz-form-item>
- <nz-range-picker
- formControlName="plannedSendAt"
- nzPlaceHolder="计划发送时间"
- style="width: 100%;"
- ></nz-range-picker>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="6">
- <nz-form-item>
- <nz-range-picker
- formControlName="actualSendAt"
- nzPlaceHolder="实际发送时间"
- style="width: 100%;"
- ></nz-range-picker>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="6">
- <nz-form-item>
- <nz-range-picker
- formControlName="deliveredAt"
- nzPlaceHolder="送达时间"
- style="width: 100%;"
- ></nz-range-picker>
- </nz-form-item>
- </div>
- <div nz-col [nzSpan]="6">
- <nz-form-item>
- <nz-range-picker
- formControlName="openedAt"
- nzPlaceHolder="打开时间"
- style="width: 100%;"
- ></nz-range-picker>
- </nz-form-item>
- </div>
- </div>
- </form>
- <nz-spin [nzSpinning]="isLoading">
- <nz-table
- #recordsTable
- [nzData]="records"
- [nzLoading]="isLoading"
- [nzFrontPagination]="false"
- [nzBordered]="true"
- [nzSize]="'small'"
- [nzShowPagination]="false"
- >
- <thead>
- <tr>
- <th>用户ID</th>
- <th>CC</th>
- <th>消息</th>
- <th>来源</th>
- <th>状态</th>
- <th>计划发送时间</th>
- <th>实际发送时间</th>
- <th>送达时间</th>
- <th>打开时间</th>
- <th>创建时间</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let record of records">
- <td>
- <a (click)="showUserDetail(record.uid)">{{ record.uid }}</a>
- </td>
- <td>{{ record.cc }}</td>
- <td class="message-content-cell">
- <div class="message-title">
- {{ record.title }}
- </div>
- <div class="message-content">
- {{ record.content }}
- </div>
- </td>
- <td class="source-cell">
- @if(record.activityName) {
- <div class="source-item">
- <span class="source-label">活动:</span>
- <span class="source-value">{{ record.activityName }}</span>
- </div>
- } @if(record.strategyName) {
- <div class="source-item">
- <span class="source-label">策略:</span>
- <span class="source-value">{{ record.strategyName }}</span>
- </div>
- } @if(record.templateName) {
- <div class="source-item">
- <span class="source-label">模板:</span>
- <span class="source-value">{{ record.templateName }}</span>
- </div>
- }
- </td>
- <td>
- <nz-tag [nzColor]="getStatusColor(record.status)">
- {{ getStatusName(record.status) }}
- </nz-tag>
- </td>
- <td>{{ record.plannedSendAt | date : 'yyyy-MM-dd HH:mm' }}</td>
- <td>{{ record.actualSendAt | date : 'yyyy-MM-dd HH:mm' }}</td>
- <td>{{ record.deliveredAt | date : 'yyyy-MM-dd HH:mm' }}</td>
- <td>{{ record.openedAt | date : 'yyyy-MM-dd HH:mm' }}</td>
- <td>{{ record.createdAt | date : 'yyyy-MM-dd HH:mm' }}</td>
- <td>
- <a (click)="showDetail(record)">详情</a>
- </td>
- </tr>
- </tbody>
- </nz-table>
- <nz-empty
- *ngIf="records.length === 0 && !isLoading"
- nzNotFoundContent="暂无消息记录"
- ></nz-empty>
- </nz-spin>
- <div class="pagination-container">
- <nz-pagination
- [nzPageIndex]="pagination.page"
- [nzTotal]="pagination.total"
- [nzPageSize]="pagination.limit"
- [nzShowTotal]="totalTemplate"
- (nzPageIndexChange)="onPageChange($event)"
- (nzPageSizeChange)="onPageSizeChange($event)"
- nzShowSizeChanger
- nzShowQuickJumper
- ></nz-pagination>
- <ng-template #totalTemplate let-total>
- <span>共 {{ total }} 条记录</span>
- </ng-template>
- </div>
- </nz-card>
- `,
- styles: [
- `
- nz-select {
- width: 100%;
- }
- .filter-form {
- margin-bottom: 16px;
- nz-range-picker {
- width: 100%;
- }
- }
- .button-col {
- display: flex;
- gap: 8px;
- justify-content: flex-end;
- }
- nz-table {
- margin-top: 16px;
- }
- .pagination-container {
- display: flex;
- justify-content: center;
- margin-top: 16px;
- }
- nz-tag {
- margin-right: 0;
- }
- /* 详情按钮样式 */
- a[nz-button] {
- padding: 0 8px;
- }
- @media (max-width: 768px) {
- nz-col {
- margin-bottom: 8px;
- }
- .button-col {
- justify-content: flex-start;
- }
- }
- /* 消息内容单元格样式 */
- .message-content-cell {
- max-width: 300px;
- min-width: 200px;
- }
- /* 消息标题样式 */
- .message-title {
- font-weight: bold;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-bottom: 4px;
- }
- /* 消息内容样式 */
- .message-content {
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
- line-height: 1.4;
- color: #666;
- }
- /* 来源单元格样式 */
- .source-cell {
- min-width: 200px;
- max-width: 300px;
- }
- .source-item {
- display: flex;
- margin-bottom: 4px;
- line-height: 1.5;
- }
- .source-label {
- font-weight: 500;
- color: #666;
- min-width: 40px;
- margin-right: 8px;
- }
- .source-value {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- /* 响应式调整 */
- @media (max-width: 1200px) {
- .source-cell {
- min-width: 150px;
- max-width: 200px;
- }
- }
- `,
- ],
- })
- export class MessageRecordComponent implements OnInit, OnDestroy {
- records: IMessageRecord[] = [];
- isLoading = false;
- private queryParamsSubscription!: Subscription;
- // 使用 FormGroup 管理所有筛选字段
- filterForm: FormGroup;
- // 分页参数
- pagination = {
- page: 1,
- limit: 30,
- total: 0,
- };
- constructor(
- private messageService: MessageService,
- private message: NzMessageService,
- private modalService: NzModalService,
- private fb: FormBuilder,
- private router: Router,
- private route: ActivatedRoute
- ) {
- this.filterForm = this.fb.group({
- uid: [null],
- activityName: [null],
- templateName: [null],
- strategyName: [null],
- status: [null],
- plannedSendAt: [null],
- actualSendAt: [null],
- deliveredAt: [null],
- openedAt: [null],
- });
- }
- ngOnInit(): void {
- // 订阅 URL 查询参数的变化
- this.queryParamsSubscription = this.route.queryParams.subscribe(
- (params) => {
- // 更新分页状态
- this.pagination.page = params['page']
- ? parseInt(params['page'], 10)
- : 1;
- this.pagination.limit = params['limit']
- ? parseInt(params['limit'], 10)
- : 30;
- // 更新表单控件的值,日期需要特殊处理
- const formValue: any = {};
- for (const key in this.filterForm.controls) {
- if (params[key]) {
- // 日期范围参数格式: "2023-01-01T00:00:00Z,2023-01-31T23:59:59Z"
- if (key.includes('At')) {
- const dateRange = params[key].split(',');
- if (dateRange.length === 2) {
- formValue[key] = [
- new Date(dateRange[0]),
- new Date(dateRange[1]),
- ];
- }
- } else {
- formValue[key] = params[key];
- }
- }
- }
- // 使用 patchValue 来避免未提供的控件报错
- this.filterForm.patchValue(formValue, { emitEvent: false });
- this.loadRecords();
- }
- );
- // 监听表单变化,自动触发查询
- this.filterForm.valueChanges
- .pipe(
- debounceTime(500),
- distinctUntilChanged(
- (prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)
- )
- )
- .subscribe(() => {
- this.onFilter();
- });
- }
- ngOnDestroy(): void {
- if (this.queryParamsSubscription) {
- this.queryParamsSubscription.unsubscribe();
- }
- }
- loadRecords(): void {
- this.isLoading = true;
- // 准备查询参数
- const params: any = this.prepareFilters();
- params['page'] = this.pagination.page;
- params['limit'] = this.pagination.limit;
- this.messageService.getPaginatedRecords(params).subscribe({
- next: (response) => {
- this.records = response.data;
- this.pagination.total = response.pagination.total;
- this.isLoading = false;
- },
- error: (err) => {
- this.message.error('加载记录失败: ' + (err.message || '未知错误'));
- this.isLoading = false;
- },
- });
- }
- private prepareFilters(): any {
- const formValue = this.filterForm.value;
- const filters: any = {};
- for (const key in formValue) {
- if (
- formValue.hasOwnProperty(key) &&
- formValue[key] !== null &&
- formValue[key] !== ''
- ) {
- if (Array.isArray(formValue[key])) {
- if (formValue[key].length === 2) {
- const start = formValue[key][0].toISOString();
- const end = formValue[key][1].toISOString();
- filters[key] = `${start},${end}`;
- }
- } else {
- filters[key] = formValue[key];
- }
- }
- }
- return filters;
- }
- // 重构为私有方法,用于导航到新 URL
- private navigateToUrl(): void {
- const filters = this.prepareFilters();
- const queryParams = {
- page: this.pagination.page,
- limit: this.pagination.limit,
- ...filters,
- };
- // FIX: Removed queryParamsHandling: 'merge' to allow empty filters to be removed from the URL
- this.router.navigate([], {
- relativeTo: this.route,
- queryParams,
- });
- }
- onFilter(): void {
- this.pagination.page = 1; // 筛选时重置到第一页
- this.navigateToUrl();
- }
- resetFilters(): void {
- this.filterForm.reset();
- this.onFilter();
- }
- // 分页变更处理
- onPageChange(page: number): void {
- this.pagination.page = page;
- this.navigateToUrl();
- }
- onPageSizeChange(size: number): void {
- this.pagination.limit = size;
- this.pagination.page = 1;
- this.navigateToUrl();
- }
- // 显示详情模态框
- showDetail(record: IMessageRecord): void {
- this.modalService.create({
- nzTitle: `消息记录详情 - ${record.uid}`,
- nzWidth: '800px',
- nzContent: MessageRecordDetailComponent,
- nzData: { record },
- nzFooter: null,
- });
- }
- // 获取状态名称
- getStatusName(status: number): string {
- const statuses: Record<number, string> = {
- 0: '未发送',
- 1: '发送成功',
- 2: '已送达',
- 3: '已打开',
- [-1]: '发送失败',
- };
- return statuses[status] || '未知状态';
- }
- // 获取状态颜色
- getStatusColor(status: number): string {
- const colors: Record<number, string> = {
- 0: 'default',
- 1: 'processing',
- 2: 'success',
- 3: 'green',
- [-1]: 'error',
- };
- return colors[status] || 'default';
- }
- showUserDetail(uid: string): void {
- this.modalService.create({
- nzTitle: `用户详情 - ${uid}`,
- nzContent: UserDetailModalComponent,
- nzWidth: '800px',
- nzData: { uid }, // 传递用户UID
- nzFooter: null,
- nzBodyStyle: {
- 'max-height': '70vh',
- 'overflow-y': 'auto',
- },
- });
- }
- }
|