示例源码如下,请查看 pre > code 标签中的内容
<template>
<!-- #ifdef APP -->
<scroll-view style="flex: 1;padding: 6px;">
<!-- #endif -->
<text class="uni-h2">日历 API</text>
<text class="notice">本页将 `uni.addPhoneCalendar` 和 `uni.addPhoneRepeatCalendar` 合并为一个可直接提交的表单,覆盖 API 的全部可填参数。在有权限的情况下可直接写入系统日历,在无权限的情况下会打开系统的日历新建页面</text>
<text class="margin-v">开始时间默认是当前时间的下一个整点,结束时间默认顺延一小时;保留预填数据即可直接添加日历。</text>
<text class="uni-h3">当前结果</text>
<view class="log-list">
<text class="log-item">最近动作:{{ lastAction }}</text>
<text class="log-item">执行状态:{{ lastStatus }}</text>
<text class="log-item">返回信息:{{ lastPayload }}</text>
</view>
<text class="uni-h3">添加日历</text>
<text class="margin-v">`repeatInterval` 选为“不重复”时调用单次事件 API,其余选项调用重复事件 API。</text>
<view class="field-group">
<text class="field-label">标题 title</text>
<input class="field-input" v-model="title" placeholder="请输入日历标题" />
</view>
<view class="field-group">
<text class="field-label">开始时间 startTime</text>
<view class="picker-row">
<picker class="picker-field" mode="date" :value="startDate" :start="dateRangeStart" :end="dateRangeEnd" @change="handleStartDateChange">
<view class="picker-box">
<text class="picker-text">{{ startDate }}</text>
</view>
</picker>
<picker v-if="!allDay" class="picker-field picker-field-space" mode="time" :value="startClock" @change="handleStartClockChange">
<view class="picker-box">
<text class="picker-text">{{ startClock }}</text>
</view>
</picker>
</view>
</view>
<view class="field-group">
<view class="switch-row">
<text class="field-label">全天 allDay</text>
<switch :checked="allDay" @change="handleAllDayChange" />
</view>
</view>
<view class="field-group">
<text class="field-label">备注 notes</text>
<textarea class="field-textarea" v-model="notesText" placeholder="请输入事件备注" :maxlength="-1" />
</view>
<view class="field-group">
<text class="field-label">地点 location</text>
<input class="field-input" v-model="location" placeholder="请输入地点" />
</view>
<view class="field-group">
<text class="field-label">结束时间 endTime</text>
<view class="picker-row">
<picker class="picker-field" mode="date" :value="endDate" :start="dateRangeStart" :end="dateRangeEnd" @change="handleEndDateChange">
<view class="picker-box">
<text class="picker-text">{{ endDate }}</text>
</view>
</picker>
<picker v-if="!allDay" class="picker-field picker-field-space" mode="time" :value="endClock" @change="handleEndClockChange">
<view class="picker-box">
<text class="picker-text">{{ endClock }}</text>
</view>
</picker>
</view>
</view>
<view class="field-group">
<view class="switch-row">
<text class="field-label">提醒 alarm</text>
<switch :checked="alarm" @change="handleAlarmChange" />
</view>
</view>
<view class="field-group">
<text class="field-label">提醒提前量 alarmOffset(秒)</text>
<input class="field-input" v-model="alarmOffsetSeconds" type="number" placeholder="例如 900" :disabled="!alarm" />
</view>
<view class="field-group">
<text class="field-label">点击日程的激活URL path(微信小程序下填path时必须填signature)</text>
<input class="field-input" v-model="path" placeholder="请输入页面路径" />
</view>
<view class="field-group">
<text class="field-label">签名 signature(仅微信小程序生效)</text>
<input class="field-input" v-model="signature" placeholder="请输入签名" />
</view>
<view class="field-group">
<text class="field-label">重复周期 repeatInterval</text>
<picker class="picker-field picker-full" :value="repeatIntervalIndex" :range="repeatIntervalLabels" @change="handleRepeatIntervalChange">
<view class="picker-box">
<text class="picker-text">{{ repeatIntervalText }}</text>
</view>
</picker>
</view>
<view class="field-group">
<text class="field-label">重复截止时间 repeatEndTime</text>
<view class="picker-row">
<picker class="picker-field" mode="date" :value="repeatEndDate" :start="dateRangeStart" :end="dateRangeEnd" :disabled="isRepeatEndDisabled" @change="handleRepeatEndDateChange">
<view class="picker-box">
<text class="picker-text">{{ repeatEndDate }}</text>
</view>
</picker>
<picker v-if="!allDay" class="picker-field picker-field-space" mode="time" :value="repeatEndClock" :disabled="isRepeatEndDisabled" @change="handleRepeatEndClockChange">
<view class="picker-box">
<text class="picker-text">{{ repeatEndClock }}</text>
</view>
</picker>
</view>
</view>
<button class="submit-button" type="primary" @tap="submitCalendar">添加日历</button>
<text class="uni-h3">调用日志</text>
<view class="log-list">
<text class="log-item" v-for="item in logItems" :key="item">{{ item }}</text>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="uts">
type RepeatOptionValue = 'none' | CalendarRepeatInterval
const lastAction = ref<string>('等待调用')
const lastStatus = ref<string>('未执行')
const lastPayload = ref<string>('点击下方按钮开始体验')
const logItems = ref<Array<string>>([])
const title = ref<string>('uni-calendar 表单示例')
const allDay = ref<boolean>(false)
const notesText = ref<string>('通过单个表单覆盖日历 API 全量可填参数。')
const location = ref<string>('会议室 A')
const alarm = ref<boolean>(true)
const alarmOffsetSeconds = ref<string>('900')
// #ifndef MP-WEIXIN
const path = ref<string>('https://uniappx.dcloud.net.cn/ulink')
// #endif
// #ifdef MP-WEIXIN
const path = ref<string>('') //微信小程序填path必须签名,默认不填
// #endif
const signature = ref<string>('')
const repeatIntervalLabels : Array<string> = ['不重复', '每天', '每周', '每月', '每年']
const repeatIntervalValues : Array<RepeatOptionValue> = ['none', 'day', 'week', 'month', 'year']
const repeatIntervalIndex = ref<number>(0)
const dateRangeStart = ref<string>('')
const dateRangeEnd = ref<string>('')
const startDate = ref<string>('')
const startClock = ref<string>('')
const endDate = ref<string>('')
const endClock = ref<string>('')
const repeatEndDate = ref<string>('')
const repeatEndClock = ref<string>('')
function pad2(value : number) : string {
return value > 9 ? value.toString() : `0${value}`
}
function formatDateValue(timestamp : number) : string {
const date = new Date(timestamp)
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
}
function formatTimeValue(timestamp : number) : string {
const date = new Date(timestamp)
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
}
function createNextHourTimestamp() : number {
const date = new Date()
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
date.setHours(date.getHours() + 1)
return date.getTime()
}
function createTimestampAfterDays(baseTimestamp : number, dayOffset : number) : number {
const date = new Date(baseTimestamp)
date.setDate(date.getDate() + dayOffset)
return date.getTime()
}
function buildTimestamp(dateValue : string, timeValue : string) : number {
const dateParts = dateValue.split('-')
const timeParts = timeValue.split(':')
if (dateParts.length != 3 || timeParts.length != 2) {
return 0
}
const year = parseInt(dateParts[0])
const month = parseInt(dateParts[1]) - 1
const day = parseInt(dateParts[2])
const hour = parseInt(timeParts[0])
const minute = parseInt(timeParts[1])
const date = new Date()
date.setFullYear(year)
date.setMonth(month)
date.setDate(day)
date.setHours(hour)
date.setMinutes(minute)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
function buildDateTimestamp(dateValue : string) : number {
const dateParts = dateValue.split('-')
if (dateParts.length != 3) {
return 0
}
const year = parseInt(dateParts[0])
const month = parseInt(dateParts[1]) - 1
const day = parseInt(dateParts[2])
const date = new Date()
date.setFullYear(year)
date.setMonth(month)
date.setDate(day)
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
function parseOffsetSeconds(value : string) : number | null {
if (value.length == 0) {
return null
}
const seconds = parseInt(value)
if (seconds <= 0) {
return 0
}
return seconds
}
function describeCalendarError(errCode : number) : string {
switch (errCode) {
case 601:
return '标题不能为空'
case 602:
return '开始时间无效'
case 603:
return '结束时间不能早于开始时间'
case 604:
return '设置提醒提前量前需要先开启提醒'
case 606:
return '重复规则无效'
case 607:
return '当前设备的日历服务不可用'
case 608:
return '写入日历失败'
case 609:
return '用户取消了系统日历创建'
default:
return '未知错误'
}
}
function pushLog(line : string) : void {
logItems.value.unshift(line)
if (logItems.value.length > 8) {
logItems.value.splice(8)
}
}
function updateResult(action : string, status : string, payload : string) : void {
lastAction.value = action
lastStatus.value = status
lastPayload.value = payload
pushLog(`${action} -> ${status}`)
}
function handleCalendarSuccessResult(action : string, errMsg : string | null) : void {
const resultMessage = errMsg != null ? errMsg : `${action}:ok`
updateResult(action, '成功', resultMessage)
}
function handleAddPhoneCalendarSuccess(action : string, res : AddPhoneCalendarSuccess) : void {
handleCalendarSuccessResult(action, res.errMsg)
}
function handleAddPhoneRepeatCalendarSuccess(action : string, res : AddPhoneRepeatCalendarSuccess) : void {
handleCalendarSuccessResult(action, res.errMsg)
}
function handleCalendarFailResult(action : string, errSubject : string | null, errCode : number, errMsg : string | null) : void {
const subject = errSubject != null ? errSubject : 'uni-calendar'
const message = errMsg != null ? errMsg : ''
const errorDescription = describeCalendarError(errCode)
updateResult(action, `失败 (${errCode})`, `${errorDescription}; errSubject=${subject}; errCode=${errCode}; errMsg=${message}`)
}
function handleAddPhoneCalendarFail(action : string, error : AddPhoneCalendarFail) : void {
handleCalendarFailResult(action, error.errSubject, error.errCode, error.errMsg)
}
function handleAddPhoneRepeatCalendarFail(action : string, error : AddPhoneRepeatCalendarFail) : void {
handleCalendarFailResult(action, error.errSubject, error.errCode, error.errMsg)
}
function fillDefaultForm() : void {
const startTimestamp = createNextHourTimestamp()
const endTimestamp = startTimestamp + 60 * 60 * 1000
const repeatEndTimestamp = createTimestampAfterDays(startTimestamp, 30)
const rangeStartTimestamp = createTimestampAfterDays(startTimestamp, -365)
const rangeEndTimestamp = createTimestampAfterDays(startTimestamp, 365 * 5)
dateRangeStart.value = formatDateValue(rangeStartTimestamp)
dateRangeEnd.value = formatDateValue(rangeEndTimestamp)
startDate.value = formatDateValue(startTimestamp)
startClock.value = formatTimeValue(startTimestamp)
endDate.value = formatDateValue(endTimestamp)
endClock.value = formatTimeValue(endTimestamp)
repeatEndDate.value = formatDateValue(repeatEndTimestamp)
repeatEndClock.value = formatTimeValue(startTimestamp)
}
function handleStartDateChange(event : UniPickerChangeEvent) : void {
startDate.value = event.detail.value as string
}
function handleStartClockChange(event : UniPickerChangeEvent) : void {
startClock.value = event.detail.value as string
}
function handleEndDateChange(event : UniPickerChangeEvent) : void {
endDate.value = event.detail.value as string
}
function handleEndClockChange(event : UniPickerChangeEvent) : void {
endClock.value = event.detail.value as string
}
function handleRepeatEndDateChange(event : UniPickerChangeEvent) : void {
repeatEndDate.value = event.detail.value as string
}
function handleRepeatEndClockChange(event : UniPickerChangeEvent) : void {
repeatEndClock.value = event.detail.value as string
}
function handleRepeatIntervalChange(event : UniPickerChangeEvent) : void {
repeatIntervalIndex.value = event.detail.value as number
}
function handleAllDayChange(event : UniSwitchChangeEvent) : void {
allDay.value = event.detail.value
}
function handleAlarmChange(event : UniSwitchChangeEvent) : void {
alarm.value = event.detail.value
}
const repeatIntervalText = computed(() : string => {
return repeatIntervalLabels[repeatIntervalIndex.value]
})
const isRepeatEndDisabled = computed(() : boolean => {
return repeatIntervalValues[repeatIntervalIndex.value] == 'none'
})
function buildStartTimeForSubmit() : number {
if (allDay.value) {
return buildDateTimestamp(startDate.value)
}
return buildTimestamp(startDate.value, startClock.value)
}
function buildEndTimeForSubmit() : number {
if (allDay.value) {
const endDateTimestamp = buildDateTimestamp(endDate.value)
if (endDateTimestamp <= 0) {
return 0
}
return createTimestampAfterDays(endDateTimestamp, 1)
}
return buildTimestamp(endDate.value, endClock.value)
}
function buildRepeatEndTimeForSubmit() : number {
if (allDay.value) {
const repeatEndDateTimestamp = buildDateTimestamp(repeatEndDate.value)
if (repeatEndDateTimestamp <= 0) {
return 0
}
return createTimestampAfterDays(repeatEndDateTimestamp, 1)
}
return buildTimestamp(repeatEndDate.value, repeatEndClock.value)
}
function submitCalendar() : void {
const startTime = buildStartTimeForSubmit()
const endTime = buildEndTimeForSubmit()
const alarmOffset = parseOffsetSeconds(alarmOffsetSeconds.value)
const repeatValue = repeatIntervalValues[repeatIntervalIndex.value]
const baseOptions : AddPhoneCalendarOptions = {
title: title.value,
startTime: startTime,
allDay: allDay.value,
notes: notesText.value,
location: location.value,
endTime: endTime,
alarm: alarm.value,
alarmOffset: alarmOffset,
path: path.value,
signature: signature.value,
success: (res) => {
handleAddPhoneCalendarSuccess('addPhoneCalendar', res)
},
fail: (error) => {
handleAddPhoneCalendarFail('addPhoneCalendar', error)
}
}
if (repeatValue == 'none') {
uni.addPhoneCalendar(baseOptions)
return
}
const repeatCalendarValue = repeatValue as CalendarRepeatInterval
const repeatEndTime = buildRepeatEndTimeForSubmit()
const repeatOptions : AddPhoneRepeatCalendarOptions = {
title: baseOptions.title,
startTime: baseOptions.startTime,
allDay: baseOptions.allDay,
notes: baseOptions.notes,
location: baseOptions.location,
endTime: baseOptions.endTime,
alarm: baseOptions.alarm,
alarmOffset: baseOptions.alarmOffset,
path: baseOptions.path,
signature: baseOptions.signature,
repeatInterval: repeatCalendarValue,
repeatEndTime: repeatEndTime,
success: (res) => {
handleAddPhoneRepeatCalendarSuccess('addPhoneRepeatCalendar', res)
},
fail: (error) => {
handleAddPhoneRepeatCalendarFail('addPhoneRepeatCalendar', error)
}
}
uni.addPhoneRepeatCalendar(repeatOptions)
}
fillDefaultForm()
</script>
<style>
.margin-v {
margin: 5px 0;
}
.notice {
color: #550000;
font-size: 14px;
font-style: italic;
}
.field-group {
margin: 8px 0;
display: flex;
flex-direction: column;
}
.field-label {
font-size: 14px;
}
.field-input {
height: 42px;
margin-top: 5px;
padding-left: 12px;
padding-right: 12px;
border: 1px solid #ccc;
}
.field-textarea {
min-height: 96px;
margin-top: 5px;
padding: 12px;
border: 1px solid #ccc;
}
.picker-row {
margin-top: 5px;
display: flex;
flex-direction: row;
}
.picker-field {
flex: 1;
}
.picker-field-space {
margin-left: 10px;
}
.picker-full {
margin-top: 8px;
}
.picker-box {
min-height: 42px;
padding-left: 12px;
padding-right: 12px;
border: 1px solid #ccc;
display: flex;
flex-direction: row;
align-items: center;
}
.picker-text {
font-size: 14px;
}
.switch-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.submit-button {
margin: 10px 0;
}
.log-list {
margin: 5px 0;
padding: 10px;
border: 1px solid #ccc;
display: flex;
flex-direction: column;
}
.log-item {
font-size: 14px;
margin: 4px 0;
}
</style>