feat: 实现微博签到小程序功能

- 实现签到主页面,包含签到按钮、连续天数、今日状态展示
- 实现签到记录页面,包含日历视图和签到历史列表
- 实现个人中心页面,包含用户信息和签到统计
- 后端实现签到、查询状态、查询历史三个接口
- 使用 Supabase 存储签到记录数据
- 采用星空主题设计,深蓝紫渐变背景 + 金色星光强调色
- 完成所有接口测试和前后端匹配验证
- 通过 ESLint 检查和编译验证
This commit is contained in:
jaystar
2026-03-16 11:17:17 +08:00
commit e209fe02a4
64 changed files with 26475 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
export default typeof definePageConfig === 'function'
? definePageConfig({
navigationBarTitleText: '微博签到',
navigationBarBackgroundColor: '#111827',
navigationBarTextStyle: 'white'
})
: {
navigationBarTitleText: '微博签到',
navigationBarBackgroundColor: '#111827',
navigationBarTextStyle: 'white'
}

View File

@@ -0,0 +1 @@
/* 优先使用 tailwindcss如无必要请不要使用css */

122
src/pages/index/index.tsx Normal file
View File

@@ -0,0 +1,122 @@
import { View, Text } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import { FC, useState } from 'react'
import { Network } from '@/network'
import './index.css'
interface SignInStatus {
todaySignedIn: boolean
continuousDays: number
totalDays: number
}
/**
* 签到主页面
*/
const IndexPage: FC = () => {
const [signInStatus, setSignInStatus] = useState<SignInStatus>({
todaySignedIn: false,
continuousDays: 0,
totalDays: 0
})
const [loading, setLoading] = useState(false)
// 页面显示时获取签到状态
useDidShow(async () => {
await fetchSignInStatus()
})
// 获取签到状态
const fetchSignInStatus = async () => {
try {
const res = await Network.request({
url: '/api/signin/status',
method: 'GET'
})
console.log('签到状态:', res.data)
if (res.data?.code === 200 && res.data?.data) {
setSignInStatus(res.data.data)
}
} catch (error) {
console.error('获取签到状态失败:', error)
}
}
// 签到
const handleSignIn = async () => {
if (loading || signInStatus.todaySignedIn) return
setLoading(true)
try {
const res = await Network.request({
url: '/api/signin',
method: 'POST'
})
console.log('签到结果:', res.data)
if (res.data?.code === 200) {
// 更新签到状态
await fetchSignInStatus()
}
} catch (error) {
console.error('签到失败:', error)
} finally {
setLoading(false)
}
}
return (
<View className="min-h-screen bg-gradient-to-b from-gray-900 via-blue-900 to-gray-900 px-4 py-6">
{/* 顶部标题 */}
<View className="text-center mb-8">
<Text className="block text-white text-3xl font-bold"></Text>
<Text className="block text-blue-300 text-sm mt-2"></Text>
</View>
{/* 统计卡片 */}
<View className="flex flex-row gap-3 mb-8">
<View className="flex-1 bg-gradient-to-br from-blue-600 to-purple-600 rounded-2xl p-4">
<Text className="block text-blue-200 text-xs"></Text>
<Text className="block text-white text-3xl font-bold mt-1">{signInStatus.continuousDays}</Text>
<Text className="block text-blue-200 text-xs mt-1"></Text>
</View>
<View className="flex-1 bg-gray-800 rounded-2xl p-4">
<Text className="block text-gray-400 text-xs"></Text>
<Text className="block text-white text-3xl font-bold mt-1">{signInStatus.totalDays}</Text>
<Text className="block text-gray-400 text-xs mt-1"></Text>
</View>
</View>
{/* 签到按钮 */}
<View className="flex justify-center items-center mb-8">
<View
className={`w-40 h-40 rounded-full flex items-center justify-center shadow-lg ${
signInStatus.todaySignedIn
? 'bg-gradient-to-br from-green-500 to-green-600 shadow-green-500/50'
: 'bg-gradient-to-br from-blue-500 to-purple-600 shadow-blue-500/50'
} ${loading ? 'opacity-50' : ''}`}
onClick={handleSignIn}
>
{loading ? (
<Text className="text-white text-xl font-bold">...</Text>
) : signInStatus.todaySignedIn ? (
<Text className="text-white text-xl font-bold"></Text>
) : (
<Text className="text-white text-2xl font-bold"></Text>
)}
</View>
</View>
{/* 提示文字 */}
<View className="text-center">
{signInStatus.todaySignedIn ? (
<Text className="block text-green-400 text-sm"></Text>
) : (
<Text className="block text-yellow-400 text-sm"></Text>
)}
</View>
</View>
)
}
export default IndexPage

View File

@@ -0,0 +1,11 @@
export default typeof definePageConfig === 'function'
? definePageConfig({
navigationBarTitleText: '我的',
navigationBarBackgroundColor: '#111827',
navigationBarTextStyle: 'white'
})
: {
navigationBarTitleText: '我的',
navigationBarBackgroundColor: '#111827',
navigationBarTextStyle: 'white'
}

View File

@@ -0,0 +1 @@
/* 个人中心页面样式 */

105
src/pages/profile/index.tsx Normal file
View File

@@ -0,0 +1,105 @@
import { View, Text } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import { FC, useState } from 'react'
import { Network } from '@/network'
import './index.css'
interface SignInStatus {
todaySignedIn: boolean
continuousDays: number
totalDays: number
}
/**
* 个人中心页面
*/
const ProfilePage: FC = () => {
const [signInStatus, setSignInStatus] = useState<SignInStatus>({
todaySignedIn: false,
continuousDays: 0,
totalDays: 0
})
// 页面显示时获取签到统计
useDidShow(async () => {
await fetchSignInStatus()
})
// 获取签到状态
const fetchSignInStatus = async () => {
try {
const res = await Network.request({
url: '/api/signin/status',
method: 'GET'
})
console.log('签到状态:', res.data)
if (res.data?.code === 200 && res.data?.data) {
setSignInStatus(res.data.data)
}
} catch (error) {
console.error('获取签到状态失败:', error)
}
}
return (
<View className="min-h-screen bg-gray-900 px-4 py-6">
{/* 用户信息卡片 */}
<View className="bg-gradient-to-br from-blue-600 to-purple-600 rounded-2xl p-6 mb-6">
<View className="flex flex-row items-center gap-4 mb-4">
<View className="w-16 h-16 rounded-full bg-white bg-opacity-20 flex items-center justify-center">
<Text className="text-white text-2xl">👤</Text>
</View>
<View className="flex-1">
<Text className="block text-white text-xl font-bold"></Text>
<Text className="block text-blue-200 text-sm mt-1">ID: default_user</Text>
</View>
</View>
</View>
{/* 签到统计 */}
<View className="mb-6">
<Text className="block text-white text-lg font-bold mb-4"></Text>
<View className="flex flex-row gap-3">
<View className="flex-1 bg-gray-800 rounded-2xl p-4">
<Text className="block text-gray-400 text-xs"></Text>
<Text className="block text-white text-3xl font-bold mt-1">{signInStatus.continuousDays}</Text>
<Text className="block text-gray-400 text-xs mt-1"></Text>
</View>
<View className="flex-1 bg-gray-800 rounded-2xl p-4">
<Text className="block text-gray-400 text-xs"></Text>
<Text className="block text-white text-3xl font-bold mt-1">{signInStatus.totalDays}</Text>
<Text className="block text-gray-400 text-xs mt-1"></Text>
</View>
</View>
</View>
{/* 今日状态 */}
<View className="bg-gray-800 rounded-2xl p-4 mb-6">
<View className="flex flex-row items-center justify-between">
<View>
<Text className="block text-white text-base font-semibold"></Text>
<Text className="block text-gray-400 text-sm mt-1">
{signInStatus.todaySignedIn ? '已完成' : '未完成'}
</Text>
</View>
<View
className={`w-12 h-12 rounded-full flex items-center justify-center ${
signInStatus.todaySignedIn ? 'bg-green-500' : 'bg-gray-700'
}`}
>
<Text className="text-white text-xl">{signInStatus.todaySignedIn ? '✓' : '○'}</Text>
</View>
</View>
</View>
{/* 提示信息 */}
<View className="bg-gray-800 rounded-2xl p-4">
<Text className="block text-gray-400 text-sm leading-relaxed">
</Text>
</View>
</View>
)
}
export default ProfilePage

View File

@@ -0,0 +1,11 @@
export default typeof definePageConfig === 'function'
? definePageConfig({
navigationBarTitleText: '签到记录',
navigationBarBackgroundColor: '#111827',
navigationBarTextStyle: 'white'
})
: {
navigationBarTitleText: '签到记录',
navigationBarBackgroundColor: '#111827',
navigationBarTextStyle: 'white'
}

View File

@@ -0,0 +1 @@
/* 签到记录页面样式 */

170
src/pages/record/index.tsx Normal file
View File

@@ -0,0 +1,170 @@
import { View, Text } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import { FC, useState } from 'react'
import { Network } from '@/network'
import './index.css'
interface SignInRecord {
id: number
user_id: string
sign_date: string
created_at: string
}
/**
* 签到记录页面
*/
const RecordPage: FC = () => {
const [records, setRecords] = useState<SignInRecord[]>([])
const [loading, setLoading] = useState(true)
// 页面显示时获取签到记录
useDidShow(async () => {
await fetchRecords()
})
// 获取签到记录
const fetchRecords = async () => {
try {
setLoading(true)
const res = await Network.request({
url: '/api/signin/history',
method: 'GET'
})
console.log('签到记录:', res.data)
if (res.data?.code === 200 && res.data?.data) {
setRecords(res.data.data)
}
} catch (error) {
console.error('获取签到记录失败:', error)
} finally {
setLoading(false)
}
}
// 格式化日期显示
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const weekDay = weekDays[date.getDay()]
return `${year}${month}${day}${weekDay}`
}
// 获取当前月份的天数
const getCurrentMonthDays = () => {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth()
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const days: { date: Date; isSigned: boolean }[] = []
// 填充前面的空白
for (let i = 0; i < firstDay.getDay(); i++) {
days.push({ date: null as any, isSigned: false })
}
// 填充日期
for (let i = 1; i <= lastDay.getDate(); i++) {
const date = new Date(year, month, i)
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
const isSigned = records.some(r => r.sign_date === dateStr)
days.push({ date, isSigned })
}
return days
}
const monthDays = getCurrentMonthDays()
const now = new Date()
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
return (
<View className="min-h-screen bg-gray-900 px-4 py-6">
{/* 月份标题 */}
<View className="mb-6">
<Text className="block text-white text-xl font-bold">{monthNames[now.getMonth()]} {now.getFullYear()}</Text>
</View>
{/* 日历视图 */}
<View className="bg-gray-800 rounded-2xl p-4 mb-6">
{/* 星期标题 */}
<View className="flex flex-row mb-3">
{['日', '一', '二', '三', '四', '五', '六'].map((day, index) => (
<View key={index} className="flex-1 flex justify-center">
<Text className="text-gray-400 text-sm">{day}</Text>
</View>
))}
</View>
{/* 日期网格 */}
<View className="flex flex-row flex-wrap">
{monthDays.map((item, index) => (
<View key={index} className="w-[14.28%] aspect-square flex justify-center items-center mb-1">
{item.date ? (
<View
className={`w-8 h-8 rounded-full flex items-center justify-center ${
item.isSigned
? 'bg-blue-500'
: item.date.toDateString() === now.toDateString()
? 'bg-gray-700 border border-blue-400'
: 'bg-transparent'
}`}
>
<Text
className={`text-sm ${
item.isSigned
? 'text-white'
: item.date.toDateString() === now.toDateString()
? 'text-blue-400'
: 'text-gray-400'
}`}
>
{item.date.getDate()}
</Text>
</View>
) : (
<View />
)}
</View>
))}
</View>
</View>
{/* 签到记录列表 */}
<View className="bg-gray-800 rounded-2xl p-4">
<Text className="block text-white text-lg font-bold mb-4"></Text>
{loading ? (
<View className="flex justify-center py-8">
<Text className="text-gray-500">...</Text>
</View>
) : records.length === 0 ? (
<View className="flex flex-col items-center justify-center py-8">
<Text className="text-gray-500 text-base"></Text>
<Text className="text-gray-600 text-sm mt-2"></Text>
</View>
) : (
<View className="flex flex-col gap-3">
{records.map((record) => (
<View key={record.id} className="flex flex-row items-center gap-3 py-3 border-b border-gray-700 last:border-b-0">
<View className="w-2 h-2 rounded-full bg-blue-500" />
<View className="flex-1">
<Text className="block text-white text-sm">{formatDate(record.sign_date)}</Text>
</View>
<View className="bg-blue-500 rounded-full px-3 py-1">
<Text className="text-white text-xs"></Text>
</View>
</View>
))}
</View>
)}
</View>
</View>
)
}
export default RecordPage