首次提交
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
const api = require('../../../services/api')
|
||||
Page({
|
||||
data: { name: '', category: 'study', cycleType: 'daily', dailyGoal: '', privacy: 2, submitting: false },
|
||||
onNameInput(e) { this.setData({ name: e.detail.value }) },
|
||||
onGoalInput(e) { this.setData({ dailyGoal: e.detail.value }) },
|
||||
setCategory(e) { this.setData({ category: e.currentTarget.dataset.cat }) },
|
||||
setCycle(e) { this.setData({ cycleType: e.currentTarget.dataset.cycle }) },
|
||||
setPrivacy(e) { this.setData({ privacy: parseInt(e.currentTarget.dataset.val) }) },
|
||||
async onSubmit() {
|
||||
if (!this.data.name.trim()) { wx.showToast({ title: '请输入名称', icon: 'none' }); return }
|
||||
this.setData({ submitting: true })
|
||||
try {
|
||||
await api.post('/api/checkin/items', {
|
||||
name: this.data.name.trim(), category: this.data.category,
|
||||
cycleType: this.data.cycleType, dailyGoal: this.data.dailyGoal || undefined, privacy: this.data.privacy
|
||||
})
|
||||
wx.showToast({ title: '创建成功' })
|
||||
wx.navigateBack()
|
||||
} catch(e) { wx.showToast({ title: e.message || '创建失败', icon: 'none' }) }
|
||||
this.setData({ submitting: false })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
{ "navigationBarTitleText": "创建打卡项", "usingComponents": {} }
|
||||
@@ -0,0 +1,32 @@
|
||||
<view class="form">
|
||||
<text class="label">名称 *</text>
|
||||
<input class="input-field" value="{{name}}" bindinput="onNameInput" placeholder="如「每天背单词」" style="margin-bottom:20px" />
|
||||
|
||||
<text class="label">分类</text>
|
||||
<view class="category-grid">
|
||||
<view class="cat-item {{category==='study'?'active':''}}" data-cat="study" bindtap="setCategory">📚 学习</view>
|
||||
<view class="cat-item {{category==='health'?'active':''}}" data-cat="health" bindtap="setCategory">❤️ 健康</view>
|
||||
<view class="cat-item {{category==='habit'?'active':''}}" data-cat="habit" bindtap="setCategory">🔄 习惯</view>
|
||||
<view class="cat-item {{category==='hobby'?'active':''}}" data-cat="hobby" bindtap="setCategory">⭐ 兴趣</view>
|
||||
<view class="cat-item {{category==='goal'?'active':''}}" data-cat="goal" bindtap="setCategory">🎯 目标</view>
|
||||
</view>
|
||||
|
||||
<text class="label" style="margin-top:20px">打卡周期</text>
|
||||
<view class="cycle-row">
|
||||
<view class="cycle-item {{cycleType==='daily'?'active':''}}" data-cycle="daily" bindtap="setCycle">每日</view>
|
||||
<view class="cycle-item {{cycleType==='weekly'?'active':''}}" data-cycle="weekly" bindtap="setCycle">每周</view>
|
||||
<view class="cycle-item {{cycleType==='every_other_day'?'active':''}}" data-cycle="every_other_day" bindtap="setCycle">隔日</view>
|
||||
</view>
|
||||
|
||||
<text class="label" style="margin-top:20px">每日目标(选填)</text>
|
||||
<input class="input-field" value="{{dailyGoal}}" bindinput="onGoalInput" placeholder="如「背 50 个单词」" style="margin-bottom:20px" />
|
||||
|
||||
<text class="label">隐私设置</text>
|
||||
<view class="privacy-row">
|
||||
<view class="privacy-item {{privacy===1?'active':''}}" data-val="1" bindtap="setPrivacy">公开</view>
|
||||
<view class="privacy-item {{privacy===2?'active':''}}" data-val="2" bindtap="setPrivacy">好友可见</view>
|
||||
<view class="privacy-item {{privacy===3?'active':''}}" data-val="3" bindtap="setPrivacy">私密</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary" style="margin-top:32px" loading="{{submitting}}" disabled="{{submitting}}" bindtap="onSubmit">创建打卡项</button>
|
||||
</view>
|
||||
@@ -0,0 +1,11 @@
|
||||
.form { padding: 16px; }
|
||||
.label { font-size: 13px; color: #86868B; margin-bottom: 6px; display: block; }
|
||||
.category-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||||
.cat-item { padding: 12px 8px; border-radius: 10px; border: 2px solid #E5E5EA; background: #f8f8fa; text-align: center; font-size: 14px; }
|
||||
.cat-item.active { border-color: #FF6B35; background: #FFF0EB; font-weight: 600; }
|
||||
.cycle-row { display: flex; gap: 8px; }
|
||||
.cycle-item { padding: 8px 16px; border-radius: 8px; border: 1.5px solid #E5E5EA; background: #f8f8fa; font-size: 14px; }
|
||||
.cycle-item.active { border-color: #FF6B35; background: #FFF0EB; font-weight: 600; }
|
||||
.privacy-row { display: flex; gap: 8px; }
|
||||
.privacy-item { padding: 8px 16px; border-radius: 8px; border: 1.5px solid #E5E5EA; background: #f8f8fa; font-size: 14px; }
|
||||
.privacy-item.active { border-color: #FF6B35; background: #FFF0EB; font-weight: 600; }
|
||||
@@ -0,0 +1,55 @@
|
||||
const api = require('../../../services/api')
|
||||
Page({
|
||||
data: { item: null, streak: 0, records: [], year: 0, month: 0, days: [], loading: true, showActions: false },
|
||||
onLoad(options) {
|
||||
const now = new Date()
|
||||
this.setData({ id: options.id, year: now.getFullYear(), month: now.getMonth() + 1 })
|
||||
this.loadData()
|
||||
},
|
||||
async loadData() {
|
||||
const { id, year, month } = this.data
|
||||
try {
|
||||
const [item, streak, records] = await Promise.all([
|
||||
api.get('/api/checkin/items/' + id),
|
||||
api.get('/api/checkin/streak/' + id),
|
||||
api.get('/api/checkin/records?itemId=' + id + '&start=' + year + '-' + String(month).padStart(2,'0') + '-01&end=' + year + '-' + String(month).padStart(2,'0') + '-31'),
|
||||
])
|
||||
const days = this.buildCalendar(year, month, records.map(r => r.checkDate.slice(0,10)))
|
||||
this.setData({ item, streak: streak.streak, records, days, loading: false })
|
||||
} catch(e) { wx.showToast({ title: '加载失败', icon: 'none' }); this.setData({ loading: false }) }
|
||||
},
|
||||
buildCalendar(y, m, recordDates) {
|
||||
const daysInMonth = new Date(y, m, 0).getDate()
|
||||
const firstDay = new Date(y, m - 1, 1).getDay()
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const days = []
|
||||
for (let i = 0; i < firstDay; i++) days.push({ empty: true })
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateStr = y + '-' + String(m).padStart(2,'0') + '-' + String(d).padStart(2,'0')
|
||||
days.push({ day: d, checked: recordDates.includes(dateStr), isToday: dateStr === today, dateStr })
|
||||
}
|
||||
return days
|
||||
},
|
||||
prevMonth() {
|
||||
let { year, month } = this.data
|
||||
if (month === 1) { year--; month = 12 } else month--
|
||||
this.setData({ year, month, loading: true }); this.loadData()
|
||||
},
|
||||
nextMonth() {
|
||||
let { year, month } = this.data
|
||||
if (month === 12) { year++; month = 1 } else month++
|
||||
this.setData({ year, month, loading: true }); this.loadData()
|
||||
},
|
||||
toggleActions() { this.setData({ showActions: !this.data.showActions }) },
|
||||
async onDelete() {
|
||||
wx.showModal({ title: '确认删除', content: '确定删除这个打卡项?历史记录将保留。', success: async (res) => {
|
||||
if (res.confirm) { try { await api.del('/api/checkin/items/' + this.data.id); wx.navigateBack() } catch(e) { wx.showToast({ title: '删除失败', icon: 'none' }) } }
|
||||
}})
|
||||
},
|
||||
async onTogglePause() {
|
||||
const { id, item } = this.data
|
||||
if (!item) return
|
||||
try { await api.post('/api/checkin/items/' + id + '/' + (item.status==='active'?'pause':'resume')); this.loadData() }
|
||||
catch(e) { wx.showToast({ title: '操作失败', icon: 'none' }) }
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
{ "navigationBarTitleText": "打卡详情", "usingComponents": {} }
|
||||
@@ -0,0 +1,44 @@
|
||||
<view wx:if="{{loading}}" class="loading">加载中...</view>
|
||||
<view wx:else>
|
||||
<!-- Actions button -->
|
||||
<view class="action-btn" bindtap="toggleActions">⋯</view>
|
||||
<view class="action-menu" wx:if="{{showActions}}">
|
||||
<view class="action-item" bindtap="onTogglePause">{{item?.status==='active'?'暂停打卡':'恢复打卡'}}</view>
|
||||
<view class="action-item" bindtap="onDelete" style="color:#FF3B30">删除</view>
|
||||
</view>
|
||||
|
||||
<!-- Item info -->
|
||||
<view class="card" wx:if="{{item}}">
|
||||
<view class="item-header">
|
||||
<view class="item-icon">{{item.icon || '📋'}}</view>
|
||||
<view>
|
||||
<text class="item-name">{{item.name}}</text>
|
||||
<text class="item-meta">{{item.category}} · {{item.cycleType}} · {{item.status==='active'?'进行中':'已暂停'}}</text>
|
||||
<text class="item-goal" wx:if="{{item.dailyGoal}}">🎯 {{item.dailyGoal}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Stats -->
|
||||
<view class="stats-row">
|
||||
<view class="stat"><text class="stat-num">{{streak}}</text><text class="stat-lbl">连续天数</text></view>
|
||||
<view class="stat"><text class="stat-num">{{records.length}}</text><text class="stat-lbl">本月打卡</text></view>
|
||||
<view class="stat"><text class="stat-num">{{days.filter(d=>d.checked).length}}/{{days.length}}</text><text class="stat-lbl">完成率</text></view>
|
||||
</view>
|
||||
|
||||
<!-- Calendar -->
|
||||
<view class="card calendar">
|
||||
<view class="cal-header">
|
||||
<text class="cal-nav" bindtap="prevMonth">←</text>
|
||||
<text>{{year}}年{{month}}月</text>
|
||||
<text class="cal-nav" bindtap="nextMonth">→</text>
|
||||
</view>
|
||||
<view class="cal-grid">
|
||||
<view wx:for="{{['日','一','二','三','四','五','六']}}" wx:key="*this" class="cal-weekday">{{item}}</view>
|
||||
<view wx:for="{{days}}" wx:key="day" class="cal-day {{item.checked?'checked':''}} {{item.isToday?'today':''}}" wx:for-item="d">
|
||||
<text wx:if="{{!d.empty}}">{{d.day}}</text>
|
||||
<view class="cal-dot" wx:if="{{!d.empty && d.checked}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,24 @@
|
||||
.loading { text-align: center; padding: 40px; color: #86868B; }
|
||||
.card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
||||
.action-btn { position: absolute; right: 16px; top: 12px; font-size: 20px; padding: 4px 8px; z-index: 10; }
|
||||
.action-menu { position: absolute; right: 16px; top: 44px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 10; min-width: 120px; }
|
||||
.action-item { padding: 12px 16px; font-size: 14px; border-bottom: 0.5px solid #E5E5EA; }
|
||||
.action-item:last-child { border-bottom: none; }
|
||||
.item-header { display: flex; align-items: center; gap: 12px; }
|
||||
.item-icon { width: 52px; height: 52px; border-radius: 14px; background: #FFF0EB; display: flex; align-items: center; justify-content: center; font-size: 26px; }
|
||||
.item-name { font-size: 18px; font-weight: 700; display: block; }
|
||||
.item-meta { font-size: 13px; color: #86868B; display: block; margin-top: 2px; }
|
||||
.item-goal { font-size: 13px; color: #86868B; display: block; margin-top: 2px; }
|
||||
.stats-row { display: flex; justify-content: space-around; background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
||||
.stat { text-align: center; }
|
||||
.stat-num { font-size: 28px; font-weight: 700; color: #FF6B35; display: block; }
|
||||
.stat-lbl { font-size: 13px; color: #86868B; }
|
||||
.calendar { padding: 16px; }
|
||||
.cal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 16px; font-weight: 600; }
|
||||
.cal-nav { font-size: 18px; padding: 4px 8px; }
|
||||
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; }
|
||||
.cal-weekday { font-size: 12px; color: #86868B; padding: 6px 0; }
|
||||
.cal-day { padding: 8px 0; font-size: 14px; border-radius: 8px; position: relative; }
|
||||
.cal-day.checked { font-weight: 600; }
|
||||
.cal-day.today { background: #FFF0EB; }
|
||||
.cal-dot { width: 6px; height: 6px; border-radius: 3px; background: #34C759; margin: 2px auto 0; }
|
||||
Reference in New Issue
Block a user