741 lines
17 KiB
Vue
741 lines
17 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="header">
|
||
<text class="title">监督人机制</text>
|
||
<text class="subtitle">邀请朋友监督你的戒烟旅程,或者你来监督别人</text>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-head">
|
||
<text class="card-title">邀请监督人</text>
|
||
<text class="card-meta">已绑定 {{ supervisorItems.length }}/3</text>
|
||
</view>
|
||
|
||
<view v-if="inviteToken" class="invite-box">
|
||
<text class="invite-label">邀请口令</text>
|
||
<text class="invite-token">{{ inviteToken }}</text>
|
||
<text class="invite-hint">对方打开小程序后,进入“绑定监督”页面输入口令即可</text>
|
||
|
||
<view class="invite-actions">
|
||
<button class="btn btn-ghost" @tap="copyInviteToken">复制口令</button>
|
||
<button class="btn" @tap="copyInvitePath">复制链接</button>
|
||
</view>
|
||
|
||
<text v-if="inviteExpireAt" class="invite-expire">过期时间:{{ formatDateTime(inviteExpireAt) }}</text>
|
||
</view>
|
||
|
||
<view v-else class="invite-empty">
|
||
<text class="invite-empty-text">
|
||
{{ supervisorItems.length >= 3 ? '监督人已满(最多 3 人),你可以先解除一个再邀请' : '生成一个邀请口令,发给你信得过的人' }}
|
||
</text>
|
||
<button class="btn" :disabled="inviteLoading || supervisorItems.length >= 3" @tap="generateInvite">
|
||
{{ supervisorItems.length >= 3 ? '已达上限' : (inviteLoading ? '生成中...' : '生成邀请口令') }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-head">
|
||
<text class="card-title">我监督的人</text>
|
||
<text class="card-meta">{{ overviewItems.length }} 人</text>
|
||
</view>
|
||
|
||
<view v-if="overviewItems.length === 0" class="empty">
|
||
<text class="empty-text">还没有绑定监督关系</text>
|
||
<text class="empty-hint">收到口令后可去“绑定监督”页面完成绑定</text>
|
||
<button class="btn btn-ghost" @tap="gotoBindPage">去绑定监督</button>
|
||
</view>
|
||
|
||
<view v-else class="list">
|
||
<view v-for="item in overviewItems" :key="item.owner?.user_id" class="row">
|
||
<image class="avatar" :src="item.owner?.avatar_url || defaultAvatar" mode="aspectFill"></image>
|
||
<view class="row-main">
|
||
<text class="name">{{ item.owner?.nickname || `用户 ${item.owner?.user_id}` }}</text>
|
||
<view class="meta">
|
||
<text class="pill">连续 {{ item.home?.summary?.current_streak_days || 0 }} 天</text>
|
||
<text class="pill">HP {{ item.home?.summary?.hp_current ?? '--' }}</text>
|
||
<text class="pill" :class="hpDeltaClass(item.home?.summary?.hp_change_today)">
|
||
{{ hpDeltaText(item.home?.summary?.hp_change_today) }}
|
||
</text>
|
||
</view>
|
||
<text class="status">今日:{{ statusText(item.home?.daily_status?.status) }}</text>
|
||
<view class="row-actions">
|
||
<button class="mini-btn" @tap.stop="confirmRevoke(item)">解除监督</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-head">
|
||
<text class="card-title">监督我的人</text>
|
||
<text class="card-meta">{{ supervisorItems.length }} 人</text>
|
||
</view>
|
||
<view v-if="supervisorItems.length === 0" class="empty">
|
||
<text class="empty-text">还没有人监督你</text>
|
||
<text class="empty-hint">你可以先生成邀请口令发送给朋友</text>
|
||
</view>
|
||
<view v-else class="list">
|
||
<view v-for="u in supervisorItems" :key="u.user_id" class="row">
|
||
<image class="avatar" :src="u.avatar_url || defaultAvatar" mode="aspectFill"></image>
|
||
<view class="row-main">
|
||
<text class="name">{{ u.nickname || `用户 ${u.user_id}` }}</text>
|
||
<text class="status">可查看你的戒烟概览</text>
|
||
</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">
|
||
<button class="btn btn-ghost" :disabled="loading" @tap="refreshAll">刷新</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
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,
|
||
getSupervisorReminderSettings,
|
||
updateSupervisorReminderSettings,
|
||
runSupervisorReminders
|
||
} from '@/api/smoke'
|
||
|
||
const { waitForLogin } = useLogin()
|
||
const userStore = useUserStore()
|
||
|
||
const loading = ref(false)
|
||
const inviteLoading = ref(false)
|
||
const inviteToken = ref('')
|
||
const inviteExpireAt = ref('')
|
||
|
||
const overviewItems = ref([])
|
||
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)
|
||
if (Number.isNaN(d.getTime())) return value
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const day = String(d.getDate()).padStart(2, '0')
|
||
const hh = String(d.getHours()).padStart(2, '0')
|
||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||
return `${y}-${m}-${day} ${hh}:${mm}`
|
||
}
|
||
|
||
function statusText(status) {
|
||
if (status === 'checked_in') return '已打卡'
|
||
if (status === 'relapsed') return '已复吸'
|
||
if (status === 'pending') return '未打卡'
|
||
if (status === 'missed') return '缺失'
|
||
return status || '--'
|
||
}
|
||
|
||
function hpDeltaText(delta) {
|
||
const n = Number(delta)
|
||
if (Number.isNaN(n) || n === 0) return '今日 0'
|
||
return n > 0 ? `今日 +${n}` : `今日 ${n}`
|
||
}
|
||
|
||
function hpDeltaClass(delta) {
|
||
const n = Number(delta)
|
||
if (Number.isNaN(n) || n === 0) return 'pill-muted'
|
||
return n > 0 ? 'pill-up' : 'pill-down'
|
||
}
|
||
|
||
function copy(text) {
|
||
if (!text) return
|
||
uni.setClipboardData({
|
||
data: text,
|
||
success: () => uni.showToast({ title: '已复制', icon: 'success' })
|
||
})
|
||
}
|
||
|
||
function invitePath() {
|
||
if (!inviteToken.value) return ''
|
||
return `/pages/supervisor/bind?token=${inviteToken.value}`
|
||
}
|
||
|
||
function copyInviteToken() {
|
||
copy(inviteToken.value)
|
||
}
|
||
|
||
function copyInvitePath() {
|
||
copy(invitePath())
|
||
}
|
||
|
||
function gotoBindPage() {
|
||
uni.navigateTo({ url: '/pages/supervisor/bind' })
|
||
}
|
||
|
||
function confirmRevoke(item) {
|
||
const ownerUID = item?.owner?.user_id
|
||
if (!ownerUID) return
|
||
uni.showModal({
|
||
title: '解除监督',
|
||
content: '解除后你将无法再查看对方的戒烟概览。确定要解除吗?',
|
||
confirmText: '解除',
|
||
confirmColor: '#b91c1c',
|
||
success: async (res) => {
|
||
if (!res.confirm) return
|
||
await doRevoke(ownerUID)
|
||
}
|
||
})
|
||
}
|
||
|
||
async function doRevoke(ownerUID) {
|
||
try {
|
||
loading.value = true
|
||
const myUID = Number(userStore.user?.id)
|
||
if (!myUID) {
|
||
uni.showToast({ title: '登录信息缺失', icon: 'none' })
|
||
return
|
||
}
|
||
await revokeSupervisorBinding(ownerUID, myUID)
|
||
uni.showToast({ title: '已解除', icon: 'success' })
|
||
await refreshAll()
|
||
} catch (e) {
|
||
console.error('revoke error:', e)
|
||
uni.showToast({ title: '解除失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function generateInvite() {
|
||
if (inviteLoading.value) return
|
||
inviteLoading.value = true
|
||
try {
|
||
const res = await createSupervisorInvite(7)
|
||
inviteToken.value = res.data?.token || ''
|
||
inviteExpireAt.value = res.data?.expire_at || ''
|
||
if (!inviteToken.value) {
|
||
uni.showToast({ title: '生成失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
console.error('generateInvite error:', e)
|
||
uni.showToast({ title: '生成失败', icon: 'none' })
|
||
} finally {
|
||
inviteLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function refreshAll() {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
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' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
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 () => {
|
||
await waitForLogin()
|
||
await refreshAll()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
padding: 28rpx 28rpx 40rpx;
|
||
box-sizing: border-box;
|
||
background: linear-gradient(180deg, #eef7f3 0%, #f7faf8 40%, #fbfdff 100%);
|
||
}
|
||
|
||
.header {
|
||
padding: 8rpx 6rpx 18rpx;
|
||
}
|
||
|
||
.title {
|
||
display: block;
|
||
font-size: 40rpx;
|
||
font-weight: 800;
|
||
color: #0f172a;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
|
||
.subtitle {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.6;
|
||
color: #64748b;
|
||
}
|
||
|
||
.card {
|
||
margin-top: 18rpx;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
border-radius: 26rpx;
|
||
border: 1rpx solid rgba(15, 23, 42, 0.06);
|
||
padding: 22rpx 22rpx;
|
||
box-shadow: 0 10rpx 26rpx rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
.card-head {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 28rpx;
|
||
font-weight: 800;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.card-meta {
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.invite-box {
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.invite-label {
|
||
display: block;
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
.invite-token {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 36rpx;
|
||
font-weight: 900;
|
||
letter-spacing: 2rpx;
|
||
color: #0f766e;
|
||
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
||
}
|
||
|
||
.invite-hint {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 22rpx;
|
||
line-height: 1.6;
|
||
color: #475569;
|
||
}
|
||
|
||
.invite-actions {
|
||
display: flex;
|
||
gap: 14rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.invite-expire {
|
||
display: block;
|
||
margin-top: 14rpx;
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.invite-empty {
|
||
margin-top: 18rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.invite-empty-text {
|
||
font-size: 24rpx;
|
||
line-height: 1.6;
|
||
color: #475569;
|
||
}
|
||
|
||
.empty {
|
||
margin-top: 18rpx;
|
||
padding: 16rpx 6rpx 6rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.empty-hint {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 22rpx;
|
||
line-height: 1.6;
|
||
color: #64748b;
|
||
}
|
||
|
||
.list {
|
||
margin-top: 18rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
align-items: flex-start;
|
||
padding: 16rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(241, 245, 249, 0.6);
|
||
border: 1rpx solid rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.avatar {
|
||
width: 74rpx;
|
||
height: 74rpx;
|
||
border-radius: 50%;
|
||
background: #e2e8f0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.row-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.name {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
font-weight: 800;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.meta {
|
||
margin-top: 10rpx;
|
||
display: flex;
|
||
gap: 10rpx;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.pill {
|
||
padding: 8rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(15, 23, 42, 0.06);
|
||
font-size: 20rpx;
|
||
color: #334155;
|
||
}
|
||
|
||
.pill-muted {
|
||
color: #64748b;
|
||
}
|
||
|
||
.pill-up {
|
||
color: #0f766e;
|
||
background: rgba(204, 251, 241, 0.6);
|
||
}
|
||
|
||
.pill-down {
|
||
color: #b91c1c;
|
||
background: rgba(254, 226, 226, 0.8);
|
||
}
|
||
|
||
.status {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
.row-actions {
|
||
margin-top: 12rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.mini-btn {
|
||
height: 56rpx;
|
||
line-height: 56rpx;
|
||
padding: 0 18rpx;
|
||
border-radius: 14rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(185, 28, 28, 0.28);
|
||
color: #b91c1c;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.footer {
|
||
margin-top: 20rpx;
|
||
padding-bottom: 20rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.btn {
|
||
height: 76rpx;
|
||
line-height: 76rpx;
|
||
padding: 0 22rpx;
|
||
border-radius: 18rpx;
|
||
background: linear-gradient(180deg, #1aa37a 0%, #0f766e 100%);
|
||
color: #ffffff;
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.btn[disabled] {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.btn-ghost {
|
||
background: #ffffff;
|
||
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>
|