1
This commit is contained in:
288
pages/ai-assistant/ai-assistant.js
Normal file
288
pages/ai-assistant/ai-assistant.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
7
pages/ai-assistant/ai-assistant.json
Normal file
7
pages/ai-assistant/ai-assistant.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "启思AI",
|
||||
"navigationBarBackgroundColor": "#6C5CE7",
|
||||
"navigationBarTextStyle": "white",
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundColor": "#F5F6FA"
|
||||
}
|
||||
88
pages/ai-assistant/ai-assistant.wxml
Normal file
88
pages/ai-assistant/ai-assistant.wxml
Normal 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>
|
||||
356
pages/ai-assistant/ai-assistant.wxss
Normal file
356
pages/ai-assistant/ai-assistant.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
159
pages/countdown/countdown.js
Normal file
159
pages/countdown/countdown.js
Normal 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('删除成功')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
3
pages/countdown/countdown.json
Normal file
3
pages/countdown/countdown.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "考试倒计时"
|
||||
}
|
||||
78
pages/countdown/countdown.wxml
Normal file
78
pages/countdown/countdown.wxml
Normal 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>
|
||||
163
pages/countdown/countdown.wxss
Normal file
163
pages/countdown/countdown.wxss
Normal 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;
|
||||
}
|
||||
82
pages/course-detail/course-detail.js
Normal file
82
pages/course-detail/course-detail.js
Normal 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}`
|
||||
}
|
||||
}
|
||||
})
|
||||
3
pages/course-detail/course-detail.json
Normal file
3
pages/course-detail/course-detail.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "课程详情"
|
||||
}
|
||||
121
pages/course-detail/course-detail.wxml
Normal file
121
pages/course-detail/course-detail.wxml
Normal 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>
|
||||
220
pages/course-detail/course-detail.wxss
Normal file
220
pages/course-detail/course-detail.wxss
Normal 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
204
pages/courses/courses.js
Normal 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()
|
||||
}
|
||||
})
|
||||
3
pages/courses/courses.json
Normal file
3
pages/courses/courses.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "课程筛选"
|
||||
}
|
||||
115
pages/courses/courses.wxml
Normal file
115
pages/courses/courses.wxml
Normal 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
482
pages/courses/courses.wxss
Normal 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;
|
||||
}
|
||||
823
pages/dashboard/dashboard.js
Normal file
823
pages/dashboard/dashboard.js
Normal 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
|
||||
});
|
||||
}
|
||||
});
|
||||
7
pages/dashboard/dashboard.json
Normal file
7
pages/dashboard/dashboard.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "学习数据",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"navigationBarTextStyle": "white",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#F5F6FA"
|
||||
}
|
||||
123
pages/dashboard/dashboard.wxml
Normal file
123
pages/dashboard/dashboard.wxml
Normal 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>
|
||||
279
pages/dashboard/dashboard.wxss
Normal file
279
pages/dashboard/dashboard.wxss
Normal 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);
|
||||
}
|
||||
}
|
||||
272
pages/forum-detail/forum-detail.js
Normal file
272
pages/forum-detail/forum-detail.js
Normal 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}`
|
||||
}
|
||||
}
|
||||
})
|
||||
3
pages/forum-detail/forum-detail.json
Normal file
3
pages/forum-detail/forum-detail.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "帖子详情"
|
||||
}
|
||||
111
pages/forum-detail/forum-detail.wxml
Normal file
111
pages/forum-detail/forum-detail.wxml
Normal 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>
|
||||
278
pages/forum-detail/forum-detail.wxss
Normal file
278
pages/forum-detail/forum-detail.wxss
Normal 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
191
pages/forum/forum.js
Normal 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
3
pages/forum/forum.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "学科论坛"
|
||||
}
|
||||
99
pages/forum/forum.wxml
Normal file
99
pages/forum/forum.wxml
Normal 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
338
pages/forum/forum.wxss
Normal 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
150
pages/gpa/gpa.js
Normal 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
3
pages/gpa/gpa.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "GPA计算器"
|
||||
}
|
||||
72
pages/gpa/gpa.wxml
Normal file
72
pages/gpa/gpa.wxml
Normal 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
328
pages/gpa/gpa.wxss
Normal 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
283
pages/index/index.js
Normal 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
3
pages/index/index.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "知芽小筑"
|
||||
}
|
||||
119
pages/index/index.wxml
Normal file
119
pages/index/index.wxml
Normal 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
643
pages/index/index.wxss
Normal 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
726
pages/my/my.js
Normal 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
3
pages/my/my.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
90
pages/my/my.wxml
Normal file
90
pages/my/my.wxml
Normal 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
421
pages/my/my.wxss
Normal 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
120
pages/post/post.js
Normal 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
3
pages/post/post.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "发布帖子"
|
||||
}
|
||||
82
pages/post/post.wxml
Normal file
82
pages/post/post.wxml
Normal 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
187
pages/post/post.wxss
Normal 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
421
pages/schedule/schedule.js
Normal 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() {}
|
||||
})
|
||||
|
||||
3
pages/schedule/schedule.json
Normal file
3
pages/schedule/schedule.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "课程表"
|
||||
}
|
||||
123
pages/schedule/schedule.wxml
Normal file
123
pages/schedule/schedule.wxml
Normal 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>
|
||||
526
pages/schedule/schedule.wxss
Normal file
526
pages/schedule/schedule.wxss
Normal 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
86
pages/tools/tools.js
Normal 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
3
pages/tools/tools.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "实用工具"
|
||||
}
|
||||
27
pages/tools/tools.wxml
Normal file
27
pages/tools/tools.wxml
Normal 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
96
pages/tools/tools.wxss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user