TortoiseGit 3 months ago
parent 8211bcd028
commit ff742c0c43

@ -1,2 +1,10 @@
// app.js
App({})
App({
onLaunch() {
wx.cloud.init({
env: 'cloud1-0g2sr1117862afae'
});
console.log('小程序启动,云环境初始化完成');
}
})

@ -2,16 +2,16 @@
"pages": [
"pages/guiding/guiding",
"pages/home/home",
"pages/recite/recite",
"pages/review/review",
"pages/login/login",
"pages/index/index",
"pages/study/study",
"pages/search/search",
"pages/login/login",
"pages/keyWord/keyWord",
"pages/questionAnswer/questionAnswer",
"pages/recite/recite",
"pages/review/review",
"pages/pendingQuestion/pendingQuestion",
"pages/textbookFilter/textbookFilter"
"pages/keyWord/keyWord",
"pages/question/question",
"pages/managePoems/managePoems"
],
"window": {
"navigationBarTitleText": "古诗学习助手",

@ -60,10 +60,15 @@ Component({
lifetimes: {
attached() {
const rect = wx.getMenuButtonBoundingClientRect()
const platform = (wx.getDeviceInfo() || wx.getSystemInfoSync()).platform
const deviceInfo = wx.getDeviceInfo()
const platform = deviceInfo ? deviceInfo.platform : 'devtools'
const isAndroid = platform === 'android'
const isDevtools = platform === 'devtools'
const { windowWidth, safeArea: { top = 0, bottom = 0 } = {} } = wx.getWindowInfo() || wx.getSystemInfoSync()
const windowInfo = wx.getWindowInfo()
const windowWidth = windowInfo ? windowInfo.windowWidth : 375
const safeArea = windowInfo && windowInfo.safeArea ? windowInfo.safeArea : {}
const top = safeArea.top || 0
const bottom = safeArea.bottom || 0
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`,

@ -1,71 +1,134 @@
// pages/guiding/guiding.js
Page({
/**
* 页面的初始数据
*/
data: {
ifEntry: false,
userInfo: null
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 页面加载时先检查本地缓存
this.checkLocalLoginStatus();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
// 检查本地登录状态(快速检查)
checkLocalLoginStatus() {
try {
const cachedUserInfo = wx.getStorageSync('userInfo');
if (cachedUserInfo) {
this.setData({
ifEntry: true,
userInfo: cachedUserInfo
});
console.log('从缓存读取用户信息:', cachedUserInfo);
} else {
this.setData({
ifEntry: false,
userInfo: null
});
}
} catch (error) {
console.error('读取缓存失败:', error);
this.setData({ ifEntry: false });
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
//检查登录状态
async checkLoginStatus() {
try {
// 强制清除缓存,确保从服务器获取最新的用户信息
this.clearStorage();
// 先检查本地是否有缓存
const cachedUserInfo = wx.getStorageSync('userInfo');
if (cachedUserInfo) {
console.log('使用缓存用户信息,直接进入主页');
this.setData({
ifEntry: true,
userInfo: cachedUserInfo
});
// 调试日志:查看缓存中的用户角色值
console.log('缓存用户角色值:', cachedUserInfo.role, '类型:', typeof cachedUserInfo.role);
// 有缓存时直接跳转到主页 - 安全比较,处理不同类型的值
if (cachedUserInfo.role === true || cachedUserInfo.role === 'true') {
console.log('缓存中为普通用户跳转到index页面');
wx.reLaunch({
url: `/pages/index/index?openid=${cachedUserInfo.openid}`
});
} else {
console.log('缓存中为管理员跳转到pendingQuestion页面');
wx.reLaunch({
url: `/pages/pendingQuestion/pendingQuestion?openid=${cachedUserInfo.openid}`
});
}
return; // 跳转后可以return避免后续代码执行
}
// 1. 如果没有缓存,调用 wx.login() 获取 code
const loginRes = await wx.login();
const code = loginRes.code;
if (!code) {
console.error('获取code失败');
return;
}
// 2. 调用后端登录接口将code传给后端
const result = await wx.cloud.callFunction({
name: 'auth',
data: {
action: 'loginWithCode',
code: code
}
});
if (result.result.success) {
const userInfo = result.result.data.userInfo;
wx.setStorageSync('userInfo', userInfo);
// 登录成功,直接进入用户页面
this.setData({
ifEntry: true,
userInfo: userInfo
});
// 调试日志:查看用户角色值
console.log('用户角色值:', userInfo.role, '类型:', typeof userInfo.role);
// 跳转到用户首页 - 安全比较,处理不同类型的值
if (userInfo.role === true || userInfo.role === 'true') {
console.log('普通用户跳转到index页面');
wx.reLaunch({
url: `/pages/index/index?openid=${userInfo.openid}`
});
} else {
console.log('管理员跳转到pendingQuestion页面');
wx.reLaunch({
url: `/pages/pendingQuestion/pendingQuestion?openid=${userInfo.openid}`
});
}
} else {
// 登录失败
console.error('登录失败:', result.result.message);
this.setData({
ifEntry: false
});
const openid = result.result.openid || '';
wx.reLaunch({
url: `/pages/login/login?openid=${openid}`
});
}
} catch (error) {
console.error('检查登录状态失败:', error);
this.clearStorage();
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
clearStorage() {
wx.removeStorageSync('token');
wx.removeStorageSync('userInfo');
this.setData({ ifEntry: false });
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
},
request_enter(){
wx.redirectTo({
url: '/pages/login/login',
});
// 进入按钮
async request_enter() {
this.checkLoginStatus();
}
})
});

@ -1,86 +0,0 @@
// pages/home/nome.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
},
review(){
wx.navigateTo({
url: '/pages/review/review'
})
},
filter(){
wx.navigateTo({
url: '/pages/textbookFilter/textbookFilter'
})
},
question(){
wx.navigateTo({
url: '/pages/pendingQuestion/pendingQuestion'
})
},
search(){
wx.navigateTo({
url: '/pages/search/search'
})
}
})

@ -1,4 +0,0 @@
{
"usingComponents": {}
}

@ -1,33 +0,0 @@
<!--pages/home/home.wxml-->
<view class="container">
<view class="user-info">
<text class="user-name">欢迎回来{{userInfo.nickName || '勤奋的'}}小朋友</text>
<image
class="user-avatar"
src="{{userInfo.avatarUrl || '/images/default-avatar.png'}}"
mode="widthFix"
></image>
</view>
<view class="bottom-buttons">
<button class="func-btn" bind:tap="search">
<image class="btn-icon" src="/images/icon-search.png" mode="widthFix"></image>
</button>
<button class="func-btn" bindtap="question">
<image class="btn-icon" src="/images/icon-answer.png" mode="widthFix"></image>
</button>
<button class="func-btn" catch:tap="filter">
<image class="btn-icon" src="/images/icon-filter.png" mode="widthFix"></image>
</button>
<button class="func-btn" catch:tap="review">
<image class="btn-icon" src="/images/icon-review.png" mode="widthFix"></image>
</button>
</view>
</view>

@ -1,50 +0,0 @@
/* pages/home/home.wxss */
.bottom-buttons {
display: flex; /* 按钮横向排列 */
position: fixed;
justify-content: space-around;
bottom: 200rpx ;
border-top: 1px solid rgba(255, 255, 255, 0); /* 顶部边框分隔 */
border-bottom: 1px solid #eee;
width: 100%;
left : 0%;
}
.func-btn {
display: flex;
flex-direction: column;
align-items: center;
background-color: transparent; /* 清除默认按钮背景 */
border-top: 1px solid rgba(255, 255, 255, 0);
width: 20%;
}
.btn-icon {
width: 200rpx;
}
.user-info {
display: flex;
align-items: baseline;
padding: 30rpx 20rpx;
top: 0;
right:0;
position: absolute; /* 脱离文档流,基于父容器 .home-container 定位 */
z-index: 1; /* 确保在其他元素上方显示 */
}
/* 头像:圆形裁剪,固定大小 */
.user-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%; /* 圆形 */
border: 2rpx solid #000; /* 白色边框增加层次感 */
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); /* 轻微阴影 */
margin-right: 20rpx; /* 与昵称间距 */
}
/* 昵称:字体大小和颜色 */
.user-name {
font-size: 36rpx;
font-weight: 500;
color: #333;
}
.container{
position: relative; /* 关键:让子元素 .user-info 能基于它做绝对定位 */
}

@ -1,2 +1,185 @@
// index.js 代码控制
Page({})
Page({
data: {
userInfo: {},
openid: '',
poems: [], // 所有诗词列表
reviewedPoems: [], // 需要复习的诗词
otherPoems: [], // 其他诗词
isLoading: false
},
onLoad(options) {
console.log('首页加载完成');
if (options.openid) {
this.setData({
openid: options.openid
});
console.log('获取到openid:', options.openid);
// 根据openid加载用户数据和诗词数据
this.loadUserDataAndPoems();
} else {
console.error('未收到openid参数');
wx.showToast({
title: '参数错误',
icon: 'none'
});
}
},
// 加载用户数据和诗词数据
async loadUserDataAndPoems() {
try {
this.setData({ isLoading: true });
// 并行执行:获取用户信息和诗词列表
const [userResult, poemsResult] = await Promise.all([
this.getUserInfo(this.data.openid),
this.getAllPoems()
]);
if (userResult.success) {
const userInfo = userResult.data.userInfo;
this.setData({
userInfo: userInfo,
reviewedPoems: userInfo.reviewedPoems || {}
});
console.log('用户复习记录:', this.data.reviewedPoems);
}
if (poemsResult.success) {
const allPoems = poemsResult.data.poems;
// 筛选其他诗词
const { reviewedPoems, otherPoems } = this.classifyPoems(allPoems);
this.setData({
poems: allPoems,
reviewedPoems: reviewedPoems,
otherPoems: otherPoems
});
}
} catch (error) {
console.error('加载数据失败:', error);
wx.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
this.setData({ isLoading: false });
}
},
// 获取用户信息
async getUserInfo(openid) {
try {
const result = await wx.cloud.callFunction({
name: 'auth',
data: {
action: 'getUserByOpenid',
openid: openid
}
});
return result.result;
} catch (error) {
console.error('获取用户信息失败:', error);
return { success: false, message: '获取用户信息失败' };
}
},
// 获取所有诗词
async getAllPoems() {
try {
const result = await wx.cloud.callFunction({
name: 'poemManagement',
data: {
action: 'getAllPoems'
}
});
return result.result;
} catch (error) {
console.error('获取诗词列表失败:', error);
return { success: false, message: '获取诗词失败' };
}
},
// 分类诗词:今天需要复习的 vs 其他诗词
classifyPoems(allPoems) {
const reviewedPoems = this.data.reviewedPoems;
const otherPoems = [];
allPoems.forEach(poem => {
const poemReviewData = reviewedPoems[poem._id];
if (poemReviewData==null){
otherPoems.push(poem);
}
});
return { reviewedPoems, otherPoems };
},
// 进入学习页面
goToStudy(e) {
const poemId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/study/study?id=${poemId}`
});
},
// 进入背诵页面
goToRecite(e) {
const poemId = e.currentTarget.dataset.id;
console.log('跳转到背诵页面诗歌ID:', poemId);
const poem = this.data.poems.find(p => p._id === poemId);
if (poemId && poem) {
wx.navigateTo({
url: `/pages/recite/recite?id=${poemId}&title=${encodeURIComponent(poem.title)}&author=${encodeURIComponent(poem.author)}`
});
}
},
review(){
const userInfo= this.data.userInfo;
console.log('跳转到复习页面诗歌ID:', userInfo);
wx.navigateTo({
url: `/pages/review/review?userInfo=${userInfo}`
})
},
question(){
wx.navigateTo({
url: '/pages/question/question'
})
},
search(){
wx.navigateTo({
url: '/pages/search/search'
})
},
onShow() {
// 每次显示页面时检查数据
if (this.data.poems.length === 0 && !this.data.isLoading) {
this.loadPoemsFromDatabase();
}
},
onPullDownRefresh() {
this.loadPoemsFromDatabase().finally(() => {
wx.stopPullDownRefresh();
});
},
onShareAppMessage() {
return {
title: '古诗学习小程序',
path: '/pages/index/index'
};
}
})

@ -1,5 +1,5 @@
{
"usingComponents": {
}
"usingComponents": {},
"enablePullDownRefresh": true,
"backgroundTextStyle": "light"
}

@ -1,6 +1,68 @@
<!--index.wxml-->
<view class="page-container">
<!-- 头部区域 -->
<view class="header-section">
<text class="header-title">古诗学习</text>
<text class="header-subtitle">传承经典文化,品味诗词之美</text>
</view>
<view class="container">
Weixin
</view>
<!-- 功能按钮网格 -->
<view class="function-grid">
<view class="function-item" bindtap="review">
<view class="function-icon">📚</view>
<text class="function-text">复习</text>
</view>
<view class="function-item" bindtap="question">
<view class="function-icon">❓</view>
<text class="function-text">问答</text>
</view>
<view class="function-item" bindtap="search">
<view class="function-icon">🔎</view>
<text class="function-text">搜索</text>
</view>
</view>
<!-- 诗歌列表区域 -->
<view class="poem-section">
<view class="list-header">
<text class="list-title">推荐诗歌</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{isLoading}}">
<text class="state-icon">⏳</text>
<text class="state-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view class="error-state" wx:elif="{{loadError}}">
<text class="state-icon">⚠️</text>
<text class="state-text">加载失败,请重试</text>
<button class="btn-retry" bindtap="loadPoemsFromDatabase">重新加载</button>
<button class="btn-mock" bindtap="useMockData">使用示例数据</button>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:elif="{{poems.length === 0}}">
<text class="state-icon">📖</text>
<text class="state-text">暂无诗歌数据</text>
</view>
<!-- 诗歌列表 -->
<view class="poem-list" wx:else>
<view class="poem-item" wx:for="{{poems}}" wx:key="_id" data-id="{{item._id}}">
<view class="poem-content">
<text class="poem-title">{{item.title}}</text>
<view class="poem-meta">
<text class="poem-author">{{item.author}}</text>
<text class="poem-dynasty">{{item.dynasty}}</text>
</view>
<text class="poem-preview">点击学习经典唐诗</text>
</view>
<view class="action-buttons">
<view class="btn-study" bindtap="goToStudy" data-id="{{item._id}}">学习</view>
<view class="btn-recite" bindtap="goToRecite" data-id="{{item._id}}">背诵</view>
</view>
</view>
</view>
</view>
</view>

@ -1,10 +1,226 @@
/**index.wxss**/
page {
height: 100vh;
/* 页面容器 */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 头部区域 */
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 40rpx;
color: white;
}
.header-title {
font-size: 48rpx;
font-weight: 700;
margin-bottom: 16rpx;
}
.header-subtitle {
font-size: 28rpx;
opacity: 0.9;
}
/* 功能按钮区域 */
.function-grid {
display: flex;
justify-content: space-between;
padding: 30rpx 40rpx;
background: white;
margin: 20rpx 40rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
}
.function-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.function-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 16rpx;
border-radius: 50%;
background: #f0f7ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
}
.function-text {
font-size: 24rpx;
color: #333;
}
/* 诗歌列表区域 */
.poem-section {
background: white;
margin: 0 40rpx 40rpx;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
}
.scrollarea {
.list-header {
padding: 30rpx 40rpx 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.list-title {
font-size: 36rpx;
font-weight: 700;
color: #333;
}
.poem-list {
padding: 0;
}
.poem-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #f8f8f8;
transition: background-color 0.3s;
}
.poem-item:active {
background-color: #f8f9fa;
}
.poem-content {
flex: 1;
overflow-y: hidden;
margin-right: 30rpx;
}
.poem-title {
display: block;
font-size: 34rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.poem-meta {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.poem-author {
font-size: 26rpx;
color: #666;
margin-right: 20rpx;
}
.poem-dynasty {
font-size: 24rpx;
color: #999;
background: #f0f0f0;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.poem-preview {
display: block;
font-size: 26rpx;
color: #999;
font-style: italic;
line-height: 1.4;
}
.action-buttons {
display: flex;
gap: 15rpx;
}
.btn-study, .btn-recite {
padding: 16rpx 24rpx;
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 500;
transition: all 0.3s;
}
.btn-study {
background-color: #1989fa;
color: white;
}
.btn-study:active {
background-color: #1576d9;
}
.btn-recite {
background-color: #07c160;
color: white;
}
.btn-recite:active {
background-color: #06a652;
}
/* 状态样式 */
.loading-state, .empty-state, .error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
text-align: center;
}
.loading-state {
color: #666;
}
.empty-state {
color: #999;
}
.error-state {
color: #ff4757;
}
.state-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
opacity: 0.7;
}
.state-text {
font-size: 32rpx;
margin-bottom: 40rpx;
}
.btn-retry, .btn-mock {
padding: 20rpx 40rpx;
border-radius: 30rpx;
font-size: 28rpx;
margin: 10rpx 0;
transition: all 0.3s;
}
.btn-retry {
background-color: #1989fa;
color: white;
}
.btn-retry:active {
background-color: #1576d9;
}
.btn-mock {
background-color: #07c160;
color: white;
}
.btn-mock:active {
background-color: #06a652;
}

@ -1,20 +1,327 @@
// pages/keyWord/keyWord.js
Page({
/**
* 页面的初始数据
*/
data: {
questionId: '',
question: '',
answer: '',
// 关键词相关数据
allKeywords: [], // 所有关键词数组
currentLevel: 0, // 当前关键词层级
selectedKeywords: [], // 已选择的关键词路径
availableKeywords: [], // 当前层级可用的关键词
newKeyword: '', // 新建关键词输入
isCreatingNew: false // 是否正在创建新关键词
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options) {
this.setData({
questionId: options.id || '',
question: options.question ? decodeURIComponent(options.question) : ''
});
// 加载已有的关键词数据
this.loadExistingKeywords();
}
},
// 加载已有关键词数据
async loadExistingKeywords() {
try {
const db = wx.cloud.database();
// 从数据库获取所有已有的关键词数据
const result = await db.collection('Answer').get();
const allKeywords = [];
result.data.forEach(item => {
if (item.keywords && Array.isArray(item.keywords)) {
// 将每个答案的关键词路径添加到总列表中
allKeywords.push(item.keywords);
}
});
this.setData({ allKeywords });
this.updateAvailableKeywords();
} catch (error) {
console.error('加载关键词失败:', error);
// 如果answers集合不存在就初始化为空数组
this.setData({ allKeywords: [] });
}
},
// 更新当前可用的关键词
updateAvailableKeywords() {
const { selectedKeywords, allKeywords, currentLevel } = this.data;
// 获取当前层级的所有可能关键词
const available = new Set();
allKeywords.forEach(keywordPath => {
if (keywordPath.length > currentLevel) {
// 检查路径是否匹配已选择的关键词
let isMatch = true;
for (let i = 0; i < selectedKeywords.length; i++) {
if (keywordPath[i] !== selectedKeywords[i]) {
isMatch = false;
break;
}
}
if (isMatch && keywordPath[currentLevel]) {
available.add(keywordPath[currentLevel]);
}
}
});
this.setData({
availableKeywords: Array.from(available)
});
},
// 选择关键词
selectKeyword(e) {
const keyword = e.currentTarget.dataset.keyword;
const { selectedKeywords, currentLevel } = this.data;
// 更新已选择的关键词路径
const newSelectedKeywords = [...selectedKeywords];
if (newSelectedKeywords.length > currentLevel) {
newSelectedKeywords[currentLevel] = keyword;
} else {
newSelectedKeywords.push(keyword);
}
this.setData({
selectedKeywords: newSelectedKeywords,
currentLevel: currentLevel + 1,
isCreatingNew: false,
newKeyword: ''
});
// 更新可用关键词
this.updateAvailableKeywords();
},
// 新建关键词
toggleCreateNew() {
this.setData({
isCreatingNew: !this.data.isCreatingNew,
newKeyword: ''
});
},
// 输入新建关键词
onNewKeywordInput(e) {
this.setData({
newKeyword: e.detail.value
});
},
// 确认新建关键词
confirmNewKeyword() {
const { newKeyword, selectedKeywords, currentLevel } = this.data;
if (!newKeyword.trim()) {
wx.showToast({
title: '请输入关键词',
icon: 'none'
});
return;
}
// 更新已选择的关键词路径
const newSelectedKeywords = [...selectedKeywords];
if (newSelectedKeywords.length > currentLevel) {
newSelectedKeywords[currentLevel] = newKeyword.trim();
} else {
newSelectedKeywords.push(newKeyword.trim());
}
this.setData({
selectedKeywords: newSelectedKeywords,
currentLevel: currentLevel + 1,
isCreatingNew: false,
newKeyword: ''
});
// 更新可用关键词(新建的关键词会出现在下一级选择中)
this.updateAvailableKeywords();
},
// 返回上一级
goBack() {
const { currentLevel } = this.data;
if (currentLevel > 0) {
this.setData({
currentLevel: currentLevel - 1,
isCreatingNew: false,
newKeyword: ''
});
this.updateAvailableKeywords();
}
},
// 输入回答
onAnswerInput(e) {
this.setData({
answer: e.detail.value
});
},
// 获取当前关键词路径显示
getCurrentKeywordPath() {
const { selectedKeywords, currentLevel } = this.data;
const currentPath = selectedKeywords.slice(0, currentLevel);
if (currentPath.length === 0) {
return '请选择关键词(从第一级开始)';
}
return currentPath.join(' > ');
},
// 重置关键词
resetKeywords() {
this.setData({
selectedKeywords: [],
currentLevel: 0,
isCreatingNew: false,
newKeyword: ''
});
this.updateAvailableKeywords();
wx.showToast({
title: '关键词已重置',
icon: 'success'
});
},
// 提交答案和关键词
async submitAnswer() {
const { questionId, question, selectedKeywords, answer } = this.data;
// 验证输入
if (selectedKeywords.length === 0) {
wx.showToast({
title: '请至少选择一个关键词',
icon: 'none'
});
return;
}
if (!answer.trim()) {
wx.showToast({
title: '请输入答案',
icon: 'none'
});
return;
}
// 显示加载中
wx.showLoading({
title: '提交中...',
mask: true
});
try {
const db = wx.cloud.database();
// 1. 从Question集合获取问题详情包括用户的_openid
let userOpenid = '';
let questionDocId = questionId;
if (questionId) {
try {
const questionDetail = await db.collection('Question').doc(questionId).get();
userOpenid = questionDetail.data._openid || '';
console.log('获取到用户openid:', userOpenid);
} catch (detailError) {
console.error('获取问题详情失败:', detailError);
// 如果通过ID查询失败尝试通过问题内容查询
const queryResult = await db.collection('Question').where({
question: question
}).get();
if (queryResult.data.length > 0) {
const firstQuestion = queryResult.data[0];
userOpenid = firstQuestion._openid || '';
questionDocId = firstQuestion._id;
console.log('通过问题内容获取到用户openid:', userOpenid);
}
}
}
// 2. 保存答案到Answer集合按照数据库要求的格式
const saveData = {
createTime: db.serverDate(), // 使用服务器时间
keywords: selectedKeywords, // 关键词数组
question: question, // 问题内容
questionId: questionDocId, // 保存问题ID便于后续查询
answers: [answer.trim()], // 答案使用answers数组
status: 1, // 标记为已回答状态
userId: userOpenid // 保存提问用户的openid但使用userId而非_openid避免系统保留字段冲突
};
await db.collection('Answer').add({
data: saveData
});
console.log('答案保存成功:', saveData);
// 3. 更新Question集合中对应问题的状态为已回答status: 1
if (questionDocId) {
try {
await db.collection('Question').doc(questionDocId).update({
data: {
status: 1
}
});
console.log('问题状态更新成功问题ID:', questionDocId);
} catch (updateError) {
console.error('更新问题状态失败:', updateError);
// 尝试通过问题内容更新
const updateResult = await db.collection('Question').where({
question: question
}).update({
data: {
status: 1
}
});
console.log('通过问题内容更新状态结果:', updateResult);
}
}
wx.hideLoading();
wx.showToast({
title: '提交成功',
icon: 'success',
duration: 2000,
success: () => {
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
});
} catch (error) {
console.error('提交失败:', error);
wx.hideLoading();
let errorMsg = '提交失败,请重试';
if (error.errCode === -502005) {
errorMsg = '数据库集合不存在,请检查配置';
} else if (error.errCode === -501007) {
errorMsg = '参数错误,请检查输入';
}
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/

@ -1,2 +1,83 @@
<!--pages/keyWord/keyWord.wxml-->
<text>pages/keyWord/keyWord.wxml</text>
<view class="header">
<text class="title">关键词回答</text>
</view>
<view class="content">
<!-- 问题显示框 -->
<view class="form-group">
<text class="label">问题:</text>
<view class="question-box">
<text class="question-text">{{question}}</text>
</view>
</view>
<!-- 关键词管理区域 -->
<view class="form-group">
<text class="label">关键词管理:</text>
<!-- 当前关键词路径显示 -->
<view class="keyword-path-box">
<text class="path-label">当前路径:</text>
<text class="path-text">{{getCurrentKeywordPath()}}</text>
</view>
<!-- 当前层级关键词选择 -->
<view class="keywords-level-section">
<text class="level-title">第{{currentLevel + 1}}级关键词</text>
<!-- 已有关键词列表 -->
<view class="keywords-list">
<block wx:for="{{availableKeywords}}" wx:key="*this">
<view class="keyword-item" bindtap="selectKeyword" data-keyword="{{item}}">
{{item}}
</view>
</block>
</view>
<!-- 新建关键词区域 -->
<view class="new-keyword-section">
<view class="create-new-btn {{isCreatingNew ? 'active' : ''}}" bindtap="toggleCreateNew">
<text class="btn-icon">+</text>
{{isCreatingNew ? '取消新建' : '新建关键词'}}
</view>
<view wx:if="{{isCreatingNew}}" class="new-keyword-input">
<input
value="{{newKeyword}}"
placeholder="请输入新关键词"
bindinput="onNewKeywordInput"
class="keyword-input"
focus="{{isCreatingNew}}"
/>
<button class="confirm-btn" bindtap="confirmNewKeyword">确认</button>
</view>
</view>
</view>
<!-- 关键词操作按钮 -->
<view class="keyword-actions">
<button wx:if="{{currentLevel > 0}}" class="action-btn back-btn" bindtap="goBack">
返回上一级
</button>
<button class="action-btn reset-btn" bindtap="resetKeywords">
重置关键词
</button>
</view>
</view>
<!-- 答案输入框 -->
<view class="form-group">
<text class="label">答案:</text>
<textarea
class="textarea answer-textarea"
placeholder="请输入问题的答案"
value="{{answer}}"
bindinput="onAnswerInput"
auto-height
/>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" type="primary" bindtap="submitAnswer">提交</button>
</view>

@ -1 +1,231 @@
/* pages/keyWord/keyWord.wxss */
/* pages/keyWord/keyWord.wxss */
.header {
text-align: center;
margin-bottom: 15rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.content {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
}
.form-group {
margin-bottom: 40rpx;
}
.label {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 15rpx;
}
.question-box {
background-color: #f9f9f9;
border: 1rpx solid #eee;
border-radius: 10rpx;
padding: 20rpx;
min-height: 100rpx;
}
.question-text {
font-size: 30rpx;
color: #666;
line-height: 44rpx;
}
/* 关键词路径显示 */
.keyword-path-box {
background-color: #f0f8ff;
border: 1rpx solid #d1e9ff;
border-radius: 10rpx;
padding: 20rpx;
margin-bottom: 30rpx;
}
.path-label {
font-size: 28rpx;
color: #1890ff;
font-weight: bold;
}
.path-text {
font-size: 28rpx;
color: #333;
margin-left: 15rpx;
}
/* 关键词层级区域 */
.keywords-level-section {
background-color: #fafafa;
border: 1rpx solid #f0f0f0;
border-radius: 10rpx;
padding: 25rpx;
margin-bottom: 20rpx;
}
.level-title {
font-size: 28rpx;
color: #666;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
/* 关键词列表 */
.keywords-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 25rpx;
}
.keyword-item {
background: #1890ff;
color: white;
padding: 16rpx 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
cursor: pointer;
transition: all 0.3s;
}
.keyword-item:active {
background: #096dd9;
transform: scale(0.95);
}
/* 新建关键词区域 */
.new-keyword-section {
border-top: 1rpx dashed #e8e8e8;
padding-top: 25rpx;
}
.create-new-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
border: 2rpx dashed #1890ff;
color: #1890ff;
border-radius: 8rpx;
font-size: 28rpx;
transition: all 0.3s;
}
.create-new-btn.active {
background: #e6f7ff;
border-style: solid;
}
.btn-icon {
margin-right: 10rpx;
font-weight: bold;
}
.new-keyword-input {
display: flex;
gap: 15rpx;
align-items: center;
margin-top: 20rpx;
}
.keyword-input {
flex: 1;
border: 1rpx solid #ddd;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
background: white;
}
.confirm-btn {
background: #52c41a;
color: white;
border: none;
padding: 20rpx 30rpx;
border-radius: 8rpx;
font-size: 26rpx;
}
.confirm-btn:active {
background: #389e0d;
}
/* 关键词操作按钮 */
.keyword-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
border: none;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
transition: all 0.3s;
}
.back-btn {
background: #faad14;
color: white;
}
.back-btn:active {
background: #d48806;
}
.reset-btn {
background: #ff4d4f;
color: white;
}
.reset-btn:active {
background: #cf1322;
}
/* 答案输入框 */
.textarea {
width: 100%;
border: 1rpx solid #eee;
border-radius: 10rpx;
padding: 20rpx;
font-size: 30rpx;
color: #333;
min-height: 120rpx;
box-sizing: border-box;
}
.answer-textarea {
min-height: 500rpx;
}
/* 提交按钮 */
.submit-btn {
margin-top: 40rpx;
font-size: 32rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: #1890ff;
color: white;
}
.submit-btn:active {
background: #096dd9;
}
/* 去除textarea默认样式 */
input, textarea {
font-family: inherit;
outline: none;
}

@ -5,14 +5,30 @@ Page({
* 页面的初始数据
*/
data: {
isLoading: false // 默认不显示提示文字
isLoading: false ,// 默认不显示提示文字
errorLoading: false,
openid:''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 接收从guiding页面传递的openid
if (options.openid) {
this.setData({
openid: options.openid
});
console.log('接收到的openid:', options.openid);
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
});
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
},
/**
@ -63,28 +79,90 @@ Page({
onShareAppMessage() {
},
request_login(){
//更新当前页面数据
this.setData({
isLoading: true,
errorLoading: false
});
// 这里可以添加实际的微信授权逻辑(示例)
wx.getUserProfile({
desc: '用于古诗学习助手的登录授权', // 授权说明
success: (res) => {
console.log('授权成功', res);
// 授权成功后,可隐藏提示文字(根据需求)
async request_login() {
try {
this.setData({ isLoading: true });
// 获取用户授权信息
const { userInfo } = await wx.getUserProfile({
desc: '用于古诗学习助手的登录授权'
});
console.log('授权成功', userInfo);
this.setData({ isLoading: false });
// 调用注册接口
await this.registerUser(userInfo);
} catch (err) {
console.log('授权失败', err);
this.setData({
isLoading: false,
errorLoading: true
});
wx.showToast({
title: '授权失败,请重试',
icon: 'none'
});
}
},
// 注册用户方法
async registerUser(userInfo) {
try {
console.log('开始注册用户openid:', this.data.openid);
const result = await wx.cloud.callFunction({
name: 'auth',
data: {
action: 'registerUser',
openid: this.data.openid, // 使用传递的openid
userInfo: userInfo
}
});
console.log('注册结果:', result);
if (result.result.success) {
const userData = result.result.data.userInfo;
// 保存用户信息到本地
wx.setStorageSync('userInfo', userData);
this.setData({ isLoading: false });
wx.navigateTo({
url: '/pages/home/home'
});
},
fail: (err) => {
console.log('授权失败', err);
// 授权失败也隐藏提示文字
this.setData({ isLoading: false ,errorLoading: true});
wx.showToast({
title: '注册成功',
icon: 'success',
success: () => {
// 注册成功后跳转
setTimeout(() => {
// 根据用户角色决定跳转页面role为false时是管理员跳转到pendingQuestion页面
if (userData.role === false) {
wx.reLaunch({
url: `/pages/pendingQuestion/pendingQuestion?openid=${this.data.openid}`
});
} else {
wx.reLaunch({
url: `/pages/index/index?openid=${this.data.openid}`
});
}
}, 1500);
}
});
} else {
throw new Error(result.result.message || '注册失败');
}
});
} catch (error) {
console.error('注册失败:', error);
this.setData({ isLoading: false });
wx.showToast({
title: '注册失败: ' + error.message,
icon: 'none'
});
}
}
})

@ -1,31 +1,127 @@
// pages/pendingQuestion/pendingQuestion.js
Page({
data: {
pendingQuestions: [] // 初始化为空数组
},
onLoad(options) {
this.loadPendingQuestions();
},
onShow() {
// 页面显示时重新加载数据,确保数据最新
this.loadPendingQuestions();
},
/**
* 页面的初始数据
* 从Question集合加载待解决问题
*/
data: {
async loadPendingQuestions() {
try {
const db = wx.cloud.database();
const _ = db.command;
// 从Question集合获取待解决问题status为0或不存在的问题
const result = await db.collection('Question').where({
status: _.in([0, null, undefined])
}).get();
console.log('待解决问题数据:', result.data);
// 转换数据格式,适配现有界面
const pendingQuestions = result.data.map(item => {
return {
id: item._id, // 使用数据库的_id作为唯一标识
question: item.question || '未知问题', // 使用question字段
createdAt: this.formatTime(item.createTime || item.timestamp || new Date()),
status: item.status || 0 // 记录问题状态
};
});
this.setData({
pendingQuestions: pendingQuestions
});
if (pendingQuestions.length === 0) {
console.log('暂无待解决问题');
}
} catch (error) {
console.error('加载待解决问题失败:', error);
// 处理集合不存在的情况
if (error.errCode === -502005) {
console.log('Question集合不存在请先创建集合');
// 保持空数组,界面会显示"暂无待解决问题"
this.setData({
pendingQuestions: []
});
} else {
wx.showToast({
title: '加载失败',
icon: 'none'
});
}
}
},
/**
* 生命周期函数--监听页面加载
* 格式化时间
*/
onLoad(options) {
formatTime(date) {
if (!date) return '未知时间';
const d = new Date(date);
const year = d.getFullYear();
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const hours = d.getHours().toString().padStart(2, '0');
const minutes = d.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
/**
* 跳转到关键词回答页面
*/
goToKeywordAnswer(e) {
const questionId = e.currentTarget.dataset.id;
const question = e.currentTarget.dataset.question;
if (!questionId) {
wx.showToast({
title: '问题ID不存在',
icon: 'none'
});
return;
}
wx.navigateTo({
url: `/pages/keyWord/keyWord?id=${questionId}&question=${encodeURIComponent(question)}`
});
},
/**
* 生命周期函数--监听页面初次渲染完成
* 跳转到待解决问题页面当前页面
*/
onReady() {
goToPendingQuestion() {
// 已经在当前页面,刷新数据
this.loadPendingQuestions();
},
/**
* 跳转到古诗管理页面
*/
goToManagePoems() {
wx.navigateTo({
url: '/pages/managePoems/managePoems'
});
},
/**
* 生命周期函数--监听页面显示
* 生命周期函数--监听页面初次渲染完成
*/
onShow() {
onReady() {
},
@ -47,7 +143,11 @@ Page({
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.loadPendingQuestions().then(() => {
wx.stopPullDownRefresh();
}).catch(() => {
wx.stopPullDownRefresh();
});
},
/**
@ -61,6 +161,9 @@ Page({
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '待解决问题',
path: '/pages/pendingQuestion/pendingQuestion'
};
}
})

@ -1,2 +1,38 @@
<!--pages/pendingQuestion/pendingQuestion.wxml-->
<text>pages/pendingQuestion/pendingQuestion.wxml</text>
<view class="header">
<text class="title">待解决问题</text>
</view>
<view class="question-list">
<view
wx:for="{{pendingQuestions}}"
wx:key="id"
class="question-item"
bindtap="goToKeywordAnswer"
data-id="{{item.id}}"
data-question="{{item.question}}"
>
<view class="question-content">
<text class="question-text">{{item.question}}</text>
</view>
<view class="question-meta">
<text class="question-time">{{item.createdAt}}</text>
<text class="question-arrow"></text>
</view>
</view>
<!-- 空状态 -->
<view wx:if="{{pendingQuestions.length === 0}}" class="empty-state">
<text>暂无待解决问题</text>
</view>
</view>
<!-- 底部导航栏 -->
<view class="tab-bar">
<view class="tab-item active" bindtap="goToPendingQuestion">
<text class="tab-text">待解决问题</text>
</view>
<view class="tab-item" bindtap="goToManagePoems">
<text class="tab-text">管理古诗</text>
</view>
</view>

@ -1 +1,102 @@
/* pages/pendingQuestion/pendingQuestion.wxss */
/* pages/pendingQuestion/pendingQuestion.wxss */
page {
background-color: #f5f5f5;
padding-bottom: 120rpx; /* 为底部导航栏留出空间 */
}
.header {
text-align: center;
margin-bottom: 15rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.question-list {
background-color: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.question-item {
padding: 30rpx;
border-bottom: 1rpx solid rgb(123, 135, 168);
display: flex;
flex-direction: column;
gap: 15rpx;
}
.question-item:last-child {
border-bottom: none;
}
.question-content {
flex: 1;
}
.question-text {
font-size: 32rpx;
color: #333;
line-height: 48rpx;
}
.question-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.question-time {
font-size: 26rpx;
color: #999;
}
/* 底部导航栏样式 */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 120rpx;
background-color: #fff;
display: flex;
border-top: 1rpx solid #e0e0e0;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 999;
}
.tab-item {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.tab-item.active {
background-color: #f8f8f8;
}
.tab-text {
font-size: 32rpx;
color: #666;
}
.tab-item.active .tab-text {
color: #07c160;
font-weight: bold;
}
.question-arrow {
font-size: 36rpx;
color: #999;
}
.empty-state {
padding: 80rpx 0;
text-align: center;
color: #999;
font-size: 32rpx;
}

@ -1,66 +0,0 @@
// pages/questionAnswer/questionAnswer.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

@ -1,3 +0,0 @@
{
"usingComponents": {}
}

@ -1,2 +0,0 @@
<!--pages/questionAnswer/questionAnswer.wxml-->
<text>pages/questionAnswer/questionAnswer.wxml</text>

@ -1 +0,0 @@
/* pages/questionAnswer/questionAnswer.wxss */

@ -1,66 +1,621 @@
// pages/recite/recite.js
const recorderManager = wx.getRecorderManager()
const options = {
duration: 60000,//指定录音的时长,单位 ms
sampleRate: 16000,//采样率
numberOfChannels: 1,//录音通道数
encodeBitRate: 48000,//编码码率
format: 'pcm',//音频格式,有效值 aac/mp3
}
var filesize,tempFilePath
Page({
/**
* 页面的初始数据
*/
data: {
token:'',
content:'',
poemId: '',
poemTitle: '',
poemAuthor: '',
originalText: '',
accuracyRate: 0,
showResult: false,
startReciteTime: 0, // 开始背诵时间
reciteDuration: '', // 背诵时长
reciteDateTime: '', // 背诵时间
openid: '' // 添加 openid
},
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('背诵页面参数:', options);
// 接收从学习页面传递的参数
if (options.poemData) {
try {
const poemData = decodeURIComponent(options.poemData);
const poem = JSON.parse(poemData);
console.log('从学习页面传递的诗歌数据:', poem);
this.setData({
poemId: poem._id || options.id,
poemTitle: poem.title || '',
poemAuthor: poem.author || '',
originalText: poem.content || '' // 直接从传递的数据中获取原文
});
} catch (error) {
console.error('解析诗歌数据失败:', error);
// 如果解析失败尝试使用id获取
if (options.id) {
this.setData({
poemId: options.id
});
this.getPoemDetail(options.id);
}
}
}
// 接收从index页面传递的参数
else if (options.id && options.title && options.author) {
this.setData({
poemId: options.id,
poemTitle: decodeURIComponent(options.title),
poemAuthor: decodeURIComponent(options.author)
});
// 获取古诗原文
this.getPoemDetail(options.id);
}
// 只传递了id的情况
else if (options.id) {
this.setData({
poemId: options.id
});
// 获取古诗原文
this.getPoemDetail(options.id);
}
//获取用户openid
this.getUserOpenId();
//获取storge中的token
let that=this;
wx.getStorage({
key:'expires_in',
success(res){
console.log("缓存中有access_token")
console.log("token失效时间",res.data)
const newT = new Date().getTime();
// 用当前时间和存储的时间判断token是否已过期
if (newT > parseInt(res.data)) {
console.log("token过期重新获取token")
that.getToken();
} else {
console.log("获取本地缓存的token")
that.setData({
token:wx.getStorageSync('access_token')
});
}
},fail(){
console.log("缓存中没有access_token")
that.getToken();
}
});
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
// 获取用户 openid
async getUserOpenId() {
try {
const res = await wx.cloud.callFunction({
name: 'getOpenId'
})
this.setData({
openid: res.result.openid
})
console.log('获取用户openid:', this.data.openid)
} catch (error) {
console.error('获取openid失败:', error)
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 保存背诵记录到数据库
async saveReviewRecord(reviewData) {
try {
console.log('开始保存背诵记录')
console.log('传入的reviewData完整内容:', JSON.stringify(reviewData))
console.log('传入的reviewData数据结构:', Object.keys(reviewData || {}).join(', '))
// 验证必要字段
if (!reviewData || !reviewData.poemId || !reviewData.openid) {
console.error('缺少必要字段')
wx.showToast({
title: '数据保存失败:缺少必要字段',
icon: 'none'
})
return false
}
// 先检查并删除可能存在的content和score字段
if (reviewData.hasOwnProperty('content')) {
console.warn('前端发现content字段将被删除:', reviewData.content)
delete reviewData.content
}
if (reviewData.hasOwnProperty('score')) {
console.warn('前端发现score字段将被删除:', reviewData.score)
delete reviewData.score
}
// 详细日志记录清理后的数据结构
console.log('前端清理后reviewData的数据结构:', Object.keys(reviewData).join(', '))
// 创建一个全新的干净对象,不继承任何原型属性,只包含我们需要的字段
const dataToSave = {};
dataToSave.openid = reviewData.openid;
dataToSave.poemId = reviewData.poemId;
dataToSave.poemName = reviewData.poemName || this.data.poemTitle || '';
dataToSave.accuracy = Number(reviewData.accuracy || 0);
dataToSave.duration = Number(reviewData.duration || 0);
dataToSave.reciteDateTime = reviewData.reciteDateTime || new Date().toISOString();
// 再次检查dataToSave中是否存在content和score字段
if (dataToSave.hasOwnProperty('content')) {
console.error('警告: 前端dataToSave中仍存在content字段将被强制删除!')
delete dataToSave.content;
}
if (dataToSave.hasOwnProperty('score')) {
console.error('警告: 前端dataToSave中仍存在score字段将被强制删除!')
delete dataToSave.score;
}
// 详细日志记录最终发送的数据
console.log('前端最终发送到数据库的数据结构:', Object.keys(dataToSave).join(', '))
console.log('前端最终发送到数据库的数据:', JSON.stringify(dataToSave))
// 调用云函数
const result = await wx.cloud.callFunction({
name: 'reviewManagement',
data: {
action: 'addReviewRecord',
reviewData: dataToSave
}
})
// 处理结果
if (result && result.result && result.result.success) {
console.log('背诵记录保存成功:', result.result._id)
console.log('云函数返回的保存字段列表:', result.result.savedFields?.join(', ') || '未知')
return true
} else {
console.error('背诵记录保存失败:', result.result?.message || '未知错误')
return false
}
} catch (error) {
console.error('保存背诵记录异常:', error)
return false
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
// 计算背诵难度
calculateDifficulty(duration, originalText) {
// 基于原文长度和背诵时间计算难度
const textLength = (originalText || '').length
const avgSpeed = textLength / Math.max(1, duration)
if (textLength > 200 || avgSpeed < 0.5) {
return 'difficult'
} else if (textLength > 100 || avgSpeed < 1) {
return 'medium'
} else {
return 'easy'
}
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
// 获取古诗详情
async getPoemDetail(poemId) {
try {
console.log('调用云函数获取古诗详情ID:', poemId);
const result = await wx.cloud.callFunction({
name: 'poemManagement',
data: {
action: 'getPoemDetail',
id: poemId
}
});
console.log('云函数返回结果:', result);
if (result.result && result.result.success) {
// 直接使用result.result.data因为云函数返回的结构是{success: true, data: poem对象}
const poem = result.result.data;
this.setData({
originalText: poem.content || ''
});
console.log('获取到古诗原文:', this.data.originalText);
} else {
const errorMessage = result.result?.message || '获取古诗详情失败';
console.error('获取古诗详情失败:', errorMessage);
wx.showToast({
title: errorMessage,
icon: 'none'
});
}
} catch (error) {
console.error('获取古诗详情出错:', error);
wx.showToast({
title: '网络错误,请重试',
icon: 'none'
});
}
},
// 清理文本,去除标点符号和空白字符
cleanText(text) {
if (!text) return '';
// 转换为字符串
text = String(text);
// 去除所有空格(包括全角空格)、换行符、制表符
text = text.replace(/[\s\n\t\u3000]/g, '');
// 去除所有标点符号(包括中文标点)
text = text.replace(/[\p{P}\p{S}]/gu, '');
// 去除所有特殊符号
text = text.replace(/[!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]/g, '');
console.log('清理后的文本:', text);
return text;
},
// 计算文本相似度(正确率)
calculateAccuracy(recognizedText, originalText) {
const cleanedRecognized = this.cleanText(recognizedText);
const cleanedOriginal = this.cleanText(originalText);
console.log('清理后的识别文本:', cleanedRecognized);
console.log('清理后的原文:', cleanedOriginal);
console.log('原文长度:', cleanedOriginal.length);
if (!cleanedOriginal || cleanedOriginal.length === 0) {
console.log('原文为空或长度为0');
return 0;
}
if (!cleanedRecognized || cleanedRecognized.length === 0) {
console.log('识别文本为空或长度为0');
return 0;
}
// 使用动态规划计算最长公共子序列长度
const m = cleanedRecognized.length;
const n = cleanedOriginal.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (cleanedRecognized[i - 1] === cleanedOriginal[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
const lcsLength = dp[m][n];
console.log('最长公共子序列长度:', lcsLength);
// 计算正确率(以原文长度为基准)
const accuracyRate = Math.round((lcsLength / cleanedOriginal.length) * 100);
console.log('计算得到的正确率:', accuracyRate + '%');
return accuracyRate;
},
// 检查背诵结果
// 修改 checkRecitation 方法,在计算完正确率后保存记录
async checkRecitation() {
console.log('开始检查背诵结果');
console.log('当前识别文本类型:', typeof this.data.content);
console.log('当前识别文本内容:', this.data.content);
console.log('当前古诗原文:', this.data.originalText);
// 确保content是字符串格式
let contentText = this.data.content;
console.log('开始处理识别文本格式...');
if (Array.isArray(contentText)) {
console.log('识别文本是数组格式,长度:', contentText.length);
// 处理可能的嵌套数组情况
if (contentText.length > 0 && Array.isArray(contentText[0])) {
console.log('识别文本是嵌套数组');
contentText = contentText[0].join('');
} else {
contentText = contentText.join('');
}
} else if (contentText !== null && contentText !== undefined) {
console.log('识别文本不是数组,转换为字符串');
contentText = String(contentText);
} else {
console.log('识别文本为空');
contentText = '';
}
console.log('处理后的识别文本:', contentText);
if (!contentText || contentText.trim() === '') {
wx.showToast({
title: '请先进行录音',
icon: 'none'
});
console.log('识别文本为空,无法检查');
return;
}
if (!this.data.originalText || this.data.originalText.trim() === '') {
wx.showToast({
title: '获取古诗原文失败',
icon: 'none'
});
console.log('古诗原文为空,无法检查');
// 尝试重新获取古诗原文
if (this.data.poemId) {
console.log('尝试重新获取古诗原文');
this.getPoemDetail(this.data.poemId);
}
return;
}
// 计算正确率
const accuracyRate = this.calculateAccuracy(contentText, this.data.originalText);
console.log('检查背诵结果完成,正确率:', accuracyRate + '%');
// 计算背诵时长
let reciteDuration = '';
let durationSeconds = 0;
if (this.data.startReciteTime > 0) {
const endTime = new Date().getTime();
const durationMs = endTime - this.data.startReciteTime;
durationSeconds = Math.floor(durationMs / 1000);
// 格式化时长显示 (MM:SS)
const minutes = Math.floor(durationSeconds / 60);
const seconds = durationSeconds % 60;
reciteDuration = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
console.log('背诵时长:', reciteDuration);
} else {
reciteDuration = '--:--';
console.log('未记录开始时间,无法计算背诵时长');
}
// 记录和格式化背诵时间
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const reciteDateTime = `${year}-${month}-${day} ${hours}:${minutes}`;
console.log('背诵时间:', reciteDateTime);
// 准备保存到数据库的数据包含所有必要字段确保古诗ID和古诗名挨着放
const reviewRecord = {
openid: this.data.openid, // 用户ID
poemId: this.data.poemId, // 古诗ID
poemName: this.data.poemTitle, // 古诗名称与古诗ID挨着放
accuracy: accuracyRate, // 正确率
duration: durationSeconds, // 背诵时长(秒)
reciteDateTime: reciteDateTime // 背诵时间
};
// 先显示结果然后异步保存避免UI阻塞
this.setData({
accuracyRate: accuracyRate,
showResult: true,
reciteDuration: reciteDuration,
reciteDateTime: reciteDateTime
});
// 异步保存到数据库
try {
const saveSuccess = await this.saveReviewRecord(reviewRecord);
if (saveSuccess) {
console.log('背诵记录保存成功');
// saveReviewRecord 内部已经有成功提示
} else {
console.log('背诵记录保存失败,但不影响结果显示');
// 不重复显示错误提示saveReviewRecord 内部已经处理
}
} catch (err) {
console.error('保存背诵记录时发生异常:', err);
// 异常已在 saveReviewRecord 内部处理
}
// 延迟显示根据正确率的提示消息,避免与保存结果提示重叠
setTimeout(() => {
console.log('背诵正确率:', accuracyRate + '%');
console.log('背诵时长:', reciteDuration);
// 根据正确率给出提示
let message = '';
if (accuracyRate >= 90) {
message = '太棒了!背诵得很准确!';
} else if (accuracyRate >= 70) {
message = '不错的背诵!继续加油!';
} else if (accuracyRate >= 50) {
message = '还可以,再练习一下会更好!';
} else {
message = '需要多练习,加油!';
}
wx.showToast({
title: message,
icon: 'none',
duration: 2000
});
}, 1000); // 延迟1秒显示提示消息
},
/**
* 页面相关事件处理函数--监听用户下拉动作
* 重置背诵状态清除识别结果和正确率
*/
onPullDownRefresh() {
resetRecitation: function() {
console.log('重置背诵状态');
// 重置所有相关数据
this.setData({
content: '',
showResult: false,
accuracyRate: 0,
startReciteTime: 0,
reciteDuration: '',
reciteDateTime: ''
});
console.log('重置完成,准备新的背诵');
},
/**
* 页面上拉触底事件的处理函数
* 当用户编辑识别结果时触发
*/
onReachBottom() {
onContentInput(e) {
console.log('用户编辑了识别结果', e.detail.value);
this.setData({
content: e.detail.value
});
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
// 获取token
getToken:function(){
let that=this;
let ApiKey='qdHcePu7v0WpIlJnaeGJZZqp';
let SecretKey='nuZCuBziZoO2gjE6NGEMKCdxKs4sbaNq';
const url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id='+ApiKey+'&client_secret='+SecretKey
wx.request({
url:url,
method: 'POST',
success(res){
console.log("创建access_token成功",res)
//将access_token存储到storage中
wx.setStorage({
key:'access_token',
data:res.data.access_token
});
var date=new Date().getTime();
let time=date+2592000*1000;
console.log('三十天后的时间',time);
wx.setStorage({
key:'expires_in',
data:time
});
that.setData({
token:res.data.access_token
});
},
fail(err) {
console.error('获取token失败:', err);
}
});
},
//开始录音
touchStart: function () {
const that = this; // 保存this引用
wx.authorize({
scope: 'scope.record',
success() {
console.log("录音授权成功");
// 记录开始背诵的时间
const startTime = new Date().getTime();
console.log('开始背诵时间:', startTime);
that.setData({
startReciteTime: startTime
});
recorderManager.start(options);
recorderManager.onStart(() => {
console.log('recorder start')
});
},
fail() {
console.log("录音失败");
}
})
},
}
//停止录音
touchEnd: function () {
let that = this
recorderManager.stop();
recorderManager.onStop((res) => {
console.log('文件路径==', res)
tempFilePath= res.tempFilePath;
//获取文件长度
wx.getFileSystemManager().getFileInfo({
filePath: tempFilePath,
success: function (res) {
filesize = res.size
console.log('文件长度', res)
that.shibie()
}, fail: function (res) {
console.log("读取文件长度错误",res);
}
})
});
},
//语音识别
shibie(){
let that = this
wx.getFileSystemManager().readFile({
filePath: tempFilePath,
encoding: 'base64',
success: function (res) {
wx.request({
url: 'http://vop.baidu.com/server_api',
data: {
token: that.data.token,
cuid: "12_56",
format: 'pcm',
rate: 16000,
channel: 1,
speech: res.data,
len: filesize
},
headers: {
'Content-Type': 'application/json'
},
method: "post",
success: function (res) {
if (res.data.result == '') {
wx.showModal({
title: '提示',
content: '听不清楚,请重新说一遍!',
showCancel: false
})
return;
}
console.log("识别成功==",res.data);
let recognizedText = res.data.result;
console.log("原始识别结果格式:", typeof recognizedText);
console.log("原始识别结果内容:", recognizedText);
// 百度API通常返回数组格式的结果需要正确处理
if (Array.isArray(recognizedText)) {
console.log("处理数组格式结果");
// 百度API的标准格式是返回字符串数组如["识别结果"]
if (recognizedText.length > 0) {
recognizedText = recognizedText[0];
} else {
recognizedText = "";
}
}
// 确保最终是字符串格式
if (recognizedText !== null && recognizedText !== undefined) {
recognizedText = String(recognizedText);
} else {
recognizedText = "";
}
console.log("最终处理后的识别文本:", recognizedText);
that.setData({
content: recognizedText
});
console.log('语音识别完成,识别文本:', recognizedText);
},
fail: function (res) {
console.log("失败",res);
}
}); //语音识别结束
}
})
}
})

@ -1,2 +1,64 @@
<!--pages/recite/recite.wxml-->
<text>pages/recite/recite.wxml</text>
<view class="recite-container">
<!-- 头部标题 -->
<view class="header">
<text class="title">背诵《{{poemTitle}}》</text>
<text class="subtitle">{{poemAuthor}}</text>
</view>
<!-- 语音识别结果 -->
<view class="recognition-section">
<text class="section-title">识别结果:</text>
<textarea class="result-textarea" placeholder='等待说话...' value='{{content}}' bindinput="onContentInput"></textarea>
</view>
<!-- 检查按钮 -->
<view class="check-section">
<button class="btn-check" type="primary" bindtap="checkRecitation" wx:if="{{content && !showResult}}">检查背诵</button>
</view>
<!-- 正确率显示 -->
<view class="result-section" wx:if="{{showResult}}">
<!-- 进度条样式正确率显示 -->
<view class="accuracy-progress-container">
<view class="accuracy-header">
<text class="accuracy-title">背诵评分</text>
<text class="accuracy-value">{{accuracyRate}}%</text>
</view>
<!-- 背诵时长和时间显示 -->
<view class="info-section">
<!-- 背诵用时 -->
<view class="info-item">
<text class="info-label">背诵用时</text>
<text class="info-value duration-value">{{reciteDuration}}</text>
</view>
<!-- 背诵时间 -->
<view class="info-item">
<text class="info-label">背诵时间</text>
<text class="info-value date-value">{{reciteDateTime}}</text>
</view>
</view>
<!-- 进度条 -->
<view class="progress-bar">
<view class="progress-bg">
<view class="progress-fill"
style="width: {{accuracyRate}}%; background: {{accuracyRate >= 90 ? '#4CAF50' : accuracyRate >= 70 ? '#FFC107' : accuracyRate >= 50 ? '#FF9800' : '#F44336'}};">
</view>
</view>
</view>
<!-- 评价文字 -->
<text class="accuracy-comment">{{accuracyRate >= 90 ? '优秀!背诵得非常准确' : accuracyRate >= 70 ? '良好!继续保持' : accuracyRate >= 50 ? '一般,还需要练习' : '需要多加练习'}}</text>
</view>
<button class="btn-reset" type="default" bindtap="resetRecitation">重新背诵</button>
</view>
<!-- 录音控制按钮 -->
<view class="control-buttons" wx:if="{{!showResult}}">
<button class="btn-record-start" type="primary" size="mini" bindtap="touchStart">开始录音</button>
<button class="btn-record-stop" type="warn" size="mini" bindtap="touchEnd">结束录音</button>
</view>
</view>

@ -1 +1,218 @@
/* pages/recite/recite.wxss */
/* pages/recite/recite.wxss */
.recite-container {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
/* 头部样式 */
.header {
text-align: center;
margin-bottom: 40rpx;
padding: 30rpx 0;
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
border-radius: 15rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 32rpx;
color: #666;
}
/* 原文和识别结果区域 */
.original-section,
.recognition-section {
background: #fff;
padding: 30rpx;
margin-bottom: 30rpx;
border-radius: 15rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.original-text {
font-size: 34rpx;
line-height: 1.8;
color: #444;
white-space: pre-wrap;
word-break: break-all;
}
.result-textarea {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
font-size: 34rpx;
line-height: 1.6;
color: #333;
background: #f9f9f9;
border: 1rpx solid #ddd;
border-radius: 10rpx;
box-sizing: border-box;
}
/* 正确率显示区域 */
.result-section {
text-align: center;
margin: 40rpx 0;
}
/* 进度条样式正确率显示 */
.accuracy-progress-container {
background: #fff;
padding: 40rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
margin-bottom: 30rpx;
}
.accuracy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
/* 背诵信息显示区域 */
.info-section {
margin-bottom: 30rpx;
padding: 15rpx 0;
border-top: 1rpx solid #f0f0f0;
}
/* 信息项样式 */
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
}
.info-label {
font-size: 32rpx;
color: #666;
}
.info-value {
font-size: 34rpx;
font-weight: bold;
}
/* 背诵时长样式 */
.duration-value {
color: #007AFF;
letter-spacing: 2rpx;
}
/* 背诵时间样式 */
.date-value {
color: #34C759;
font-family: 'Courier New', monospace;
}
.accuracy-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.accuracy-value {
font-size: 48rpx;
font-weight: bold;
color: #4CAF50;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
/* 进度条样式 */
.progress-bar {
margin-bottom: 30rpx;
}
.progress-bg {
width: 100%;
height: 30rpx;
background: #f0f0f0;
border-radius: 15rpx;
overflow: hidden;
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.progress-fill {
height: 100%;
border-radius: 15rpx;
transition: width 0.6s ease;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
/* 评价文字 */
.accuracy-comment {
font-size: 32rpx;
color: #666;
display: block;
padding: 20rpx 0;
background: #f9f9f9;
border-radius: 10rpx;
margin-top: 10rpx;
}
/* 检查按钮区域 */
.check-section {
text-align: center;
margin: 40rpx 0;
}
.btn-check {
width: 60%;
font-size: 36rpx;
padding: 20rpx 0;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
border: none;
color: white;
}
/* 重置按钮 */
.btn-reset {
margin-top: 30rpx;
font-size: 32rpx;
width: 50%;
}
/* 控制按钮区域 */
.control-buttons {
display: flex;
justify-content: space-around;
margin-top: 40rpx;
padding-bottom: 40rpx;
}
.btn-record-start,
.btn-record-stop {
width: 280rpx;
font-size: 34rpx;
padding: 20rpx 0;
}
/* 响应式设计 */
@media screen and (min-width: 768px) {
.recite-container {
max-width: 800rpx;
margin: 0 auto;
}
}

@ -1,52 +1,747 @@
// pages/review/review.js
// SM2算法实现
class SM2 {
constructor() {
this.ef = 2.5; // 初始易度因子Easiness Factor
this.reps = 0; // 重复次数
this.lastInterval = 0; // 上次复习间隔
}
/**
* 根据用户回忆质量计算下次复习间隔和更新易度因子
* @param {number} quality - 回忆质量 (0-5): 0=完全错误, 5=完美回忆
* @returns {object} - {nextInterval: 下次复习间隔(), newEF: 新的易度因子}
*/
calculateNextReview(quality) {
// 根据回忆质量更新易度因子
if (quality < 3) { // 回忆失败 (0-2分)
// 重置重复次数间隔设为1天
this.reps = 0;
this.lastInterval = 1;
// 降低易度因子但不低于1.3
this.ef = Math.max(1.3, this.ef - 0.2);
} else { // 回忆成功 (3-5分)
// 第一次成功后,使用特定的计算方式
if (this.reps === 0) {
this.lastInterval = 1; // 第一次成功后1天后复习
} else if (this.reps === 1) {
this.lastInterval = 6; // 第二次成功后6天后复习
} else {
// 之后使用易度因子计算
this.lastInterval = Math.round(this.lastInterval * this.ef);
}
// 更新易度因子公式EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
this.ef = Math.max(1.3, this.ef + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)));
this.reps++;
}
return {
nextInterval: this.lastInterval,
newEF: this.ef,
repetitions: this.reps
};
}
/**
* 基于艾宾浩斯公式计算记忆保留率
* R = e^(-t/S)其中t是时间间隔(小时)S是记忆稳定性
* @param {number} hoursSinceLastReview - 距离上次复习的小时数
* @param {number} stability - 记忆稳定性可从易度因子推导
* @returns {number} - 记忆保留率 (0-100%)
*/
calculateRetentionRate(hoursSinceLastReview, stability) {
// 默认稳定性,如果未提供则基于易度因子计算
const S = stability || (this.ef * 10); // 简化:易度因子*10作为稳定性
const R = Math.exp(-hoursSinceLastReview / S);
return Math.round(R * 100);
}
}
Page({
/**
* 页面的初始数据
*/
data: {
// 遗忘曲线数据 - 显示未来7天的复习计划
复习计划数据: [],
// 需要复习的古诗列表
poemsToReview: [],
// Canvas尺寸
canvasWidth: 0,
canvasHeight: 300,
// 今日需要复习的总数
todayReviewCount: 0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
onLoad: function (options) {
// 初始化数据加载
this.loadPoemsToReview().then(() => {
// 数据加载完成后绘制图表
this.drawReviewPlanChart();
});
},
/**
* 加载需要复习的古诗数据
*/
async loadPoemsToReview() {
try {
wx.showLoading({ title: '加载复习计划...' })
// 从数据库加载真实背诵记录
let reciteRecords = await this.loadReciteRecordsFromDatabase();
// 如果没有记录或记录为空,使用模拟数据
let recordsToUse = reciteRecords && reciteRecords.length > 0 ? reciteRecords : this.getMockReciteRecords();
// 增强:为每条记录获取完整的诗歌信息,特别是作者信息
if (reciteRecords && reciteRecords.length > 0) {
recordsToUse = await this.enrichRecordsWithPoemDetails(reciteRecords);
}
// 使用SM2算法计算复习计划
const poemsWithReviewPlan = this.calculateReviewPlan(recordsToUse);
// 生成未来7天的复习计划数据用于图表显示
const reviewPlanData = this.generateReviewPlanData(poemsWithReviewPlan);
// 计算今天需要复习的数量
const todayReviewCount = poemsWithReviewPlan.filter(p => p.needsReviewToday).length;
this.setData({
poemsToReview: poemsWithReviewPlan,
复习计划数据: reviewPlanData,
todayReviewCount
});
wx.hideLoading()
return poemsWithReviewPlan;
} catch (error) {
wx.hideLoading()
console.error('加载复习数据失败:', error);
// 出错时使用模拟数据
const mockRecords = this.getMockReciteRecords();
const poemsWithReviewPlan = this.calculateReviewPlan(mockRecords);
const reviewPlanData = this.generateReviewPlanData(poemsWithReviewPlan);
const todayReviewCount = poemsWithReviewPlan.filter(p => p.needsReviewToday).length;
this.setData({
poemsToReview: poemsWithReviewPlan,
复习计划数据: reviewPlanData,
todayReviewCount
});
return poemsWithReviewPlan;
}
},
/**
* 为复习记录获取完整的诗歌详情信息
*/
async enrichRecordsWithPoemDetails(records) {
try {
console.log('正在获取完整诗歌信息...');
// 为每条记录异步获取诗歌详情
const enrichedRecords = [];
for (const record of records) {
try {
// 调用云函数获取诗歌详情
const { result } = await wx.cloud.callFunction({
name: 'poemManagement',
data: {
action: 'getPoemDetail',
id: record.poemId || record._id
}
});
// 如果成功获取到详情,更新作者信息
if (result && result.success && result.data) {
const poemDetail = result.data;
// 创建增强后的记录,保留原始字段,更新作者信息
const enrichedRecord = {
...record,
poemAuthor: poemDetail.author || record.poemAuthor || record.author || '未知作者',
poemTitle: poemDetail.title || record.poemTitle || record.poemName || '未知标题'
};
enrichedRecords.push(enrichedRecord);
console.log(`成功获取诗歌ID: ${record.poemId || record._id} 的完整信息`);
} else {
// 获取失败时保留原始记录
console.log(`无法获取诗歌ID: ${record.poemId || record._id} 的详情,使用原始数据`);
enrichedRecords.push(record);
}
} catch (error) {
// 单条记录获取失败不影响整体
console.error(`获取诗歌详情失败: ${record.poemId || record._id}`, error);
enrichedRecords.push(record);
}
// 添加小延迟避免请求过于密集
await new Promise(resolve => setTimeout(resolve, 50));
}
console.log('诗歌信息获取完成');
return enrichedRecords;
} catch (error) {
console.error('增强诗歌记录时发生错误:', error);
// 如果整体出错,返回原始记录
return records;
}
},
/**
* 获取模拟的背诵记录数据
*/
getMockReciteRecords() {
// 仅在数据库加载失败时使用模拟数据
const now = Date.now();
const dayMs = 24 * 60 * 60 * 1000;
return [
{
poemId: 'poet_001',
poemTitle: '静夜思',
poemAuthor: '李白',
lastReciteTime: now - dayMs, // 1天前
accuracy: 85,
reciteCount: 2,
ef: 2.3,
nextReviewInterval: 1
},
{
poemId: 'poet_002',
poemTitle: '春晓',
poemAuthor: '孟浩然',
lastReciteTime: now - 3 * dayMs, // 3天前
accuracy: 60,
reciteCount: 1,
ef: 2.0,
nextReviewInterval: 1
},
{
poemId: 'poet_003',
poemTitle: '望庐山瀑布',
poemAuthor: '李白',
lastReciteTime: now - 0.5 * dayMs, // 0.5天前
accuracy: 95,
reciteCount: 3,
ef: 2.7,
nextReviewInterval: 6
},
{
poemId: 'poet_004',
poemTitle: '登鹳雀楼',
poemAuthor: '王之涣',
lastReciteTime: now - 7 * dayMs, // 7天前
accuracy: 70,
reciteCount: 2,
ef: 2.1,
nextReviewInterval: 3
}
]
},
/**
* 从数据库加载真实的背诵记录
*/
async loadReciteRecordsFromDatabase() {
try {
// 首先获取用户openid
const { result } = await wx.cloud.callFunction({
name: 'getOpenId'
})
if (!result.openid) {
throw new Error('获取用户信息失败')
}
// 查询用户的背诵记录
const db = wx.cloud.database()
const records = await db.collection('Review')
.where({
openid: result.openid
})
.orderBy('reciteDateTime', 'desc')
.get()
console.log('从数据库获取的背诵记录:', records.data);
// 转换数据库记录格式,确保字段名一致
const formattedRecords = records.data.map(record => {
return {
poemId: record.poemId,
poemTitle: record.poemName || '未知标题',
poemAuthor: record.poemAuthor || record.author || '未知作者', // 尝试多种可能的作者字段
lastReciteTime: new Date(record.reciteDateTime).getTime(),
accuracy: record.accuracy || record.matchRate || 0,
reciteCount: record.reciteCount || 1,
ef: record.ef || 2.5,
nextReviewInterval: record.nextReviewInterval || 1
};
});
return formattedRecords;
} catch (error) {
console.error('加载背诵记录失败:', error);
// 如果加载失败,返回空数组,让上层函数处理
return [];
}
},
/**
* 手动初始化数据库
*/
async initDatabaseManually() {
try {
wx.showLoading({ title: '初始化数据库...' })
const result = await wx.cloud.callFunction({
name: 'initDatabase'
})
wx.hideLoading()
if (result.result && result.result.success) {
wx.showToast({
title: '数据库初始化成功',
icon: 'success'
})
// 重新加载数据
this.loadPoemsToReview()
} else {
wx.showToast({
title: result.result?.message || '初始化失败',
icon: 'none'
})
}
} catch (error) {
wx.hideLoading()
wx.showToast({
title: '初始化失败: ' + error.message,
icon: 'none'
})
}
},
/**
* 基于SM2算法计算复习计划和优先级
*/
calculateReviewPlan(records) {
const now = Date.now();
const sm2 = new SM2();
// 去重处理,确保每首诗只出现一次
const uniqueRecords = [];
const poemIdSet = new Set();
// 优先保留最新的记录(根据传入的顺序,假设前面的记录更新)
records.forEach(record => {
const poemId = record.poemId || record._id;
if (poemId && !poemIdSet.has(poemId)) {
poemIdSet.add(poemId);
uniqueRecords.push(record);
}
});
console.log('去重后的记录数量:', uniqueRecords.length);
// 计算每首诗的复习状态和下次复习时间
const poemsWithReviewStatus = uniqueRecords.map(record => {
// 标准化字段名,确保兼容性
const poem = {
id: record.poemId || record._id,
title: record.poemTitle || record.poemName || '未知标题',
author: record.poemAuthor || '未知作者',
lastReciteTime: record.lastReciteTime || (record.reciteDateTime ? new Date(record.reciteDateTime).getTime() : now - 24 * 60 * 60 * 1000),
accuracy: record.accuracy || record.matchRate || 0,
reciteCount: record.reciteCount || 0,
ef: record.ef || 2.5, // 易度因子
nextReviewInterval: record.nextReviewInterval || 1 // 下次复习间隔
};
// 计算距离上次背诵的时间(天)
const daysSinceLastRecite = (now - poem.lastReciteTime) / (24 * 60 * 60 * 1000);
// 根据正确率转换为SM2质量评分 (0-5分)
let quality = 0;
if (poem.accuracy >= 90) quality = 5;
else if (poem.accuracy >= 80) quality = 4;
else if (poem.accuracy >= 60) quality = 3;
else if (poem.accuracy >= 40) quality = 2;
else if (poem.accuracy >= 20) quality = 1;
// 使用SM2计算下次复习间隔
sm2.ef = poem.ef;
sm2.reps = poem.reciteCount;
sm2.lastInterval = poem.nextReviewInterval;
const reviewResult = sm2.calculateNextReview(quality);
// 判断是否需要今天复习
const needsReviewToday = daysSinceLastRecite >= poem.nextReviewInterval - 0.1; // 允许0.1天的误差
// 计算记忆保留率(基于艾宾浩斯公式)
const hoursSinceLastReview = daysSinceLastRecite * 24;
const retentionRate = sm2.calculateRetentionRate(hoursSinceLastReview, poem.ef * 10);
return {
...poem,
daysSinceLastRecite: Math.round(daysSinceLastRecite * 10) / 10, // 保留一位小数
nextReviewInterval: reviewResult.nextInterval,
newEF: reviewResult.newEF,
needsReviewToday,
retentionRate,
reviewUrgency: needsReviewToday ? (100 - retentionRate) : 0
};
});
// 按复习紧急程度排序(今天需要复习的排前面,按记忆保留率从低到高)
poemsWithReviewStatus.sort((a, b) => {
if (a.needsReviewToday && !b.needsReviewToday) return -1;
if (!a.needsReviewToday && b.needsReviewToday) return 1;
return b.reviewUrgency - a.reviewUrgency;
});
return poemsWithReviewStatus;
},
/**
* 生成未来7天的复习计划数据用于图表显示
*/
generateReviewPlanData(poems) {
const planData = [];
const now = new Date();
// 初始化未来7天的数据
for (let i = 0; i < 7; i++) {
const date = new Date(now);
date.setDate(date.getDate() + i);
planData.push({
day: i === 0 ? '今天' : i === 1 ? '明天' : `${i}天后`,
dateStr: `${date.getMonth() + 1}/${date.getDate()}`,
count: 0
});
}
// 统计每天需要复习的古诗数量
poems.forEach(poem => {
// 优先使用needsReviewToday标志来判断是否今天需要复习
if (poem.needsReviewToday) {
// 如果标记为今天需要复习,直接计入今天的数量
planData[0].count++;
return; // 跳过后续计算
}
// 对于不是今天需要复习的,正常计算下次复习日期
const lastReviewDate = new Date(poem.lastReciteTime);
const nextReviewDate = new Date(lastReviewDate);
nextReviewDate.setDate(nextReviewDate.getDate() + poem.nextReviewInterval);
// 计算下次复习日期距离今天的天数(使用宽松计算,不四舍五入)
const daysUntilNextReview = Math.floor((nextReviewDate - now) / (24 * 60 * 60 * 1000));
// 更新对应天数的复习数量
if (daysUntilNextReview >= 0 && daysUntilNextReview < 7) {
planData[daysUntilNextReview].count++;
}
});
return planData;
},
/**
* 跳转到学习页面
*/
goToStudy(e) {
const poemId = e.currentTarget.dataset.id;
if (poemId) {
wx.navigateTo({
url: `/pages/study/study?id=${poemId}`,
fail: function(res) {
console.error('跳转到学习页面失败:', res);
wx.showToast({
title: '跳转失败,请重试',
icon: 'none'
});
}
});
} else {
console.error('缺少诗歌ID参数');
wx.showToast({
title: '参数错误',
icon: 'none'
});
}
},
/**
* 跳转到背诵页面
*/
goToRecite(e) {
const poemId = e.currentTarget.dataset.id;
const poemTitle = e.currentTarget.dataset.title;
const poemAuthor = e.currentTarget.dataset.author;
if (poemId) {
// 查找对应的诗歌完整数据
const poem = this.data.poemsToReview.find(p => p.id === poemId);
try {
if (poem) {
// 构建完整的诗歌数据对象
const poemData = {
_id: poem.id,
title: poem.title,
author: poem.author,
content: '加载中...' // 实际内容会在背诵页面加载
};
// 编码并传递数据
const encodedPoemData = encodeURIComponent(JSON.stringify(poemData));
wx.navigateTo({
url: `/pages/recite/recite?id=${poemId}&poemData=${encodedPoemData}`,
fail: function(res) {
console.error('跳转到背诵页面失败:', res);
// 降级方案传递ID、标题和作者
wx.navigateTo({
url: `/pages/recite/recite?id=${poemId}&title=${encodeURIComponent(poemTitle || '未知古诗')}&author=${encodeURIComponent(poemAuthor || '未知作者')}`
});
}
});
} else {
// 降级方案传递ID、标题和作者
console.warn('无法找到完整诗歌数据传递ID、标题和作者:', poemId);
wx.navigateTo({
url: `/pages/recite/recite?id=${poemId}&title=${encodeURIComponent(poemTitle || '未知古诗')}&author=${encodeURIComponent(poemAuthor || '未知作者')}`,
fail: function(res) {
console.error('跳转到背诵页面失败:', res);
wx.showToast({
title: '跳转失败,请重试',
icon: 'none'
});
}
});
}
} catch (error) {
console.error('处理诗歌数据时出错:', error);
// 异常情况下,使用降级方案
wx.navigateTo({
url: `/pages/recite/recite?id=${poemId}&title=${encodeURIComponent(poemTitle || '未知古诗')}&author=${encodeURIComponent(poemAuthor || '未知作者')}`
});
}
} else {
console.error('缺少诗歌ID参数');
wx.showToast({
title: '参数错误',
icon: 'none'
});
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
onReady: function () {
// 获取系统信息设置Canvas尺寸
const sysInfo = wx.getSystemInfoSync();
const windowWidth = sysInfo.windowWidth;
this.setData({
canvasWidth: windowWidth - 40, // 左右各留20px边距
canvasHeight: 300
});
// 确保数据加载完成后绘制图表
if (this.data.复习计划数据 && this.data.复习计划数据.length > 0) {
this.drawReviewPlanChart();
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
onShow: function() {
// 每次页面显示时重新加载数据并绘制图表
this.loadPoemsToReview().then(() => {
setTimeout(() => {
// 延迟执行确保Canvas已准备好
this.drawReviewPlanChart();
}, 100);
});
},
/**
* 生命周期函数--监听页面隐藏
* 绘制复习计划图表
* 显示未来7天每天需要复习的古诗数量
*/
onHide() {
drawReviewPlanChart() {
const ctx = wx.createCanvasContext('reviewPlanChart');
const { canvasWidth, canvasHeight } = this.data;
const data = this.data.复习计划数据;
// 确保有数据
if (!data || data.length === 0) {
// 绘制无数据状态
ctx.setFillStyle('#ffffff');
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.setFillStyle('#999999');
ctx.setFontSize(14);
ctx.setTextAlign('center');
ctx.fillText('暂无复习计划数据', canvasWidth / 2, canvasHeight / 2);
ctx.draw();
return;
}
// 设置画布背景
ctx.setFillStyle('#ffffff');
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// 计算坐标转换参数
const padding = 40;
const plotWidth = canvasWidth - 2 * padding;
const plotHeight = canvasHeight - 2 * padding;
const maxCount = Math.max(...data.map(item => item.count), 1); // 至少为1避免除以0
// 绘制坐标轴
ctx.setStrokeStyle('#e0e0e0');
ctx.setLineWidth(1);
// 垂直网格线
for (let i = 0; i <= 6; i++) {
const x = padding + (plotWidth / 6) * i;
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, canvasHeight - padding);
ctx.stroke();
}
// 水平网格线
const maxYAxisValue = Math.ceil(maxCount * 1.2); // 留出20%的顶部空间
const yAxisSteps = Math.min(5, maxYAxisValue); // 最多5条水平线
for (let i = 0; i <= yAxisSteps; i++) {
const y = padding + (plotHeight / yAxisSteps) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(canvasWidth - padding, y);
ctx.stroke();
}
// 绘制坐标轴标签
ctx.setFillStyle('#666666');
ctx.setFontSize(12);
ctx.setTextAlign('center');
// X轴标签显示日期
data.forEach((item, index) => {
const x = padding + (plotWidth / 6) * index;
// 绘制日期
ctx.fillText(item.dateStr, x, canvasHeight - padding + 15);
// 绘制天数
ctx.fillText(item.day, x, canvasHeight - padding + 30);
});
// Y轴标签
ctx.setTextAlign('right');
for (let i = 0; i <= yAxisSteps; i++) {
const count = maxYAxisValue * (1 - i / yAxisSteps);
const y = padding + (plotHeight / yAxisSteps) * i;
ctx.fillText(Math.round(count), padding - 10, y + 5);
}
// 绘制曲线图
if (data.length > 1) {
ctx.setStrokeStyle('#4CAF50'); // 使用绿色作为主色调
ctx.setLineWidth(2);
ctx.beginPath();
data.forEach((item, index) => {
const x = padding + (plotWidth / 6) * index;
const valueRatio = item.count / maxYAxisValue;
const y = canvasHeight - padding - (valueRatio * plotHeight);
// 绘制点
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#4CAF50';
ctx.fill();
// 连接点形成曲线
if (index === 0) {
ctx.moveTo(x, y);
} else {
// 使用贝塞尔曲线使线条更平滑
const prevItem = data[index - 1];
const prevX = padding + (plotWidth / 6) * (index - 1);
const prevY = canvasHeight - padding - ((prevItem.count / maxYAxisValue) * plotHeight);
const cpx1 = prevX + (x - prevX) / 2;
const cpy1 = prevY;
const cpx2 = prevX + (x - prevX) / 2;
const cpy2 = y;
ctx.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y);
}
});
ctx.stroke();
}
// 绘制每个数据点的值
ctx.setFillStyle('#333333');
ctx.setFontSize(12);
ctx.setTextAlign('center');
data.forEach((item, index) => {
const x = padding + (plotWidth / 6) * index;
const valueRatio = item.count / maxYAxisValue;
const y = canvasHeight - padding - (valueRatio * plotHeight);
// 在点的上方显示数值
ctx.fillText(item.count.toString(), x, y - 10);
});
// 绘制标题
ctx.setFillStyle('#333333');
ctx.setFontSize(16);
ctx.setTextAlign('center');
ctx.fillText('未来7天复习计划', canvasWidth / 2, padding - 10);
ctx.draw();
},
// 删除不再需要的辅助函数所有功能现在在drawReviewPlanChart中集中实现
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
// 刷新页面数据
this.drawForgettingCurve();
wx.stopPullDownRefresh();
},
/**
* 生命周期函数--监听页面卸载
* 生命周期函数--监听页面隐藏
*/
onUnload() {
onHide() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
* 生命周期函数--监听页面卸载
*/
onPullDownRefresh() {
onUnload() {
},

@ -1,2 +1,56 @@
<!--pages/review/review.wxml-->
<text>pages/review/review.wxml</text>
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">复习</text>
</view>
<!-- 复习计划图表 -->
<view class="curve-section">
<view class="section-title">复习计划</view>
<view class="curve-container">
<canvas canvas-id="reviewPlanChart" style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"></canvas>
<view class="curve-description">
<text>根据SM2算法和艾宾浩斯遗忘曲线生成的复习计划横轴为天数纵轴为每天需要复习的古诗数量。</text>
</view>
<!-- 今日复习统计 -->
<view class="today-summary" wx:if="{{todayReviewCount > 0}}">
<text class="today-count">今日有 <text class="highlight">{{todayReviewCount}}</text> 首古诗需要复习</text>
</view>
<view class="today-summary" wx:else>
<text class="today-count">今日暂无需要复习的古诗</text>
</view>
</view>
</view>
<!-- 复习推荐列表 -->
<view class="review-section">
<view class="section-title">今日推荐复习</view>
<block wx:if="{{poemsToReview.length > 0}}">
<view class="poem-list">
<view wx:for="{{poemsToReview}}" wx:key="id" class="poem-item {{item.needsReviewToday ? 'urgent' : ''}}">
<view class="poem-header">
<text class="poem-title">{{item.title}}</text>
<text class="poem-author">{{item.author}}</text>
<text class="review-tag" wx:if="{{item.needsReviewToday}}">今日必背</text>
</view>
<view class="poem-meta">
<text class="meta-item">上次背诵: {{item.daysSinceLastRecite}}天前</text>
<text class="meta-item">正确率: {{item.accuracy}}%</text>
<text class="meta-item retention-rate" wx:if="{{item.retentionRate}}">记忆保留率: {{item.retentionRate}}%</text>
<text class="meta-item next-review" wx:if="{{!item.needsReviewToday && item.nextReviewInterval}}">下次复习: {{item.nextReviewInterval}}天后</text>
</view>
<view class="action-buttons">
<button class="study-btn" data-id="{{item.id}}" data-title="{{item.title}}" bindtap="goToStudy">学习</button>
<button class="recite-btn" data-id="{{item.id}}" data-title="{{item.title}}" data-author="{{item.author}}" bindtap="goToRecite">背诵</button>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<text>暂无推荐复习的古诗</text>
<text class="hint">开始学习新古诗后,系统会自动生成复习计划</text>
</view>
</view>

@ -1 +1,215 @@
/* pages/review/review.wxss */
/* pages/review/review.wxss */
/* 页面容器 */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
/* 页面标题 */
.page-header {
text-align: center;
margin-bottom: 15rpx;
}
.page-title {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
/* 遗忘曲线区域 */
.curve-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.curve-container {
display: flex;
flex-direction: column;
align-items: center;
}
.curve-description {
margin-top: 20rpx;
font-size: 24rpx;
color: rgb(126, 123, 123);
line-height: 36rpx;
text-align: center;
}
/* 复习计划区域 */
.plan-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.section-subtitle {
font-size: 24rpx;
color: #666;
margin-bottom: 20rpx;
}
/* 复习古诗列表 */
.poem-list {
width: 100%;
}
.poem-item {
background-color: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
transition: transform 0.2s ease;
position: relative;
}
.poem-item.urgent {
border-left: 8rpx solid #ff6b6b;
}
.poem-item:last-child {
margin-bottom: 0;
}
.poem-header {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
}
.poem-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
display: flex;
align-items: center;
}
.poem-details {
margin-bottom: 15rpx;
}
.poem-author {
font-size: 24rpx;
color: #666;
margin-bottom: 15rpx;
}
.review-tag {
background-color: #ff6b6b;
color: white;
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
margin-left: 16rpx;
vertical-align: middle;
}
.poem-meta {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 20rpx;
}
.meta-item {
font-size: 22rpx;
color: #666;
background-color: #f5f5f5;
padding: 6rpx 16rpx;
border-radius: 24rpx;
}
.meta-item.retention-rate {
background-color: #e8f5e9;
color: #388e3c;
}
.meta-item.next-review {
background-color: #e3f2fd;
color: #1976d2;
}
.poem-stats {
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
margin-top: 10rpx;
}
.study-btn, .recite-btn {
flex: 1;
font-size: 24rpx;
padding: 16rpx 0;
margin: 0;
border-radius: 40rpx;
border: none;
}
.study-btn {
background-color: #f0f0f0;
color: #333;
}
.recite-btn {
background-color: #4CAF50;
color: white;
}
/* 今日复习统计 */
.today-summary {
margin-top: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 16rpx;
text-align: center;
}
.today-count {
font-size: 28rpx;
color: #555;
}
.today-count .highlight {
color: #ff6b6b;
font-weight: bold;
font-size: 36rpx;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60rpx 0;
color: #999;
}
.empty-state .hint {
margin-top: 20rpx;
font-size: 26rpx;
color: #bbb;
}

@ -5,14 +5,22 @@ Page({
* 页面的初始数据
*/
data: {
keyword: '',
showResults: false,
searchResults: [],
searchHistory: [],
isLoading: false,
hasMore: true,
page: 1,
pageSize: 10,
total: 0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadSearchHistory();
},
/**
@ -26,7 +34,246 @@ Page({
* 生命周期函数--监听页面显示
*/
onShow() {
this.loadSearchHistory();
},
// 从本地存储加载搜索历史
loadSearchHistory() {
const history = wx.getStorageSync('searchHistory') || [];
this.setData({
searchHistory: history
});
},
// 输入处理
onInput(e) {
const value = e.detail.value;
this.setData({
keyword: value
});
// 如果输入为空,隐藏结果
if (!value.trim()) {
this.setData({
showResults: false,
searchResults: []
});
}
},
// 清空输入框
clearInput() {
this.setData({
keyword: '',
showResults: false,
searchResults: []
});
},
// 执行搜索
async doSearch() {
const { keyword, page } = this.data;
if (!keyword.trim()) {
wx.showToast({
title: '请输入搜索关键词',
icon: 'none'
});
return;
}
// 显示加载状态
if (page === 1) {
this.setData({
isLoading: true
});
}
try {
// 调用云函数搜索
const result = await wx.cloud.callFunction({
name: 'poemManagement',
data: {
action: 'searchPoems',
keyword: keyword.trim(),
page: page,
pageSize: this.data.pageSize
}
});
if (result.result.success) {
const { poems, total } = result.result.data;
const newResults = page === 1 ? poems : [...this.data.searchResults, ...poems];
this.setData({
searchResults: newResults,
showResults: true,
total: total,
hasMore: (page * this.data.pageSize) < total,
isLoading: false
});
// 添加到搜索历史
this.addToHistory(keyword.trim());
// 显示搜索结果提示
if (page === 1 && poems.length > 0) {
wx.showToast({
title: `找到 ${total} 条结果`,
icon: 'success',
duration: 1500
});
}
} else {
wx.showToast({
title: result.result.message || '搜索失败',
icon: 'none'
});
this.setData({ isLoading: false });
// 如果云函数失败,使用模拟数据作为备选
this.useMockData(keyword);
}
} catch (error) {
console.error('搜索错误:', error);
wx.showToast({
title: '搜索失败,请检查网络',
icon: 'none'
});
this.setData({ isLoading: false });
// 如果云函数调用失败,使用模拟数据作为备选
this.useMockData(keyword);
}
},
// 使用模拟数据(备选方案)
useMockData(keyword) {
const mockPoems = [
{
_id: '1',
title: '静夜思',
author: '李白',
dynasty: '唐代',
content: '床前明月光,疑是地上霜。举头望明月,低头思故乡。'
},
{
_id: '2',
title: '望庐山瀑布',
author: '李白',
dynasty: '唐代',
content: '日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'
},
{
_id: '3',
title: '早发白帝城',
author: '李白',
dynasty: '唐代',
content: '朝辞白帝彩云间,千里江陵一日还。两岸猿声啼不住,轻舟已过万重山。'
},
{
_id: '4',
title: '春晓',
author: '孟浩然',
dynasty: '唐代',
content: '春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。'
},
{
_id: '5',
title: '登鹳雀楼',
author: '王之涣',
dynasty: '唐代',
content: '白日依山尽,黄河入海流。欲穷千里目,更上一层楼。'
},
];
// 过滤搜索结果
const results = mockPoems.filter(poem =>
poem.title.includes(keyword) ||
poem.author.includes(keyword) ||
poem.content.includes(keyword) ||
poem.dynasty.includes(keyword)
);
this.setData({
searchResults: results,
showResults: true,
total: results.length,
hasMore: false,
isLoading: false
});
// 添加到搜索历史
this.addToHistory(keyword);
},
// 通过历史记录搜索
searchByHistory(e) {
const keyword = e.currentTarget.dataset.keyword;
this.setData({
keyword: keyword,
page: 1,
searchResults: [],
hasMore: true
});
this.doSearch();
},
// 添加到搜索历史
addToHistory(keyword) {
let { searchHistory } = this.data;
// 移除已存在的相同关键词
searchHistory = searchHistory.filter(item => item !== keyword);
// 添加到最前面
searchHistory.unshift(keyword);
// 限制历史记录数量
if (searchHistory.length > 10) {
searchHistory = searchHistory.slice(0, 10);
}
// 保存到本地存储
wx.setStorageSync('searchHistory', searchHistory);
this.setData({
searchHistory: searchHistory
});
},
// 清除搜索历史
clearHistory() {
wx.showModal({
title: '确认清除',
content: '确定要清除所有搜索历史吗?',
success: (res) => {
if (res.confirm) {
wx.removeStorageSync('searchHistory');
this.setData({
searchHistory: []
});
}
}
});
},
// 跳转到学习页面
goToStudy(e) {
const poem = e.currentTarget.dataset.poem;
if (poem && poem._id) {
wx.navigateTo({
url: `/pages/study/study?id=${poem._id}`
});
} else {
const id = e.currentTarget.dataset.id;
if (id) {
wx.navigateTo({
url: `/pages/study/study?id=${id}`
});
} else {
wx.showToast({
title: '诗歌信息错误',
icon: 'none'
});
}
}
},
/**
@ -47,14 +294,32 @@ Page({
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.setData({
page: 1,
searchResults: [],
hasMore: true
});
if (this.data.keyword.trim()) {
this.doSearch().then(() => {
wx.stopPullDownRefresh();
});
} else {
wx.stopPullDownRefresh();
}
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
if (this.data.hasMore && !this.data.isLoading) {
this.setData({
page: this.data.page + 1
}, () => {
this.doSearch();
});
}
},
/**

@ -1,2 +1,98 @@
<!--pages/search/search.wxml-->
<text>pages/search/search.wxml</text>
<view class="search-container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrapper">
<input
class="search-input"
placeholder="搜索古诗名、作者或诗句"
value="{{keyword}}"
bindinput="onInput"
bindconfirm="doSearch"
confirm-type="search"
/>
<view wx:if="{{keyword}}" class="clear-btn" bindtap="clearInput">
<text>✕</text>
</view>
</view>
<view class="search-btn" bindtap="doSearch">搜索</view>
</view>
<!-- 加载状态 -->
<view wx:if="{{isLoading}}" class="loading-section">
<view class="loading-spinner"></view>
<text class="loading-text">搜索中...</text>
</view>
<!-- 搜索历史 -->
<view wx:if="{{!showResults && !isLoading && searchHistory.length > 0}}" class="history-section">
<view class="history-header">
<text class="history-title">搜索历史</text>
<text class="clear-history" bindtap="clearHistory">清除</text>
</view>
<view class="history-tags">
<view
wx:for="{{searchHistory}}"
wx:key="*this"
class="history-tag"
bindtap="searchByHistory"
data-keyword="{{item}}"
>
{{item}}
</view>
</view>
</view>
<!-- 空状态提示 -->
<view wx:if="{{!showResults && !isLoading && searchHistory.length === 0}}" class="empty-section">
<text class="empty-text">请输入关键词搜索古诗</text>
<text class="empty-subtext">支持搜索古诗名、作者、诗句内容</text>
</view>
<!-- 搜索结果 -->
<view wx:if="{{showResults && !isLoading}}" class="results-section">
<view class="results-header">
<text class="results-title">搜索结果 ({{total}})</text>
</view>
<view class="results-list">
<view
wx:for="{{searchResults}}"
wx:key="_id"
class="result-item"
bindtap="goToStudy"
data-poem="{{item}}"
>
<view class="poem-info">
<view class="poem-title">{{item.title}}</view>
<view class="poem-meta">
<text class="poem-author">{{item.author}}</text>
<text class="poem-dynasty">{{item.dynasty}}</text>
</view>
<view class="poem-content">{{item.content}}</view>
</view>
<view class="study-btn">
<text>学习</text>
</view>
</view>
<!-- 无结果状态 -->
<view wx:if="{{searchResults.length === 0}}" class="empty-results">
<text class="empty-icon">🔍</text>
<text class="empty-text">没有找到相关古诗</text>
<text class="empty-subtext">换个关键词试试吧</text>
</view>
<!-- 加载更多 -->
<view wx:if="{{hasMore && searchResults.length > 0}}" class="load-more" bindtap="onReachBottom">
<text class="load-more-text">点击加载更多</text>
<text class="load-more-tips">已加载 {{searchResults.length}} / {{total}} 条</text>
</view>
<!-- 没有更多数据 -->
<view wx:if="{{!hasMore && searchResults.length > 0}}" class="no-more">
<text>没有更多数据了</text>
</view>
</view>
</view>
</view>

@ -1 +1,302 @@
/* pages/search/search.wxss */
/* pages/search/search.wxss */
.search-container {
min-height: 100vh;
background-color: #f8f9fa;
padding: 20rpx;
}
/* 搜索栏样式 */
.search-bar {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 30rpx;
}
.search-input-wrapper {
flex: 1;
position: relative;
background: #ffffff;
border-radius: 50rpx;
padding: 20rpx 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.search-input {
width: 100%;
font-size: 28rpx;
color: #333;
}
.clear-btn {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
width: 40rpx;
height: 40rpx;
background: #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 24rpx;
}
.search-btn {
background: #07c160;
color: white;
padding: 20rpx 40rpx;
border-radius: 50rpx;
font-size: 28rpx;
font-weight: 500;
white-space: nowrap;
}
/* 加载状态 */
.loading-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #07c160;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: #666;
}
/* 搜索历史 */
.history-section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.history-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.clear-history {
font-size: 26rpx;
color: #07c160;
}
.history-tags {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.history-tag {
background: #f8f9fa;
padding: 16rpx 30rpx;
border-radius: 30rpx;
font-size: 26rpx;
color: #333;
border: 1rpx solid #e9ecef;
}
.history-tag:active {
background: #e9ecef;
}
/* 空状态 */
.empty-section {
text-align: center;
padding: 120rpx 40rpx;
}
.empty-text {
display: block;
font-size: 30rpx;
color: #666;
margin-bottom: 15rpx;
}
.empty-subtext {
display: block;
font-size: 26rpx;
color: #999;
}
/* 搜索结果 */
.results-section {
background: white;
border-radius: 20rpx;
overflow: hidden;
}
.results-header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.results-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
}
.results-list {
padding: 0;
}
.result-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f8f9fa;
transition: background-color 0.2s;
}
.result-item:active {
background-color: #f8f9fa;
}
.result-item:last-child {
border-bottom: none;
}
.poem-info {
flex: 1;
}
.poem-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.poem-meta {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 15rpx;
}
.poem-author {
font-size: 26rpx;
color: #07c160;
font-weight: 500;
}
.poem-dynasty {
font-size: 24rpx;
color: #999;
background: #f8f9fa;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.poem-content {
font-size: 26rpx;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.study-btn {
background: #07c160;
color: white;
padding: 15rpx 25rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
white-space: nowrap;
margin-left: 20rpx;
}
/* 空结果状态 */
.empty-results {
text-align: center;
padding: 80rpx 40rpx;
}
.empty-icon {
font-size: 80rpx;
display: block;
margin-bottom: 20rpx;
}
.empty-text {
display: block;
font-size: 30rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-subtext {
display: block;
font-size: 26rpx;
color: #999;
}
/* 加载更多 */
.load-more {
text-align: center;
padding: 40rpx;
background: #f8f9fa;
margin: 20rpx;
border-radius: 15rpx;
}
.load-more:active {
background: #e9ecef;
}
.load-more-text {
display: block;
font-size: 28rpx;
color: #07c160;
margin-bottom: 10rpx;
}
.load-more-tips {
display: block;
font-size: 24rpx;
color: #999;
}
/* 没有更多数据 */
.no-more {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
background: #f8f9fa;
margin: 20rpx;
border-radius: 15rpx;
}

@ -1,66 +1,418 @@
// pages/all.js
// pages/study/study.js
const innerAudioContext = wx.createInnerAudioContext()
Page({
/**
* 页面的初始数据
*/
data: {
poemTitle: '加载中...',
poetInfo: '',
originalText: '',
translationText: '',
backgroundInfo: '',
showTranslation: false,
showBackground: false,
poem: null,
isLoading: true,
authorInfo: '',
loadError: false,
poemType: '',
token: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('学习页面参数:', options);
this.options = options;
// 初始化语音上下文事件监听
this.initAudioEvents();
// 获取token
this.getToken();
if (options.poemData) {
// 从首页传递的完整诗歌数据
try {
const poemData = decodeURIComponent(options.poemData);
const poem = JSON.parse(poemData);
console.log('解析后的诗歌数据:', poem);
this.processPoemData(poem);
} catch (error) {
console.error('解析诗歌数据失败:', error);
this.showError('数据解析失败');
}
} else if (options.id) {
// 根据ID从数据库加载诗词数据
console.log('通过ID加载诗歌:', options.id);
this.loadPoemFromDatabase(options.id);
} else {
this.showError('缺少诗歌信息');
}
},
// 初始化音频事件监听
initAudioEvents() {
innerAudioContext.onPlay(() => {
console.log('开始播放语音');
});
innerAudioContext.onStop(() => {
console.log('停止播放语音');
});
innerAudioContext.onEnded(() => {
console.log('语音播放结束');
});
innerAudioContext.onError((res) => {
console.error('语音播放错误:', res);
wx.showToast({
title: '语音播放失败',
icon: 'none'
});
});
},
// 获取token复用recite.js中的逻辑
getToken() {
let that = this;
wx.getStorage({
key: 'expires_in',
success(res) {
console.log("缓存中有access_token");
console.log("token失效时间", res.data);
const newT = new Date().getTime();
// 用当前时间和存储的时间判断token是否已过期
if (newT > parseInt(res.data)) {
console.log("token过期重新获取token");
that.fetchNewToken();
} else {
console.log("获取本地缓存的token");
that.setData({
token: wx.getStorageSync('access_token')
});
}
}, fail() {
console.log("缓存中没有access_token直接获取新token");
// 尝试从storage直接获取token
const token = wx.getStorageSync('access_token');
if (token) {
that.setData({ token: token });
} else {
// 直接获取新token不再依赖背诵功能初始化
that.fetchNewToken();
}
}
});
},
// 获取新token
fetchNewToken() {
let that = this;
let ApiKey = 'qdHcePu7v0WpIlJnaeGJZZqp';
let SecretKey = 'nuZCuBziZoO2gjE6NGEMKCdxKs4sbaNq';
const url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + ApiKey + '&client_secret=' + SecretKey;
wx.request({
url: url,
method: 'POST',
success(res) {
console.log("创建access_token成功", res);
// 将access_token存储到storage中
wx.setStorage({
key: 'access_token',
data: res.data.access_token
});
var date = new Date().getTime();
let time = date + 2592000 * 1000;
console.log('三十天后的时间', time);
wx.setStorage({
key: 'expires_in',
data: time
});
that.setData({
token: res.data.access_token
});
},
fail(err) {
console.error('获取token失败:', err);
wx.showToast({
title: '语音服务初始化失败',
icon: 'none'
});
}
});
},
// 语音合成播放函数
playstart(text) {
let that = this;
if (!that.data.token) {
wx.showToast({
title: '语音服务未初始化',
icon: 'none'
});
return;
}
console.log("开始语音合成", text);
// 对文本进行URL编码
const encodedText = encodeURIComponent(text);
innerAudioContext.src = 'https://tsn.baidu.com/text2audio?lan=zh&ctp=1&cuid=12_56&tok=' + that.data.token + '&tex=' + encodedText + '&vol=10&per=111&spd=5&pit=5&aue=3';
innerAudioContext.play();
wx.showToast({
title: '开始朗读',
icon: 'success',
duration: 1000
});
},
// 从数据库加载诗歌数据
async loadPoemFromDatabase(id) {
try {
console.log('开始从数据库加载诗歌ID:', id);
wx.showLoading({
title: '加载中...',
});
const db = wx.cloud.database();
const result = await db.collection('poeties').doc(id).get();
wx.hideLoading();
console.log('数据库查询结果:', result);
if (result.data) {
this.processPoemData(result.data);
} else {
this.showError('诗歌不存在');
}
} catch (error) {
wx.hideLoading();
console.error('数据库查询失败:', error);
// 如果直接查询失败,尝试使用云函数
this.loadPoemFromCloudFunction(id);
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
// 云函数加载(备用方案)
async loadPoemFromCloudFunction(id) {
try {
console.log('尝试通过云函数加载诗歌ID:', id);
const result = await wx.cloud.callFunction({
name: 'getPoemDetail',
data: { id: id }
});
},
console.log('云函数返回结果:', result);
/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (result.result && result.result.success) {
const poem = result.result.data;
this.processPoemData(poem);
} else {
this.showError(result.result?.message || '诗歌不存在');
}
} catch (error) {
console.error('调用云函数失败:', error);
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
// 处理诗歌数据
processPoemData(poem) {
console.log('处理诗歌数据:', poem);
// 设置页面数据
this.setData({
poem: poem,
poemTitle: poem.title || '未知标题',
poetInfo: this.formatPoetInfo(poem),
originalText: poem.content || '暂无内容',
translationText: poem.translation || '暂无译文',
backgroundInfo: poem.background || '暂无背景信息',
authorInfo: poem.author_info ? poem.author_info.intro : (poem.author ? `暂无${poem.author}的详细信息` : '暂无作者信息'),
poemType: poem.type || '诗',
isLoading: false,
loadError: false
});
// 设置导航栏标题
wx.setNavigationBarTitle({
title: poem.title || '古诗学习'
});
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
// 格式化作者信息
formatPoetInfo(poem) {
let info = '';
if (poem.dynasty) {
info += poem.dynasty;
}
if (poem.author) {
info += (info ? ' • ' : '') + poem.author;
}
return info || '未知信息';
},
// 切换译文显示状态
toggleTranslation() {
this.setData({
showTranslation: !this.data.showTranslation
});
},
// 切换背景信息显示状态
toggleBackground() {
this.setData({
showBackground: !this.data.showBackground
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
// 显示错误信息
showError(message) {
this.setData({
isLoading: false,
loadError: true,
poemTitle: '加载失败',
originalText: message
});
wx.showToast({
title: message,
icon: 'none',
duration: 2000
});
},
// 重新加载
reloadData() {
const options = this.options || {};
if (options.id) {
this.setData({ isLoading: true, loadError: false });
this.loadPoemFromDatabase(options.id);
} else {
wx.navigateBack();
}
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
// 复制诗歌内容
copyContent() {
const content = this.data.originalText;
wx.setClipboardData({
data: content,
success: () => {
wx.showToast({
title: '已复制到剪贴板',
icon: 'success'
});
}
});
},
// 语音播报功能(支持不同类型内容的朗读)
speakPoem(e) {
// 获取要朗读的内容类型
const contentType = e?.currentTarget?.dataset?.type || 'all';
let textToSpeak = '';
let title = '';
switch(contentType) {
case 'content':
textToSpeak = this.data.originalText;
title = '朗读原文';
break;
case 'translation':
textToSpeak = this.data.translationText;
title = '朗读译文';
break;
case 'background':
textToSpeak = this.data.backgroundInfo;
title = '朗读背景';
break;
case 'author':
textToSpeak = this.data.authorInfo;
title = '朗读作者简介';
break;
default:
// 默认朗读全部内容(包括题目、作者、原文、译文、背景、作者简介)
textToSpeak = `${this.data.poemTitle}${this.data.poetInfo}${this.data.originalText}${this.data.translationText}${this.data.backgroundInfo}${this.data.authorInfo}`;
title = '语音播报';
}
console.log(`准备朗读${contentType}内容:`, textToSpeak);
// 调用语音合成播放函数
this.playstart(textToSpeak);
},
/**
* 用户点击右上角分享
*/
// 分享功能
onShareAppMessage() {
const poem = this.data.poem;
if (poem && poem._id) {
return {
title: `${poem.title} - ${poem.author}`,
path: `/pages/study/study?id=${poem._id}`
};
}
return {
title: `${this.data.poemTitle}`,
path: '/pages/study/study'
};
},
onReady() {
// 可以在这里添加动画效果等
},
onShow() {
// 页面显示时的逻辑
},
onHide() {
},
onUnload() {
},
onPullDownRefresh() {
const options = this.options || {};
if (options.poemData) {
// 如果是传递完整数据,不需要重新加载
wx.stopPullDownRefresh();
} else if (options.id) {
this.loadPoemFromDatabase(options.id).then(() => {
wx.stopPullDownRefresh();
});
} else {
wx.stopPullDownRefresh();
}
},
onReachBottom() {
// 可以在这里添加加载更多相关内容
},
// 跳转到背诵页面
goToRecite() {
const poem = this.data.poem;
if (poem && poem._id) {
console.log('跳转到背诵页面诗歌ID:', poem._id);
// 将诗歌数据编码后传递给背诵页面
const poemData = encodeURIComponent(JSON.stringify(poem));
wx.navigateTo({
url: `/pages/recite/recite?id=${poem._id}&poemData=${poemData}`
});
} else {
wx.showToast({
title: '背诵功能暂不可用',
icon: 'none'
});
}
}
})

@ -1,2 +1,124 @@
<!--pages/all.wxml-->
<text>pages/all.wxml</text>
<!-- pages/study/study.wxml -->
<view class="study-container">
<!-- 加载状态 -->
<view wx:if="{{isLoading}}" class="loading-state">
<view class="loading-content">
<image src="/images/loading.gif" class="loading-icon"></image>
<text class="loading-text">诗歌加载中...</text>
</view>
</view>
<!-- 错误状态 -->
<view wx:elif="{{loadError}}" class="error-state">
<view class="error-content">
<image src="/images/error-icon.png" class="error-icon"></image>
<text class="error-title">加载失败</text>
<text class="error-desc">{{originalText}}</text>
<button class="btn-retry" bindtap="reloadData">重新加载</button>
</view>
</view>
<!-- 正常内容 -->
<view wx:else class="poem-detail">
<!-- 头部卡片 -->
<view class="header-card">
<view class="poem-basic-info">
<view class="poem-title">{{poemTitle}}</view>
<view class="poem-meta">
<text class="poet-info">{{poetInfo}}</text>
<view class="poem-type-tag">{{poemType}}</view>
</view>
</view>
<!-- 操作按钮组 -->
<view class="action-buttons">
<!-- 语音播报按钮 -->
<button class="btn-speak" type="default" size="mini" bindtap="speakPoem">
<text class="btn-icon">🔊</text>
语音播报
</button>
</view>
</view>
<!-- 诗歌内容卡片 -->
<view class="content-card">
<view class="card-header">
<text class="card-title">诗歌原文</text>
<!-- 诗歌原文语音播报按钮 -->
<button class="btn-speak-icon" type="default" size="mini" bindtap="speakPoem" data-type="content">
<text class="btn-icon">🔊</text>
</button>
<view class="header-actions">
<button class="btn-copy" bindtap="copyContent">
<text class="btn-icon">📋</text>
复制
</button>
</view>
</view>
<view class="original-text">
<text class="poem-content">{{originalText}}</text>
</view>
</view>
<!-- 译文卡片 -->
<view class="content-card">
<view class="section-header" bindtap="toggleTranslation">
<view class="section-title-wrapper">
<text class="card-title">诗歌译文</text>
<!-- 译文语音播报按钮 -->
<button class="btn-speak-icon" type="default" size="mini" bindtap="speakPoem" data-type="translation">
<text class="btn-icon">🔊</text>
</button>
</view>
<view class="toggle-wrapper">
<text class="toggle-text">{{showTranslation ? '收起' : '展开'}}</text>
<text class="toggle-icon">{{showTranslation ? '▲' : '▼'}}</text>
</view>
</view>
<view wx:if="{{showTranslation}}" class="translation-text">
<text class="translation-content">{{translationText}}</text>
</view>
</view>
<!-- 背景信息卡片 -->
<view class="content-card">
<view class="section-header" bindtap="toggleBackground">
<view class="section-title-wrapper">
<text class="card-title">创作背景</text>
<!-- 背景语音播报按钮 -->
<button class="btn-speak-icon" type="default" size="mini" bindtap="speakPoem" data-type="background">
<text class="btn-icon">🔊</text>
</button>
</view>
<view class="toggle-wrapper">
<text class="toggle-text">{{showBackground ? '收起' : '展开'}}</text>
<text class="toggle-icon">{{showBackground ? '▲' : '▼'}}</text>
</view>
</view>
<view wx:if="{{showBackground}}" class="background-text">
<text class="background-content">{{backgroundInfo}}</text>
</view>
</view>
<!-- 作者信息卡片 -->
<view class="content-card">
<view class="card-header">
<view class="section-title-wrapper">
<image src="/images/author-icon.png" class="section-icon"></image>
<text class="card-title">作者简介</text>
<!-- 作者简介语音播报按钮 -->
<button class="btn-speak-icon" type="default" size="mini" bindtap="speakPoem" data-type="author">
<text class="btn-icon">🔊</text>
</button>
</view>
</view>
<view class="author-text">
<text class="author-content">{{authorInfo}}</text>
</view>
</view>
</view>
<!-- 底部背诵按钮 -->
<view class="bottom-container">
<button class="recite-button" bindtap="goToRecite">开始背诵</button>
</view>
</view>

@ -1 +1,350 @@
/* pages/all.wxss */
/* pages/study/study.wxss */
/* 页面容器 */
.study-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20rpx;
box-sizing: border-box;
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 70vh;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
}
.loading-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
}
.loading-text {
font-size: 32rpx;
color: #666;
}
/* 错误状态 */
.error-state {
display: flex;
justify-content: center;
align-items: center;
height: 70vh;
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 0 60rpx;
}
.error-icon {
width: 150rpx;
height: 150rpx;
margin-bottom: 30rpx;
opacity: 0.7;
}
.error-title {
font-size: 36rpx;
font-weight: bold;
color: #ff6b6b;
margin-bottom: 20rpx;
}
.error-desc {
font-size: 28rpx;
color: #999;
margin-bottom: 50rpx;
line-height: 1.6;
}
.btn-retry {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50rpx;
padding: 20rpx 60rpx;
font-size: 30rpx;
box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.4);
}
/* 诗歌详情样式 */
.poem-detail {
padding-bottom: 40rpx;
}
/* 卡片通用样式 */
.header-card,
.content-card {
background: white;
border-radius: 20rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.08);
margin-bottom: 30rpx;
overflow: hidden;
transition: all 0.3s ease;
}
.content-card:active {
transform: translateY(-4rpx);
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.12);
}
/* 头部卡片 */
.header-card {
padding: 40rpx 30rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
}
/* 操作按钮组 */
.action-buttons {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
gap: 20rpx;
}
.btn-icon {
font-size: 28rpx;
margin-right: 8rpx;
}
.header-actions {
display: flex;
gap: 10rpx;
}
/* 文本区域语音播报按钮样式 */
.btn-speak-text {
margin-top: 20rpx;
font-size: 28rpx;
padding: 0 20rpx;
align-self: flex-start;
}
/* 小喇叭图标按钮样式 */
.btn-speak-icon {
margin-left: 8px;
padding: 0;
width: 30px;
height: 30px;
line-height: 30px;
display: flex;
align-items: center;
justify-content: center;
min-width: 30px;
}
.poem-title {
display: block;
font-size: 48rpx;
font-weight: bold;
margin-bottom: 20rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.poem-meta {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.poet-info {
font-size: 30rpx;
opacity: 0.9;
margin-right: 20rpx;
}
.poem-type-tag {
background: rgba(255, 255, 255, 0.2);
padding: 8rpx 20rpx;
border-radius: 30rpx;
font-size: 24rpx;
backdrop-filter: blur(10rpx);
}
/* 内容卡片内部样式 */
.content-card {
padding: 30rpx;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
}
.section-title-wrapper {
display: flex;
align-items: center;
}
.section-icon {
width: 36rpx;
height: 36rpx;
margin-right: 15rpx;
}
.card-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.toggle-wrapper {
display: flex;
align-items: center;
}
.toggle-text {
font-size: 26rpx;
color: #666;
margin-right: 10rpx;
}
.toggle-icon {
font-size: 24rpx;
color: #999;
}
/* 复制按钮 */
.btn-copy {
display: flex;
align-items: center;
background: #f8f9fa;
border: 1rpx solid #e9ecef;
border-radius: 30rpx;
padding: 12rpx 24rpx;
font-size: 24rpx;
color: #666;
}
.btn-speak {
font-size: 28rpx;
padding: 0 20rpx;
}
.copy-icon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
/* 诗歌内容 */
.original-text {
padding: 10rpx 0;
}
.poem-content {
font-size: 36rpx;
line-height: 1.8;
color: #2c3e50;
text-align: center;
white-space: pre-line;
}
/* 译文内容 */
.translation-text {
padding-top: 25rpx;
border-top: 1rpx solid #f1f3f4;
}
.translation-content {
font-size: 30rpx;
line-height: 1.7;
color: #555;
white-space: pre-line;
}
/* 背景信息 */
.background-text {
padding-top: 25rpx;
border-top: 1rpx solid #f1f3f4;
}
.background-content {
font-size: 30rpx;
line-height: 1.7;
color: #555;
white-space: pre-line;
}
/* 作者信息 */
.author-text {
padding-top: 10rpx;
}
.author-content {
font-size: 30rpx;
line-height: 1.7;
color: #555;
white-space: pre-line;
margin-bottom: 16px; /* 恢复正常边距 */
}
/* 底部背诵按钮容器 */
.bottom-container {
margin: 20px;
margin-bottom: 30px;
}
/* 背诵按钮样式 */
.recite-button {
width: 100%;
height: 50px;
background-color: #07c160;
color: #fff;
border-radius: 25px;
font-size: 18px;
font-weight: 600;
line-height: 50px;
padding: 0;
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3);
}
.recite-button:active {
background-color: #06ad56;
box-shadow: 0 2px 6px rgba(7, 193, 96, 0.4);
}
/* 响应式调整 */
@media (max-width: 480px) {
.study-container {
padding: 15rpx;
}
.header-card,
.content-card {
border-radius: 15rpx;
}
.poem-title {
font-size: 42rpx;
}
.poem-content {
font-size: 32rpx;
}
}

@ -1,66 +0,0 @@
// pages/textbookFilter/textbookFilter.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

@ -1,3 +0,0 @@
{
"usingComponents": {}
}

@ -1,2 +0,0 @@
<!--pages/textbookFilter/textbookFilter.wxml-->
<text>pages/textbookFilter/textbookFilter.wxml</text>

@ -1 +0,0 @@
/* pages/textbookfilter/textbookfilter.wxss */

@ -5,6 +5,7 @@
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmManually": false,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
@ -12,7 +13,15 @@
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
"minifyWXML": true,
"compileWorklet": false,
"uploadWithSourceMap": true,
"minifyWXSS": true,
"localPlugins": false,
"disableUseStrict": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
@ -21,5 +30,8 @@
"include": []
},
"appid": "wx60a2f42279236d44",
"editorSetting": {}
"editorSetting": {},
"libVersion": "3.10.2",
"cloudfunctionRoot": "cloudfunctions/",
"cloudfunctionTemplateRoot": "cloudfunctionTemplate/"
}

@ -1,48 +1,42 @@
{
"libVersion": "3.10.2",
"projectname": "miniprogram-1",
"setting": {
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
},
"condition": {
"miniprogram": {
"list": [
{
"name": "pages/login/login",
"pathName": "pages/login/login",
"query": "",
"scene": null,
"launchMode": "default"
},
{
"name": "pages/guiding/guiding",
"pathName": "pages/guiding/guiding",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "pages/search/search",
"pathName": "pages/search/search",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "pages/home/home",
"pathName": "pages/home/home",
"query": "",
"launchMode": "default",
"scene": null
"libVersion": "3.10.2",
"projectname": "miniprogram-2",
"setting": {
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true,
"useApiHook": true,
"useApiHostProcess": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
},
"condition": {
"miniprogram": {
"list": [
{
"name": "pages/question/question",
"pathName": "pages/question/question",
"query": "",
"scene": null,
"launchMode": "default"
},
{
"name": "pages/managePoems/managePoems",
"pathName": "pages/managePoems/managePoems",
"query": "",
"launchMode": "default",
"scene": null
}
]
}
]
}
}
}
Loading…
Cancel
Save