Compare commits

...

2 Commits

Author SHA1 Message Date
KirisameVanilla 632caa28ac
fix: name 2025-11-05 18:29:20 +08:00
KirisameVanilla 92db732e7e
feat: so on 2025-11-05 18:09:38 +08:00
11 changed files with 1800 additions and 44 deletions

1194
api.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dictionay</title> <title>Lexiverse</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

89
src/api/ai.ts Normal file
View File

@ -0,0 +1,89 @@
import apiClient from './client'
/**
* AI助手问答请求参数
*/
export interface AiQuestionRequest {
word: string
question: string
}
/**
* AI助手问答响应
*/
export interface AiQuestionResponse {
word: string
answer: string
model: string
tokens_used: number
}
/**
*
* @param data
* @returns AI回答
*/
export const askAiAssistant = async (data: AiQuestionRequest): Promise<AiQuestionResponse> => {
try {
const response = await apiClient.post<AiQuestionResponse>('/ai_assist/word/exp', data)
return response.data
} catch (error: any) {
console.error('AI Assistant API error:', error)
if (error.response) {
const status = error.response.status
const message = error.response.data?.message || 'AI助手请求失败'
switch (status) {
case 400:
throw new Error('本月API使用量已超')
case 401:
throw new Error('请先登录后再使用AI助手')
case 500:
throw new Error('AI服务暂时不可用请稍后重试')
default:
throw new Error(`AI助手请求失败${message}`)
}
} else if (error.request) {
throw new Error('网络连接失败,请检查网络连接')
} else {
throw new Error('AI助手请求失败请稍后重试')
}
}
}
/**
*
* @param word
* @returns
*/
export const clearAiHistory = async (word: string): Promise<{ msg: string }> => {
try {
const response = await apiClient.post<{ msg: string }>('/ai_assist/clear', null, {
params: { word }
})
return response.data
} catch (error: any) {
console.error('Clear AI history error:', error)
if (error.response) {
const message = error.response.data?.message || '清除聊天记录失败'
throw new Error(message)
} else {
throw new Error('清除聊天记录失败,请稍后重试')
}
}
}
/**
* AI对话
* @returns
*/
export const universalAiChat = async (): Promise<void> => {
try {
await apiClient.post('/ai_assist/univer')
} catch (error: any) {
console.error('Universal AI chat error:', error)
throw new Error('通用AI对话功能暂未开放')
}
}

View File

@ -8,8 +8,11 @@ export interface LoginRequest {
export interface RegisterRequest { export interface RegisterRequest {
username: string username: string
password: string password: string
email: string
phone?: string
lang_pref: 'jp' | 'fr' | 'private' lang_pref: 'jp' | 'fr' | 'private'
portrait?: string portrait?: string
code: string
} }
export interface LoginResponse { export interface LoginResponse {
@ -27,6 +30,34 @@ export interface RegisterResponse {
message: string message: string
} }
export interface EmailVerifyRequest {
email: string
}
export interface EmailVerifyResponse {
message: string
}
export interface VerifyCodeRequest {
email: string
code: string
}
export interface VerifyCodeResponse {
reset_token: string
}
export interface ResetPasswordRequest {
password: string
}
export interface UpdateUserRequest {
current_password: string
new_username?: string
new_password?: string
new_language?: 'jp' | 'fr' | 'private'
}
// 用户登录 // 用户登录
export const login = async (credentials: LoginRequest): Promise<LoginResponse> => { export const login = async (credentials: LoginRequest): Promise<LoginResponse> => {
const response = await apiClient.post('/users/login', credentials) const response = await apiClient.post('/users/login', credentials)
@ -44,6 +75,40 @@ export const register = async (userData: RegisterRequest): Promise<RegisterRespo
return response.data return response.data
} }
// 邮箱验证 - 注册时
export const sendEmailVerification = async (data: EmailVerifyRequest): Promise<EmailVerifyResponse> => {
const response = await apiClient.post('/users/register/email_verify', data)
return response.data
}
// 邮箱找回密码(发送验证码)
export const sendPasswordResetEmail = async (data: EmailVerifyRequest): Promise<EmailVerifyResponse> => {
const response = await apiClient.post('/users/auth/forget-password/email', data)
return response.data
}
// 邮箱验证码验证
export const verifyEmailCode = async (data: VerifyCodeRequest): Promise<VerifyCodeResponse> => {
const response = await apiClient.post('/users/auth/varify_code/email', data)
return response.data
}
// 重置密码
export const resetPassword = async (data: ResetPasswordRequest, resetToken: string): Promise<{ message: string }> => {
const response = await apiClient.post('/users/auth/reset-password', data, {
headers: {
'x-reset-token': resetToken
}
})
return response.data
}
// 更新用户信息
export const updateUser = async (data: UpdateUserRequest): Promise<{ message: string }> => {
const response = await apiClient.put('/users/update', data)
return response.data
}
// 用户登出 // 用户登出
export const logout = async (): Promise<void> => { export const logout = async (): Promise<void> => {
await apiClient.post('/users/logout') await apiClient.post('/users/logout')

41
src/api/comment.ts Normal file
View File

@ -0,0 +1,41 @@
import apiClient from './client'
/**
*
*/
export interface WordCommentRequest {
comment_word: string
comment_content: string
}
/**
*
* @param lang (fr jp)
* @param data
* @returns
*/
export const addWordComment = async (lang: 'fr' | 'jp', data: WordCommentRequest): Promise<void> => {
try {
await apiClient.post(`/comment/word/${lang}`, data)
} catch (error: any) {
console.error('Add word comment error:', error)
if (error.response) {
const status = error.response.status
const message = error.response.data?.message || '添加评论失败'
switch (status) {
case 401:
throw new Error('请先登录后再添加评论')
case 422:
throw new Error('评论内容格式不正确')
default:
throw new Error(`添加评论失败:${message}`)
}
} else if (error.request) {
throw new Error('网络连接失败,请检查网络连接')
} else {
throw new Error('添加评论失败,请稍后重试')
}
}
}

View File

@ -12,13 +12,17 @@ export interface WordDefinition {
} }
export interface SearchParams { export interface SearchParams {
lang_pref: string language: string
query_word: string query: string
sort?: string
order?: string
} }
export interface SearchSuggestParams { export interface SearchSuggestParams {
query: string query: string
language: string language: string
sort?: string
order?: string
} }
export interface SearchSuggestResponse { export interface SearchSuggestResponse {
@ -28,21 +32,97 @@ export interface SearchSuggestResponse {
// 搜索单词 // 搜索单词
export const searchWord = async (params: SearchParams): Promise<WordDefinition> => { export const searchWord = async (params: SearchParams): Promise<WordDefinition> => {
const packet = { const packet = {
language: params.lang_pref, language: params.language,
query: params.query_word, query: params.query,
sort: "relevance", sort: params.sort || "relevance",
order: "asc" order: params.order || "des"
} }
const response = await apiClient.post('/search', packet) const response = await apiClient.post('/search/word', packet)
console.log(response) console.log(response)
return response.data return response.data
} }
// 搜索推荐 // 搜索推荐
export const searchSuggest = async (params: SearchSuggestParams): Promise<SearchSuggestResponse> => { export const searchSuggest = async (params: SearchSuggestParams): Promise<SearchSuggestResponse> => {
const response = await apiClient.post('/search/list', { const response = await apiClient.post('/search/list/word', {
query: params.query, query: params.query,
language: params.language language: params.language,
sort: params.sort || "relevance",
order: params.order || "des"
})
return response.data
}
/**
*
*/
export interface ProverbDetail {
text: string
chi_exp: string
freq: number
}
export interface ProverbListItem {
id: number
proverb: string
chi_exp: string
}
// 获取法语谚语详情
export const getProverbDetail = async (proverbId: number): Promise<{ result: ProverbDetail }> => {
const formData = new FormData()
formData.append('proverb_id', proverbId.toString())
const response = await apiClient.post('/search/proverb', formData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
return response.data
}
// 获取谚语联想建议
export const searchProverbSuggest = async (query: string, dictLanguage: string): Promise<{ list: ProverbListItem[] }> => {
const response = await apiClient.post('/search/list/proverb', {
query,
dict_language: dictLanguage
})
return response.data
}
/**
*
*/
export interface IdiomDetail {
id: number
text: string
search_text: string
chi_exp: string
example: string
freq: number
}
export interface IdiomListItem {
id: number
proverb: string
chi_exp: string
}
// 获取日语惯用语详情
export const getIdiomDetail = async (queryId: number): Promise<{ result: IdiomDetail }> => {
const response = await apiClient.post('/search/idiom', null, {
params: { query_id: queryId }
})
return response.data
}
// 获取日语惯用语联想建议
export const searchIdiomSuggest = async (query: string, dictLanguage: string = 'jp'): Promise<{ list: IdiomListItem[] }> => {
const response = await apiClient.post('/search/list/idiom', {
query,
dict_language: dictLanguage
}) })
return response.data return response.data
} }

61
src/api/feedback.ts Normal file
View File

@ -0,0 +1,61 @@
import apiClient from './client'
/**
*
*/
export type FeedbackType =
| 'ui_design'
| 'dict_fr'
| 'dict_jp'
| 'user'
| 'translate'
| 'writting'
| 'ai_assist'
| 'pronounce'
/**
*
*/
export interface FeedbackRequest {
report_part: FeedbackType
text: string
}
/**
*
*/
export interface FeedbackResponse {
massages: string
}
/**
*
* @param data
* @returns
*/
export const submitFeedback = async (data: FeedbackRequest): Promise<FeedbackResponse> => {
try {
const response = await apiClient.post<FeedbackResponse>('/improvements', data)
return response.data
} catch (error: any) {
console.error('Submit feedback error:', error)
if (error.response) {
const status = error.response.status
const message = error.response.data?.message || '提交反馈失败'
switch (status) {
case 401:
throw new Error('请先登录后再提交反馈')
case 422:
throw new Error('反馈内容格式不正确,请检查后重试')
default:
throw new Error(`提交反馈失败:${message}`)
}
} else if (error.request) {
throw new Error('网络连接失败,请检查网络连接')
} else {
throw new Error('提交反馈失败,请稍后重试')
}
}
}

142
src/api/writing.ts Normal file
View File

@ -0,0 +1,142 @@
import apiClient from './client'
/**
*
*/
export interface ArticleReviewRequest {
title_content: string
article_type: string
}
/**
*
*/
export interface ArticleReviewResponse {
reply: string
tokens: number
conversation_length: number
}
/**
*
*/
export interface ArticleQuestionRequest {
query: string
}
/**
*
*/
export interface ArticleQuestionResponse {
reply: string
tokens: number
conversation_length: number
}
/**
*
*/
export interface ResetSessionResponse {
message: string
}
/**
*
* @param data
* @param lang fr-FR
* @returns AI批改结果
*/
export const reviewArticle = async (
data: ArticleReviewRequest,
lang: string = 'fr-FR'
): Promise<ArticleReviewResponse> => {
try {
const response = await apiClient.post<ArticleReviewResponse>(
'/article-director/article',
data,
{
params: { lang }
}
)
return response.data
} catch (error: any) {
console.error('Article review API error:', error)
if (error.response) {
const status = error.response.status
const message = error.response.data?.message || '作文批改失败'
switch (status) {
case 401:
throw new Error('请先登录后再使用作文批改功能')
case 500:
throw new Error('作文批改服务暂时不可用,请稍后重试')
default:
throw new Error(`作文批改失败:${message}`)
}
} else if (error.request) {
throw new Error('网络连接失败,请检查网络连接')
} else {
throw new Error('作文批改请求失败,请稍后重试')
}
}
}
/**
*
* @param data
* @returns AI回答
*/
export const askArticleQuestion = async (
data: ArticleQuestionRequest
): Promise<ArticleQuestionResponse> => {
try {
const response = await apiClient.post<ArticleQuestionResponse>(
'/article-director/question',
data
)
return response.data
} catch (error: any) {
console.error('Article question API error:', error)
if (error.response) {
const status = error.response.status
const message = error.response.data?.message || '追问失败'
switch (status) {
case 401:
throw new Error('请先登录后再使用追问功能')
case 500:
throw new Error('追问服务暂时不可用,请稍后重试')
default:
throw new Error(`追问失败:${message}`)
}
} else if (error.request) {
throw new Error('网络连接失败,请检查网络连接')
} else {
throw new Error('追问请求失败,请稍后重试')
}
}
}
/**
*
* @returns
*/
export const resetArticleSession = async (): Promise<ResetSessionResponse> => {
try {
const response = await apiClient.post<ResetSessionResponse>(
'/article-director/reset'
)
return response.data
} catch (error: any) {
console.error('Reset article session error:', error)
if (error.response) {
const message = error.response.data?.message || '重置会话失败'
throw new Error(message)
} else {
throw new Error('重置会话失败,请稍后重试')
}
}
}

View File

@ -90,8 +90,8 @@ const handleSearch = async (query: string, lang: string) => {
try { try {
const result = await searchWord({ const result = await searchWord({
lang_pref: lang, language: lang,
query_word: query.trim() query: query.trim()
}) })
searchResult.value = result searchResult.value = result
} catch (err: any) { } catch (err: any) {

View File

@ -64,7 +64,7 @@
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl" class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
/> />
<!-- 注册时显示确认密码 --> <!-- 注册时显示额外字段 -->
<template v-if="!isLogin"> <template v-if="!isLogin">
<label class="block mt-8 mb-6 text-gray-500 text-xl">确认密码</label> <label class="block mt-8 mb-6 text-gray-500 text-xl">确认密码</label>
<input <input
@ -74,6 +74,42 @@
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl" class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
/> />
<label class="block mt-8 mb-6 text-gray-500 text-xl">邮箱</label>
<input
v-model="registerForm.email"
type="email"
required
placeholder="请输入邮箱地址"
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
/>
<label class="block mt-8 mb-6 text-gray-500 text-xl">邮箱验证码</label>
<div class="flex gap-4">
<input
v-model="registerForm.code"
type="text"
required
placeholder="请输入邮箱验证码"
class="flex-1 px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none h-[63px] text-2xl"
/>
<button
type="button"
@click="sendVerificationCode"
:disabled="sendingCode || countdown > 0 || !registerForm.email"
class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 px-6 rounded-full text-white text-lg whitespace-nowrap disabled:cursor-not-allowed"
>
{{ countdown > 0 ? `${countdown}秒后重试` : (codeSent ? '重新发送' : '发送验证码') }}
</button>
</div>
<label class="block mt-8 mb-6 text-gray-500 text-xl">手机号可选</label>
<input
v-model="registerForm.phone"
type="tel"
placeholder="请输入手机号(可选)"
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
/>
<label class="block mt-8 mb-6 text-gray-500 text-xl">语言偏好</label> <label class="block mt-8 mb-6 text-gray-500 text-xl">语言偏好</label>
<select <select
v-model="registerForm.lang_pref" v-model="registerForm.lang_pref"
@ -104,10 +140,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuth } from '../composables/useAuth' import { useAuth } from '../composables/useAuth'
import { useModal } from '../composables/useModal' import { useModal } from '../composables/useModal'
import { sendEmailVerification } from '../api/auth'
const router = useRouter() const router = useRouter()
const { login, register } = useAuth() const { login, register } = useAuth()
@ -123,11 +160,17 @@ const loginForm = ref({
const registerForm = ref({ const registerForm = ref({
username: '', username: '',
password: '', password: '',
email: '',
phone: '',
lang_pref: 'fr' as 'jp' | 'fr' | 'private', lang_pref: 'fr' as 'jp' | 'fr' | 'private',
portrait: '' portrait: '',
code: ''
}) })
const confirmPassword = ref('') const confirmPassword = ref('')
const sendingCode = ref(false)
const codeSent = ref(false)
const countdown = ref(0)
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const successMessage = ref('') const successMessage = ref('')
@ -225,10 +268,15 @@ const handleRegister = async () => {
registerForm.value = { registerForm.value = {
username: '', username: '',
password: '', password: '',
email: '',
phone: '',
lang_pref: 'fr', lang_pref: 'fr',
portrait: '' portrait: '',
code: ''
} }
confirmPassword.value = '' confirmPassword.value = ''
codeSent.value = false
countdown.value = 0
}, 2000) }, 2000)
} else { } else {
// //
@ -245,6 +293,46 @@ const handleRegister = async () => {
} }
} }
//
const sendVerificationCode = async () => {
if (!registerForm.value.email) {
error.value = '请先输入邮箱地址'
return
}
//
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(registerForm.value.email)) {
error.value = '请输入有效的邮箱地址'
return
}
sendingCode.value = true
error.value = ''
try {
await sendEmailVerification({ email: registerForm.value.email })
showSuccess('验证码已发送到您的邮箱,请查收')
codeSent.value = true
//
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
} catch (err: any) {
console.error('Send verification code error:', err)
const errorMessage = err.response?.data?.detail || err.response?.data?.message || '发送验证码失败'
showError(errorMessage)
error.value = errorMessage
} finally {
sendingCode.value = false
}
}
// //
const clearMessages = () => { const clearMessages = () => {
error.value = '' error.value = ''
@ -252,7 +340,6 @@ const clearMessages = () => {
} }
// //
import { watch } from 'vue'
watch(isLogin, () => { watch(isLogin, () => {
clearMessages() clearMessages()
}) })

View File

@ -69,7 +69,7 @@
<h3 class="mb-4 font-semibold text-gray-700">AI写作指导</h3> <h3 class="mb-4 font-semibold text-gray-700">AI写作指导</h3>
<div class="h-[300px] overflow-y-auto"> <div class="h-[300px] overflow-y-auto">
<div v-if="loading" class="flex justify-center items-center h-full text-gray-500"> <div v-if="loading" class="flex justify-center items-center h-full text-gray-500">
<div class="border-b-2 border-blue-700 rounded-full w-8 h-8 animate-spin"></div> <div class="border-blue-700 border-b-2 rounded-full w-8 h-8 animate-spin"></div>
<span class="ml-2">AI正在分析您的作文...</span> <span class="ml-2">AI正在分析您的作文...</span>
</div> </div>
<div v-else-if="writingFeedback" class="space-y-4"> <div v-else-if="writingFeedback" class="space-y-4">
@ -124,9 +124,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onUnmounted } from 'vue'
import AppHeader from '../components/AppHeader.vue' import AppHeader from '../components/AppHeader.vue'
import AppFooter from '../components/AppFooter.vue' import AppFooter from '../components/AppFooter.vue'
import { reviewArticle, resetArticleSession } from '../api/writing'
const writingTypes = [ const writingTypes = [
{ value: 'essay', label: '议论文' }, { value: 'essay', label: '议论文' },
@ -189,42 +190,38 @@ const getWritingHelp = async () => {
error.value = '' error.value = ''
try { try {
// AI // API
await new Promise(resolve => setTimeout(resolve, 2000)) const result = await reviewArticle({
title_content: userText.value.trim(),
article_type: writingTypes.find(t => t.value === selectedType.value)?.label || '议论文'
}, 'fr-FR')
// AI // APIreply
// overall
// APIJSON使
writingFeedback.value = { writingFeedback.value = {
overall: `您的作文整体结构清晰,主题明确。建议在以下方面进行改进:`, overall: result.reply,
grammar: [ tokens: `使用Token数: ${result.tokens}`,
'注意动词变位的准确性', conversation_length: `对话长度: ${result.conversation_length}`
'检查形容词与名词的性数一致',
'确保时态使用的连贯性'
],
vocabulary: [
'可以使用更多高级词汇来丰富表达',
'避免重复使用相同的词汇',
'尝试使用同义词增加文章的多样性'
],
structure: [
'段落之间的过渡可以更加自然',
'可以增加更多的连接词',
'结论部分可以更加有力'
],
style: [
'保持正式的写作风格',
'句子长度可以适当变化',
'增加一些修辞手法会更好'
]
} }
} catch (err: any) { } catch (err: any) {
console.error('Writing help error:', err) console.error('Writing help error:', err)
error.value = '获取写作指导失败,请稍后重试' error.value = err.message || '获取写作指导失败,请稍后重试'
} finally { } finally {
loading.value = false loading.value = false
} }
} }
//
onUnmounted(async () => {
try {
await resetArticleSession()
} catch (err) {
console.error('Failed to reset article session:', err)
}
})
const useTemplate = (template: string) => { const useTemplate = (template: string) => {
if (userText.value && !confirm('使用模板将替换当前内容,确定继续吗?')) { if (userText.value && !confirm('使用模板将替换当前内容,确定继续吗?')) {
return return