feat(word): show/play UK-US audio and add example sentence

This commit is contained in:
2026-02-26 13:22:03 +08:00
parent e2a9ebc7b7
commit e5a245155a
5 changed files with 132 additions and 36 deletions

View File

@@ -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"`
} }

View File

@@ -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:使用有道 dictvoicetype=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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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>