From 9c8583a7fc6052b91b7511cababef5ab54508326 Mon Sep 17 00:00:00 2001 From: nepiedg Date: Thu, 16 Apr 2026 13:40:48 +0800 Subject: [PATCH] feat(supervisor): add reminder settings UI and test trigger --- src/api/smoke.js | 13 ++ src/pages/supervisor/index.vue | 248 ++++++++++++++++++++++++++++++++- 2 files changed, 259 insertions(+), 2 deletions(-) diff --git a/src/api/smoke.js b/src/api/smoke.js index 44300cf..e8502c3 100644 --- a/src/api/smoke.js +++ b/src/api/smoke.js @@ -152,3 +152,16 @@ export function getSupervisorStatus() { export function revokeSupervisorBinding(owner_uid, supervisor_uid) { return request.request({ url: '/supervisor/revoke', method: 'POST', data: { owner_uid, supervisor_uid }, baseUrl: BASE_URL_V2 }) } + +// 监督提醒(Phase 3 / #42) +export function getSupervisorReminderSettings() { + return request.request({ url: '/supervisor/reminders/settings', method: 'GET', baseUrl: BASE_URL_V2 }) +} + +export function updateSupervisorReminderSettings(data = {}) { + return request.request({ url: '/supervisor/reminders/settings', method: 'PUT', data, baseUrl: BASE_URL_V2 }) +} + +export function runSupervisorReminders() { + return request.request({ url: '/supervisor/reminders/run', method: 'POST', baseUrl: BASE_URL_V2 }) +} diff --git a/src/pages/supervisor/index.vue b/src/pages/supervisor/index.vue index c786baf..a08e10b 100644 --- a/src/pages/supervisor/index.vue +++ b/src/pages/supervisor/index.vue @@ -87,6 +87,67 @@ + + + 提醒设置 + 默认关闭 + + + + + 启用提醒 + + + + + 提醒时间 + + + + + + + 每日上限 + + + 每个监督人每天最多 N 次(建议 1) + + + + + + + + + 提示:当前版本后端仅记录提醒日志(stub),尚未接入真实订阅消息发送。 + + + + + + 提醒测试(监督人) + 仅写日志 + + + 你作为监督人时,可手动触发一次提醒流程(用于联调频控与条件判断)。 + + {{ lastRunText }} + + + @@ -98,7 +159,15 @@ import { ref, onMounted } from 'vue' import { onShow } from '@dcloudio/uni-app' import { useLogin } from '@/hooks/useLogin' import { useUserStore } from '@/stores/user' -import { createSupervisorInvite, getSupervisorOverview, getSupervisorStatus, revokeSupervisorBinding } from '@/api/smoke' +import { + createSupervisorInvite, + getSupervisorOverview, + getSupervisorStatus, + revokeSupervisorBinding, + getSupervisorReminderSettings, + updateSupervisorReminderSettings, + runSupervisorReminders +} from '@/api/smoke' const { waitForLogin } = useLogin() const userStore = useUserStore() @@ -113,6 +182,14 @@ const supervisorItems = ref([]) const defaultAvatar = 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png' +const savingSettings = ref(false) +const reminderEnabled = ref(false) +const reminderNotifyTime = ref('21:00') +const reminderMaxPerDay = ref(1) + +const runningReminders = ref(false) +const lastRunText = ref('') + function formatDateTime(value) { if (!value) return '--' const d = new Date(value) @@ -226,9 +303,14 @@ async function refreshAll() { if (loading.value) return loading.value = true try { - const [overview, status] = await Promise.all([getSupervisorOverview(), getSupervisorStatus()]) + const [overview, status, reminder] = await Promise.all([ + getSupervisorOverview(), + getSupervisorStatus(), + getSupervisorReminderSettings() + ]) overviewItems.value = overview.data?.items || [] supervisorItems.value = status.data?.items || [] + applyReminderSettings(reminder.data) } catch (e) { console.error('refreshAll error:', e) uni.showToast({ title: '刷新失败', icon: 'none' }) @@ -237,6 +319,94 @@ async function refreshAll() { } } +function applyReminderSettings(data) { + reminderEnabled.value = !!data?.enabled + reminderNotifyTime.value = data?.notify_time || '21:00' + const maxN = Number(data?.max_per_day) + reminderMaxPerDay.value = Number.isNaN(maxN) ? 1 : Math.min(Math.max(maxN, 0), 10) +} + +async function reloadReminderSettings() { + try { + const res = await getSupervisorReminderSettings() + applyReminderSettings(res.data) + uni.showToast({ title: '已重载', icon: 'success' }) + } catch (e) { + console.error('reloadReminderSettings error:', e) + uni.showToast({ title: '重载失败', icon: 'none' }) + } +} + +function onToggleReminder(e) { + reminderEnabled.value = !!e?.detail?.value +} + +function pickNotifyTime() { + if (!reminderEnabled.value) return + uni.showModal({ + title: '设置提醒时间', + content: '当前版本请手动输入 HH:MM(例如 21:00)。', + editable: true, + placeholderText: reminderNotifyTime.value || '21:00', + success: (res) => { + if (!res.confirm) return + const v = String(res.content || '').trim() + if (!/^\d{2}:\d{2}$/.test(v)) { + uni.showToast({ title: '格式应为 HH:MM', icon: 'none' }) + return + } + reminderNotifyTime.value = v + } + }) +} + +function onMaxPerDayInput(e) { + const v = Number(e?.detail?.value) + if (Number.isNaN(v)) { + reminderMaxPerDay.value = 1 + return + } + reminderMaxPerDay.value = Math.min(Math.max(Math.floor(v), 0), 10) +} + +async function saveReminderSettings() { + if (savingSettings.value) return + savingSettings.value = true + try { + const payload = { + enabled: reminderEnabled.value, + notify_time: reminderNotifyTime.value, + max_per_day: reminderMaxPerDay.value + } + const res = await updateSupervisorReminderSettings(payload) + applyReminderSettings(res.data) + uni.showToast({ title: '已保存', icon: 'success' }) + } catch (e) { + console.error('saveReminderSettings error:', e) + uni.showToast({ title: '保存失败', icon: 'none' }) + } finally { + savingSettings.value = false + } +} + +async function runRemindersNow() { + if (runningReminders.value) return + runningReminders.value = true + lastRunText.value = '' + try { + const res = await runSupervisorReminders() + const created = res.data?.created ?? 0 + const skipped = res.data?.skipped ?? 0 + lastRunText.value = `本次触发:写入 ${created} 条,跳过 ${skipped} 条` + uni.showToast({ title: '已触发', icon: 'success' }) + } catch (e) { + console.error('runRemindersNow error:', e) + uni.showToast({ title: '触发失败', icon: 'none' }) + } finally { + runningReminders.value = false + } +} + onMounted(() => {}) onShow(async () => { @@ -493,4 +663,78 @@ onShow(async () => { color: #0f766e; border: 1rpx solid rgba(15, 118, 110, 0.25); } + +.mini-btn-neutral { + border-color: rgba(15, 23, 42, 0.12); + color: #334155; +} + +.settings { + margin-top: 18rpx; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.setting-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18rpx; + padding: 14rpx 12rpx; + border-radius: 18rpx; + background: rgba(241, 245, 249, 0.45); + border: 1rpx solid rgba(15, 23, 42, 0.04); +} + +.setting-label { + font-size: 24rpx; + font-weight: 800; + color: #0f172a; + padding-top: 6rpx; +} + +.setting-control { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10rpx; +} + +.num-input { + width: 200rpx; + height: 64rpx; + padding: 0 14rpx; + border-radius: 14rpx; + background: rgba(255, 255, 255, 0.92); + border: 1rpx solid rgba(15, 23, 42, 0.08); + font-size: 26rpx; + text-align: right; +} + +.setting-hint { + font-size: 20rpx; + line-height: 1.5; + color: #64748b; + text-align: right; +} + +.setting-actions { + display: flex; + gap: 14rpx; +} + +.settings-note { + font-size: 22rpx; + line-height: 1.6; + color: #64748b; +} + +.run-result { + font-size: 22rpx; + color: #0f766e; + font-weight: 700; +}