feat(supervisor): add reminder settings UI and test trigger
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user