Files
yunzerwebsiteallinone/uniapp/pages/tools/schedule/edit.vue
T
2026-07-20 11:41:39 +08:00

468 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view v-if="isCompleted" class="completed-notice">
<FaIcon name="circle-check" color="#67c23a" :size="40" />
<text>该日程已完成不可编辑</text>
</view>
<view class="form-card" :class="{ 'is-disabled': isCompleted }">
<view class="field">
<text class="field-label">日程内容 <text class="required">*</text></text>
<textarea
class="field-textarea content-textarea"
v-model="form.content"
placeholder="输入日程内容,支持多行"
placeholder-class="placeholder"
maxlength="1000"
:show-confirm-bar="false"
:disabled="isCompleted"
/>
<text class="char-count">{{ form.content.length }}/1000</text>
</view>
<view class="field">
<text class="field-label">发生日期 <text class="required">*</text></text>
<picker mode="date" :value="form.date" @change="onDateChange" :disabled="isCompleted">
<view class="picker-value">
<text>{{ form.date || '选择日期' }}</text>
<FaIcon name="calendar-days" color="#909399" :size="16" />
</view>
</picker>
</view>
<view class="field">
<text class="field-label">发生时间 <text class="required">*</text></text>
<picker mode="time" :value="form.time" @change="onTimeChange" :disabled="isCompleted">
<view class="picker-value">
<text>{{ form.time || '选择时间' }}</text>
<FaIcon name="clock" color="#909399" :size="16" />
</view>
</picker>
</view>
<view class="field">
<text class="field-label">提醒渠道</text>
<text class="field-hint">可多选接入接口后按渠道下发提醒</text>
<view class="channel-grid">
<view
v-for="channel in channelOptions"
:key="channel.value"
class="channel-chip"
:class="{ active: form.remindChannels.includes(channel.value) }"
@tap="!isCompleted && toggleChannel(channel.value)"
>
<text>{{ channel.label }}</text>
</view>
</view>
</view>
<view class="field picker-field">
<text class="field-label">提前提醒时间</text>
<picker :range="remindLabels" :value="remindIndex" @change="onRemindChange" :disabled="isCompleted">
<view class="picker-value">
<text>{{ remindLabels[remindIndex] }}</text>
<FaIcon name="bell" color="#909399" :size="16" />
</view>
</picker>
</view>
<view class="field">
<text class="field-label">重复提醒间隔分钟</text>
<view class="number-input">
<button class="number-btn" @tap="form.repeatIntervalMinutes = Math.max(0, form.repeatIntervalMinutes - 1)" :disabled="isCompleted"></button>
<input
v-model.number="form.repeatIntervalMinutes"
type="number"
class="number-field"
:disabled="isCompleted"
@blur="form.repeatIntervalMinutes = Math.max(0, Math.min(1440, form.repeatIntervalMinutes))"
/>
<button class="number-btn" @tap="form.repeatIntervalMinutes = Math.min(1440, form.repeatIntervalMinutes + 1)" :disabled="isCompleted">+</button>
</view>
<text class="form-tip">0表示不重复仅发送一次</text>
</view>
<view class="field">
<text class="field-label">最多提醒次数</text>
<view class="number-input">
<button class="number-btn" @tap="form.maxSendCount = Math.max(1, form.maxSendCount - 1)" :disabled="isCompleted"></button>
<input
v-model.number="form.maxSendCount"
type="number"
class="number-field"
:disabled="isCompleted"
@blur="form.maxSendCount = Math.max(1, Math.min(100, form.maxSendCount))"
/>
<button class="number-btn" @tap="form.maxSendCount = Math.min(100, form.maxSendCount + 1)" :disabled="isCompleted">+</button>
</view>
<text class="form-tip">最多发送的提醒次数</text>
</view>
</view>
<view v-if="!isCompleted" class="footer">
<view class="btn-primary full" :class="{ disabled: saving }" @tap="onSave">
{{ saving ? '保存中...' : '保存' }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
getSchedule,
createSchedule,
updateSchedule,
REMIND_CHANNEL_OPTIONS,
REMIND_MINUTES_OPTIONS
} from '@/api/schedule.js'
import { formatDate, pad } from '@/utils/date.js'
const scheduleId = ref('')
const saving = ref(false)
const isCompleted = ref(false)
const channelOptions = REMIND_CHANNEL_OPTIONS
const remindOptions = REMIND_MINUTES_OPTIONS
const remindLabels = remindOptions.map(item => item.label)
const form = reactive({
content: '',
date: '',
time: '',
remindChannels: ['app'],
remindMinutes: 0,
repeatIntervalMinutes: 0,
maxSendCount: 1
})
const remindIndex = computed(() => {
const index = remindOptions.findIndex(item => item.value === form.remindMinutes)
return index >= 0 ? index : 3
})
onLoad(async (query) => {
const now = new Date()
form.date = formatDate(now)
form.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
if (query?.id) {
scheduleId.value = query.id
uni.setNavigationBarTitle({ title: '编辑日程' })
await loadSchedule(query.id)
} else {
uni.setNavigationBarTitle({ title: '新建日程' })
}
})
async function loadSchedule(id) {
try {
const item = await getSchedule(id)
if (!item) {
uni.showToast({ title: '日程不存在', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
return
}
const d = new Date(item.datetime)
form.content = item.content || ''
form.date = formatDate(d)
form.time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
form.remindChannels = [...(item.remindChannels || [])]
form.remindMinutes = item.remindMinutes ?? 0
form.repeatIntervalMinutes = item.repeatIntervalMinutes ?? 0
form.maxSendCount = item.maxSendCount ?? 1
isCompleted.value = !!item.completed
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
}
function toggleChannel(value) {
const index = form.remindChannels.indexOf(value)
if (index >= 0) {
form.remindChannels.splice(index, 1)
} else {
form.remindChannels.push(value)
}
}
function onDateChange(e) {
form.date = e.detail.value
}
function onTimeChange(e) {
form.time = e.detail.value
}
function onRemindChange(e) {
const index = Number(e.detail.value)
form.remindMinutes = remindOptions[index].value
}
function buildDatetime() {
const [year, month, day] = form.date.split('-').map(Number)
const [hour, minute] = form.time.split(':').map(Number)
return new Date(year, month - 1, day, hour, minute).getTime()
}
async function onSave() {
if (saving.value) return
if (!form.content.trim()) {
uni.showToast({ title: '请输入日程内容', icon: 'none' })
return
}
if (!form.date || !form.time) {
uni.showToast({ title: '请选择发生时间', icon: 'none' })
return
}
const datetime = buildDatetime()
if (datetime < Date.now() - 60000 && !scheduleId.value) {
uni.showModal({
title: '时间提示',
content: '发生时间已过去,仍要保存吗?',
success: async (res) => {
if (res.confirm) await saveData(datetime)
}
})
return
}
await saveData(datetime)
}
async function saveData(datetime) {
saving.value = true
const payload = {
content: form.content,
datetime,
remindChannels: [...form.remindChannels],
remindMinutes: form.remindMinutes,
repeatIntervalMinutes: form.repeatIntervalMinutes,
maxSendCount: form.maxSendCount
}
try {
if (scheduleId.value) {
await updateSchedule(scheduleId.value, payload)
} else {
await createSchedule(payload)
}
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 400)
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
} finally {
saving.value = false
}
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding: 24rpx $page-padding-x;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
}
.completed-notice {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 20rpx 0;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #67c23a;
background: #f0f9eb;
border-radius: $radius-md;
}
.is-disabled {
opacity: 0.7;
pointer-events: none;
}
.form-card {
@include card;
padding: 8rpx 0;
}
.field {
padding: 24rpx 28rpx;
& + & {
border-top: 1rpx solid $color-divider;
}
}
.field-label {
display: block;
font-size: 24rpx;
color: $color-text-muted;
margin-bottom: 12rpx;
}
.field-hint {
display: block;
font-size: 22rpx;
color: $color-text-muted;
margin-bottom: 16rpx;
}
.required {
color: #f56c6c;
}
.field-textarea {
width: 100%;
font-size: 30rpx;
color: $color-text;
line-height: 1.6;
}
.content-textarea {
height: 240rpx;
}
.placeholder {
color: $color-text-muted;
}
.char-count {
display: block;
text-align: right;
font-size: 22rpx;
color: $color-text-muted;
margin-top: 12rpx;
}
.picker-value {
display: flex;
align-items: center;
justify-content: space-between;
height: 72rpx;
padding: 0 20rpx;
background: $color-bg-page;
border-radius: $radius-md;
font-size: 28rpx;
color: $color-text;
}
.channel-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.channel-chip {
padding: 14rpx 28rpx;
border-radius: $radius-full;
font-size: 26rpx;
color: $color-text-secondary;
background: $color-bg-page;
border: 1rpx solid transparent;
&.active {
color: $color-primary;
background: $color-primary-bg;
border-color: $color-primary;
font-weight: 500;
}
}
.number-input {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.number-btn {
width: 60rpx;
height: 60rpx;
border-radius: $radius-md;
background: $color-bg-page;
border: 1rpx solid $color-divider;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: $color-text;
flex-shrink: 0;
&:active:not(:disabled) {
background: $color-primary-bg;
color: $color-primary;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.number-field {
flex: 1;
height: 60rpx;
padding: 0 16rpx;
background: $color-bg-page;
border: 1rpx solid $color-divider;
border-radius: $radius-md;
font-size: 28rpx;
color: $color-text;
text-align: center;
&:disabled {
opacity: 0.5;
}
}
.form-tip {
display: block;
font-size: 22rpx;
color: $color-text-muted;
margin-top: 8rpx;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 20rpx;
padding: 20rpx $page-padding-x;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: $color-card;
border-top: 1rpx solid $color-border;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.btn-primary {
flex: 1;
height: 88rpx;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
background: $color-primary;
color: #fff;
&:active {
background: $color-primary-dark;
}
&.disabled {
opacity: 0.6;
}
&.full {
flex: 1;
}
}
</style>