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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user