feat: 新增日程管理完整功能模块

1.  新增scheduleManager云函数,实现日程增删改查与权限校验
2.  重构日程页面,支持筛选、分类展示与状态切换
3.  完善提醒编辑页面,支持新增和编辑日程
4.  调整全局安全边距与TabBar样式适配
5.  修复日程查询条件,替换completed判断为status字段
This commit is contained in:
Yuhang Wu 2026-07-31 17:33:38 +08:00
parent c709e72896
commit 78305c00dc
7 changed files with 1321 additions and 25 deletions

View File

@ -359,7 +359,7 @@
const userLocation = ref<{ longitude : number; latitude : number } | null>(null) const userLocation = ref<{ longitude : number; latitude : number } | null>(null)
const hospitals = ref<Hospital[]>([]) const hospitals = ref<Hospital[]>([])
const articles = ref<Article[]>([]) const articles = ref<Article[]>([])
const scrollPaddingBottom = ref(140) const scrollPaddingBottom = ref(78)
const primaryButtonStyle = { const primaryButtonStyle = {
background: 'linear-gradient(135deg, #F97316, #FB923C)', background: 'linear-gradient(135deg, #F97316, #FB923C)',
@ -583,7 +583,7 @@
function calcLayout() { function calcLayout() {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
scrollPaddingBottom.value = (systemInfo.safeAreaInsets?.bottom || 0) + 150 scrollPaddingBottom.value = (systemInfo.safeAreaInsets?.bottom || 0) + 78
} }
async function loadDiscoveryData() { async function loadDiscoveryData() {
@ -944,6 +944,11 @@
.tab-card :deep(.tn-tabs) { .tab-card :deep(.tn-tabs) {
width: 100%; width: 100%;
align-self: center;
}
.tab-card :deep(.tn-tabs__container) {
align-items: center;
} }
.tab-card :deep(.tn-tabs__bar-container) { .tab-card :deep(.tn-tabs__bar-container) {

View File

@ -18,7 +18,7 @@
</view> </view>
</view> </view>
<TnTabbar v-model="currentTabbar" fixed switch-animation> <TnTabbar v-model="currentTabbar" fixed switch-animation :placeholder="false">
<TnTabbarItem <TnTabbarItem
v-for="(item, index) in tabbarData" v-for="(item, index) in tabbarData"
:key="index" :key="index"

View File

@ -261,8 +261,7 @@ onShow(() => {
function calcSafeArea() { function calcSafeArea() {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
// TabBar 50px safeAreaBottom.value = (systemInfo.safeAreaInsets?.bottom || 0) + 78
safeAreaBottom.value = (systemInfo.safeAreaInsets?.bottom || 0) + 20
} }
function formatDate(dateStr: string) { function formatDate(dateStr: string) {

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
<scroll-view class="schedule-scroll" scroll-y> <scroll-view class="schedule-scroll" scroll-y>
<view class="hero-card"> <view class="hero-card">
<view> <view>
<text class="hero-title">添加提醒</text> <text class="hero-title">{{ pageTitle }}</text>
<text class="hero-desc">为疫苗驱虫体检或自定义事项设置提醒</text> <text class="hero-desc">为疫苗驱虫体检或自定义事项设置提醒</text>
</view> </view>
<TnIcon name="calendar-fill" :size="54" color="#F97316" /> <TnIcon name="calendar-fill" :size="54" color="#F97316" />
@ -38,7 +38,7 @@
<TnButton text-color="#FFFFFF" shape="round" :bold="true" :custom-style="primaryButtonStyle" <TnButton text-color="#FFFFFF" shape="round" :bold="true" :custom-style="primaryButtonStyle"
:loading="submitting" :disabled="submitting || !canSubmit" :loading="submitting" :disabled="submitting || !canSubmit"
width="100%" height="96" :font-size="30" @click="handleSubmit"> width="100%" height="96" :font-size="30" @click="handleSubmit">
{{ submitting ? '保存中...' : '保存提醒' }} {{ submitText }}
</TnButton> </TnButton>
</view> </view>
@ -89,30 +89,45 @@ const submitting = ref(false)
const safeBottom = ref(0) const safeBottom = ref(0)
const showPetPicker = ref(false) const showPetPicker = ref(false)
const showDatePicker = ref(false) const showDatePicker = ref(false)
const scheduleId = ref('')
const primaryButtonStyle = { const primaryButtonStyle = {
background: 'linear-gradient(135deg, #F97316, #FB923C)', background: 'linear-gradient(135deg, #F97316, #FB923C)',
} }
const form = reactive({ const form = reactive({
petId: '', petId: '',
type: 'custom',
title: '', title: '',
scheduledDate: '', scheduledDate: '',
frequency: 'once',
note: '', note: '',
}) })
const selectedPetName = computed(() => petList.value.find((pet) => pet._id === form.petId)?.name || '') const selectedPetName = computed(() => petList.value.find((pet) => pet._id === form.petId)?.name || '')
const canSubmit = computed(() => !!(form.petId && form.title.trim() && form.scheduledDate)) 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 todayStr = computed(() => {
const d = new Date() const d = new Date()
return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} 00:00:00` return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} 00:00:00`
}) })
onLoad(() => { onLoad((options: any) => {
scheduleId.value = options?.id || ''
const sys = uni.getSystemInfoSync() const sys = uni.getSystemInfoSync()
safeBottom.value = sys.safeAreaInsets?.bottom || 0 safeBottom.value = sys.safeAreaInsets?.bottom || 0
loadPets() uni.setNavigationBarTitle({ title: scheduleId.value ? '编辑提醒' : '添加提醒' })
loadPageData()
}) })
async function loadPageData() {
await loadPets()
if (scheduleId.value) await loadScheduleDetail()
}
async function loadPets() { async function loadPets() {
try { try {
const uid = uni.getStorageSync('uid') const uid = uni.getStorageSync('uid')
@ -127,6 +142,30 @@ async function loadPets() {
} }
} }
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) { function selectPet(id: string) {
form.petId = id form.petId = id
showPetPicker.value = false showPetPicker.value = false
@ -137,30 +176,38 @@ async function handleSubmit() {
submitting.value = true submitting.value = true
try { try {
const uid = uni.getStorageSync('uid') const uid = uni.getStorageSync('uid')
const db = uniCloud.database() const payload: any = {
await db.collection('schedules').add({ action: scheduleId.value ? 'update' : 'add',
user_id: uid, uid,
pet_id: form.petId, pet_id: form.petId,
type: 'custom', type: form.type,
title: form.title.trim(), title: form.title.trim(),
scheduledDate: form.scheduledDate, scheduledDate: form.scheduledDate,
frequency: 'once', frequency: form.frequency,
interval: 1, interval: 1,
status: 'pending',
reminderDays: [7, 3, 1], reminderDays: [7, 3, 1],
note: form.note.trim(), note: form.note.trim(),
createdAt: Date.now(), }
updatedAt: Date.now(), if (scheduleId.value) payload.scheduleId = scheduleId.value
}) const res = await uniCloud.callFunction({ name: 'scheduleManager', data: payload })
uni.showToast({ title: '添加成功', icon: 'success' }) if (res.result?.code !== 0) throw new Error(res.result?.message || '保存失败')
uni.showToast({ title: scheduleId.value ? '更新成功' : '添加成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1000) setTimeout(() => uni.navigateBack(), 1000)
} catch (e) { } catch (e) {
console.error('保存提醒失败', e) console.error('保存提醒失败', e)
uni.showToast({ title: '保存失败,请检查云权限', icon: 'none' }) uni.showToast({ title: '保存失败,请稍后再试', icon: 'none' })
} finally { } finally {
submitting.value = false 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -0,0 +1,154 @@
'use strict'
const db = uniCloud.database()
const allowedUpdateFields = [
'pet_id',
'type',
'title',
'scheduledDate',
'frequency',
'interval',
'status',
'reminderDays',
'relatedRecordId',
'note'
]
exports.main = async (event, context) => {
const { action, uid } = event
if (!uid) {
return { code: -1, message: '缺少用户标识' }
}
try {
switch (action) {
case 'list': {
const { status, petId, page, pageSize } = event
const where = { user_id: uid }
if (status && status !== 'all') where.status = status
if (petId) where.pet_id = petId
const p = page || 1
const ps = pageSize || 100
const skip = (p - 1) * ps
const res = await db.collection('schedules')
.where(where)
.orderBy('scheduledDate', 'asc')
.skip(skip)
.limit(ps)
.get()
return {
code: 0,
data: {
list: res.data || [],
total: res.affectedDocs !== undefined ? res.affectedDocs : (res.data || []).length
}
}
}
case 'detail': {
const { scheduleId } = event
if (!scheduleId) {
return { code: -1, message: '缺少提醒ID' }
}
const res = await db.collection('schedules').doc(scheduleId).get()
const schedule = res.data && res.data[0]
if (!schedule || schedule.user_id !== uid) {
return { code: -1, message: '提醒不存在' }
}
return { code: 0, data: schedule }
}
case 'add': {
const { pet_id, type, title, scheduledDate, frequency, interval, reminderDays, relatedRecordId, note } = event
const scheduleDate = normalizeScheduleDate(scheduledDate)
if (!pet_id || !title || !scheduleDate) {
return { code: -1, message: '请填写宠物、标题和日期' }
}
const now = Date.now()
const scheduleData = {
user_id: uid,
pet_id,
type: type || 'custom',
title: title || '',
scheduledDate: scheduleDate,
frequency: frequency || 'once',
interval: interval || 1,
status: 'pending',
reminderDays: Array.isArray(reminderDays) ? reminderDays : [7, 3, 1],
relatedRecordId: relatedRecordId || '',
note: note || '',
createdAt: now,
updatedAt: now
}
const res = await db.collection('schedules').add(scheduleData)
return { code: 0, data: { _id: res.id }, message: '添加成功' }
}
case 'update': {
const { scheduleId } = event
if (!scheduleId) {
return { code: -1, message: '缺少提醒ID' }
}
const detailRes = await db.collection('schedules').doc(scheduleId).get()
const schedule = detailRes.data && detailRes.data[0]
if (!schedule || schedule.user_id !== uid) {
return { code: -1, message: '提醒不存在' }
}
const updateFields = {}
allowedUpdateFields.forEach((field) => {
if (event[field] !== undefined) updateFields[field] = event[field]
})
if (event.scheduledDate !== undefined) {
const scheduleDate = normalizeScheduleDate(event.scheduledDate)
if (!scheduleDate) {
return { code: -1, message: '日期格式不正确' }
}
updateFields.scheduledDate = scheduleDate
}
updateFields.updatedAt = Date.now()
await db.collection('schedules').doc(scheduleId).update(updateFields)
return { code: 0, message: '更新成功' }
}
case 'delete': {
const { scheduleId } = event
if (!scheduleId) {
return { code: -1, message: '缺少提醒ID' }
}
const detailRes = await db.collection('schedules').doc(scheduleId).get()
const schedule = detailRes.data && detailRes.data[0]
if (!schedule || schedule.user_id !== uid) {
return { code: -1, message: '提醒不存在' }
}
await db.collection('schedules').doc(scheduleId).remove()
return { code: 0, message: '删除成功' }
}
default:
return { code: -1, message: `未知操作: ${action}` }
}
} catch (err) {
console.error('scheduleManager error:', err)
return { code: -1, message: err.message || '操作失败' }
}
}
function normalizeScheduleDate(value) {
if (!value) return 0
if (typeof value === 'number') return value
if (typeof value === 'string') {
const match = value.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/)
if (match) {
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getTime()
}
}
const date = new Date(value)
return Number.isNaN(date.getTime()) ? 0 : date.getTime()
}

View File

@ -72,7 +72,7 @@ exports.main = async (event, context) => {
try { try {
const schedulesRes = await db.collection('schedules') const schedulesRes = await db.collection('schedules')
.where({ user_id: uid, completed: false }) .where({ user_id: uid, status: 'pending' })
.count() .count()
pendingScheduleCount = schedulesRes.total || 0 pendingScheduleCount = schedulesRes.total || 0
} catch (e) { } catch (e) {