首次提交

This commit is contained in:
zeronline
2026-06-25 08:39:47 +08:00
parent 20a93c703a
commit c5cccd346b
169 changed files with 24720 additions and 0 deletions
+22
View File
@@ -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 })
}
})
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "创建打卡项", "usingComponents": {} }
+32
View File
@@ -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>
+11
View File
@@ -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; }
+55
View File
@@ -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' }) }
},
})
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "打卡详情", "usingComponents": {} }
+44
View File
@@ -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>
+24
View File
@@ -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; }
+25
View File
@@ -0,0 +1,25 @@
const api = require('../../services/api')
const app = getApp()
Page({
data: { user: null, items: [], streaks: {}, loading: true },
onShow() {
if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return }
this.setData({ user: app.globalData.user })
this.loadItems()
},
onPullDownRefresh() { this.loadItems().then(() => wx.stopPullDownRefresh()) },
async loadItems() {
try {
const items = await api.get('/api/checkin/items')
const streaks = {}
for (const item of items.filter(i => i.status === 'active')) {
try { const s = await api.get('/api/checkin/streak/' + item.id); streaks[item.id] = s.streak } catch(e) {}
}
this.setData({ items: items.filter(i => i.status !== 'deleted'), streaks, loading: false })
} catch(e) { this.setData({ loading: false }) }
},
goCreate() { wx.navigateTo({ url: '/pages/checkin/create/index' }) },
goDetail(e) { wx.navigateTo({ url: '/pages/checkin/detail/index?id=' + e.currentTarget.dataset.id }) },
goProfile() { wx.switchTab({ url: '/pages/profile/index/index' }) },
})
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "寸进", "usingComponents": {} }
+31
View File
@@ -0,0 +1,31 @@
<view wx:if="{{!loading}}">
<view class="header-card">
<view class="avatar">{{user.nickname ? user.nickname[0] : (user.phone ? user.phone.slice(-4) : '?')}}</view>
<view class="header-info">
<text class="header-name">{{user.nickname || '用户'}}</text>
<text class="header-sub">积分 {{user.points}} · {{items.filter(i=>i.status==='active').length}} 个活跃项</text>
</view>
<button class="profile-btn" bindtap="goProfile">我的</button>
</view>
<view class="section-header">
<text class="section-title">今日打卡</text>
<button class="create-btn" bindtap="goCreate">+ 创建</button>
</view>
<view wx:for="{{items}}" wx:key="id" class="checkin-card" data-id="{{item.id}}" bindtap="goDetail" wx:for-item="item">
<view class="card-icon">{{item.icon || '📋'}}</view>
<view class="card-body">
<text class="card-name">{{item.name}}</text>
<text class="card-meta">{{item.category}} · {{item.cycleType}}</text>
</view>
<text class="card-streak" wx:if="{{streaks[item.id]}}">🔥 {{streaks[item.id]}}天</text>
</view>
<view class="empty" wx:if="{{items.length===0}}">
<text class="empty-icon">📋</text>
<text class="empty-text">还没有打卡项</text>
<text class="empty-sub">点击上方"创建"开始</text>
</view>
</view>
<view wx:if="{{loading}}" class="loading">加载中...</view>
+20
View File
@@ -0,0 +1,20 @@
.header-card { background: white; border-radius: 12px; margin: 12px 16px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.avatar { width: 44px; height: 44px; border-radius: 22px; background: linear-gradient(135deg, #FF6B35, #FF4D6D); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; font-weight: 600; flex-shrink: 0; }
.header-info { flex: 1; }
.header-name { font-size: 16px; font-weight: 600; display: block; }
.header-sub { font-size: 13px; color: #86868B; }
.profile-btn { background: none; border: 1px solid #FF6B35; color: #FF6B35; font-size: 14px; padding: 4px 12px; border-radius: 8px; }
.section-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 16px 8px; }
.section-title { font-size: 20px; font-weight: 700; }
.create-btn { background: #FF6B35; color: white; border: none; font-size: 14px; padding: 6px 14px; border-radius: 8px; }
.checkin-card { background: white; border-radius: 12px; margin: 8px 16px; padding: 14px 16px; display: flex; align-items: center; gap: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.card-icon { width: 44px; height: 44px; border-radius: 12px; background: #FFF0EB; display: flex; align-items: center; justify-content: center; font-size: 22px; }
.card-body { flex: 1; }
.card-name { font-size: 16px; font-weight: 600; display: block; }
.card-meta { font-size: 13px; color: #86868B; }
.card-streak { font-size: 14px; color: #FF6B35; font-weight: 600; }
.empty { text-align: center; padding: 60px 16px; }
.empty-icon { font-size: 40px; }
.empty-text { font-size: 15px; color: #86868B; display: block; margin-top: 8px; }
.empty-sub { font-size: 13px; color: #86868B; }
.loading { text-align: center; padding: 40px; color: #86868B; font-size: 15px; }
+24
View File
@@ -0,0 +1,24 @@
const api = require('../../services/api')
const app = getApp()
Page({
data: { phone: '', loading: false, error: '' },
onLoad() {
if (app.globalData.token) wx.reLaunch({ url: '/pages/index/index' })
},
onPhoneInput(e) { this.setData({ phone: e.detail.value.replace(/\D/g, ''), error: '' }) },
async onLogin() {
if (this.data.phone.length !== 11) { this.setData({ error: '请输入正确的手机号' }); return }
this.setData({ loading: true, error: '' })
try {
const result = await api.post('/api/auth/login', { phone: this.data.phone })
app.globalData.token = result.token
app.globalData.user = result.user
wx.setStorageSync('token', result.token)
wx.setStorageSync('user', result.user)
wx.reLaunch({ url: '/pages/index/index' })
} catch (e) {
this.setData({ error: e.message || '登录失败' })
} finally { this.setData({ loading: false }) }
}
})
+4
View File
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "登录",
"usingComponents": {}
}
+25
View File
@@ -0,0 +1,25 @@
<view class="container">
<view class="logo-area">
<view class="logo">📈</view>
<text class="slogan">寸进</text>
<text class="sub-slogan">日日皆有成长</text>
</view>
<input class="input-field" type="text" maxlength="11" placeholder="手机号" value="{{phone}}" bindinput="onPhoneInput" />
<view class="error" wx:if="{{error}}">{{error}}</view>
<button class="btn-primary" loading="{{loading}}" disabled="{{loading || phone.length<11}}" bindtap="onLogin">
{{loading ? '登录中...' : '登录 / 注册'}}
</button>
<text class="tip">未注册的手机号将自动创建账号</text>
</view>
<style>
.container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 32px; }
.logo-area { text-align: center; margin-bottom: 48px; }
.logo { width: 80px; height: 80px; border-radius: 20px; background: linear-gradient(135deg, #FF6B35, #FF4D6D); display: flex; align-items: center; justify-content: center; font-size: 36px; margin: 0 auto 16px; }
.slogan { font-size: 28px; font-weight: 700; display: block; }
.sub-slogan { font-size: 15px; color: #86868B; margin-top: 4px; display: block; }
.input-field { margin-bottom: 12px; width: 100%; }
.error { font-size: 13px; color: #FF3B30; margin-bottom: 12px; }
.btn-primary { margin-top: 12px; }
.tip { font-size: 13px; color: #86868B; margin-top: 16px; }
</style>
+9
View File
@@ -0,0 +1,9 @@
page { background: white; }
.container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 32px; box-sizing: border-box; }
.logo-area { text-align: center; margin-bottom: 48px; }
.logo { width: 80px; height: 80px; line-height: 80px; border-radius: 20px; background: linear-gradient(135deg, #FF6B35, #FF4D6D); text-align: center; font-size: 36px; margin: 0 auto 16px; }
.slogan { font-size: 28px; font-weight: 700; }
.sub-slogan { font-size: 15px; color: #86868B; margin-top: 4px; }
.error { font-size: 13px; color: #FF3B30; margin-bottom: 12px; text-align: center; }
.btn-primary { margin-top: 12px; }
.tip { font-size: 13px; color: #86868B; margin-top: 16px; text-align: center; }
+28
View File
@@ -0,0 +1,28 @@
const api = require('../../../services/api')
const app = getApp()
Page({
data: { user: null },
onShow() {
if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return }
this.loadProfile()
},
async loadProfile() {
try {
const user = await api.get('/api/users/profile')
app.globalData.user = user
this.setData({ user })
} catch(e) {}
},
goEdit() { wx.navigateTo({ url: '/pages/profile/edit/index' }) },
goAchievements() { wx.navigateTo({ url: '/pages/achievements/index' }) },
onLogout() {
wx.showModal({ title: '确认退出', content: '确定要退出登录吗?', success: (res) => {
if (res.confirm) {
app.globalData.token = ''; app.globalData.user = null
wx.removeStorageSync('token'); wx.removeStorageSync('user')
wx.reLaunch({ url: '/pages/login/index' })
}
}})
},
})
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "我的", "usingComponents": {} }
+16
View File
@@ -0,0 +1,16 @@
<view class="profile-card" wx:if="{{user}}">
<view class="profile-avatar">{{(user.nickname?.[0]) || (user.phone?.slice(-4)) || '?'}}</view>
<text class="profile-name">{{user.nickname || '未设置昵称'}}</text>
<text class="profile-bio">{{user.bio || '还没有个性签名'}}</text>
</view>
<view class="stats-card">
<view class="stat-item"><text class="stat-value">{{user?.points || 0}}</text><text class="stat-label">积分</text></view>
<view class="stat-item"><text class="stat-value">--</text><text class="stat-label">连续</text></view>
<view class="stat-item"><text class="stat-value">--</text><text class="stat-label">徽章</text></view>
</view>
<view class="menu-card">
<view class="menu-item" bindtap="goAchievements"><text>成就徽章</text><text class="menu-arrow">→</text></view>
<view class="menu-item" bindtap="onLogout" style="color:#FF3B30"><text>退出登录</text><text class="menu-arrow">→</text></view>
</view>
+12
View File
@@ -0,0 +1,12 @@
.profile-card { background: white; border-radius: 12px; margin: 12px 16px; padding: 32px 16px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.profile-avatar { width: 72px; height: 72px; border-radius: 36px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; font-size: 28px; color: white; font-weight: 600; margin: 0 auto 12px; }
.profile-name { font-size: 22px; font-weight: 700; display: block; }
.profile-bio { font-size: 14px; color: #86868B; margin-top: 4px; display: block; }
.stats-card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; display: flex; justify-content: space-around; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.stat-item { text-align: center; }
.stat-value { font-size: 24px; font-weight: 700; color: #FF6B35; display: block; }
.stat-label { font-size: 13px; color: #86868B; }
.menu-card { background: white; border-radius: 12px; margin: 8px 16px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.menu-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 0.5px solid #E5E5EA; font-size: 15px; }
.menu-item:last-child { border-bottom: none; }
.menu-arrow { font-size: 15px; color: #86868B; }
+28
View File
@@ -0,0 +1,28 @@
const api = require('../../../services/api')
Page({
data: { posts: [], page: 1, loading: true, showCreate: false, content: '' },
onShow() { this.loadFeed() },
onPullDownRefresh() { this.setData({ page: 1 }); this.loadFeed().then(() => wx.stopPullDownRefresh()) },
async loadFeed() {
try {
const data = await api.get('/api/social/feed?page=' + this.data.page + '&pageSize=20')
if (this.data.page === 1) this.setData({ posts: data.list || [] })
else this.setData({ posts: [...this.data.posts, ...(data.list || [])] })
} catch(e) {}
this.setData({ loading: false })
},
openCreate() { this.setData({ showCreate: true, content: '' }) },
closeCreate() { this.setData({ showCreate: false }) },
onContentInput(e) { this.setData({ content: e.detail.value }) },
async submitPost() {
if (!this.data.content.trim()) return
try { await api.post('/api/social/posts', { content: this.data.content.trim(), privacy: 1 }); this.closeCreate(); this.onPullDownRefresh() }
catch(e) { wx.showToast({ title: e.message || '发布失败', icon: 'none' }) }
},
async onLike(e) {
const id = e.currentTarget.dataset.id; const posts = this.data.posts
try { await api.post('/api/social/posts/' + id + '/like'); const idx = posts.findIndex(p => p.id === id); if (idx > -1) { posts[idx].likes++; this.setData({ posts }) } }
catch(e) { wx.showToast({ title: '操作失败', icon: 'none' }) }
},
loadMore() { this.setData({ page: this.data.page + 1 }); this.loadFeed() },
})
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "广场", "enablePullDownRefresh": true, "usingComponents": {} }
+29
View File
@@ -0,0 +1,29 @@
<button class="create-post-btn" bindtap="openCreate">+ 分享动态</button>
<view wx:for="{{posts}}" wx:key="id" class="post-card" wx:for-item="p">
<view class="post-header">
<view class="post-avatar">{{p.user?.nickname?.[0] || '?'}}</view>
<view><text class="post-user">{{p.user?.nickname || '用户'}}</text><text class="post-time">{{p.createdAt?.slice(0,10)}}</text></view>
</view>
<text class="post-content" wx:if="{{p.content}}">{{p.content}}</text>
<view class="post-actions">
<text class="action-btn" data-id="{{p.id}}" bindtap="onLike">{{p.likes > 0 ? '❤️' : '🤍'}} {{p.likes}}</text>
<text class="action-btn">💬 {{p.comments}}</text>
</view>
</view>
<view class="load-more" wx:if="{{posts.length>0}}" bindtap="loadMore">加载更多</view>
<view class="empty" wx:if="{{!loading && posts.length===0}}"><text>🌊</text><text class="empty-text">还没有动态</text></view>
<view wx:if="{{loading}}" class="loading">加载中...</view>
<!-- 发布弹层 -->
<view class="modal-mask" wx:if="{{showCreate}}" bindtap="closeCreate">
<view class="modal-content" catchtap="">
<text class="modal-title">发布动态</text>
<textarea class="modal-textarea" value="{{content}}" bindinput="onContentInput" placeholder="分享你的打卡故事..." maxlength="500" />
<view class="modal-actions">
<button class="cancel-btn" bindtap="closeCreate">取消</button>
<button class="submit-btn" disabled="{{!content.trim()}}" bindtap="submitPost">发布</button>
</view>
</view>
</view>
+22
View File
@@ -0,0 +1,22 @@
.create-post-btn { display: block; margin: 12px 16px; background: white; border-radius: 12px; padding: 16px; color: #86868B; font-size: 15px; text-align: left; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.post-card { background: white; border-radius: 12px; margin: 8px 16px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.post-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.post-avatar { width: 36px; height: 36px; border-radius: 18px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; }
.post-user { font-size: 14px; font-weight: 600; display: block; }
.post-time { font-size: 12px; color: #86868B; }
.post-content { font-size: 15px; line-height: 1.5; display: block; }
.post-actions { display: flex; gap: 20px; padding-top: 8px; margin-top: 8px; border-top: 0.5px solid #F0F0F0; }
.action-btn { font-size: 13px; color: #86868B; }
.load-more { text-align: center; padding: 16px; color: #86868B; font-size: 14px; }
.empty { text-align: center; padding: 60px 16px; font-size: 40px; }
.empty-text { font-size: 15px; color: #86868B; display: block; margin-top: 8px; }
.loading { text-align: center; padding: 40px; color: #86868B; }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: flex-end; z-index: 100; }
.modal-content { background: white; border-radius: 16px 16px 0 0; width: 100%; padding: 20px 16px; }
.modal-title { font-size: 17px; font-weight: 600; text-align: center; display: block; margin-bottom: 12px; }
.modal-textarea { width: 100%; border: 1px solid #E5E5EA; border-radius: 10px; padding: 12px; font-size: 15px; min-height: 120px; box-sizing: border-box; }
.modal-actions { display: flex; gap: 12px; margin-top: 12px; }
.cancel-btn { flex: 1; padding: 12px; background: #f8f8fa; border: 1px solid #E5E5EA; border-radius: 10px; font-size: 15px; text-align: center; }
.submit-btn { flex: 1; padding: 12px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; text-align: center; }
.submit-btn[disabled] { opacity: 0.5; }
+21
View File
@@ -0,0 +1,21 @@
const api = require('../../../services/api')
Page({
data: { friends: [], addPhone: '', loading: true },
onShow() { this.loadFriends() },
async loadFriends() {
try { const f = await api.get('/api/social/friends'); this.setData({ friends: f || [] }) } catch(e) {}
this.setData({ loading: false })
},
onPhoneInput(e) { this.setData({ addPhone: e.detail.value }) },
async onAdd() {
if (!this.data.addPhone.trim()) return
try { await api.post('/api/social/friends/request', { friendId: this.data.addPhone.trim() }); wx.showToast({ title: '请求已发送' }); this.setData({ addPhone: '' }) }
catch(e) { wx.showToast({ title: e.message || '添加失败', icon: 'none' }) }
},
async onRemove(e) {
const id = e.currentTarget.dataset.id
wx.showModal({ title: '确认', content: '确定删除好友?', success: async (res) => {
if (res.confirm) { try { await api.del('/api/social/friends/' + id); this.loadFriends() } catch(e) {} }
}})
}
})
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "好友", "usingComponents": {} }
+12
View File
@@ -0,0 +1,12 @@
<view class="add-section">
<input class="input-field" value="{{addPhone}}" bindinput="onPhoneInput" placeholder="输入手机号添加" style="flex:1;margin-bottom:0" />
<button class="add-btn" bindtap="onAdd" disabled="{{!addPhone.trim()}}">添加</button>
</view>
<text class="section-title">好友列表 ({{friends.length}})</text>
<view wx:for="{{friends}}" wx:key="id" class="friend-item">
<view class="friend-avatar">{{item.nickname?.[0] || '?'}}</view>
<text class="friend-name">{{item.nickname || '用户'}}</text>
<button class="remove-btn" data-id="{{item.id}}" bindtap="onRemove">删除</button>
</view>
<view class="empty" wx:if="{{!loading && friends.length===0}}"><text class="empty-text">还没有好友</text></view>
+10
View File
@@ -0,0 +1,10 @@
.add-section { display: flex; gap: 8px; padding: 12px 16px; align-items: center; }
.add-btn { padding: 10px 20px; background: #FF6B35; color: white; border: none; border-radius: 10px; font-size: 14px; }
.add-btn[disabled] { background: #E5E5EA; }
.section-title { font-size: 15px; color: #86868B; padding: 16px 16px 8px; display: block; }
.friend-item { display: flex; align-items: center; gap: 10px; background: white; margin: 4px 16px; padding: 12px 16px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.friend-avatar { width: 36px; height: 36px; border-radius: 18px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; }
.friend-name { flex: 1; font-size: 15px; }
.remove-btn { background: none; border: none; color: #FF3B30; font-size: 13px; padding: 4px 8px; }
.empty { text-align: center; padding: 40px; }
.empty-text { font-size: 15px; color: #86868B; }