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;
+}