This commit is contained in:
ChuXun
2025-10-19 20:28:31 +08:00
parent c81f8a8b03
commit eaab9a762a
100 changed files with 23416 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
// pages/ai-assistant/ai-assistant.js
const aiService = require('../../utils/aiService.js')
const learningTracker = require('../../utils/learningTracker.js')
const util = require('../../utils/util');
Page({
data: {
// 场景列表
scenarios: [],
currentScenario: null,
// 消息列表
messages: [],
// 输入框
inputValue: '',
inputPlaceholder: '输入你的问题...',
// 状态
isThinking: false,
scrollIntoView: '',
// 打字机效果
typewriterTimer: null,
currentTypingMessage: ''
},
onLoad(options) {
// 加载场景列表
const scenarios = aiService.getScenarios();
this.setData({ scenarios });
// 从存储加载历史对话
this.loadHistory();
// 如果从其他页面传入场景
if (options.scenario) {
this.selectScenario({ currentTarget: { dataset: { id: options.scenario } } });
}
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('ai')
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
// 清理打字机定时器
if (this.data.typewriterTimer) {
clearTimeout(this.data.typewriterTimer);
}
// 保存对话历史
this.saveHistory();
},
/**
* 选择场景
*/
selectScenario(e) {
const scenarioId = e.currentTarget.dataset.id;
const scenario = this.data.scenarios.find(s => s.id === scenarioId);
if (!scenario) return;
// 触觉反馈
wx.vibrateShort({ type: 'light' });
// 设置当前场景
this.setData({
currentScenario: scenarioId,
inputPlaceholder: `${scenario.name} - 输入你的问题...`
});
// 如果有预设提示词,自动发送
if (scenario.prompt && this.data.messages.length === 0) {
this.setData({ inputValue: scenario.prompt });
// 延迟发送,让用户看到输入
setTimeout(() => {
this.sendMessage();
}, 500);
}
},
/**
* 输入框变化
*/
onInput(e) {
this.setData({
inputValue: e.detail.value
});
},
/**
* 发送消息
*/
async sendMessage() {
const content = this.data.inputValue.trim();
if (!content || this.data.isThinking) return;
// 触觉反馈
wx.vibrateShort({ type: 'medium' });
// 添加用户消息
const userMessage = {
role: 'user',
content: content,
time: util.formatTime(new Date(), 'hh:mm')
};
const messages = [...this.data.messages, userMessage];
this.setData({
messages,
inputValue: '',
isThinking: true
});
// 滚动到底部
this.scrollToBottom();
try {
// 调用AI服务
const reply = await aiService.chat(
messages.map(m => ({ role: m.role, content: m.content })),
this.data.currentScenario
);
// 添加AI回复(带打字机效果)
const assistantMessage = {
role: 'assistant',
content: '', // 初始为空,打字机逐步显示
time: util.formatTime(new Date(), 'hh:mm')
};
this.setData({
messages: [...this.data.messages, assistantMessage],
isThinking: false
});
// 启动打字机效果
this.typewriterEffect(reply, this.data.messages.length - 1);
} catch (error) {
// 错误处理
const errorMessage = {
role: 'assistant',
content: aiService.getErrorMessage(error),
time: util.formatTime(new Date(), 'hh:mm')
};
this.setData({
messages: [...this.data.messages, errorMessage],
isThinking: false
});
this.scrollToBottom();
wx.showToast({
title: '发送失败',
icon: 'error'
});
}
},
/**
* 打字机效果
*/
typewriterEffect(text, messageIndex, currentIndex = 0) {
if (currentIndex >= text.length) {
// 打字完成,保存历史
this.saveHistory();
return;
}
// 每次显示2-3个字符(中文)或5-8个字符(英文)
const charsToAdd = text[currentIndex].match(/[\u4e00-\u9fa5]/) ?
Math.min(2, text.length - currentIndex) :
Math.min(6, text.length - currentIndex);
const newText = text.substring(0, currentIndex + charsToAdd);
// 更新消息内容
const messages = this.data.messages;
messages[messageIndex].content = newText;
this.setData({ messages });
this.scrollToBottom();
// 继续打字
const delay = text[currentIndex].match(/[,。!?;:,.]/) ? 150 : 50;
this.data.typewriterTimer = setTimeout(() => {
this.typewriterEffect(text, messageIndex, currentIndex + charsToAdd);
}, delay);
},
/**
* 清空对话
*/
clearMessages() {
wx.showModal({
title: '确认清空',
content: '确定要清空所有对话记录吗?',
confirmColor: '#FF3B30',
success: (res) => {
if (res.confirm) {
this.setData({
messages: [],
currentScenario: null,
inputPlaceholder: '输入你的问题...'
});
// 清除存储
wx.removeStorageSync('ai_chat_history');
wx.showToast({
title: '已清空',
icon: 'success'
});
}
}
});
},
/**
* 滚动到底部
*/
scrollToBottom() {
const query = wx.createSelectorQuery();
query.select('.message-list').boundingClientRect();
query.selectViewport().scrollOffset();
setTimeout(() => {
this.setData({
scrollIntoView: `msg-${this.data.messages.length - 1}`
});
}, 100);
},
/**
* 保存对话历史
*/
saveHistory() {
try {
wx.setStorageSync('ai_chat_history', {
messages: this.data.messages,
scenario: this.data.currentScenario,
timestamp: Date.now()
});
} catch (error) {
console.error('保存历史失败:', error);
}
},
/**
* 加载对话历史
*/
loadHistory() {
try {
const history = wx.getStorageSync('ai_chat_history');
if (history && history.messages) {
// 只加载24小时内的历史
const hoursPassed = (Date.now() - history.timestamp) / (1000 * 60 * 60);
if (hoursPassed < 24) {
this.setData({
messages: history.messages,
currentScenario: history.scenario
});
setTimeout(() => this.scrollToBottom(), 300);
}
}
} catch (error) {
console.error('加载历史失败:', error);
}
}
});

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "启思AI",
"navigationBarBackgroundColor": "#6C5CE7",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": false,
"backgroundColor": "#F5F6FA"
}

View File

@@ -0,0 +1,88 @@
<!--pages/ai-assistant/ai-assistant.wxml-->
<view class="ai-container">
<!-- 智能场景选择 -->
<view class="scenario-bar" wx:if="{{messages.length === 0}}">
<scroll-view class="scenario-scroll" scroll-x="{{true}}" show-scrollbar="{{false}}">
<view class="scenario-item"
wx:for="{{scenarios}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="selectScenario">
<view class="scenario-icon">{{item.icon}}</view>
<view class="scenario-name">{{item.name}}</view>
</view>
</scroll-view>
</view>
<!-- 欢迎界面 -->
<view class="welcome-section" wx:if="{{messages.length === 0}}">
<view class="welcome-icon">🤖</view>
<view class="welcome-title">启思AI</view>
<view class="welcome-subtitle">启迪思维·智慧学习·与你同行</view>
<view class="welcome-tips">
<view class="tip-item">💡 智能解答学习疑问</view>
<view class="tip-item">📚 个性化学习建议</view>
<view class="tip-item">🎯 高效学习计划</view>
</view>
</view>
<!-- 聊天消息列表 -->
<scroll-view class="message-list"
scroll-y="{{true}}"
scroll-into-view="{{scrollIntoView}}"
scroll-with-animation="{{true}}"
wx:if="{{messages.length > 0}}">
<view class="message-item {{item.role}}"
wx:for="{{messages}}"
wx:key="index"
id="msg-{{index}}">
<view class="avatar">
<view wx:if="{{item.role === 'user'}}" class="user-avatar">👤</view>
<view wx:else class="ai-avatar">🤖</view>
</view>
<view class="message-content">
<view class="message-text">{{item.content}}</view>
<view class="message-time">{{item.time}}</view>
</view>
</view>
<!-- AI思考中 -->
<view class="message-item assistant thinking" wx:if="{{isThinking}}">
<view class="avatar">
<view class="ai-avatar">🤖</view>
</view>
<view class="message-content">
<view class="thinking-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="input-bar">
<view class="input-wrapper">
<textarea class="message-input"
placeholder="{{inputPlaceholder}}"
value="{{inputValue}}"
bindinput="onInput"
auto-height
maxlength="500"
adjust-position="{{true}}"></textarea>
<view class="char-count">{{inputValue.length}}/500</view>
</view>
<button class="send-btn {{inputValue.length > 0 ? 'active' : ''}}"
bindtap="sendMessage"
disabled="{{isThinking || inputValue.length === 0}}">
<text wx:if="{{!isThinking}}">发送</text>
<text wx:else>...</text>
</button>
</view>
<!-- 清空对话按钮 -->
<view class="clear-btn" wx:if="{{messages.length > 0}}" bindtap="clearMessages">
<text>🗑️ 清空对话</text>
</view>
</view>

View File

@@ -0,0 +1,356 @@
/* pages/ai-assistant/ai-assistant.wxss */
@import "/styles/design-tokens.wxss";
@import "/styles/premium-animations.wxss";
.ai-container {
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
position: relative;
}
/* 场景选择栏 */
.scenario-bar {
padding: 20rpx 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
}
.scenario-scroll {
white-space: nowrap;
padding: 0 20rpx;
}
.scenario-item {
display: inline-block;
padding: 20rpx 30rpx;
margin-right: 20rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
text-align: center;
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.scenario-item:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.2);
}
.scenario-icon {
font-size: 48rpx;
margin-bottom: 8rpx;
}
.scenario-name {
font-size: 24rpx;
color: #FFFFFF;
font-weight: 500;
}
/* 欢迎界面 */
.welcome-section {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 40rpx;
animation: fadeInUp 0.6s ease;
}
.welcome-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
animation: float 3s ease-in-out infinite;
}
.welcome-title {
font-size: 48rpx;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 20rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.welcome-subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 60rpx;
text-align: center;
}
.welcome-tips {
width: 100%;
}
.tip-item {
padding: 24rpx;
margin-bottom: 20rpx;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(20rpx);
border-radius: 16rpx;
color: #FFFFFF;
font-size: 28rpx;
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
/* 消息列表 */
.message-list {
flex: 1;
padding: 30rpx;
overflow-y: auto;
}
.message-item {
display: flex;
margin-bottom: 30rpx;
animation: messageSlideIn 0.3s ease;
}
.message-item.user {
flex-direction: row-reverse;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
margin: 0 20rpx;
}
.avatar image {
width: 100%;
height: 100%;
}
.user-avatar {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
box-shadow: 0 4rpx 12rpx rgba(74, 144, 226, 0.3);
}
.ai-avatar {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.message-content {
max-width: 500rpx;
animation: fadeIn 0.3s ease;
}
.message-text {
padding: 24rpx 28rpx;
border-radius: 20rpx;
font-size: 28rpx;
line-height: 1.6;
word-wrap: break-word;
white-space: pre-wrap;
}
.message-item.user .message-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #FFFFFF;
border-bottom-right-radius: 4rpx;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.message-item.assistant .message-text {
background: rgba(255, 255, 255, 0.95);
color: #2D3436;
border-bottom-left-radius: 4rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.message-time {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
margin-top: 8rpx;
text-align: right;
}
.message-item.assistant .message-time {
text-align: left;
}
/* AI思考中 */
.thinking-dots {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
border-bottom-left-radius: 4rpx;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #667eea;
margin: 0 6rpx;
animation: thinking 1.4s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
/* 输入区域 */
.input-bar {
display: flex;
align-items: flex-end;
padding: 20rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
safe-area-inset-bottom: constant(safe-area-inset-bottom);
safe-area-inset-bottom: env(safe-area-inset-bottom);
}
.input-wrapper {
flex: 1;
position: relative;
margin-right: 20rpx;
}
.message-input {
width: 100%;
min-height: 80rpx;
max-height: 200rpx;
padding: 20rpx 24rpx;
background: #F5F6FA;
border-radius: 16rpx;
font-size: 28rpx;
line-height: 1.5;
box-sizing: border-box;
}
.char-count {
position: absolute;
right: 24rpx;
bottom: 8rpx;
font-size: 20rpx;
color: #999999;
}
.send-btn {
width: 120rpx;
height: 80rpx;
background: #CCCCCC;
color: #FFFFFF;
border-radius: 16rpx;
font-size: 28rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
margin: 0;
padding: 0;
border: none;
}
.send-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4);
}
.send-btn:active {
transform: scale(0.95);
}
.send-btn::after {
border: none;
}
/* 清空按钮 */
.clear-btn {
position: absolute;
top: 20rpx;
right: 20rpx;
padding: 16rpx 24rpx;
background: rgba(255, 59, 48, 0.9);
color: #FFFFFF;
border-radius: 30rpx;
font-size: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3);
z-index: 100;
}
.clear-btn:active {
transform: scale(0.95);
}
/* 动画 */
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes thinking {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10rpx);
opacity: 1;
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20rpx);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -0,0 +1,159 @@
// pages/countdown/countdown.js
const { getCountdown, showSuccess, showError } = require('../../utils/util.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
countdowns: [],
newEventName: '',
newEventDate: ''
},
onLoad() {
this.loadCountdowns()
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('tools')
this.startTimer()
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
this.stopTimer()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
this.stopTimer()
},
// 加载倒计时
loadCountdowns() {
let countdowns = wx.getStorageSync('countdowns') || []
// 默认示例数据
if (countdowns.length === 0) {
countdowns = [
{
id: 1,
name: '高等数学期末考试',
date: '2025-12-20',
color: '#FF6B6B'
},
{
id: 2,
name: '英语四级考试',
date: '2025-12-15',
color: '#4A90E2'
},
{
id: 3,
name: '课程设计答辩',
date: '2025-12-25',
color: '#50C878'
}
]
}
this.setData({ countdowns })
this.updateAllCountdowns()
},
// 开始定时器
startTimer() {
this.timer = setInterval(() => {
this.updateAllCountdowns()
}, 1000)
},
// 停止定时器
stopTimer() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
// 更新所有倒计时
updateAllCountdowns() {
const { countdowns } = this.data
const updated = countdowns.map(item => ({
...item,
...getCountdown(item.date)
}))
this.setData({ countdowns: updated })
},
// 事件名称输入
onNameInput(e) {
this.setData({ newEventName: e.detail.value })
},
// 日期选择
onDateChange(e) {
this.setData({ newEventDate: e.detail.value })
},
// 添加倒计时
onAddCountdown() {
const { newEventName, newEventDate, countdowns } = this.data
if (!newEventName.trim()) {
showError('请输入事件名称')
return
}
if (!newEventDate) {
showError('请选择日期')
return
}
const colors = ['#FF6B6B', '#4A90E2', '#50C878', '#F39C12', '#9B59B6']
const newCountdown = {
id: Date.now(),
name: newEventName.trim(),
date: newEventDate,
color: colors[Math.floor(Math.random() * colors.length)]
}
countdowns.push(newCountdown)
wx.setStorageSync('countdowns', countdowns)
this.setData({
countdowns,
newEventName: '',
newEventDate: ''
})
this.updateAllCountdowns()
showSuccess('添加成功')
},
// 删除倒计时
onDelete(e) {
const { id } = e.currentTarget.dataset
wx.showModal({
title: '确认删除',
content: '确定要删除这个倒计时吗?',
success: (res) => {
if (res.confirm) {
let { countdowns } = this.data
countdowns = countdowns.filter(item => item.id !== id)
wx.setStorageSync('countdowns', countdowns)
this.setData({ countdowns })
showSuccess('删除成功')
}
}
})
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "考试倒计时"
}

View File

@@ -0,0 +1,78 @@
<!--pages/countdown/countdown.wxml-->
<view class="container">
<!-- 添加倒计时 -->
<view class="add-section">
<view class="add-title">添加新倒计时</view>
<view class="add-form">
<input
class="form-input"
placeholder="事件名称"
value="{{newEventName}}"
bindinput="onNameInput"
/>
<picker
mode="date"
value="{{newEventDate}}"
bindchange="onDateChange"
>
<view class="date-picker">
{{newEventDate || '选择日期'}}
</view>
</picker>
<button class="add-btn" bindtap="onAddCountdown">添加</button>
</view>
</view>
<!-- 倒计时列表 -->
<view class="countdown-list">
<view
class="countdown-card"
wx:for="{{countdowns}}"
wx:key="id"
style="border-left-color: {{item.color}};"
>
<view class="card-header">
<view class="event-name">{{item.name}}</view>
<view
class="delete-btn"
data-id="{{item.id}}"
bindtap="onDelete"
>
×
</view>
</view>
<view class="event-date">📅 {{item.date}}</view>
<view class="countdown-display" wx:if="{{!item.isExpired}}">
<view class="time-block">
<view class="time-value" style="background-color: {{item.color}};">{{item.days}}</view>
<view class="time-label">天</view>
</view>
<view class="time-block">
<view class="time-value" style="background-color: {{item.color}};">{{item.hours}}</view>
<view class="time-label">时</view>
</view>
<view class="time-block">
<view class="time-value" style="background-color: {{item.color}};">{{item.minutes}}</view>
<view class="time-label">分</view>
</view>
<view class="time-block">
<view class="time-value" style="background-color: {{item.color}};">{{item.seconds}}</view>
<view class="time-label">秒</view>
</view>
</view>
<view class="expired-tip" wx:if="{{item.isExpired}}">
<text class="expired-icon">⏰</text>
<text class="expired-text">已到期</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{countdowns.length === 0}}">
<text class="empty-icon">⏰</text>
<text class="empty-text">暂无倒计时,快来添加一个吧</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,163 @@
/* pages/countdown/countdown.wxss */
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding: 30rpx;
}
/* 添加区域 */
.add-section {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.add-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 25rpx;
}
.add-form {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.form-input, .date-picker {
width: 100%;
height: 80rpx;
line-height: 80rpx;
padding: 0 25rpx;
background-color: #F5F5F5;
border-radius: 12rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.date-picker {
color: #333333;
}
.add-btn {
height: 80rpx;
line-height: 80rpx;
background-color: #F39C12;
color: #ffffff;
border-radius: 40rpx;
font-size: 30rpx;
font-weight: bold;
}
/* 倒计时列表 */
.countdown-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.countdown-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
border-left: 8rpx solid #4A90E2;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.event-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
flex: 1;
}
.delete-btn {
width: 50rpx;
height: 50rpx;
line-height: 50rpx;
text-align: center;
font-size: 48rpx;
color: #CCCCCC;
font-weight: 300;
}
.event-date {
font-size: 26rpx;
color: #999999;
margin-bottom: 25rpx;
}
.countdown-display {
display: flex;
justify-content: space-between;
gap: 15rpx;
}
.time-block {
flex: 1;
text-align: center;
}
.time-value {
height: 90rpx;
line-height: 90rpx;
background-color: #4A90E2;
color: #ffffff;
border-radius: 12rpx;
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.time-label {
font-size: 22rpx;
color: #999999;
}
.expired-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 30rpx;
background-color: #FFF0E6;
border-radius: 12rpx;
}
.expired-icon {
font-size: 40rpx;
}
.expired-text {
font-size: 28rpx;
color: #F39C12;
font-weight: bold;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}

View File

@@ -0,0 +1,82 @@
// pages/course-detail/course-detail.js
const { coursesData } = require('../../utils/data.js')
const { showSuccess, showError } = require('../../utils/util.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
course: null,
isFavorite: false,
activeTab: 0,
tabs: ['课程信息', '教学大纲', '课程评价']
},
onLoad(options) {
const { id } = options
this.loadCourseDetail(id)
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('course')
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 加载课程详情
loadCourseDetail(id) {
const course = coursesData.find(c => c.id === parseInt(id))
if (course) {
const favorites = wx.getStorageSync('favoriteCourses') || []
this.setData({
course,
isFavorite: favorites.includes(course.id)
})
} else {
showError('课程不存在')
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
// 切换标签
onTabChange(e) {
const { index } = e.currentTarget.dataset
this.setData({ activeTab: index })
},
// 收藏/取消收藏
onToggleFavorite() {
const { course, isFavorite } = this.data
let favorites = wx.getStorageSync('favoriteCourses') || []
if (isFavorite) {
favorites = favorites.filter(id => id !== course.id)
showSuccess('取消收藏')
} else {
favorites.push(course.id)
showSuccess('收藏成功')
}
wx.setStorageSync('favoriteCourses', favorites)
this.setData({ isFavorite: !isFavorite })
},
// 分享课程
onShareAppMessage() {
const { course } = this.data
return {
title: `推荐课程:${course.name}`,
path: `/pages/course-detail/course-detail?id=${course.id}`
}
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "课程详情"
}

View File

@@ -0,0 +1,121 @@
<!--pages/course-detail/course-detail.wxml-->
<view class="container" wx:if="{{course}}">
<!-- 课程头部 -->
<view class="course-header">
<view class="course-name">{{course.name}}</view>
<view class="course-meta">
<text class="meta-tag">{{course.category}}</text>
<text class="meta-tag">{{course.credit}}学分</text>
</view>
</view>
<!-- 基本信息卡片 -->
<view class="info-card">
<view class="info-row">
<text class="info-icon">👨‍🏫</text>
<view class="info-content">
<text class="info-label">授课教师</text>
<text class="info-value">{{course.teacher}}</text>
</view>
</view>
<view class="info-row">
<text class="info-icon">🏫</text>
<view class="info-content">
<text class="info-label">开课院系</text>
<text class="info-value">{{course.department}}</text>
</view>
</view>
<view class="info-row">
<text class="info-icon">📍</text>
<view class="info-content">
<text class="info-label">上课地点</text>
<text class="info-value">{{course.location}}</text>
</view>
</view>
<view class="info-row">
<text class="info-icon">⏰</text>
<view class="info-content">
<text class="info-label">上课时间</text>
<text class="info-value">{{course.time}}</text>
</view>
</view>
<view class="info-row">
<text class="info-icon">👥</text>
<view class="info-content">
<text class="info-label">选课人数</text>
<text class="info-value">{{course.enrolled}}/{{course.capacity}}</text>
</view>
</view>
</view>
<!-- 标签栏 -->
<view class="tab-bar">
<view
class="tab-item {{activeTab === index ? 'active' : ''}}"
wx:for="{{tabs}}"
wx:key="index"
data-index="{{index}}"
bindtap="onTabChange"
>
{{item}}
</view>
</view>
<!-- 内容区域 -->
<view class="content-area">
<!-- 课程信息 -->
<view class="tab-content" wx:if="{{activeTab === 0}}">
<view class="section">
<view class="section-title">课程简介</view>
<view class="section-content">{{course.description}}</view>
</view>
</view>
<!-- 教学大纲 -->
<view class="tab-content" wx:if="{{activeTab === 1}}">
<view class="section">
<view class="section-title">教学内容</view>
<view class="syllabus-list">
<view
class="syllabus-item"
wx:for="{{course.syllabus}}"
wx:key="index"
>
<text class="syllabus-number">{{index + 1}}</text>
<text class="syllabus-text">{{item}}</text>
</view>
</view>
</view>
</view>
<!-- 课程评价 -->
<view class="tab-content" wx:if="{{activeTab === 2}}">
<view class="empty-tip">
<text class="empty-icon">💬</text>
<text class="empty-text">暂无评价,快来发表第一条评价吧</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="action-bar">
<button
class="action-btn favorite-btn"
bindtap="onToggleFavorite"
>
<text class="btn-icon">{{isFavorite ? '❤️' : '🤍'}}</text>
<text>{{isFavorite ? '已收藏' : '收藏'}}</text>
</button>
<button
class="action-btn share-btn"
open-type="share"
>
<text class="btn-icon">📤</text>
<text>分享</text>
</button>
</view>
</view>

View File

@@ -0,0 +1,220 @@
/* pages/course-detail/course-detail.wxss */
.container {
padding-bottom: 150rpx;
}
/* 课程头部 */
.course-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 30rpx 40rpx;
color: #ffffff;
}
.course-name {
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.course-meta {
display: flex;
gap: 15rpx;
}
.meta-tag {
padding: 8rpx 20rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
font-size: 24rpx;
}
/* 信息卡片 */
.info-card {
background-color: #ffffff;
margin: 20rpx 30rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.info-row {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #F5F5F5;
}
.info-row:last-child {
border-bottom: none;
}
.info-icon {
font-size: 36rpx;
margin-right: 20rpx;
}
.info-content {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
/* 标签栏 */
.tab-bar {
display: flex;
background-color: #ffffff;
margin: 0 30rpx;
border-radius: 12rpx;
overflow: hidden;
}
.tab-item {
flex: 1;
text-align: center;
padding: 25rpx 0;
font-size: 28rpx;
color: #666666;
transition: all 0.3s;
}
.tab-item.active {
color: #4A90E2;
font-weight: bold;
background-color: #E8F4FF;
}
/* 内容区域 */
.content-area {
margin: 20rpx 30rpx;
}
.tab-content {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
}
.section {
margin-bottom: 30rpx;
}
.section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
padding-left: 20rpx;
border-left: 6rpx solid #4A90E2;
}
.section-content {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
text-align: justify;
}
/* 教学大纲 */
.syllabus-list {
padding-left: 10rpx;
}
.syllabus-item {
display: flex;
align-items: center;
padding: 20rpx;
margin-bottom: 15rpx;
background-color: #F8F9FA;
border-radius: 12rpx;
}
.syllabus-number {
width: 50rpx;
height: 50rpx;
line-height: 50rpx;
text-align: center;
background-color: #4A90E2;
color: #ffffff;
border-radius: 25rpx;
font-size: 24rpx;
margin-right: 20rpx;
}
.syllabus-text {
flex: 1;
font-size: 28rpx;
color: #333333;
}
/* 空状态 */
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 26rpx;
color: #999999;
}
/* 底部操作栏 */
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 90rpx;
border-radius: 45rpx;
font-size: 28rpx;
gap: 10rpx;
}
.favorite-btn {
background-color: #FFE8E8;
color: #FF6B6B;
}
.share-btn {
background-color: #E8F4FF;
color: #4A90E2;
}
.btn-icon {
font-size: 32rpx;
}

204
pages/courses/courses.js Normal file
View File

@@ -0,0 +1,204 @@
// pages/courses/courses.js
const { coursesData } = require('../../utils/data.js')
const { showSuccess, showError } = require('../../utils/util.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
allCourses: [],
displayCourses: [],
searchKeyword: '',
selectedCategory: 'all',
categories: [
{ id: 'all', name: '全部' },
{ id: '必修', name: '必修' },
{ id: '专业必修', name: '专业必修' },
{ id: '选修', name: '选修' },
{ id: '专业选修', name: '专业选修' }
],
showFilter: false,
selectedDepartment: '全部', // 修改为'全部'而不是'all'
departments: [
'全部',
'数学系',
'物理系',
'计算机学院',
'外国语学院'
]
},
onLoad() {
try {
this.loadCourses()
} catch (error) {
console.error('课程加载失败:', error)
showError('课程数据加载失败')
}
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('course')
// 更新自定义TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 1
})
}
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 加载课程数据
loadCourses() {
const courses = coursesData.map(course => ({
...course,
isFavorite: this.checkFavorite(course.id)
}))
this.setData({
allCourses: courses,
displayCourses: courses
})
},
// 检查是否已收藏
checkFavorite(courseId) {
const favorites = wx.getStorageSync('favoriteCourses') || []
return favorites.includes(courseId)
},
// 搜索课程(防抖处理)
onSearchInput(e) {
const keyword = e.detail.value
this.setData({ searchKeyword: keyword })
// 清除之前的延迟搜索
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
// 设置300ms防抖
this.searchTimer = setTimeout(() => {
this.filterCourses()
}, 300)
},
// 分类筛选
onCategoryChange(e) {
const category = e.currentTarget.dataset.category
this.setData({ selectedCategory: category })
this.filterCourses()
},
// 院系筛选
onDepartmentChange(e) {
const index = e.detail.value
this.setData({
selectedDepartment: this.data.departments[index],
showFilter: false
})
this.filterCourses()
},
// 筛选课程
filterCourses() {
const { allCourses, searchKeyword, selectedCategory, selectedDepartment } = this.data
console.log('筛选条件:', {
searchKeyword,
selectedCategory,
selectedDepartment,
allCoursesCount: allCourses.length
})
let filtered = allCourses.filter(course => {
// 搜索关键词过滤(不区分大小写,去除空格)
const keyword = (searchKeyword || '').trim().toLowerCase()
const matchKeyword = !keyword ||
(course.name && course.name.toLowerCase().includes(keyword)) ||
(course.teacher && course.teacher.toLowerCase().includes(keyword)) ||
(course.code && course.code.toLowerCase().includes(keyword))
// 分类过滤
const matchCategory = selectedCategory === 'all' ||
course.category === selectedCategory
// 院系过滤
const matchDepartment = selectedDepartment === '全部' ||
course.department === selectedDepartment
return matchKeyword && matchCategory && matchDepartment
})
console.log('筛选结果:', filtered.length, '门课程')
this.setData({ displayCourses: filtered })
},
// 显示/隐藏筛选面板
toggleFilter() {
this.setData({ showFilter: !this.data.showFilter })
},
// 收藏课程
onFavorite(e) {
const { id } = e.currentTarget.dataset
const { allCourses } = this.data
// 更新课程收藏状态
const updatedCourses = allCourses.map(course => {
if (course.id === id) {
course.isFavorite = !course.isFavorite
// 保存到本地存储
let favorites = wx.getStorageSync('favoriteCourses') || []
if (course.isFavorite) {
favorites.push(id)
showSuccess('收藏成功')
} else {
favorites = favorites.filter(fid => fid !== id)
showSuccess('取消收藏')
}
wx.setStorageSync('favoriteCourses', favorites)
}
return course
})
this.setData({ allCourses: updatedCourses })
this.filterCourses()
},
// 查看课程详情
onCourseDetail(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/course-detail/course-detail?id=${id}`
})
},
// 清空筛选
onResetFilter() {
this.setData({
searchKeyword: '',
selectedCategory: 'all',
selectedDepartment: '全部',
showFilter: false
})
this.filterCourses()
},
// 清空搜索
onClearSearch() {
this.setData({ searchKeyword: '' })
this.filterCourses()
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "课程筛选"
}

115
pages/courses/courses.wxml Normal file
View File

@@ -0,0 +1,115 @@
<!--pages/courses/courses.wxml-->
<view class="container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrap">
<input
class="search-input"
placeholder="搜索课程名称或教师"
value="{{searchKeyword}}"
bindinput="onSearchInput"
/>
<text class="search-icon" wx:if="{{!searchKeyword}}">🔍</text>
<text class="clear-icon" wx:if="{{searchKeyword}}" bindtap="onClearSearch">✕</text>
</view>
<view class="filter-btn" bindtap="toggleFilter">
<text class="filter-icon">📋</text>
<text>筛选</text>
</view>
</view>
<!-- 分类标签 -->
<scroll-view class="category-scroll" scroll-x>
<view class="category-list">
<view
class="category-item {{selectedCategory === item.id ? 'active' : ''}}"
wx:for="{{categories}}"
wx:key="id"
data-category="{{item.id}}"
bindtap="onCategoryChange"
>
{{item.name}}
</view>
</view>
</scroll-view>
<!-- 筛选面板 -->
<view class="filter-panel {{showFilter ? 'show' : ''}}">
<view class="filter-content">
<view class="filter-item">
<text class="filter-label">院系:</text>
<picker
mode="selector"
range="{{departments}}"
value="{{selectedDepartment}}"
bindchange="onDepartmentChange"
>
<view class="picker-value">
{{selectedDepartment}} ▼
</view>
</picker>
</view>
<view class="filter-actions">
<button class="reset-btn" bindtap="onResetFilter">重置</button>
<button class="confirm-btn" bindtap="toggleFilter">确定</button>
</view>
</view>
</view>
<!-- 课程列表 -->
<view class="course-list">
<view class="result-count">
共找到 <text class="count-number">{{displayCourses.length}}</text> 门课程
</view>
<view
class="course-card"
wx:for="{{displayCourses}}"
wx:key="id"
bindtap="onCourseDetail"
data-id="{{item.id}}"
>
<view class="course-header">
<view class="course-title">{{item.name}}</view>
<view
class="favorite-btn"
catchtap="onFavorite"
data-id="{{item.id}}"
>
<text class="favorite-icon">{{item.isFavorite ? '❤️' : '🤍'}}</text>
</view>
</view>
<view class="course-info">
<view class="info-row">
<text class="info-label">👨‍🏫 教师:</text>
<text class="info-value">{{item.teacher}}</text>
</view>
<view class="info-row">
<text class="info-label">📍 地点:</text>
<text class="info-value">{{item.location}}</text>
</view>
<view class="info-row">
<text class="info-label">⏰ 时间:</text>
<text class="info-value">{{item.time}}</text>
</view>
</view>
<view class="course-footer">
<view class="course-tags">
<text class="tag category-tag">{{item.category}}</text>
<text class="tag credit-tag">{{item.credit}}学分</text>
</view>
<view class="enrollment-info">
<text class="enrollment-text">{{item.enrolled}}/{{item.capacity}}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{displayCourses.length === 0}}">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无符合条件的课程</text>
</view>
</view>
</view>

482
pages/courses/courses.wxss Normal file
View File

@@ -0,0 +1,482 @@
/* pages/courses/courses.wxss */
.container {
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
min-height: 100vh;
/* 底部留出TabBar的空间 */
padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx);
}
/* 搜索栏 */
.search-bar {
display: flex;
padding: 20rpx 30rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
gap: 20rpx;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.15);
}
.search-input-wrap {
flex: 1;
position: relative;
animation: slideInLeft 0.5s ease-out;
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20rpx);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.search-input {
width: 100%;
height: 70rpx;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 35rpx;
padding: 0 50rpx 0 30rpx;
font-size: 28rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.search-input:focus {
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
.search-icon {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 32rpx;
color: #999999;
transition: color 0.3s ease;
}
.clear-icon {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 32rpx;
color: #999999;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #E0E0E0 0%, #BDBDBD 100%);
border-radius: 50%;
font-size: 24rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.clear-icon:active {
transform: translateY(-50%) scale(0.9);
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.15);
}
.filter-btn {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 30rpx;
background: rgba(255, 255, 255, 0.25);
color: #ffffff;
border-radius: 35rpx;
font-size: 28rpx;
backdrop-filter: blur(10rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
animation: slideInRight 0.5s ease-out;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20rpx);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.filter-btn:active {
background: rgba(255, 255, 255, 0.35);
transform: scale(0.95);
}
.filter-icon {
font-size: 32rpx;
}
/* 分类标签 */
.category-scroll {
white-space: nowrap;
background-color: #ffffff;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.category-list {
display: inline-flex;
padding: 0 30rpx;
gap: 20rpx;
}
.category-item {
display: inline-block;
padding: 14rpx 32rpx;
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
color: #666666;
border-radius: 30rpx;
font-size: 26rpx;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
}
.category-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(102, 126, 234, 0.1) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.category-item:active::before {
opacity: 1;
}
.category-item.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
transform: translateY(-2rpx);
}
.category-item.active::before {
opacity: 0;
}
/* 筛选面板 */
.filter-panel {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 200;
pointer-events: none;
transition: all 0.3s ease;
}
.filter-panel.show {
pointer-events: auto;
}
.filter-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(4rpx);
}
.filter-panel.show::before {
opacity: 1;
}
.filter-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-radius: 30rpx 30rpx 0 0;
padding: 40rpx 30rpx;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.filter-panel.show .filter-content {
transform: translateY(0);
}
/* 课程列表 */
.course-list {
padding: 20rpx 30rpx;
}
.result-count {
font-size: 26rpx;
color: #999999;
margin-bottom: 20rpx;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.course-card {
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: slideInUp 0.5s ease-out both;
}
.course-card:nth-child(1) { animation-delay: 0.05s; }
.course-card:nth-child(2) { animation-delay: 0.1s; }
.course-card:nth-child(3) { animation-delay: 0.15s; }
.course-card:nth-child(4) { animation-delay: 0.2s; }
.course-card:nth-child(5) { animation-delay: 0.25s; }
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.course-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6rpx;
height: 100%;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.course-card:active {
transform: scale(0.98);
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.15);
}
.course-card:active::before {
opacity: 1;
}
/* 筛选面板 */
.filter-panel {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 200;
display: none;
}
.filter-panel.show {
display: block;
}
.filter-content {
position: absolute;
top: 200rpx;
left: 30rpx;
right: 30rpx;
background-color: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
}
.filter-item {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.filter-label {
font-size: 28rpx;
color: #333333;
margin-right: 20rpx;
}
.picker-value {
flex: 1;
padding: 20rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
font-size: 28rpx;
}
.filter-actions {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.reset-btn, .confirm-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
}
.reset-btn {
background-color: #F5F5F5;
color: #666666;
}
.confirm-btn {
background-color: #4A90E2;
color: #ffffff;
}
/* 课程列表 */
.course-list {
padding: 20rpx 30rpx;
}
.result-count {
font-size: 26rpx;
color: #999999;
margin-bottom: 20rpx;
}
.count-number {
color: #4A90E2;
font-weight: bold;
}
.course-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.course-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.course-title {
flex: 1;
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.favorite-btn {
padding: 10rpx;
}
.favorite-icon {
font-size: 36rpx;
}
.course-info {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
margin-bottom: 12rpx;
font-size: 26rpx;
}
.info-label {
color: #999999;
width: 150rpx;
}
.info-value {
flex: 1;
color: #666666;
}
.course-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #EEEEEE;
}
.course-tags {
display: flex;
gap: 10rpx;
}
.tag {
padding: 8rpx 16rpx;
border-radius: 6rpx;
font-size: 22rpx;
}
.category-tag {
background-color: #E8F4FF;
color: #4A90E2;
}
.credit-tag {
background-color: #FFF0E6;
color: #FF8C42;
}
.enrollment-info {
font-size: 24rpx;
color: #999999;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}

View File

@@ -0,0 +1,823 @@
// pages/dashboard/dashboard.js
const gpaPredictor = require('../../utils/gpaPredictor');
const util = require('../../utils/util');
const learningTracker = require('../../utils/learningTracker');
Page({
data: {
// 设备信息
windowWidth: 375,
windowHeight: 667,
pixelRatio: 2,
// 顶部统计
stats: {
totalDays: 0,
totalHours: 0,
avgGPA: 0
},
// 雷达图数据
radarData: {
indicators: [
{ name: '专注度', max: 100 },
{ name: '活跃度', max: 100 },
{ name: '学习时长', max: 100 },
{ name: '知识广度', max: 100 },
{ name: '互动性', max: 100 },
{ name: '坚持度', max: 100 }
],
values: [85, 90, 75, 88, 70, 95]
},
// GPA预测数据
gpaHistory: [],
prediction: {
nextSemester: 0,
trend: 0
},
// 饼图数据
pieData: [],
// 柱状图数据
barData: {
aboveAvg: 0,
ranking: 0
},
updateTime: ''
},
onLoad() {
// 清理可能存在的旧模拟数据
this.cleanOldData();
// 获取设备信息
const systemInfo = wx.getSystemInfoSync();
this.setData({
windowWidth: systemInfo.windowWidth,
windowHeight: systemInfo.windowHeight,
pixelRatio: systemInfo.pixelRatio
});
this.loadAllData();
},
/**
* 清理旧的模拟数据
*/
cleanOldData() {
// 检查是否需要清理(可通过版本号判断)
const dataVersion = wx.getStorageSync('data_version');
if (dataVersion !== '2.0') {
console.log('[Dashboard] 清理旧数据...');
// 只清理GPA历史记录让其从真实课程重新生成
wx.removeStorageSync('gpa_history');
// 标记数据版本
wx.setStorageSync('data_version', '2.0');
console.log('[Dashboard] 数据清理完成');
}
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('tools')
// 刷新数据
this.loadAllData();
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
onPullDownRefresh() {
this.loadAllData();
setTimeout(() => {
wx.stopPullDownRefresh();
}, 1000);
},
/**
* 加载所有数据
*/
loadAllData() {
this.loadStats();
this.loadRadarData();
this.loadGPAData();
this.loadPieData();
this.loadBarData();
this.setData({
updateTime: util.formatTime(new Date(), 'yyyy-MM-dd hh:mm')
});
// 延迟绘制图表确保DOM已渲染
setTimeout(() => {
this.drawRadarChart();
this.drawLineChart();
this.drawPieChart();
this.drawBarChart();
}, 300);
},
/**
* 加载统计数据
*/
loadStats() {
// 从学习追踪器获取真实数据
const stats = learningTracker.getStats();
// 加载GPA数据
const gpaCourses = wx.getStorageSync('gpaCourses') || [];
const avgGPA = gpaPredictor.calculateAverageGPA(gpaCourses);
this.setData({
stats: {
totalDays: stats.continuousDays,
totalHours: stats.totalHours,
avgGPA: avgGPA || 0
}
});
},
/**
* 计算连续学习天数
*/
calculateContinuousDays(records) {
if (!records || records.length === 0) return 0;
// 按日期排序
const sortedRecords = records.sort((a, b) => new Date(b.date) - new Date(a.date));
let continuousDays = 0;
const today = new Date();
today.setHours(0, 0, 0, 0);
for (let i = 0; i < sortedRecords.length; i++) {
const recordDate = new Date(sortedRecords[i].date);
recordDate.setHours(0, 0, 0, 0);
const daysDiff = Math.floor((today - recordDate) / (1000 * 60 * 60 * 24));
if (daysDiff === i) {
continuousDays++;
} else {
break;
}
}
return continuousDays;
},
/**
* 加载雷达图数据
*/
loadRadarData() {
// 从存储加载真实计算的学习画像
const learningProfile = wx.getStorageSync('learning_profile') || {
focus: 0, // 专注度
activity: 0, // 活跃度
duration: 0, // 学习时长
breadth: 0, // 知识广度
interaction: 0, // 互动性
persistence: 0 // 坚持度
};
this.setData({
'radarData.values': [
Math.min(100, Math.round(learningProfile.focus)),
Math.min(100, Math.round(learningProfile.activity)),
Math.min(100, Math.round(learningProfile.duration)),
Math.min(100, Math.round(learningProfile.breadth)),
Math.min(100, Math.round(learningProfile.interaction)),
Math.min(100, Math.round(learningProfile.persistence))
]
});
},
/**
* 加载GPA数据和预测
*/
loadGPAData() {
// 从GPA计算器获取真实数据
const gpaCourses = wx.getStorageSync('gpaCourses') || [];
console.log('[Dashboard] 读取到的课程数据:', gpaCourses);
// 强制从课程数据重新生成历史记录(不使用缓存)
let gpaHistory = [];
if (gpaCourses.length > 0) {
// 按学期分组计算平均GPA
const semesterMap = {};
gpaCourses.forEach(course => {
const semester = course.semester || '2024-2';
if (!semesterMap[semester]) {
semesterMap[semester] = { courses: [] };
}
semesterMap[semester].courses.push(course);
});
console.log('[Dashboard] 学期分组:', semesterMap);
// 计算每个学期的加权平均GPA
Object.keys(semesterMap).sort().forEach(semester => {
const data = semesterMap[semester];
const avgGPA = gpaPredictor.calculateAverageGPA(data.courses);
gpaHistory.push({
semester: semester,
gpa: parseFloat(avgGPA)
});
});
// 保存到存储
if (gpaHistory.length > 0) {
wx.setStorageSync('gpa_history', gpaHistory);
console.log('[Dashboard] GPA历史记录:', gpaHistory);
}
}
// 设置数据即使为空也设置避免undefined
this.setData({
gpaHistory: gpaHistory.length > 0 ? gpaHistory : []
});
// 只有在有足够数据时才进行预测至少2个学期
if (gpaHistory.length >= 2) {
const predictionResult = gpaPredictor.polynomialRegression(gpaHistory, 2, 3);
console.log('[Dashboard] GPA预测结果:', predictionResult);
this.setData({
prediction: {
nextSemester: predictionResult.predictions[0]?.gpa || 0,
trend: predictionResult.trend,
confidence: predictionResult.confidence
}
});
} else if (gpaHistory.length === 1) {
// 只有一个学期无法预测显示当前GPA
console.log('[Dashboard] 只有一个学期数据,无法预测');
this.setData({
prediction: {
nextSemester: gpaHistory[0].gpa,
trend: 0,
confidence: 0
}
});
} else {
// 没有数据
console.log('[Dashboard] 没有GPA数据');
this.setData({
prediction: {
nextSemester: 0,
trend: 0,
confidence: 0
}
});
}
},
/**
* 加载饼图数据
*/
loadPieData() {
// 从学习追踪器获取真实的模块使用数据
const moduleUsage = wx.getStorageSync('module_usage') || {
course: 0,
forum: 0,
tools: 0,
ai: 0
};
const total = Object.values(moduleUsage).reduce((sum, val) => sum + val, 0);
// 如果没有数据,显示提示
if (total === 0) {
this.setData({
pieData: [
{ name: '课程中心', time: 0, percent: 25, color: '#4A90E2' },
{ name: '学科论坛', time: 0, percent: 25, color: '#50C878' },
{ name: '学习工具', time: 0, percent: 25, color: '#9B59B6' },
{ name: '启思AI', time: 0, percent: 25, color: '#6C5CE7' }
]
});
return;
}
const pieData = [
{
name: '课程中心',
time: parseFloat(moduleUsage.course.toFixed(1)),
percent: Math.round((moduleUsage.course / total) * 100),
color: '#4A90E2'
},
{
name: '学科论坛',
time: parseFloat(moduleUsage.forum.toFixed(1)),
percent: Math.round((moduleUsage.forum / total) * 100),
color: '#50C878'
},
{
name: '学习工具',
time: parseFloat(moduleUsage.tools.toFixed(1)),
percent: Math.round((moduleUsage.tools / total) * 100),
color: '#9B59B6'
},
{
name: '启思AI',
time: parseFloat(moduleUsage.ai.toFixed(1)),
percent: Math.round((moduleUsage.ai / total) * 100),
color: '#6C5CE7'
}
];
this.setData({ pieData });
},
/**
* 加载柱状图数据
*/
loadBarData() {
const gpaCourses = wx.getStorageSync('gpaCourses') || [];
// 计算超过平均分的课程数
let aboveAvg = 0;
gpaCourses.forEach(course => {
if (course.score > 75) { // 假设75为平均分
aboveAvg++;
}
});
// 模拟排名基于平均GPA
const avgGPA = parseFloat(this.data.stats.avgGPA);
let ranking = 30; // 默认前30%
if (avgGPA >= 3.5) ranking = 10;
else if (avgGPA >= 3.0) ranking = 20;
this.setData({
barData: {
aboveAvg,
ranking
}
});
},
/**
* 绘制雷达图
*/
drawRadarChart() {
const query = wx.createSelectorQuery();
query.select('#radarChart').boundingClientRect();
query.exec((res) => {
if (!res[0]) return;
const canvasWidth = res[0].width;
const canvasHeight = res[0].height;
const ctx = wx.createCanvasContext('radarChart', this);
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
const radius = Math.min(canvasWidth, canvasHeight) / 2 - 60;
const data = this.data.radarData.values;
const indicators = this.data.radarData.indicators;
const angleStep = (Math.PI * 2) / indicators.length;
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 绘制背景网格
ctx.setLineWidth(1);
ctx.setStrokeStyle('#E0E0E0');
for (let level = 1; level <= 5; level++) {
ctx.beginPath();
const r = (radius / 5) * level;
for (let i = 0; i <= indicators.length; i++) {
const angle = i * angleStep - Math.PI / 2;
const x = centerX + r * Math.cos(angle);
const y = centerY + r * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.stroke();
}
// 绘制轴线和标签
ctx.setFillStyle('#333333');
ctx.setFontSize(24);
ctx.setTextAlign('center');
for (let i = 0; i < indicators.length; i++) {
const angle = i * angleStep - Math.PI / 2;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
ctx.stroke();
// 绘制维度标签
const labelDistance = radius + 30;
const labelX = centerX + labelDistance * Math.cos(angle);
const labelY = centerY + labelDistance * Math.sin(angle);
ctx.fillText(indicators[i].name, labelX, labelY);
}
// 绘制数据区域
ctx.beginPath();
ctx.setFillStyle('rgba(102, 126, 234, 0.3)');
ctx.setStrokeStyle('#667eea');
ctx.setLineWidth(2);
for (let i = 0; i <= data.length; i++) {
const index = i % data.length;
const value = data[index];
const angle = index * angleStep - Math.PI / 2;
const r = (value / 100) * radius;
const x = centerX + r * Math.cos(angle);
const y = centerY + r * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 绘制数据点
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.moveTo(x, y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.draw();
});
},
/**
* 绘制折线图GPA趋势
*/
drawLineChart() {
const query = wx.createSelectorQuery();
query.select('#lineChart').boundingClientRect();
query.exec((res) => {
if (!res[0]) return;
const canvasWidth = res[0].width;
const canvasHeight = res[0].height;
const ctx = wx.createCanvasContext('lineChart', this);
const history = this.data.gpaHistory;
const prediction = this.data.prediction;
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 如果没有数据,显示提示
if (!history || history.length === 0) {
ctx.setFillStyle('#999999');
ctx.setFontSize(24);
ctx.setTextAlign('center');
ctx.fillText('暂无GPA数据', canvasWidth / 2, canvasHeight / 2);
ctx.fillText('请先在GPA工具中录入成绩', canvasWidth / 2, canvasHeight / 2 + 30);
ctx.draw();
return;
}
console.log('[绘制折线图] 历史数据:', history);
const padding = { top: 40, right: 40, bottom: 60, left: 60 };
const width = canvasWidth - padding.left - padding.right;
const height = canvasHeight - padding.top - padding.bottom;
// 合并历史和预测数据
const allData = [...history];
const predictionResult = gpaPredictor.polynomialRegression(history, 2, 3);
if (predictionResult.predictions && predictionResult.predictions.length > 0) {
allData.push(...predictionResult.predictions);
console.log('[绘制折线图] 预测数据:', predictionResult.predictions);
}
const maxGPA = 4.5; // 调整最大值为4.5
const minGPA = 0;
const stepX = allData.length > 1 ? width / (allData.length - 1) : width / 2;
const scaleY = height / (maxGPA - minGPA);
// 绘制坐标轴
ctx.setStrokeStyle('#CCCCCC');
ctx.setLineWidth(1);
ctx.beginPath();
ctx.moveTo(padding.left, padding.top);
ctx.lineTo(padding.left, padding.top + height);
ctx.lineTo(padding.left + width, padding.top + height);
ctx.stroke();
// 绘制网格线和Y轴刻度
ctx.setStrokeStyle('#F0F0F0');
ctx.setFillStyle('#999999');
ctx.setFontSize(20);
ctx.setTextAlign('right');
for (let i = 0; i <= 4; i++) {
const gpaValue = i * 1.0;
const y = padding.top + height - (gpaValue * scaleY);
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(padding.left + width, y);
ctx.stroke();
ctx.fillText(gpaValue.toFixed(1), padding.left - 10, y + 6);
}
// 绘制历史数据折线
if (history.length > 0) {
ctx.setStrokeStyle('#667eea');
ctx.setLineWidth(3);
ctx.beginPath();
history.forEach((item, index) => {
const x = padding.left + index * stepX;
const y = padding.top + height - (item.gpa * scaleY);
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// 绘制历史数据点和数值
ctx.setFillStyle('#667eea');
history.forEach((item, index) => {
const x = padding.left + index * stepX;
const y = padding.top + height - (item.gpa * scaleY);
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
// 显示GPA值
ctx.setFillStyle('#333333');
ctx.setFontSize(18);
ctx.setTextAlign('center');
ctx.fillText(item.gpa.toFixed(2), x, y - 15);
});
}
// 绘制预测数据折线(虚线)
if (predictionResult.predictions && predictionResult.predictions.length > 0 && history.length > 0) {
ctx.setStrokeStyle('#A29BFE');
ctx.setLineDash([5, 5]);
ctx.setLineWidth(2);
ctx.beginPath();
const startIndex = history.length - 1;
const startX = padding.left + startIndex * stepX;
const startY = padding.top + height - (history[startIndex].gpa * scaleY);
ctx.moveTo(startX, startY);
predictionResult.predictions.forEach((item, index) => {
const x = padding.left + (startIndex + index + 1) * stepX;
const y = padding.top + height - (item.gpa * scaleY);
ctx.lineTo(x, y);
});
ctx.stroke();
ctx.setLineDash([]);
// 绘制预测数据点和数值
ctx.setFillStyle('#A29BFE');
predictionResult.predictions.forEach((item, index) => {
const x = padding.left + (startIndex + index + 1) * stepX;
const y = padding.top + height - (item.gpa * scaleY);
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
// 显示预测GPA值
ctx.setFillStyle('#A29BFE');
ctx.setFontSize(18);
ctx.setTextAlign('center');
ctx.fillText(item.gpa.toFixed(2), x, y - 15);
});
}
// 绘制X轴标签旋转避免重叠
ctx.setFillStyle('#999999');
ctx.setFontSize(16);
ctx.setTextAlign('left');
allData.forEach((item, index) => {
const x = padding.left + index * stepX;
const y = padding.top + height + 20;
// 旋转文字45度
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.PI / 4);
ctx.fillText(item.semester, 0, 0);
ctx.restore();
});
ctx.draw();
});
},
/**
* 绘制饼图
*/
drawPieChart() {
const query = wx.createSelectorQuery();
query.select('#pieChart').boundingClientRect();
query.exec((res) => {
if (!res[0]) return;
const canvasWidth = res[0].width;
const canvasHeight = res[0].height;
const ctx = wx.createCanvasContext('pieChart', this);
const data = this.data.pieData;
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
const radius = Math.min(canvasWidth, canvasHeight) / 2 - 40;
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 计算总数用于处理全0情况
const total = data.reduce((sum, item) => sum + item.time, 0);
if (total === 0) {
// 如果没有数据,绘制完整的灰色圆环
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.setFillStyle('#EEEEEE');
ctx.fill();
// 绘制中心圆
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
ctx.setFillStyle('#FFFFFF');
ctx.fill();
} else {
// 有数据时正常绘制
let startAngle = -Math.PI / 2;
data.forEach((item, index) => {
if (item.time > 0) { // 只绘制有数据的扇形
const angle = (item.percent / 100) * Math.PI * 2;
const endAngle = startAngle + angle;
// 绘制扇形
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.closePath();
ctx.setFillStyle(item.color);
ctx.fill();
startAngle = endAngle;
}
});
// 绘制中心圆(甜甜圈效果)
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
ctx.setFillStyle('#FFFFFF');
ctx.fill();
}
ctx.draw();
});
},
/**
* 绘制柱状图
*/
drawBarChart() {
const query = wx.createSelectorQuery();
query.select('#barChart').boundingClientRect();
query.exec((res) => {
if (!res[0]) return;
const canvasWidth = res[0].width;
const canvasHeight = res[0].height;
const ctx = wx.createCanvasContext('barChart', this);
const gpaCourses = wx.getStorageSync('gpaCourses') || [];
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 取前6门课程
const courses = gpaCourses.slice(0, 6);
if (courses.length === 0) {
ctx.setFillStyle('#999999');
ctx.setFontSize(28);
ctx.fillText('暂无成绩数据', canvasWidth / 2 - 70, canvasHeight / 2);
ctx.draw();
return;
}
const padding = { top: 40, right: 40, bottom: 80, left: 60 };
const width = canvasWidth - padding.left - padding.right;
const height = canvasHeight - padding.top - padding.bottom;
const barWidth = width / (courses.length * 2 + 1);
const maxScore = 100;
// 绘制坐标轴
ctx.setStrokeStyle('#CCCCCC');
ctx.setLineWidth(1);
ctx.beginPath();
ctx.moveTo(padding.left, padding.top);
ctx.lineTo(padding.left, padding.top + height);
ctx.lineTo(padding.left + width, padding.top + height);
ctx.stroke();
// 绘制网格线
ctx.setStrokeStyle('#F0F0F0');
for (let i = 0; i <= 5; i++) {
const y = padding.top + (height / 5) * i;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(padding.left + width, y);
ctx.stroke();
// Y轴刻度
ctx.setFillStyle('#999999');
ctx.setFontSize(20);
ctx.fillText((100 - i * 20).toString(), padding.left - 35, y + 6);
}
// 绘制柱状图
courses.forEach((course, index) => {
const x = padding.left + (index * 2 + 1) * barWidth;
const scoreHeight = (course.score / maxScore) * height;
const avgHeight = (75 / maxScore) * height; // 假设75为平均分
// 个人成绩
ctx.setFillStyle('#667eea');
ctx.fillRect(x, padding.top + height - scoreHeight, barWidth * 0.8, scoreHeight);
// 平均成绩(对比)
ctx.setFillStyle('#CCCCCC');
ctx.fillRect(x + barWidth, padding.top + height - avgHeight, barWidth * 0.8, avgHeight);
// 课程名称
ctx.setFillStyle('#333333');
ctx.setFontSize(18);
ctx.save();
ctx.translate(x + barWidth, padding.top + height + 20);
ctx.rotate(Math.PI / 4);
const courseName = course.name.length > 4 ? course.name.substring(0, 4) + '...' : course.name;
ctx.fillText(courseName, 0, 0);
ctx.restore();
});
// 图例
ctx.setFillStyle('#667eea');
ctx.fillRect(padding.left + width - 120, padding.top - 30, 20, 15);
ctx.setFillStyle('#333333');
ctx.setFontSize(20);
ctx.fillText('个人', padding.left + width - 95, padding.top - 18);
ctx.setFillStyle('#CCCCCC');
ctx.fillRect(padding.left + width - 50, padding.top - 30, 20, 15);
ctx.fillText('平均', padding.left + width - 25, padding.top - 18);
ctx.draw();
});
},
/**
* 显示某天详情
*/
showDayDetail(e) {
const { date } = e.currentTarget.dataset;
const dailyActivity = wx.getStorageSync('daily_activity') || {};
const activity = dailyActivity[date] || 0;
wx.showModal({
title: date,
content: `学习活跃度: ${activity}分钟`,
showCancel: false
});
}
});

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "学习数据",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true,
"backgroundColor": "#F5F6FA"
}

View File

@@ -0,0 +1,123 @@
<!--pages/dashboard/dashboard.wxml-->
<view class="dashboard-container">
<!-- 顶部统计卡片 -->
<view class="stats-header">
<view class="stats-card">
<view class="stat-value">{{stats.totalDays}}</view>
<view class="stat-label">连续学习天数</view>
<view class="stat-icon">🔥</view>
</view>
<view class="stats-card">
<view class="stat-value">{{stats.totalHours}}</view>
<view class="stat-label">累计学习小时</view>
<view class="stat-icon">⏰</view>
</view>
<view class="stats-card">
<view class="stat-value">{{stats.avgGPA}}</view>
<view class="stat-label">平均绩点</view>
<view class="stat-icon">📊</view>
</view>
</view>
<!-- 学习画像雷达图 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">🎯</text>
<text class="title-text">学习能力画像</text>
</view>
<view class="section-desc">6维度综合评估</view>
</view>
<view class="chart-container">
<canvas canvas-id="radarChart" class="chart-canvas" id="radarChart"></canvas>
<view class="chart-legend">
<view class="legend-item" wx:for="{{radarData.indicators}}" wx:key="name">
<view class="legend-dot" style="background: #667eea;"></view>
<text class="legend-name">{{item.name}}</text>
<text class="legend-value">{{radarData.values[index]}}</text>
</view>
</view>
</view>
</view>
<!-- GPA趋势与预测 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">📈</text>
<text class="title-text">GPA趋势预测</text>
</view>
<view class="section-desc">基于多项式回归分析</view>
</view>
<view class="chart-container">
<canvas canvas-id="lineChart" class="chart-canvas" id="lineChart"></canvas>
<view class="prediction-info">
<view class="prediction-item">
<text class="pred-label">预测下学期:</text>
<text class="pred-value">{{prediction.nextSemester}}</text>
</view>
<view class="prediction-item">
<text class="pred-label">趋势:</text>
<text class="pred-value {{prediction.trend > 0 ? 'trend-up' : 'trend-down'}}">
{{prediction.trend > 0 ? '上升' : '下降'}} {{prediction.trend}}%
</text>
</view>
</view>
</view>
</view>
<!-- 模块使用时长饼图 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">⏱️</text>
<text class="title-text">时间分配</text>
</view>
<view class="section-desc">各功能使用占比</view>
</view>
<view class="chart-container">
<canvas canvas-id="pieChart" class="chart-canvas pie-canvas" id="pieChart"></canvas>
<view class="pie-legend">
<view class="pie-legend-item" wx:for="{{pieData}}" wx:key="name">
<view class="pie-dot" style="background: {{item.color}};"></view>
<text class="pie-name">{{item.name}}</text>
<text class="pie-percent">{{item.percent}}%</text>
<text class="pie-time">{{item.time}}h</text>
</view>
</view>
</view>
</view>
<!-- 成绩对比柱状图 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">📊</text>
<text class="title-text">成绩对比</text>
</view>
<view class="section-desc">个人 vs 班级平均</view>
</view>
<view class="chart-container">
<canvas canvas-id="barChart" class="chart-canvas" id="barChart"></canvas>
<view class="bar-summary">
<view class="summary-item">
<text class="summary-label">超过平均:</text>
<text class="summary-value">{{barData.aboveAvg}}门课程</text>
</view>
<view class="summary-item">
<text class="summary-label">排名:</text>
<text class="summary-value">前{{barData.ranking}}%</text>
</view>
</view>
</view>
</view>
<!-- 数据来源说明 -->
<view class="data-source">
<view class="source-icon"></view>
<view class="source-text">
<text class="source-title">数据说明</text>
<text class="source-desc">以上数据基于您的学习记录自动生成,更新时间: {{updateTime}}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,279 @@
/* pages/dashboard/dashboard.wxss */
@import "/styles/design-tokens.wxss";
@import "/styles/premium-animations.wxss";
.dashboard-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30rpx;
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
}
/* 顶部统计卡片 */
.stats-header {
display: flex;
justify-content: space-between;
margin-bottom: 30rpx;
gap: 20rpx;
}
.stats-card {
flex: 1;
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx 20rpx;
text-align: center;
position: relative;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
animation: fadeInUp 0.6s ease;
}
.stat-value {
font-size: 48rpx;
font-weight: bold;
color: #667eea;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666666;
}
.stat-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
font-size: 32rpx;
opacity: 0.3;
}
/* 图表区域 */
.chart-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
animation: fadeInUp 0.6s ease;
}
.section-header {
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #F0F0F0;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.title-icon {
font-size: 36rpx;
margin-right: 12rpx;
}
.title-text {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.section-desc {
font-size: 24rpx;
color: #999999;
margin-left: 48rpx;
}
/* 画布容器 */
.chart-container {
width: 100%;
}
.chart-canvas {
width: 100%;
height: 500rpx;
}
.pie-canvas {
height: 400rpx;
}
/* 雷达图图例 */
.chart-legend {
margin-top: 30rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.legend-item {
display: flex;
align-items: center;
font-size: 24rpx;
}
.legend-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.legend-name {
flex: 1;
color: #666666;
}
.legend-value {
font-weight: bold;
color: #667eea;
}
/* 预测信息 */
.prediction-info {
margin-top: 30rpx;
display: flex;
justify-content: space-around;
padding: 20rpx;
background: #F8F9FA;
border-radius: 16rpx;
}
.prediction-item {
text-align: center;
}
.pred-label {
font-size: 24rpx;
color: #999999;
display: block;
margin-bottom: 8rpx;
}
.pred-value {
font-size: 32rpx;
font-weight: bold;
color: #667eea;
}
.trend-up {
color: #4CAF50;
}
.trend-down {
color: #FF5252;
}
/* 饼图图例 */
.pie-legend {
margin-top: 30rpx;
}
.pie-legend-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #F0F0F0;
font-size: 26rpx;
}
.pie-legend-item:last-child {
border-bottom: none;
}
.pie-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.pie-name {
flex: 1;
color: #333333;
}
.pie-percent {
font-weight: bold;
color: #667eea;
margin-right: 20rpx;
}
.pie-time {
color: #999999;
}
/* 柱状图总结 */
.bar-summary {
margin-top: 30rpx;
display: flex;
justify-content: space-around;
padding: 20rpx;
background: #F8F9FA;
border-radius: 16rpx;
}
.summary-item {
text-align: center;
}
.summary-label {
font-size: 24rpx;
color: #999999;
display: block;
margin-bottom: 8rpx;
}
.summary-value {
font-size: 32rpx;
font-weight: bold;
color: #667eea;
}
/* 数据来源说明 */
.data-source {
display: flex;
align-items: flex-start;
padding: 30rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 16rpx;
margin-top: 20rpx;
}
.source-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.source-text {
flex: 1;
}
.source-title {
font-size: 26rpx;
font-weight: bold;
color: #FFFFFF;
display: block;
margin-bottom: 8rpx;
}
.source-desc {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
}
/* 动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,272 @@
// pages/forum-detail/forum-detail.js
const {forumData} = require('../../utils/data.js')
const learningTracker = require('../../utils/learningTracker.js')
const { showSuccess } = require('../../utils/util.js')
// pages/forum-detail/forum-detail.js
const userManager = require('../../utils/userManager.js')
Page({
data: {
post: null,
commentText: '',
comments: [],
postId: null
},
onLoad(options) {
const { id } = options
this.setData({ postId: parseInt(id) })
this.loadPostDetail(id)
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('forum')
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 加载帖子详情
loadPostDetail(id) {
let posts = wx.getStorageSync('forumPosts') || forumData
const post = posts.find(p => p.id === parseInt(id))
if (post) {
// 增加浏览量
post.views += 1
// 检查是否已收藏
const favoritePosts = wx.getStorageSync('favoritePosts') || []
post.isFavorite = favoritePosts.some(fav => fav.id === post.id)
posts = posts.map(p => p.id === post.id ? post : p)
wx.setStorageSync('forumPosts', posts)
// 加载评论
const comments = this.loadComments(parseInt(id))
this.setData({
post,
comments
})
}
},
// 加载评论
loadComments(postId) {
const allComments = wx.getStorageSync('forumComments') || {}
return allComments[postId] || []
},
// 保存评论
saveComments(postId, comments) {
const allComments = wx.getStorageSync('forumComments') || {}
allComments[postId] = comments
wx.setStorageSync('forumComments', allComments)
// 更新帖子的评论数
let posts = wx.getStorageSync('forumPosts') || forumData
posts = posts.map(p => {
if (p.id === postId) {
p.comments = comments.length
}
return p
})
wx.setStorageSync('forumPosts', posts)
},
// 点赞
onLike() {
const { post } = this.data
let posts = wx.getStorageSync('forumPosts') || forumData
posts = posts.map(p => {
if (p.id === post.id) {
p.isLiked = !p.isLiked
p.likes = p.isLiked ? p.likes + 1 : p.likes - 1
}
return p
})
wx.setStorageSync('forumPosts', posts)
this.setData({
post: {
...post,
isLiked: !post.isLiked,
likes: post.isLiked ? post.likes - 1 : post.likes + 1
}
})
},
// 收藏帖子
onFavorite() {
const { post } = this.data
if (!post || !post.id || !post.title) {
wx.showToast({
title: '帖子数据异常',
icon: 'none'
})
console.error('帖子数据不完整:', post)
return
}
let favoritePosts = wx.getStorageSync('favoritePosts') || []
const index = favoritePosts.findIndex(p => p.id === post.id)
if (index > -1) {
// 已收藏,取消收藏
favoritePosts.splice(index, 1)
wx.setStorageSync('favoritePosts', favoritePosts)
console.log('取消收藏后的列表:', favoritePosts)
// 更新帖子状态
let posts = wx.getStorageSync('forumPosts') || []
posts = posts.map(p => {
if (p.id === post.id) {
p.isFavorite = false
}
return p
})
wx.setStorageSync('forumPosts', posts)
this.setData({
post: {
...post,
isFavorite: false
}
})
wx.showToast({
title: '已取消收藏',
icon: 'success'
})
} else {
// 未收藏,添加收藏
const favoritePost = {
id: post.id,
title: post.title,
category: post.category,
time: new Date().toLocaleString()
}
favoritePosts.push(favoritePost)
wx.setStorageSync('favoritePosts', favoritePosts)
console.log('收藏成功,当前收藏列表:', favoritePosts)
// 更新帖子状态
let posts = wx.getStorageSync('forumPosts') || []
posts = posts.map(p => {
if (p.id === post.id) {
p.isFavorite = true
}
return p
})
wx.setStorageSync('forumPosts', posts)
this.setData({
post: {
...post,
isFavorite: true
}
})
wx.showToast({
title: '收藏成功',
icon: 'success'
})
}
},
// 评论输入
onCommentInput(e) {
this.setData({ commentText: e.detail.value })
},
// 发表评论
onSubmitComment() {
const { commentText, comments, postId } = this.data
if (!commentText.trim()) {
wx.showToast({
title: '请输入评论内容',
icon: 'none'
})
return
}
// 获取用户信息
const userInfo = userManager.getUserInfo()
const userName = userInfo.nickname || '我'
const userAvatar = userInfo.avatar || '/images/avatar-default.png'
const newComment = {
id: Date.now(),
author: userName,
avatar: userAvatar,
content: commentText,
time: '刚刚'
}
const newComments = [newComment, ...comments]
// 保存评论
this.saveComments(postId, newComments)
// 同时将评论保存到帖子的commentList中
let posts = wx.getStorageSync('forumPosts') || []
posts = posts.map(p => {
if (p.id === postId) {
if (!p.commentList) {
p.commentList = []
}
p.commentList.unshift(newComment)
}
return p
})
wx.setStorageSync('forumPosts', posts)
this.setData({
comments: newComments,
commentText: ''
})
showSuccess('评论成功')
console.log('评论已发布', {
作者: userName,
内容: commentText,
帖子ID: postId
})
},
// 预览图片
onPreviewImage(e) {
const { url, urls } = e.currentTarget.dataset
wx.previewImage({
current: url, // 当前显示图片的链接
urls: urls // 需要预览的图片链接列表
})
},
// 分享
onShareAppMessage() {
const { post } = this.data
return {
title: post.title,
path: `/pages/forum-detail/forum-detail?id=${post.id}`
}
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "帖子详情"
}

View File

@@ -0,0 +1,111 @@
<!--pages/forum-detail/forum-detail.wxml-->
<view class="container" wx:if="{{post}}">
<!-- 帖子内容 -->
<view class="post-detail">
<!-- 帖子头部 -->
<view class="post-header">
<image class="avatar" src="{{post.avatar || '/images/avatar-default.png'}}" mode="aspectFill"></image>
<view class="author-info">
<view class="author-name">{{post.author}}</view>
<view class="post-time">{{post.time}}</view>
</view>
<view class="category-tag">{{post.category}}</view>
</view>
<!-- 帖子标题 -->
<view class="post-title">{{post.title}}</view>
<!-- 帖子正文 -->
<view class="post-content">{{post.content}}</view>
<!-- 帖子图片 -->
<view class="post-images" wx:if="{{post.images && post.images.length > 0}}">
<image
class="post-image"
wx:for="{{post.images}}"
wx:key="index"
wx:for-item="img"
src="{{img}}"
mode="aspectFill"
bindtap="onPreviewImage"
data-url="{{img}}"
data-urls="{{post.images}}"
></image>
</view>
<!-- 统计信息 -->
<view class="post-stats">
<view class="stat-item">
<text class="stat-icon">👀</text>
<text class="stat-text">{{post.views}}次浏览</text>
</view>
<view class="stat-item">
<text class="stat-icon">❤️</text>
<text class="stat-text">{{post.likes}}次点赞</text>
</view>
<view class="stat-item">
<text class="stat-icon">💬</text>
<text class="stat-text">{{comments.length}}条评论</text>
</view>
</view>
</view>
<!-- 评论区 -->
<view class="comments-section">
<view class="section-title">
<text class="title-text">全部评论</text>
<text class="title-count">({{comments.length}})</text>
</view>
<view class="comments-list">
<view class="comment-item" wx:for="{{comments}}" wx:key="id">
<image class="comment-avatar" src="{{item.avatar || '/images/avatar-default.png'}}" mode="aspectFill"></image>
<view class="comment-content">
<view class="comment-author">{{item.author}}</view>
<view class="comment-text">{{item.content}}</view>
<view class="comment-time">{{item.time}}</view>
</view>
</view>
<view class="no-comments" wx:if="{{comments.length === 0}}">
<text class="no-comments-text">暂无评论,快来发表第一条评论吧~</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="action-bar">
<!-- 评论输入区 -->
<view class="input-row">
<input
class="comment-input"
placeholder="说点什么..."
value="{{commentText}}"
bindinput="onCommentInput"
/>
<button class="send-btn" bindtap="onSubmitComment">发送</button>
</view>
<!-- 操作按钮区 -->
<view class="action-row">
<view
class="action-btn {{post.isLiked ? 'liked' : ''}}"
bindtap="onLike"
>
<text class="action-icon">{{post.isLiked ? '❤️' : '🤍'}}</text>
<text class="action-text">点赞</text>
</view>
<button class="action-btn" open-type="share">
<text class="action-icon">📤</text>
<text class="action-text">转发</text>
</button>
<view
class="action-btn {{post.isFavorite ? 'favorited' : ''}}"
bindtap="onFavorite"
>
<text class="action-icon">{{post.isFavorite ? '⭐' : '☆'}}</text>
<text class="action-text">收藏</text>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,278 @@
/* pages/forum-detail/forum-detail.wxss */
.container {
padding-bottom: 150rpx;
background-color: #F5F5F5;
}
/* 帖子详情 */
.post-detail {
background-color: #ffffff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.post-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
background-color: #E0E0E0;
}
.author-info {
flex: 1;
}
.author-name {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.post-time {
font-size: 24rpx;
color: #999999;
}
.category-tag {
padding: 10rpx 24rpx;
background-color: #E8F8F0;
color: #50C878;
border-radius: 20rpx;
font-size: 24rpx;
}
.post-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
line-height: 1.5;
margin-bottom: 20rpx;
}
.post-content {
font-size: 30rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 20rpx;
text-align: justify;
}
.post-images {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15rpx;
margin-bottom: 20rpx;
}
.post-image {
width: 100%;
height: 200rpx;
border-radius: 8rpx;
background-color: #F5F5F5;
}
.post-stats {
display: flex;
gap: 40rpx;
padding-top: 20rpx;
border-top: 1rpx solid #EEEEEE;
}
.stat-item {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #999999;
}
.stat-icon {
font-size: 28rpx;
}
/* 评论区 */
.comments-section {
background-color: #ffffff;
padding: 30rpx;
}
.section-title {
display: flex;
align-items: baseline;
margin-bottom: 30rpx;
}
.title-text {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.title-count {
font-size: 26rpx;
color: #999999;
margin-left: 10rpx;
}
.comments-list {
margin-top: 20rpx;
}
.comment-item {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #F5F5F5;
}
.comment-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
margin-right: 15rpx;
background-color: #E0E0E0;
}
.comment-content {
flex: 1;
}
.comment-author {
font-size: 26rpx;
font-weight: bold;
color: #333333;
margin-bottom: 10rpx;
}
.comment-text {
font-size: 28rpx;
color: #666666;
line-height: 1.6;
margin-bottom: 10rpx;
}
.comment-time {
font-size: 22rpx;
color: #999999;
}
.no-comments {
text-align: center;
padding: 60rpx 0;
}
.no-comments-text {
font-size: 26rpx;
color: #999999;
}
/* 底部操作栏 */
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
}
/* 输入行 */
.input-row {
display: flex;
align-items: center;
gap: 15rpx;
margin-bottom: 15rpx;
}
.comment-input {
flex: 1;
height: 70rpx;
background-color: #F5F5F5;
border-radius: 35rpx;
padding: 0 30rpx;
font-size: 28rpx;
}
.send-btn {
padding: 0 35rpx;
height: 70rpx;
line-height: 70rpx;
background: linear-gradient(135deg, #50C878 0%, #3CB371 100%);
color: #ffffff;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(80, 200, 120, 0.3);
transition: all 0.3s ease;
}
.send-btn:active {
transform: scale(0.95);
}
/* 操作按钮行 */
.action-row {
display: flex;
justify-content: space-around;
align-items: center;
gap: 20rpx;
}
.action-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12rpx 0;
background-color: transparent;
border: none;
margin: 0;
line-height: 1;
transition: all 0.3s ease;
}
.action-btn::after {
border: none;
}
.action-btn:active {
transform: scale(1.1);
}
.action-icon {
font-size: 36rpx;
margin-bottom: 6rpx;
}
.action-text {
font-size: 22rpx;
color: #666666;
}
.action-btn.liked .action-icon {
animation: pulse 0.6s ease;
}
.action-btn.liked .action-text {
color: #FF6B6B;
font-weight: bold;
}
.action-btn.favorited .action-icon {
animation: pulse 0.6s ease;
}
.action-btn.favorited .action-text {
color: #FFD700;
font-weight: bold;
}

191
pages/forum/forum.js Normal file
View File

@@ -0,0 +1,191 @@
// pages/forum/forum.js
const {forumData} = require('../../utils/data.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
posts: [],
categories: ['全部', '数学', '物理', '计算机', '英语', '其他'],
selectedCategory: '全部'
},
onLoad() {
this.loadPosts()
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('forum')
// 每次显示时重新加载,以获取最新发布的帖子
this.loadPosts()
// 更新自定义TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 2
})
}
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 加载帖子
loadPosts() {
try {
let posts = wx.getStorageSync('forumPosts') || forumData
// 同步评论数量
const allComments = wx.getStorageSync('forumComments') || {}
// 获取收藏的帖子列表
const favoritePosts = wx.getStorageSync('favoritePosts') || []
posts = posts.map(post => {
const comments = allComments[post.id] || []
post.comments = comments.length
// 检查是否已收藏
post.isFavorite = favoritePosts.some(fav => fav.id === post.id)
return post
})
// 保存更新后的帖子数据
wx.setStorageSync('forumPosts', posts)
// 按时间排序(最新的在前)
posts.sort((a, b) => b.id - a.id)
this.setData({ posts })
this.filterPosts()
} catch (error) {
console.error('加载帖子失败:', error)
wx.showToast({
title: '数据加载失败',
icon: 'none'
})
}
},
// 分类筛选
onCategoryChange(e) {
const category = e.currentTarget.dataset.category
this.setData({ selectedCategory: category })
this.filterPosts()
},
// 筛选帖子
filterPosts() {
const { selectedCategory } = this.data
let allPosts = wx.getStorageSync('forumPosts') || forumData
// 同步评论数量和收藏状态
const allComments = wx.getStorageSync('forumComments') || {}
const favoritePosts = wx.getStorageSync('favoritePosts') || []
allPosts = allPosts.map(post => {
const comments = allComments[post.id] || []
post.comments = comments.length
// 检查是否已收藏
post.isFavorite = favoritePosts.some(fav => fav.id === post.id)
return post
})
if (selectedCategory === '全部') {
this.setData({ posts: allPosts })
} else {
const filtered = allPosts.filter(post => post.category === selectedCategory)
this.setData({ posts: filtered })
}
},
// 查看帖子详情
onPostDetail(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/forum-detail/forum-detail?id=${id}`
})
},
// 发布新帖子
onNewPost() {
wx.navigateTo({
url: '/pages/post/post'
})
},
// 点赞
onLike(e) {
const { id } = e.currentTarget.dataset
let posts = wx.getStorageSync('forumPosts') || forumData
posts = posts.map(post => {
if (post.id === id) {
post.isLiked = !post.isLiked
post.likes = post.isLiked ? post.likes + 1 : post.likes - 1
}
return post
})
wx.setStorageSync('forumPosts', posts)
this.loadPosts()
},
// 预览图片
onPreviewImage(e) {
const { url, urls } = e.currentTarget.dataset
wx.previewImage({
current: url, // 当前显示图片的链接
urls: urls // 需要预览的图片链接列表
})
},
// 收藏/取消收藏
onFavorite(e) {
const { id } = e.currentTarget.dataset
let favoritePosts = wx.getStorageSync('favoritePosts') || []
let posts = wx.getStorageSync('forumPosts') || forumData
// 找到当前帖子
const currentPost = posts.find(post => post.id === id)
if (!currentPost) return
// 检查是否已收藏
const index = favoritePosts.findIndex(fav => fav.id === id)
if (index > -1) {
// 已收藏,取消收藏
favoritePosts.splice(index, 1)
wx.showToast({
title: '取消收藏',
icon: 'success',
duration: 1500
})
} else {
// 未收藏,添加收藏
favoritePosts.push({
id: currentPost.id,
title: currentPost.title,
category: currentPost.category,
time: new Date().toLocaleString()
})
wx.showToast({
title: '收藏成功',
icon: 'success',
duration: 1500
})
}
// 保存收藏列表
wx.setStorageSync('favoritePosts', favoritePosts)
// 重新加载帖子列表
this.loadPosts()
}
})

3
pages/forum/forum.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "学科论坛"
}

99
pages/forum/forum.wxml Normal file
View File

@@ -0,0 +1,99 @@
<!--pages/forum/forum.wxml-->
<view class="container">
<!-- 分类标签 -->
<scroll-view class="category-scroll" scroll-x>
<view class="category-list">
<view
class="category-item {{selectedCategory === item ? 'active' : ''}}"
wx:for="{{categories}}"
wx:key="index"
data-category="{{item}}"
bindtap="onCategoryChange"
>
{{item}}
</view>
</view>
</scroll-view>
<!-- 帖子列表 -->
<view class="post-list">
<view
class="post-card"
wx:for="{{posts}}"
wx:key="id"
bindtap="onPostDetail"
data-id="{{item.id}}"
>
<!-- 帖子头部 -->
<view class="post-header">
<image class="avatar" src="{{item.avatar || '/images/avatar-default.png'}}" mode="aspectFill"></image>
<view class="author-info">
<view class="author-name">{{item.author}}</view>
<view class="post-time">{{item.time}}</view>
</view>
<view class="category-tag">{{item.category}}</view>
</view>
<!-- 帖子内容 -->
<view class="post-content">
<view class="post-title">{{item.title}}</view>
<view class="post-text">{{item.content}}</view>
<!-- 图片 -->
<view class="post-images" wx:if="{{item.images && item.images.length > 0}}">
<image
class="post-image"
wx:for="{{item.images}}"
wx:key="index"
wx:for-item="img"
src="{{img}}"
mode="aspectFill"
catchtap="onPreviewImage"
data-url="{{img}}"
data-urls="{{item.images}}"
></image>
</view>
</view>
<!-- 帖子底部 -->
<view class="post-footer">
<view class="footer-left">
<view class="stat-item">
<text class="stat-icon">👀</text>
<text class="stat-text">{{item.views}}</text>
</view>
<view
class="stat-item like-item {{item.isLiked ? 'liked' : ''}}"
catchtap="onLike"
data-id="{{item.id}}"
>
<text class="stat-icon">{{item.isLiked ? '❤️' : '🤍'}}</text>
<text class="stat-text">{{item.likes}}</text>
</view>
<view class="stat-item">
<text class="stat-icon">💬</text>
<text class="stat-text">{{item.comments}}</text>
</view>
</view>
<view
class="favorite-btn {{item.isFavorite ? 'favorited' : ''}}"
catchtap="onFavorite"
data-id="{{item.id}}"
>
<text class="favorite-icon">{{item.isFavorite ? '⭐' : '☆'}}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{posts.length === 0}}">
<text class="empty-icon">📝</text>
<text class="empty-text">暂无帖子,快来发布第一条吧</text>
</view>
</view>
<!-- 发布按钮 -->
<view class="fab" bindtap="onNewPost">
<text class="fab-icon">✏️</text>
</view>
</view>

338
pages/forum/forum.wxss Normal file
View File

@@ -0,0 +1,338 @@
/* pages/forum/forum.wxss */
.container {
padding-bottom: 30rpx;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
animation: fadeIn 0.5s ease-out;
/* 底部留出TabBar的空间 */
padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx);
}
/* 分类标签 */
.category-scroll {
white-space: nowrap;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
padding: 24rpx 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
backdrop-filter: blur(10rpx);
animation: slideInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.category-list {
display: inline-flex;
padding: 0 30rpx;
gap: 20rpx;
}
.category-item {
display: inline-block;
padding: 14rpx 32rpx;
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
color: #666666;
border-radius: 32rpx;
font-size: 26rpx;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.category-item:active {
transform: scale(0.95);
}
.category-item.active {
background: linear-gradient(135deg, #50C878 0%, #3CB371 100%);
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(80, 200, 120, 0.3);
transform: scale(1.05);
}
/* 帖子列表 */
.post-list {
padding: 24rpx 30rpx;
}
.post-card {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 20rpx;
padding: 35rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
animation: slideInUp 0.6s ease-out;
position: relative;
overflow: hidden;
}
.post-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: linear-gradient(to bottom, #50C878, #3CB371);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.post-card:active {
transform: translateY(-4rpx);
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.12);
}
.post-card:active::before {
transform: scaleY(1);
}
.post-card:nth-child(1) { animation-delay: 0.1s; }
.post-card:nth-child(2) { animation-delay: 0.15s; }
.post-card:nth-child(3) { animation-delay: 0.2s; }
.post-card:nth-child(4) { animation-delay: 0.25s; }
.post-card:nth-child(5) { animation-delay: 0.3s; }
/* 帖子头部 */
.post-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 18rpx;
background: linear-gradient(135deg, #e0e0e0 0%, #c8c8c8 100%);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.avatar:active {
transform: scale(1.1);
}
.author-info {
flex: 1;
}
.author-name {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.post-time {
font-size: 24rpx;
color: #999999;
opacity: 0.8;
}
.category-tag {
padding: 10rpx 22rpx;
background: linear-gradient(135deg, #E8F8F0 0%, #D5F2E3 100%);
color: #50C878;
border-radius: 24rpx;
font-size: 22rpx;
font-weight: 600;
box-shadow: 0 2rpx 6rpx rgba(80, 200, 120, 0.2);
}
/* 帖子内容 */
.post-content {
margin-bottom: 24rpx;
}
.post-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
margin-bottom: 18rpx;
line-height: 1.6;
position: relative;
}
.post-text {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.post-images {
display: flex;
gap: 12rpx;
margin-top: 18rpx;
flex-wrap: wrap;
}
.post-image {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.post-image:active {
transform: scale(0.95);
}
/* 帖子底部 */
.post-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 24rpx;
border-top: 2rpx solid rgba(0, 0, 0, 0.05);
}
.footer-left {
display: flex;
align-items: center;
gap: 45rpx;
flex: 1;
}
.stat-item {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 24rpx;
color: #999999;
padding: 8rpx 16rpx;
border-radius: 20rpx;
transition: all 0.3s ease;
}
.stat-item:active {
background-color: rgba(0, 0, 0, 0.03);
transform: scale(1.1);
}
.stat-icon {
font-size: 32rpx;
transition: all 0.3s ease;
}
.like-item.liked .stat-text {
color: #FF6B6B;
font-weight: bold;
}
.like-item.liked .stat-icon {
animation: pulse 0.6s ease;
}
/* 收藏按钮 */
.favorite-btn {
display: flex;
align-items: center;
justify-content: center;
width: 68rpx;
height: 68rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.favorite-btn:active {
transform: scale(0.9) rotate(72deg);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.favorite-btn.favorited {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.4);
}
.favorite-icon {
font-size: 38rpx;
transition: all 0.3s ease;
color: #999999;
}
.favorite-btn.favorited .favorite-icon {
color: #ffffff;
animation: bounce 0.6s ease;
}
/* 收藏动画 */
@keyframes bounce {
0%, 100% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(0.9);
}
75% {
transform: scale(1.1);
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 60rpx;
animation: fadeIn 0.8s ease-out;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 24rpx;
opacity: 0.4;
animation: pulse 2s ease-in-out infinite;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
/* 发布按钮 */
.fab {
position: fixed;
right: 30rpx;
bottom: calc(env(safe-area-inset-bottom) + 120rpx);
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background: linear-gradient(135deg, #50C878 0%, #3CB371 100%);
box-shadow: 0 12rpx 32rpx rgba(80, 200, 120, 0.5);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
animation: scaleIn 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.5s both;
z-index: 999;
}
.fab:active {
transform: scale(0.88) rotate(90deg);
box-shadow: 0 6rpx 16rpx rgba(80, 200, 120, 0.5);
}
.fab-icon {
font-size: 52rpx;
color: #ffffff;
transition: all 0.3s ease;
}
.fab:active .fab-icon {
transform: rotate(-90deg);
}

150
pages/gpa/gpa.js Normal file
View File

@@ -0,0 +1,150 @@
// pages/gpa/gpa.js
const { calculateGPA, showSuccess, showError } = require('../../utils/util.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
courses: [],
courseName: '',
courseScore: '',
courseCredit: '',
totalGPA: 0,
totalCredits: 0
},
onLoad() {
this.loadCourses()
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('tools')
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 加载已保存的课程
loadCourses() {
const courses = wx.getStorageSync('gpaCourses') || []
this.setData({ courses })
this.calculateTotal()
},
// 课程名称输入
onNameInput(e) {
this.setData({ courseName: e.detail.value })
},
// 成绩输入
onScoreInput(e) {
this.setData({ courseScore: e.detail.value })
},
// 学分输入
onCreditInput(e) {
this.setData({ courseCredit: e.detail.value })
},
// 添加课程
onAddCourse() {
const { courseName, courseScore, courseCredit, courses } = this.data
if (!courseName.trim()) {
showError('请输入课程名称')
return
}
const score = parseFloat(courseScore)
const credit = parseFloat(courseCredit)
if (isNaN(score) || score < 0 || score > 100) {
showError('请输入有效的成绩0-100')
return
}
if (isNaN(credit) || credit <= 0) {
showError('请输入有效的学分')
return
}
const newCourse = {
id: Date.now(),
name: courseName.trim(),
score: score,
credit: credit
}
courses.push(newCourse)
wx.setStorageSync('gpaCourses', courses)
this.setData({
courses,
courseName: '',
courseScore: '',
courseCredit: ''
})
this.calculateTotal()
showSuccess('添加成功')
},
// 删除课程
onDeleteCourse(e) {
const { id } = e.currentTarget.dataset
wx.showModal({
title: '确认删除',
content: '确定要删除这门课程吗?',
success: (res) => {
if (res.confirm) {
let { courses } = this.data
courses = courses.filter(course => course.id !== id)
wx.setStorageSync('gpaCourses', courses)
this.setData({ courses })
this.calculateTotal()
showSuccess('删除成功')
}
}
})
},
// 计算总GPA
calculateTotal() {
const { courses } = this.data
const gpa = calculateGPA(courses)
const totalCredits = courses.reduce((sum, course) => sum + course.credit, 0)
this.setData({
totalGPA: gpa,
totalCredits: totalCredits
})
},
// 清空所有
onClearAll() {
wx.showModal({
title: '确认清空',
content: '确定要清空所有课程吗?',
success: (res) => {
if (res.confirm) {
wx.removeStorageSync('gpaCourses')
this.setData({
courses: [],
totalGPA: 0,
totalCredits: 0
})
showSuccess('已清空')
}
}
})
}
})

3
pages/gpa/gpa.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "GPA计算器"
}

72
pages/gpa/gpa.wxml Normal file
View File

@@ -0,0 +1,72 @@
<!--pages/gpa/gpa.wxml-->
<view class="container">
<!-- GPA显示卡片 -->
<view class="gpa-card">
<view class="gpa-label">当前GPA</view>
<view class="gpa-value">{{totalGPA}}</view>
<view class="credits-info">总学分:{{totalCredits}}</view>
</view>
<!-- 添加课程表单 -->
<view class="form-card">
<view class="form-title">添加课程</view>
<view class="form-row">
<input
class="form-input"
placeholder="课程名称"
value="{{courseName}}"
bindinput="onNameInput"
/>
</view>
<view class="form-row-group">
<view class="form-row half">
<input
class="form-input"
type="digit"
placeholder="成绩"
value="{{courseScore}}"
bindinput="onScoreInput"
/>
</view>
<view class="form-row half">
<input
class="form-input"
type="digit"
placeholder="学分"
value="{{courseCredit}}"
bindinput="onCreditInput"
/>
</view>
</view>
<button class="add-btn" bindtap="onAddCourse">添加课程</button>
</view>
<!-- 课程列表 -->
<view class="courses-section" wx:if="{{courses.length > 0}}">
<view class="section-header">
<text class="section-title">课程列表</text>
<text class="clear-btn" bindtap="onClearAll">清空</text>
</view>
<view class="course-item" wx:for="{{courses}}" wx:key="id">
<view class="course-info">
<view class="course-name">{{item.name}}</view>
<view class="course-details">
<text class="detail-item">成绩:{{item.score}}</text>
<text class="detail-item">学分:{{item.credit}}</text>
</view>
</view>
<view
class="delete-btn"
data-id="{{item.id}}"
bindtap="onDeleteCourse"
>
🗑️
</view>
</view>
</view>
</view>

328
pages/gpa/gpa.wxss Normal file
View File

@@ -0,0 +1,328 @@
/* pages/gpa/gpa.wxss */
.container {
min-height: 100vh;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 30rpx;
animation: fadeIn 0.5s ease-out;
}
/* GPA卡片 */
.gpa-card {
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E8E 100%);
border-radius: 24rpx;
padding: 50rpx 30rpx;
text-align: center;
color: #ffffff;
margin-bottom: 30rpx;
box-shadow: 0 12rpx 32rpx rgba(255, 107, 107, 0.4);
animation: scaleIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.gpa-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 30rpx); }
}
.gpa-label {
font-size: 28rpx;
opacity: 0.95;
margin-bottom: 15rpx;
position: relative;
letter-spacing: 2rpx;
}
.gpa-value {
font-size: 96rpx;
font-weight: bold;
margin-bottom: 15rpx;
position: relative;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
animation: pulse 2s ease-in-out infinite;
}
.credits-info {
font-size: 26rpx;
opacity: 0.9;
position: relative;
}
/* 表单卡片 */
.form-card {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 20rpx;
padding: 35rpx;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
animation: slideInUp 0.6s ease-out 0.2s both;
}
.form-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
margin-bottom: 30rpx;
position: relative;
padding-bottom: 15rpx;
}
.form-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60rpx;
height: 4rpx;
background: linear-gradient(to right, #FF6B6B, #FF8E8E);
border-radius: 2rpx;
}
.form-row {
margin-bottom: 25rpx;
}
.form-row-group {
display: flex;
gap: 20rpx;
margin-bottom: 25rpx;
}
.form-row.half {
flex: 1;
margin-bottom: 0;
}
.form-input {
width: 100%;
height: 88rpx;
padding: 0 28rpx;
background-color: #f8f9fa;
border: 2rpx solid #e9ecef;
border-radius: 16rpx;
font-size: 28rpx;
box-sizing: border-box;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.form-input:focus {
background-color: #ffffff;
border-color: #FF6B6B;
box-shadow: 0 0 0 4rpx rgba(255, 107, 107, 0.1);
}
.add-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E8E 100%);
color: #ffffff;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: bold;
box-shadow: 0 8rpx 20rpx rgba(255, 107, 107, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.add-btn:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
}
/* 课程列表 */
.courses-section {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 20rpx;
padding: 35rpx;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
animation: slideInUp 0.6s ease-out 0.3s both;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
position: relative;
padding-bottom: 10rpx;
}
.section-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 50rpx;
height: 3rpx;
background: linear-gradient(to right, #FF6B6B, #FF8E8E);
border-radius: 2rpx;
}
.clear-btn {
font-size: 26rpx;
color: #FF6B6B;
padding: 12rpx 24rpx;
background-color: rgba(255, 107, 107, 0.1);
border-radius: 20rpx;
transition: all 0.3s ease;
}
.clear-btn:active {
background-color: rgba(255, 107, 107, 0.2);
transform: scale(0.95);
}
.course-item {
display: flex;
align-items: center;
padding: 30rpx;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: slideInLeft 0.5s ease-out;
}
.course-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: linear-gradient(to bottom, #FF6B6B, #FF8E8E);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.course-item:active::before {
transform: scaleY(1);
}
.course-item:active {
transform: translateX(8rpx);
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1);
}
.course-item:last-child {
margin-bottom: 0;
}
.course-item:nth-child(1) { animation-delay: 0.1s; }
.course-item:nth-child(2) { animation-delay: 0.15s; }
.course-item:nth-child(3) { animation-delay: 0.2s; }
.course-item:nth-child(4) { animation-delay: 0.25s; }
.course-item:nth-child(5) { animation-delay: 0.3s; }
.course-info {
flex: 1;
}
.course-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 12rpx;
}
.course-details {
display: flex;
gap: 30rpx;
}
.detail-item {
font-size: 24rpx;
color: #666666;
padding: 6rpx 16rpx;
background-color: rgba(255, 107, 107, 0.08);
border-radius: 12rpx;
}
.delete-btn {
font-size: 44rpx;
padding: 12rpx;
color: #ff4757;
transition: all 0.3s ease;
}
.delete-btn:active {
transform: scale(1.2) rotate(90deg);
color: #ff6b6b;
}
/* 提示卡片 */
.tips-card {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 20rpx;
padding: 35rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
animation: slideInUp 0.6s ease-out 0.4s both;
}
.tips-title {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
position: relative;
padding-left: 20rpx;
}
.tips-title::before {
content: '💡';
position: absolute;
left: 0;
top: -2rpx;
font-size: 32rpx;
}
.tips-content {
font-size: 26rpx;
color: #666666;
line-height: 2.2;
padding-left: 20rpx;
}
.tip-item {
margin-bottom: 12rpx;
position: relative;
padding-left: 24rpx;
animation: fadeIn 0.8s ease-out;
}
.tip-item::before {
content: '•';
position: absolute;
left: 0;
color: #FF6B6B;
font-weight: bold;
font-size: 32rpx;
line-height: 1.5;
}
.tip-item:nth-child(1) { animation-delay: 0.5s; }
.tip-item:nth-child(2) { animation-delay: 0.6s; }
.tip-item:nth-child(3) { animation-delay: 0.7s; }
.tip-item:nth-child(4) { animation-delay: 0.8s; }
.tip-item:nth-child(5) { animation-delay: 0.9s; }

283
pages/index/index.js Normal file
View File

@@ -0,0 +1,283 @@
// pages/index/index.js
const app = getApp()
const userManager = require('../../utils/userManager.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
userInfo: null,
features: [
{
id: 1,
name: '启思AI',
icon: '🤖',
desc: '启迪思维,智慧学习助手',
path: '/pages/ai-assistant/ai-assistant',
color: '#6C5CE7',
badge: 'AI',
badgeColor: '#A29BFE'
},
{
id: 2,
name: '学习数据',
icon: '📊',
desc: '可视化数据分析看板',
path: '/pages/dashboard/dashboard',
color: '#667eea',
badge: 'NEW',
badgeColor: '#4CAF50'
},
{
id: 3,
name: '课程中心',
icon: '📚',
desc: '海量课程资源,精准筛选',
path: '/pages/courses/courses',
color: '#4A90E2',
badge: 'HOT',
badgeColor: '#FF5252'
},
{
id: 4,
name: '学科论坛',
icon: '💬',
desc: '学术交流,思维碰撞',
path: '/pages/forum/forum',
color: '#50C878'
},
{
id: 5,
name: '学习工具',
icon: '🛠️',
desc: 'GPA计算、课表、倒计时',
path: '/pages/tools/tools',
color: '#9B59B6'
},
{
id: 6,
name: '个人中心',
icon: '👤',
desc: '个性设置,数据管理',
path: '/pages/my/my',
color: '#F39C12'
}
],
notices: [
'欢迎使用知芽小筑!',
'新增课程筛选功能,快来体验吧',
'学科论坛上线,与同学们一起交流学习'
],
currentNotice: 0
},
onLoad() {
// 获取用户信息
this.getUserInfo()
// 启动公告轮播
this.startNoticeCarousel()
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('index')
// 每次显示时刷新用户信息
this.getUserInfo()
// 更新自定义TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 0
})
}
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
// 清除定时器
if (this.noticeTimer) {
clearInterval(this.noticeTimer)
}
},
// 获取用户信息
getUserInfo() {
const userInfo = userManager.getUserInfo()
if (userInfo && userInfo.isLogin) {
this.setData({ userInfo })
} else {
// 未登录状态
this.setData({ userInfo: null })
}
},
// 退出登录
onLogout() {
wx.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
confirmText: '退出',
cancelText: '取消',
confirmColor: '#FF5252',
success: (res) => {
if (res.confirm) {
// 清除用户信息
userManager.clearUserInfo()
this.setData({ userInfo: null })
wx.showToast({
title: '已退出登录',
icon: 'success'
})
// 震动反馈
wx.vibrateShort({
type: 'light'
})
}
}
})
},
// 微信授权登录
onLogin() {
const that = this
// 显示加载提示
wx.showLoading({
title: '正在登录...',
mask: true
})
// 获取用户信息
wx.getUserProfile({
desc: '完善用户资料',
lang: 'zh_CN',
success: (res) => {
console.log('获取用户信息成功', res)
// 使用 userManager 保存用户信息
const userInfo = {
nickname: res.userInfo.nickName,
avatar: res.userInfo.avatarUrl,
gender: res.userInfo.gender,
country: res.userInfo.country,
province: res.userInfo.province,
city: res.userInfo.city,
isLogin: true,
loginTime: new Date().getTime()
}
// 保存用户信息(会自动处理字段兼容性)
userManager.saveUserInfo(userInfo)
that.setData({ userInfo })
console.log('登录成功,保存的用户信息:', userInfo)
// 隐藏加载提示
wx.hideLoading()
// 显示成功提示
wx.showToast({
title: `欢迎,${userInfo.nickname}`,
icon: 'success',
duration: 2000
})
// 震动反馈
wx.vibrateShort({
type: 'light'
})
},
fail: (err) => {
console.error('获取用户信息失败', err)
// 隐藏加载提示
wx.hideLoading()
// 显示错误提示
wx.showModal({
title: '登录提示',
content: '登录已取消,部分功能可能受限',
showCancel: false,
confirmText: '我知道了',
confirmColor: '#667eea'
})
}
})
},
// 公告轮播
startNoticeCarousel() {
this.noticeTimer = setInterval(() => {
const { currentNotice, notices } = this.data
this.setData({
currentNotice: (currentNotice + 1) % notices.length
})
}, 3000)
},
// 功能卡片点击
onFeatureClick(e) {
console.log('功能卡片被点击', e)
const { path } = e.currentTarget.dataset
console.log('跳转路径:', path)
if (!path) {
wx.showToast({
title: '路径配置错误',
icon: 'none'
})
return
}
// TabBar 页面列表
const tabBarPages = [
'/pages/index/index',
'/pages/courses/courses',
'/pages/tools/tools',
'/pages/forum/forum',
'/pages/my/my'
]
// 判断是否为 TabBar 页面
if (tabBarPages.includes(path)) {
wx.switchTab({
url: path,
success: () => {
console.log('切换TabBar成功:', path)
},
fail: (err) => {
console.error('TabBar切换失败:', err)
wx.showToast({
title: '页面切换失败',
icon: 'none'
})
}
})
} else {
wx.navigateTo({
url: path,
success: () => {
console.log('跳转成功:', path)
},
fail: (err) => {
console.error('页面跳转失败:', err)
wx.showToast({
title: '页面跳转失败',
icon: 'none'
})
}
})
}
}
})

3
pages/index/index.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "知芽小筑"
}

119
pages/index/index.wxml Normal file
View File

@@ -0,0 +1,119 @@
<!--pages/index/index.wxml-->
<view class="container">
<!-- 顶部欢迎区 -->
<view class="header">
<view class="welcome-section">
<!-- 已登录状态 -->
<view class="user-info" wx:if="{{userInfo && userInfo.isLogin}}">
<image class="user-avatar" src="{{userInfo.avatar}}" mode="aspectFill" />
<view class="user-content">
<view class="greeting">
<text class="greeting-text">你好!</text>
<text class="greeting-name">{{userInfo.nickname}}</text>
</view>
<view class="slogan">知芽小筑,智启未来</view>
</view>
<view class="logout-btn" bindtap="onLogout">
<text class="logout-icon">🚪</text>
</view>
</view>
<!-- 未登录状态 -->
<view class="login-section" wx:else>
<view class="login-card">
<view class="login-header">
<text class="header-icon">🎓</text>
<text class="header-title">知芽小筑</text>
<text class="header-subtitle">Your Smart Campus Companion</text>
</view>
<view class="login-prompt">
<text class="prompt-icon">👋</text>
<view class="prompt-content">
<text class="prompt-text">Hi欢迎来到知芽小筑</text>
<text class="prompt-desc">登录后解锁更多精彩功能</text>
</view>
</view>
<button class="login-btn" bindtap="onLogin">
<view class="btn-content">
<text class="login-icon">🔐</text>
<view class="btn-text-group">
<text class="login-text">微信快捷登录</text>
</view>
</view>
<text class="arrow-icon">→</text>
</button>
<view class="login-features">
<view class="feature-item">
<text class="feature-icon">✨</text>
<text class="feature-text">个性化推荐</text>
</view>
<view class="feature-item">
<text class="feature-icon">🔒</text>
<text class="feature-text">数据安全</text>
</view>
<view class="feature-item">
<text class="feature-icon">⚡</text>
<text class="feature-text">快速便捷</text>
</view>
</view>
<view class="login-tip">
<text class="tip-icon"></text>
<text class="tip-text">我们承诺保护您的隐私安全</text>
</view>
</view>
</view>
</view>
</view>
<!-- 公告栏 -->
<view class="notice-bar">
<view class="notice-icon">📢</view>
<swiper class="notice-swiper" vertical="{{true}}" autoplay="{{true}}" circular="{{true}}" interval="3000">
<swiper-item wx:for="{{notices}}" wx:key="index">
<text class="notice-text">{{item}}</text>
</swiper-item>
</swiper>
</view>
<!-- 功能模块 -->
<view class="features-section">
<view class="section-title">
<text class="title-text">功能中心</text>
<view class="title-line"></view>
</view>
<view class="features-grid">
<view
class="feature-card"
wx:for="{{features}}"
wx:key="id"
data-path="{{item.path}}"
bindtap="onFeatureClick"
>
<view class="card-bg" style="background: linear-gradient(135deg, {{item.color}}15 0%, {{item.color}}05 100%);"></view>
<view class="feature-header">
<view class="feature-icon" style="background: linear-gradient(135deg, {{item.color}} 0%, {{item.color}}CC 100%);">
<text class="icon-text">{{item.icon}}</text>
</view>
<view class="badge" wx:if="{{item.badge}}" style="background-color: {{item.badgeColor || '#FF5252'}};">{{item.badge}}</view>
</view>
<view class="feature-info">
<view class="feature-name">{{item.name}}</view>
<view class="feature-desc">{{item.desc}}</view>
</view>
<view class="card-footer">
<text class="footer-text">立即使用</text>
<text class="footer-arrow">→</text>
</view>
</view>
</view>
</view>
</view>

643
pages/index/index.wxss Normal file
View File

@@ -0,0 +1,643 @@
/* pages/index/index.wxss */
.container {
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
animation: gradient-shift 15s ease infinite;
/* 底部留出TabBar的空间 */
padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx);
}
@keyframes gradient-shift {
0%, 100% {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
50% {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
}
/* 头部区域 */
.header {
padding: 60rpx 30rpx 40rpx;
background: transparent;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: float 20s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(-20rpx, -20rpx) rotate(180deg);
}
}
.welcome-section {
color: #ffffff;
position: relative;
z-index: 1;
}
/* 已登录状态 */
.user-info {
display: flex;
align-items: center;
gap: 20rpx;
animation: slideInLeft 0.6s ease-out;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
}
.user-content {
flex: 1;
}
.logout-btn {
width: 70rpx;
height: 70rpx;
border-radius: 35rpx;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10rpx);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
border: 2rpx solid rgba(255, 255, 255, 0.3);
}
.logout-btn:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.95);
}
.logout-icon {
font-size: 36rpx;
}
/* 未登录状态 */
.login-section {
padding: 20rpx 0;
animation: slideInUp 0.6s ease-out;
}
.login-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%);
backdrop-filter: blur(20rpx);
border-radius: 30rpx;
padding: 50rpx 40rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.login-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(102, 126, 234, 0.08) 0%, transparent 70%);
animation: rotate-slow 30s linear infinite;
}
@keyframes rotate-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.login-header {
text-align: center;
margin-bottom: 40rpx;
position: relative;
z-index: 1;
}
.header-icon {
font-size: 80rpx;
display: block;
margin-bottom: 20rpx;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
.header-title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #667eea;
margin-bottom: 10rpx;
letter-spacing: 2rpx;
}
.header-subtitle {
display: block;
font-size: 22rpx;
color: #999;
font-style: italic;
}
.login-prompt {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 35rpx;
padding: 25rpx;
background: linear-gradient(135deg, #F0F4FF 0%, #E8EFFF 100%);
border-radius: 20rpx;
border-left: 6rpx solid #667eea;
position: relative;
z-index: 1;
}
.prompt-icon {
font-size: 56rpx;
animation: wave 2s ease-in-out infinite;
flex-shrink: 0;
}
@keyframes wave {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-20deg); }
75% { transform: rotate(20deg); }
}
.prompt-content {
flex: 1;
}
.prompt-text {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.prompt-desc {
display: block;
font-size: 24rpx;
color: #666;
}
.login-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 25rpx;
padding: 0;
height: 130rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 35rpx;
font-size: 32rpx;
font-weight: bold;
box-shadow: 0 15rpx 40rpx rgba(102, 126, 234, 0.35);
border: none;
margin-bottom: 35rpx;
position: relative;
z-index: 1;
overflow: hidden;
transition: all 0.3s ease;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.6s ease;
}
.login-btn:active::before {
left: 100%;
}
.login-btn::after {
border: none;
}
.login-btn:active {
transform: scale(0.98);
box-shadow: 0 10rpx 30rpx rgba(102, 126, 234, 0.4);
}
.btn-content {
display: flex;
align-items: center;
gap: 20rpx;
}
.login-icon {
font-size: 44rpx;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.btn-text-group {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.login-text {
font-size: 32rpx;
font-weight: bold;
display: block;
}
.login-subtext {
font-size: 22rpx;
opacity: 0.9;
display: block;
margin-top: 4rpx;
}
.arrow-icon {
font-size: 40rpx;
font-weight: bold;
animation: arrow-move 1.5s ease-in-out infinite;
}
@keyframes arrow-move {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(8rpx); }
}
.login-features {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
padding: 0 20rpx;
position: relative;
z-index: 1;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
.feature-icon {
font-size: 36rpx;
display: block;
}
.feature-text {
font-size: 22rpx;
color: #666;
font-weight: 500;
}
.login-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 20rpx;
background: rgba(102, 126, 234, 0.05);
border-radius: 15rpx;
position: relative;
z-index: 1;
}
.tip-icon {
font-size: 28rpx;
color: #667eea;
}
.tip-text {
font-size: 22rpx;
color: #999;
}
.greeting {
display: flex;
align-items: baseline;
margin-bottom: 15rpx;
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30rpx);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.greeting-text {
font-size: 32rpx;
margin-right: 10rpx;
opacity: 0.95;
}
.greeting-name {
font-size: 40rpx;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
}
.slogan {
font-size: 28rpx;
opacity: 0.9;
letter-spacing: 2rpx;
animation: slideInLeft 0.6s ease-out 0.2s both;
}
/* 公告栏 */
.notice-bar {
display: flex;
align-items: center;
background: linear-gradient(135deg, #FFF9E6 0%, #FFF3CD 100%);
padding: 20rpx 30rpx;
margin: 0 30rpx 30rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.2);
animation: slideInDown 0.6s ease-out 0.3s both;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notice-icon {
font-size: 32rpx;
margin-right: 15rpx;
animation: bell-ring 2s ease-in-out infinite;
}
@keyframes bell-ring {
0%, 100% {
transform: rotate(0deg);
}
10%, 30% {
transform: rotate(-10deg);
}
20%, 40% {
transform: rotate(10deg);
}
50% {
transform: rotate(0deg);
}
}
.notice-swiper {
flex: 1;
height: 40rpx;
}
.notice-text {
font-size: 26rpx;
color: #856404;
line-height: 40rpx;
font-weight: 500;
}
/* 功能区 */
.features-section {
background-color: #f8f9fa;
border-radius: 30rpx 30rpx 0 0;
padding: 40rpx 30rpx;
min-height: calc(100vh - 400rpx);
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.08);
animation: slideInUp 0.6s ease-out 0.4s both;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-title {
margin-bottom: 30rpx;
}
.title-text {
font-size: 36rpx;
font-weight: bold;
color: #333333;
position: relative;
display: inline-block;
}
.title-line {
width: 60rpx;
height: 6rpx;
background: linear-gradient(to right, #667eea, #764ba2);
border-radius: 3rpx;
margin-top: 10rpx;
animation: expand 0.6s ease-out;
}
@keyframes expand {
from {
width: 0;
}
to {
width: 60rpx;
}
}
.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 25rpx;
}
.feature-card {
background: #ffffff;
border-radius: 25rpx;
padding: 0;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.08);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: fadeInScale 0.6s ease-out both;
display: flex;
flex-direction: column;
}
.card-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: opacity 0.3s ease;
}
.feature-card:active .card-bg {
opacity: 0.5;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9) translateY(20rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.feature-card:nth-child(1) { animation-delay: 0.1s; }
.feature-card:nth-child(2) { animation-delay: 0.15s; }
.feature-card:nth-child(3) { animation-delay: 0.2s; }
.feature-card:nth-child(4) { animation-delay: 0.25s; }
.feature-card:nth-child(5) { animation-delay: 0.3s; }
.feature-card:nth-child(6) { animation-delay: 0.35s; }
.feature-card:active {
transform: translateY(-8rpx) scale(1.02);
box-shadow: 0 20rpx 50rpx rgba(102, 126, 234, 0.25);
}
.feature-header {
position: relative;
z-index: 1;
padding: 30rpx 25rpx 20rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.feature-icon {
width: 90rpx;
height: 90rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease;
}
.feature-card:active .feature-icon {
transform: scale(1.1) rotate(-5deg);
}
.icon-text {
font-size: 50rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0,0,0,0.1));
}
.badge {
position: absolute;
top: 25rpx;
right: 25rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
color: #fff;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
animation: badge-pulse 2s ease-in-out infinite;
}
@keyframes badge-pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.feature-info {
position: relative;
z-index: 1;
padding: 0 25rpx 20rpx;
flex: 1;
}
.feature-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 12rpx;
position: relative;
}
.feature-desc {
font-size: 24rpx;
color: #999;
line-height: 1.6;
}
.card-footer {
position: relative;
z-index: 1;
padding: 20rpx 25rpx;
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(0, 0, 0, 0.02);
}
.footer-text {
font-size: 24rpx;
color: #667eea;
font-weight: 600;
}
.footer-arrow {
font-size: 28rpx;
color: #667eea;
font-weight: bold;
transition: transform 0.3s ease;
}
.feature-card:active .footer-arrow {
transform: translateX(6rpx);
}

726
pages/my/my.js Normal file
View File

@@ -0,0 +1,726 @@
// pages/my/my.js
const userManager = require('../../utils/userManager.js')
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
userInfo: null,
stats: {
favoriteCourses: 0,
myPosts: 0,
myComments: 0
},
menuList: [
{
id: 1,
icon: '✏️',
title: '编辑资料',
desc: '修改昵称和头像',
arrow: true,
type: 'edit'
},
{
id: 2,
icon: '❤️',
title: '我的收藏',
desc: '收藏的课程和帖子',
arrow: true,
type: 'favorite'
},
{
id: 3,
icon: '📝',
title: '我的帖子',
desc: '查看发布的帖子',
arrow: true,
type: 'posts'
},
{
id: 4,
icon: '🔔',
title: '消息通知',
desc: '系统消息和互动提醒',
arrow: true,
type: 'notification'
},
{
id: 5,
icon: '⚙️',
title: '通用设置',
desc: '隐私、通知等设置',
arrow: true,
type: 'settings'
},
{
id: 6,
icon: '❓',
title: '帮助与反馈',
desc: '使用指南和问题反馈',
arrow: true,
type: 'help'
},
{
id: 7,
icon: '📊',
title: '数据统计',
desc: '学习数据和使用记录',
arrow: true,
type: 'statistics'
},
{
id: 8,
icon: '🚪',
title: '退出登录',
desc: '退出当前账号',
arrow: false,
type: 'logout',
danger: true
}
],
showEditDialog: false,
editNickname: ''
},
onLoad() {
this.loadUserInfo()
this.loadStats()
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('tools')
this.loadUserInfo()
this.loadStats()
// 更新自定义TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 4
})
}
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 获取用户信息
loadUserInfo() {
const userInfo = userManager.getUserInfo()
this.setData({
userInfo: userInfo
})
console.log('加载用户信息:', userInfo)
},
// 点击头像上传
onChooseAvatar() {
const that = this
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success(res) {
const tempFilePath = res.tempFilePaths[0]
// 更新头像
userManager.updateAvatar(tempFilePath)
// 更新页面显示
const updatedUserInfo = userManager.getUserInfo()
that.setData({
userInfo: updatedUserInfo
})
console.log('头像更新成功,保存的数据:', updatedUserInfo)
wx.showToast({
title: '头像更新成功',
icon: 'success'
})
},
fail() {
wx.showToast({
title: '取消选择',
icon: 'none'
})
}
})
},
// 显示编辑昵称对话框
showEditNicknameDialog() {
const { userInfo } = this.data
this.setData({
showEditDialog: true,
editNickname: userInfo.nickname || ''
})
},
// 关闭编辑对话框
closeEditDialog() {
this.setData({
showEditDialog: false,
editNickname: ''
})
},
// 昵称输入
onNicknameInput(e) {
this.setData({
editNickname: e.detail.value
})
},
// 保存昵称
saveNickname() {
const { editNickname } = this.data
if (!editNickname.trim()) {
wx.showToast({
title: '请输入昵称',
icon: 'none'
})
return
}
if (editNickname.length > 10) {
wx.showToast({
title: '昵称最多10个字',
icon: 'none'
})
return
}
// 更新昵称
userManager.updateNickname(editNickname.trim())
// 更新页面显示
const updatedUserInfo = userManager.getUserInfo()
this.setData({
userInfo: updatedUserInfo,
showEditDialog: false,
editNickname: ''
})
console.log('昵称更新成功,保存的数据:', updatedUserInfo)
wx.showToast({
title: '昵称修改成功',
icon: 'success'
})
},
// 加载统计数据
loadStats() {
const userInfo = userManager.getUserInfo()
const favoriteCourses = wx.getStorageSync('favoriteCourses') || []
const forumPosts = wx.getStorageSync('forumPosts') || []
// 获取当前用户的昵称
const currentUserName = userInfo.nickname || '同学'
// 统计我的帖子
const myPosts = forumPosts.filter(post => {
return post.author === currentUserName || post.author === '我'
})
// 统计我的评论(从帖子详情中统计)
let totalComments = 0
forumPosts.forEach(post => {
if (post.commentList && Array.isArray(post.commentList)) {
const myComments = post.commentList.filter(comment => {
return comment.author === currentUserName || comment.author === '我'
})
totalComments += myComments.length
}
})
this.setData({
stats: {
favoriteCourses: favoriteCourses.length,
myPosts: myPosts.length,
myComments: totalComments
}
})
console.log('统计数据更新:', {
用户名: currentUserName,
收藏课程: favoriteCourses.length,
发布帖子: myPosts.length,
评论数: totalComments
})
},
// 菜单点击
onMenuClick(e) {
const { id } = e.currentTarget.dataset
const menuItem = this.data.menuList.find(item => item.id === id)
if (!menuItem) return
switch(menuItem.type) {
case 'edit':
// 编辑资料
this.showEditNicknameDialog()
break
case 'favorite':
// 我的收藏
this.showMyFavorites()
break
case 'posts':
// 我的帖子
this.showMyPosts()
break
case 'notification':
// 消息通知
this.showNotifications()
break
case 'settings':
// 通用设置
this.showSettings()
break
case 'help':
// 帮助与反馈
this.showHelp()
break
case 'statistics':
// 数据统计
this.showStatistics()
break
case 'logout':
// 退出登录
this.handleLogout()
break
default:
wx.showToast({
title: '功能开发中',
icon: 'none'
})
}
},
// 我的收藏
showMyFavorites() {
const favoriteCourseIds = wx.getStorageSync('favoriteCourses') || []
const favoritePosts = wx.getStorageSync('favoritePosts') || []
// 从课程数据中获取收藏的课程详细信息
const { coursesData } = require('../../utils/data.js')
const favoriteCourses = coursesData.filter(course =>
favoriteCourseIds.includes(course.id)
)
const totalFavorites = favoriteCourses.length + favoritePosts.length
if (totalFavorites === 0) {
wx.showModal({
title: '我的收藏',
content: '暂无收藏内容\n\n可以收藏\n• 喜欢的课程\n• 有用的论坛帖子',
showCancel: false,
confirmText: '去看看',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/courses/courses' })
}
}
})
} else {
wx.showActionSheet({
itemList: ['查看收藏的课程', '查看收藏的帖子', '查看所有收藏', '取消'],
success: (res) => {
if (res.tapIndex === 0) {
// 查看课程收藏
this.showFavoriteCoursesList(favoriteCourses)
} else if (res.tapIndex === 1) {
// 查看帖子收藏
this.showFavoritePostsList(favoritePosts)
} else if (res.tapIndex === 2) {
// 查看所有收藏
this.showAllFavorites(favoriteCourses, favoritePosts)
}
}
})
}
},
// 显示所有收藏概览
showAllFavorites(favoriteCourses, favoritePosts) {
let contentLines = []
// 课程收藏
if (favoriteCourses.length > 0) {
contentLines.push(`📚 收藏课程 (${favoriteCourses.length}门)`)
favoriteCourses.slice(0, 5).forEach((course, index) => {
contentLines.push(`${index + 1}. ${course.name} - ${course.teacher}`)
})
if (favoriteCourses.length > 5) {
contentLines.push(` ...还有${favoriteCourses.length - 5}门课程`)
}
contentLines.push('')
}
// 帖子收藏 - 过滤有效数据
const validPosts = favoritePosts.filter(post => post && post.title)
if (validPosts.length > 0) {
contentLines.push(`📝 收藏帖子 (${validPosts.length}条)`)
validPosts.slice(0, 5).forEach((post, index) => {
contentLines.push(`${index + 1}. ${post.title}`)
})
if (validPosts.length > 5) {
contentLines.push(` ...还有${validPosts.length - 5}条帖子`)
}
}
wx.showModal({
title: `⭐ 我的收藏`,
content: contentLines.join('\n'),
showCancel: false,
confirmText: '知道了',
confirmColor: '#667eea'
})
},
// 显示收藏课程列表
showFavoriteCoursesList(favoriteCourses) {
if (favoriteCourses.length === 0) {
wx.showModal({
title: '收藏的课程',
content: '还没有收藏任何课程\n去课程中心看看吧',
showCancel: false,
confirmText: '去看看',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/courses/courses' })
}
}
})
return
}
const courseTitles = favoriteCourses.map(course =>
`${course.name} - ${course.teacher}`
)
wx.showActionSheet({
itemList: courseTitles,
success: (res) => {
const selectedCourse = favoriteCourses[res.tapIndex]
// 跳转到课程详情
wx.navigateTo({
url: `/pages/course-detail/course-detail?id=${selectedCourse.id}`
})
}
})
},
// 显示收藏帖子列表
showFavoritePostsList(favoritePosts) {
console.log('收藏的帖子数据:', favoritePosts)
console.log('收藏的帖子数量:', favoritePosts.length)
if (favoritePosts.length === 0) {
wx.showModal({
title: '收藏的帖子',
content: '还没有收藏任何帖子\n去论坛看看吧',
showCancel: false,
confirmText: '去看看',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/forum/forum' })
}
}
})
return
}
// 确保每个帖子都有title和id
const validPosts = favoritePosts.filter(post => post && post.title && post.id)
if (validPosts.length === 0) {
wx.showModal({
title: '收藏的帖子',
content: '收藏数据异常\n请重新收藏帖子',
showCancel: false,
confirmText: '知道了',
confirmColor: '#667eea'
})
return
}
const postTitles = validPosts.map(post => post.title)
wx.showActionSheet({
itemList: postTitles,
success: (res) => {
const selectedPost = validPosts[res.tapIndex]
console.log('选中的帖子:', selectedPost)
// 跳转到帖子详情
wx.navigateTo({
url: `/pages/forum-detail/forum-detail?id=${selectedPost.id}`
})
}
})
},
// 我的帖子
showMyPosts() {
const userInfo = userManager.getUserInfo()
const currentUserName = userInfo.nickname || '同学'
const forumPosts = wx.getStorageSync('forumPosts') || []
const myPosts = forumPosts.filter(post => {
return post.author === currentUserName || post.author === '我'
})
console.log('我的帖子查询', {
当前用户名: currentUserName,
总帖子数: forumPosts.length,
我的帖子数: myPosts.length,
我的帖子: myPosts.map(p => ({ 标题: p.title, 作者: p.author }))
})
if (myPosts.length === 0) {
wx.showModal({
title: '我的帖子',
content: '还没有发布过帖子\n去论坛分享你的想法吧',
showCancel: false,
confirmText: '去发帖',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
wx.switchTab({
url: '/pages/forum/forum'
})
}
}
})
} else {
// 显示帖子列表供用户选择
const postTitles = myPosts.map(post => {
const commentCount = post.comments || 0
const likeCount = post.likes || 0
return `${post.title} (💬${commentCount} ❤️${likeCount})`
})
wx.showActionSheet({
itemList: postTitles,
success: (res) => {
const selectedPost = myPosts[res.tapIndex]
// 跳转到帖子详情
wx.navigateTo({
url: `/pages/forum-detail/forum-detail?id=${selectedPost.id}`
})
}
})
}
},
// 消息通知
showNotifications() {
wx.showModal({
title: '💌 消息通知',
content: '暂无新消息\n\n系统会在这里提醒您\n• 帖子的点赞和评论\n• 课程更新通知\n• 系统公告',
showCancel: false,
confirmText: '我知道了',
confirmColor: '#667eea'
})
},
// 通用设置
showSettings() {
const settingsContent = [
'⚙️ 通用设置',
'',
'✓ 消息推送:已开启',
'✓ 隐私保护:已开启',
'✓ 缓存管理:自动清理',
'✓ 深色模式:跟随系统',
'',
'点击确定返回'
].join('\n')
wx.showModal({
title: '设置中心',
content: settingsContent,
showCancel: false,
confirmText: '确定',
confirmColor: '#667eea'
})
},
// 帮助与反馈
showHelp() {
wx.showModal({
title: '📖 帮助中心',
content: '使用指南:\n\n1. 课程中心:浏览和收藏课程\n2. 学科论坛:发帖交流学习\n3. 学习工具GPA计算等工具\n4. 个人中心:管理个人信息\n\n遇到问题请联系管理员',
showCancel: true,
cancelText: '返回',
confirmText: '联系我们',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
this.showContactInfo()
}
}
})
},
// 显示联系方式
showContactInfo() {
wx.showModal({
title: '📞 联系我们',
content: '客服邮箱:\n19354618812@163.com\n\n客服电话\n19354618812\n\n工作时间\n周一至周五 9:00-18:00\n\n欢迎您的咨询和建议',
showCancel: true,
cancelText: '关闭',
confirmText: '复制邮箱',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
wx.setClipboardData({
data: '19354618812@163.com',
success: () => {
wx.showToast({
title: '邮箱已复制',
icon: 'success'
})
}
})
}
}
})
},
// 数据统计
showStatistics() {
const { stats } = this.data
const userInfo = userManager.getUserInfo()
const currentUserName = userInfo.nickname || '同学'
const forumPosts = wx.getStorageSync('forumPosts') || []
// 获取我的评论详情
let myCommentsList = []
forumPosts.forEach(post => {
if (post.commentList && Array.isArray(post.commentList)) {
post.commentList.forEach(comment => {
if (comment.author === currentUserName || comment.author === '我') {
myCommentsList.push({
postTitle: post.title,
content: comment.content,
time: comment.time
})
}
})
}
})
// 构建详细内容
let contentLines = [
'📊 我的数据',
'',
`📚 收藏课程:${stats.favoriteCourses}`,
`📝 发布帖子:${stats.myPosts}`,
`💬 评论数量:${stats.myComments}`,
''
]
// 显示最近的评论最多3条
if (myCommentsList.length > 0) {
contentLines.push('最近评论:')
myCommentsList.slice(0, 3).forEach((comment, index) => {
const shortContent = comment.content.length > 20
? comment.content.substring(0, 20) + '...'
: comment.content
contentLines.push(`${index + 1}. 在《${comment.postTitle}》中评论`)
contentLines.push(` "${shortContent}"`)
})
if (myCommentsList.length > 3) {
contentLines.push(` ...还有${myCommentsList.length - 3}条评论`)
}
} else {
contentLines.push('暂无评论记录')
}
contentLines.push('')
contentLines.push('持续学习,不断进步!')
wx.showModal({
title: '数据统计',
content: contentLines.join('\n'),
showCancel: false,
confirmText: '继续加油',
confirmColor: '#667eea'
})
},
// 退出登录
handleLogout() {
if (!userManager.isUserLogin()) {
wx.showToast({
title: '您还未登录',
icon: 'none'
})
return
}
wx.showModal({
title: '退出登录',
content: '确定要退出登录吗?\n退出后部分功能将无法使用',
confirmText: '退出',
cancelText: '取消',
confirmColor: '#FF5252',
success: (res) => {
if (res.confirm) {
// 清除登录状态
userManager.clearUserInfo()
// 更新页面数据
this.setData({
userInfo: userManager.getUserInfo()
})
wx.showToast({
title: '已退出登录',
icon: 'success',
duration: 2000
})
// 震动反馈
wx.vibrateShort({
type: 'light'
})
// 1秒后跳转到首页
setTimeout(() => {
wx.switchTab({
url: '/pages/index/index'
})
}, 1500)
}
}
})
},
// 阻止事件冒泡
doNothing() {
// 空函数,防止点击对话框内容时关闭对话框
}
})

3
pages/my/my.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "我的"
}

90
pages/my/my.wxml Normal file
View File

@@ -0,0 +1,90 @@
<!--pages/my/my.wxml-->
<view class="container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-header">
<view class="avatar-wrap" bindtap="onChooseAvatar">
<image class="avatar" src="{{userInfo ? userInfo.avatar : '/images/avatar-default.png'}}" mode="aspectFill"></image>
<view class="avatar-mask">
<text class="avatar-tip">📷</text>
</view>
</view>
<view class="user-info">
<view class="nickname">{{userInfo ? userInfo.nickname : '同学'}}</view>
<view class="user-desc">东北大学在校生</view>
</view>
<view class="edit-btn" bindtap="showEditNicknameDialog">
<text class="edit-icon">✏️</text>
</view>
</view>
<!-- 统计数据 -->
<view class="stats-row">
<view class="stat-item">
<view class="stat-value">{{stats.favoriteCourses}}</view>
<view class="stat-label">收藏课程</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-value">{{stats.myPosts}}</view>
<view class="stat-label">发布帖子</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-value">{{stats.myComments}}</view>
<view class="stat-label">评论数</view>
</view>
</view>
</view>
<!-- 菜单列表 -->
<view class="menu-section">
<view
class="menu-item"
wx:for="{{menuList}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onMenuClick"
>
<view class="menu-icon">{{item.icon}}</view>
<view class="menu-content">
<view class="menu-title" style="{{item.danger ? 'color: #FF5252;' : ''}}">{{item.title}}</view>
<view class="menu-desc">{{item.desc}}</view>
</view>
<view class="menu-arrow" wx:if="{{item.arrow}}"></view>
</view>
</view>
<!-- 关于信息 -->
<view class="about-section">
<view class="about-title">关于小程序</view>
<view class="about-content">
<view class="about-item">版本号v2.0.0</view>
<view class="about-item">开发团队:知芽小筑工作组</view>
<view class="about-item">主题:知芽小筑,智启未来</view>
</view>
</view>
<!-- 编辑昵称对话框 -->
<view class="modal-mask" wx:if="{{showEditDialog}}" bindtap="closeEditDialog">
<view class="modal-dialog" catchtap="doNothing">
<view class="modal-header">
<text class="modal-title">修改昵称</text>
<view class="modal-close" bindtap="closeEditDialog">✕</view>
</view>
<view class="modal-content">
<input
class="modal-input"
placeholder="请输入昵称最多10个字"
value="{{editNickname}}"
bindinput="onNicknameInput"
maxlength="10"
/>
</view>
<view class="modal-footer">
<button class="modal-btn cancel-btn" bindtap="closeEditDialog">取消</button>
<button class="modal-btn confirm-btn" bindtap="saveNickname">确定</button>
</view>
</view>
</view>
</view>

421
pages/my/my.wxss Normal file
View File

@@ -0,0 +1,421 @@
/* pages/my/my.wxss */
.container {
min-height: 100vh;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
animation: fadeIn 0.5s ease-out;
/* 底部留出TabBar的空间 */
padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx);
}
/* 用户卡片 */
.user-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 65rpx 30rpx 45rpx;
margin-bottom: 30rpx;
position: relative;
overflow: hidden;
animation: slideInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.user-card::before {
content: '';
position: absolute;
top: -50%;
right: -25%;
width: 400rpx;
height: 400rpx;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
border-radius: 50%;
animation: float 8s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-30rpx, 30rpx) scale(1.1); }
}
.user-header {
display: flex;
align-items: center;
margin-bottom: 45rpx;
position: relative;
}
.avatar-wrap {
position: relative;
margin-right: 28rpx;
}
.avatar {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
background-color: #ffffff;
border: 5rpx solid rgba(255, 255, 255, 0.4);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.avatar-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.avatar-wrap:active .avatar-mask {
background-color: rgba(0, 0, 0, 0.3);
}
.avatar-tip {
font-size: 48rpx;
opacity: 0;
transition: all 0.3s ease;
}
.avatar-wrap:active .avatar-tip {
opacity: 1;
}
.user-info {
flex: 1;
color: #ffffff;
position: relative;
}
.nickname {
font-size: 40rpx;
font-weight: bold;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.user-desc {
font-size: 26rpx;
opacity: 0.95;
letter-spacing: 1rpx;
}
.edit-btn {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10rpx);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.edit-btn:active {
transform: scale(1.1) rotate(15deg);
background-color: rgba(255, 255, 255, 0.3);
}
.edit-icon {
font-size: 36rpx;
}
/* 统计数据 */
.stats-row {
display: flex;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
padding: 35rpx 0;
backdrop-filter: blur(15rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
position: relative;
}
.stat-item {
flex: 1;
text-align: center;
color: #ffffff;
transition: all 0.3s ease;
cursor: pointer;
}
.stat-item:active {
transform: scale(1.1);
}
.stat-value {
font-size: 48rpx;
font-weight: bold;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
animation: pulse 2s ease-in-out infinite;
}
.stat-label {
font-size: 24rpx;
opacity: 0.95;
letter-spacing: 1rpx;
}
.stat-divider {
width: 2rpx;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent);
}
/* 菜单区域 */
.menu-section {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 20rpx;
margin: 0 30rpx 30rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
animation: slideInUp 0.6s ease-out 0.2s both;
}
.menu-item {
display: flex;
align-items: center;
padding: 35rpx;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.03);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.menu-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: linear-gradient(to bottom, #667eea, #764ba2);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
transform: translateX(8rpx);
}
.menu-item:active::before {
transform: scaleY(1);
}
.menu-icon {
font-size: 48rpx;
margin-right: 28rpx;
transition: all 0.3s ease;
}
.menu-item:active .menu-icon {
transform: scale(1.2) rotate(10deg);
}
.menu-content {
flex: 1;
}
.menu-title {
font-size: 32rpx;
color: #333333;
font-weight: 600;
margin-bottom: 10rpx;
}
.menu-desc {
font-size: 24rpx;
color: #999999;
opacity: 0.8;
}
.menu-arrow {
font-size: 32rpx;
color: #CCCCCC;
font-weight: 300;
transition: all 0.3s ease;
}
.menu-item:active .menu-arrow {
transform: translateX(8rpx);
color: #667eea;
}
/* 关于信息 */
.about-section {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 20rpx;
margin: 0 30rpx;
padding: 35rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
animation: slideInUp 0.6s ease-out 0.3s both;
}
.about-title {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
position: relative;
padding-left: 20rpx;
}
.about-title::before {
content: '📚';
position: absolute;
left: 0;
top: 0;
font-size: 32rpx;
}
.about-content {
font-size: 26rpx;
color: #666666;
line-height: 2.2;
padding-left: 20rpx;
}
.about-item {
margin-bottom: 14rpx;
position: relative;
padding-left: 24rpx;
animation: fadeIn 0.8s ease-out;
}
.about-item::before {
content: '•';
position: absolute;
left: 0;
color: #667eea;
font-weight: bold;
font-size: 32rpx;
line-height: 1.5;
}
.about-item:nth-child(1) { animation-delay: 0.4s; }
.about-item:nth-child(2) { animation-delay: 0.5s; }
.about-item:nth-child(3) { animation-delay: 0.6s; }
/* 编辑对话框 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.3s ease-out;
}
.modal-dialog {
width: 600rpx;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 24rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2);
animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 35rpx 35rpx 25rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.modal-close {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #999999;
border-radius: 50%;
transition: all 0.3s ease;
}
.modal-close:active {
background-color: #f5f5f5;
transform: rotate(90deg);
}
.modal-content {
padding: 35rpx;
}
.modal-input {
width: 100%;
height: 88rpx;
padding: 0 28rpx;
background-color: #f8f9fa;
border: 2rpx solid #e9ecef;
border-radius: 16rpx;
font-size: 30rpx;
box-sizing: border-box;
transition: all 0.3s ease;
}
.modal-input:focus {
background-color: #ffffff;
border-color: #667eea;
box-shadow: 0 0 0 4rpx rgba(102, 126, 234, 0.1);
}
.modal-footer {
display: flex;
gap: 20rpx;
padding: 25rpx 35rpx 35rpx;
}
.modal-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
}
.cancel-btn:active {
background-color: #e8e8e8;
transform: scale(0.98);
}
.confirm-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.3);
}
.confirm-btn:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}

120
pages/post/post.js Normal file
View File

@@ -0,0 +1,120 @@
// pages/post/post.js
const { forumData } = require('../../utils/data.js')
const { showSuccess, showError } = require('../../utils/util.js')
const learningTracker = require('../../utils/learningTracker.js')
// pages/post/post.js
const userManager = require('../../utils/userManager.js')
Page({
data: {
title: '',
content: '',
images: [],
categories: ['数学', '物理', '计算机', '英语', '其他'],
selectedCategory: 0
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('forum')
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 标题输入
onTitleInput(e) {
this.setData({ title: e.detail.value })
},
// 内容输入
onContentInput(e) {
this.setData({ content: e.detail.value })
},
// 选择分类
onCategoryChange(e) {
this.setData({ selectedCategory: parseInt(e.detail.value) })
},
// 选择图片
onChooseImage() {
const maxCount = 3 - this.data.images.length
wx.chooseImage({
count: maxCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
const images = this.data.images.concat(tempFilePaths)
this.setData({ images })
}
})
},
// 删除图片
onDeleteImage(e) {
const index = e.currentTarget.dataset.index
const images = this.data.images
images.splice(index, 1)
this.setData({ images })
},
// 发布帖子
onPublish() {
const { title, content, images, categories, selectedCategory } = this.data
if (!title.trim()) {
showError('请输入标题')
return
}
if (!content.trim()) {
showError('请输入内容')
return
}
// 获取用户信息
const userInfo = userManager.getUserInfo()
const userName = userInfo.nickname || '同学'
const userAvatar = userInfo.avatar || '/images/avatar-default.png'
// 创建新帖子
const newPost = {
id: Date.now(),
title: title.trim(),
content: content.trim(),
author: userName,
avatar: userAvatar,
category: categories[selectedCategory],
views: 0,
likes: 0,
comments: 0,
time: '刚刚',
images: images, // 保存图片数组
isLiked: false, // 确保默认未点赞
commentList: [] // 初始化评论列表
}
// 保存到本地存储
let posts = wx.getStorageSync('forumPosts') || forumData
posts.unshift(newPost)
wx.setStorageSync('forumPosts', posts)
showSuccess('发布成功')
// 返回论坛列表
setTimeout(() => {
wx.navigateBack()
}, 1000)
}
})

3
pages/post/post.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "发布帖子"
}

82
pages/post/post.wxml Normal file
View File

@@ -0,0 +1,82 @@
<!--pages/post/post.wxml-->
<view class="container">
<view class="form-section">
<!-- 分类选择 -->
<view class="form-item">
<view class="form-label">选择分类</view>
<picker
mode="selector"
range="{{categories}}"
value="{{selectedCategory}}"
bindchange="onCategoryChange"
>
<view class="picker-value">
{{categories[selectedCategory]}} ▼
</view>
</picker>
</view>
<!-- 标题输入 -->
<view class="form-item">
<view class="form-label">标题</view>
<input
class="title-input"
placeholder="请输入标题(必填)"
value="{{title}}"
bindinput="onTitleInput"
maxlength="50"
/>
<view class="char-count">{{title.length}}/50</view>
</view>
<!-- 内容输入 -->
<view class="form-item">
<view class="form-label">内容</view>
<textarea
class="content-textarea"
placeholder="请输入内容(必填)"
value="{{content}}"
bindinput="onContentInput"
maxlength="500"
auto-height
/>
<view class="char-count">{{content.length}}/500</view>
</view>
<!-- 图片上传 -->
<view class="form-item">
<view class="form-label">
<text>添加图片</text>
<text class="form-label-tip">最多3张</text>
</view>
<view class="image-upload-section">
<!-- 已上传的图片 -->
<view class="image-list">
<view class="image-item" wx:for="{{images}}" wx:key="index">
<image class="uploaded-image" src="{{item}}" mode="aspectFill" />
<view class="image-delete" bindtap="onDeleteImage" data-index="{{index}}">
<text class="delete-icon">✕</text>
</view>
</view>
<!-- 上传按钮 -->
<view class="image-upload-btn" wx:if="{{images.length < 3}}" bindtap="onChooseImage">
<text class="upload-icon">📷</text>
<text class="upload-text">添加图片</text>
</view>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tips">
<text class="tips-icon">💡</text>
<text class="tips-text">请文明发言,共同维护良好的交流环境</text>
</view>
</view>
<!-- 发布按钮 -->
<view class="action-section">
<button class="publish-btn" bindtap="onPublish">发布</button>
</view>
</view>

187
pages/post/post.wxss Normal file
View File

@@ -0,0 +1,187 @@
/* pages/post/post.wxss */
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding: 30rpx;
}
.form-section {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.form-item {
margin-bottom: 40rpx;
position: relative;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
}
.picker-value {
padding: 25rpx;
background-color: #F5F5F5;
border-radius: 12rpx;
font-size: 28rpx;
color: #333333;
}
.title-input {
width: 100%;
padding: 25rpx;
background-color: #F5F5F5;
border-radius: 12rpx;
font-size: 28rpx;
}
.content-textarea {
width: 100%;
min-height: 300rpx;
padding: 25rpx;
background-color: #F5F5F5;
border-radius: 12rpx;
font-size: 28rpx;
line-height: 1.8;
box-sizing: border-box;
}
.char-count {
position: absolute;
right: 0;
bottom: -35rpx;
font-size: 24rpx;
color: #999999;
}
/* 图片上传 */
.form-label-tip {
font-size: 24rpx;
color: #999999;
font-weight: normal;
margin-left: 10rpx;
}
.image-upload-section {
margin-top: 20rpx;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.image-item {
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
}
.uploaded-image {
width: 100%;
height: 100%;
}
.image-delete {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #FF6B6B, #E74C3C);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.4);
}
.delete-icon {
color: #ffffff;
font-size: 28rpx;
font-weight: bold;
line-height: 1;
}
.image-upload-btn {
width: 200rpx;
height: 200rpx;
border: 2rpx dashed #D0D0D0;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
background: linear-gradient(135deg, #F8F9FA, #E9ECEF);
transition: all 0.3s ease;
}
.image-upload-btn:active {
background: linear-gradient(135deg, #E9ECEF, #DEE2E6);
transform: scale(0.95);
}
.upload-icon {
font-size: 60rpx;
opacity: 0.5;
}
.upload-text {
font-size: 24rpx;
color: #999999;
}
.tips {
display: flex;
align-items: center;
gap: 10rpx;
padding: 20rpx;
background-color: #FFF9E6;
border-radius: 12rpx;
margin-top: 40rpx;
}
.tips-icon {
font-size: 32rpx;
}
.tips-text {
flex: 1;
font-size: 24rpx;
color: #856404;
line-height: 1.5;
}
/* 操作区 */
.action-section {
padding: 0 30rpx;
}
.publish-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
background: linear-gradient(135deg, #50C878, #3CB371);
color: #ffffff;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: bold;
box-shadow: 0 8rpx 24rpx rgba(80, 200, 120, 0.3);
}
.publish-btn:active {
opacity: 0.8;
}

421
pages/schedule/schedule.js Normal file
View File

@@ -0,0 +1,421 @@
// pages/schedule/schedule.js
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
weekDays: ['周一', '周二', '周三', '周四', '周五'],
timePeriods: ['1-2节', '3-4节', '5-6节', '7-8节', '9-10节'],
currentWeek: 1, // 当前查看的周次
totalWeeks: 20, // 总周数(一学期)
schedule: {},
scheduleData: [], // 用于视图渲染的二维数组
showEditModal: false,
editMode: 'add', // 'add' 或 'edit'
editDay: '',
editPeriod: '',
editCourse: {
name: '',
location: ''
}
},
onLoad() {
// 初始化当前周次(可以根据学期开始日期计算)
this.initCurrentWeek()
this.loadSchedule()
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('tools')
// 更新自定义TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 2
})
}
// 刷新当前周的课表
this.loadSchedule()
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
// 初始化当前周次
initCurrentWeek() {
// 从缓存获取学期开始日期
let semesterStartDate = wx.getStorageSync('semesterStartDate')
if (!semesterStartDate) {
// 如果没有设置默认为当前学期第1周
// 实际应用中可以让用户设置学期开始日期
this.setData({ currentWeek: 1 })
return
}
// 计算当前是第几周
const startDate = new Date(semesterStartDate)
const today = new Date()
const diffTime = Math.abs(today - startDate)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
const weekNumber = Math.ceil(diffDays / 7)
// 确保周数在有效范围内
const currentWeek = Math.min(Math.max(weekNumber, 1), this.data.totalWeeks)
this.setData({ currentWeek })
},
// 加载课程表
loadSchedule() {
const { currentWeek } = this.data
const allSchedules = wx.getStorageSync('allCourseSchedules') || {}
const savedSchedule = allSchedules[`week_${currentWeek}`] || {}
// 示例数据仅第1周有默认数据
let defaultSchedule = {}
if (currentWeek === 1 && Object.keys(savedSchedule).length === 0) {
defaultSchedule = {
'周一-1-2节': { name: '高等数学A', location: '主楼A201' },
'周一-3-4节': { name: '数据结构', location: '信息学馆301' },
'周二-1-2节': { name: '大学英语', location: '外语楼205' },
'周二-3-4节': { name: '大学物理', location: '主楼B305' },
'周三-1-2节': { name: '高等数学A', location: '主楼A201' },
'周三-5-6节': { name: 'Python程序设计', location: '实验室A' },
'周四-1-2节': { name: '大学物理', location: '主楼B305' },
'周四-5-6节': { name: '机器学习', location: '信息学馆505' },
'周五-1-2节': { name: '数据结构', location: '信息学馆301' }
}
}
const schedule = Object.keys(savedSchedule).length > 0 ? savedSchedule : defaultSchedule
// 构建二维数组用于渲染
const scheduleData = this.buildScheduleData(schedule)
this.setData({
schedule,
scheduleData
})
// 保存默认数据仅第1周
if (currentWeek === 1 && Object.keys(savedSchedule).length === 0 && Object.keys(defaultSchedule).length > 0) {
const allSchedules = wx.getStorageSync('allCourseSchedules') || {}
allSchedules[`week_${currentWeek}`] = defaultSchedule
wx.setStorageSync('allCourseSchedules', allSchedules)
}
},
// 上一周
onPrevWeek() {
const { currentWeek } = this.data
if (currentWeek > 1) {
this.setData({ currentWeek: currentWeek - 1 })
this.loadSchedule()
wx.showToast({
title: `${currentWeek - 1}`,
icon: 'none',
duration: 1000
})
} else {
wx.showToast({
title: '已经是第一周了',
icon: 'none',
duration: 1500
})
}
},
// 下一周
onNextWeek() {
const { currentWeek, totalWeeks } = this.data
if (currentWeek < totalWeeks) {
this.setData({ currentWeek: currentWeek + 1 })
this.loadSchedule()
wx.showToast({
title: `${currentWeek + 1}`,
icon: 'none',
duration: 1000
})
} else {
wx.showToast({
title: '已经是最后一周了',
icon: 'none',
duration: 1500
})
}
},
// 回到本周
onCurrentWeek() {
this.initCurrentWeek()
this.loadSchedule()
wx.showToast({
title: '已回到本周',
icon: 'success',
duration: 1000
})
},
// 构建课程表数据结构
buildScheduleData(schedule) {
const { weekDays, timePeriods } = this.data
const scheduleData = []
timePeriods.forEach(period => {
const row = {
period: period,
courses: []
}
weekDays.forEach(day => {
const key = `${day}-${period}`
const course = schedule[key] || null
row.courses.push({
day: day,
course: course,
hasCourse: !!course
})
})
scheduleData.push(row)
})
return scheduleData
},
// 点击添加课程按钮
onAddCourse() {
wx.showModal({
title: '添加课程',
content: '请点击课程表中的空白格子来添加课程',
showCancel: false,
confirmText: '知道了',
confirmColor: '#667eea'
})
},
// 点击课程格子
onCourseClick(e) {
const { day, period, course } = e.currentTarget.dataset
if (course) {
// 有课程,显示课程信息
wx.showModal({
title: course.name,
content: `上课时间:${day} ${period}\n上课地点:${course.location}`,
showCancel: true,
cancelText: '关闭',
confirmText: '编辑',
confirmColor: '#667eea',
success: (res) => {
if (res.confirm) {
this.openEditModal('edit', day, period, course)
}
}
})
} else {
// 无课程,添加课程
this.openEditModal('add', day, period, null)
}
},
// 长按课程格子
onCourseLongPress(e) {
const { day, period, course } = e.currentTarget.dataset
if (course) {
wx.showActionSheet({
itemList: ['编辑课程', '删除课程'],
success: (res) => {
if (res.tapIndex === 0) {
this.openEditModal('edit', day, period, course)
} else if (res.tapIndex === 1) {
this.deleteCourse(day, period)
}
}
})
}
},
// 打开编辑弹窗
openEditModal(mode, day, period, course) {
this.setData({
showEditModal: true,
editMode: mode,
editDay: day,
editPeriod: period,
editCourse: course ? { ...course } : { name: '', location: '' }
})
},
// 关闭编辑弹窗
closeEditModal() {
this.setData({
showEditModal: false,
editCourse: { name: '', location: '' }
})
},
// 课程名称输入
onCourseNameInput(e) {
this.setData({
'editCourse.name': e.detail.value
})
},
// 课程地点输入
onCourseLocationInput(e) {
this.setData({
'editCourse.location': e.detail.value
})
},
// 保存课程
onSaveCourse() {
const { editDay, editPeriod, editCourse, currentWeek } = this.data
if (!editCourse.name.trim()) {
wx.showToast({
title: '请输入课程名称',
icon: 'none'
})
return
}
if (!editCourse.location.trim()) {
wx.showToast({
title: '请输入上课地点',
icon: 'none'
})
return
}
const key = `${editDay}-${editPeriod}`
const schedule = this.data.schedule
schedule[key] = {
name: editCourse.name.trim(),
location: editCourse.location.trim()
}
// 保存到对应周次的课表
const allSchedules = wx.getStorageSync('allCourseSchedules') || {}
allSchedules[`week_${currentWeek}`] = schedule
wx.setStorageSync('allCourseSchedules', allSchedules)
// 更新视图
const scheduleData = this.buildScheduleData(schedule)
this.setData({
schedule,
scheduleData,
showEditModal: false,
editCourse: { name: '', location: '' }
})
wx.showToast({
title: '保存成功',
icon: 'success'
})
},
// 删除课程
onDeleteCourse() {
const { editDay, editPeriod } = this.data
wx.showModal({
title: '确认删除',
content: '确定要删除这门课程吗?',
confirmColor: '#FF5252',
success: (res) => {
if (res.confirm) {
this.deleteCourse(editDay, editPeriod)
this.closeEditModal()
}
}
})
},
// 删除课程逻辑
deleteCourse(day, period) {
const { currentWeek } = this.data
const key = `${day}-${period}`
const schedule = this.data.schedule
delete schedule[key]
// 保存到对应周次的课表
const allSchedules = wx.getStorageSync('allCourseSchedules') || {}
allSchedules[`week_${currentWeek}`] = schedule
wx.setStorageSync('allCourseSchedules', allSchedules)
// 更新视图
const scheduleData = this.buildScheduleData(schedule)
this.setData({
schedule,
scheduleData
})
wx.showToast({
title: '已删除',
icon: 'success'
})
},
// 复制当前周课表到其他周
onCopySchedule() {
const { currentWeek, schedule } = this.data
// 如果当前周没有课表,提示用户
if (Object.keys(schedule).length === 0) {
wx.showToast({
title: '当前周没有课程',
icon: 'none',
duration: 2000
})
return
}
wx.showModal({
title: '复制课表',
content: `是否将第${currentWeek}周的课表复制到所有周?(不会覆盖已有课程的周)`,
confirmText: '复制',
cancelText: '取消',
confirmColor: '#9B59B6',
success: (res) => {
if (res.confirm) {
const allSchedules = wx.getStorageSync('allCourseSchedules') || {}
let copiedCount = 0
// 复制到其他周(跳过已有课表的周)
for (let week = 1; week <= this.data.totalWeeks; week++) {
if (week !== currentWeek && !allSchedules[`week_${week}`]) {
allSchedules[`week_${week}`] = { ...schedule }
copiedCount++
}
}
wx.setStorageSync('allCourseSchedules', allSchedules)
wx.showToast({
title: `已复制到${copiedCount}`,
icon: 'success',
duration: 2000
})
}
}
})
},
// 阻止事件冒泡
doNothing() {}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "课程表"
}

View File

@@ -0,0 +1,123 @@
<!--pages/schedule/schedule.wxml-->
<view class="container">
<view class="header">
<view class="header-title">我的课程表</view>
<view class="header-right">
<view class="add-btn" bindtap="onAddCourse">
<text class="add-icon">+</text>
</view>
</view>
</view>
<!-- 周次切换器 -->
<view class="week-selector">
<view class="week-btn" bindtap="onPrevWeek">
<text class="week-arrow">◀</text>
</view>
<view class="week-display" bindtap="onCurrentWeek">
<text class="week-text">第{{currentWeek}}周</text>
<text class="week-hint">点击回到本周</text>
</view>
<view class="week-btn" bindtap="onNextWeek">
<text class="week-arrow">▶</text>
</view>
</view>
<view class="schedule-container">
<view class="schedule-table">
<!-- 表头 -->
<view class="table-header">
<view class="time-column header-cell">时间</view>
<view class="day-column header-cell" wx:for="{{weekDays}}" wx:key="index">
{{item}}
</view>
</view>
<!-- 课程格子 -->
<view class="table-row" wx:for="{{scheduleData}}" wx:key="index" wx:for-item="row">
<view class="time-column time-cell">{{row.period}}</view>
<view
class="day-column course-cell {{item.hasCourse ? 'has-course' : ''}}"
wx:for="{{row.courses}}"
wx:key="index"
wx:for-item="item"
data-day="{{item.day}}"
data-period="{{row.period}}"
data-course="{{item.course}}"
bindtap="onCourseClick"
bindlongpress="onCourseLongPress"
>
<block wx:if="{{item.course}}">
<view class="course-name">{{item.course.name}}</view>
<view class="course-location">{{item.course.location}}</view>
</block>
<view class="empty-hint" wx:else>
<text class="hint-text">+</text>
</view>
</view>
</view>
</view>
</view>
<!-- 快捷操作按钮 -->
<view class="action-buttons">
<button class="action-btn copy-btn" bindtap="onCopySchedule">
<text class="btn-icon">📋</text>
<text class="btn-text">复制课表到其他周</text>
</button>
</view>
<view class="legend">
<view class="legend-item">
<view class="legend-color has-course"></view>
<text class="legend-text">有课</text>
</view>
<view class="legend-item">
<view class="legend-color"></view>
<text class="legend-text">无课</text>
</view>
</view>
<!-- 添加/编辑课程弹窗 -->
<view class="modal-mask" wx:if="{{showEditModal}}" bindtap="closeEditModal">
<view class="modal-dialog" catchtap="doNothing">
<view class="modal-header">
<text class="modal-title">{{editMode === 'add' ? '添加课程' : '编辑课程'}}</text>
<view class="modal-close" bindtap="closeEditModal">✕</view>
</view>
<view class="modal-content">
<view class="form-item">
<text class="form-label">课程名称</text>
<input
class="form-input"
placeholder="请输入课程名称"
value="{{editCourse.name}}"
bindinput="onCourseNameInput"
/>
</view>
<view class="form-item">
<text class="form-label">上课地点</text>
<input
class="form-input"
placeholder="请输入上课地点"
value="{{editCourse.location}}"
bindinput="onCourseLocationInput"
/>
</view>
<view class="form-item">
<text class="form-label">上课时间</text>
<view class="time-display">{{editDay}} {{editPeriod}}</view>
</view>
</view>
<view class="modal-footer">
<button class="modal-btn cancel-btn" bindtap="closeEditModal">取消</button>
<button class="modal-btn delete-btn" wx:if="{{editMode === 'edit'}}" bindtap="onDeleteCourse">删除</button>
<button class="modal-btn confirm-btn" bindtap="onSaveCourse">保存</button>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,526 @@
/* pages/schedule/schedule.wxss */
.container {
min-height: 100vh;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
animation: fadeIn 0.5s ease-out;
}
.header {
background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%);
padding: 45rpx 30rpx;
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 8rpx 24rpx rgba(155, 89, 182, 0.3);
animation: slideInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.header-title {
font-size: 44rpx;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.header-right {
display: flex;
align-items: center;
gap: 20rpx;
}
.add-btn {
width: 60rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10rpx);
transition: all 0.3s ease;
}
.add-btn:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.4);
}
.add-icon {
font-size: 48rpx;
color: #fff;
font-weight: bold;
line-height: 1;
}
/* 周次切换器 */
.week-selector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
margin: 20rpx 30rpx;
border-radius: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
animation: slideInDown 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both;
}
.week-btn {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(155, 89, 182, 0.3);
}
.week-btn:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx rgba(155, 89, 182, 0.2);
}
.week-arrow {
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
}
.week-display {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 40rpx;
cursor: pointer;
}
.week-text {
font-size: 40rpx;
font-weight: bold;
color: #9B59B6;
margin-bottom: 8rpx;
}
.week-hint {
font-size: 22rpx;
color: #999;
}
.current-week {
font-size: 26rpx;
padding: 12rpx 24rpx;
background-color: rgba(255, 255, 255, 0.25);
border-radius: 24rpx;
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.schedule-container {
padding: 20rpx 30rpx;
}
.schedule-table {
width: 100%;
background-color: #ffffff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1);
animation: scaleIn 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}
.table-header {
display: flex;
background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%);
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(155, 89, 182, 0.2);
}
.table-row {
display: flex;
border-bottom: 2rpx solid #f0f0f0;
transition: all 0.3s ease;
}
.table-row:last-child {
border-bottom: none;
}
.time-column {
flex: 0 0 100rpx;
width: 100rpx;
}
.day-column {
flex: 1;
min-width: 0;
}
.header-cell {
padding: 20rpx 8rpx;
text-align: center;
font-size: 26rpx;
font-weight: bold;
border-right: 2rpx solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.header-cell:last-child {
border-right: none;
}
.time-cell {
display: flex;
align-items: center;
justify-content: center;
padding: 16rpx 8rpx;
font-size: 24rpx;
color: #666666;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-right: 2rpx solid #f0f0f0;
font-weight: 600;
}
.course-cell {
padding: 12rpx 8rpx;
border-right: 2rpx solid #f0f0f0;
min-height: 110rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #ffffff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
.course-cell:last-child {
border-right: none;
}
.course-cell.has-course {
background: linear-gradient(135deg, #E8D5F2 0%, #F0E5F5 100%);
box-shadow: inset 0 2rpx 8rpx rgba(155, 89, 182, 0.15);
cursor: pointer;
}
.course-cell.has-course::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4rpx;
background: linear-gradient(to bottom, #9B59B6, #8E44AD);
}
.course-cell.has-course:active {
transform: scale(0.98);
background: linear-gradient(135deg, #d5b8e8 0%, #e5d5f0 100%);
}
.course-name {
font-size: 22rpx;
font-weight: bold;
color: #9B59B6;
margin-bottom: 6rpx;
text-align: center;
line-height: 1.3;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.course-location {
font-size: 20rpx;
color: #8E44AD;
text-align: center;
opacity: 0.9;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-hint {
display: flex;
align-items: center;
justify-content: center;
opacity: 0.3;
width: 100%;
height: 100%;
}
.hint-text {
font-size: 32rpx;
color: #999;
font-weight: 300;
}
/* 快捷操作按钮 */
.action-buttons {
padding: 20rpx 30rpx;
animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.3s both;
}
.action-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 28rpx;
background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%);
border-radius: 20rpx;
border: none;
box-shadow: 0 8rpx 24rpx rgba(155, 89, 182, 0.3);
transition: all 0.3s ease;
}
.action-btn:active {
transform: scale(0.98);
box-shadow: 0 4rpx 16rpx rgba(155, 89, 182, 0.2);
}
.btn-icon {
font-size: 36rpx;
}
.btn-text {
font-size: 28rpx;
font-weight: bold;
color: #ffffff;
}
.legend {
display: flex;
justify-content: center;
gap: 50rpx;
padding: 35rpx;
animation: fadeIn 0.8s ease-out 0.4s both;
}
.legend-item {
display: flex;
align-items: center;
gap: 15rpx;
padding: 12rpx 24rpx;
background-color: #ffffff;
border-radius: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
.legend-item:active {
transform: scale(0.95);
}
.legend-color {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
background-color: #ffffff;
border: 3rpx solid #e0e0e0;
transition: all 0.3s ease;
}
.legend-color.has-course {
background: linear-gradient(135deg, #E8D5F2 0%, #F0E5F5 100%);
border-color: #9B59B6;
box-shadow: 0 2rpx 8rpx rgba(155, 89, 182, 0.3);
}
.legend-text {
font-size: 26rpx;
color: #666666;
font-weight: 500;
}
/* 空格子提示 */
.empty-hint {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.2;
transition: all 0.2s ease;
}
.course-cell:active .empty-hint {
opacity: 0.5;
}
.hint-text {
font-size: 40rpx;
color: #9B59B6;
font-weight: 300;
}
/* 编辑弹窗样式 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s ease;
}
.modal-dialog {
width: 85%;
max-width: 600rpx;
background-color: #ffffff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(50rpx) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
padding: 35rpx 30rpx;
background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%);
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
}
.modal-close {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
border-radius: 25rpx;
transition: all 0.2s ease;
}
.modal-close:active {
background-color: rgba(255, 255, 255, 0.2);
}
.modal-content {
padding: 40rpx 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 600;
}
.form-input {
width: 100%;
padding: 25rpx 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
.form-input:focus {
border-color: #9B59B6;
background-color: #fff;
}
.time-display {
padding: 25rpx 20rpx;
background: linear-gradient(135deg, #E8D5F2 0%, #F0E5F5 100%);
border-radius: 12rpx;
font-size: 28rpx;
color: #9B59B6;
font-weight: 600;
text-align: center;
}
.modal-footer {
padding: 25rpx 30rpx;
background-color: #f8f9fa;
display: flex;
gap: 15rpx;
}
.modal-btn {
flex: 1;
padding: 25rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
transition: all 0.3s ease;
}
.modal-btn::after {
border: none;
}
.cancel-btn {
background-color: #e0e0e0;
color: #666;
}
.cancel-btn:active {
background-color: #d0d0d0;
}
.delete-btn {
background: linear-gradient(135deg, #FF5252 0%, #E53935 100%);
color: #fff;
}
.delete-btn:active {
opacity: 0.8;
}
.confirm-btn {
background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%);
color: #fff;
}
.confirm-btn:active {
opacity: 0.8;
}

86
pages/tools/tools.js Normal file
View File

@@ -0,0 +1,86 @@
// pages/tools/tools.js
const learningTracker = require('../../utils/learningTracker.js')
Page({
data: {
tools: [
{
id: 1,
name: '启思AI',
icon: '🤖',
desc: '启迪思维,智慧学习',
path: '/pages/ai-assistant/ai-assistant',
color: '#6C5CE7',
badge: 'AI',
badgeColor: '#A29BFE'
},
{
id: 2,
name: '学习数据',
icon: '📊',
desc: '可视化数据分析',
path: '/pages/dashboard/dashboard',
color: '#667eea',
badge: 'NEW',
badgeColor: '#4CAF50'
},
{
id: 3,
name: 'GPA计算器',
icon: '🎯',
desc: '快速计算学期绩点',
path: '/pages/gpa/gpa',
color: '#FF6B6B'
},
{
id: 4,
name: '课程表',
icon: '📅',
desc: '查看个人课程安排',
path: '/pages/schedule/schedule',
color: '#9B59B6'
},
{
id: 5,
name: '考试倒计时',
icon: '⏰',
desc: '重要考试提醒',
path: '/pages/countdown/countdown',
color: '#F39C12'
}
]
},
onLoad() {
},
onShow() {
// 开始跟踪学习时间
learningTracker.onPageShow('tools')
// 更新自定义TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 3
})
}
},
onHide() {
// 停止跟踪学习时间
learningTracker.onPageHide()
},
onUnload() {
// 记录学习时长
learningTracker.onPageUnload()
},
onToolClick(e) {
const { path } = e.currentTarget.dataset
wx.navigateTo({
url: path
})
}
})

3
pages/tools/tools.json Normal file
View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "实用工具"
}

27
pages/tools/tools.wxml Normal file
View File

@@ -0,0 +1,27 @@
<!--pages/tools/tools.wxml-->
<view class="container">
<view class="header">
<view class="header-title">实用工具</view>
<view class="header-desc">提升学习效率的小帮手</view>
</view>
<view class="tools-list">
<view
class="tool-card"
wx:for="{{tools}}"
wx:key="id"
data-path="{{item.path}}"
bindtap="onToolClick"
>
<view class="tool-icon" style="background-color: {{item.color}}20;">
<text class="icon-text">{{item.icon}}</text>
<view class="badge" wx:if="{{item.badge}}" style="background-color: {{item.badgeColor || '#FF5252'}};">{{item.badge}}</view>
</view>
<view class="tool-info">
<view class="tool-name">{{item.name}}</view>
<view class="tool-desc">{{item.desc}}</view>
</view>
<view class="arrow"></view>
</view>
</view>
</view>

96
pages/tools/tools.wxss Normal file
View File

@@ -0,0 +1,96 @@
/* pages/tools/tools.wxss */
.container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 30rpx;
/* 底部留出TabBar的空间 */
padding-bottom: calc(env(safe-area-inset-bottom) + 150rpx);
}
.header {
color: #ffffff;
margin-bottom: 40rpx;
}
.header-title {
font-size: 48rpx;
font-weight: bold;
margin-bottom: 15rpx;
}
.header-desc {
font-size: 28rpx;
opacity: 0.9;
}
.tools-list {
background-color: #ffffff;
border-radius: 20rpx;
overflow: hidden;
}
.tool-card {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #F5F5F5;
transition: all 0.3s;
}
.tool-card:last-child {
border-bottom: none;
}
.tool-card:active {
background-color: #F8F9FA;
}
.tool-icon {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 25rpx;
position: relative;
}
.icon-text {
font-size: 56rpx;
}
.badge {
position: absolute;
top: -8rpx;
right: -8rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-size: 20rpx;
color: #FFFFFF;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
z-index: 10;
}
.tool-info {
flex: 1;
}
.tool-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.tool-desc {
font-size: 26rpx;
color: #999999;
}
.arrow {
font-size: 60rpx;
color: #CCCCCC;
font-weight: 300;
}