feat: 新增日程管理完整功能模块
1. 新增scheduleManager云函数,实现日程增删改查与权限校验 2. 重构日程页面,支持筛选、分类展示与状态切换 3. 完善提醒编辑页面,支持新增和编辑日程 4. 调整全局安全边距与TabBar样式适配 5. 修复日程查询条件,替换completed判断为status字段
This commit is contained in:
parent
c709e72896
commit
78305c00dc
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue