Files
trade-assistant/uni-app/src/pages/index/index.vue
T
TradeMate Dev d2736d1ef6 feat: AI routing DB-driven, payment gateway full integration, WeChat mini-program CI/CD
- AI routing rules now stored in system_configs DB table instead of hardcoded config
- Multi-model support via name|model composite key for same-provider routing
- UnifiedPayService with HMAC-SHA256 gateway integration (alipay/wechat)
- Admin payment panel: list, stats, search, filter, refund
- WeChat mini-program CI/CD via miniprogram-ci (v1.0.9)
- Translation quota extended to LLM provider tier
- SearchService with DB-driven provider config (bing/google_cse/searxng)
- Footer cleanup across admin/workspace/uni-app
- Private key excluded from git tracking
2026-06-09 17:19:45 +08:00

1330 lines
36 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="index-container">
<view class="header">
<view class="user-info" v-if="hasLogin">
<text class="username">{{ userInfo?.username || '用户' }}</text>
<text class="tier">{{ userInfo?.tier === 'pro' ? 'Pro' : '免费版' }}</text>
</view>
<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>
<text class="logout" @click="handleLogout" v-if="hasLogin">退出</text>
</view>
<view class="stats-grid" v-if="hasLogin">
<view class="stat-card">
<text class="stat-value">{{ stats.customers }}</text>
<text class="stat-label">客户总数</text>
</view>
<view class="stat-card warning">
<text class="stat-value">{{ stats.silentCustomers }}</text>
<text class="stat-label">沉默客户</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.todayTranslations }}</text>
<text class="stat-label">今日翻译</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.quotations }}</text>
<text class="stat-label">报价单</text>
</view>
</view>
<view class="guest-welcome" v-else>
<view class="welcome-content">
<text class="welcome-title">欢迎使用外贸小助手 🎉</text>
<text class="welcome-desc">您当前处于游客模式可以体验翻译和信息提取功能</text>
<text class="welcome-hint">登录后可解锁客户管理报价单生成数据分析等更多功能</text>
</view>
<view class="quick-try-section">
<view class="section-title">快速体验</view>
<view class="try-area">
<textarea
class="try-input"
v-model="tryText"
placeholder="输入中文或英文,体验智能翻译..."
/>
<view class="try-actions">
<button class="try-btn primary" @click="handleTryTranslate" :disabled="tryLoading">
{{ tryLoading ? '翻译中...' : '翻译' }}
</button>
<button class="try-btn" @click="handleTryExtract" :disabled="tryLoading">
{{ tryLoading ? '提取中...' : '提取信息' }}
</button>
</view>
</view>
<view class="try-result" v-if="tryResult">
<view class="result-header">
<text class="result-label">翻译结果</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>
</view>
</view>
<view class="try-extracted" v-if="tryExtracted && Object.keys(tryExtracted).length">
<view class="result-header">
<text class="result-label">提取结果</text>
</view>
<view class="extracted-content">
<view class="extract-field" v-for="(val, key) in tryExtracted" :key="key">
<text class="extract-field-label">{{ extractFieldLabels[key] || key }}</text>
<text class="extract-field-value">{{ val || '-' }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="section" v-if="hasLogin">
<view class="section-title">待跟进客户</view>
<view class="silent-list" v-if="silentCustomers.length > 0">
<view class="silent-item" v-for="item in silentCustomers" :key="item.id">
<view class="silent-info">
<text class="silent-name">{{ item.name }}</text>
<text class="silent-country">{{ item.country }}</text>
</view>
<view class="silent-days">
<text class="days">{{ item.silence_days }}</text>
<text class="label">未联系</text>
</view>
</view>
</view>
<view class="empty" v-else>暂无待跟进客户</view>
</view>
<view class="section" v-if="hasLogin && followupStats.pending > 0">
<view class="section-title">
<text>待跟进提醒</text>
<text class="section-more" @click="goToPage(PAGES.FOLLOWUP)">查看全部 ></text>
</view>
<view class="followup-card" @click="goToPage(PAGES.FOLLOWUP)">
<text class="followup-count">{{ followupStats.pending }}</text>
<text class="followup-label">个客户需要跟进</text>
<text class="followup-hint">{{ followupStats.sent }} 已发送 · {{ followupStats.replied }} 已回复</text>
</view>
</view>
<view class="section" v-if="hasLogin">
<view class="section-title">
<text>快捷翻译</text>
<text class="section-more" @click="goToPage(PAGES.TRANSLATE)">去翻译 ></text>
</view>
<view class="translate-mini">
<textarea class="translate-mini-input" v-model="quickTranslateText" placeholder="输入文本,快速翻译..." />
<view class="translate-mini-actions">
<button class="translate-mini-btn" @click="doQuickTranslate" :disabled="!quickTranslateText.trim()">翻译</button>
<button class="translate-mini-btn extract" @click="doQuickExtract" :disabled="!quickTranslateText.trim()">提取信息</button>
</view>
<text class="translate-mini-result" v-if="quickTranslateResult">{{ quickTranslateResult }}</text>
</view>
</view>
<view class="more-section">
<view class="section-title">功能矩阵</view>
<view class="more-grid">
<view class="more-item" @click="goToPage(PAGES.TRANSLATE)">
<text class="more-icon">🔤</text>
<text class="more-text">翻译</text>
</view>
<view class="more-item" @click="goToPage(PAGES.DISCOVERY)">
<text class="more-icon">🔍</text>
<text class="more-text">挖掘新客</text>
</view>
<view class="more-item" @click="hasLogin ? goToPage(PAGES.PRODUCT) : goToLogin()">
<text class="more-icon">📦</text>
<text class="more-text">产品库</text>
</view>
<view class="more-item" @click="hasLogin ? goToPage(PAGES.FOLLOWUP) : goToLogin()">
<text class="more-icon">📋</text>
<text class="more-text">跟进</text>
<text class="notif-badge" v-if="hasLogin && followupStats.pending > 0">{{ followupStats.pending > 99 ? '99+' : followupStats.pending }}</text>
</view>
<view class="more-item" @click="hasLogin ? goToPage(PAGES.NOTIFICATION) : goToLogin()">
<text class="more-icon">🔔</text>
<text class="more-text">通知</text>
<text class="notif-badge" v-if="hasLogin && unreadCount > 0">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
</view>
<view class="more-item" @click="hasLogin ? goToPage(PAGES.ANALYTICS) : goToLogin()">
<text class="more-icon">📊</text>
<text class="more-text">分析</text>
</view>
<view class="more-item" @click="goToPage(PAGES.UPGRADE)">
<text class="more-icon">💎</text>
<text class="more-text">升级</text>
</view>
<view class="more-item" @click="goToPage(PAGES.FEEDBACK)">
<text class="more-icon">💬</text>
<text class="more-text">反馈</text>
</view>
<view class="more-item" @click="hasLogin ? goToPage(PAGES.TEAM) : goToLogin()">
<text class="more-icon">👨👩👧👦</text>
<text class="more-text">团队</text>
</view>
<view class="more-item" @click="showWechatModal = true">
<text class="more-icon">💁</text>
<text class="more-text">联系客服</text>
</view>
</view>
</view>
<view class="onboarding-overlay" v-if="showOnboarding">
<view class="onboarding-modal">
<text class="ob-title">欢迎使用外贸小助手</text>
<text class="ob-subtitle">先告诉我你的产品信息我会为你生成营销素材</text>
<view class="ob-input-group" v-if="onboardingStep === 1">
<text class="ob-label">产品名称</text>
<input class="ob-input" v-model="productName" placeholder="例如:户外折叠椅" />
</view>
<view class="ob-input-group" v-if="onboardingStep === 1">
<text class="ob-label">产品描述</text>
<textarea class="ob-input ob-textarea" v-model="productDesc" placeholder="例如:承重150kg,防水面料,带杯架和扶手" />
</view>
<view class="ob-input-group" v-if="onboardingStep === 1">
<text class="ob-label">目标市场</text>
<input class="ob-input" v-model="targetMarket" placeholder="例如:US importers" />
</view>
<view class="ob-generating" v-if="onboardingStep === 2">
<text class="ob-gen-text">正在为你生成营销素材...</text>
</view>
<view class="ob-result" v-if="onboardingStep === 3 && generatedContent.length > 0">
<text class="ob-result-title">已为你生成以下内容</text>
<view class="ob-content-item" v-for="(item, i) in generatedContent" :key="i">
<text class="ob-content-text">{{ item }}</text>
</view>
<text class="ob-result-hint">你可以去"营销素材""产品库"查看更多</text>
</view>
<view class="ob-upgrade" v-if="onboardingStep === 4">
<text class="ob-upgrade-title">🚀 升级 Pro解锁全部功能</text>
<view class="ob-compare-row"><text>翻译字符/</text><text class="free">5,000</text><text class="pro">50,000</text></view>
<view class="ob-compare-row"><text>客户管理</text><text class="free">最多5个</text><text class="pro">最多100个</text></view>
<view class="ob-compare-row"><text>产品管理</text><text class="free">最多1个</text><text class="pro">最多20个</text></view>
<view class="ob-compare-row"><text>跟进提醒</text><text class="free"></text><text class="pro"></text></view>
<view class="ob-compare-row"><text>挖掘新客</text><text class="free"></text><text class="pro"></text></view>
<text class="ob-upgrade-price"> ¥99/</text>
</view>
<view class="ob-actions">
<button class="ob-btn ob-btn-primary" @click="onboardingNext" v-if="onboardingStep === 1">
开始生成
</button>
<button class="ob-btn ob-btn-primary" @click="onboardingStep = 4" v-if="onboardingStep === 3">
开始使用
</button>
<button class="ob-btn ob-btn-primary" @click="goUpgrade" v-if="onboardingStep === 4">
升级 Pro
</button>
<button class="ob-btn ob-btn-secondary" @click="finishOnboarding" v-if="onboardingStep === 4">
暂时不用
</button>
</view>
<text class="ob-skip" @click="finishOnboarding" v-if="onboardingStep === 1">跳过以后再说</text>
</view>
</view>
<view class="modal-overlay" v-if="showWechatModal" @click="showWechatModal = false">
<view class="contact-modal" @click.stop>
<text class="contact-title">📞 联系我们</text>
<view class="contact-body">
<view class="contact-item">
<text class="contact-label">客服微信</text>
<text class="contact-value selectable" selectable>TradeMate_Support</text>
</view>
<view class="contact-item">
<text class="contact-label">用户交流群</text>
<text class="contact-value">添加客服微信后拉你入群</text>
</view>
<view class="contact-qr-placeholder">
<text class="qr-icon">📷</text>
<text class="qr-hint">客服微信二维码</text>
</view>
<text class="contact-tip">添加好友时备注"外贸小助手"</text>
</view>
<button class="announcement-btn" @click="showWechatModal = false">知道了</button>
</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 class="footer">
<view class="footer-links">
<text class="footer-link" @click="goToPage(PAGES.AGREEMENT_PRIVACY)">隐私政策</text>
<text class="footer-divider">|</text>
<text class="footer-link" @click="goToPage(PAGES.AGREEMENT_TERMS)">用户协议</text>
</view>
</view>
<AiAssistant />
</view>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi, BASE_URL } from '@/utils/api.js'
import AiAssistant from '@/components/ai-assistant.vue'
import { STORAGE_KEYS, PAGES, EXTRACT_FIELD_LABELS } from '@/config.js'
const showAnnouncement = ref(false)
const currentAnnouncement = ref(0)
const announcements = [
'全新 AI 翻译引擎上线,支持多语言商务翻译',
'登录后免费使用客户管理与报价单功能',
'每日数据看板上线,实时掌握业务动态',
]
let announcementTimer = null
const hasLogin = ref(false)
function checkLogin() {
const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
const isGuest = uni.getStorageSync(STORAGE_KEYS.IS_GUEST)
hasLogin.value = !!token && !isGuest
}
const userInfo = ref(null)
const stats = ref({
customers: 0,
silentCustomers: 0,
todayTranslations: 0,
quotations: 0,
})
const silentCustomers = ref([])
const unreadCount = ref(0)
const followupStats = ref({ pending: 0, sent: 0, replied: 0 })
const showWechatModal = ref(false)
const showOnboarding = ref(false)
const onboardingStep = ref(1)
const productName = ref('')
const productDesc = ref('')
const targetMarket = ref('US importers')
const generatedContent = ref([])
const quickTranslateText = ref('')
const quickTranslateResult = ref('')
const tryText = ref('')
const tryResult = ref('')
const tryExtracted = ref(null)
const extractFieldLabels = EXTRACT_FIELD_LABELS
const tryLoading = ref(false)
onShow(() => {
announcementTimer = setInterval(() => {
currentAnnouncement.value = (currentAnnouncement.value + 1) % announcements.length
}, 4000)
checkLogin()
if (hasLogin.value) {
uni.setStorageSync(STORAGE_KEYS.IS_GUEST, false)
loadData()
checkOnboarding()
loadUnread()
loadFollowupStats()
} else {
tryResult.value = ''
tryExtracted.value = null
tryText.value = ''
}
})
onUnmounted(() => {
if (announcementTimer) clearInterval(announcementTimer)
})
const checkOnboarding = async () => {
if (uni.getStorageSync('onboarded')) return
try {
const res = await onboardingApi.status()
if (!res.onboarded) {
showOnboarding.value = true
}
} catch (_) {
}
}
const onboardingNext = async () => {
if (!productName.value.trim()) {
uni.showToast({ title: '请输入产品名称', icon: 'none' })
return
}
onboardingStep.value = 2
try {
const res = await onboardingApi.createProduct(
productName.value.trim(),
productDesc.value.trim(),
'',
targetMarket.value.trim() || 'US importers',
)
generatedContent.value = res.generated_content || []
onboardingStep.value = 3
} catch (err) {
uni.showToast({ title: err.message || '生成失败', icon: 'none' })
onboardingStep.value = 1
}
}
const loadUnread = async () => {
try {
const res = await notificationApi.unreadCount()
unreadCount.value = res.count || 0
} catch (_) {}
}
const loadFollowupStats = async () => {
try {
const res = await followupApi.stats()
followupStats.value = res
} catch (_) {}
}
const finishOnboarding = () => {
uni.setStorageSync(STORAGE_KEYS.ONBOARDED, true)
showOnboarding.value = false
onboardingStep.value = 1
productName.value = ''
productDesc.value = ''
generatedContent.value = []
loadData()
}
const goUpgrade = () => {
finishOnboarding()
uni.navigateTo({ url: PAGES.UPGRADE })
}
const loadData = async () => {
try {
const [userRes, silentRes, overviewRes] = await Promise.all([
authApi.getUserInfo(),
customerApi.getSilent(3),
analyticsApi.getOverview(),
])
userInfo.value = userRes
silentCustomers.value = silentRes.customers || []
stats.value = {
customers: overviewRes.customers?.total || 0,
silentCustomers: overviewRes.customers?.silent_customers || 0,
todayTranslations: overviewRes.translations?.today || 0,
quotations: overviewRes.quotations?.total || 0,
}
} catch (err) {
console.error('加载数据失败', err)
}
}
const tabbarPages = [PAGES.INDEX, PAGES.CUSTOMERS, PAGES.MARKETING, PAGES.QUOTATION]
const goToPage = (url) => {
if (tabbarPages.includes(url)) {
uni.switchTab({ url })
} else {
uni.navigateTo({ url })
}
}
const goToLogin = () => {
uni.showModal({
title: '提示',
content: '请先登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: PAGES.LOGIN })
}
},
})
}
function handleLogout() {
uni.showModal({
title: '退出登录',
content: '确定退出当前账号?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
uni.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
uni.removeStorageSync(STORAGE_KEYS.HAS_LOGIN)
uni.removeStorageSync(STORAGE_KEYS.IS_GUEST)
hasLogin.value = false
uni.switchTab({ url: PAGES.INDEX })
}
},
})
}
const doQuickTranslate = async () => {
if (!quickTranslateText.value.trim()) return
try {
const chinesePattern = /[\u4e00-\u9fa5]/
const targetLang = chinesePattern.test(quickTranslateText.value) ? 'en' : 'zh'
const res = await translateApi.translate(quickTranslateText.value, targetLang, 'auto')
quickTranslateResult.value = res.translated_text || res.translated || '翻译成功'
} catch {
quickTranslateResult.value = '翻译失败'
}
}
const doQuickExtract = async () => {
if (!quickTranslateText.value.trim()) return
try {
const res = await translateApi.extract(quickTranslateText.value, 'auto')
const extracted = res.extracted || {}
const obj = typeof extracted === 'string' ? { raw: extracted } : extracted
quickTranslateResult.value = Object.entries(obj).map(([k, v]) => `${k}: ${v}`).join('\n')
} catch {
quickTranslateResult.value = '提取失败'
}
}
const handleTryTranslate = async () => {
if (!tryText.value.trim()) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
tryLoading.value = true
tryResult.value = ''
tryExtracted.value = null
try {
const chinesePattern = /[\u4e00-\u9fa5]/
const targetLang = chinesePattern.test(tryText.value) ? 'en' : 'zh'
const isGuest = uni.getStorageSync('isGuest')
const res = isGuest
? await translateApi.publicTranslate(tryText.value, targetLang, 'auto')
: await translateApi.translate(tryText.value, targetLang, 'auto')
tryResult.value = res.translated_text || res.translated || '翻译成功'
uni.showToast({ title: '翻译成功', icon: 'success' })
} catch (err) {
uni.showToast({ title: err.message || '翻译失败', icon: 'none' })
} finally {
tryLoading.value = false
}
}
const handleTryExtract = async () => {
if (!tryText.value.trim()) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
tryLoading.value = true
tryResult.value = ''
tryExtracted.value = null
try {
const isGuest = uni.getStorageSync('isGuest')
const res = isGuest
? await translateApi.publicExtract(tryText.value, 'auto')
: await translateApi.extract(tryText.value, 'auto')
const extracted = res.extracted || {}
tryExtracted.value = typeof extracted === 'string' ? { raw: extracted } : extracted
uni.showToast({ title: '提取成功', icon: 'success' })
} catch (err) {
uni.showToast({ title: err.message || '提取失败', icon: 'none' })
} finally {
tryLoading.value = false
}
}
const copyTryResult = () => {
if (!tryResult.value) return
uni.setClipboardData({
data: tryResult.value,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
},
})
}
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 text = tryResult.value
uni.showLoading({ title: '语音生成中...' })
if (typeof window !== 'undefined' && window.Audio) {
uni.request({
url: `${BASE_URL}/translate/tts?text=${encodeURIComponent(text)}&lang=${lang}`,
method: 'GET',
header: { Authorization: `Bearer ${token}` },
responseType: 'arraybuffer',
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200 && res.data) {
const blob = new Blob([res.data], { type: 'audio/mpeg' })
const url = URL.createObjectURL(blob)
const audio = new Audio(url)
audio.onended = () => { URL.revokeObjectURL(url) }
audio.play().catch(() => {
uni.showToast({ title: '播放失败,请检查音量', icon: 'none' })
})
} else {
uni.showToast({ title: '语音生成失败', icon: 'none' })
}
},
fail: () => {
uni.hideLoading()
uni.showToast({ title: '语音生成失败', icon: 'none' })
},
})
} else {
uni.downloadFile({
url: `${BASE_URL}/translate/tts?text=${encodeURIComponent(text)}&lang=${lang}`,
header: { 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>
.index-container {
min-height: 100%;
background: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
border-radius: 16rpx;
margin-bottom: 30rpx;
}
.user-info {
display: flex;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
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 {
background: rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 24rpx;
padding: 8rpx 24rpx;
border-radius: 32rpx;
border: none;
}
.username {
font-size: 32rpx;
color: #fff;
font-weight: 600;
margin-right: 16rpx;
}
.tier {
font-size: 22rpx;
color: #fff;
background: rgba(255, 255, 255, 0.2);
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.logout {
font-size: 26rpx;
color: #fff;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 30rpx;
}
.stat-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
text-align: center;
}
.stat-card.warning .stat-value {
color: #ff4d4f;
}
.stat-value {
font-size: 48rpx;
font-weight: bold;
color: #1890ff;
display: block;
}
.stat-label {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.guest-welcome {
background: #fff;
border-radius: 20rpx;
padding: 40rpx 32rpx;
margin-bottom: 30rpx;
}
.welcome-content {
text-align: center;
margin-bottom: 40rpx;
}
.welcome-title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 16rpx;
}
.welcome-desc {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 12rpx;
}
.welcome-hint {
font-size: 24rpx;
color: #1890ff;
background: #e6f7ff;
padding: 12rpx 24rpx;
border-radius: 32rpx;
display: inline-block;
}
.quick-try-section {
border-top: 2rpx solid #f5f5f5;
padding-top: 32rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-more {
font-size: 24rpx;
color: #1890ff;
}
.try-area {
background: #fafafa;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.try-input {
width: 100%;
min-height: 180rpx;
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
line-height: 1.6;
box-sizing: border-box;
}
.try-actions {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
}
.try-btn {
flex: 1;
height: 80rpx;
background: #fff;
border: 2rpx solid #d9d9d9;
color: #666;
border-radius: 12rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.try-btn.primary {
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
.try-btn[disabled] {
opacity: 0.6;
}
.try-result {
background: #f6ffed;
border-radius: 12rpx;
padding: 24rpx;
margin-top: 20rpx;
}
.try-extracted {
background: #f9f0ff;
border-radius: 12rpx;
padding: 24rpx;
margin-top: 20rpx;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.result-label {
font-size: 24rpx;
color: #52c41a;
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;
padding: 16rpx;
}
.result-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
white-space: pre-wrap;
}
.extract-field {
display: flex;
padding: 8rpx 0;
border-bottom: 1rpx solid rgba(114, 46, 209, 0.1);
}
.extract-field:last-child {
border-bottom: none;
}
.extract-field-label {
width: 160rpx;
font-size: 24rpx;
color: #722ed1;
flex-shrink: 0;
}
.extract-field-value {
flex: 1;
font-size: 24rpx;
color: #333;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.silent-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.silent-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
}
.silent-info {
display: flex;
flex-direction: column;
}
.silent-name {
font-size: 28rpx;
color: #333;
}
.silent-country {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
.silent-days {
text-align: right;
}
.silent-days .days {
font-size: 28rpx;
color: #ff4d4f;
font-weight: 600;
}
.silent-days .label {
font-size: 22rpx;
color: #999;
display: block;
}
.empty {
text-align: center;
color: #999;
padding: 40rpx;
}
.followup-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16rpx;
padding: 32rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.followup-count {
font-size: 64rpx;
font-weight: bold;
color: #fff;
}
.followup-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
margin-top: 8rpx;
}
.followup-hint {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
margin-top: 12rpx;
}
.translate-mini {
padding: 24rpx;
}
.translate-mini-input {
width: 100%;
height: 120rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
box-sizing: border-box;
resize: none;
}
.translate-mini-actions {
display: flex;
gap: 16rpx;
margin-top: 16rpx;
}
.translate-mini-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
text-align: center;
background: #1890ff;
color: #fff;
border-radius: 10rpx;
font-size: 26rpx;
}
.translate-mini-btn.extract {
background: #f5f5f5;
color: #333;
}
.translate-mini-result {
display: block;
margin-top: 16rpx;
padding: 16rpx 20rpx;
background: #f0f5ff;
border-radius: 10rpx;
font-size: 26rpx;
color: #333;
white-space: pre-wrap;
}
.more-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-top: 20rpx;
}
.more-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
margin-top: 20rpx;
}
.more-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.more-icon {
width: 72rpx;
height: 72rpx;
background: #f0f5ff;
color: #667eea;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.more-text {
font-size: 22rpx;
color: #666;
}
.notif-badge {
position: absolute; top: -8rpx; right: -8rpx;
background: #ff4d4f; color: #fff; font-size: 18rpx;
min-width: 30rpx; height: 30rpx; border-radius: 15rpx;
text-align: center; line-height: 30rpx; padding: 0 6rpx;
}
.onboarding-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;
}
.onboarding-modal {
background: #fff; border-radius: 24rpx; padding: 48rpx 40rpx;
width: 100%; max-width: 600rpx;
}
.ob-title { font-size: 36rpx; font-weight: 700; color: #333; display: block; text-align: center; }
.ob-subtitle { font-size: 26rpx; color: #999; display: block; text-align: center; margin: 16rpx 0 40rpx; }
.ob-input-group { margin-bottom: 24rpx; }
.ob-label { font-size: 26rpx; color: #666; display: block; margin-bottom: 8rpx; }
.ob-input { width: 100%; height: 80rpx; background: #f5f5f5; border-radius: 12rpx; padding: 0 20rpx; font-size: 28rpx; box-sizing: border-box; }
.ob-textarea { height: 160rpx; padding: 20rpx; }
.ob-generating { padding: 60rpx 0; text-align: center; }
.ob-gen-text { font-size: 28rpx; color: #1890ff; }
.ob-result { margin: 24rpx 0; }
.ob-result-title { font-size: 26rpx; color: #333; font-weight: 500; display: block; margin-bottom: 16rpx; }
.ob-content-item {
padding: 16rpx; background: #f9f9f9; border-radius: 12rpx; margin-bottom: 12rpx;
}
.ob-content-text { font-size: 24rpx; color: #555; line-height: 1.6; }
.ob-result-hint { font-size: 22rpx; color: #999; display: block; text-align: center; margin-top: 16rpx; }
.ob-actions { margin-top: 32rpx; }
.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-btn-secondary { background: #f5f5f5; color: #666; margin-top: 12rpx; }
.ob-skip { display: block; text-align: center; margin-top: 24rpx; font-size: 24rpx; color: #999; }
.ob-upgrade { margin: 20rpx 0; }
.ob-upgrade-title { font-size: 30rpx; font-weight: 600; color: #333; display: block; text-align: center; margin-bottom: 24rpx; }
.ob-compare-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 1px solid #f0f0f0; font-size: 26rpx; }
.ob-compare-row .free { color: #999; width: 80rpx; text-align: center; }
.ob-compare-row .pro { color: #1890ff; font-weight: 500; width: 80rpx; text-align: center; }
.ob-upgrade-price { display: block; text-align: center; margin-top: 20rpx; font-size: 28rpx; color: #ff4d4f; font-weight: 600; }
.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;
}
.contact-modal {
background: #fff;
border-radius: 20rpx;
width: 85%;
max-width: 560rpx;
max-height: 80vh;
overflow-y: auto;
padding: 40rpx;
}
.contact-title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
display: block;
margin-bottom: 30rpx;
}
.contact-body { margin-bottom: 30rpx; }
.contact-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.contact-label { font-size: 26rpx; color: #666; }
.contact-value { font-size: 28rpx; color: #333; font-weight: 500; }
.contact-qr-placeholder {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
background: #f9f9f9;
border-radius: 12rpx;
margin: 20rpx 0;
}
.qr-icon { font-size: 64rpx; margin-bottom: 16rpx; }
.qr-hint { font-size: 24rpx; color: #999; }
.qr-path { font-size: 20rpx; color: #ccc; margin-top: 8rpx; }
.contact-tip { font-size: 22rpx; color: #999; text-align: center; display: block; }
.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;
}
.footer {
padding: 30rpx 20rpx 20rpx;
background: #f8f8f8;
text-align: center;
}
.footer-qrcode {
display: flex;
justify-content: center;
gap: 24rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
}
.qrcode-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
}
.qrcode-img {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
}
.qrcode-label {
font-size: 20rpx;
color: #999;
}
.footer-links {
display: flex;
justify-content: center;
align-items: center;
gap: 16rpx;
margin-bottom: 12rpx;
}
.footer-link {
font-size: 24rpx;
color: #666;
}
.footer-divider {
font-size: 24rpx;
color: #d9d9d9;
}
.footer-links {
display: flex;
justify-content: center;
align-items: center;
gap: 16rpx;
margin-bottom: 20rpx;
}
.footer-link {
font-size: 24rpx;
color: #1890ff;
}
.footer-divider {
font-size: 24rpx;
color: #d9d9d9;
}
.footer-beian {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 12rpx;
}
.footer-beian-link {
font-size: 22rpx;
color: #999;
}
.footer-copyright {
font-size: 22rpx;
color: #bbb;
display: block;
}
/* On desktop, hide the feature matrix section (items are in the sidebar) */
@media (min-width: 1024px) {
.more-section {
display: none !important;
}
}
</style>