feat(supervisor): add reminder settings UI and test trigger

This commit is contained in:
nepiedg
2026-04-16 13:40:48 +08:00
parent 5928329e76
commit 9c8583a7fc
2 changed files with 259 additions and 2 deletions
+13
View File
@@ -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 })
}
+246 -2
View File
@@ -87,6 +87,67 @@
</view>
</view>
<view class="card">
<view class="card-head">
<text class="card-title">提醒设置</text>
<text class="card-meta">默认关闭</text>
</view>
<view class="settings">
<view class="setting-row">
<text class="setting-label">启用提醒</text>
<switch :checked="reminderEnabled" @change="onToggleReminder" />
</view>
<view class="setting-row">
<text class="setting-label">提醒时间</text>
<view class="setting-control">
<button class="mini-btn mini-btn-neutral" :disabled="!reminderEnabled" @tap="pickNotifyTime">
{{ reminderNotifyTime || '21:00' }}
</button>
</view>
</view>
<view class="setting-row">
<text class="setting-label">每日上限</text>
<view class="setting-control">
<input
class="num-input"
type="number"
:value="String(reminderMaxPerDay)"
:disabled="!reminderEnabled"
placeholder="1"
@input="onMaxPerDayInput"
/>
<text class="setting-hint">每个监督人每天最多 N 建议 1</text>
</view>
</view>
<view class="setting-actions">
<button class="btn btn-ghost" :disabled="savingSettings" @tap="reloadReminderSettings">重载</button>
<button class="btn" :disabled="savingSettings" @tap="saveReminderSettings">
{{ savingSettings ? '保存中...' : '保存设置' }}
</button>
</view>
<text class="settings-note">提示当前版本后端仅记录提醒日志stub尚未接入真实订阅消息发送</text>
</view>
</view>
<view class="card">
<view class="card-head">
<text class="card-title">提醒测试监督人</text>
<text class="card-meta">仅写日志</text>
</view>
<view class="settings">
<text class="settings-note">你作为监督人时可手动触发一次提醒流程用于联调频控与条件判断</text>
<button class="btn" :disabled="runningReminders" @tap="runRemindersNow">
{{ runningReminders ? '触发中...' : '手动触发提醒' }}
</button>
<text v-if="lastRunText" class="run-result">{{ lastRunText }}</text>
</view>
</view>
<view class="footer">
<button class="btn btn-ghost" :disabled="loading" @tap="refreshAll">刷新</button>
</view>
@@ -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;
}
</style>