fix: clarify client amounts and log date filters

This commit is contained in:
hectorzhao
2026-08-27 19:50:12 +08:00
parent ec721bf95a
commit 33fa3709d9
18 changed files with 136 additions and 82 deletions
@@ -431,10 +431,12 @@ export class AdminSystemLogsController {
@Query('status') status?: string,
@Query('keyword') keyword?: string,
@Query('range') range?: string,
@Query('createdAtFrom') createdAtFrom?: string,
@Query('createdAtTo') createdAtTo?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.protocolLogs.list({ protocol, direction, eventType, status, keyword, range, page: Number(page), pageSize: Number(pageSize) });
return this.protocolLogs.list({ protocol, direction, eventType, status, keyword, range, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) });
}
@Get()
@@ -445,14 +447,16 @@ export class AdminSystemLogsController {
@Query('level') level?: string,
@Query('module') module?: string,
@Query('range') range?: string,
@Query('createdAtFrom') createdAtFrom?: string,
@Query('createdAtTo') createdAtTo?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.operations.systemLogs({ tenantId, userId, keyword, level, module, range, page: Number(page), pageSize: Number(pageSize) });
return this.operations.systemLogs({ tenantId, userId, keyword, level, module, range, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) });
}
@Post('exports')
export(@Body() body: { tenantId?: string; userId?: string; keyword?: string; level?: string; module?: string; range?: string }) {
export(@Body() body: { tenantId?: string; userId?: string; keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string }) {
return this.operations.exportSystemLogs(body);
}
}
@@ -67,16 +67,18 @@ export class ClientOperationsController {
@Query('level') level?: string,
@Query('module') module?: string,
@Query('range') range?: string,
@Query('createdAtFrom') createdAtFrom?: string,
@Query('createdAtTo') createdAtTo?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.operations.systemLogs({ tenantId, keyword, level, module, range, page: Number(page), pageSize: Number(pageSize) });
return this.operations.systemLogs({ tenantId, keyword, level, module, range, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) });
}
@Post('system-logs/exports')
exportSystemLogs(
@CurrentSessionUserId() userId: string | undefined,
@Body() body: { keyword?: string; level?: string; module?: string; range?: string },
@Body() body: { keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string },
) {
return this.operations.exportSystemLogs(body, userId);
}
@@ -29,6 +29,8 @@ export interface OperationLogQuery {
level?: string;
module?: string;
range?: string;
createdAtFrom?: string;
createdAtTo?: string;
page?: number;
pageSize?: number;
}
@@ -890,6 +890,22 @@ describe('OperationsService', () => {
});
});
it('applies an explicit Beijing date interval to operation logs', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);
await service.systemLogs({ createdAtFrom: '2026-08-21', createdAtTo: '2026-08-27' });
expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
createdAt: {
gte: new Date('2026-08-20T16:00:00.000Z'),
lte: new Date('2026-08-27T15:59:59.999Z'),
},
}),
}));
});
it('exports filtered operation logs with a traceable operation id', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);
+8 -3
View File
@@ -31,7 +31,7 @@ async systemLogs(query: OperationLogQuery) {
const where: Prisma.OperationLogWhereInput = {
tenantId: query.tenantId,
userId: query.userId,
createdAt: createdAtRange(query.range),
createdAt: this.operationLogDateRange(query),
resource: query.module && query.module !== 'all' ? query.module : undefined,
AND: query.level && query.level !== 'all' ? operationLogLevelWhere(query.level) : undefined,
OR: query.keyword ? [
@@ -73,7 +73,7 @@ async exportSystemLogs(query: OperationLogQuery, clientUserId?: string) {
const where: Prisma.OperationLogWhereInput = {
tenantId: effectiveQuery.tenantId,
userId: effectiveQuery.userId,
createdAt: createdAtRange(effectiveQuery.range),
createdAt: this.operationLogDateRange(effectiveQuery),
resource: effectiveQuery.module && effectiveQuery.module !== 'all' ? effectiveQuery.module : undefined,
AND: effectiveQuery.level && effectiveQuery.level !== 'all' ? operationLogLevelWhere(effectiveQuery.level) : undefined,
OR: effectiveQuery.keyword ? [
@@ -107,9 +107,14 @@ async exportSystemLogs(query: OperationLogQuery, clientUserId?: string) {
recordCount: exportedRows.length,
truncated,
content: [headers, ...values].map((row) => row.map((cell) => escapeCsvCell(String(cell ?? ''))).join(',')).join('\n'),
filters: { keyword: effectiveQuery.keyword, level: effectiveQuery.level, module: effectiveQuery.module, range: effectiveQuery.range },
filters: { keyword: effectiveQuery.keyword, level: effectiveQuery.level, module: effectiveQuery.module, range: effectiveQuery.range, createdAtFrom: effectiveQuery.createdAtFrom, createdAtTo: effectiveQuery.createdAtTo },
};
}
private operationLogDateRange(query: OperationLogQuery) {
const createdAtFrom = parseDateBoundary(query.createdAtFrom, false);
const createdAtTo = parseDateBoundary(query.createdAtTo, true);
return createdAtFrom || createdAtTo ? { gte: createdAtFrom, lte: createdAtTo } : createdAtRange(query.range);
}
private async resolveClientTenantId(userId: string) {
const user = await this.prisma.user.findFirst({
where: { id: userId, status: 'active', deletedAt: null, tenantId: { not: null } },
@@ -76,4 +76,18 @@ describe('ProtocolLogsService', () => {
}),
}));
});
it('applies an explicit Beijing date interval to protocol logs', async () => {
const service = new ProtocolLogsService(prisma as never);
await service.list({ createdAtFrom: '2026-08-21', createdAtTo: '2026-08-27' });
expect(prisma.protocolInteractionLog.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
createdAt: {
gte: new Date('2026-08-20T16:00:00.000Z'),
lte: new Date('2026-08-27T15:59:59.999Z'),
},
}),
}));
});
});
+10 -1
View File
@@ -1,6 +1,7 @@
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { parseDateBoundary } from '../operations/operations.helpers';
export type ProtocolLogInput = {
protocol: 'cmpp' | 'http';
@@ -34,6 +35,8 @@ export type ProtocolLogQuery = {
status?: string;
keyword?: string;
range?: string;
createdAtFrom?: string;
createdAtTo?: string;
page?: number;
pageSize?: number;
};
@@ -115,7 +118,7 @@ export class ProtocolLogsService implements OnModuleInit, OnModuleDestroy {
direction: selected(query.direction),
eventType: selected(query.eventType),
status: selected(query.status),
createdAt: rangeWhere(query.range),
createdAt: protocolLogDateRange(query),
OR: query.keyword ? [
{ messageId: { contains: query.keyword } },
{ gatewayMessageId: { contains: query.keyword } },
@@ -197,6 +200,12 @@ function rangeWhere(range?: string): Prisma.DateTimeFilter | undefined {
return undefined;
}
function protocolLogDateRange(query: ProtocolLogQuery): Prisma.DateTimeFilter | undefined {
const createdAtFrom = parseDateBoundary(query.createdAtFrom, false);
const createdAtTo = parseDateBoundary(query.createdAtTo, true);
return createdAtFrom || createdAtTo ? { gte: createdAtFrom, lte: createdAtTo } : rangeWhere(query.range);
}
function sanitizeDetail(detail?: Record<string, unknown> | null): Prisma.InputJsonValue | undefined {
if (!detail) return undefined;
const blocked = /password|secret|token|signature|authorization|content|raw|body/i;
@@ -1702,6 +1702,7 @@
8. 长短信任一分片返回非成功终态时,系统必须通过该分片审计关联的提交记录识别当前发送尝试,不得仅以主记录保存的首片上游消息号判断;确认属于当前尝试后,整条短信立即进入失败/补发或退款终态,无需等待其余分片回执。
9. 回执和上行投递方式不得由运营人员选择。企业应用开通CMPP接口即按CMPP投递,开通HTTP接口且对应Webhook地址非空即按HTTP投递,两者同时满足时双投;任一地址为空时只跳过该类HTTP事件。运营端企业应用HTTP参数页必须始终可编辑回执和上行Webhook地址,不因HTTP接口开关关闭而隐藏。
10. Gateway向企业应用发送真实 `CMPP_DELIVER` 以及收到企业应用真实 `CMPP_DELIVER_RESP` 时,都必须各写一条通讯交互日志,分别使用“平台→企业应用”和“企业应用→平台”方向;下游投递记录继续承担排队、重试和ACK业务状态,不得以通讯日志替代。
11. 运营端“系统与操作日志”和“通讯交互日志”以及客户端“系统日志”必须提供可选起止日期的区间控件,首次进入和重置后默认北京时间近7天;查询、分页和导出必须使用同一真实后端日期边界,结束日期包含当日23:59:59.999,不得只在前端裁剪列表。
## 2026-07-26 依赖安全治理补充
+14
View File
@@ -2811,6 +2811,20 @@
- 到期日志按 `archiveMonth=YYYY-MM` 进入归档表,在线表只删除已成功归档的记录,原始 id 和详情不丢失。
- 归档使用有界小批量和 `SKIP LOCKED`;归档失败时源日志仍保留,不阻塞正常日志写入。
### TC-LOG-012 系统日志日期区间默认值与后端过滤
- 优先级:P1
- 前置条件:运营端和客户端均存在跨越7天以上的系统日志,通讯交互日志也存在跨日数据。
- 步骤:
1. 分别进入运营端“系统与操作日志”“通讯交互日志”和客户端“系统日志”。
2. 核对日期区间默认值,查询并翻页。
3. 改为自定义起止日期后再次查询;运营端同时导出系统与操作日志。
4. 点击重置。
- 预期结果:
- 三处日期区间初始和重置后均为北京时间近7天,允许通过日期控件选择任意合法区间。
- 请求真实携带`createdAtFrom/createdAtTo`,后端按北京时间当日00:00:00.000至结束日23:59:59.999过滤。
- 列表总数、分页和导出使用同一日期条件,不返回区间外数据。
### TC-CUSTOMER-001 运营端创建客户并初始化租户
- 优先级:P0
+7
View File
@@ -4057,3 +4057,10 @@ git diff --check
- 测试环境真实服务核验定位到客户端接口对接页原500的根因:测试机使用HTTP源地址,而后端强制要求HTTPS。追加提交`070a951e7cffb467734a4486ac46a83605a7f218`,默认仍拒绝不安全源地址,仅允许隔离测试环境通过`HTTP_API_ALLOW_INSECURE_ORIGIN=true`显式放行HTTP;新增OpenAPI单测11/11和API正式构建通过。
- 第二次发布前重新建立并校验恢复点`/opt/cmpp-platform-backups/ui-http-origin-20260827T104430Z`。最终部署标记为`070a951e7cffb467734a4486ac46a83605a7f218`;以`screenshot_client`所属企业的真实应用直接调用配置、凭据、Webhook、请求日志、投递日志五个服务方法全部成功,配置接口不再抛出Internal server error。8项核心服务active、API/Gateway健康、结果Stream`pending=0/lag=0`、发布后error级日志为空,运营端/客户端登录页及API健康地址从工作站访问均HTTP 200。
- 两次发布均仅操作测试环境`100.93.204.60`;未访问、回退、覆盖或部署预生产`8.160.169.106`,未发送短信或执行压力测试。第二阶段孤立审核任务事务/补偿仍未实施。
## 2026-08-27 客户端金额样式复核与系统日志日期区间(本地门禁)
- 用户在另一台电脑复核后指出首页金额视觉没有变化。重新比对最终CSS确认首次改动仅把原有约30px深色粗体重复声明为30px深色粗体,视觉差异不足;同时`MoneyText`组件不生成`.money-text`类,首次样式中的后代选择器不会命中。现改为明确的Arial/微软雅黑普通系统字体、38px、500字重、纯黑`#111111`,并显式清除背景、文字填充、阴影及特殊裁剪效果;最近充值使用28px紧凑规格。
- 运营端系统与操作日志、运营端通讯交互日志、客户端系统日志均改为可选起止日期的真实日期区间,初始及重置默认北京时间近7天。查询、分页和导出统一向后端传递`createdAtFrom/createdAtTo`,后端按开始日00:00:00.000至结束日23:59:59.999Asia/Shanghai)过滤。
- Chrome视觉夹具计算样式确认三个关键金额均为`rgb(17,17,17)`、38px、500字重、Arial/微软雅黑且控制台无error/warn;前端TypeScript与Vite生产构建通过。API正式TypeScript构建、操作日志/通讯日志专项34项、API全量45套532项通过。
- 本节当前仅记录本地修复和门禁;测试环境发布与登录后真实页面复核需在提交并重新建立恢复资产后完成。预生产未访问。
+3 -3
View File
@@ -8,11 +8,11 @@ export const adminOperationsApi = {
getSendQuality: (date?: string) => request<SendQualityResponse>(withQuery('/admin/operations/send-quality', { date })),
getSignatureQuality: (query: { date?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
request<SignatureChannelQualityResponse>(withQuery('/admin/operations/signature-quality', query)),
listSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }) =>
listSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string; page?: number; pageSize?: number }) =>
request<OperationLogResponse>(withQuery('/admin/system-logs', query)),
listProtocolInteractionLogs: (query: { protocol?: string; direction?: string; eventType?: string; status?: string; keyword?: string; range?: string; page?: number; pageSize?: number }) =>
listProtocolInteractionLogs: (query: { protocol?: string; direction?: string; eventType?: string; status?: string; keyword?: string; range?: string; createdAtFrom?: string; createdAtTo?: string; page?: number; pageSize?: number }) =>
request<ProtocolInteractionLogResponse>(withQuery('/admin/system-logs/protocol-interactions', query)),
exportSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string }) =>
exportSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string }) =>
request<SystemLogExportResult>('/admin/system-logs/exports', { method: 'POST', body: JSON.stringify(query) }),
listReconciliationReports: (query: { dateFrom?: string; dateTo?: string; tenantId?: string; applicationId?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResponse<DailyReconciliationReport> & { summary: ReconciliationReportSummary }>(withQuery('/admin/reports/reconciliation', query)),
+2 -2
View File
@@ -36,9 +36,9 @@ export const clientApi = {
tenantId,
body: JSON.stringify({ ...body, tenantId }),
}),
listSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
listSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string; page?: number; pageSize?: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<OperationLogResponse>(withQuery('/client/operations/system-logs', query), { tenantId }),
exportSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string }) =>
exportSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string }) =>
request<SystemLogExportResult>('/client/operations/system-logs/exports', { method: 'POST', body: JSON.stringify(query) }),
listOrders: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<RechargeOrder[]>('/client/billing/orders', { tenantId }),
+1 -1
View File
@@ -331,7 +331,7 @@ export type SystemLogExportResult = {
recordCount: number;
truncated: boolean;
content: string;
filters: { keyword?: string; level?: string; module?: string; range?: string };
filters: { keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string };
};
export type DailyReconciliationReport = {
+18 -25
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
import { CalendarDays, FileText, Search } from 'lucide-react';
import { Button, Input, Modal, Pagination, Select, SystemLogExport, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
import { FileText, Search } from 'lucide-react';
import { Button, DateRangeInput, Input, Modal, Pagination, Select, SystemLogExport, Table, Tabs, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
import { adminApi, type OperationLogItem, type ProtocolInteractionLogItem } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { formatDateTime, recentBeijingDateRange } from '@/utils/dateTime';
type LogLevel = 'info' | 'success' | 'warning' | 'error';
@@ -25,8 +25,11 @@ export function AdminSystemLogsPage() {
const [keyword, setKeyword] = useState('');
const [level, setLevel] = useState('all');
const [module, setModule] = useState('all');
const [range, setRange] = useState('today');
const [filters, setFilters] = useState({ keyword: '', level: 'all', module: 'all', range: 'today' });
const [dateRange, setDateRange] = useState<DateRangeValue>(() => recentBeijingDateRange(7));
const [filters, setFilters] = useState<{ keyword: string; level: string; module: string; createdAtFrom?: string; createdAtTo?: string }>(() => {
const initialRange = recentBeijingDateRange(7);
return { keyword: '', level: 'all', module: 'all', createdAtFrom: initialRange.start, createdAtTo: initialRange.end };
});
const [page, setPage] = useState(1);
const pageSize = 5;
const [logs, setLogs] = useState<OperationLogItem[]>([]);
@@ -59,16 +62,17 @@ export function AdminSystemLogsPage() {
function query() {
setPage(1);
setFilters({ keyword: keyword.trim(), level, module, range });
setFilters({ keyword: keyword.trim(), level, module, createdAtFrom: dateRange.start, createdAtTo: dateRange.end });
}
function reset() {
setKeyword('');
setLevel('all');
setModule('all');
setRange('today');
const nextDateRange = recentBeijingDateRange(7);
setDateRange(nextDateRange);
setPage(1);
setFilters({ keyword: '', level: 'all', module: 'all', range: 'today' });
setFilters({ keyword: '', level: 'all', module: 'all', createdAtFrom: nextDateRange.start, createdAtTo: nextDateRange.end });
}
const columns = useMemo<Array<TableColumn<OperationLogItem>>>(() => [
@@ -118,14 +122,13 @@ export function AdminSystemLogsPage() {
value={level}
/>
<Select onChange={(event) => setModule(event.target.value)} options={moduleOptions} value={module} />
<DateRangeInput label="日志时间" onChange={setDateRange} placeholder="选择日志时间区间" value={dateRange} />
<div className="system-log-filters__actions">
<Button icon={<Search size={16} />} onClick={query}></Button>
<Button onClick={reset} variant="ghost"></Button>
</div>
</div>
<LogRange range={range} onChange={setRange} />
<div className="surface system-table-card">
<Table columns={columns} data={logs} emptyText={error || '暂无系统日志'} pagination={false} rowKey="id" />
<Pagination
@@ -222,7 +225,7 @@ function protocolStatusLabel(record: ProtocolInteractionLogItem) {
}
function ProtocolInteractionPanel({ active }: { active: boolean }) {
const [inputs, setInputs] = useState({ keyword: '', protocol: 'all', direction: 'all', eventType: 'all', status: 'all', range: 'today' });
const [inputs, setInputs] = useState(() => ({ keyword: '', protocol: 'all', direction: 'all', eventType: 'all', status: 'all', dateRange: recentBeijingDateRange(7) as DateRangeValue }));
const [filters, setFilters] = useState(inputs);
const [items, setItems] = useState<ProtocolInteractionLogItem[]>([]);
const [eventTypes, setEventTypes] = useState<string[]>([]);
@@ -235,7 +238,8 @@ function ProtocolInteractionPanel({ active }: { active: boolean }) {
useEffect(() => {
if (!active) return;
let cancelled = false;
adminApi.listProtocolInteractionLogs({ ...filters, page, pageSize })
const { dateRange, ...queryFilters } = filters;
adminApi.listProtocolInteractionLogs({ ...queryFilters, createdAtFrom: dateRange.start, createdAtTo: dateRange.end, page, pageSize })
.then((data) => {
if (cancelled) return;
setItems(data.items);
@@ -271,7 +275,7 @@ function ProtocolInteractionPanel({ active }: { active: boolean }) {
}
function reset() {
const next = { keyword: '', protocol: 'all', direction: 'all', eventType: 'all', status: 'all', range: 'today' };
const next = { keyword: '', protocol: 'all', direction: 'all', eventType: 'all', status: 'all', dateRange: recentBeijingDateRange(7) };
setInputs(next);
setFilters(next);
setPage(1);
@@ -286,9 +290,9 @@ function ProtocolInteractionPanel({ active }: { active: boolean }) {
<Select onChange={(event) => setInputs((value) => ({ ...value, direction: event.target.value }))} options={[{ label: '全部方向', value: 'all' }, ...Object.entries(directionLabels).map(([value, label]) => ({ value, label }))]} value={inputs.direction} />
<Select onChange={(event) => setInputs((value) => ({ ...value, eventType: event.target.value }))} options={[{ label: '全部事件', value: 'all' }, ...eventTypes.map((value) => ({ label: value, value }))]} value={inputs.eventType} />
<Select onChange={(event) => setInputs((value) => ({ ...value, status: event.target.value }))} options={[{ label: '全部结果', value: 'all' }, { label: '已接收', value: 'received' }, { label: '已受理', value: 'accepted' }, { label: '成功', value: 'success' }, { label: '重试中', value: 'retrying' }, { label: '失败', value: 'failed' }]} value={inputs.status} />
<DateRangeInput label="日志时间" onChange={(dateRange) => setInputs((value) => ({ ...value, dateRange }))} placeholder="选择日志时间区间" value={inputs.dateRange} />
<div className="system-log-filters__actions"><Button icon={<Search size={16} />} onClick={query}></Button><Button onClick={reset} variant="ghost"></Button></div>
</div>
<LogRange range={inputs.range} onChange={(range) => setInputs((value) => ({ ...value, range }))} />
<div className="surface system-table-card protocol-log-table">
<Table columns={columns} data={items} emptyText={error || '暂无通讯交互日志'} pagination={false} rowKey="id" />
<Pagination nextDisabled={page >= totalPages} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} onPrevious={() => setPage((value) => Math.max(1, value - 1))} page={Math.min(page, totalPages)} totalPages={totalPages} onPageChange={setPage} previousDisabled={page <= 1} total={total} />
@@ -299,14 +303,3 @@ function ProtocolInteractionPanel({ active }: { active: boolean }) {
</div>
);
}
function LogRange({ range, onChange }: { range: string; onChange: (value: string) => void }) {
return (
<div className="system-log-range">
<span><CalendarDays size={18} /> </span>
{[{ label: '今天', value: 'today' }, { label: '近7天', value: '7d' }, { label: '近30天', value: '30d' }, { label: '全部', value: 'all' }].map((item) => (
<Button key={item.value} onClick={() => onChange(item.value)} size="sm" variant={range === item.value ? 'primary' : 'secondary'}>{item.label}</Button>
))}
</div>
);
}
+9 -26
View File
@@ -1,17 +1,18 @@
import { useEffect, useMemo, useState } from 'react';
import { CalendarDays, FileText, Search } from 'lucide-react';
import { FileText, Search } from 'lucide-react';
import {
Button,
DateRangeInput,
Input,
Pagination,
Select,
SystemLogExport,
Table,
Tag,
type DateRangeValue,
type TableColumn,
} from '@/components/ui';
import { clientApi, type OperationLogItem } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { formatDateTime, recentBeijingDateRange } from '@/utils/dateTime';
type LogLevel = 'info' | 'success' | 'warning' | 'error';
@@ -33,7 +34,7 @@ export function ClientSystemLogsPage() {
const [keyword, setKeyword] = useState('');
const [level, setLevel] = useState('all');
const [module, setModule] = useState('all');
const [range, setRange] = useState('today');
const [dateRange, setDateRange] = useState<DateRangeValue>(() => recentBeijingDateRange(7));
const [page, setPage] = useState(1);
const pageSize = 5;
const [logs, setLogs] = useState<OperationLogItem[]>([]);
@@ -42,7 +43,7 @@ export function ClientSystemLogsPage() {
const [error, setError] = useState('');
useEffect(() => {
clientApi.listSystemLogs({ keyword, level, module, range, page, pageSize })
clientApi.listSystemLogs({ keyword, level, module, createdAtFrom: dateRange.start, createdAtTo: dateRange.end, page, pageSize })
.then((data) => {
setLogs(data.items);
setModules(data.modules);
@@ -54,7 +55,7 @@ export function ClientSystemLogsPage() {
setTotal(0);
setError(err instanceof Error ? err.message : '系统日志加载失败');
});
}, [keyword, level, module, range, page]);
}, [keyword, level, module, dateRange.end, dateRange.start, page]);
const moduleOptions = useMemo(() => {
return [{ label: '全部模块', value: 'all' }, ...modules.map((item) => ({ label: item, value: item }))];
@@ -79,7 +80,7 @@ export function ClientSystemLogsPage() {
<span className="sms-send-title__icon"><FileText size={22} /></span>
<h1></h1>
</div>
<SystemLogExport exportLogs={clientApi.exportSystemLogs} filters={{ keyword, level, module, range }} portal="client" />
<SystemLogExport exportLogs={clientApi.exportSystemLogs} filters={{ keyword, level, module, createdAtFrom: dateRange.start, createdAtTo: dateRange.end }} portal="client" />
</div>
<div className="system-log-filters">
@@ -101,25 +102,7 @@ export function ClientSystemLogsPage() {
value={level}
/>
<Select onChange={(event) => { setModule(event.target.value); setPage(1); }} options={moduleOptions} value={module} />
</div>
<div className="system-log-range">
<span><CalendarDays size={18} /> </span>
{[
{ label: '今天', value: 'today' },
{ label: '近7天', value: '7d' },
{ label: '近30天', value: '30d' },
{ label: '全部', value: 'all' },
].map((item) => (
<Button
key={item.value}
onClick={() => setRange(item.value)}
size="sm"
variant={range === item.value ? 'primary' : 'secondary'}
>
{item.label}
</Button>
))}
<DateRangeInput label="日志时间" onChange={(value) => { setDateRange(value); setPage(1); }} placeholder="选择日志时间区间" value={dateRange} />
</div>
<div className="surface system-table-card">
+4 -3
View File
@@ -9,6 +9,7 @@ export type DateRangeValue = {
type DateRangeInputProps = {
className?: string;
label?: string;
placeholder?: string;
value: DateRangeValue;
onChange: (value: DateRangeValue) => void;
};
@@ -66,7 +67,7 @@ function getCalendarDays(viewDate: Date) {
});
}
export function DateRangeInput({ className = '', label, value, onChange }: DateRangeInputProps) {
export function DateRangeInput({ className = '', label, placeholder = '选择提交时间区间', value, onChange }: DateRangeInputProps) {
const [open, setOpen] = useState(false);
const [viewDate, setViewDate] = useState(() => parseDate(value.start) ?? new Date());
const startDate = parseDate(value.start);
@@ -83,8 +84,8 @@ export function DateRangeInput({ className = '', label, value, onChange }: DateR
return `${value.start} 至 ...`;
}
return '选择提交时间区间';
}, [value.end, value.start]);
return placeholder;
}, [placeholder, value.end, value.start]);
function moveMonth(offset: number) {
setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1));
+1 -1
View File
@@ -3,7 +3,7 @@ import { Download, RotateCcw } from 'lucide-react';
import type { SystemLogExportResult } from '@/api/adminApi';
import { Button } from './Button';
type Filters = { keyword?: string; level?: string; module?: string; range?: string };
type Filters = { keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string };
export function SystemLogExport({
portal,
+15 -12
View File
@@ -580,20 +580,23 @@
justify-content: flex-end;
}
.client-amount-value,
.client-amount-value .money-text {
color: #111827 !important;
font-family: inherit !important;
font-size: 30px;
.client-amount-value {
background: none !important;
color: #111111 !important;
font-family: Arial, "Microsoft YaHei", sans-serif !important;
font-size: 38px !important;
font-style: normal;
font-variant-numeric: tabular-nums;
font-weight: 700;
letter-spacing: 0;
font-weight: 500 !important;
letter-spacing: 0 !important;
line-height: 1.2 !important;
text-shadow: none !important;
-webkit-background-clip: border-box !important;
-webkit-text-fill-color: #111111 !important;
}
.client-amount-value--compact,
.client-amount-value--compact .money-text {
font-size: 22px;
.client-amount-value--compact {
font-size: 28px !important;
}
.send-submit-hint {
@@ -3595,7 +3598,7 @@
align-items: end;
display: grid;
gap: 22px;
grid-template-columns: minmax(320px, 1fr) minmax(220px, 0.32fr) minmax(220px, 0.32fr);
grid-template-columns: minmax(280px, 1.2fr) repeat(3, minmax(190px, .65fr)) auto;
}
.system-log-range {
@@ -3706,7 +3709,7 @@
}
.protocol-log-filters {
grid-template-columns: minmax(260px, 1.4fr) repeat(4, minmax(135px, .55fr));
grid-template-columns: minmax(240px, 1.2fr) repeat(4, minmax(125px, .5fr)) minmax(230px, .8fr) auto;
}
.protocol-log-table .ui-table {