feat(word): show/play UK-US audio and add example sentence
This commit is contained in:
@@ -13,6 +13,7 @@ type Word struct {
|
|||||||
AudioUS string `json:"audio_us" gorm:"size:500"`
|
AudioUS string `json:"audio_us" gorm:"size:500"`
|
||||||
PartOfSpeech string `json:"part_of_speech" gorm:"size:50"`
|
PartOfSpeech string `json:"part_of_speech" gorm:"size:50"`
|
||||||
Definition string `json:"definition" gorm:"type:text"`
|
Definition string `json:"definition" gorm:"type:text"`
|
||||||
|
ExampleSentence string `json:"example_sentence" gorm:"type:text"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,6 +27,65 @@ func NewWordService(db *gorm.DB) *WordService {
|
|||||||
return &WordService{db: db}
|
return &WordService{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dictAPIEntry struct {
|
||||||
|
PhoneticUK string
|
||||||
|
PhoneticUS string
|
||||||
|
Part string
|
||||||
|
Example string
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: dictionaryapi.dev(补充音标/词性/例句)
|
||||||
|
func (s *WordService) fetchDictMeta(word string) *dictAPIEntry {
|
||||||
|
endpoint := "https://api.dictionaryapi.dev/api/v2/entries/en/" + url.PathEscape(strings.ToLower(word))
|
||||||
|
resp, err := http.Get(endpoint)
|
||||||
|
if err != nil || resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var arr []map[string]any
|
||||||
|
if err := json.Unmarshal(body, &arr); err != nil || len(arr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entry := &dictAPIEntry{}
|
||||||
|
first := arr[0]
|
||||||
|
if p, ok := first["phonetics"].([]any); ok {
|
||||||
|
for _, x := range p {
|
||||||
|
if m, ok := x.(map[string]any); ok {
|
||||||
|
if t, ok := m["text"].(string); ok && t != "" {
|
||||||
|
if entry.PhoneticUK == "" {
|
||||||
|
entry.PhoneticUK = t
|
||||||
|
}
|
||||||
|
if entry.PhoneticUS == "" {
|
||||||
|
entry.PhoneticUS = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ms, ok := first["meanings"].([]any); ok && len(ms) > 0 {
|
||||||
|
if m0, ok := ms[0].(map[string]any); ok {
|
||||||
|
if p, ok := m0["partOfSpeech"].(string); ok {
|
||||||
|
entry.Part = p
|
||||||
|
}
|
||||||
|
if defs, ok := m0["definitions"].([]any); ok && len(defs) > 0 {
|
||||||
|
if d0, ok := defs[0].(map[string]any); ok {
|
||||||
|
if ex, ok := d0["example"].(string); ok {
|
||||||
|
entry.Example = ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
// 查询单词(调用有道API)
|
// 查询单词(调用有道API)
|
||||||
func (s *WordService) QueryWord(word string) (*model.YoudaoResponse, error) {
|
func (s *WordService) QueryWord(word string) (*model.YoudaoResponse, error) {
|
||||||
// 优先用 app_id,兼容 app_key
|
// 优先用 app_id,兼容 app_key
|
||||||
@@ -117,7 +177,7 @@ func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析有道API响应
|
// 解析有道API响应
|
||||||
var phoneticUK, phoneticUS, partOfSpeech, definition string
|
var phoneticUK, phoneticUS, partOfSpeech, definition, exampleSentence string
|
||||||
|
|
||||||
// 解析音标
|
// 解析音标
|
||||||
if youdaoResp.Basic.UkPhonetic != "" {
|
if youdaoResp.Basic.UkPhonetic != "" {
|
||||||
@@ -156,25 +216,40 @@ func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*
|
|||||||
partOfSpeech = "n./v./adj."
|
partOfSpeech = "n./v./adj."
|
||||||
}
|
}
|
||||||
|
|
||||||
// 音频 URL - 有道返回的 speakUrl 示例: https://dict.youdao.com/dictvoice?audio=hello
|
// 音频 URL:使用有道 dictvoice,type=1(英音) type=2(美音)
|
||||||
audioPath := config.AppConfig.Audio.Path
|
audioPath := config.AppConfig.Audio.Path
|
||||||
var audioUK, audioUS string
|
var audioUK, audioUS string
|
||||||
if youdaoResp.SpeakUrl != "" {
|
ukURL := fmt.Sprintf("https://dict.youdao.com/dictvoice?audio=%s&type=1", url.QueryEscape(word))
|
||||||
// 默认是美音,加 _uk 是英音
|
usURL := fmt.Sprintf("https://dict.youdao.com/dictvoice?audio=%s&type=2", url.QueryEscape(word))
|
||||||
audioUK = strings.Replace(youdaoResp.SpeakUrl, "audio=", "audio=word_uk_", 1)
|
|
||||||
audioUS = youdaoResp.SpeakUrl
|
|
||||||
|
|
||||||
// 异步下载音频文件
|
|
||||||
go func() {
|
|
||||||
if audioPath != "" {
|
if audioPath != "" {
|
||||||
ukPath := fmt.Sprintf("%s/%s_uk.mp3", audioPath, word)
|
ukPath := fmt.Sprintf("%s/%s_uk.mp3", audioPath, word)
|
||||||
usPath := fmt.Sprintf("%s/%s_us.mp3", audioPath, word)
|
usPath := fmt.Sprintf("%s/%s_us.mp3", audioPath, word)
|
||||||
s.DownloadAudio(audioUK, ukPath)
|
go s.DownloadAudio(ukURL, ukPath)
|
||||||
s.DownloadAudio(audioUS, usPath)
|
go s.DownloadAudio(usURL, usPath)
|
||||||
|
audioUK = ukPath
|
||||||
|
audioUS = usPath
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
audioUK = fmt.Sprintf("%s/%s_uk.mp3", audioPath, word)
|
// 补充元信息(音标/词性/例句)
|
||||||
audioUS = fmt.Sprintf("%s/%s_us.mp3", audioPath, word)
|
meta := s.fetchDictMeta(word)
|
||||||
|
if meta != nil {
|
||||||
|
if phoneticUK == "" && meta.PhoneticUK != "" {
|
||||||
|
phoneticUK = meta.PhoneticUK
|
||||||
|
}
|
||||||
|
if phoneticUS == "" && meta.PhoneticUS != "" {
|
||||||
|
phoneticUS = meta.PhoneticUS
|
||||||
|
}
|
||||||
|
if partOfSpeech == "" || partOfSpeech == "n./v./adj." {
|
||||||
|
if meta.Part != "" {
|
||||||
|
partOfSpeech = meta.Part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if meta.Example != "" {
|
||||||
|
exampleSentence = meta.Example
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exampleSentence == "" {
|
||||||
|
exampleSentence = fmt.Sprintf("Example: %s", word)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新单词
|
// 创建新单词
|
||||||
@@ -186,6 +261,7 @@ func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*
|
|||||||
AudioUS: audioUS,
|
AudioUS: audioUS,
|
||||||
PartOfSpeech: partOfSpeech,
|
PartOfSpeech: partOfSpeech,
|
||||||
Definition: definition,
|
Definition: definition,
|
||||||
|
ExampleSentence: exampleSentence,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&newWord).Error; err != nil {
|
if err := s.db.Create(&newWord).Error; err != nil {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS words (
|
|||||||
audio_us VARCHAR(500) COMMENT '美式音频文件路径',
|
audio_us VARCHAR(500) COMMENT '美式音频文件路径',
|
||||||
part_of_speech VARCHAR(50) COMMENT '词性',
|
part_of_speech VARCHAR(50) COMMENT '词性',
|
||||||
definition TEXT COMMENT '标准释义',
|
definition TEXT COMMENT '标准释义',
|
||||||
|
example_sentence TEXT COMMENT '例句',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
INDEX idx_word (word)
|
INDEX idx_word (word)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface Word {
|
|||||||
audio_us?: string
|
audio_us?: string
|
||||||
part_of_speech?: string
|
part_of_speech?: string
|
||||||
definition?: string
|
definition?: string
|
||||||
|
example_sentence?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemoryRecord {
|
export interface MemoryRecord {
|
||||||
|
|||||||
@@ -32,8 +32,13 @@
|
|||||||
<el-descriptions-item label="单词">{{ saved.word }}</el-descriptions-item>
|
<el-descriptions-item label="单词">{{ saved.word }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="词性">{{ saved.part_of_speech }}</el-descriptions-item>
|
<el-descriptions-item label="词性">{{ saved.part_of_speech }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="释义">{{ saved.definition }}</el-descriptions-item>
|
<el-descriptions-item label="释义">{{ saved.definition }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="英音">{{ saved.phonetic_uk }}</el-descriptions-item>
|
<el-descriptions-item label="英音">{{ saved.phonetic_uk || '暂无' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="美音">{{ saved.phonetic_us }}</el-descriptions-item>
|
<el-descriptions-item label="美音">{{ saved.phonetic_us || '暂无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="例句">{{ saved.example_sentence || '暂无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发音播放">
|
||||||
|
<el-button size="small" @click="playUK" :disabled="!saved.word">播放英音</el-button>
|
||||||
|
<el-button size="small" @click="playUS" :disabled="!saved.word">播放美音</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,7 +46,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { addWord } from '../services/api'
|
import { addWord, audioUrl } from '../services/api'
|
||||||
|
|
||||||
const word = ref('')
|
const word = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -67,6 +72,18 @@ async function submit() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playUK() {
|
||||||
|
if (!saved.value?.word) return
|
||||||
|
const audio = new Audio(audioUrl(saved.value.word, 'uk'))
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function playUS() {
|
||||||
|
if (!saved.value?.word) return
|
||||||
|
const audio = new Audio(audioUrl(saved.value.word, 'us'))
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user