Commit 68086014 authored by chunsen's avatar chunsen

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

parent f023b993
No preview for this file type
......@@ -23,16 +23,20 @@
<div>
<!-- 单词 -->
<div style="margin-bottom: 10px; display: flex; align-items: center;">
<span style="width: 120px; text-align: right;">单词:</span>
<input type="text" nz-input [(ngModel)]="word.word" (blur)="save()" style="width: 250px;">
<span style="width: 120px; text-align: right;">
<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 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;">
<a nz-dropdown [nzDropdownMenu]="menu">
{{word.partOfSpeech || ''}}
{{word.partOfSpeech || '请选择词性'}}
<i nz-icon nzType="down"></i>
</a>
<button style="margin-left: 10px;" nz-button (click)="word.partOfSpeech = ''; save()">
......@@ -55,6 +59,9 @@
<app-audio-recorder [audioUrl]="word.wordAudioUrl"
(audioUploaded)="onItemAudioUploadSuccess($event, word, 'wordAudioUrl')">
</app-audio-recorder>
<button nz-button style="margin-left: 10px;" (click)="generateWordAudio(word)" [disabled]="!word.word">
<i nz-icon nzType="sound"></i> 生成音频
</button>
</div>
<!-- 翻译 -->
......@@ -66,16 +73,15 @@
<div style="margin-top: 20px; border-top: 1px dashed #eee; padding-top: 15px;">
<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;">
<label style="margin-right: 10px; font-size: 14px;">
<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()"> 单词卡
<input type="radio" [(ngModel)]="word.displayType" [value]="1" (change)="save()"> 词卡单词
</label>
<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>
</div>
</div>
......@@ -96,6 +102,9 @@
<app-audio-recorder [audioUrl]="word.exampleSentenceAudioUrl"
(audioUploaded)="onItemAudioUploadSuccess($event, word, 'exampleSentenceAudioUrl')">
</app-audio-recorder>
<button nz-button style="margin-left: 10px;" (click)="generateExampleAudio(word)" [disabled]="!word.exampleSentence">
<i nz-icon nzType="sound"></i> 生成音频
</button>
</div>
<!-- 图片 -->
......@@ -139,6 +148,75 @@
</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>
......
import { Component, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { NzMessageService } from 'ng-zorro-antd/message';
declare function commonPostMessageWithCallback(
params: any,
callback: (res: any) => void
): void;
@Component({
selector: 'app-form',
......@@ -11,7 +14,7 @@ import { NzMessageService } from 'ng-zorro-antd/message';
export class FormComponent implements OnInit, OnChanges, OnDestroy {
// 储存数据用
saveKey = "learn_word";
saveKey = "jj_learn_word";
// 储存对象
item;
......@@ -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() {
this.item = {};
// 获取父窗口URL中的courseId参数作为pictureBookId
const courseId = this.getParentUrlParam('courseId');
if (courseId) {
console.log('从父窗口URL获取到courseId:', courseId);
}
// 获取存储的数据
(<any>window).courseware.getData((data) => {
if (data) {
......@@ -56,6 +80,15 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
}
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));
this.init();
......@@ -72,6 +105,46 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
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() {
......@@ -86,9 +159,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.item.data.push({
id:'',
pictureBookId: '',
pictureBookId: this.item.pictureBookId || '',
word: '',
displayType: '',
displayType: '3', // 默认为"核心单词"
partOfSpeech: '',
exampleSentence: '',
wordAudioUrl: '',
......@@ -99,7 +172,11 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
britishPhonetic: '',
americanPhonetic: '',
englishDefinition: '',
backContent: {},
backContent: {
content: '',
audioUrl: '',
answer: []
},
createdTime: new Date().toISOString()
});
}
......@@ -141,23 +218,34 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
* 储存数据
*/
save() {
// 更新data中的时间戳
// if (this.item.data && this.item.data.length > 0) {
// const now = new Date().toISOString();
// this.item.data.forEach(item => {
// if (!item.createdTime) {
// item.createdTime = now;
// }
// });
// }
// 删除item中可能存在的quesArr属性
// if (this.item.quesArr) {
// delete this.item.quesArr;
// }
// 验证必填字段
let hasError = false;
if (this.item.data && this.item.data.length > 0) {
this.item.data.forEach((word, index) => {
if (!word.word || word.word.trim() === '') {
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;
}
});
}
const newItem = this.getNewSortItem();
// 如果有错误,不保存
if (hasError) {
return;
}
// 确保所有单词都有ID
this.ensureWordIds();
const newItem = this.getNewSortItem();
(<any>window).courseware.setData(newItem, null, this.saveKey);
......@@ -180,9 +268,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
addNewWord() {
this.item.data.push({
id: '',
pictureBookId: '',
pictureBookId: this.item.pictureBookId || '',
word: '',
displayType: '',
displayType: '3', // 默认为"核心单词"
partOfSpeech: '',
exampleSentence: '',
wordAudioUrl: '',
......@@ -193,9 +281,17 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
britishPhonetic: '',
americanPhonetic: '',
englishDefinition: '',
backContent: {},
backContent: {
content: '',
audioUrl: '',
answer: []
},
createdTime: new Date().toISOString()
});
// 确保所有单词都有ID
this.ensureWordIds();
this.save();
}
......@@ -205,4 +301,269 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
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