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) {
|
export function revokeSupervisorBinding(owner_uid, supervisor_uid) {
|
||||||
return request.request({ url: '/supervisor/revoke', method: 'POST', data: { owner_uid, supervisor_uid }, baseUrl: BASE_URL_V2 })
|
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>
|
</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">
|
<view class="footer">
|
||||||
<button class="btn btn-ghost" :disabled="loading" @tap="refreshAll">刷新</button>
|
<button class="btn btn-ghost" :disabled="loading" @tap="refreshAll">刷新</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -98,7 +159,15 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useLogin } from '@/hooks/useLogin'
|
import { useLogin } from '@/hooks/useLogin'
|
||||||
import { useUserStore } from '@/stores/user'
|
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 { waitForLogin } = useLogin()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -113,6 +182,14 @@ const supervisorItems = ref([])
|
|||||||
|
|
||||||
const defaultAvatar = 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png'
|
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) {
|
function formatDateTime(value) {
|
||||||
if (!value) return '--'
|
if (!value) return '--'
|
||||||
const d = new Date(value)
|
const d = new Date(value)
|
||||||
@@ -226,9 +303,14 @@ async function refreshAll() {
|
|||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [overview, status] = await Promise.all([getSupervisorOverview(), getSupervisorStatus()])
|
const [overview, status, reminder] = await Promise.all([
|
||||||
|
getSupervisorOverview(),
|
||||||
|
getSupervisorStatus(),
|
||||||
|
getSupervisorReminderSettings()
|
||||||
|
])
|
||||||
overviewItems.value = overview.data?.items || []
|
overviewItems.value = overview.data?.items || []
|
||||||
supervisorItems.value = status.data?.items || []
|
supervisorItems.value = status.data?.items || []
|
||||||
|
applyReminderSettings(reminder.data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('refreshAll error:', e)
|
console.error('refreshAll error:', e)
|
||||||
uni.showToast({ title: '刷新失败', icon: 'none' })
|
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(() => {})
|
onMounted(() => {})
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
@@ -493,4 +663,78 @@ onShow(async () => {
|
|||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
border: 1rpx solid rgba(15, 118, 110, 0.25);
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user