Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { LocalScheduleReader } from '../../application/interfaces';
import type { LocalReminderSchedule } from '../../domain';

import { MOCK_REMINDER_SCHEDULES } from './mockReminderSchedules';

/** 代替本地数据库端口的固定只读适配器。 */
export class MockLocalScheduleReader implements LocalScheduleReader {
readonly schedules = MOCK_REMINDER_SCHEDULES;

async listReminderSchedules(): Promise<readonly LocalReminderSchedule[]> {
return this.schedules;
}

async getReminderSchedule(scheduleId: string): Promise<LocalReminderSchedule | null> {
return this.schedules.find((schedule) => schedule.id === scheduleId) ?? null;
}

subscribe(listener: (schedules: readonly LocalReminderSchedule[]) => void): () => void {
listener(this.schedules);
return () => undefined;
}
}
105 changes: 105 additions & 0 deletions frontend/src/features/reminder/data/local/MockReminderApplication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type {
ReminderApplicationDependencies,
ReminderApplicationPort,
ReminderApplicationResult,
ReminderSnoozeRequest,
} from '../../application/interfaces';
import type {
LocalReminderSchedule,
LocationSample,
ReminderDeliveryReceipt,
ReminderRegistration,
ReminderTrigger,
} from '../../domain';

const MOCK_DELIVERY: ReminderDeliveryReceipt = {
delivery_id: 'mock-delivery-001',
schedule_id: 'mock-schedule-time-001',
delivered_at: '2026-08-07T01:00:00.000Z',
channels: ['system_notification', 'popup', 'vibration'],
used_fallback_audio: false,
};

function registrationFor(schedule: LocalReminderSchedule): ReminderRegistration {
if (schedule.schedule_type === 'location') {
return {
schedule_id: schedule.id,
time_listener_id: null,
location_listener_id: `mock-location-listener-${schedule.id}`,
alarm_id: null,
};
}

return {
schedule_id: schedule.id,
time_listener_id: `mock-time-listener-${schedule.id}`,
location_listener_id: null,
alarm_id: `mock-alarm-${schedule.id}`,
};
}

/** 真实协调器完成前供应用外壳使用的应用门面。 */
export class MockReminderApplication implements ReminderApplicationPort {
constructor(readonly dependencies: ReminderApplicationDependencies) {}

async start(): Promise<void> {
return Promise.resolve();
}

async stop(): Promise<void> {
return Promise.resolve();
}

async register(schedule: LocalReminderSchedule): Promise<ReminderRegistration> {
return registrationFor(schedule);
}

async rebuild(): Promise<readonly ReminderRegistration[]> {
const schedules = await this.dependencies.schedules.listReminderSchedules();
return schedules.map(registrationFor);
}

async handleTime(_tick: { observed_at: string }): Promise<void> {
return Promise.resolve();
}

async handleLocation(_sample: LocationSample): Promise<void> {
return Promise.resolve();
}

async deliver(trigger: ReminderTrigger): Promise<ReminderDeliveryReceipt> {
return { ...MOCK_DELIVERY, schedule_id: trigger.schedule_id };
}

async confirm(scheduleId: string, confirmedAt: string): Promise<ReminderApplicationResult> {
return {
accepted: true,
schedule_id: scheduleId,
disposition: {
schedule_id: scheduleId,
state: 'confirmed',
updated_at: confirmedAt,
snoozed_until: null,
sync_status: 'pending',
},
};
}

async snooze(request: ReminderSnoozeRequest): Promise<ReminderApplicationResult> {
// 确定性 mock:绝对时间原样回传;相对分钟映射到固定 fixture 时刻。
const snoozed_until =
'snooze_minutes' in request ? '2026-08-07T01:10:00.000Z' : request.snooze_until;

return {
accepted: true,
schedule_id: request.schedule_id,
disposition: {
schedule_id: request.schedule_id,
state: 'snoozed',
updated_at: '2026-08-07T01:00:00.000Z',
snoozed_until,
sync_status: 'pending',
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type {
ReminderConfirmedDisposition,
ReminderDispositionSyncPort,
ReminderDispositionSyncReceipt,
} from '../../application/interfaces';

/** 最终确认状态网络同步回调的模拟实现。 */
export class MockReminderDispositionSync implements ReminderDispositionSyncPort {
async submitConfirmed(
disposition: ReminderConfirmedDisposition,
): Promise<ReminderDispositionSyncReceipt> {
return {
schedule_id: disposition.schedule_id,
accepted: true,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ReminderStateStore } from '../../application/interfaces';
import type { ReminderDisposition, ReminderRuntimeState } from '../../domain';

const MOCK_STATE: ReminderRuntimeState = {
reminder_disposition_state: null,
next_trigger_at: '2026-08-07T01:00:00.000Z',
snoozed_until: null,
geofence_armed: false,
disposition_updated_at: null,
sync_status: 'synced',
recorded_location: null,
};

/** 读取结果固定的本地状态空操作适配器,不打开本地数据库。 */
export class MockReminderStateStore implements ReminderStateStore {
async read(_scheduleId: string): Promise<ReminderRuntimeState | null> {
return { ...MOCK_STATE };
}

async write(_scheduleId: string, _state: ReminderRuntimeState): Promise<void> {
return Promise.resolve();
}

async setDisposition(_scheduleId: string, _disposition: ReminderDisposition): Promise<void> {
return Promise.resolve();
}
}
5 changes: 5 additions & 0 deletions frontend/src/features/reminder/data/local/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { MockLocalScheduleReader } from './MockLocalScheduleReader';
export { MockReminderApplication } from './MockReminderApplication';
export { MockReminderDispositionSync } from './MockReminderDispositionSync';
export { MockReminderStateStore } from './MockReminderStateStore';
export { MOCK_REMINDER_SCHEDULES } from './mockReminderSchedules';
73 changes: 73 additions & 0 deletions frontend/src/features/reminder/data/local/mockReminderSchedules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { LocalReminderSchedule } from '../../domain';

const MOCK_RUNTIME = {
reminder_disposition_state: null,
next_trigger_at: '2026-08-07T01:00:00.000Z',
snoozed_until: null,
geofence_armed: false,
disposition_updated_at: null,
sync_status: 'synced' as const,
recorded_location: null,
};

/** 接入本地数据库读取器前使用的固定快照。 */
export const MOCK_REMINDER_SCHEDULES: readonly LocalReminderSchedule[] = [
{
id: 'mock-schedule-time-001',
account_id: 'mock-account-001',
title: '项目例会',
schedule_type: 'time',
schedule_kind: 'once',
is_all_day: false,
start_time: '2026-08-07T01:15:00.000Z',
end_time: null,
timezone: 'Asia/Shanghai',
recurrence_rule: null,
location_name: '203 会议室',
latitude: null,
longitude: null,
geofence_radius_meters: 100,
reminder: {
reminder_type: 'before_start',
reminder_trigger_at: null,
reminder_offset_minutes: 15,
reminder_strength: 'medium',
},
runtime: { ...MOCK_RUNTIME },
status: 'active',
revision: 1,
cloud_revision: 1,
updated_at: '2026-08-06T09:00:00.000Z',
},
{
id: 'mock-schedule-location-001',
account_id: 'mock-account-001',
title: '取车提醒',
schedule_type: 'location',
schedule_kind: 'once',
is_all_day: false,
start_time: null,
end_time: null,
timezone: 'Asia/Shanghai',
recurrence_rule: null,
location_name: '停车场',
latitude: 31.2304,
longitude: 121.4737,
geofence_radius_meters: 100,
reminder: {
reminder_type: 'return_to_recorded_location',
reminder_trigger_at: null,
reminder_offset_minutes: null,
reminder_strength: 'high',
},
runtime: {
...MOCK_RUNTIME,
next_trigger_at: null,
recorded_location: { latitude: 31.2304, longitude: 121.4737 },
},
status: 'active',
revision: 1,
cloud_revision: 1,
updated_at: '2026-08-06T09:00:00.000Z',
},
];
72 changes: 72 additions & 0 deletions frontend/src/features/reminder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export type {
DeliveryChannel,
GeoPoint,
LocalReminderSchedule,
LocalScheduleKind,
LocalScheduleStatus,
LocalScheduleType,
LocationSample,
ReminderConfiguration,
ReminderDeliveryReceipt,
ReminderDeliveryRequest,
ReminderDisposition,
ReminderDispositionState,
ReminderRegistration,
ReminderRuntimeState,
ReminderStrength,
ReminderSyncStatus,
ReminderTrigger,
ReminderTriggerReason,
ReminderType,
} from './domain';
export { DEFAULT_SNOOZE_MINUTES } from './domain';
export type {
AlarmScheduleReceipt,
AlarmScheduleRequest,
AlarmSchedulerPort,
AudioPlaybackPort,
AudioPlaybackReceipt,
AudioPlaybackRequest,
DeviceCapabilityPort,
DeviceCapabilityStatus,
DevicePermission,
LocalScheduleReader,
LocalTimeTick,
LocationMonitorEvent,
LocationMonitorPort,
LocationWatchHandle,
LocationWatchMode,
LocationWatchRequest,
PopupPort,
PopupReceipt,
PopupRequest,
ReminderApplicationDependencies,
ReminderApplicationPort,
ReminderApplicationResult,
ReminderConfirmedDisposition,
ReminderDeliveryPort,
ReminderDispositionSyncPort,
ReminderDispositionSyncReceipt,
ReminderPresentationAction,
ReminderPresentationReceipt,
ReminderPresenterPort,
ReminderRecoveryPort,
ReminderRecoveryReceipt,
ReminderSnoozeRequest,
ReminderStateStore,
SystemNotificationPort,
SystemNotificationReceipt,
SystemNotificationRequest,
TimeListenerHandle,
TimeListenerOptions,
TimeListenerPort,
VibrationPort,
} from './application';
export {
MockLocalScheduleReader,
MockReminderApplication,
MockReminderDispositionSync,
MockReminderStateStore,
MOCK_REMINDER_SCHEDULES,
} from './data/local';
export { MockReminderPresenter } from './presentation';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {
ReminderPresenterPort,
ReminderPresentationAction,
ReminderPresentationReceipt,
} from '../application/interfaces';
import type { ReminderDeliveryRequest } from '../domain';

/** 应用级弹窗/悬浮层完成前使用的固定展示器。 */
export class MockReminderPresenter implements ReminderPresenterPort {
async show(_request: ReminderDeliveryRequest): Promise<ReminderPresentationReceipt> {
return {
presentation_id: 'mock-presentation-001',
visible: true,
};
}

async hide(_scheduleId: string): Promise<void> {
return Promise.resolve();
}

onAction(
_listener: (event: { schedule_id: string; action: ReminderPresentationAction }) => void,
): () => void {
return () => undefined;
}
}
14 changes: 14 additions & 0 deletions frontend/src/features/reminder/presentation/ReminderViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ReminderPresentationAction } from '../application/interfaces';
import type { ReminderDeliveryRequest } from '../domain';

/** 仅供视图使用的模型;界面悬浮层无需了解端口即可消费。 */
export type ReminderViewModel = {
schedule_id: string;
title: string;
strength: ReminderDeliveryRequest['strength'];
trigger_reason: ReminderDeliveryRequest['trigger']['reason'];
visible: boolean;
actions: readonly ReminderPresentationAction[];
};

export type ReminderActionHandler = (action: ReminderPresentationAction) => void;
2 changes: 2 additions & 0 deletions frontend/src/features/reminder/presentation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { MockReminderPresenter } from './MockReminderPresenter';
export type { ReminderActionHandler, ReminderViewModel } from './ReminderViewModel';
Loading
Loading