Initial Commit

This commit is contained in:
yuming
2025-10-26 19:29:30 +08:00
commit 6747ade9c4
33 changed files with 4387 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
// add-anniversary.js
const storage = require('../../utils/storage')
const dateUtils = require('../../utils/date')
Page({
data: {
anniversaryId: null,
personId: null,
personList: [],
personIndex: 0,
selectedPerson: '',
typeList: ['公历生日', '农历生日', '结婚纪念日', '订婚纪念日', '其他纪念日'],
typeIndex: 0,
showCustomType: false,
dateValue: '',
remindDaysList: ['提前3天', '提前7天', '提前14天', '提前30天', '自定义'],
remindDaysIndex: 0,
formData: {
isLunar: false,
type: 'birthday',
customTypeName: '',
solarYear: '',
solarMonth: '',
solarDay: '',
lunarYear: '',
lunarMonth: '',
lunarDay: '',
importance: 'low',
remindEnabled: true,
remindDays: 7,
remark: ''
}
},
onLoad(options) {
// 获取人员列表
const persons = storage.getPersons()
this.setData({ personList: persons })
if (options.personId) {
// 从人员详情页进入
const index = persons.findIndex(p => p.id === options.personId)
if (index !== -1) {
this.setData({
personIndex: index,
personId: options.personId,
selectedPerson: persons[index].name
})
}
}
if (options.id) {
// 编辑模式
this.setData({ anniversaryId: options.id })
this.loadAnniversary(options.id)
} else {
// 设置默认日期为今天
const today = new Date()
this.setData({
dateValue: dateUtils.formatDate(today, 'YYYY-MM-DD')
})
}
},
/**
* 加载纪念日信息(编辑模式)
*/
loadAnniversary(id) {
const anniversaries = storage.getAnniversaries()
const anniversary = anniversaries.find(a => a.id === id)
if (anniversary) {
const date = dateUtils.formatDate(
new Date(anniversary.solarYear, anniversary.solarMonth - 1, anniversary.solarDay),
'YYYY-MM-DD'
)
// 设置类型
const typeIndex = this.getTypeIndex(anniversary.type)
this.setData({
formData: anniversary,
dateValue: date,
typeIndex,
showCustomType: anniversary.type === 'other'
})
wx.setNavigationBarTitle({ title: '编辑纪念日' })
}
},
/**
* 获取类型索引
*/
getTypeIndex(type) {
const indexMap = {
birthday: 0,
lunar_birthday: 1,
wedding: 2,
engagement: 3,
other: 4
}
return indexMap[type] || 0
},
/**
* 选择人员
*/
onPersonChange(e) {
const index = parseInt(e.detail.value)
const person = this.data.personList[index]
this.setData({
personIndex: index,
personId: person.id,
selectedPerson: person.name
})
},
/**
* 选择类型
*/
onTypeChange(e) {
const index = parseInt(e.detail.value)
const types = ['birthday', 'lunar_birthday', 'wedding', 'engagement', 'other']
const isOther = index === 4
this.setData({
typeIndex: index,
showCustomType: isOther,
'formData.type': types[index]
})
},
/**
* 自定义类型输入
*/
onCustomTypeInput(e) {
this.setData({
'formData.customTypeName': e.detail.value
})
},
/**
* 日期类型改变
*/
onDateTypeChange(e) {
const isLunar = e.detail.value === 'lunar'
this.setData({
'formData.isLunar': isLunar
})
},
/**
* 日期改变
*/
onDateChange(e) {
const dateStr = e.detail.value
const parts = dateStr.split('-')
this.setData({
dateValue: dateStr,
'formData.solarYear': parseInt(parts[0]),
'formData.solarMonth': parseInt(parts[1]),
'formData.solarDay': parseInt(parts[2])
})
},
/**
* 重要程度改变
*/
onImportanceChange(e) {
this.setData({
'formData.importance': e.detail.value
})
},
/**
* 提醒开关改变
*/
onRemindEnabledChange(e) {
this.setData({
'formData.remindEnabled': e.detail.value
})
},
/**
* 提醒天数改变
*/
onRemindDaysChange(e) {
const index = parseInt(e.detail.value)
const days = [3, 7, 14, 30, 7][index]
this.setData({
remindDaysIndex: index,
'formData.remindDays': days
})
},
/**
* 备注改变
*/
onRemarkChange(e) {
this.setData({
'formData.remark': e.detail.value
})
},
/**
* 取消
*/
onCancel() {
wx.navigateBack()
},
/**
* 提交
*/
onSubmit() {
const { formData, personId, anniversaryId } = this.data
// 验证关联人员
if (!personId) {
wx.showToast({ title: '请选择关联人员', icon: 'none' })
return
}
// 验证日期
if (!formData.solarYear || !formData.solarMonth || !formData.solarDay) {
wx.showToast({ title: '请选择日期', icon: 'none' })
return
}
// 验证自定义类型
if (formData.type === 'other' && !formData.customTypeName) {
wx.showToast({ title: '请输入自定义类型', icon: 'none' })
return
}
if (anniversaryId) {
// 编辑模式
const success = storage.updateAnniversary(anniversaryId, {
personId,
...formData
})
if (success) {
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
}
} else {
// 新增模式
const newAnniversary = storage.addAnniversary({
personId,
...formData
})
if (newAnniversary) {
wx.showToast({ title: '添加成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
}
}
}
})
+101
View File
@@ -0,0 +1,101 @@
<!--add-anniversary.wxml-->
<view class="container">
<view class="form">
<!-- 关联人员 -->
<view class="form-item">
<text class="label required">关联人员</text>
<picker mode="selector" range="{{personList}}" range-key="name" value="{{personIndex}}" bindchange="onPersonChange">
<view class="picker">
<text wx:if="{{selectedPerson}}" class="picker-text">{{selectedPerson}}</text>
<text wx:else class="picker-placeholder">请选择关联人员</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 纪念日类型 -->
<view class="form-item">
<text class="label required">纪念日类型</text>
<picker mode="selector" range="{{typeList}}" value="{{typeIndex}}" bindchange="onTypeChange">
<view class="picker">
<text class="picker-text">{{typeList[typeIndex]}}</text>
<text class="picker-arrow"></text>
</view>
</picker>
<input wx:if="{{showCustomType}}" class="input" placeholder="请输入自定义类型" value="{{formData.customTypeName}}" bindinput="onCustomTypeInput" />
</view>
<!-- 日期类型 -->
<view class="form-item">
<text class="label required">日期类型</text>
<radio-group class="radio-group" bindchange="onDateTypeChange">
<label class="radio-item">
<radio value="solar" checked="{{formData.isLunar === false}}" />
<text>公历</text>
</label>
<label class="radio-item">
<radio value="lunar" checked="{{formData.isLunar === true}}" />
<text>农历</text>
</label>
</radio-group>
</view>
<!-- 日期选择 -->
<view class="form-item">
<text class="label required">日期</text>
<picker mode="date" value="{{dateValue}}" bindchange="onDateChange">
<view class="picker">
<text class="picker-text">{{dateValue || '请选择日期'}}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 重要程度 -->
<view class="form-item">
<text class="label">重要程度</text>
<radio-group class="radio-group" bindchange="onImportanceChange">
<label class="radio-item">
<radio value="high" checked="{{formData.importance === 'high'}}" />
<text class="importance-high">非常重要</text>
</label>
<label class="radio-item">
<radio value="medium" checked="{{formData.importance === 'medium'}}" />
<text class="importance-medium">重要</text>
</label>
<label class="radio-item">
<radio value="low" checked="{{formData.importance === 'low' || !formData.importance}}" />
<text class="importance-low">一般</text>
</label>
</radio-group>
</view>
<!-- 提醒设置 -->
<view class="form-item">
<view class="switch-item">
<text class="label">开启提醒</text>
<switch checked="{{formData.remindEnabled}}" bindchange="onRemindEnabledChange" color="#07c160" />
</view>
<picker wx:if="{{formData.remindEnabled}}" mode="selector" range="{{remindDaysList}}" value="{{remindDaysIndex}}" bindchange="onRemindDaysChange">
<view class="picker">
<text class="picker-text">提前{{formData.remindDays}}天提醒</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 备注 -->
<view class="form-item">
<text class="label">备注</text>
<textarea class="textarea" placeholder="添加备注信息(可选)" value="{{formData.remark}}" bindinput="onRemarkChange" />
</view>
<!-- 操作按钮 -->
<view class="buttons">
<button class="btn btn-cancel" bindtap="onCancel">取消</button>
<button class="btn btn-submit" bindtap="onSubmit">保存</button>
</view>
</view>
</view>
+139
View File
@@ -0,0 +1,139 @@
/**add-anniversary.wxss**/
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.form {
padding: 32rpx;
}
.form-item {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 24rpx;
display: block;
}
.label.required::after {
content: ' *';
color: #ff5722;
}
.picker {
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-text {
font-size: 28rpx;
color: #333;
}
.picker-placeholder {
font-size: 28rpx;
color: #999;
}
.picker-arrow {
font-size: 36rpx;
color: #999;
}
.input {
margin-top: 16rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 24rpx;
font-size: 28rpx;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.radio-item {
display: flex;
align-items: center;
padding: 16rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.radio-item text {
font-size: 28rpx;
margin-left: 16rpx;
}
.importance-high {
color: #ff5722;
}
.importance-medium {
color: #ff9800;
}
.importance-low {
color: #999;
}
.switch-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.textarea {
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 24rpx;
font-size: 28rpx;
min-height: 160rpx;
}
.buttons {
display: flex;
gap: 24rpx;
margin-top: 40rpx;
}
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
}
.btn-cancel {
background-color: #f5f5f5;
color: #666;
}
.btn-submit {
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
color: #fff;
}
.btn-cancel::after,
.btn-submit::after {
border: none;
}
+136
View File
@@ -0,0 +1,136 @@
// add-person.js
const storage = require('../../utils/storage')
Page({
data: {
personId: null,
formData: {
avatar: '',
name: '',
nickname: '',
remark: ''
}
},
onLoad(options) {
// 如果是编辑模式
if (options.id) {
this.setData({ personId: options.id })
this.loadPerson(options.id)
}
},
/**
* 加载人员信息(编辑模式)
*/
loadPerson(id) {
const person = storage.getPersonById(id)
if (person) {
this.setData({ formData: person })
wx.setNavigationBarTitle({ title: '编辑人员' })
}
},
/**
* 输入框变化
*/
onInputChange(e) {
const field = e.currentTarget.dataset.field
const value = e.detail.value
this.setData({
[`formData.${field}`]: value
})
},
/**
* 文本框变化
*/
onTextareaChange(e) {
this.setData({
'formData.remark': e.detail.value
})
},
/**
* 选择头像
*/
chooseAvatar() {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// 这里可以上传到服务器,暂时使用本地路径
this.setData({
'formData.avatar': res.tempFilePaths[0]
})
}
})
},
/**
* 取消
*/
onCancel() {
wx.navigateBack()
},
/**
* 提交
*/
onSubmit() {
const { formData, personId } = this.data
// 验证姓名
if (!formData.name || formData.name.trim() === '') {
wx.showToast({
title: '请输入姓名',
icon: 'none'
})
return
}
if (personId) {
// 编辑模式
const success = storage.updatePerson(personId, formData)
if (success) {
wx.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
} else {
wx.showToast({
title: '保存失败',
icon: 'none'
})
}
} else {
// 新增模式
const newPerson = storage.addPerson({
name: formData.name,
nickname: formData.nickname || '',
avatar: formData.avatar || '',
remark: formData.remark || ''
})
if (newPerson) {
wx.showToast({
title: '添加成功',
icon: 'success'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
} else {
wx.showToast({
title: '添加失败',
icon: 'none'
})
}
}
}
})
+41
View File
@@ -0,0 +1,41 @@
<!--add-person.wxml-->
<view class="container">
<view class="form">
<!-- 头像上传 -->
<view class="form-item">
<text class="label">头像(可选)</text>
<view class="avatar-upload" bindtap="chooseAvatar">
<image wx:if="{{formData.avatar}}" class="avatar" src="{{formData.avatar}}" mode="aspectFill" />
<view wx:else class="avatar-placeholder">
<text class="icon">📷</text>
<text class="text">点击上传头像</text>
</view>
</view>
</view>
<!-- 姓名 -->
<view class="form-item">
<text class="label required">姓名</text>
<input class="input" placeholder="请输入姓名" value="{{formData.name}}" bindinput="onInputChange" data-field="name" />
</view>
<!-- 昵称 -->
<view class="form-item">
<text class="label">昵称</text>
<input class="input" placeholder="请输入昵称(可选)" value="{{formData.nickname}}" bindinput="onInputChange" data-field="nickname" />
</view>
<!-- 备注 -->
<view class="form-item">
<text class="label">备注</text>
<textarea class="textarea" placeholder="添加一些备注信息(可选)" value="{{formData.remark}}" bindinput="onTextareaChange" />
</view>
<!-- 操作按钮 -->
<view class="buttons">
<button class="btn btn-cancel" bindtap="onCancel">取消</button>
<button class="btn btn-submit" bindtap="onSubmit">保存</button>
</view>
</view>
</view>
+115
View File
@@ -0,0 +1,115 @@
/**add-person.wxss**/
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.form {
padding: 32rpx;
}
.form-item {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 24rpx;
display: block;
}
.label.required::after {
content: ' *';
color: #ff5722;
}
/* 头像上传 */
.avatar-upload {
text-align: center;
}
.avatar {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background-color: #f0f0f0;
}
.avatar-placeholder {
width: 200rpx;
height: 200rpx;
margin: 0 auto;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #ddd;
}
.avatar-placeholder .icon {
font-size: 60rpx;
display: block;
margin-bottom: 16rpx;
}
.avatar-placeholder .text {
font-size: 24rpx;
color: #999;
}
.input {
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 24rpx;
font-size: 28rpx;
}
.textarea {
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 24rpx;
font-size: 28rpx;
min-height: 160rpx;
}
.buttons {
display: flex;
gap: 24rpx;
margin-top: 40rpx;
}
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
}
.btn-cancel {
background-color: #f5f5f5;
color: #666;
}
.btn-submit {
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
color: #fff;
}
.btn-cancel::after {
border: none;
}
.btn-submit::after {
border: none;
}
+234
View File
@@ -0,0 +1,234 @@
// calendar.js
const storage = require('../../utils/storage')
const dateUtils = require('../../utils/date')
Page({
data: {
currentYear: 2024,
currentMonth: 1,
calendarDays: [],
monthEvents: []
},
onLoad() {
const today = new Date()
this.setData({
currentYear: today.getFullYear(),
currentMonth: today.getMonth() + 1
})
this.renderCalendar()
this.loadMonthEvents()
},
/**
* 渲染日历
*/
renderCalendar() {
const { currentYear, currentMonth } = this.data
const anniversaries = storage.getAnniversaries()
// 获取本月第一天是星期几
const firstDay = new Date(currentYear, currentMonth - 1, 1)
const startWeekday = firstDay.getDay()
// 获取本月天数
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate()
// 获取上个月的天数
const prevMonthDays = new Date(currentYear, currentMonth - 1, 0).getDate()
// 构建日历天数数组
const calendarDays = []
const today = new Date()
// 填充上个月的日期
for (let i = startWeekday - 1; i >= 0; i--) {
const day = prevMonthDays - i
calendarDays.push({
day,
date: new Date(currentYear, currentMonth - 2, day),
isToday: false,
isOtherMonth: true,
events: [],
hasMore: false
})
}
// 填充本月的日期
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(currentYear, currentMonth - 1, day)
const isToday = dateUtils.isToday(date)
// 查找该日期的纪念日
const events = anniversaries
.filter(a => this.isAnniversaryOnDate(a, date))
.map(a => ({
id: a.id,
color: this.getEventColor(a)
}))
.slice(0, 3)
calendarDays.push({
day,
date,
isToday,
isOtherMonth: false,
events,
hasMore: events.length > 2,
moreCount: anniversaries.filter(a => this.isAnniversaryOnDate(a, date)).length - 2
})
}
// 填充下个月的日期(补全6行)
const remainingDays = 42 - calendarDays.length
for (let day = 1; day <= remainingDays; day++) {
calendarDays.push({
day,
date: new Date(currentYear, currentMonth, day),
isToday: false,
isOtherMonth: true,
events: [],
hasMore: false
})
}
this.setData({ calendarDays })
},
/**
* 判断纪念日是否在某日期
*/
isAnniversaryOnDate(anniversary, date) {
return anniversary.solarYear === date.getFullYear() &&
anniversary.solarMonth === date.getMonth() + 1 &&
anniversary.solarDay === date.getDate()
},
/**
* 获取事件颜色
*/
getEventColor(anniversary) {
const colors = {
high: '#ff5722',
medium: '#ff9800',
low: '#07c160'
}
return colors[anniversary.importance] || '#07c160'
},
/**
* 加载本月事件
*/
loadMonthEvents() {
const { currentYear, currentMonth } = this.data
const persons = storage.getPersons()
const anniversaries = storage.getAnniversaries()
// 筛选本月纪念日
const monthEvents = anniversaries
.filter(a => a.solarYear === currentYear && a.solarMonth === currentMonth)
.map(a => {
const person = persons.find(p => p.id === a.personId)
const date = new Date(currentYear, currentMonth - 1, a.solarDay)
const daysUntil = dateUtils.getDaysUntil(date)
return {
...a,
personName: person ? person.name : '未知',
typeIcon: this.getTypeIcon(a.type),
typeName: a.customTypeName || this.getTypeName(a.type),
dateText: dateUtils.formatDate(date, 'MM月DD日'),
daysUntil
}
})
.sort((a, b) => a.solarDay - b.solarDay)
this.setData({ monthEvents })
},
/**
* 获取类型图标
*/
getTypeIcon(type) {
const icons = {
birthday: '🎂',
lunar_birthday: '🌙',
wedding: '💍',
engagement: '💕',
other: '📅'
}
return icons[type] || '📅'
},
/**
* 获取类型名称
*/
getTypeName(type) {
const names = {
birthday: '公历生日',
lunar_birthday: '农历生日',
wedding: '结婚纪念日',
engagement: '订婚纪念日',
other: '其他纪念日'
}
return names[type] || '其他'
},
/**
* 上一个月
*/
onPrevMonth() {
let { currentYear, currentMonth } = this.data
if (currentMonth === 1) {
currentYear--
currentMonth = 12
} else {
currentMonth--
}
this.setData({ currentYear, currentMonth })
this.renderCalendar()
this.loadMonthEvents()
},
/**
* 下一个月
*/
onNextMonth() {
let { currentYear, currentMonth } = this.data
if (currentMonth === 12) {
currentYear++
currentMonth = 1
} else {
currentMonth++
}
this.setData({ currentYear, currentMonth })
this.renderCalendar()
this.loadMonthEvents()
},
/**
* 回到今天
*/
onGoToday() {
const today = new Date()
this.setData({
currentYear: today.getFullYear(),
currentMonth: today.getMonth() + 1
})
this.renderCalendar()
this.loadMonthEvents()
},
/**
* 点击事件
*/
onEventTap(e) {
const id = e.currentTarget.dataset.id
const anniversary = storage.getAnniversaries().find(a => a.id === id)
if (anniversary) {
wx.navigateTo({
url: `/pages/person-detail/person-detail?id=${anniversary.personId}`
})
}
}
})
+52
View File
@@ -0,0 +1,52 @@
<!--calendar.wxml-->
<view class="container">
<!-- 日历头部 -->
<view class="calendar-header">
<view class="month-navigation">
<text class="nav-btn" bindtap="onPrevMonth"></text>
<text class="month-title">{{currentYear}}年{{currentMonth}}月</text>
<text class="nav-btn" bindtap="onNextMonth"></text>
</view>
<button class="today-btn" bindtap="onGoToday">今天</button>
</view>
<!-- 星期标题 -->
<view class="week-header">
<view wx:for="{{['日', '一', '二', '三', '四', '五', '六']}}" wx:key="*this" class="week-title">{{item}}</view>
</view>
<!-- 日历内容 -->
<scroll-view class="calendar-content" scroll-y>
<view class="calendar-grid">
<view wx:for="{{calendarDays}}" wx:key="index" class="calendar-cell {{item.isToday ? 'today' : ''}} {{item.isOtherMonth ? 'other-month' : ''}}">
<text class="date-num">{{item.day}}</text>
<view class="events">
<view wx:for="{{item.events}}" wx:key="id" wx:for-item="event" class="event-dot" style="background-color: {{event.color}};"></view>
</view>
<view wx:if="{{item.hasMore}}" class="more-text">+{{item.moreCount}}</view>
</view>
</view>
<!-- 列表视图 -->
<view class="events-list">
<view class="section-title">本月纪念日</view>
<view wx:if="{{monthEvents.length === 0}}" class="empty-state">
<text class="icon">📅</text>
<text class="text">本月没有纪念日</text>
</view>
<view wx:for="{{monthEvents}}" wx:key="id" class="event-card" bindtap="onEventTap" data-id="{{item.id}}">
<view class="event-header">
<text class="event-icon">{{item.typeIcon}}</text>
<text class="event-title">{{item.personName}} - {{item.typeName}}</text>
<text class="event-date">{{item.dateText}}</text>
</view>
<view class="event-info">
<text wx:if="{{item.isLunar}}" class="lunar-badge">农历</text>
<text wx:if="{{item.daysUntil === 0}}" class="status urgent">今天</text>
<text wx:elif="{{item.daysUntil === 1}}" class="status warning">明天</text>
</view>
</view>
</view>
</scroll-view>
</view>
+215
View File
@@ -0,0 +1,215 @@
/**calendar.wxss**/
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.calendar-header {
background-color: #fff;
padding: 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.month-navigation {
display: flex;
align-items: center;
gap: 32rpx;
}
.nav-btn {
font-size: 48rpx;
color: #07c160;
font-weight: 300;
width: 60rpx;
text-align: center;
}
.month-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
min-width: 240rpx;
text-align: center;
}
.today-btn {
padding: 12rpx 32rpx;
background-color: #f5f5f5;
color: #666;
border-radius: 40rpx;
font-size: 24rpx;
border: none;
}
.today-btn::after {
border: none;
}
.week-header {
display: flex;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
.week-title {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 24rpx;
color: #999;
font-weight: 500;
}
.calendar-content {
height: calc(100vh - 200rpx);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
background-color: #fff;
}
.calendar-cell {
min-height: 120rpx;
border: 1px solid #f0f0f0;
padding: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.calendar-cell.other-month {
background-color: #fafafa;
}
.calendar-cell.today .date-num {
background-color: #07c160;
color: #fff;
border-radius: 50%;
width: 48rpx;
height: 48rpx;
line-height: 48rpx;
text-align: center;
}
.date-num {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.calendar-cell.other-month .date-num {
color: #ccc;
}
.events {
display: flex;
gap: 4rpx;
}
.event-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
}
.more-text {
font-size: 20rpx;
color: #999;
margin-top: 4rpx;
}
/* 事件列表 */
.events-list {
padding: 32rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
.empty-state {
text-align: center;
padding: 120rpx 40rpx;
}
.empty-state .icon {
font-size: 120rpx;
display: block;
margin-bottom: 32rpx;
}
.empty-state .text {
font-size: 28rpx;
color: #999;
}
.event-card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.event-header {
display: flex;
align-items: center;
}
.event-icon {
font-size: 40rpx;
margin-right: 16rpx;
}
.event-title {
flex: 1;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.event-date {
font-size: 24rpx;
color: #999;
}
.event-info {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 16rpx;
}
.lunar-badge {
padding: 4rpx 12rpx;
background-color: #e3f2fd;
color: #1976d2;
border-radius: 4rpx;
font-size: 20rpx;
}
.status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.status.urgent {
background-color: #fff3f0;
color: #ff5722;
}
.status.warning {
background-color: #fff8e6;
color: #ff9800;
}
+165
View File
@@ -0,0 +1,165 @@
// index.js
const storage = require('../../utils/storage')
const dateUtils = require('../../utils/date')
Page({
data: {
persons: [],
originalPersons: [], // 原始数据备份
searchKeyword: '',
currentFilter: 'all'
},
onLoad() {
this.loadPersons()
},
onShow() {
// 每次显示页面时刷新数据
this.loadPersons()
},
/**
* 加载人员列表
*/
loadPersons() {
const persons = storage.getPersons()
const anniversaries = storage.getAnniversaries()
// 为每个人员添加纪念日信息
const personsWithAnniversaries = persons.map(person => {
const personAnniversaries = anniversaries.filter(a => a.personId === person.id)
// 找到最近的纪念日
let nextAnniversary = null
if (personAnniversaries.length > 0) {
const today = new Date()
const upcoming = personAnniversaries
.map(a => {
// 如果是农历,需要特殊处理
const date = new Date(a.solarYear, a.solarMonth - 1, a.solarDay)
return {
...a,
date,
daysUntil: dateUtils.getDaysUntil(date)
}
})
.filter(a => a.daysUntil >= 0)
.sort((a, b) => a.daysUntil - b.daysUntil)
if (upcoming.length > 0) {
const next = upcoming[0]
nextAnniversary = {
type: next.type,
dateText: dateUtils.formatDate(next.date, 'MM月DD日'),
daysUntil: next.daysUntil,
daysUntilText: this.formatDaysUntil(next.daysUntil)
}
}
}
return {
...person,
anniversaryCount: personAnniversaries.length,
nextAnniversary
}
})
// 按最近的纪念日排序
const sorted = personsWithAnniversaries.sort((a, b) => {
if (!a.nextAnniversary && !b.nextAnniversary) return 0
if (!a.nextAnniversary) return 1
if (!b.nextAnniversary) return -1
return a.nextAnniversary.daysUntil - b.nextAnniversary.daysUntil
})
this.setData({
persons: sorted,
originalPersons: sorted
})
},
/**
* 格式化剩余天数
*/
formatDaysUntil(days) {
if (days === 0) return '今天'
if (days === 1) return '明天'
if (days < 7) return `${days}天后`
if (days < 30) return `还有${Math.floor(days / 7)}`
return `还有${Math.floor(days / 30)}个月`
},
/**
* 搜索输入
*/
onSearchInput(e) {
const keyword = e.detail.value
this.setData({ searchKeyword: keyword })
this.filterPersons()
},
/**
* 筛选切换
*/
onFilterTap(e) {
const filter = e.currentTarget.dataset.filter
this.setData({ currentFilter: filter })
this.filterPersons()
},
/**
* 筛选人员
*/
filterPersons() {
const { originalPersons, searchKeyword, currentFilter } = this.data
let filtered = [...originalPersons]
// 关键词搜索
if (searchKeyword) {
filtered = filtered.filter(p =>
p.name.includes(searchKeyword) ||
(p.nickname && p.nickname.includes(searchKeyword))
)
}
// 类型筛选(暂时保留,后续可以实现更精确的筛选)
// if (currentFilter !== 'all') {
// // 可以实现更精确的筛选逻辑
// }
this.setData({ persons: filtered })
},
/**
* 点击人员
*/
onPersonTap(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/person-detail/person-detail?id=${id}`
})
},
/**
* 点击添加按钮
*/
onAddTap() {
wx.showActionSheet({
itemList: ['添加人员', '添加纪念日'],
success: (res) => {
if (res.tapIndex === 0) {
wx.navigateTo({
url: '/pages/add-person/add-person'
})
} else if (res.tapIndex === 1) {
wx.navigateTo({
url: '/pages/add-anniversary/add-anniversary'
})
}
}
})
}
})
+56
View File
@@ -0,0 +1,56 @@
<!--index.wxml-->
<view class="container">
<!-- 搜索栏 -->
<view class="search-bar">
<input class="search-input" placeholder="搜索姓名" value="{{searchKeyword}}" bindinput="onSearchInput" />
</view>
<!-- 筛选栏 -->
<view class="filter-bar">
<text class="filter-label">筛选:</text>
<scroll-view class="filter-scroll" scroll-x>
<view class="filter-item {{currentFilter === 'all' ? 'active' : ''}}" bindtap="onFilterTap" data-filter="all">全部</view>
<view class="filter-item {{currentFilter === 'birthday' ? 'active' : ''}}" bindtap="onFilterTap" data-filter="birthday">生日</view>
<view class="filter-item {{currentFilter === 'anniversary' ? 'active' : ''}}" bindtap="onFilterTap" data-filter="anniversary">纪念日</view>
<view class="filter-item {{currentFilter === 'upcoming' ? 'active' : ''}}" bindtap="onFilterTap" data-filter="upcoming">即将到来</view>
</scroll-view>
</view>
<!-- 人员列表 -->
<scroll-view class="person-list" scroll-y>
<view wx:if="{{persons.length === 0}}" class="empty-state">
<text class="icon">📅</text>
<text class="text">还没有添加任何人</text>
<text class="hint">点击右下角 + 添加第一个</text>
</view>
<view wx:for="{{persons}}" wx:key="id" class="person-card" bindtap="onPersonTap" data-id="{{item.id}}">
<view class="person-header">
<image class="avatar" src="{{item.avatar || '/images/default-avatar.png'}}" mode="aspectFill" />
<view class="person-info">
<text class="person-name">{{item.name}}</text>
<text wx:if="{{item.nickname}}" class="person-nickname">{{item.nickname}}</text>
</view>
<view class="person-count">
<text wx:if="{{item.anniversaryCount}}" class="count-text">{{item.anniversaryCount}}</text>
<text class="count-label">个纪念日</text>
</view>
</view>
<!-- 最近的纪念日 -->
<view wx:if="{{item.nextAnniversary}}" class="next-anniversary">
<text class="next-label">📌 </text>
<text class="next-text">{{item.nextAnniversary.type}}: {{item.nextAnniversary.dateText}}</text>
<text wx:if="{{item.nextAnniversary.daysUntil}}" class="days-text {{item.nextAnniversary.daysUntil <= 7 ? 'urgent' : ''}}">
{{item.nextAnniversary.daysUntilText}}
</text>
</view>
</view>
</scroll-view>
<!-- 浮动添加按钮 -->
<view class="fab" bindtap="onAddTap">
<text>+</text>
</view>
</view>
+193
View File
@@ -0,0 +1,193 @@
/**index.wxss**/
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 搜索栏 */
.search-bar {
padding: 24rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
.search-input {
background-color: #f5f5f5;
border-radius: 40rpx;
padding: 24rpx 32rpx;
font-size: 28rpx;
}
/* 筛选栏 */
.filter-bar {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
.filter-label {
font-size: 26rpx;
color: #666;
margin-right: 16rpx;
white-space: nowrap;
}
.filter-scroll {
white-space: nowrap;
}
.filter-item {
display: inline-block;
padding: 12rpx 32rpx;
margin-right: 16rpx;
border-radius: 40rpx;
font-size: 26rpx;
background-color: #f5f5f5;
color: #666;
}
.filter-item.active {
background-color: #07c160;
color: #fff;
}
/* 人员列表 */
.person-list {
height: calc(100vh - 200rpx);
padding: 24rpx;
}
.person-card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.person-header {
display: flex;
align-items: center;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
margin-right: 24rpx;
background-color: #f0f0f0;
}
.person-info {
flex: 1;
display: flex;
flex-direction: column;
}
.person-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.person-nickname {
font-size: 24rpx;
color: #999;
}
.person-count {
text-align: right;
}
.count-text {
font-size: 36rpx;
font-weight: 600;
color: #07c160;
display: block;
}
.count-label {
font-size: 22rpx;
color: #999;
}
/* 最近的纪念日 */
.next-anniversary {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1px solid #f0f0f0;
display: flex;
align-items: center;
font-size: 26rpx;
color: #666;
}
.next-label {
margin-right: 8rpx;
}
.next-text {
flex: 1;
}
.days-text {
color: #07c160;
font-weight: 500;
}
.days-text.urgent {
color: #ff5722;
font-weight: 600;
}
/* 空状态 */
.empty-state {
padding: 160rpx 40rpx;
text-align: center;
}
.empty-state .icon {
font-size: 120rpx;
display: block;
margin-bottom: 32rpx;
}
.empty-state .text {
font-size: 28rpx;
color: #999;
display: block;
margin-bottom: 16rpx;
}
.empty-state .hint {
font-size: 24rpx;
color: #bbb;
display: block;
}
/* 浮动按钮 */
.fab {
position: fixed;
bottom: 120rpx;
right: 40rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
z-index: 100;
transition: all 0.3s;
}
.fab:active {
transform: scale(0.95);
}
+188
View File
@@ -0,0 +1,188 @@
// person-detail.js
const storage = require('../../utils/storage')
const dateUtils = require('../../utils/date')
Page({
data: {
personId: null,
person: {},
anniversaries: []
},
onLoad(options) {
if (!options.id) {
wx.showToast({ title: '参数错误', icon: 'none' })
wx.navigateBack()
return
}
this.setData({ personId: options.id })
this.loadPerson()
this.loadAnniversaries()
},
onShow() {
// 重新加载数据
this.loadPerson()
this.loadAnniversaries()
},
/**
* 加载人员信息
*/
loadPerson() {
const person = storage.getPersonById(this.data.personId)
if (person) {
this.setData({ person })
wx.setNavigationBarTitle({ title: person.name })
} else {
wx.showToast({ title: '人员不存在', icon: 'none' })
wx.navigateBack()
}
},
/**
* 加载纪念日
*/
loadAnniversaries() {
const anniversaries = storage.getAnniversariesByPersonId(this.data.personId)
// 格式化纪念日数据
const formatted = anniversaries.map(a => {
const date = new Date(a.solarYear, a.solarMonth - 1, a.solarDay)
const daysUntil = dateUtils.getDaysUntil(date)
return {
...a,
date,
dateText: dateUtils.formatDate(date, 'YYYY年MM月DD日'),
daysUntil,
typeIcon: this.getTypeIcon(a.type),
typeName: a.customTypeName || this.getTypeName(a.type),
importanceText: this.getImportanceText(a.importance)
}
})
// 按日期排序
formatted.sort((a, b) => a.daysUntil - b.daysUntil)
this.setData({ anniversaries: formatted })
},
/**
* 获取类型图标
*/
getTypeIcon(type) {
const icons = {
birthday: '🎂',
lunar_birthday: '🌙',
wedding: '💍',
engagement: '💕',
other: '📅'
}
return icons[type] || '📅'
},
/**
* 获取类型名称
*/
getTypeName(type) {
const names = {
birthday: '公历生日',
lunar_birthday: '农历生日',
wedding: '结婚纪念日',
engagement: '订婚纪念日',
other: '其他纪念日'
}
return names[type] || '其他'
},
/**
* 获取重要程度文本
*/
getImportanceText(importance) {
const texts = {
high: '非常重要',
medium: '重要',
low: '一般'
}
return texts[importance] || '一般'
},
/**
* 编辑人员
*/
onEditPerson() {
wx.navigateTo({
url: `/pages/add-person/add-person?id=${this.data.personId}`
})
},
/**
* 删除人员
*/
onDeletePerson() {
wx.showModal({
title: '确认删除',
content: `确定要删除"${this.data.person.name}"吗?相关纪念日也将被删除。`,
success: (res) => {
if (res.confirm) {
// 先删除相关纪念日
const anniversaries = storage.getAnniversariesByPersonId(this.data.personId)
anniversaries.forEach(a => storage.deleteAnniversary(a.id))
// 再删除人员
const success = storage.deletePerson(this.data.personId)
if (success) {
wx.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
}
}
})
},
/**
* 添加纪念日
*/
onAddAnniversary() {
wx.navigateTo({
url: `/pages/add-anniversary/add-anniversary?personId=${this.data.personId}`
})
},
/**
* 编辑纪念日
*/
onEditAnniversary(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/add-anniversary/add-anniversary?personId=${this.data.personId}&id=${id}`
})
},
/**
* 删除纪念日
*/
onDeleteAnniversary(e) {
const id = e.currentTarget.dataset.id
const anniversary = this.data.anniversaries.find(a => a.id === id)
wx.showModal({
title: '确认删除',
content: '确定要删除这条纪念日吗?',
success: (res) => {
if (res.confirm) {
const success = storage.deleteAnniversary(id)
if (success) {
wx.showToast({ title: '删除成功', icon: 'success' })
this.loadAnniversaries()
}
}
}
})
}
})
+63
View File
@@ -0,0 +1,63 @@
<!--person-detail.wxml-->
<view class="container">
<!-- 人员信息卡片 -->
<view class="person-card">
<view class="person-header">
<image class="avatar" src="{{person.avatar || '/images/default-avatar.png'}}" mode="aspectFill" />
<view class="person-info">
<text class="person-name">{{person.name}}</text>
<text wx:if="{{person.nickname}}" class="person-nickname">{{person.nickname}}</text>
</view>
</view>
<text wx:if="{{person.remark}}" class="person-remark">{{person.remark}}</text>
<view class="actions">
<button class="action-btn" bindtap="onEditPerson">编辑</button>
<button class="action-btn danger" bindtap="onDeletePerson">删除</button>
</view>
</view>
<!-- 纪念日列表 -->
<view class="section">
<view class="section-header">
<text class="section-title">纪念日列表 ({{anniversaries.length}})</text>
<button class="add-btn" bindtap="onAddAnniversary">+ 添加纪念日</button>
</view>
<view wx:if="{{anniversaries.length === 0}}" class="empty-state">
<text class="icon">📅</text>
<text class="text">还没有添加纪念日</text>
</view>
<view wx:for="{{anniversaries}}" wx:key="id" class="anniversary-card">
<view class="anniversary-header">
<view class="anniversary-type">
<text class="type-icon">{{item.typeIcon}}</text>
<text class="type-text">{{item.typeName}}</text>
</view>
<text class="importance-badge importance-{{item.importance}}">{{item.importanceText}}</text>
</view>
<view class="anniversary-date">
<text class="date-label">日期:</text>
<text class="date-text">{{item.dateText}}</text>
<text wx:if="{{item.isLunar}}" class="lunar-badge">农历</text>
</view>
<view wx:if="{{item.daysUntil !== undefined}}" class="days-info">
<text wx:if="{{item.daysUntil === 0}}" class="days-text urgent">今天</text>
<text wx:else-if="{{item.daysUntil === 1}}" class="days-text warning">明天</text>
<text wx:else-if="{{item.daysUntil > 0}}" class="days-text">{{item.daysUntil}}天后</text>
<text wx:else class="days-text past">已过{{Math.abs(item.daysUntil)}}天</text>
</view>
<view wx:if="{{item.remark}}" class="anniversary-remark">{{item.remark}}</view>
<view class="anniversary-actions">
<button class="edit-btn" bindtap="onEditAnniversary" data-id="{{item.id}}">编辑</button>
<button class="delete-btn" bindtap="onDeleteAnniversary" data-id="{{item.id}}">删除</button>
</view>
</view>
</view>
</view>
+273
View File
@@ -0,0 +1,273 @@
/**person-detail.wxss**/
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.person-card {
background-color: #fff;
margin: 32rpx;
border-radius: 16rpx;
padding: 40rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.person-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-right: 32rpx;
background-color: #f0f0f0;
}
.person-info {
flex: 1;
}
.person-name {
font-size: 36rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.person-nickname {
font-size: 26rpx;
color: #999;
}
.person-remark {
font-size: 28rpx;
color: #666;
line-height: 1.6;
margin-bottom: 24rpx;
padding: 24rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.actions {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
}
.action-btn {
flex: 1;
height: 72rpx;
line-height: 72rpx;
border-radius: 12rpx;
font-size: 28rpx;
background-color: #f5f5f5;
color: #666;
border: none;
}
.action-btn.danger {
background-color: #fff3f0;
color: #ff5722;
}
.action-btn::after {
border: none;
}
/* 纪念日列表 */
.section {
padding: 0 32rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.add-btn {
padding: 12rpx 24rpx;
background-color: #07c160;
color: #fff;
border-radius: 40rpx;
font-size: 24rpx;
border: none;
}
.add-btn::after {
border: none;
}
/* 纪念日卡片 */
.anniversary-card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.anniversary-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.anniversary-type {
display: flex;
align-items: center;
}
.type-icon {
font-size: 40rpx;
margin-right: 12rpx;
}
.type-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.importance-badge {
padding: 8rpx 20rpx;
border-radius: 40rpx;
font-size: 22rpx;
}
.importance-high {
background-color: #fff3f0;
color: #ff5722;
}
.importance-medium {
background-color: #fff8e6;
color: #ff9800;
}
.importance-low {
background-color: #f5f5f5;
color: #999;
}
.anniversary-date {
margin-bottom: 16rpx;
font-size: 26rpx;
color: #666;
}
.date-label {
color: #999;
}
.date-text {
color: #333;
font-weight: 500;
margin-right: 12rpx;
}
.lunar-badge {
display: inline-block;
padding: 4rpx 12rpx;
background-color: #e3f2fd;
color: #1976d2;
border-radius: 4rpx;
font-size: 20rpx;
}
.days-info {
margin: 16rpx 0;
}
.days-text {
font-size: 28rpx;
font-weight: 500;
color: #07c160;
}
.days-text.urgent {
color: #ff5722;
font-weight: 600;
}
.days-text.warning {
color: #ff9800;
}
.days-text.past {
color: #999;
}
.anniversary-remark {
font-size: 26rpx;
color: #999;
line-height: 1.6;
margin: 16rpx 0;
padding: 16rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.anniversary-actions {
display: flex;
gap: 16rpx;
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1px solid #f0f0f0;
}
.edit-btn,
.delete-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
border-radius: 8rpx;
font-size: 24rpx;
border: none;
}
.edit-btn {
background-color: #f5f5f5;
color: #666;
}
.delete-btn {
background-color: #fff3f0;
color: #ff5722;
}
.edit-btn::after,
.delete-btn::after {
border: none;
}
.empty-state {
text-align: center;
padding: 120rpx 40rpx;
}
.empty-state .icon {
font-size: 120rpx;
display: block;
margin-bottom: 32rpx;
}
.empty-state .text {
font-size: 28rpx;
color: #999;
}
+133
View File
@@ -0,0 +1,133 @@
// settings.js
const storage = require('../../utils/storage')
Page({
data: {
dataCount: {
persons: 0,
anniversaries: 0
}
},
onLoad() {
this.loadDataCount()
},
onShow() {
this.loadDataCount()
},
/**
* 加载数据统计
*/
loadDataCount() {
const persons = storage.getPersons()
const anniversaries = storage.getAnniversaries()
this.setData({
dataCount: {
persons: persons.length,
anniversaries: anniversaries.length
}
})
},
/**
* 导出数据
*/
onExportData() {
const data = storage.exportData()
// 转换为JSON字符串
const jsonStr = JSON.stringify(data, null, 2)
// 在小程序中,可以通过提示用户复制
wx.setClipboardData({
data: jsonStr,
success: () => {
wx.showToast({
title: '数据已复制到剪贴板',
icon: 'success'
})
}
})
},
/**
* 导入数据
*/
onImportData() {
wx.showModal({
title: '导入数据',
content: '将从剪贴板读取数据并导入。注意:导入会覆盖现有数据,请先备份!',
confirmText: '确认导入',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 读取剪贴板
wx.getClipboardData({
success: (res) => {
try {
const data = JSON.parse(res.data)
const success = storage.importData(data)
if (success) {
wx.showToast({
title: '导入成功',
icon: 'success'
})
setTimeout(() => {
wx.reLaunch({
url: '/pages/index/index'
})
}, 1500)
} else {
wx.showToast({
title: '导入失败,请检查数据格式',
icon: 'none'
})
}
} catch (e) {
wx.showToast({
title: '数据格式错误',
icon: 'none'
})
}
}
})
}
}
})
},
/**
* 清空所有数据
*/
onClearData() {
wx.showModal({
title: '确认清空',
content: '确定要清空所有数据吗?此操作不可恢复!',
confirmText: '确认清空',
confirmColor: '#ff5722',
success: (res) => {
if (res.confirm) {
const success = storage.clearAllData()
if (success) {
wx.showToast({
title: '已清空',
icon: 'success'
})
setTimeout(() => {
wx.reLaunch({
url: '/pages/index/index'
})
}, 1500)
}
}
}
})
}
})
+46
View File
@@ -0,0 +1,46 @@
<!--settings.wxml-->
<view class="container">
<view class="settings-list">
<!-- 数据备份 -->
<view class="setting-section">
<text class="section-title">数据管理</text>
<view class="setting-item" bindtap="onExportData">
<text class="item-label">📤 导出数据</text>
<text class="item-arrow"></text>
</view>
<view class="setting-item" bindtap="onImportData">
<text class="item-label">📥 导入数据</text>
<text class="item-arrow"></text>
</view>
<view class="setting-item" bindtap="onClearData">
<text class="item-label danger">🗑️ 清空所有数据</text>
<text class="item-arrow"></text>
</view>
</view>
<!-- 关于 -->
<view class="setting-section">
<text class="section-title">关于</text>
<view class="setting-item">
<text class="item-label">版本号</text>
<text class="item-value">v1.0.0</text>
</view>
<view class="setting-item">
<text class="item-label">开发作者</text>
<text class="item-value">生日提醒团队</text>
</view>
</view>
<!-- 提示信息 -->
<view class="info-box">
<text class="info-title">💡 温馨提示</text>
<text class="info-text">1. 建议定期导出数据备份\n2. 农历转换目前使用简化算法\n3. 提醒功能需要小程序权限</text>
</view>
</view>
</view>
+82
View File
@@ -0,0 +1,82 @@
/**settings.wxss**/
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.settings-list {
padding: 32rpx;
}
.setting-section {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 32rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx 32rpx;
background-color: #f9f9f9;
font-size: 26rpx;
color: #999;
font-weight: 500;
border-bottom: 1px solid #f0f0f0;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1px solid #f0f0f0;
}
.setting-item:last-child {
border-bottom: none;
}
.item-label {
font-size: 28rpx;
color: #333;
}
.item-label.danger {
color: #ff5722;
}
.item-value {
font-size: 26rpx;
color: #999;
}
.item-arrow {
font-size: 40rpx;
color: #ccc;
}
.info-box {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.info-title {
font-size: 28rpx;
color: #333;
font-weight: 600;
display: block;
margin-bottom: 16rpx;
}
.info-text {
font-size: 26rpx;
color: #666;
line-height: 2;
white-space: pre-line;
display: block;
}