357 lines
9.4 KiB
Vue
357 lines
9.4 KiB
Vue
<template>
|
|
<view class="schedule-page">
|
|
<scroll-view class="schedule-scroll" scroll-y>
|
|
<view class="hero-card">
|
|
<view>
|
|
<text class="hero-title">{{ pageTitle }}</text>
|
|
<text class="hero-desc">为疫苗、驱虫、体检或自定义事项设置提醒。</text>
|
|
</view>
|
|
<TnIcon name="calendar-fill" :size="54" color="#F97316" />
|
|
</view>
|
|
|
|
<view class="form-card">
|
|
<view class="form-row" @click="showPetPicker = true">
|
|
<text class="form-label required">宠物</text>
|
|
<text class="form-value" :class="{ placeholder: !form.petId }">{{ selectedPetName || '请选择' }}</text>
|
|
<TnIcon name="right" :size="28" color="#CBD5E1" />
|
|
</view>
|
|
<view class="divider" />
|
|
<view class="form-row">
|
|
<text class="form-label required">标题</text>
|
|
<TnInput v-model="form.title" placeholder="如:下一次驱虫" :border="false" text-align="right" />
|
|
</view>
|
|
<view class="divider" />
|
|
<view class="form-row" @click="showDatePicker = true">
|
|
<text class="form-label required">日期</text>
|
|
<text class="form-value" :class="{ placeholder: !form.scheduledDate }">{{ form.scheduledDate || '请选择' }}</text>
|
|
<TnIcon name="calendar" :size="28" color="#CBD5E1" />
|
|
</view>
|
|
<view class="divider" />
|
|
<view class="form-row">
|
|
<text class="form-label">备注</text>
|
|
<TnInput v-model="form.note" placeholder="可填写提醒说明" :border="false" text-align="right" />
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<view class="bottom-bar" :style="{ paddingBottom: safeBottom + 'px' }">
|
|
<TnButton text-color="#FFFFFF" shape="round" :bold="true" :custom-style="primaryButtonStyle"
|
|
:loading="submitting" :disabled="submitting || !canSubmit"
|
|
width="100%" height="96" :font-size="30" @click="handleSubmit">
|
|
{{ submitText }}
|
|
</TnButton>
|
|
</view>
|
|
|
|
<TnPopup v-model="showPetPicker" open-direction="bottom" :radius="32" :safe-area-inset-bottom="true">
|
|
<view class="picker-panel">
|
|
<view class="panel-header">
|
|
<text class="panel-title">选择宠物</text>
|
|
<view class="panel-close" @click="showPetPicker = false">
|
|
<TnIcon name="close" :size="32" color="#64748B" />
|
|
</view>
|
|
</view>
|
|
<view v-if="petList.length === 0" class="picker-empty">暂无宠物,请先添加</view>
|
|
<view v-for="pet in petList" :key="pet._id" class="pet-option" @click="selectPet(pet._id)">
|
|
<TnAvatar :url="pet._displayAvatar || ''" :icon="pet._displayAvatar ? '' : 'floral'"
|
|
:size="72" shape="circle" bg-color="#FFF7ED" />
|
|
<text class="pet-name">{{ pet.name }}</text>
|
|
<TnIcon v-if="form.petId === pet._id" name="success-circle-fill" :size="36" color="#F97316" />
|
|
<TnIcon v-else name="circle" :size="36" color="#E2E8F0" />
|
|
</view>
|
|
</view>
|
|
</TnPopup>
|
|
|
|
<TnDateTimePicker v-model="form.scheduledDate" v-model:open="showDatePicker"
|
|
mode="date" :min-time="todayStr"
|
|
@confirm="showDatePicker = false" @cancel="showDatePicker = false" />
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, reactive, ref } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import TnAvatar from '@/uni_modules/tuniaoui-vue3/components/avatar/src/avatar.vue'
|
|
import TnButton from '@/uni_modules/tuniaoui-vue3/components/button/src/button.vue'
|
|
import TnDateTimePicker from '@/uni_modules/tuniaoui-vue3/components/date-time-picker/src/date-time-picker.vue'
|
|
import TnIcon from '@/uni_modules/tuniaoui-vue3/components/icon/src/icon.vue'
|
|
import TnInput from '@/uni_modules/tuniaoui-vue3/components/input/src/input.vue'
|
|
import TnPopup from '@/uni_modules/tuniaoui-vue3/components/popup/src/popup.vue'
|
|
|
|
interface Pet {
|
|
_id: string
|
|
name: string
|
|
avatarUrl?: string
|
|
_displayAvatar?: string
|
|
}
|
|
|
|
const petList = ref<Pet[]>([])
|
|
const submitting = ref(false)
|
|
const safeBottom = ref(0)
|
|
const showPetPicker = ref(false)
|
|
const showDatePicker = ref(false)
|
|
const scheduleId = ref('')
|
|
const primaryButtonStyle = {
|
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
|
}
|
|
|
|
const form = reactive({
|
|
petId: '',
|
|
type: 'custom',
|
|
title: '',
|
|
scheduledDate: '',
|
|
frequency: 'once',
|
|
note: '',
|
|
})
|
|
|
|
const selectedPetName = computed(() => petList.value.find((pet) => pet._id === form.petId)?.name || '')
|
|
const canSubmit = computed(() => !!(form.petId && form.title.trim() && form.scheduledDate))
|
|
const pageTitle = computed(() => (scheduleId.value ? '编辑提醒' : '添加提醒'))
|
|
const submitText = computed(() => {
|
|
if (submitting.value) return '保存中...'
|
|
return scheduleId.value ? '更新提醒' : '保存提醒'
|
|
})
|
|
const todayStr = computed(() => {
|
|
const d = new Date()
|
|
return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} 00:00:00`
|
|
})
|
|
|
|
onLoad((options: any) => {
|
|
scheduleId.value = options?.id || ''
|
|
const sys = uni.getSystemInfoSync()
|
|
safeBottom.value = sys.safeAreaInsets?.bottom || 0
|
|
uni.setNavigationBarTitle({ title: scheduleId.value ? '编辑提醒' : '添加提醒' })
|
|
loadPageData()
|
|
})
|
|
|
|
async function loadPageData() {
|
|
await loadPets()
|
|
if (scheduleId.value) await loadScheduleDetail()
|
|
}
|
|
|
|
async function loadPets() {
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
if (!uid) return
|
|
const res = await uniCloud.callFunction({ name: 'petManager', data: { action: 'list', uid } })
|
|
if (res.result?.code === 0) {
|
|
petList.value = res.result.data || []
|
|
if (petList.value.length === 1) form.petId = petList.value[0]._id
|
|
}
|
|
} catch (e) {
|
|
console.warn('加载宠物失败', e)
|
|
}
|
|
}
|
|
|
|
async function loadScheduleDetail() {
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
if (!uid || !scheduleId.value) return
|
|
const res = await uniCloud.callFunction({
|
|
name: 'scheduleManager',
|
|
data: { action: 'detail', uid, scheduleId: scheduleId.value },
|
|
})
|
|
if (res.result?.code !== 0 || !res.result.data) {
|
|
throw new Error(res.result?.message || '提醒不存在')
|
|
}
|
|
const data = res.result.data
|
|
form.petId = data.pet_id || ''
|
|
form.type = data.type || 'custom'
|
|
form.title = data.title || ''
|
|
form.scheduledDate = normalizeDateValue(data.scheduledDate)
|
|
form.frequency = data.frequency || 'once'
|
|
form.note = data.note || ''
|
|
} catch (e) {
|
|
console.warn('加载提醒详情失败', e)
|
|
uni.showToast({ title: '提醒不存在或已删除', icon: 'none' })
|
|
}
|
|
}
|
|
|
|
function selectPet(id: string) {
|
|
form.petId = id
|
|
showPetPicker.value = false
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (!canSubmit.value || submitting.value) return
|
|
submitting.value = true
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
const payload: any = {
|
|
action: scheduleId.value ? 'update' : 'add',
|
|
uid,
|
|
pet_id: form.petId,
|
|
type: form.type,
|
|
title: form.title.trim(),
|
|
scheduledDate: form.scheduledDate,
|
|
frequency: form.frequency,
|
|
interval: 1,
|
|
reminderDays: [7, 3, 1],
|
|
note: form.note.trim(),
|
|
}
|
|
if (scheduleId.value) payload.scheduleId = scheduleId.value
|
|
const res = await uniCloud.callFunction({ name: 'scheduleManager', data: payload })
|
|
if (res.result?.code !== 0) throw new Error(res.result?.message || '保存失败')
|
|
uni.showToast({ title: scheduleId.value ? '更新成功' : '添加成功', icon: 'success' })
|
|
setTimeout(() => uni.navigateBack(), 1000)
|
|
} catch (e) {
|
|
console.error('保存提醒失败', e)
|
|
uni.showToast({ title: '保存失败,请稍后再试', icon: 'none' })
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function normalizeDateValue(value: number | string | undefined) {
|
|
if (!value) return ''
|
|
if (typeof value === 'string') return value
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return ''
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
$text-dark: #0F172A;
|
|
$text-muted: #64748B;
|
|
$card-bg: #FFFFFF;
|
|
$shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
|
|
|
|
.schedule-page {
|
|
height: 100vh;
|
|
background: #F8FAFC;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.schedule-scroll {
|
|
flex: 1;
|
|
padding: 24rpx 28rpx 220rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.hero-card,
|
|
.form-card {
|
|
background: $card-bg;
|
|
border-radius: 24rpx;
|
|
box-shadow: $shadow;
|
|
}
|
|
|
|
.hero-card {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
padding: 28rpx;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.hero-title {
|
|
display: block;
|
|
font-size: 36rpx;
|
|
font-weight: 900;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.hero-desc {
|
|
display: block;
|
|
margin-top: 8rpx;
|
|
font-size: 25rpx;
|
|
color: $text-muted;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.form-card {
|
|
padding: 0 28rpx;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
min-height: 92rpx;
|
|
padding: 22rpx 0;
|
|
}
|
|
|
|
.form-label {
|
|
width: 112rpx;
|
|
font-size: 28rpx;
|
|
font-weight: 700;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.required::before {
|
|
content: '*';
|
|
color: #EF4444;
|
|
margin-right: 4rpx;
|
|
}
|
|
|
|
.form-value {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
text-align: right;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.placeholder {
|
|
color: #CBD5E1;
|
|
}
|
|
|
|
.divider {
|
|
height: 1rpx;
|
|
background: #F1F5F9;
|
|
}
|
|
|
|
.bottom-bar {
|
|
padding: 16rpx 28rpx;
|
|
background: #FFFFFF;
|
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.picker-panel {
|
|
padding: 32rpx 28rpx;
|
|
}
|
|
|
|
.panel-header,
|
|
.pet-option {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.panel-header {
|
|
justify-content: space-between;
|
|
margin-bottom: 28rpx;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 32rpx;
|
|
font-weight: 800;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.panel-close {
|
|
width: 56rpx;
|
|
height: 56rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.picker-empty {
|
|
padding: 60rpx 0;
|
|
text-align: center;
|
|
color: $text-muted;
|
|
}
|
|
|
|
.pet-option {
|
|
gap: 18rpx;
|
|
padding: 18rpx 0;
|
|
}
|
|
|
|
.pet-name {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
font-weight: 700;
|
|
color: $text-dark;
|
|
}
|
|
</style>
|