feat: Add statistics page and enhance settings with data management features
This commit is contained in:
parent
5c3d956de9
commit
a4505f1aa5
269
README.md
Normal file
269
README.md
Normal file
@ -0,0 +1,269 @@
|
||||
# Pure Todo - 纯待办小程序
|
||||
|
||||
一个简洁高效的微信小程序待办事项管理应用,帮助你更好地管理时间和任务。
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
### 🎨 现代化设计
|
||||
- **渐变背景** - 使用美丽的渐变色彩设计
|
||||
- **卡片式布局** - 清晰的信息层次和视觉分离
|
||||
- **动画效果** - 流畅的过渡动画和交互反馈
|
||||
- **响应式设计** - 适配不同屏幕尺寸
|
||||
|
||||
### 📊 智能功能
|
||||
- **任务管理** - 添加、编辑、删除、完成任务
|
||||
- **优先级分类** - 高、中、低三个优先级等级
|
||||
- **智能搜索** - 快速查找特定任务
|
||||
- **状态筛选** - 按完成状态筛选任务
|
||||
- **数据统计** - 详细的完成率统计和分析
|
||||
|
||||
### 📈 数据分析
|
||||
- **总体概览** - 任务总数、完成数、完成率
|
||||
- **周数据图表** - 可视化展示一周的任务情况
|
||||
- **优先级分析** - 不同优先级的完成情况
|
||||
- **最近活动** - 查看最近的任务操作记录
|
||||
|
||||
### 🔧 个性化设置
|
||||
- **深色模式** - 保护眼睛的深色主题
|
||||
- **通知设置** - 自定义推送通知
|
||||
- **数据备份** - 导出和导入数据
|
||||
- **应用信息** - 版本信息和开发者联系方式
|
||||
|
||||
## 🚀 技术栈
|
||||
|
||||
- **框架**: 微信小程序原生开发
|
||||
- **UI组件**: TDesign 小程序组件库
|
||||
- **语言**: TypeScript
|
||||
- **样式**: WXSS (CSS)
|
||||
- **数据存储**: 微信小程序本地存储
|
||||
|
||||
## 📱 页面结构
|
||||
|
||||
### 首页 (index)
|
||||
- 个性化问候语
|
||||
- 今日概览统计
|
||||
- 今日任务列表
|
||||
- 最近任务记录
|
||||
- 快速操作入口
|
||||
|
||||
### 任务列表 (list)
|
||||
- 智能搜索功能
|
||||
- 多维度筛选(状态、优先级)
|
||||
- 排序选项(时间、优先级、名称)
|
||||
- 批量操作(清空已完成)
|
||||
- 任务详情展示
|
||||
|
||||
### 数据统计 (statistics)
|
||||
- 总体数据概览
|
||||
- 周数据图表
|
||||
- 优先级统计
|
||||
- 分类统计
|
||||
- 最近活动记录
|
||||
- 数据管理功能
|
||||
|
||||
### 设置 (settings)
|
||||
- 外观设置(深色模式)
|
||||
- 通知设置
|
||||
- 数据管理(备份、导入、清空)
|
||||
- 应用设置(重置、更新、评分)
|
||||
- 关于应用信息
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### 任务管理
|
||||
```typescript
|
||||
// 添加任务
|
||||
todoStorage.addTodo(text, priority, category);
|
||||
|
||||
// 更新任务
|
||||
todoStorage.updateTodo(id, updates);
|
||||
|
||||
// 删除任务
|
||||
todoStorage.deleteTodo(id);
|
||||
|
||||
// 切换完成状态
|
||||
todoStorage.toggleTodo(id);
|
||||
```
|
||||
|
||||
### 数据统计
|
||||
```typescript
|
||||
// 获取统计信息
|
||||
const stats = todoStorage.getStats();
|
||||
|
||||
// 按优先级获取任务
|
||||
const highPriorityTodos = todoStorage.getTodosByPriority('high');
|
||||
|
||||
// 获取今日任务
|
||||
const todayTodos = todoStorage.getTodayTodos();
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
```typescript
|
||||
// 导出数据
|
||||
const data = todoStorage.exportData();
|
||||
|
||||
// 导入数据
|
||||
const success = todoStorage.importData(dataString);
|
||||
|
||||
// 清空数据
|
||||
todoStorage.clearAllData();
|
||||
```
|
||||
|
||||
## 🎨 设计特色
|
||||
|
||||
### 色彩方案
|
||||
- **主色调**: #667eea (渐变蓝紫)
|
||||
- **辅助色**: #764ba2 (深紫)
|
||||
- **成功色**: #48dbfb (青色)
|
||||
- **警告色**: #feca57 (橙色)
|
||||
- **危险色**: #ff6b6b (红色)
|
||||
|
||||
### 交互设计
|
||||
- **卡片悬浮效果** - 鼠标悬停时的阴影变化
|
||||
- **按钮点击反馈** - 按下时的缩放动画
|
||||
- **页面切换动画** - 平滑的页面过渡
|
||||
- **加载动画** - 数据加载时的渐入效果
|
||||
|
||||
## 📦 安装和运行
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone [项目地址]
|
||||
cd pure-todo
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
# 或
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **开发工具**
|
||||
- 使用微信开发者工具打开项目
|
||||
- 确保已安装 TDesign 小程序组件库
|
||||
|
||||
4. **编译运行**
|
||||
- 在微信开发者工具中点击编译
|
||||
- 在模拟器中预览效果
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### app.json 配置
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/list/list",
|
||||
"pages/statistics/statistics",
|
||||
"pages/settings/settings"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"navigationBarTitleText": "Pure Todo",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f5f7fa"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件配置
|
||||
每个页面都配置了必要的 TDesign 组件:
|
||||
- `t-navbar` - 导航栏
|
||||
- `t-button` - 按钮
|
||||
- `t-input` - 输入框
|
||||
- `t-icon` - 图标
|
||||
- `t-card` - 卡片
|
||||
- `t-tag` - 标签
|
||||
- `t-progress` - 进度条
|
||||
- `t-checkbox` - 复选框
|
||||
- `t-switch` - 开关
|
||||
- `t-dialog` - 对话框
|
||||
|
||||
## 📊 数据结构
|
||||
|
||||
### 任务对象 (ITodo)
|
||||
```typescript
|
||||
interface ITodo {
|
||||
id: string; // 唯一标识
|
||||
text: string; // 任务内容
|
||||
completed: boolean; // 完成状态
|
||||
priority: 'high' | 'medium' | 'low'; // 优先级
|
||||
createdAt: number; // 创建时间
|
||||
completedAt?: number; // 完成时间
|
||||
category?: string; // 分类
|
||||
}
|
||||
```
|
||||
|
||||
### 统计对象 (ITodoStats)
|
||||
```typescript
|
||||
interface ITodoStats {
|
||||
total: number; // 总任务数
|
||||
completed: number; // 已完成数
|
||||
pending: number; // 待完成数
|
||||
completionRate: number; // 完成率
|
||||
todayCompleted: number; // 今日完成数
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 使用指南
|
||||
|
||||
### 添加任务
|
||||
1. 在首页点击右上角的 "+" 按钮
|
||||
2. 输入任务内容
|
||||
3. 选择优先级(高/中/低)
|
||||
4. 点击"添加"完成
|
||||
|
||||
### 管理任务
|
||||
1. 在任务列表页面查看所有任务
|
||||
2. 使用搜索框快速查找
|
||||
3. 使用筛选器按状态或优先级筛选
|
||||
4. 点击复选框标记完成
|
||||
5. 点击删除图标删除任务
|
||||
|
||||
### 查看统计
|
||||
1. 在统计页面查看总体数据
|
||||
2. 查看周数据图表了解趋势
|
||||
3. 分析不同优先级的完成情况
|
||||
4. 查看最近的活动记录
|
||||
|
||||
### 数据备份
|
||||
1. 在设置页面点击"导出数据"
|
||||
2. 复制备份数据并保存
|
||||
3. 需要恢复时点击"导入数据"
|
||||
4. 粘贴备份数据完成恢复
|
||||
|
||||
## 🔮 未来规划
|
||||
|
||||
- [ ] **云端同步** - 支持多设备数据同步
|
||||
- [ ] **提醒功能** - 任务到期提醒
|
||||
- [ ] **团队协作** - 多人共享任务列表
|
||||
- [ ] **数据导出** - 支持导出为Excel/PDF
|
||||
- [ ] **主题定制** - 更多主题色彩选择
|
||||
- [ ] **语音输入** - 语音添加任务
|
||||
- [ ] **智能建议** - AI智能任务建议
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个项目!
|
||||
|
||||
### 开发规范
|
||||
- 使用 TypeScript 进行类型检查
|
||||
- 遵循 ESLint 代码规范
|
||||
- 保持代码注释的完整性
|
||||
- 测试新功能后再提交
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
- 邮箱:developer@example.com
|
||||
- 微信:developer_wechat
|
||||
|
||||
---
|
||||
|
||||
**Pure Todo** - 让任务管理变得简单高效 ✨
|
||||
@ -2,9 +2,17 @@
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/list/list",
|
||||
"pages/settings/settings",
|
||||
"pages/logs/logs"
|
||||
"pages/statistics/statistics",
|
||||
"pages/settings/settings"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "dark",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTitleText": "Pure Todo",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "default"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"list": [
|
||||
@ -14,11 +22,30 @@
|
||||
{
|
||||
"pagePath": "pages/list/list"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/statistics/statistics"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/settings/settings"
|
||||
}
|
||||
]
|
||||
},
|
||||
"componentFramework": "glass-easel",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"usingComponents": {
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-input": "tdesign-miniprogram/input/input",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
|
||||
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
|
||||
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-progress": "tdesign-miniprogram/progress/progress",
|
||||
"t-switch": "tdesign-miniprogram/switch/switch",
|
||||
"t-dialog": "tdesign-miniprogram/dialog/dialog",
|
||||
"t-toast": "tdesign-miniprogram/toast/toast"
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,128 @@
|
||||
/**app.wxss**/
|
||||
|
||||
@import 'miniprogram_npm/tdesign-miniprogram/common/style/theme/_index.wxss';
|
||||
|
||||
/* app.wxss */
|
||||
page {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
/* 添加安全区适配 */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.primary-btn {
|
||||
background: #0052d9;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.primary-btn:active {
|
||||
background: #0034b5;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.modern-input {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e3e6eb;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
border-color: #0052d9;
|
||||
box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-high {
|
||||
background: #e34d59;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-medium {
|
||||
background: #ed7b2f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-low {
|
||||
background: #00a870;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress-container {
|
||||
background: #f3f3f3;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state .t-icon {
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 自定义标签栏 */
|
||||
.custom-tab-bar {
|
||||
background: #ffffff !important;
|
||||
border-top: 1px solid #e3e6eb;
|
||||
box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.05);
|
||||
/* 添加底部安全区适配 */
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 安全区适配工具类 */
|
||||
.safe-area-top {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.safe-area-left {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.safe-area-right {
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-input": "tdesign-miniprogram/input/input",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
|
||||
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-dialog": "tdesign-miniprogram/dialog/dialog",
|
||||
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
|
||||
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,395 @@
|
||||
// pages/index/index.ts
|
||||
import { todoStorage, ITodo, ITodoStats } from '../../utils/todoStorage';
|
||||
|
||||
Component({
|
||||
data: {
|
||||
activeTab: 'home', // For tab bar
|
||||
activeTab: 'home',
|
||||
stats: {} as ITodoStats,
|
||||
todayTodos: [] as ITodo[],
|
||||
recentTodos: [] as ITodo[],
|
||||
greeting: '',
|
||||
showAddSheet: false,
|
||||
showDateTimePicker: false,
|
||||
newTodoText: '',
|
||||
newTodoPriority: 'medium' as 'high' | 'medium' | 'low',
|
||||
newTodoCategory: '',
|
||||
newTodoDeadline: 0,
|
||||
newTodoNote: '',
|
||||
dateTimeRange: [
|
||||
[], // 年月日
|
||||
[] // 时分
|
||||
] as string[][],
|
||||
dateTimeValue: [0, 0] as number[]
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.loadData();
|
||||
this.setGreeting();
|
||||
}
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const stats = todoStorage.getStats();
|
||||
const todayTodos = todoStorage.getTodayTodos();
|
||||
const allTodos = todoStorage.getAllTodos();
|
||||
const recentTodos = allTodos.slice(0, 5);
|
||||
|
||||
this.setData({
|
||||
stats,
|
||||
todayTodos,
|
||||
recentTodos
|
||||
});
|
||||
},
|
||||
|
||||
// 获取成就图标
|
||||
getAchievementIcon(completionRate: number): string {
|
||||
if (completionRate === 100) return 'star-filled';
|
||||
if (completionRate >= 80) return 'medal';
|
||||
if (completionRate >= 60) return 'thumb-up';
|
||||
if (completionRate >= 40) return 'check-circle';
|
||||
if (completionRate >= 20) return 'time';
|
||||
return 'hourglass';
|
||||
},
|
||||
|
||||
// 获取成就颜色
|
||||
getAchievementColor(completionRate: number): string {
|
||||
if (completionRate === 100) return '#ffd700';
|
||||
if (completionRate >= 80) return '#00a870';
|
||||
if (completionRate >= 60) return '#0052d9';
|
||||
if (completionRate >= 40) return '#ed7b2f';
|
||||
if (completionRate >= 20) return '#666';
|
||||
return '#999';
|
||||
},
|
||||
|
||||
// 获取成就文本
|
||||
getAchievementText(completionRate: number): string {
|
||||
if (completionRate === 100) return '完美完成!';
|
||||
if (completionRate >= 80) return '表现优秀';
|
||||
if (completionRate >= 60) return '进展良好';
|
||||
if (completionRate >= 40) return '稳步前进';
|
||||
if (completionRate >= 20) return '开始行动';
|
||||
return '加油冲刺';
|
||||
},
|
||||
|
||||
// 设置问候语
|
||||
setGreeting() {
|
||||
const hour = new Date().getHours();
|
||||
let greeting = '';
|
||||
|
||||
if (hour < 6) {
|
||||
greeting = '夜深了,注意休息';
|
||||
} else if (hour < 12) {
|
||||
greeting = '早上好!新的一天开始了';
|
||||
} else if (hour < 18) {
|
||||
greeting = '下午好!继续加油';
|
||||
} else {
|
||||
greeting = '晚上好!今天过得怎么样';
|
||||
}
|
||||
|
||||
this.setData({ greeting });
|
||||
},
|
||||
|
||||
// 标签页切换
|
||||
onTabChange(e: any) {
|
||||
const targetPage = e.detail.value;
|
||||
if (targetPage === 'list') {
|
||||
wx.switchTab({ url: '/pages/list/list' });
|
||||
} else if (targetPage === 'statistics') {
|
||||
wx.switchTab({ url: '/pages/statistics/statistics' });
|
||||
} else if (targetPage === 'settings') {
|
||||
wx.switchTab({ url: '/pages/settings/settings' });
|
||||
}
|
||||
// No need to navigate if already on 'home'
|
||||
},
|
||||
|
||||
// 快速操作
|
||||
goToTasks() {
|
||||
wx.switchTab({ url: '/pages/list/list' });
|
||||
},
|
||||
|
||||
goToStatistics() {
|
||||
wx.switchTab({ url: '/pages/statistics/statistics' });
|
||||
},
|
||||
|
||||
goToSettings() {
|
||||
wx.switchTab({ url: '/pages/settings/settings' });
|
||||
},
|
||||
|
||||
// 显示添加半屏
|
||||
showAddSheet() {
|
||||
this.initDateTimeRange();
|
||||
this.setData({
|
||||
showAddSheet: true,
|
||||
newTodoText: '',
|
||||
newTodoPriority: 'medium',
|
||||
newTodoCategory: '',
|
||||
newTodoDeadline: 0,
|
||||
newTodoNote: ''
|
||||
});
|
||||
},
|
||||
|
||||
// 隐藏添加半屏
|
||||
hideAddSheet() {
|
||||
this.setData({ showAddSheet: false });
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {
|
||||
// 空函数,用于阻止事件冒泡
|
||||
},
|
||||
|
||||
// 输入新任务
|
||||
onNewTodoInput(e: any) {
|
||||
this.setData({ newTodoText: e.detail.value });
|
||||
},
|
||||
|
||||
// 输入备注
|
||||
onNewTodoNoteInput(e: any) {
|
||||
this.setData({ newTodoNote: e.detail.value });
|
||||
},
|
||||
|
||||
// 设置优先级
|
||||
onPriorityChange(e: any) {
|
||||
this.setData({ newTodoPriority: e.currentTarget.dataset.value });
|
||||
},
|
||||
|
||||
// 设置分类
|
||||
onCategoryChange(e: any) {
|
||||
this.setData({ newTodoCategory: e.currentTarget.dataset.value });
|
||||
},
|
||||
|
||||
// 初始化日期时间选择器
|
||||
initDateTimeRange() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
const date = now.getDate();
|
||||
|
||||
// 生成日期选项(今天及之后30天)
|
||||
const dateOptions = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const d = new Date(year, month, date + i);
|
||||
const dateStr = this.formatDateOption(d);
|
||||
dateOptions.push(dateStr);
|
||||
}
|
||||
|
||||
// 生成时间选项
|
||||
const timeOptions = [];
|
||||
for (let h = 0; h < 24; h++) {
|
||||
for (let m = 0; m < 60; m += 30) {
|
||||
const timeStr = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
||||
timeOptions.push(timeStr);
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({
|
||||
dateTimeRange: [dateOptions, timeOptions],
|
||||
dateTimeValue: [0, 16] // 默认选择今天 08:00
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化日期选项
|
||||
formatDateOption(date: Date): string {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
if (date.getTime() === today.getTime()) {
|
||||
return '今天';
|
||||
} else if (date.getTime() === tomorrow.getTime()) {
|
||||
return '明天';
|
||||
} else {
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
},
|
||||
|
||||
// 显示日期时间选择器
|
||||
showDateTimePicker() {
|
||||
// 先触发选择器
|
||||
const that = this;
|
||||
wx.showActionSheet({
|
||||
itemList: ['今天', '明天', '后天', '自定义日期'],
|
||||
success(res) {
|
||||
const now = new Date();
|
||||
let selectedDate = new Date();
|
||||
|
||||
switch(res.tapIndex) {
|
||||
case 0: // 今天
|
||||
selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0);
|
||||
break;
|
||||
case 1: // 明天
|
||||
selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 18, 0);
|
||||
break;
|
||||
case 2: // 后天
|
||||
selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2, 18, 0);
|
||||
break;
|
||||
case 3: // 自定义
|
||||
that.showCustomDatePicker();
|
||||
return;
|
||||
}
|
||||
|
||||
that.setData({
|
||||
newTodoDeadline: selectedDate.getTime()
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 显示自定义日期选择器
|
||||
showCustomDatePicker() {
|
||||
const now = new Date();
|
||||
const currentDate = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
|
||||
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
wx.showModal({
|
||||
title: '选择截止时间',
|
||||
content: '请在系统设置中选择日期和时间',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '使用当前时间',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 使用当前时间加1小时作为默认截止时间
|
||||
const deadline = new Date();
|
||||
deadline.setHours(deadline.getHours() + 1);
|
||||
this.setData({
|
||||
newTodoDeadline: deadline.getTime()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 清除截止时间
|
||||
clearDeadline() {
|
||||
this.setData({
|
||||
newTodoDeadline: 0
|
||||
});
|
||||
},
|
||||
|
||||
// 隐藏日期时间选择器
|
||||
hideDateTimePicker() {
|
||||
this.setData({ showDateTimePicker: false });
|
||||
},
|
||||
|
||||
// 日期时间选择器改变
|
||||
onDateTimeChange(e: any) {
|
||||
const [dateIndex, timeIndex] = e.detail.value;
|
||||
const { dateTimeRange } = this.data;
|
||||
|
||||
// 计算选择的日期时间
|
||||
const now = new Date();
|
||||
const selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + dateIndex);
|
||||
const timeStr = dateTimeRange[1][timeIndex];
|
||||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||||
|
||||
selectedDate.setHours(hours, minutes, 0, 0);
|
||||
|
||||
this.setData({
|
||||
dateTimeValue: [dateIndex, timeIndex],
|
||||
newTodoDeadline: selectedDate.getTime(),
|
||||
showDateTimePicker: false
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化日期时间显示
|
||||
formatDateTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
let dateStr = '';
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
dateStr = '今天';
|
||||
} else if (date.toDateString() === tomorrow.toDateString()) {
|
||||
dateStr = '明天';
|
||||
} else {
|
||||
dateStr = `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
|
||||
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
return `${dateStr} ${timeStr}`;
|
||||
},
|
||||
|
||||
// 添加新任务
|
||||
addTodo() {
|
||||
const { newTodoText, newTodoPriority, newTodoCategory, newTodoDeadline, newTodoNote } = this.data;
|
||||
if (newTodoText.trim() === '') {
|
||||
wx.showToast({
|
||||
title: '请输入任务内容',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
todoStorage.addTodo(
|
||||
newTodoText.trim(),
|
||||
newTodoPriority,
|
||||
newTodoCategory || undefined,
|
||||
newTodoDeadline || undefined,
|
||||
newTodoNote.trim() || undefined
|
||||
);
|
||||
|
||||
this.hideAddSheet();
|
||||
this.loadData();
|
||||
|
||||
wx.showToast({
|
||||
title: '任务已添加',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
// 切换任务状态
|
||||
toggleTodo(e: any) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
todoStorage.toggleTodo(id);
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 删除任务
|
||||
deleteTodo(e: any) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个任务吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
todoStorage.deleteTodo(id);
|
||||
this.loadData();
|
||||
wx.showToast({
|
||||
title: '已删除',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return '今天';
|
||||
} else if (days === 1) {
|
||||
return '昨天';
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,29 +1,265 @@
|
||||
<!--pages/index/index.wxml-->
|
||||
<view class="container">
|
||||
<t-navbar title="首页" />
|
||||
|
||||
<view class="hero-section">
|
||||
<image class="hero-image" src="/assets/images/dashboard-art.png" mode="aspectFit" />
|
||||
<text class="hero-title">欢迎回来!</text>
|
||||
<text class="hero-subtitle">今天有什么计划?让我们开始吧。</text>
|
||||
<!-- 顶部渐变背景 -->
|
||||
<view class="gradient-background"></view>
|
||||
|
||||
<!-- 顶部问候区域 -->
|
||||
<view class="greeting-section">
|
||||
<view class="greeting-content">
|
||||
<text class="greeting">{{greeting}}</text>
|
||||
<text class="date">{{formatTime(Date.now())}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<view class="stats-overview">
|
||||
<!-- 紧凑型进度卡片 -->
|
||||
<view class="compact-progress-card">
|
||||
<view class="progress-mini-circle">
|
||||
<view class="mini-circle-bg"></view>
|
||||
<view class="mini-circle-progress" style="transform: rotate({{stats.completionRate * 3.6 - 90}}deg);"></view>
|
||||
<view class="mini-circle-inner">
|
||||
<text class="mini-percentage">{{stats.completionRate}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="compact-stats">
|
||||
<view class="compact-info">
|
||||
<text class="compact-title">今日进度</text>
|
||||
<view class="compact-numbers">
|
||||
<text class="compact-number">{{stats.completed}}/{{stats.total}} 完成</text>
|
||||
<view class="achievement-mini">
|
||||
<t-icon name="{{getAchievementIcon(stats.completionRate)}}" size="12" color="{{getAchievementColor(stats.completionRate)}}" />
|
||||
<text class="achievement-mini-text">{{getAchievementText(stats.completionRate)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="quick-actions-mini">
|
||||
<view class="action-item" bind:tap="goToTasks">
|
||||
<t-icon name="bulletpoint" size="16" color="#0052d9" />
|
||||
<text class="action-text">全部</text>
|
||||
</view>
|
||||
<view class="action-item" bind:tap="showAddSheet">
|
||||
<t-icon name="add" size="16" color="#00a870" />
|
||||
<text class="action-text">添加</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日任务 -->
|
||||
<view class="today-tasks">
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日任务</text>
|
||||
<text class="task-count">{{todayTodos.length}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{todayTodos.length === 0}}" class="empty-state">
|
||||
<t-icon name="bulletpoint" size="24" color="#cccccc" />
|
||||
<text class="empty-text">暂无任务</text>
|
||||
</view>
|
||||
|
||||
<view wx:else class="task-list">
|
||||
<view wx:for="{{todayTodos}}" wx:key="id" class="task-item">
|
||||
<view class="task-content">
|
||||
<t-checkbox
|
||||
checked="{{item.completed}}"
|
||||
bind:change="toggleTodo"
|
||||
data-id="{{item.id}}"
|
||||
/>
|
||||
<text class="task-text {{item.completed ? 'completed' : ''}}">{{item.text}}</text>
|
||||
</view>
|
||||
<view class="task-actions">
|
||||
<t-tag
|
||||
theme="{{item.priority === 'high' ? 'danger' : item.priority === 'medium' ? 'warning' : 'primary'}}"
|
||||
size="small"
|
||||
variant="light"
|
||||
>
|
||||
{{item.priority === 'high' ? '高' : item.priority === 'medium' ? '中' : '低'}}
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<view class="quick-actions">
|
||||
<t-button theme="primary" size="large" block bind:tap="goToTasks" class="action-button">
|
||||
<t-button
|
||||
theme="light"
|
||||
size="medium"
|
||||
bind:tap="goToTasks"
|
||||
block
|
||||
>
|
||||
<t-icon name="bulletpoint" slot="icon" />
|
||||
查看我的任务
|
||||
</t-button>
|
||||
<t-button theme="default" size="large" block bind:tap="goToSettings" class="action-button">
|
||||
<t-icon name="setting" slot="icon" />
|
||||
打开设置
|
||||
查看所有任务
|
||||
</t-button>
|
||||
</view>
|
||||
|
||||
<view class="footer-space"></view>
|
||||
<!-- 添加任务半屏 -->
|
||||
<view class="add-task-sheet {{showAddSheet ? 'show' : ''}}" bind:tap="hideAddSheet">
|
||||
<view class="sheet-content" catch:tap="stopPropagation">
|
||||
<view class="sheet-header">
|
||||
<view class="sheet-handle"></view>
|
||||
<text class="sheet-title">添加新任务</text>
|
||||
<view class="sheet-close" bind:tap="hideAddSheet">
|
||||
<t-icon name="close" size="20" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sheet-body">
|
||||
<!-- 任务内容输入 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">任务内容</view>
|
||||
<t-input
|
||||
placeholder="请输入要完成的任务..."
|
||||
value="{{newTodoText}}"
|
||||
bind:change="onNewTodoInput"
|
||||
class="task-input"
|
||||
autoFocus="{{showAddSheet}}"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 优先级选择 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">优先级</view>
|
||||
<view class="priority-selector">
|
||||
<view
|
||||
class="priority-item {{newTodoPriority === 'high' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityChange"
|
||||
data-value="high"
|
||||
>
|
||||
<view class="priority-dot high"></view>
|
||||
<text class="priority-text">高优先级</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-item {{newTodoPriority === 'medium' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityChange"
|
||||
data-value="medium"
|
||||
>
|
||||
<view class="priority-dot medium"></view>
|
||||
<text class="priority-text">中优先级</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-item {{newTodoPriority === 'low' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityChange"
|
||||
data-value="low"
|
||||
>
|
||||
<view class="priority-dot low"></view>
|
||||
<text class="priority-text">低优先级</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类选择 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">分类</view>
|
||||
<view class="category-selector">
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'work' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="work"
|
||||
>
|
||||
<t-icon name="business" size="16" />
|
||||
<text>工作</text>
|
||||
</view>
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'personal' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="personal"
|
||||
>
|
||||
<t-icon name="user" size="16" />
|
||||
<text>个人</text>
|
||||
</view>
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'study' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="study"
|
||||
>
|
||||
<t-icon name="education" size="16" />
|
||||
<text>学习</text>
|
||||
</view>
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'life' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="life"
|
||||
>
|
||||
<t-icon name="home" size="16" />
|
||||
<text>生活</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 截止时间 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">截止时间(可选)</view>
|
||||
<view class="datetime-picker" bind:tap="showDateTimePicker">
|
||||
<t-icon name="time" size="16" color="#666" />
|
||||
<text class="datetime-text {{newTodoDeadline ? '' : 'placeholder'}}">
|
||||
{{newTodoDeadline ? formatDateTime(newTodoDeadline) : '点击选择截止时间'}}
|
||||
</text>
|
||||
<t-icon name="chevron-right" size="16" color="#ccc" />
|
||||
</view>
|
||||
<view wx:if="{{newTodoDeadline}}" class="datetime-clear" bind:tap="clearDeadline">
|
||||
<text class="clear-text">清除截止时间</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">备注(可选)</view>
|
||||
<t-input
|
||||
placeholder="添加备注信息..."
|
||||
value="{{newTodoNote}}"
|
||||
bind:change="onNewTodoNoteInput"
|
||||
class="note-input"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sheet-footer">
|
||||
<t-button
|
||||
theme="light"
|
||||
size="large"
|
||||
bind:tap="hideAddSheet"
|
||||
class="cancel-btn"
|
||||
>
|
||||
取消
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="large"
|
||||
bind:tap="addTodo"
|
||||
class="confirm-btn"
|
||||
disabled="{{newTodoText === '' || newTodoText.length === 0}}"
|
||||
>
|
||||
添加任务
|
||||
</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期时间选择器 -->
|
||||
<picker
|
||||
wx:if="{{showDateTimePicker}}"
|
||||
mode="multiSelector"
|
||||
range="{{dateTimeRange}}"
|
||||
value="{{dateTimeValue}}"
|
||||
bind:change="onDateTimeChange"
|
||||
bind:cancel="hideDateTimePicker"
|
||||
>
|
||||
<view></view>
|
||||
</picker>
|
||||
|
||||
<!-- 自定义标签栏 -->
|
||||
<t-tab-bar value="{{activeTab}}" bind:change="onTabChange" t-class="custom-tab-bar">
|
||||
<t-tab-bar-item value="home" icon="home" aria-label="首页">首页</t-tab-bar-item>
|
||||
<t-tab-bar-item value="list" icon="bulletpoint" aria-label="任务">任务</t-tab-bar-item>
|
||||
<t-tab-bar-item value="statistics" icon="chart" aria-label="统计">统计</t-tab-bar-item>
|
||||
<t-tab-bar-item value="settings" icon="setting" aria-label="设置">设置</t-tab-bar-item>
|
||||
</t-tab-bar>
|
||||
</view>
|
||||
|
||||
@ -1,83 +1,613 @@
|
||||
/**index.wxss**/
|
||||
page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f8f9fa; /* Lighter, cleaner background */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
/* pages/index/index.wxss */
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
/* 增大安全区,避开胶囊按钮 */
|
||||
padding-top: max(env(safe-area-inset-top), 60px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 顶部渐变背景 */
|
||||
.gradient-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 220px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(114, 67, 255, 0.28) 0%,
|
||||
rgba(67, 123, 255, 0.22) 20%,
|
||||
rgba(0, 191, 255, 0.15) 40%,
|
||||
rgba(159, 122, 234, 0.08) 60%,
|
||||
rgba(255, 255, 255, 0.03) 80%,
|
||||
rgba(255, 255, 255, 0) 100%);
|
||||
/* 添加动态效果 */
|
||||
background-size: 200% 200%;
|
||||
animation: gradientShift 8s ease-in-out infinite;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 渐变动画 */
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 问候区域 */
|
||||
.greeting-section {
|
||||
padding: 30px 16px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.greeting-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.date {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 统计概览 */
|
||||
.stats-overview {
|
||||
padding: 0 16px 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 紧凑型进度卡片 */
|
||||
.compact-progress-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid rgba(240, 240, 240, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 迷你圆形进度环 */
|
||||
.progress-mini-circle {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mini-circle-bg {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #f0f0f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mini-circle-progress {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid transparent;
|
||||
border-top-color: #0052d9;
|
||||
box-sizing: border-box;
|
||||
transform-origin: center;
|
||||
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.mini-circle-inner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-percentage {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0052d9;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 紧凑统计信息 */
|
||||
.compact-stats {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.compact-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 120rpx; /* Add padding for the fixed tab bar */
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.t-navbar {
|
||||
margin-bottom: 20rpx;
|
||||
.compact-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
.compact-numbers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.compact-number {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.achievement-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.achievement-mini-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 快速操作按钮 */
|
||||
.quick-actions-mini {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 60rpx 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 40rpx;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 82, 217, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 300rpx; /* Adjust as needed */
|
||||
height: 200rpx; /* Adjust as needed */
|
||||
margin-bottom: 30rpx;
|
||||
.action-item:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 28rpx;
|
||||
.action-text {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
/* 今日任务 */
|
||||
.today-tasks {
|
||||
padding: 0 16px 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.task-count {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx; /* Adds space between buttons */
|
||||
margin-bottom: 40rpx;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-button .t-button {
|
||||
width: 100%; /* Make button take full width if block attribute isn't enough */
|
||||
.task-item {
|
||||
background: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-button .t-icon {
|
||||
margin-right: 10rpx;
|
||||
.task-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-space {
|
||||
height: 100rpx; /* Space at the bottom before tab bar, if content is scrollable */
|
||||
.task-text {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.custom-tab-bar {
|
||||
.task-text.completed {
|
||||
text-decoration: line-through;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 快速操作 */
|
||||
.quick-actions {
|
||||
padding: 0 16px 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 添加任务半屏 */
|
||||
.add-task-sheet {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.add-task-sheet.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.sheet-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000; /* Ensure it's on top */
|
||||
background-color: #ffffff; /* Ensure tab bar has a background */
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
background: #ffffff;
|
||||
border-radius: 20px 20px 0 0;
|
||||
max-height: 85vh;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/** Removed old userinfo and motto styles as per the change description **/
|
||||
.add-task-sheet.show .sheet-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 半屏头部 */
|
||||
.sheet-header {
|
||||
padding: 12px 20px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sheet-handle {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #f5f5f5;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.sheet-close:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 半屏主体 */
|
||||
.sheet-body {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.task-input,
|
||||
.note-input {
|
||||
width: 100%;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.task-input:focus,
|
||||
.note-input:focus {
|
||||
border-color: #0052d9;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1);
|
||||
}
|
||||
|
||||
/* 优先级选择器 */
|
||||
.priority-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.priority-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.priority-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.priority-item.active {
|
||||
border-color: #0052d9;
|
||||
background: rgba(0, 82, 217, 0.05);
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.priority-dot.high {
|
||||
background: #e34d59;
|
||||
}
|
||||
|
||||
.priority-dot.medium {
|
||||
background: #ed7b2f;
|
||||
}
|
||||
|
||||
.priority-dot.low {
|
||||
background: #0052d9;
|
||||
}
|
||||
|
||||
.priority-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 分类选择器 */
|
||||
.category-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
border-color: #0052d9;
|
||||
background: rgba(0, 82, 217, 0.05);
|
||||
}
|
||||
|
||||
.category-item .t-icon {
|
||||
margin-bottom: 6px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.category-item.active .t-icon {
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.category-item text {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-item.active text {
|
||||
color: #0052d9;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 日期时间选择器 */
|
||||
.datetime-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datetime-picker:active {
|
||||
background: #fafafa;
|
||||
border-color: #0052d9;
|
||||
}
|
||||
|
||||
.datetime-text {
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.datetime-text.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 清除截止时间按钮 */
|
||||
.datetime-clear {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clear-text {
|
||||
font-size: 12px;
|
||||
color: #e34d59;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #e34d59;
|
||||
border-radius: 4px;
|
||||
background: rgba(227, 77, 89, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.datetime-clear:active .clear-text {
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
}
|
||||
|
||||
/* 半屏底部 */
|
||||
.sheet-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: #ffffff;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: #ffffff;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
flex: 2;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #0052d9 0%, #0043a5 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.confirm-btn[disabled] {
|
||||
background: #e0e0e0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 隐藏日期选择器 */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 对话框 */
|
||||
.dialog-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.priority-label {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.priority-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-input": "tdesign-miniprogram/input/input",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
|
||||
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
|
||||
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
|
||||
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
|
||||
"t-dialog": "tdesign-miniprogram/dialog/dialog"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "任务列表"
|
||||
|
||||
@ -1,62 +1,460 @@
|
||||
// pages/list/list.ts
|
||||
interface ITodo {
|
||||
text: string;
|
||||
completed: boolean;
|
||||
}
|
||||
import { todoStorage, ITodo, ITodoStats } from '../../utils/todoStorage';
|
||||
|
||||
Component({
|
||||
data: {
|
||||
newTodoText: '',
|
||||
activeTab: 'list',
|
||||
todos: [] as ITodo[],
|
||||
activeTab: 'list', // For tab bar
|
||||
filteredTodos: [] as ITodo[],
|
||||
stats: {} as ITodoStats,
|
||||
searchText: '',
|
||||
filterStatus: 'all' as 'all' | 'pending' | 'completed',
|
||||
filterPriority: 'all' as 'all' | 'high' | 'medium' | 'low',
|
||||
sortBy: 'createdAt' as 'createdAt' | 'priority' | 'text',
|
||||
pendingCount: 0,
|
||||
completedCount: 0,
|
||||
completionRate: 0,
|
||||
showAddSheet: false,
|
||||
showDateTimePicker: false,
|
||||
newTodoText: '',
|
||||
newTodoPriority: 'medium' as 'high' | 'medium' | 'low',
|
||||
newTodoCategory: '',
|
||||
newTodoDeadline: 0,
|
||||
newTodoNote: '',
|
||||
dateTimeRange: [
|
||||
[], // 年月日
|
||||
[] // 时分
|
||||
] as string[][],
|
||||
dateTimeValue: [0, 0] as number[]
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNewTodoInput(e: any) {
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const todos = todoStorage.getAllTodos();
|
||||
const stats = todoStorage.getStats();
|
||||
this.setData({ todos, stats });
|
||||
this.applyFilters();
|
||||
},
|
||||
|
||||
// 筛选和排序任务
|
||||
applyFilters() {
|
||||
let filtered = [...this.data.todos];
|
||||
|
||||
// 搜索筛选
|
||||
if (this.data.searchText) {
|
||||
filtered = filtered.filter(todo =>
|
||||
todo.text.toLowerCase().includes(this.data.searchText.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (this.data.filterStatus === 'pending') {
|
||||
filtered = filtered.filter(todo => !todo.completed);
|
||||
} else if (this.data.filterStatus === 'completed') {
|
||||
filtered = filtered.filter(todo => todo.completed);
|
||||
}
|
||||
|
||||
// 优先级筛选
|
||||
if (this.data.filterPriority !== 'all') {
|
||||
filtered = filtered.filter(todo => todo.priority === this.data.filterPriority);
|
||||
}
|
||||
|
||||
// 排序
|
||||
filtered.sort((a, b) => {
|
||||
switch (this.data.sortBy) {
|
||||
case 'createdAt':
|
||||
return b.createdAt - a.createdAt;
|
||||
case 'priority': {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
}
|
||||
case 'text':
|
||||
return a.text.localeCompare(b.text);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算统计数据
|
||||
const pendingCount = filtered.filter(todo => !todo.completed).length;
|
||||
const completedCount = filtered.filter(todo => todo.completed).length;
|
||||
const completionRate = filtered.length > 0 ? Math.round((completedCount / filtered.length) * 100) : 0;
|
||||
|
||||
this.setData({
|
||||
newTodoText: e.detail.value,
|
||||
filteredTodos: filtered,
|
||||
pendingCount,
|
||||
completedCount,
|
||||
completionRate
|
||||
});
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e: any) {
|
||||
this.setData({ searchText: e.detail.value }, () => {
|
||||
this.applyFilters();
|
||||
});
|
||||
},
|
||||
|
||||
// 状态筛选
|
||||
onStatusFilterChange(e: any) {
|
||||
this.setData({ filterStatus: e.currentTarget.dataset.value }, () => {
|
||||
this.applyFilters();
|
||||
});
|
||||
},
|
||||
|
||||
// 优先级筛选
|
||||
onPriorityFilterChange(e: any) {
|
||||
this.setData({ filterPriority: e.currentTarget.dataset.value }, () => {
|
||||
this.applyFilters();
|
||||
});
|
||||
},
|
||||
|
||||
// 排序
|
||||
onSortChange(e: any) {
|
||||
this.setData({ sortBy: e.currentTarget.dataset.value }, () => {
|
||||
this.applyFilters();
|
||||
});
|
||||
},
|
||||
|
||||
// 显示添加半屏
|
||||
showAddSheet() {
|
||||
this.initDateTimeRange();
|
||||
this.setData({
|
||||
showAddSheet: true,
|
||||
newTodoText: '',
|
||||
newTodoPriority: 'medium',
|
||||
newTodoCategory: '',
|
||||
newTodoDeadline: 0,
|
||||
newTodoNote: ''
|
||||
});
|
||||
},
|
||||
|
||||
// 隐藏添加半屏
|
||||
hideAddSheet() {
|
||||
this.setData({ showAddSheet: false });
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {
|
||||
// 空函数,用于阻止事件冒泡
|
||||
},
|
||||
|
||||
// 输入新任务
|
||||
onNewTodoInput(e: any) {
|
||||
this.setData({ newTodoText: e.detail.value });
|
||||
},
|
||||
|
||||
// 输入备注
|
||||
onNewTodoNoteInput(e: any) {
|
||||
this.setData({ newTodoNote: e.detail.value });
|
||||
},
|
||||
|
||||
// 设置优先级
|
||||
onPriorityChange(e: any) {
|
||||
this.setData({ newTodoPriority: e.currentTarget.dataset.value });
|
||||
},
|
||||
|
||||
// 设置分类
|
||||
onCategoryChange(e: any) {
|
||||
this.setData({ newTodoCategory: e.currentTarget.dataset.value });
|
||||
},
|
||||
|
||||
// 初始化日期时间选择器
|
||||
initDateTimeRange() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
const date = now.getDate();
|
||||
|
||||
// 生成日期选项(今天及之后30天)
|
||||
const dateOptions = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const d = new Date(year, month, date + i);
|
||||
const dateStr = this.formatDateOption(d);
|
||||
dateOptions.push(dateStr);
|
||||
}
|
||||
|
||||
// 生成时间选项
|
||||
const timeOptions = [];
|
||||
for (let h = 0; h < 24; h++) {
|
||||
for (let m = 0; m < 60; m += 30) {
|
||||
const timeStr = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
||||
timeOptions.push(timeStr);
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({
|
||||
dateTimeRange: [dateOptions, timeOptions],
|
||||
dateTimeValue: [0, 16] // 默认选择今天 08:00
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化日期选项
|
||||
formatDateOption(date: Date): string {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
if (date.getTime() === today.getTime()) {
|
||||
return '今天';
|
||||
} else if (date.getTime() === tomorrow.getTime()) {
|
||||
return '明天';
|
||||
} else {
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
},
|
||||
|
||||
// 显示日期时间选择器
|
||||
showDateTimePicker() {
|
||||
// 先触发选择器
|
||||
const that = this;
|
||||
wx.showActionSheet({
|
||||
itemList: ['今天', '明天', '后天', '自定义日期'],
|
||||
success(res) {
|
||||
const now = new Date();
|
||||
let selectedDate = new Date();
|
||||
|
||||
switch(res.tapIndex) {
|
||||
case 0: // 今天
|
||||
selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0);
|
||||
break;
|
||||
case 1: // 明天
|
||||
selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 18, 0);
|
||||
break;
|
||||
case 2: // 后天
|
||||
selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2, 18, 0);
|
||||
break;
|
||||
case 3: // 自定义
|
||||
that.showCustomDatePicker();
|
||||
return;
|
||||
}
|
||||
|
||||
that.setData({
|
||||
newTodoDeadline: selectedDate.getTime()
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 显示自定义日期选择器
|
||||
showCustomDatePicker() {
|
||||
const now = new Date();
|
||||
const currentDate = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
|
||||
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
wx.showModal({
|
||||
title: '选择截止时间',
|
||||
content: '请在系统设置中选择日期和时间',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '使用当前时间',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 使用当前时间加1小时作为默认截止时间
|
||||
const deadline = new Date();
|
||||
deadline.setHours(deadline.getHours() + 1);
|
||||
this.setData({
|
||||
newTodoDeadline: deadline.getTime()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 清除截止时间
|
||||
clearDeadline() {
|
||||
this.setData({
|
||||
newTodoDeadline: 0
|
||||
});
|
||||
},
|
||||
|
||||
// 隐藏日期时间选择器
|
||||
hideDateTimePicker() {
|
||||
this.setData({ showDateTimePicker: false });
|
||||
},
|
||||
|
||||
// 日期时间选择器改变
|
||||
onDateTimeChange(e: any) {
|
||||
const [dateIndex, timeIndex] = e.detail.value;
|
||||
const { dateTimeRange } = this.data;
|
||||
|
||||
// 计算选择的日期时间
|
||||
const now = new Date();
|
||||
const selectedDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + dateIndex);
|
||||
const timeStr = dateTimeRange[1][timeIndex];
|
||||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||||
|
||||
selectedDate.setHours(hours, minutes, 0, 0);
|
||||
|
||||
this.setData({
|
||||
dateTimeValue: [dateIndex, timeIndex],
|
||||
newTodoDeadline: selectedDate.getTime(),
|
||||
showDateTimePicker: false
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化日期时间显示
|
||||
formatDateTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
let dateStr = '';
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
dateStr = '今天';
|
||||
} else if (date.toDateString() === tomorrow.toDateString()) {
|
||||
dateStr = '明天';
|
||||
} else {
|
||||
dateStr = `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
|
||||
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
return `${dateStr} ${timeStr}`;
|
||||
},
|
||||
|
||||
// 添加新任务
|
||||
addTodo() {
|
||||
const { newTodoText, todos } = this.data;
|
||||
const { newTodoText, newTodoPriority, newTodoCategory, newTodoDeadline, newTodoNote } = this.data;
|
||||
if (newTodoText.trim() === '') {
|
||||
wx.showToast({
|
||||
title: '请输入任务内容',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newTodos = [...todos, { text: newTodoText, completed: false }];
|
||||
this.setData({
|
||||
todos: newTodos,
|
||||
newTodoText: '',
|
||||
|
||||
todoStorage.addTodo(
|
||||
newTodoText.trim(),
|
||||
newTodoPriority,
|
||||
newTodoCategory || undefined,
|
||||
newTodoDeadline || undefined,
|
||||
newTodoNote.trim() || undefined
|
||||
);
|
||||
|
||||
this.hideAddSheet();
|
||||
this.loadData();
|
||||
|
||||
wx.showToast({
|
||||
title: '任务已添加',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
// 切换任务状态
|
||||
toggleTodo(e: any) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const { todos } = this.data;
|
||||
const newTodos = todos.map((todo, i) => {
|
||||
if (i === index) {
|
||||
return { ...todo, completed: !todo.completed };
|
||||
const id = e.currentTarget.dataset.id;
|
||||
todoStorage.toggleTodo(id);
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 编辑任务
|
||||
editTodo(e: any) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const todo = this.data.todos.find(t => t.id === id);
|
||||
if (todo) {
|
||||
wx.showModal({
|
||||
title: '编辑任务',
|
||||
content: '编辑功能开发中,敬请期待!',
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 删除任务
|
||||
deleteTodo(e: any) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个任务吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
todoStorage.deleteTodo(id);
|
||||
this.loadData();
|
||||
wx.showToast({
|
||||
title: '已删除',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
return todo;
|
||||
});
|
||||
this.setData({
|
||||
todos: newTodos,
|
||||
});
|
||||
},
|
||||
removeTodo(e: any) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const { todos } = this.data;
|
||||
const newTodos = todos.filter((_, i) => i !== index);
|
||||
this.setData({
|
||||
todos: newTodos,
|
||||
|
||||
// 清空已完成任务
|
||||
clearCompleted() {
|
||||
wx.showModal({
|
||||
title: '确认清空',
|
||||
content: '确定要清空所有已完成的任务吗?此操作不可恢复。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const todos = this.data.todos.filter(todo => !todo.completed);
|
||||
todoStorage.saveAllTodos(todos);
|
||||
this.loadData();
|
||||
wx.showToast({
|
||||
title: '已清空',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 导出数据
|
||||
exportData() {
|
||||
const data = todoStorage.exportData();
|
||||
wx.setClipboardData({
|
||||
data: data,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '数据已复制到剪贴板',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return '今天';
|
||||
} else if (days === 1) {
|
||||
return '昨天';
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
},
|
||||
|
||||
// 标签页切换
|
||||
onTabChange(e: any) {
|
||||
const targetPage = e.detail.value;
|
||||
if (targetPage === 'home') {
|
||||
wx.switchTab({ url: '/pages/index/index' });
|
||||
} else if (targetPage === 'statistics') {
|
||||
wx.switchTab({ url: '/pages/statistics/statistics' });
|
||||
} else if (targetPage === 'settings') {
|
||||
wx.switchTab({ url: '/pages/settings/settings' });
|
||||
}
|
||||
// No need to navigate if already on 'list'
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1,46 +1,403 @@
|
||||
<!--pages/list/list.wxml-->
|
||||
<view class="container">
|
||||
<t-navbar title="我的任务" />
|
||||
|
||||
<view class="add-todo-section">
|
||||
<t-input
|
||||
t-class="add-todo-input"
|
||||
placeholder="写下你的任务..."
|
||||
value="{{newTodoText}}"
|
||||
bind:change="onNewTodoInput"
|
||||
bind:enter="addTodo"
|
||||
/>
|
||||
<t-button t-class="add-todo-button" theme="primary" icon="add" bind:tap="addTodo" />
|
||||
<!-- 顶部标题区域 -->
|
||||
<view class="header-section">
|
||||
<view class="header-content">
|
||||
<text class="header-title">任务清单</text>
|
||||
<view class="header-actions">
|
||||
<view class="add-button" bind:tap="showAddSheet">
|
||||
<t-icon name="add" size="18" color="white" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="header-subtitle">管理你的所有任务</text>
|
||||
</view>
|
||||
|
||||
<!-- 统计概览卡片 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-row">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.total}}</text>
|
||||
<text class="stat-label">总任务</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.completed}}</text>
|
||||
<text class="stat-label">已完成</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.pending}}</text>
|
||||
<text class="stat-label">待完成</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{stats.completionRate}}%</text>
|
||||
<text class="stat-label">完成率</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索和筛选工具栏 -->
|
||||
<view class="toolbar-section">
|
||||
<view class="search-box">
|
||||
<t-input
|
||||
placeholder="搜索任务..."
|
||||
value="{{searchText}}"
|
||||
bind:change="onSearchInput"
|
||||
prefix-icon="search"
|
||||
class="search-input"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
class="filter-tab {{filterStatus === 'all' ? 'active' : ''}}"
|
||||
bind:tap="onStatusFilterChange"
|
||||
data-value="all"
|
||||
>
|
||||
<text>全部</text>
|
||||
<text class="tab-count">{{filteredTodos.length}}</text>
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab {{filterStatus === 'pending' ? 'active' : ''}}"
|
||||
bind:tap="onStatusFilterChange"
|
||||
data-value="pending"
|
||||
>
|
||||
<text>待完成</text>
|
||||
<text class="tab-count">{{pendingCount}}</text>
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab {{filterStatus === 'completed' ? 'active' : ''}}"
|
||||
bind:tap="onStatusFilterChange"
|
||||
data-value="completed"
|
||||
>
|
||||
<text>已完成</text>
|
||||
<text class="tab-count">{{completedCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 排序和筛选选项 -->
|
||||
<view class="sort-section">
|
||||
<view class="sort-options">
|
||||
<view
|
||||
class="sort-option {{sortBy === 'createdAt' ? 'active' : ''}}"
|
||||
bind:tap="onSortChange"
|
||||
data-value="createdAt"
|
||||
>
|
||||
<t-icon name="time" size="14" />
|
||||
<text>时间</text>
|
||||
</view>
|
||||
<view
|
||||
class="sort-option {{sortBy === 'priority' ? 'active' : ''}}"
|
||||
bind:tap="onSortChange"
|
||||
data-value="priority"
|
||||
>
|
||||
<t-icon name="star" size="14" />
|
||||
<text>优先级</text>
|
||||
</view>
|
||||
<view
|
||||
class="sort-option {{sortBy === 'text' ? 'active' : ''}}"
|
||||
bind:tap="onSortChange"
|
||||
data-value="text"
|
||||
>
|
||||
<t-icon name="text" size="14" />
|
||||
<text>名称</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="priority-filter">
|
||||
<view
|
||||
class="priority-filter-item {{filterPriority === 'all' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityFilterChange"
|
||||
data-value="all"
|
||||
>
|
||||
<text>全部</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-filter-item {{filterPriority === 'high' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityFilterChange"
|
||||
data-value="high"
|
||||
>
|
||||
<view class="priority-dot high"></view>
|
||||
<text>高</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-filter-item {{filterPriority === 'medium' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityFilterChange"
|
||||
data-value="medium"
|
||||
>
|
||||
<view class="priority-dot medium"></view>
|
||||
<text>中</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-filter-item {{filterPriority === 'low' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityFilterChange"
|
||||
data-value="low"
|
||||
>
|
||||
<view class="priority-dot low"></view>
|
||||
<text>低</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<scroll-view class="todo-list-scroll" scroll-y type="list">
|
||||
<t-cell-group t-class="todo-list">
|
||||
<block wx:if="{{todos.length === 0}}">
|
||||
<view class="empty-state">
|
||||
<t-icon name="check-circle" size="xl" />
|
||||
<text>太棒了!没有待办事项</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<t-cell wx:for="{{todos}}" wx:key="index" t-class-left="todo-item-left">
|
||||
<view class="todo-item-content">
|
||||
<t-checkbox
|
||||
checked="{{item.completed}}"
|
||||
bind:change="toggleTodo"
|
||||
data-index="{{index}}"
|
||||
icon="{{ item.completed ? ['check-circle-filled', 'circle'] : ['circle', 'check-circle-filled'] }}"
|
||||
/>
|
||||
<text class="{{item.completed ? 'todo-text completed' : 'todo-text'}}">{{item.text}}</text>
|
||||
<view class="todo-list">
|
||||
<view wx:if="{{filteredTodos.length === 0}}" class="empty-state">
|
||||
<t-icon name="search" size="32" color="#ccc" />
|
||||
<text wx:if="{{searchText || filterStatus !== 'all' || filterPriority !== 'all'}}" class="empty-text">
|
||||
没有找到匹配的任务
|
||||
</text>
|
||||
<text wx:else class="empty-text">还没有任务,添加一个吧!</text>
|
||||
</view>
|
||||
|
||||
<view wx:else>
|
||||
<view wx:for="{{filteredTodos}}" wx:key="id" class="todo-item {{item.completed ? 'completed' : ''}}">
|
||||
<view class="todo-main">
|
||||
<view class="todo-checkbox">
|
||||
<t-checkbox
|
||||
checked="{{item.completed}}"
|
||||
bind:change="toggleTodo"
|
||||
data-id="{{item.id}}"
|
||||
icon="{{ item.completed ? ['check-circle-filled', 'circle'] : ['circle', 'check-circle-filled'] }}"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="todo-content">
|
||||
<view class="todo-header">
|
||||
<text class="todo-text {{item.completed ? 'completed' : ''}}">{{item.text}}</text>
|
||||
<view class="todo-priority">
|
||||
<t-tag
|
||||
theme="{{item.priority === 'high' ? 'danger' : item.priority === 'medium' ? 'warning' : 'primary'}}"
|
||||
size="small"
|
||||
variant="light"
|
||||
>
|
||||
{{item.priority === 'high' ? '高' : item.priority === 'medium' ? '中' : '低'}}
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="todo-meta">
|
||||
<view class="meta-left">
|
||||
<text class="todo-time">{{formatTime(item.createdAt)}}</text>
|
||||
<text wx:if="{{item.category}}" class="todo-category">
|
||||
{{item.category === 'work' ? '工作' : item.category === 'personal' ? '个人' : item.category === 'study' ? '学习' : '生活'}}
|
||||
</text>
|
||||
<text wx:if="{{item.deadline}}" class="todo-deadline {{item.deadline < Date.now() && !item.completed ? 'overdue' : ''}}">
|
||||
{{formatDateTime(item.deadline)}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="meta-right">
|
||||
<text wx:if="{{item.completed}}" class="completed-time">
|
||||
完成于 {{formatTime(item.completedAt)}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text wx:if="{{item.note}}" class="todo-note">{{item.note}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<t-icon name="delete" slot="right-icon" color="#e54d42" bind:tap="removeTodo" data-index="{{index}}" />
|
||||
</t-cell>
|
||||
</block>
|
||||
</t-cell-group>
|
||||
|
||||
<view class="todo-actions">
|
||||
<view class="action-btn" bind:tap="editTodo" data-id="{{item.id}}">
|
||||
<t-icon name="edit" size="16" color="#666" />
|
||||
</view>
|
||||
<view class="action-btn" bind:tap="deleteTodo" data-id="{{item.id}}">
|
||||
<t-icon name="delete" size="16" color="#e34d59" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<view class="action-group">
|
||||
<t-button
|
||||
theme="light"
|
||||
size="small"
|
||||
bind:tap="clearCompleted"
|
||||
class="clear-btn"
|
||||
>
|
||||
清空已完成
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="light"
|
||||
size="small"
|
||||
bind:tap="exportData"
|
||||
class="export-btn"
|
||||
>
|
||||
导出数据
|
||||
</t-button>
|
||||
</view>
|
||||
<text class="task-summary">
|
||||
共 {{filteredTodos.length}} 个任务,完成率 {{completionRate}}%
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 添加任务半屏 -->
|
||||
<view class="add-task-sheet {{showAddSheet ? 'show' : ''}}" bind:tap="hideAddSheet">
|
||||
<view class="sheet-content" catch:tap="stopPropagation">
|
||||
<view class="sheet-header">
|
||||
<view class="sheet-handle"></view>
|
||||
<text class="sheet-title">添加新任务</text>
|
||||
<view class="sheet-close" bind:tap="hideAddSheet">
|
||||
<t-icon name="close" size="20" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sheet-body">
|
||||
<!-- 任务内容输入 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">任务内容</view>
|
||||
<t-input
|
||||
placeholder="请输入要完成的任务..."
|
||||
value="{{newTodoText}}"
|
||||
bind:change="onNewTodoInput"
|
||||
class="task-input"
|
||||
autoFocus="{{showAddSheet}}"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 优先级选择 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">优先级</view>
|
||||
<view class="priority-selector">
|
||||
<view
|
||||
class="priority-item {{newTodoPriority === 'high' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityChange"
|
||||
data-value="high"
|
||||
>
|
||||
<view class="priority-dot high"></view>
|
||||
<text class="priority-text">高优先级</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-item {{newTodoPriority === 'medium' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityChange"
|
||||
data-value="medium"
|
||||
>
|
||||
<view class="priority-dot medium"></view>
|
||||
<text class="priority-text">中优先级</text>
|
||||
</view>
|
||||
<view
|
||||
class="priority-item {{newTodoPriority === 'low' ? 'active' : ''}}"
|
||||
bind:tap="onPriorityChange"
|
||||
data-value="low"
|
||||
>
|
||||
<view class="priority-dot low"></view>
|
||||
<text class="priority-text">低优先级</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类选择 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">分类</view>
|
||||
<view class="category-selector">
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'work' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="work"
|
||||
>
|
||||
<t-icon name="business" size="16" />
|
||||
<text>工作</text>
|
||||
</view>
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'personal' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="personal"
|
||||
>
|
||||
<t-icon name="user" size="16" />
|
||||
<text>个人</text>
|
||||
</view>
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'study' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="study"
|
||||
>
|
||||
<t-icon name="education" size="16" />
|
||||
<text>学习</text>
|
||||
</view>
|
||||
<view
|
||||
class="category-item {{newTodoCategory === 'life' ? 'active' : ''}}"
|
||||
bind:tap="onCategoryChange"
|
||||
data-value="life"
|
||||
>
|
||||
<t-icon name="home" size="16" />
|
||||
<text>生活</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 截止时间 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">截止时间(可选)</view>
|
||||
<view class="datetime-picker" bind:tap="showDateTimePicker">
|
||||
<t-icon name="time" size="16" color="#666" />
|
||||
<text class="datetime-text {{newTodoDeadline ? '' : 'placeholder'}}">
|
||||
{{newTodoDeadline ? formatDateTime(newTodoDeadline) : '点击选择截止时间'}}
|
||||
</text>
|
||||
<t-icon name="chevron-right" size="16" color="#ccc" />
|
||||
</view>
|
||||
<view wx:if="{{newTodoDeadline}}" class="datetime-clear" bind:tap="clearDeadline">
|
||||
<text class="clear-text">清除截止时间</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="input-section">
|
||||
<view class="input-label">备注(可选)</view>
|
||||
<t-input
|
||||
placeholder="添加备注信息..."
|
||||
value="{{newTodoNote}}"
|
||||
bind:change="onNewTodoNoteInput"
|
||||
class="note-input"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sheet-footer">
|
||||
<t-button
|
||||
theme="light"
|
||||
size="large"
|
||||
bind:tap="hideAddSheet"
|
||||
class="cancel-btn"
|
||||
>
|
||||
取消
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="large"
|
||||
bind:tap="addTodo"
|
||||
class="confirm-btn"
|
||||
disabled="{{newTodoText === '' || newTodoText.length === 0}}"
|
||||
>
|
||||
添加任务
|
||||
</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期时间选择器 -->
|
||||
<picker
|
||||
wx:if="{{showDateTimePicker}}"
|
||||
mode="multiSelector"
|
||||
range="{{dateTimeRange}}"
|
||||
value="{{dateTimeValue}}"
|
||||
bind:change="onDateTimeChange"
|
||||
bind:cancel="hideDateTimePicker"
|
||||
>
|
||||
<view></view>
|
||||
</picker>
|
||||
|
||||
<!-- 自定义标签栏 -->
|
||||
<t-tab-bar value="{{activeTab}}" bind:change="onTabChange" t-class="custom-tab-bar">
|
||||
<t-tab-bar-item value="home" icon="home" aria-label="首页">首页</t-tab-bar-item>
|
||||
<t-tab-bar-item value="list" icon="bulletpoint" aria-label="任务">任务</t-tab-bar-item>
|
||||
<t-tab-bar-item value="statistics" icon="chart" aria-label="统计">统计</t-tab-bar-item>
|
||||
<t-tab-bar-item value="settings" icon="setting" aria-label="设置">设置</t-tab-bar-item>
|
||||
</t-tab-bar>
|
||||
</view>
|
||||
|
||||
@ -1,115 +1,702 @@
|
||||
/* pages/list/list.wxss */
|
||||
page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f4f4f4;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
/* 确保顶部有足够的安全区 */
|
||||
padding-top: max(env(safe-area-inset-top), 16px);
|
||||
}
|
||||
|
||||
/* 顶部标题区域 */
|
||||
.header-section {
|
||||
padding: 16px 16px 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #0052d9;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-button:active {
|
||||
background: #0034b5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 统计概览卡片 */
|
||||
.stats-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin: 0 16px 16px;
|
||||
border: 1px solid rgba(240, 240, 240, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0052d9;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 搜索和筛选工具栏 */
|
||||
.toolbar-section {
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 120rpx; /* Space for tab bar */
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.add-todo-section {
|
||||
.filter-tab.active {
|
||||
background: #0052d9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-tab text {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 排序和筛选选项 */
|
||||
.sort-section {
|
||||
padding: 0 16px 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sort-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sort-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 10rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
background: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.add-todo-input {
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
.sort-option.active {
|
||||
background: #0052d9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-todo-input .t-input__control {
|
||||
font-size: 30rpx;
|
||||
.priority-filter {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.add-todo-button {
|
||||
min-width: auto;
|
||||
.priority-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
background: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.priority-filter-item.active {
|
||||
background: #0052d9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.priority-dot.high {
|
||||
background: #e34d59;
|
||||
}
|
||||
|
||||
.priority-dot.medium {
|
||||
background: #ed7b2f;
|
||||
}
|
||||
|
||||
.priority-dot.low {
|
||||
background: #0052d9;
|
||||
}
|
||||
|
||||
/* 任务列表 */
|
||||
.todo-list-scroll {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
overflow-y: auto;
|
||||
padding: 0 16px;
|
||||
/* 确保底部有足够的安全区 */
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 100px);
|
||||
}
|
||||
|
||||
.todo-item-left {
|
||||
.todo-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.todo-item-content {
|
||||
.todo-item {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.todo-item.completed {
|
||||
background: #fafafa;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.todo-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.todo-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.todo-checkbox {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.todo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.todo-text {
|
||||
margin-left: 20rpx;
|
||||
font-size: 30rpx;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
flex-grow: 1;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.todo-text.completed {
|
||||
text-decoration: line-through;
|
||||
color: #aaa;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
.todo-priority {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.todo-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-left {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.meta-right {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.todo-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.todo-category {
|
||||
font-size: 11px;
|
||||
color: #0052d9;
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.todo-deadline {
|
||||
font-size: 11px;
|
||||
color: #ed7b2f;
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.todo-deadline.overdue {
|
||||
color: #e34d59;
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
}
|
||||
|
||||
.completed-time {
|
||||
font-size: 11px;
|
||||
color: #00a870;
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.todo-note {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
background: #f9f9f9;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #0052d9;
|
||||
}
|
||||
|
||||
.todo-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.9);
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-actions {
|
||||
padding: 12px 16px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
/* 添加底部安全区适配 */
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 12px);
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.clear-btn,
|
||||
.export-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.task-summary {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state .t-icon {
|
||||
margin-bottom: 20rpx;
|
||||
color: #0052d9;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state text {
|
||||
font-size: 28rpx;
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.t-navbar {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.todo-list .t-cell {
|
||||
padding-top: 24rpx;
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.todo-list .t-cell .t-icon[name="delete"] {
|
||||
font-size: 40rpx;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.custom-tab-bar {
|
||||
/* 添加任务半屏样式 */
|
||||
.add-task-sheet {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.add-task-sheet.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.sheet-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000; /* Ensure it's on top */
|
||||
background: #ffffff;
|
||||
border-radius: 20px 20px 0 0;
|
||||
max-height: 85vh;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.add-task-sheet.show .sheet-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 半屏头部 */
|
||||
.sheet-header {
|
||||
padding: 12px 20px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sheet-handle {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sheet-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #f5f5f5;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.sheet-close:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 半屏主体 */
|
||||
.sheet-body {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.task-input,
|
||||
.note-input {
|
||||
width: 100%;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.task-input:focus,
|
||||
.note-input:focus {
|
||||
border-color: #0052d9;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1);
|
||||
}
|
||||
|
||||
/* 优先级选择器 */
|
||||
.priority-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.priority-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.priority-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.priority-item.active {
|
||||
border-color: #0052d9;
|
||||
background: rgba(0, 82, 217, 0.05);
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.priority-dot.high {
|
||||
background: #e34d59;
|
||||
}
|
||||
|
||||
.priority-dot.medium {
|
||||
background: #ed7b2f;
|
||||
}
|
||||
|
||||
.priority-dot.low {
|
||||
background: #0052d9;
|
||||
}
|
||||
|
||||
.priority-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 分类选择器 */
|
||||
.category-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
border-color: #0052d9;
|
||||
background: rgba(0, 82, 217, 0.05);
|
||||
}
|
||||
|
||||
.category-item .t-icon {
|
||||
margin-bottom: 6px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.category-item.active .t-icon {
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.category-item text {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-item.active text {
|
||||
color: #0052d9;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 日期时间选择器 */
|
||||
.datetime-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datetime-picker:active {
|
||||
background: #fafafa;
|
||||
border-color: #0052d9;
|
||||
}
|
||||
|
||||
.datetime-text {
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.datetime-text.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 清除截止时间按钮 */
|
||||
.datetime-clear {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clear-text {
|
||||
font-size: 12px;
|
||||
color: #e34d59;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #e34d59;
|
||||
border-radius: 4px;
|
||||
background: rgba(227, 77, 89, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.datetime-clear:active .clear-text {
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
}
|
||||
|
||||
/* 半屏底部 */
|
||||
.sheet-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: #ffffff;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: #ffffff;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
flex: 2;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #0052d9 0%, #0043a5 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.confirm-btn[disabled] {
|
||||
background: #e0e0e0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
// logs.ts
|
||||
// const util = require('../../utils/util.js')
|
||||
import { formatTime } from '../../utils/util'
|
||||
|
||||
Component({
|
||||
data: {
|
||||
logs: [],
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.setData({
|
||||
logs: (wx.getStorageSync('logs') || []).map((log: string) => {
|
||||
return {
|
||||
date: formatTime(new Date(log)),
|
||||
timeStamp: log
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -1,6 +0,0 @@
|
||||
<!--logs.wxml-->
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
|
||||
<view class="log-item">{{index + 1}}. {{log.date}}</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
@ -1,16 +0,0 @@
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scrollarea {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.log-item {
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.log-item:last-child {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
|
||||
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
"t-switch": "tdesign-miniprogram/switch/switch",
|
||||
"t-dialog": "tdesign-miniprogram/dialog/dialog"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "设置"
|
||||
|
||||
@ -1,18 +1,239 @@
|
||||
// pages/settings/settings.ts
|
||||
import { todoStorage } from '../../utils/todoStorage';
|
||||
|
||||
Component({
|
||||
data: {
|
||||
activeTab: 'settings', // For tab bar
|
||||
activeTab: 'settings',
|
||||
darkMode: false,
|
||||
notifications: true,
|
||||
autoBackup: true,
|
||||
stats: {} as any,
|
||||
appVersion: '1.0.0',
|
||||
showAboutDialog: false,
|
||||
showBackupDialog: false,
|
||||
backupData: ''
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.loadSettings();
|
||||
this.loadStats();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载设置
|
||||
loadSettings() {
|
||||
try {
|
||||
const settings = wx.getStorageSync('pure_todo_settings');
|
||||
if (settings) {
|
||||
const parsedSettings = JSON.parse(settings);
|
||||
this.setData({
|
||||
darkMode: parsedSettings.darkMode || false,
|
||||
notifications: parsedSettings.notifications !== false,
|
||||
autoBackup: parsedSettings.autoBackup !== false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 保存设置
|
||||
saveSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
darkMode: this.data.darkMode,
|
||||
notifications: this.data.notifications,
|
||||
autoBackup: this.data.autoBackup
|
||||
};
|
||||
wx.setStorageSync('pure_todo_settings', JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载统计信息
|
||||
loadStats() {
|
||||
const stats = todoStorage.getStats();
|
||||
this.setData({ stats });
|
||||
},
|
||||
|
||||
// 切换深色模式
|
||||
onDarkModeChange(e: any) {
|
||||
this.setData({ darkMode: e.detail.value }, () => {
|
||||
this.saveSettings();
|
||||
wx.showToast({
|
||||
title: e.detail.value ? '已开启深色模式' : '已关闭深色模式',
|
||||
icon: 'success'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 切换通知
|
||||
onNotificationsChange(e: any) {
|
||||
this.setData({ notifications: e.detail.value }, () => {
|
||||
this.saveSettings();
|
||||
wx.showToast({
|
||||
title: e.detail.value ? '已开启通知' : '已关闭通知',
|
||||
icon: 'success'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 切换自动备份
|
||||
onAutoBackupChange(e: any) {
|
||||
this.setData({ autoBackup: e.detail.value }, () => {
|
||||
this.saveSettings();
|
||||
wx.showToast({
|
||||
title: e.detail.value ? '已开启自动备份' : '已关闭自动备份',
|
||||
icon: 'success'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 导出数据
|
||||
exportData() {
|
||||
const data = todoStorage.exportData();
|
||||
this.setData({
|
||||
backupData: data,
|
||||
showBackupDialog: true
|
||||
});
|
||||
},
|
||||
|
||||
// 导入数据
|
||||
importData() {
|
||||
wx.showModal({
|
||||
title: '导入数据',
|
||||
content: '请将备份数据粘贴到输入框中',
|
||||
editable: true,
|
||||
placeholderText: '粘贴备份数据...',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const success = todoStorage.importData(res.content);
|
||||
if (success) {
|
||||
this.loadStats();
|
||||
wx.showToast({
|
||||
title: '数据导入成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '数据格式错误',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 清空数据
|
||||
clearData() {
|
||||
wx.showModal({
|
||||
title: '确认清空',
|
||||
content: '确定要清空所有数据吗?此操作不可恢复!',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
todoStorage.clearAllData();
|
||||
this.loadStats();
|
||||
wx.showToast({
|
||||
title: '数据已清空',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 重置设置
|
||||
resetSettings() {
|
||||
wx.showModal({
|
||||
title: '确认重置',
|
||||
content: '确定要重置所有设置吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({
|
||||
darkMode: false,
|
||||
notifications: true,
|
||||
autoBackup: true
|
||||
}, () => {
|
||||
this.saveSettings();
|
||||
wx.showToast({
|
||||
title: '设置已重置',
|
||||
icon: 'success'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 显示关于对话框
|
||||
showAbout() {
|
||||
this.setData({ showAboutDialog: true });
|
||||
},
|
||||
|
||||
// 隐藏关于对话框
|
||||
hideAbout() {
|
||||
this.setData({ showAboutDialog: false });
|
||||
},
|
||||
|
||||
// 隐藏备份对话框
|
||||
hideBackup() {
|
||||
this.setData({ showBackupDialog: false });
|
||||
},
|
||||
|
||||
// 复制备份数据
|
||||
copyBackupData() {
|
||||
wx.setClipboardData({
|
||||
data: this.data.backupData,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '备份数据已复制',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 标签页切换
|
||||
onTabChange(e: any) {
|
||||
const targetPage = e.detail.value;
|
||||
if (targetPage === 'home') {
|
||||
wx.switchTab({ url: '/pages/index/index' });
|
||||
} else if (targetPage === 'list') {
|
||||
wx.switchTab({ url: '/pages/list/list' });
|
||||
} else if (targetPage === 'statistics') {
|
||||
wx.switchTab({ url: '/pages/statistics/statistics' });
|
||||
}
|
||||
// No need to navigate if already on 'settings'
|
||||
},
|
||||
},
|
||||
|
||||
// 联系开发者
|
||||
contactDeveloper() {
|
||||
wx.showModal({
|
||||
title: '联系开发者',
|
||||
content: '如有问题或建议,请通过以下方式联系:\n\n邮箱:developer@example.com\n微信:developer_wechat',
|
||||
showCancel: false
|
||||
});
|
||||
},
|
||||
|
||||
// 检查更新
|
||||
checkUpdate() {
|
||||
wx.showToast({
|
||||
title: '已是最新版本',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
// 给应用评分
|
||||
rateApp() {
|
||||
wx.showModal({
|
||||
title: '给应用评分',
|
||||
content: '感谢您的使用!请在应用商店给Pure Todo一个好评吧!',
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1,13 +1,243 @@
|
||||
<!--pages/settings/settings.wxml-->
|
||||
<view class="container">
|
||||
<t-navbar title="设置" />
|
||||
<view class="content">
|
||||
<t-icon name="setting" size="xl" />
|
||||
<text>设置页面,敬请期待!</text>
|
||||
<!-- 顶部标题 -->
|
||||
<view class="header-section">
|
||||
<text class="header-title">设置</text>
|
||||
<text class="header-subtitle">个性化你的使用体验</text>
|
||||
</view>
|
||||
|
||||
<!-- 应用信息 -->
|
||||
<view class="app-info-section">
|
||||
<view class="app-info-card">
|
||||
<view class="app-info">
|
||||
<view class="app-icon">
|
||||
<t-icon name="check-circle" size="24" color="#0052d9" />
|
||||
</view>
|
||||
<view class="app-details">
|
||||
<text class="app-name">Pure Todo</text>
|
||||
<text class="app-version">版本 {{appVersion}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="app-stats">
|
||||
<text class="stat-item">总任务: {{stats.total || 0}}</text>
|
||||
<text class="stat-item">已完成: {{stats.completed || 0}}</text>
|
||||
<text class="stat-item">完成率: {{stats.completionRate || 0}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 外观设置 -->
|
||||
<view class="settings-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">外观</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="theme" size="16" color="#0052d9" />
|
||||
<text class="setting-label">深色模式</text>
|
||||
</view>
|
||||
<t-switch
|
||||
value="{{darkMode}}"
|
||||
bind:change="onDarkModeChange"
|
||||
color="#0052d9"
|
||||
/>
|
||||
</view>
|
||||
<text class="setting-desc">切换深色主题,保护眼睛</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 通知设置 -->
|
||||
<view class="settings-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">通知</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="notification" size="16" color="#0052d9" />
|
||||
<text class="setting-label">推送通知</text>
|
||||
</view>
|
||||
<t-switch
|
||||
value="{{notifications}}"
|
||||
bind:change="onNotificationsChange"
|
||||
color="#0052d9"
|
||||
/>
|
||||
</view>
|
||||
<text class="setting-desc">接收任务提醒和完成通知</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据管理 -->
|
||||
<view class="settings-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">数据管理</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="cloud" size="16" color="#0052d9" />
|
||||
<text class="setting-label">自动备份</text>
|
||||
</view>
|
||||
<t-switch
|
||||
value="{{autoBackup}}"
|
||||
bind:change="onAutoBackupChange"
|
||||
color="#0052d9"
|
||||
/>
|
||||
</view>
|
||||
<text class="setting-desc">定期自动备份数据到云端</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="exportData">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="download" size="16" color="#0052d9" />
|
||||
<text class="setting-label">导出数据</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">将数据导出为备份文件</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="importData">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="upload" size="16" color="#0052d9" />
|
||||
<text class="setting-label">导入数据</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">从备份文件恢复数据</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="clearData">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="delete" size="16" color="#e34d59" />
|
||||
<text class="setting-label danger">清空数据</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">删除所有任务和设置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 应用设置 -->
|
||||
<view class="settings-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">应用</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item" bind:tap="resetSettings">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="refresh" size="16" color="#0052d9" />
|
||||
<text class="setting-label">重置设置</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">恢复所有设置为默认值</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="checkUpdate">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="update" size="16" color="#0052d9" />
|
||||
<text class="setting-label">检查更新</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">检查是否有新版本可用</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="rateApp">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="star" size="16" color="#0052d9" />
|
||||
<text class="setting-label">给应用评分</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">在应用商店给我们好评</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="showAbout">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="info-circle" size="16" color="#0052d9" />
|
||||
<text class="setting-label">关于应用</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">查看应用信息和开发者联系方式</text>
|
||||
</view>
|
||||
|
||||
<view class="setting-item" bind:tap="contactDeveloper">
|
||||
<view class="setting-content">
|
||||
<view class="setting-info">
|
||||
<t-icon name="service" size="16" color="#0052d9" />
|
||||
<text class="setting-label">联系开发者</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="14" color="#ccc" />
|
||||
</view>
|
||||
<text class="setting-desc">反馈问题或建议新功能</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关于对话框 -->
|
||||
<t-dialog
|
||||
visible="{{showAboutDialog}}"
|
||||
title="关于 Pure Todo"
|
||||
confirm-btn="确定"
|
||||
bind:confirm="hideAbout"
|
||||
>
|
||||
<view class="about-content">
|
||||
<view class="about-header">
|
||||
<t-icon name="check-circle" size="32" color="#0052d9" />
|
||||
<text class="about-title">Pure Todo</text>
|
||||
<text class="about-version">版本 {{appVersion}}</text>
|
||||
</view>
|
||||
<text class="about-desc">
|
||||
一个简洁高效的待办事项管理应用,帮助你更好地管理时间和任务。
|
||||
</text>
|
||||
<view class="about-features">
|
||||
<text class="feature-item">• 简洁直观的界面设计</text>
|
||||
<text class="feature-item">• 智能的任务分类和筛选</text>
|
||||
<text class="feature-item">• 详细的数据统计和分析</text>
|
||||
<text class="feature-item">• 安全的数据备份和恢复</text>
|
||||
</view>
|
||||
</view>
|
||||
</t-dialog>
|
||||
|
||||
<!-- 备份数据对话框 -->
|
||||
<t-dialog
|
||||
visible="{{showBackupDialog}}"
|
||||
title="备份数据"
|
||||
confirm-btn="复制"
|
||||
cancel-btn="关闭"
|
||||
bind:confirm="copyBackupData"
|
||||
bind:cancel="hideBackup"
|
||||
>
|
||||
<view class="backup-content">
|
||||
<text class="backup-desc">以下是你的备份数据,请妥善保存:</text>
|
||||
<view class="backup-data">
|
||||
<text class="backup-text">{{backupData}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</t-dialog>
|
||||
|
||||
<!-- 自定义标签栏 -->
|
||||
<t-tab-bar value="{{activeTab}}" bind:change="onTabChange" t-class="custom-tab-bar">
|
||||
<t-tab-bar-item value="home" icon="home" aria-label="首页">首页</t-tab-bar-item>
|
||||
<t-tab-bar-item value="list" icon="bulletpoint" aria-label="任务">任务</t-tab-bar-item>
|
||||
<t-tab-bar-item value="statistics" icon="chart" aria-label="统计">统计</t-tab-bar-item>
|
||||
<t-tab-bar-item value="settings" icon="setting" aria-label="设置">设置</t-tab-bar-item>
|
||||
</t-tab-bar>
|
||||
</view>
|
||||
|
||||
@ -1,39 +1,249 @@
|
||||
/* pages/settings/settings.wxss */
|
||||
page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding-bottom: 120rpx; /* Space for tab bar */
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
/* 确保顶部有足够的安全区 */
|
||||
padding-top: max(env(safe-area-inset-top), 16px);
|
||||
/* 确保底部有足够的安全区 */
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 80px);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
/* 顶部标题 */
|
||||
.header-section {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.content .t-icon {
|
||||
margin-bottom: 20rpx;
|
||||
color: #0052d9;
|
||||
.header-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.custom-tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
.header-subtitle {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 应用信息 */
|
||||
.app-info-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app-info-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.app-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.app-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.app-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 设置区域 */
|
||||
.settings-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.setting-item:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.setting-label.danger {
|
||||
color: #e34d59;
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
.about-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.about-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 8px 0 2px;
|
||||
}
|
||||
|
||||
.about-version {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.about-desc {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.backup-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.backup-desc {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.backup-data {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.backup-text {
|
||||
font-size: 10px;
|
||||
color: #333;
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.app-stats {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
miniprogram/pages/statistics/statistics.json
Normal file
11
miniprogram/pages/statistics/statistics.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
|
||||
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-progress": "tdesign-miniprogram/progress/progress"
|
||||
}
|
||||
}
|
||||
237
miniprogram/pages/statistics/statistics.ts
Normal file
237
miniprogram/pages/statistics/statistics.ts
Normal file
@ -0,0 +1,237 @@
|
||||
// pages/statistics/statistics.ts
|
||||
import { todoStorage, ITodo, ITodoStats } from '../../utils/todoStorage';
|
||||
|
||||
Component({
|
||||
data: {
|
||||
activeTab: 'statistics',
|
||||
stats: {} as ITodoStats,
|
||||
todos: [] as ITodo[],
|
||||
weeklyData: [] as any[],
|
||||
priorityData: [] as any[],
|
||||
categoryData: [] as any[],
|
||||
recentActivity: [] as any[]
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const stats = todoStorage.getStats();
|
||||
const todos = todoStorage.getAllTodos();
|
||||
|
||||
this.setData({ stats, todos }, () => {
|
||||
this.calculateWeeklyData();
|
||||
this.calculatePriorityData();
|
||||
this.calculateCategoryData();
|
||||
this.calculateRecentActivity();
|
||||
});
|
||||
},
|
||||
|
||||
// 计算周数据
|
||||
calculateWeeklyData() {
|
||||
const weekData = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
||||
const nextDate = new Date(date);
|
||||
nextDate.setDate(nextDate.getDate() + 1);
|
||||
|
||||
const dayTodos = this.data.todos.filter(todo => {
|
||||
const todoDate = new Date(todo.createdAt);
|
||||
todoDate.setHours(0, 0, 0, 0);
|
||||
return todoDate.getTime() === date.getTime();
|
||||
});
|
||||
|
||||
const completedTodos = dayTodos.filter(todo => todo.completed);
|
||||
|
||||
weekData.push({
|
||||
date: date.toLocaleDateString('zh-CN', { weekday: 'short' }),
|
||||
created: dayTodos.length,
|
||||
completed: completedTodos.length,
|
||||
dateObj: date
|
||||
});
|
||||
}
|
||||
|
||||
this.setData({ weeklyData: weekData });
|
||||
},
|
||||
|
||||
// 计算优先级数据
|
||||
calculatePriorityData() {
|
||||
const priorityCount = { high: 0, medium: 0, low: 0 };
|
||||
const priorityCompleted = { high: 0, medium: 0, low: 0 };
|
||||
|
||||
this.data.todos.forEach(todo => {
|
||||
priorityCount[todo.priority]++;
|
||||
if (todo.completed) {
|
||||
priorityCompleted[todo.priority]++;
|
||||
}
|
||||
});
|
||||
|
||||
const priorityData = [
|
||||
{
|
||||
name: '高优先级',
|
||||
total: priorityCount.high,
|
||||
completed: priorityCompleted.high,
|
||||
rate: priorityCount.high > 0 ? Math.round((priorityCompleted.high / priorityCount.high) * 100) : 0,
|
||||
color: '#ff6b6b'
|
||||
},
|
||||
{
|
||||
name: '中优先级',
|
||||
total: priorityCount.medium,
|
||||
completed: priorityCompleted.medium,
|
||||
rate: priorityCount.medium > 0 ? Math.round((priorityCompleted.medium / priorityCount.medium) * 100) : 0,
|
||||
color: '#feca57'
|
||||
},
|
||||
{
|
||||
name: '低优先级',
|
||||
total: priorityCount.low,
|
||||
completed: priorityCompleted.low,
|
||||
rate: priorityCount.low > 0 ? Math.round((priorityCompleted.low / priorityCount.low) * 100) : 0,
|
||||
color: '#48dbfb'
|
||||
}
|
||||
];
|
||||
|
||||
this.setData({ priorityData });
|
||||
},
|
||||
|
||||
// 计算分类数据
|
||||
calculateCategoryData() {
|
||||
const categoryMap = new Map();
|
||||
|
||||
this.data.todos.forEach(todo => {
|
||||
const category = todo.category || '未分类';
|
||||
if (!categoryMap.has(category)) {
|
||||
categoryMap.set(category, { total: 0, completed: 0 });
|
||||
}
|
||||
categoryMap.get(category).total++;
|
||||
if (todo.completed) {
|
||||
categoryMap.get(category).completed++;
|
||||
}
|
||||
});
|
||||
|
||||
const categoryData = Array.from(categoryMap.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
total: data.total,
|
||||
completed: data.completed,
|
||||
rate: data.total > 0 ? Math.round((data.completed / data.total) * 100) : 0
|
||||
}));
|
||||
|
||||
this.setData({ categoryData });
|
||||
},
|
||||
|
||||
// 计算最近活动
|
||||
calculateRecentActivity() {
|
||||
const recentTodos = this.data.todos
|
||||
.sort((a, b) => b.createdAt - a.createdAt)
|
||||
.slice(0, 10);
|
||||
|
||||
const recentActivity = recentTodos.map(todo => ({
|
||||
id: todo.id,
|
||||
text: todo.text,
|
||||
completed: todo.completed,
|
||||
priority: todo.priority,
|
||||
createdAt: todo.createdAt,
|
||||
completedAt: todo.completedAt,
|
||||
timeAgo: this.getTimeAgo(todo.createdAt)
|
||||
}));
|
||||
|
||||
this.setData({ recentActivity });
|
||||
},
|
||||
|
||||
// 获取时间差
|
||||
getTimeAgo(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (minutes < 60) {
|
||||
return `${minutes}分钟前`;
|
||||
} else if (hours < 24) {
|
||||
return `${hours}小时前`;
|
||||
} else {
|
||||
return `${days}天前`;
|
||||
}
|
||||
},
|
||||
|
||||
// 标签页切换
|
||||
onTabChange(e: any) {
|
||||
const targetPage = e.detail.value;
|
||||
if (targetPage === 'home') {
|
||||
wx.switchTab({ url: '/pages/index/index' });
|
||||
} else if (targetPage === 'list') {
|
||||
wx.switchTab({ url: '/pages/list/list' });
|
||||
} else if (targetPage === 'settings') {
|
||||
wx.switchTab({ url: '/pages/settings/settings' });
|
||||
}
|
||||
},
|
||||
|
||||
// 导出数据
|
||||
exportData() {
|
||||
const data = todoStorage.exportData();
|
||||
wx.setClipboardData({
|
||||
data: data,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '数据已复制到剪贴板',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 清空数据
|
||||
clearData() {
|
||||
wx.showModal({
|
||||
title: '确认清空',
|
||||
content: '确定要清空所有数据吗?此操作不可恢复!',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
todoStorage.clearAllData();
|
||||
this.loadData();
|
||||
wx.showToast({
|
||||
title: '数据已清空',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 获取优先级颜色
|
||||
getPriorityColor(priority: string): string {
|
||||
switch (priority) {
|
||||
case 'high': return '#ff6b6b';
|
||||
case 'medium': return '#feca57';
|
||||
case 'low': return '#48dbfb';
|
||||
default: return '#667eea';
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化百分比
|
||||
formatPercentage(value: number): string {
|
||||
return `${value}%`;
|
||||
},
|
||||
|
||||
// 获取完成状态文本
|
||||
getCompletionText(completed: boolean): string {
|
||||
return completed ? '已完成' : '进行中';
|
||||
}
|
||||
}
|
||||
});
|
||||
175
miniprogram/pages/statistics/statistics.wxml
Normal file
175
miniprogram/pages/statistics/statistics.wxml
Normal file
@ -0,0 +1,175 @@
|
||||
<!--pages/statistics/statistics.wxml-->
|
||||
<view class="container">
|
||||
<!-- 顶部标题 -->
|
||||
<view class="header-section">
|
||||
<text class="header-title">数据统计</text>
|
||||
<text class="header-subtitle">了解你的任务完成情况</text>
|
||||
</view>
|
||||
|
||||
<!-- 总体统计 -->
|
||||
<view class="overview-section">
|
||||
<view class="overview-card">
|
||||
<view class="overview-header">
|
||||
<text class="overview-title">总体概览</text>
|
||||
</view>
|
||||
<view class="overview-grid">
|
||||
<view class="overview-item">
|
||||
<text class="overview-number">{{stats.total}}</text>
|
||||
<text class="overview-label">总任务</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-number">{{stats.completed}}</text>
|
||||
<text class="overview-label">已完成</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-number">{{stats.pending}}</text>
|
||||
<text class="overview-label">待完成</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-number">{{stats.completionRate}}%</text>
|
||||
<text class="overview-label">完成率</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="progress-container">
|
||||
<t-progress
|
||||
percentage="{{stats.completionRate}}"
|
||||
color="#0052d9"
|
||||
stroke-width="4"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 周数据统计 -->
|
||||
<view class="weekly-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">本周数据</text>
|
||||
</view>
|
||||
<view class="weekly-card">
|
||||
<view class="weekly-chart">
|
||||
<view wx:for="{{weeklyData}}" wx:key="date" class="chart-bar">
|
||||
<view class="bar-container">
|
||||
<view class="bar created" style="height: {{item.created * 8}}px;"></view>
|
||||
<view class="bar completed" style="height: {{item.completed * 8}}px;"></view>
|
||||
</view>
|
||||
<text class="chart-label">{{item.date}}</text>
|
||||
<text class="chart-value">{{item.created}}/{{item.completed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优先级统计 -->
|
||||
<view class="priority-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">优先级统计</text>
|
||||
</view>
|
||||
<view class="priority-list">
|
||||
<view wx:for="{{priorityData}}" wx:key="name" class="priority-item">
|
||||
<view class="priority-info">
|
||||
<view class="priority-header">
|
||||
<text class="priority-name">{{item.name}}</text>
|
||||
<text class="priority-rate">{{formatPercentage(item.rate)}}</text>
|
||||
</view>
|
||||
<view class="priority-stats">
|
||||
<text class="priority-count">已完成: {{item.completed}} / {{item.total}}</text>
|
||||
</view>
|
||||
<view class="progress-container">
|
||||
<t-progress
|
||||
percentage="{{item.rate}}"
|
||||
color="{{item.color}}"
|
||||
stroke-width="3"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类统计 -->
|
||||
<view class="category-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">分类统计</text>
|
||||
</view>
|
||||
<view class="category-list">
|
||||
<view wx:for="{{categoryData}}" wx:key="name" class="category-item">
|
||||
<view class="category-info">
|
||||
<text class="category-name">{{item.name}}</text>
|
||||
<text class="category-count">{{item.total}} 个任务</text>
|
||||
</view>
|
||||
<view class="category-stats">
|
||||
<text class="category-completed">已完成: {{item.completed}}</text>
|
||||
<text class="category-rate">{{formatPercentage(item.rate)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<view class="activity-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近活动</text>
|
||||
</view>
|
||||
<view class="activity-list">
|
||||
<view wx:for="{{recentActivity}}" wx:key="id" class="activity-item">
|
||||
<view class="activity-content">
|
||||
<view class="activity-info">
|
||||
<text class="activity-text {{item.completed ? 'completed' : ''}}">{{item.text}}</text>
|
||||
<text class="activity-time">{{item.timeAgo}}</text>
|
||||
</view>
|
||||
<view class="activity-status">
|
||||
<t-tag
|
||||
theme="{{item.priority === 'high' ? 'danger' : item.priority === 'medium' ? 'warning' : 'primary'}}"
|
||||
size="small"
|
||||
>
|
||||
{{item.priority === 'high' ? '高' : item.priority === 'medium' ? '中' : '低'}}
|
||||
</t-tag>
|
||||
<t-tag
|
||||
theme="{{item.completed ? 'success' : 'default'}}"
|
||||
size="small"
|
||||
>
|
||||
{{getCompletionText(item.completed)}}
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据管理 -->
|
||||
<view class="data-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">数据管理</text>
|
||||
</view>
|
||||
<view class="data-actions">
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="large"
|
||||
block
|
||||
bind:tap="exportData"
|
||||
class="action-btn"
|
||||
>
|
||||
<t-icon name="download" slot="icon" />
|
||||
导出数据
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="danger"
|
||||
size="large"
|
||||
block
|
||||
bind:tap="clearData"
|
||||
class="action-btn"
|
||||
>
|
||||
<t-icon name="delete" slot="icon" />
|
||||
清空数据
|
||||
</t-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义标签栏 -->
|
||||
<t-tab-bar value="{{activeTab}}" bind:change="onTabChange" t-class="custom-tab-bar">
|
||||
<t-tab-bar-item value="home" icon="home" aria-label="首页">首页</t-tab-bar-item>
|
||||
<t-tab-bar-item value="list" icon="bulletpoint" aria-label="任务">任务</t-tab-bar-item>
|
||||
<t-tab-bar-item value="statistics" icon="chart" aria-label="统计">统计</t-tab-bar-item>
|
||||
<t-tab-bar-item value="settings" icon="setting" aria-label="设置">设置</t-tab-bar-item>
|
||||
</t-tab-bar>
|
||||
</view>
|
||||
361
miniprogram/pages/statistics/statistics.wxss
Normal file
361
miniprogram/pages/statistics/statistics.wxss
Normal file
@ -0,0 +1,361 @@
|
||||
/* pages/statistics/statistics.wxss */
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
/* 确保顶部有足够的安全区 */
|
||||
padding-top: max(env(safe-area-inset-top), 16px);
|
||||
/* 确保底部有足够的安全区 */
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 80px);
|
||||
}
|
||||
|
||||
/* 顶部标题 */
|
||||
.header-section {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 总体统计 */
|
||||
.overview-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.overview-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.overview-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.overview-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview-number {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #0052d9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.overview-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 周数据统计 */
|
||||
.weekly-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.weekly-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.weekly-chart {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
height: 120px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 16px;
|
||||
border-radius: 1px;
|
||||
min-height: 2px;
|
||||
}
|
||||
|
||||
.bar.created {
|
||||
background: #0052d9;
|
||||
}
|
||||
|
||||
.bar.completed {
|
||||
background: #00a870;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.chart-value {
|
||||
font-size: 8px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 优先级统计 */
|
||||
.priority-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.priority-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.priority-item {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.priority-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.priority-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.priority-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.priority-rate {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.priority-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.priority-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 分类统计 */
|
||||
.category-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-completed {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.category-rate {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
/* 最近活动 */
|
||||
.activity-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.activity-text.completed {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.activity-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 数据管理 */
|
||||
.data-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.data-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.weekly-chart {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
219
miniprogram/utils/todoStorage.ts
Normal file
219
miniprogram/utils/todoStorage.ts
Normal file
@ -0,0 +1,219 @@
|
||||
// utils/todoStorage.ts
|
||||
|
||||
export interface ITodo {
|
||||
id: string;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
createdAt: number;
|
||||
completedAt?: number;
|
||||
category?: string;
|
||||
deadline?: number;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface ITodoStats {
|
||||
total: number;
|
||||
completed: number;
|
||||
pending: number;
|
||||
completionRate: number;
|
||||
todayCompleted: number;
|
||||
}
|
||||
|
||||
class TodoStorage {
|
||||
private readonly STORAGE_KEY = 'pure_todo_data';
|
||||
private readonly STATS_KEY = 'pure_todo_stats';
|
||||
|
||||
// 获取所有待办事项
|
||||
getAllTodos(): ITodo[] {
|
||||
try {
|
||||
const data = wx.getStorageSync(this.STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch (error) {
|
||||
console.error('获取待办事项失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 保存所有待办事项
|
||||
saveAllTodos(todos: ITodo[]): void {
|
||||
try {
|
||||
wx.setStorageSync(this.STORAGE_KEY, JSON.stringify(todos));
|
||||
this.updateStats(todos);
|
||||
} catch (error) {
|
||||
console.error('保存待办事项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新待办事项
|
||||
addTodo(
|
||||
text: string,
|
||||
priority: 'high' | 'medium' | 'low' = 'medium',
|
||||
category?: string,
|
||||
deadline?: number,
|
||||
note?: string
|
||||
): ITodo {
|
||||
const todos = this.getAllTodos();
|
||||
const newTodo: ITodo = {
|
||||
id: this.generateId(),
|
||||
text: text.trim(),
|
||||
completed: false,
|
||||
priority,
|
||||
createdAt: Date.now(),
|
||||
category,
|
||||
deadline,
|
||||
note
|
||||
};
|
||||
|
||||
todos.unshift(newTodo);
|
||||
this.saveAllTodos(todos);
|
||||
return newTodo;
|
||||
}
|
||||
|
||||
// 更新待办事项
|
||||
updateTodo(id: string, updates: Partial<ITodo>): boolean {
|
||||
const todos = this.getAllTodos();
|
||||
const index = todos.findIndex(todo => todo.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
todos[index] = { ...todos[index], ...updates };
|
||||
if (updates.completed !== undefined) {
|
||||
todos[index].completedAt = updates.completed ? Date.now() : undefined;
|
||||
}
|
||||
this.saveAllTodos(todos);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除待办事项
|
||||
deleteTodo(id: string): boolean {
|
||||
const todos = this.getAllTodos();
|
||||
const filteredTodos = todos.filter(todo => todo.id !== id);
|
||||
|
||||
if (filteredTodos.length !== todos.length) {
|
||||
this.saveAllTodos(filteredTodos);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 切换完成状态
|
||||
toggleTodo(id: string): boolean {
|
||||
const todos = this.getAllTodos();
|
||||
const todo = todos.find(t => t.id === id);
|
||||
|
||||
if (todo) {
|
||||
return this.updateTodo(id, {
|
||||
completed: !todo.completed,
|
||||
completedAt: !todo.completed ? Date.now() : undefined
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
getStats(): ITodoStats {
|
||||
try {
|
||||
const data = wx.getStorageSync(this.STATS_KEY);
|
||||
return data ? JSON.parse(data) : this.calculateStats();
|
||||
} catch (error) {
|
||||
return this.calculateStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 计算统计信息
|
||||
private calculateStats(): ITodoStats {
|
||||
const todos = this.getAllTodos();
|
||||
const total = todos.length;
|
||||
const completed = todos.filter(todo => todo.completed).length;
|
||||
const pending = total - completed;
|
||||
const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
|
||||
// 计算今日完成的任务
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayCompleted = todos.filter(todo =>
|
||||
todo.completed && todo.completedAt && todo.completedAt >= today.getTime()
|
||||
).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
completed,
|
||||
pending,
|
||||
completionRate,
|
||||
todayCompleted
|
||||
};
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
private updateStats(todos: ITodo[]): void {
|
||||
const stats = this.calculateStats();
|
||||
try {
|
||||
wx.setStorageSync(this.STATS_KEY, JSON.stringify(stats));
|
||||
} catch (error) {
|
||||
console.error('更新统计信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
private generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
// 按优先级获取待办事项
|
||||
getTodosByPriority(priority?: 'high' | 'medium' | 'low'): ITodo[] {
|
||||
const todos = this.getAllTodos();
|
||||
if (priority) {
|
||||
return todos.filter(todo => todo.priority === priority);
|
||||
}
|
||||
return todos;
|
||||
}
|
||||
|
||||
// 获取今日待办事项
|
||||
getTodayTodos(): ITodo[] {
|
||||
const todos = this.getAllTodos();
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
return todos.filter(todo => {
|
||||
const todoDate = new Date(todo.createdAt);
|
||||
todoDate.setHours(0, 0, 0, 0);
|
||||
return todoDate.getTime() === today.getTime();
|
||||
});
|
||||
}
|
||||
|
||||
// 清空所有数据
|
||||
clearAllData(): void {
|
||||
try {
|
||||
wx.removeStorageSync(this.STORAGE_KEY);
|
||||
wx.removeStorageSync(this.STATS_KEY);
|
||||
} catch (error) {
|
||||
console.error('清空数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
exportData(): string {
|
||||
const todos = this.getAllTodos();
|
||||
const stats = this.getStats();
|
||||
return JSON.stringify({ todos, stats, exportTime: Date.now() });
|
||||
}
|
||||
|
||||
// 导入数据
|
||||
importData(dataString: string): boolean {
|
||||
try {
|
||||
const data = JSON.parse(dataString);
|
||||
if (data.todos && Array.isArray(data.todos)) {
|
||||
this.saveAllTodos(data.todos);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('导入数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const todoStorage = new TodoStorage();
|
||||
@ -3,5 +3,6 @@
|
||||
"projectname": "miniprogram-1",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
},
|
||||
"libVersion": "3.9.2"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user