feat: silent wechat login, marketing tab optimization, admin page foundation

- Add silent WeChat login for MP/browser environments
- Fix Python 3.6 compatibility (remove typing.Annotated usage)
- Marketing page: tab-based content generation with category support
- Translate page: add auto-detect language default
- Homepage: add TTS playback, announcement ticker, remove redundant quick-actions
- Fix FAB button overlap with custom tabbar on customers/quotation pages
- Make openai/anthropic imports lazy for Python 3.6 compat
This commit is contained in:
TradeMate Dev
2026-05-14 00:30:48 +08:00
parent f70dd24c7d
commit 23a31f7c00
30 changed files with 485 additions and 269 deletions
+1 -1
View File
@@ -796,7 +796,7 @@ const deleteCustomer = async (id) => {
.bottom-actions {
position: fixed;
right: 40rpx;
bottom: 40rpx;
bottom: 100px;
display: flex;
flex-direction: column;
gap: 24rpx;
+166 -92
View File
@@ -5,7 +5,11 @@
<text class="username">{{ userInfo?.username || '用户' }}</text>
<text class="tier">{{ userInfo?.tier === 'pro' ? 'Pro' : '免费版' }}</text>
</view>
<view class="guest-info" v-else>
<view class="header-left" v-if="!hasLogin" @click="showAnnouncement = true">
<text class="announcement-icon">📢</text>
<text class="announcement-ticker">{{ announcements[currentAnnouncement] }}</text>
</view>
<view class="header-right" v-if="!hasLogin">
<text class="guest-label">👋 游客模式</text>
<button class="login-btn" @click="goToLogin">登录</button>
</view>
@@ -58,7 +62,10 @@
<view class="try-result" v-if="tryResult">
<view class="result-header">
<text class="result-label">翻译结果</text>
<text class="result-copy" @click="copyTryResult">复制</text>
<view class="result-actions">
<text class="result-play" @click="playTryResult">朗读</text>
<text class="result-copy" @click="copyTryResult">复制</text>
</view>
</view>
<view class="result-content">
<text class="result-text">{{ tryResult }}</text>
@@ -92,39 +99,6 @@
<view class="empty" v-else>暂无待跟进客户</view>
</view>
<view class="quick-actions">
<view class="action-item" @click="hasLogin ? goToPage('/pages/product/product') : goToLogin()">
<view class="action-icon-wrap" style="background:#e6f7ff">
<text class="action-icon-text" style="color:#1890ff"></text>
</view>
<text class="action-label">产品库</text>
</view>
<view class="action-item" @click="hasLogin ? goToPage('/pages/followup/followup') : goToLogin()">
<view class="action-icon-wrap" style="background:#f0f0ff">
<text class="action-icon-text" style="color:#667eea"></text>
</view>
<text class="action-label">
<text>跟进</text>
<text class="action-badge" v-if="hasLogin && followupStats.pending > 0">{{ followupStats.pending > 99 ? '99+' : followupStats.pending }}</text>
</text>
</view>
<view class="action-item" @click="hasLogin ? goToPage('/pages/analytics/analytics') : goToLogin()">
<view class="action-icon-wrap" style="background:#f6ffed">
<text class="action-icon-text" style="color:#52c41a"></text>
</view>
<text class="action-label">数据</text>
</view>
<view class="action-item" @click="hasLogin ? goToPage('/pages/notification/notification') : goToLogin()">
<view class="action-icon-wrap" style="background:#fff7e6">
<text class="action-icon-text" style="color:#fa8c16"></text>
</view>
<text class="action-label">
<text>通知</text>
<text class="action-badge" v-if="hasLogin && unreadCount > 0">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
</text>
</view>
</view>
<view class="section" v-if="hasLogin && followupStats.pending > 0">
<view class="section-title">
<text>待跟进提醒</text>
@@ -138,7 +112,7 @@
</view>
<view class="more-section">
<view class="section-title">更多功能</view>
<view class="section-title">功能矩阵</view>
<view class="more-grid">
<view class="more-item" @click="hasLogin ? goToPage('/pages/product/product') : goToLogin()">
<text class="more-icon">📦</text>
@@ -221,15 +195,39 @@
<text class="ob-skip" @click="finishOnboarding" v-if="onboardingStep === 1">跳过以后再说</text>
</view>
</view>
<view class="modal-overlay" v-if="showAnnouncement" @click="showAnnouncement = false">
<view class="announcement-modal" @click.stop>
<text class="announcement-title">📢 系统公告</text>
<view class="announcement-body">
<text class="announcement-line">欢迎使用外贸小助手</text>
<text class="announcement-line">登录后可解锁以下功能</text>
<text class="announcement-item"> 客户管理 管理客户信息与跟进记录</text>
<text class="announcement-item"> 报价单 快速生成并导出专业报价</text>
<text class="announcement-item"> 数据分析 查看业务统计与趋势</text>
<text class="announcement-item"> 营销素材 AI 生成营销文案与关键词</text>
<text class="announcement-line" style="margin-top: 20rpx">现在登录体验全部功能 🚀</text>
</view>
<button class="announcement-btn" @click="showAnnouncement = false; goToLogin()">去登录</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onUnmounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi } from '@/utils/api.js'
import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi, BASE_URL } from '@/utils/api.js'
const showAnnouncement = ref(false)
const currentAnnouncement = ref(0)
const announcements = [
'全新 AI 翻译引擎上线,支持多语言商务翻译',
'登录后免费使用客户管理与报价单功能',
'每日数据看板上线,实时掌握业务动态',
]
let announcementTimer = null
const userInfo = ref(null)
const hasLogin = computed(() => {
const token = uni.getStorageSync('token')
const isGuest = uni.getStorageSync('isGuest')
@@ -257,6 +255,10 @@ const tryExtracted = ref('')
const tryLoading = ref(false)
onShow(() => {
announcementTimer = setInterval(() => {
currentAnnouncement.value = (currentAnnouncement.value + 1) % announcements.length
}, 4000)
const token = uni.getStorageSync('token')
const isGuest = uni.getStorageSync('isGuest')
if (token && !isGuest) {
@@ -272,6 +274,10 @@ onShow(() => {
}
})
onUnmounted(() => {
if (announcementTimer) clearInterval(announcementTimer)
})
const checkOnboarding = async () => {
if (uni.getStorageSync('onboarded')) return
try {
@@ -435,6 +441,35 @@ const copyTryResult = () => {
},
})
}
const playTryResult = () => {
if (!tryResult.value || !tryText.value) return
const hasChinese = /[\u4e00-\u9fa5]/.test(tryText.value)
const lang = hasChinese ? 'en' : 'zh'
const token = uni.getStorageSync('token')
const url = `${BASE_URL}/translate/tts?text=${encodeURIComponent(tryResult.value)}&lang=${lang}`
uni.showLoading({ title: '语音生成中...' })
uni.downloadFile({
url,
header: token ? { Authorization: `Bearer ${token}` } : {},
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200) {
const audioCtx = uni.createInnerAudioContext()
audioCtx.src = res.tempFilePath
audioCtx.play()
audioCtx.onEnded(() => audioCtx.destroy())
} else {
uni.showToast({ title: '语音生成失败', icon: 'none' })
}
},
fail: () => {
uni.hideLoading()
uni.showToast({ title: '语音生成失败', icon: 'none' })
},
})
}
</script>
<style lang="scss" scoped>
@@ -460,15 +495,39 @@ const copyTryResult = () => {
align-items: center;
}
.guest-info {
.header-left {
display: flex;
align-items: center;
gap: 16rpx;
gap: 12rpx;
flex: 1;
cursor: pointer;
overflow: hidden;
}
.announcement-icon {
font-size: 32rpx;
flex-shrink: 0;
}
.announcement-ticker {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-right {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.guest-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
.login-btn {
@@ -654,11 +713,21 @@ const copyTryResult = () => {
font-weight: 500;
}
.result-actions {
display: flex;
gap: 16rpx;
}
.result-copy {
font-size: 24rpx;
color: #1890ff;
}
.result-play {
font-size: 24rpx;
color: #52c41a;
}
.result-content, .extracted-content {
background: #fff;
border-radius: 8rpx;
@@ -759,57 +828,6 @@ const copyTryResult = () => {
margin-top: 12rpx;
}
.quick-actions {
display: flex;
background: #fff;
border-radius: 16rpx;
padding: 20rpx 16rpx;
margin-bottom: 20rpx;
justify-content: space-around;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
cursor: pointer;
}
.action-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.action-icon-text {
font-size: 30rpx;
font-weight: bold;
}
.action-label {
font-size: 22rpx;
color: #666;
display: flex;
align-items: center;
gap: 4rpx;
}
.action-badge {
background: #ff4d4f;
color: #fff;
font-size: 18rpx;
min-width: 28rpx;
height: 28rpx;
border-radius: 14rpx;
text-align: center;
line-height: 28rpx;
padding: 0 6rpx;
}
.more-section {
background: #fff;
border-radius: 16rpx;
@@ -887,4 +905,60 @@ const copyTryResult = () => {
.ob-btn { width: 100%; height: 88rpx; border-radius: 12rpx; font-size: 30rpx; border: none; display: flex; align-items: center; justify-content: center; }
.ob-btn-primary { background: #1890ff; color: #fff; }
.ob-skip { display: block; text-align: center; margin-top: 24rpx; font-size: 24rpx; color: #999; }
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
z-index: 999; padding: 40rpx;
}
.announcement-modal {
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
width: 100%;
max-width: 560rpx;
}
.announcement-title {
font-size: 34rpx;
font-weight: 700;
color: #333;
display: block;
text-align: center;
margin-bottom: 32rpx;
}
.announcement-body {
margin-bottom: 32rpx;
}
.announcement-line {
font-size: 26rpx;
color: #555;
display: block;
line-height: 1.6;
margin-bottom: 8rpx;
}
.announcement-item {
font-size: 24rpx;
color: #666;
display: block;
line-height: 1.8;
padding-left: 20rpx;
}
.announcement-btn {
width: 100%;
height: 88rpx;
background: #1890ff;
color: #fff;
border-radius: 12rpx;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>
+87 -13
View File
@@ -1,5 +1,9 @@
<template>
<view class="login-container">
<view class="silent-loading" v-if="silentLoading">
<text class="silent-loading-text">正在自动登录...</text>
</view>
<view class="welcome-section">
<text class="logo">TradeMate</text>
<text class="subtitle">外贸小助手</text>
@@ -110,7 +114,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { authApi } from '@/utils/api.js'
const phone = ref('')
@@ -118,13 +122,73 @@ const password = ref('')
const username = ref('')
const isRegister = ref(false)
const loading = ref(false)
const silentLoading = ref(true)
const error = ref('')
const showForm = ref(false)
const isWechatAvailable = ref(false)
// #ifdef MP-WEIXIN
isWechatAvailable.value = true
// #endif
const doWechatLogin = async (code) => {
const res = await authApi.wechatLogin(code)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
uni.switchTab({ url: '/pages/index/index' })
}
onMounted(async () => {
// #ifdef MP-WEIXIN
// 微信小程序:静默登录
isWechatAvailable.value = true
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
await doWechatLogin(loginRes.code)
} catch (_) {
silentLoading.value = false
}
// #endif
// #ifdef H5
// H5 微信内置浏览器:OAuth 静默登录
const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent)
if (isWechatBrowser) {
try {
const cfg = await authApi.wechatConfig()
if (cfg.available && cfg.app_id) {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
if (code) {
// OAuth 回调,携带 code
await doWechatLogin(code)
// 清除 URL 中的 code 参数
window.history.replaceState({}, '', window.location.pathname)
return
} else {
// 跳转微信 OAuth 授权
const redirectUri = encodeURIComponent(window.location.href.split('?')[0])
window.location.href =
`https://open.weixin.qq.com/connect/oauth2/authorize` +
`?appid=${cfg.app_id}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code` +
`&scope=snsapi_base` +
`&state=STATE#wechat_redirect`
return
}
}
} catch (_) {}
}
silentLoading.value = false
// #endif
// #ifndef MP-WEIXIN
if (!/MicroMessenger/i.test(navigator.userAgent)) {
silentLoading.value = false
}
// #endif
})
const toggleMode = () => {
isRegister.value = !isRegister.value
@@ -178,15 +242,7 @@ const handleWechatLogin = () => {
success: async (loginRes) => {
try {
loading.value = true
const res = await authApi.wechatLogin(loginRes.code)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
await doWechatLogin(loginRes.code)
} catch (err) {
error.value = err.message || '微信登录失败'
} finally {
@@ -438,6 +494,24 @@ const goToQuickTry = async () => {
margin-right: 12rpx;
}
.silent-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.silent-loading-text {
font-size: 28rpx;
color: #999;
}
.footer {
text-align: center;
margin-top: 40rpx;
+96 -51
View File
@@ -4,28 +4,28 @@
<view
class="tab-item"
:class="{ active: activeTab === 'copy' }"
@click="activeTab = 'copy'; activeTab === 'copy' && loadStats()"
@click="switchTab('copy')"
>
开发信
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'whatsapp' }"
@click="activeTab = 'whatsapp'; activeTab === 'whatsapp' && loadStats()"
@click="switchTab('whatsapp')"
>
WhatsApp话术
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'product' }"
@click="activeTab = 'product'; activeTab === 'product' && loadStats()"
@click="switchTab('product')"
>
产品描述
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'keywords' }"
@click="activeTab = 'keywords'"
@click="switchTab('keywords')"
>
关键词
</view>
@@ -49,19 +49,19 @@
<view class="form-section">
<view class="form-item">
<text class="form-label">产品名称</text>
<input class="form-input" v-model="formData.product_name" placeholder="如: 户外折叠椅" />
<input class="form-input" v-model="formData.product_name" :placeholder="tabConfig[activeTab].namePlaceholder" />
</view>
<view class="form-item">
<text class="form-label">产品描述</text>
<textarea class="form-textarea" v-model="formData.description" placeholder="描述产品的特点、规格、优势..." />
<text class="form-label">{{ tabConfig[activeTab].descLabel }}</text>
<textarea class="form-textarea" v-model="formData.description" :placeholder="tabConfig[activeTab].descPlaceholder" />
</view>
<view class="form-item">
<view class="form-item" v-if="tabConfig[activeTab].showTarget">
<text class="form-label">目标市场</text>
<picker :range="targetMarkets" @change="onTargetChange">
<view class="picker-value">{{ formData.target || '选择目标市场' }}</view>
</picker>
</view>
<view class="form-item">
<view class="form-item" v-if="tabConfig[activeTab].showStyle">
<text class="form-label">文案风格</text>
<view class="style-options">
<view
@@ -88,34 +88,30 @@
</view>
</view>
<button class="generate-btn" @click="generateContent" :disabled="loading">
{{ loading ? '生成中...' : '生成文案' }}
{{ loading ? '生成中...' : tabConfig[activeTab].btnText }}
</button>
</view>
<view class="results-section" v-if="results.length > 0 && activeTab !== 'keywords'">
<view class="results-section" v-if="resultsMap[activeTab] && resultsMap[activeTab].length > 0">
<view class="results-header">
<text class="results-title">生成的文案</text>
<text class="refresh-btn" @click="generateContent">换一批</text>
<text class="export-btn" @click="exportCsv">导出CSV</text>
<text class="results-title">{{ tabConfig[activeTab].resultTitle }}</text>
<view class="results-actions">
<text class="refresh-btn" @click="generateContent">换一批</text>
<text class="export-btn" @click="exportCsv" v-if="activeTab !== 'keywords'">导出CSV</text>
</view>
</view>
<view class="results-list">
<view class="result-item" v-for="(item, index) in results" :key="index">
<view class="results-list" v-if="activeTab !== 'keywords'">
<view class="result-item" v-for="(item, index) in resultsMap[activeTab]" :key="index">
<text class="result-text">{{ item }}</text>
<view class="result-actions">
<text class="copy-btn" @click="copyText(item)">复制</text>
<text class="send-btn" @click="sendToWhatsapp(item)">发送</text>
<text class="competitor-btn" @click="runCompetitorAnalysis">竞品分析</text>
<text class="send-btn" @click="sendToWhatsapp(item)" v-if="activeTab !== 'product'">发送</text>
<text class="competitor-btn" @click="runCompetitorAnalysis" v-if="activeTab === 'copy'">竞品分析</text>
</view>
</view>
</view>
</view>
<view class="history-section" v-if="activeTab === 'keywords' && keywords.length > 0">
<view class="history-header">
<text class="history-title">关键词建议</text>
</view>
<view class="keywords-list">
<view class="keyword-tag" v-for="(kw, idx) in keywords" :key="idx" @click="copyText(kw)">
<view class="keywords-list" v-if="activeTab === 'keywords' && resultsMap.keywords && resultsMap.keywords.length > 0">
<view class="keyword-tag" v-for="(kw, idx) in resultsMap.keywords" :key="idx" @click="copyText(kw)">
{{ kw }}
</view>
</view>
@@ -131,20 +127,70 @@
</view>
</view>
<view class="empty" v-if="!loading && results.length === 0 && activeTab !== 'keywords'">
<text>输入产品信息点击生成文案</text>
<view class="empty" v-if="!loading && (!resultsMap[activeTab] || resultsMap[activeTab].length === 0)">
<text>{{ tabConfig[activeTab].emptyHint }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { ref, reactive } from 'vue'
import { marketingApi, interactionApi } from '@/utils/api.js'
const tabConfig = {
copy: {
label: '开发信',
category: 'sales_letter',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品描述',
descPlaceholder: '描述产品的特点、规格、优势...',
btnText: '生成开发信',
resultTitle: '生成的开发信',
emptyHint: '输入产品信息,点击生成开发信',
showTarget: true,
showStyle: true,
},
whatsapp: {
label: 'WhatsApp话术',
category: 'whatsapp',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品及沟通场景',
descPlaceholder: '描述产品特点,以及和客户沟通的具体场景...',
btnText: '生成话术',
resultTitle: '生成的WhatsApp话术',
emptyHint: '输入产品信息,点击生成话术',
showTarget: true,
showStyle: true,
},
product: {
label: '产品描述',
category: 'product_description',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品详细规格',
descPlaceholder: '描述产品的材质、尺寸、承重、颜色、包装等规格...',
btnText: '生成描述',
resultTitle: '生成的产品描述',
emptyHint: '输入产品信息,点击生成描述',
showTarget: false,
showStyle: false,
},
keywords: {
label: '关键词',
category: '',
namePlaceholder: '如: 户外折叠椅',
descLabel: '产品卖点',
descPlaceholder: '描述产品的核心卖点和目标客户群体...',
btnText: '生成关键词',
resultTitle: '关键词建议',
emptyHint: '输入产品信息,点击生成关键词',
showTarget: false,
showStyle: false,
},
}
const activeTab = ref('copy')
const loading = ref(false)
const results = ref([])
const keywords = ref([])
const resultsMap = reactive({ copy: [], whatsapp: [], product: [], keywords: [] })
const competitorResult = ref(null)
const stats = ref(null)
@@ -167,6 +213,11 @@ const onTargetChange = (e) => {
formData.value.target = targetMarkets.value[e.detail.value]
}
const switchTab = (tab) => {
activeTab.value = tab
loadStats()
}
const loadStats = async () => {
try {
const res = await interactionApi.getMarketingEffectStats()
@@ -183,24 +234,26 @@ const generateContent = async () => {
}
loading.value = true
const tab = activeTab.value
try {
if (activeTab.value === 'keywords') {
if (tab === 'keywords') {
const res = await marketingApi.getKeywords(
formData.value.product_name,
formData.value.description,
''
)
keywords.value = res.keywords || []
resultsMap[tab] = res.keywords || []
} else {
const cfg = tabConfig[tab]
const res = await marketingApi.generate(
formData.value.product_name,
formData.value.description,
'',
cfg.category,
formData.value.target,
formData.value.style
)
results.value = res.results || []
resultsMap[tab] = res.results || []
loadStats()
}
} catch (err) {
@@ -222,9 +275,10 @@ const copyText = (text) => {
}
const exportCsv = () => {
if (results.value.length === 0) return
const items = resultsMap[activeTab.value]
if (!items || items.length === 0) return
let csv = 'Content\n'
results.value.forEach(r => { csv += `"${r.replace(/"/g, '""')}"\n` })
items.forEach(r => { csv += `"${r.replace(/"/g, '""')}"\n` })
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
uni.downloadFile({
@@ -414,6 +468,11 @@ const runCompetitorAnalysis = async () => {
font-weight: 600;
}
.results-actions {
display: flex;
gap: 16rpx;
}
.refresh-btn {
font-size: 24rpx;
color: #1890ff;
@@ -422,7 +481,6 @@ const runCompetitorAnalysis = async () => {
.export-btn {
font-size: 24rpx;
color: #52c41a;
margin-left: 16rpx;
}
.results-list {
@@ -470,19 +528,6 @@ const runCompetitorAnalysis = async () => {
color: #722ed1;
}
.history-section {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
}
.history-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 20rpx;
display: block;
}
.keywords-list {
display: flex;
flex-wrap: wrap;
+2 -2
View File
@@ -503,7 +503,7 @@ const exportPdf = (item) => {
.export-csv-btn {
position: fixed;
right: 40rpx;
bottom: 160rpx;
bottom: calc(100px + 100rpx + 24rpx);
width: 100rpx;
height: 100rpx;
background: #722ed1;
@@ -523,7 +523,7 @@ const exportPdf = (item) => {
.add-btn {
position: fixed;
right: 40rpx;
bottom: 40rpx;
bottom: 100px;
width: 100rpx;
height: 100rpx;
background: #1890ff;
+8 -2
View File
@@ -114,13 +114,14 @@ const inputText = ref('')
const result = ref('')
const suggestions = ref([])
const loading = ref(false)
const targetIndex = ref(1)
const targetIndex = ref(0)
const keyboardHeight = ref(0)
const rating = ref(0)
const extractedInfo = ref(null)
const preferences = ref(null)
const targetLangs = ref([
{ code: 'auto', name: '自动检测' },
{ code: 'en', name: 'English' },
{ code: 'zh', name: '中文' },
{ code: 'es', name: 'Español' },
@@ -143,9 +144,14 @@ const handleTranslate = async () => {
try {
if (mode.value === 'translate') {
let targetLang = targetLangs[targetIndex.value].code
if (targetLang === 'auto') {
const hasChinese = /[\u4e00-\u9fa5]/.test(inputText.value)
targetLang = hasChinese ? 'en' : 'zh'
}
const res = await translateApi.translate(
inputText.value,
targetLangs[targetIndex.value].code
targetLang
)
result.value = res.translated
loadPreferences()
+1
View File
@@ -61,6 +61,7 @@ export const authApi = {
register: (phone, password, username) => request('/auth/register', 'POST', { phone, password, username }),
getUserInfo: () => request('/auth/me'),
wechatLogin: (code) => request('/auth/wechat-login', 'POST', { code }),
wechatConfig: () => request('/auth/wechat/config'),
guestLogin: () => requestWithoutAuth('/auth/login/guest', 'POST'),
}