Commit 68086014 authored by chunsen's avatar chunsen

feat: 更新表单组件,添加思考问题功能及音频生成,优化数据结构

parent f023b993
No preview for this file type
...@@ -23,16 +23,20 @@ ...@@ -23,16 +23,20 @@
<div> <div>
<!-- 单词 --> <!-- 单词 -->
<div style="margin-bottom: 10px; display: flex; align-items: center;"> <div style="margin-bottom: 10px; display: flex; align-items: center;">
<span style="width: 120px; text-align: right;">单词:</span> <span style="width: 120px; text-align: right;">
<input type="text" nz-input [(ngModel)]="word.word" (blur)="save()" style="width: 250px;"> <span style="color: #ff4d4f; margin-right: 4px;">*</span>单词:
</span>
<input type="text" nz-input [(ngModel)]="word.word" (blur)="save()" style="width: 250px;" placeholder="请输入单词">
</div> </div>
<!-- 词性 --> <!-- 词性 -->
<div style="margin-bottom: 10px; display: flex; align-items: center;"> <div style="margin-bottom: 10px; display: flex; align-items: center;">
<span style="width: 120px; text-align: right;">词性:</span> <span style="width: 120px; text-align: right;">
<span style="color: #ff4d4f; margin-right: 4px;">*</span>词性:
</span>
<div style="width: 250px;"> <div style="width: 250px;">
<a nz-dropdown [nzDropdownMenu]="menu"> <a nz-dropdown [nzDropdownMenu]="menu">
{{word.partOfSpeech || ''}} {{word.partOfSpeech || '请选择词性'}}
<i nz-icon nzType="down"></i> <i nz-icon nzType="down"></i>
</a> </a>
<button style="margin-left: 10px;" nz-button (click)="word.partOfSpeech = ''; save()"> <button style="margin-left: 10px;" nz-button (click)="word.partOfSpeech = ''; save()">
...@@ -55,6 +59,9 @@ ...@@ -55,6 +59,9 @@
<app-audio-recorder [audioUrl]="word.wordAudioUrl" <app-audio-recorder [audioUrl]="word.wordAudioUrl"
(audioUploaded)="onItemAudioUploadSuccess($event, word, 'wordAudioUrl')"> (audioUploaded)="onItemAudioUploadSuccess($event, word, 'wordAudioUrl')">
</app-audio-recorder> </app-audio-recorder>
<button nz-button style="margin-left: 10px;" (click)="generateWordAudio(word)" [disabled]="!word.word">
<i nz-icon nzType="sound"></i> 生成音频
</button>
</div> </div>
<!-- 翻译 --> <!-- 翻译 -->
...@@ -66,16 +73,15 @@ ...@@ -66,16 +73,15 @@
<div style="margin-top: 20px; border-top: 1px dashed #eee; padding-top: 15px;"> <div style="margin-top: 20px; border-top: 1px dashed #eee; padding-top: 15px;">
<div style="margin-bottom: 10px; display: flex; align-items: center;"> <div style="margin-bottom: 10px; display: flex; align-items: center;">
<span style="width: 120px; text-align: right;">显示类型:</span> <span style="width: 120px; text-align: right;">
<span style="color: #ff4d4f; margin-right: 4px;">*</span>显示类型:
</span>
<div style="width: auto; display: flex; align-items: center;"> <div style="width: auto; display: flex; align-items: center;">
<label style="margin-right: 10px; font-size: 14px;"> <label style="margin-right: 10px; font-size: 14px;">
<input type="radio" [(ngModel)]="word.displayType" [value]="1" (change)="save()"> 高亮 <input type="radio" [(ngModel)]="word.displayType" [value]="1" (change)="save()"> 词卡单词
</label>
<label style="margin-right: 10px; font-size: 14px;">
<input type="radio" [(ngModel)]="word.displayType" [value]="2" (change)="save()"> 单词卡
</label> </label>
<label style="font-size: 14px;"> <label style="font-size: 14px;">
<input type="radio" [(ngModel)]="word.displayType" [value]="3" (change)="save()" checked> 高亮&单词卡 <input type="radio" [(ngModel)]="word.displayType" [value]="3" (change)="save()"> 核心单词
</label> </label>
</div> </div>
</div> </div>
...@@ -96,6 +102,9 @@ ...@@ -96,6 +102,9 @@
<app-audio-recorder [audioUrl]="word.exampleSentenceAudioUrl" <app-audio-recorder [audioUrl]="word.exampleSentenceAudioUrl"
(audioUploaded)="onItemAudioUploadSuccess($event, word, 'exampleSentenceAudioUrl')"> (audioUploaded)="onItemAudioUploadSuccess($event, word, 'exampleSentenceAudioUrl')">
</app-audio-recorder> </app-audio-recorder>
<button nz-button style="margin-left: 10px;" (click)="generateExampleAudio(word)" [disabled]="!word.exampleSentence">
<i nz-icon nzType="sound"></i> 生成音频
</button>
</div> </div>
<!-- 图片 --> <!-- 图片 -->
...@@ -139,6 +148,75 @@ ...@@ -139,6 +148,75 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 思考问题部分 -->
<div style="margin-top: 20px; border-top: 1px solid #ddd; padding-top: 20px;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<h4 style="margin: 0; margin-right: 20px;">思考问题</h4>
</div>
<!-- 问题内容 -->
<div style="margin-bottom: 15px; display: flex; align-items: flex-start;">
<span style="width: 120px; text-align: right; margin-top: 5px;">题目:</span>
<div style="flex: 1; max-width: 800px;">
<textarea nz-input [(ngModel)]="getBackContent(word).content" (blur)="save()"
style="width: 400px; min-height: 50px;"></textarea>
<div style="margin-top: 10px; display: flex;">
<app-audio-recorder [audioUrl]="getBackContent(word).audioUrl"
(audioUploaded)="onBackContentAudioUploaded($event, word)">
</app-audio-recorder>
<button nz-button style="margin-left: 10px;"
(click)="generateQuestionAudio(word)"
[disabled]="!getBackContent(word).content">
<i nz-icon nzType="sound"></i> 生成音频
</button>
</div>
</div>
</div>
<!-- 选项列表 -->
<div *ngFor="let option of getBackContent(word).answer; let i = index"
style="margin-bottom: 20px; border: 1px solid #eee; padding: 15px; border-radius: 5px; background-color: #f9f9f9;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<h5 style="margin: 0;">选项 {{i+1}}</h5>
<button nz-button nzType="danger" (click)="deleteOption(word, i)">
<i nz-icon nzType="delete"></i> 删除
</button>
</div>
<div style="margin-bottom: 10px; display: flex; align-items: flex-start;">
<span style="width: 120px; text-align: right; margin-top: 5px;">答案内容:</span>
<div style="flex: 1; max-width: 600px;">
<textarea nz-input [(ngModel)]="option.answer" (blur)="save()"
style="width: 400px; min-height: 50px;"></textarea>
<div style="margin-top: 10px; display: flex;">
<app-audio-recorder [audioUrl]="option.audioUrl"
(audioUploaded)="onOptionAudioUploaded($event, option, word)">
</app-audio-recorder>
<button nz-button style="margin-left: 10px;"
(click)="generateOptionAudio(option, word)"
[disabled]="!option.answer">
<i nz-icon nzType="sound"></i> 生成音频
</button>
</div>
</div>
</div>
<div style="margin-bottom: 10px; display: flex; align-items: center;">
<span style="width: 120px; text-align: right;">正确答案:</span>
<nz-switch [(ngModel)]="option.isCorrect" (ngModelChange)="save()"></nz-switch>
</div>
</div>
<!-- 添加选项按钮 -->
<div style="text-align: center; margin: 20px 0;">
<button nz-button nzType="primary" (click)="addOption(word)">
<i nz-icon nzType="plus"></i> 添加选项
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
......
import { Component, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef } from '@angular/core';
import { JsonPipe } from '@angular/common'; import { JsonPipe } from '@angular/common';
import { NzMessageService } from 'ng-zorro-antd/message'; import { NzMessageService } from 'ng-zorro-antd/message';
declare function commonPostMessageWithCallback(
params: any,
callback: (res: any) => void
): void;
@Component({ @Component({
selector: 'app-form', selector: 'app-form',
...@@ -11,7 +14,7 @@ import { NzMessageService } from 'ng-zorro-antd/message'; ...@@ -11,7 +14,7 @@ import { NzMessageService } from 'ng-zorro-antd/message';
export class FormComponent implements OnInit, OnChanges, OnDestroy { export class FormComponent implements OnInit, OnChanges, OnDestroy {
// 储存数据用 // 储存数据用
saveKey = "learn_word"; saveKey = "jj_learn_word";
// 储存对象 // 储存对象
item; item;
...@@ -40,10 +43,31 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -40,10 +43,31 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
} }
// 从父窗口URL获取参数
getParentUrlParam(param: string): string | null {
try {
// 获取父窗口的URL
const parentUrl = window.parent.location.href;
// 创建URL对象来解析参数
const url = new URL(parentUrl);
// 获取指定的参数值
return url.searchParams.get(param);
} catch (e) {
// 处理可能的跨域安全错误
console.error("无法访问父窗口URL,可能是由于跨域限制:", e);
return null;
}
}
ngOnInit() { ngOnInit() {
this.item = {}; this.item = {};
// 获取父窗口URL中的courseId参数作为pictureBookId
const courseId = this.getParentUrlParam('courseId');
if (courseId) {
console.log('从父窗口URL获取到courseId:', courseId);
}
// 获取存储的数据 // 获取存储的数据
(<any>window).courseware.getData((data) => { (<any>window).courseware.getData((data) => {
if (data) { if (data) {
...@@ -56,6 +80,15 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -56,6 +80,15 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
} }
this.initItem(); this.initItem();
// 确保pictureBookId存在
if (courseId && (!this.item.pictureBookId || this.item.pictureBookId === '')) {
this.item.pictureBookId = courseId;
}
// 确保每个单词都有ID
this.ensureWordIds();
console.log("this.item: ", JSON.stringify(this.item)); console.log("this.item: ", JSON.stringify(this.item));
this.init(); this.init();
...@@ -72,6 +105,46 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -72,6 +105,46 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.item.data = []; this.item.data = [];
} }
// 确保pictureBookId存在
if (!this.item.pictureBookId) {
this.item.pictureBookId = '';
}
// 确保每个单词都有正确的backContent结构
if (this.item.data && this.item.data.length > 0) {
this.item.data.forEach(word => {
// 初始化backContent
if (!word.backContent) {
word.backContent = {
content: '',
audioUrl: '',
answer: []
};
} else if (!word.backContent.answer) {
// 确保answer数组存在
word.backContent.answer = [];
}
});
}
}
// 确保每个单词都有ID
ensureWordIds() {
if (!this.item.data || this.item.data.length === 0) {
return;
}
const pictureBookId = this.item.pictureBookId || '';
// 为每个没有id的单词生成id
this.item.data.forEach((word, index) => {
if (!word.id || word.id.trim() === '') {
// 使用pictureBookId + 索引生成单词ID
word.id = `${pictureBookId}${index + 1}`;
}
// 更新pictureBookId
word.pictureBookId = pictureBookId;
});
} }
init() { init() {
...@@ -86,9 +159,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -86,9 +159,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.item.data.push({ this.item.data.push({
id:'', id:'',
pictureBookId: '', pictureBookId: this.item.pictureBookId || '',
word: '', word: '',
displayType: '', displayType: '3', // 默认为"核心单词"
partOfSpeech: '', partOfSpeech: '',
exampleSentence: '', exampleSentence: '',
wordAudioUrl: '', wordAudioUrl: '',
...@@ -99,7 +172,11 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -99,7 +172,11 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
britishPhonetic: '', britishPhonetic: '',
americanPhonetic: '', americanPhonetic: '',
englishDefinition: '', englishDefinition: '',
backContent: {}, backContent: {
content: '',
audioUrl: '',
answer: []
},
createdTime: new Date().toISOString() createdTime: new Date().toISOString()
}); });
} }
...@@ -141,23 +218,34 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -141,23 +218,34 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
* 储存数据 * 储存数据
*/ */
save() { save() {
// 更新data中的时间戳 // 验证必填字段
// if (this.item.data && this.item.data.length > 0) { let hasError = false;
// const now = new Date().toISOString(); if (this.item.data && this.item.data.length > 0) {
// this.item.data.forEach(item => { this.item.data.forEach((word, index) => {
// if (!item.createdTime) { if (!word.word || word.word.trim() === '') {
// item.createdTime = now; this.message.warning(`第${index + 1}个单词的"单词"不能为空`);
// } hasError = true;
// }); }
// } if (!word.partOfSpeech || word.partOfSpeech.trim() === '') {
this.message.warning(`第${index + 1}个单词的"词性"不能为空`);
hasError = true;
}
if (!word.displayType) {
this.message.warning(`第${index + 1}个单词的"显示类型"不能为空`);
hasError = true;
}
});
}
// 删除item中可能存在的quesArr属性 // 如果有错误,不保存
// if (this.item.quesArr) { if (hasError) {
// delete this.item.quesArr; return;
// } }
// 确保所有单词都有ID
this.ensureWordIds();
const newItem = this.getNewSortItem(); const newItem = this.getNewSortItem();
(<any>window).courseware.setData(newItem, null, this.saveKey); (<any>window).courseware.setData(newItem, null, this.saveKey);
...@@ -180,9 +268,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -180,9 +268,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
addNewWord() { addNewWord() {
this.item.data.push({ this.item.data.push({
id: '', id: '',
pictureBookId: '', pictureBookId: this.item.pictureBookId || '',
word: '', word: '',
displayType: '', displayType: '3', // 默认为"核心单词"
partOfSpeech: '', partOfSpeech: '',
exampleSentence: '', exampleSentence: '',
wordAudioUrl: '', wordAudioUrl: '',
...@@ -193,9 +281,17 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -193,9 +281,17 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
britishPhonetic: '', britishPhonetic: '',
americanPhonetic: '', americanPhonetic: '',
englishDefinition: '', englishDefinition: '',
backContent: {}, backContent: {
content: '',
audioUrl: '',
answer: []
},
createdTime: new Date().toISOString() createdTime: new Date().toISOString()
}); });
// 确保所有单词都有ID
this.ensureWordIds();
this.save(); this.save();
} }
...@@ -205,4 +301,269 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -205,4 +301,269 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.save(); this.save();
} }
// 生成单词音频
generateWordAudio(word) {
if (!word.word || word.word.trim() === '') {
this.message.error('请先输入单词');
return;
}
const call_back = (res) => {
console.log("gennerateQuestionAudio res: ", res);
try {
const resData = JSON.parse(res);
if (resData.msg == "success") {
if (resData.data && word.word) {
// 确保音频URL正确赋值
word.wordAudioUrl = resData.data;
this.save(); // 保存更改
this.message.create("success", "题目音频生成成功");
} else {
this.message.create("warning", "生成的音频数据为空");
}
} else {
this.message.create("error", "音频生成失败");
}
} catch (e) {
console.error("解析音频生成响应出错:", e);
this.message.create("error", "音频生成响应解析失败");
}
};
// 检查方法是否存在
const checkAndCall = (word: any) => {
if (typeof commonPostMessageWithCallback === "function") {
commonPostMessageWithCallback(
{
action: "generateTTS",
data: word,
},
call_back
);
} else {
// setTimeout(checkAndCall, 100);
this.message.create("error", "通信方法不可用");
}
};
checkAndCall(word.word);
this.message.info(`准备为单词"${word.word}"生成音频`);
}
// 生成例句音频
generateExampleAudio(word) {
if (!word.exampleSentence || word.exampleSentence.trim() === '') {
this.message.error('请先输入例句');
return;
}
const call_back = (res) => {
console.log("generateExampleAudio res: ", res);
try {
const resData = JSON.parse(res);
if (resData.msg == "success") {
if (resData.data && word.exampleSentence) {
// 确保音频URL正确赋值
word.exampleSentenceAudioUrl = resData.data;
this.save(); // 保存更改
this.message.create("success", "例句音频生成成功");
} else {
this.message.create("warning", "生成的音频数据为空");
}
} else {
this.message.create("error", "音频生成失败");
}
} catch (e) {
console.error("解析音频生成响应出错:", e);
this.message.create("error", "音频生成响应解析失败");
}
};
// 检查方法是否存在
const checkAndCall = (sentence: any) => {
if (typeof commonPostMessageWithCallback === "function") {
commonPostMessageWithCallback(
{
action: "generateTTS",
data: sentence,
},
call_back
);
} else {
this.message.create("error", "通信方法不可用");
}
};
checkAndCall(word.exampleSentence);
this.message.info(`准备为例句生成音频`);
}
// 获取或初始化backContent
getBackContent(word) {
if (!word.backContent) {
word.backContent = {
content: '',
audioUrl: '',
answer: []
};
}
// 确保answer数组存在
if (!word.backContent.answer) {
word.backContent.answer = [];
}
return word.backContent;
}
// 添加新选项
addOption(word) {
// 确保backContent及其answer数组存在
const backContent = this.getBackContent(word);
try {
// 添加新选项
backContent.answer.push({
answer: '',
audioUrl: '',
isCorrect: false
});
this.save();
} catch (error) {
console.error('添加选项失败:', error);
this.message.error('添加选项失败,请刷新页面后重试');
}
}
// 删除选项
deleteOption(word, index) {
try {
const backContent = this.getBackContent(word);
// 检查索引是否有效
if (backContent.answer && Array.isArray(backContent.answer) && index >= 0 && index < backContent.answer.length) {
backContent.answer.splice(index, 1);
this.save();
} else {
console.warn('尝试删除不存在的选项索引:', index);
}
} catch (error) {
console.error('删除选项失败:', error);
this.message.error('删除选项失败,请刷新页面后重试');
}
}
// 思考问题音频上传成功回调
onBackContentAudioUploaded(e, word) {
const backContent = this.getBackContent(word);
backContent.audioUrl = e.url;
this.save();
}
// 选项音频上传成功回调
onOptionAudioUploaded(e, option, word) {
option.audioUrl = e.url;
this.save();
}
// 生成思考问题音频
generateQuestionAudio(word) {
const backContent = this.getBackContent(word);
if (!backContent.content || backContent.content.trim() === '') {
this.message.error('请先输入题目内容');
return;
}
const call_back = (res) => {
console.log("generateQuestionAudio res: ", res);
try {
const resData = JSON.parse(res);
if (resData.msg == "success") {
if (resData.data && backContent.content) {
backContent.audioUrl = resData.data;
this.save();
this.message.create("success", "题目音频生成成功");
} else {
this.message.create("warning", "生成的音频数据为空");
}
} else {
this.message.create("error", "音频生成失败");
}
} catch (e) {
console.error("解析音频生成响应出错:", e);
this.message.create("error", "音频生成响应解析失败");
}
};
const checkAndCall = (content) => {
if (typeof commonPostMessageWithCallback === "function") {
commonPostMessageWithCallback(
{
action: "generateTTS",
data: content,
},
call_back
);
} else {
this.message.create("error", "通信方法不可用");
}
};
checkAndCall(backContent.content);
this.message.info(`准备为题目生成音频`);
}
// 生成选项音频
generateOptionAudio(option, word) {
if (!option.answer || option.answer.trim() === '') {
this.message.error('请先输入选项内容');
return;
}
const call_back = (res) => {
console.log("generateOptionAudio res: ", res);
try {
const resData = JSON.parse(res);
if (resData.msg == "success") {
if (resData.data && option.answer) {
option.audioUrl = resData.data;
this.save();
this.message.create("success", "选项音频生成成功");
} else {
this.message.create("warning", "生成的音频数据为空");
}
} else {
this.message.create("error", "音频生成失败");
}
} catch (e) {
console.error("解析音频生成响应出错:", e);
this.message.create("error", "音频生成响应解析失败");
}
};
const checkAndCall = (content) => {
if (typeof commonPostMessageWithCallback === "function") {
commonPostMessageWithCallback(
{
action: "generateTTS",
data: content,
},
call_back
);
} else {
this.message.create("error", "通信方法不可用");
}
};
checkAndCall(option.answer);
this.message.info(`准备为选项生成音频`);
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment