Unify frontend config, fix marketing tracking field mismatch, expose customer notes in API
Centralizes all hardcoded page paths, storage keys, external URLs, and branding into a single uni-app/src/config.js. Fixes trackMarketingEffect sending wrong field names (action/content_preview -> event_type/content) that silently dropped tracking data. Adds notes, estimated_value, next_followup_at to Customer response. Removes '翻译' from bottom tab nav (5 tabs now), adds quick translate card on home page. Makes profile page header color consistent with app theme (#1890ff).
This commit is contained in:
@@ -105,49 +105,68 @@
|
||||
<view class="section" v-if="hasLogin && followupStats.pending > 0">
|
||||
<view class="section-title">
|
||||
<text>待跟进提醒</text>
|
||||
<text class="section-more" @click="goToPage('/pages/followup/followup')">查看全部 ></text>
|
||||
<text class="section-more" @click="goToPage(PAGES.FOLLOWUP)">查看全部 ></text>
|
||||
</view>
|
||||
<view class="followup-card" @click="goToPage('/pages/followup/followup')">
|
||||
<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="hasLogin ? goToPage('/pages/product/product') : goToLogin()">
|
||||
<view class="more-item" @click="goToPage(PAGES.TRANSLATE)">
|
||||
<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/followup') : goToLogin()">
|
||||
<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/notification') : goToLogin()">
|
||||
<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/analytics') : goToLogin()">
|
||||
<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/upgrade')">
|
||||
<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/feedback')">
|
||||
<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/team') : goToLogin()">
|
||||
<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" v-if="isAdmin" @click="goToPage('/pages/admin/admin')">
|
||||
<view class="more-item" v-if="isAdmin" @click="goToPage(PAGES.ADMIN)">
|
||||
<text class="more-icon">⚙️</text>
|
||||
<text class="more-text">管理</text>
|
||||
</view>
|
||||
@@ -243,15 +262,15 @@
|
||||
|
||||
<view class="footer">
|
||||
<view class="footer-links">
|
||||
<text class="footer-link" @click="goToPage('/pages/agreement/privacy')">隐私政策</text>
|
||||
<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>
|
||||
<text class="footer-link" @click="goToPage(PAGES.AGREEMENT_TERMS)">用户协议</text>
|
||||
</view>
|
||||
<view class="footer-beian">
|
||||
<a class="footer-beian-link" href="https://beian.miit.gov.cn" target="_blank">京ICP备2026007249号-1</a>
|
||||
<a class="footer-beian-link" href="https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545" target="_blank">京公网安备11011502039545号</a>
|
||||
<a class="footer-beian-link" :href="EXTERNAL_URLS.BEIAN" target="_blank">{{ APP_INFO.ICP }}</a>
|
||||
<a class="footer-beian-link" :href="EXTERNAL_URLS.BEIAN_PSB" target="_blank">{{ APP_INFO.PSB }}</a>
|
||||
</view>
|
||||
<text class="footer-copyright">© 2026 北京宇之然科技中心. 保留所有权利.</text>
|
||||
<text class="footer-copyright">© {{ APP_INFO.COPYRIGHT }}. 保留所有权利.</text>
|
||||
</view>
|
||||
<AiAssistant />
|
||||
</view>
|
||||
@@ -262,6 +281,7 @@ import { ref, computed, 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, EXTERNAL_URLS, APP_INFO, EXTRACT_FIELD_LABELS } from '@/config.js'
|
||||
|
||||
const showAnnouncement = ref(false)
|
||||
const currentAnnouncement = ref(0)
|
||||
@@ -298,15 +318,12 @@ 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 = {
|
||||
product_name: '产品名称', quantity: '数量', price: '价格',
|
||||
currency: '货币', delivery_terms: '交货条款', target_country: '目标国家',
|
||||
intent: '意图', product_interest: '感兴趣产品', budget: '预算',
|
||||
urgency: '紧迫程度', contact_info: '联系方式',
|
||||
}
|
||||
const extractFieldLabels = EXTRACT_FIELD_LABELS
|
||||
const tryLoading = ref(false)
|
||||
|
||||
onShow(() => {
|
||||
@@ -317,7 +334,7 @@ onShow(() => {
|
||||
const token = uni.getStorageSync('token')
|
||||
const isGuest = uni.getStorageSync('isGuest')
|
||||
if (token && !isGuest) {
|
||||
uni.setStorageSync('isGuest', false)
|
||||
uni.setStorageSync(STORAGE_KEYS.IS_GUEST, false)
|
||||
loadData()
|
||||
checkOnboarding()
|
||||
loadUnread()
|
||||
@@ -380,7 +397,7 @@ const loadFollowupStats = async () => {
|
||||
}
|
||||
|
||||
const finishOnboarding = () => {
|
||||
uni.setStorageSync('onboarded', true)
|
||||
uni.setStorageSync(STORAGE_KEYS.ONBOARDED, true)
|
||||
showOnboarding.value = false
|
||||
onboardingStep.value = 1
|
||||
productName.value = ''
|
||||
@@ -410,7 +427,7 @@ const loadData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const tabbarPages = ['/pages/index/index', '/pages/translate/translate', '/pages/customers/customers', '/pages/marketing/marketing', '/pages/quotation/quotation']
|
||||
const tabbarPages = [PAGES.INDEX, PAGES.TRANSLATE, PAGES.CUSTOMERS, PAGES.MARKETING, PAGES.QUOTATION]
|
||||
|
||||
const goToPage = (url) => {
|
||||
if (tabbarPages.includes(url)) {
|
||||
@@ -421,22 +438,41 @@ const goToPage = (url) => {
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.clearStorageSync()
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.reLaunch({ url: PAGES.LOGIN })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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' })
|
||||
@@ -941,6 +977,49 @@ const playTryResult = () => {
|
||||
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;
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { authApi } from '@/utils/api.js'
|
||||
import { STORAGE_KEYS, PAGES } from '@/config.js'
|
||||
|
||||
const phone = ref('')
|
||||
const password = ref('')
|
||||
@@ -129,11 +130,11 @@ const isWechatAvailable = ref(false)
|
||||
|
||||
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' })
|
||||
uni.setStorageSync(STORAGE_KEYS.TOKEN, res.access_token)
|
||||
uni.setStorageSync(STORAGE_KEYS.USER_INFO, res.user)
|
||||
uni.setStorageSync(STORAGE_KEYS.HAS_LOGIN, true)
|
||||
uni.setStorageSync(STORAGE_KEYS.IS_GUEST, false)
|
||||
uni.switchTab({ url: PAGES.INDEX })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -220,11 +221,11 @@ const handleSubmit = async () => {
|
||||
isRegister.value = false
|
||||
} else {
|
||||
const res = await authApi.login(phone.value, password.value)
|
||||
uni.setStorageSync('token', res.access_token)
|
||||
uni.setStorageSync('userInfo', res.user)
|
||||
uni.setStorageSync('hasLogin', true)
|
||||
uni.setStorageSync('isGuest', false)
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
uni.setStorageSync(STORAGE_KEYS.TOKEN, res.access_token)
|
||||
uni.setStorageSync(STORAGE_KEYS.USER_INFO, res.user)
|
||||
uni.setStorageSync(STORAGE_KEYS.HAS_LOGIN, true)
|
||||
uni.setStorageSync(STORAGE_KEYS.IS_GUEST, false)
|
||||
uni.reLaunch({ url: PAGES.INDEX })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('登录失败', err)
|
||||
@@ -258,23 +259,23 @@ const handleWechatLogin = () => {
|
||||
}
|
||||
|
||||
const goToAgreement = (type) => {
|
||||
uni.navigateTo({ url: `/pages/agreement/${type}` })
|
||||
uni.navigateTo({ url: type === 'privacy' ? PAGES.AGREEMENT_PRIVACY : PAGES.AGREEMENT_TERMS })
|
||||
}
|
||||
|
||||
const goToQuickTry = async () => {
|
||||
uni.setStorageSync('isGuest', true)
|
||||
uni.setStorageSync(STORAGE_KEYS.IS_GUEST, true)
|
||||
try {
|
||||
const res = await authApi.guestLogin()
|
||||
if (res.access_token) {
|
||||
uni.setStorageSync('token', res.access_token)
|
||||
uni.setStorageSync(STORAGE_KEYS.TOKEN, res.access_token)
|
||||
if (res.user) {
|
||||
uni.setStorageSync('userInfo', res.user)
|
||||
uni.setStorageSync(STORAGE_KEYS.USER_INFO, res.user)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Guest login failed, continuing without token:', e)
|
||||
}
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
uni.switchTab({ url: PAGES.INDEX })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ const copyText = (text) => {
|
||||
}
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
||||
interactionApi.trackMarketingEffect({ event_type: 'copy', content: text.slice(0, 200) }).catch(() => {})
|
||||
loadStats()
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
}).catch(() => fallbackCopy(text))
|
||||
@@ -351,7 +351,7 @@ const fallbackCopy = (text) => {
|
||||
ta.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
||||
interactionApi.trackMarketingEffect({ event_type: 'copy', content: text.slice(0, 200) }).catch(() => {})
|
||||
loadStats()
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
} catch {
|
||||
@@ -381,7 +381,7 @@ const exportCsv = () => {
|
||||
}
|
||||
|
||||
const sendToWhatsapp = (text) => {
|
||||
interactionApi.trackMarketingEffect({ action: 'send', content_preview: text.slice(0, 100) }).catch(() => {})
|
||||
interactionApi.trackMarketingEffect({ event_type: 'send', content: text.slice(0, 200) }).catch(() => {})
|
||||
loadStats()
|
||||
uni.showToast({ title: '请先选择客户', icon: 'none' })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="user-card">
|
||||
<view class="avatar">{{ initials }}</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ user.username || '未设置' }}</text>
|
||||
<text class="user-phone">{{ user.phone || '未绑定手机' }}</text>
|
||||
<view class="tier-badge" :class="user.tier">{{ tierLabel }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">账号设置</view>
|
||||
<view class="menu-item" v-if="user.tier !== 'guest'" @click="showProfileEdit = true">
|
||||
<text class="menu-icon">📝</text>
|
||||
<text class="menu-text">编辑资料</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" v-if="user.tier !== 'guest'" @click="showPassword = true">
|
||||
<text class="menu-icon">🔒</text>
|
||||
<text class="menu-text">修改密码</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goUpgrade">
|
||||
<text class="menu-icon">⭐</text>
|
||||
<text class="menu-text">会员升级</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">其他</view>
|
||||
<view class="menu-item" @click="goFeedback">
|
||||
<text class="menu-icon">💬</text>
|
||||
<text class="menu-text">意见反馈</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goAgreement('privacy')">
|
||||
<text class="menu-icon">📄</text>
|
||||
<text class="menu-text">隐私政策</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goAgreement('terms')">
|
||||
<text class="menu-icon">📋</text>
|
||||
<text class="menu-text">用户协议</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item">
|
||||
<text class="menu-icon">ℹ️</text>
|
||||
<text class="menu-text">版本</text>
|
||||
<text class="menu-value">1.0.0</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" v-if="user.tier !== 'guest'" @click="logout">退出登录</view>
|
||||
<view class="login-btn-bottom" v-else @click="goLogin">登录 / 注册</view>
|
||||
|
||||
<!-- 编辑资料弹窗 -->
|
||||
<view class="modal-overlay" v-if="showProfileEdit" @click="showProfileEdit = false">
|
||||
<view class="popup-card" @click.stop>
|
||||
<text class="popup-title">编辑资料</text>
|
||||
<view class="form-field">
|
||||
<text class="form-label">用户名</text>
|
||||
<input class="form-input" v-model="editForm.username" placeholder="输入用户名" />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="form-label">邮箱</text>
|
||||
<input class="form-input" v-model="editForm.email" placeholder="输入邮箱" type="email" />
|
||||
</view>
|
||||
<view class="popup-btns">
|
||||
<text class="popup-btn cancel" @click="showProfileEdit = false">取消</text>
|
||||
<text class="popup-btn confirm" @click="saveProfile">保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<view class="modal-overlay" v-if="showPassword" @click="showPassword = false">
|
||||
<view class="popup-card" @click.stop>
|
||||
<text class="popup-title">修改密码</text>
|
||||
<view class="form-field">
|
||||
<text class="form-label">旧密码</text>
|
||||
<input class="form-input" v-model="pwdForm.old" placeholder="输入旧密码" password />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="form-label">新密码</text>
|
||||
<input class="form-input" v-model="pwdForm.new1" placeholder="输入新密码" password />
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="form-label">确认新密码</text>
|
||||
<input class="form-input" v-model="pwdForm.new2" placeholder="再次输入新密码" password />
|
||||
</view>
|
||||
<view class="popup-btns">
|
||||
<text class="popup-btn cancel" @click="showPassword = false">取消</text>
|
||||
<text class="popup-btn confirm" @click="changePwd">确认修改</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiAssistant />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { authApi } from '@/utils/api.js'
|
||||
import AiAssistant from '@/components/ai-assistant.vue'
|
||||
import { STORAGE_KEYS, PAGES, TIER_LABELS } from '@/config.js'
|
||||
|
||||
const user = ref({})
|
||||
const showProfileEdit = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const editForm = ref({ username: '', email: '' })
|
||||
const pwdForm = ref({ old: '', new1: '', new2: '' })
|
||||
|
||||
const initials = computed(() => {
|
||||
const name = user.value.username || user.value.phone || 'U'
|
||||
return name.charAt(0).toUpperCase()
|
||||
})
|
||||
|
||||
const tierLabel = computed(() => TIER_LABELS[user.value.tier] || '免费版')
|
||||
|
||||
const loadUser = async () => {
|
||||
try {
|
||||
const res = await authApi.getUserInfo()
|
||||
user.value = res
|
||||
editForm.value = { username: res.username || '', email: res.email || '' }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const saveProfile = async () => {
|
||||
try {
|
||||
await authApi.updateProfile(editForm.value)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
showProfileEdit.value = false
|
||||
loadUser()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const changePwd = async () => {
|
||||
if (!pwdForm.value.old || !pwdForm.value.new1) {
|
||||
uni.showToast({ title: '请填写完整', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (pwdForm.value.new1 !== pwdForm.value.new2) {
|
||||
uni.showToast({ title: '两次新密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (pwdForm.value.new1.length < 6) {
|
||||
uni.showToast({ title: '密码长度不少于6位', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await authApi.changePassword(pwdForm.value.old, pwdForm.value.new1)
|
||||
uni.showToast({ title: '密码修改成功', icon: 'success' })
|
||||
showPassword.value = false
|
||||
pwdForm.value = { old: '', new1: '', new2: '' }
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '修改失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const goLogin = () => uni.reLaunch({ url: PAGES.LOGIN })
|
||||
const goUpgrade = () => uni.navigateTo({ url: PAGES.UPGRADE })
|
||||
const goFeedback = () => uni.navigateTo({ url: PAGES.FEEDBACK })
|
||||
const goAgreement = (type) => uni.navigateTo({ url: `/pages/agreement/${type}` })
|
||||
|
||||
const logout = () => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定退出登录?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
uni.reLaunch({ url: PAGES.LOGIN })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(loadUser)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 20rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #1890ff, #096dd9);
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.8);
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.tier-badge {
|
||||
display: inline-block;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.tier-badge.free { background: #e8f5e9; color: #2e7d32; }
|
||||
.tier-badge.pro { background: #fff3e0; color: #e65100; }
|
||||
.tier-badge.enterprise { background: #e3f2fd; color: #1565c0; }
|
||||
.tier-badge.guest { background: #fce4ec; color: #c62828; }
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 24rpx 30rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
|
||||
.menu-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 36rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.menu-value {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin: 40rpx 0;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.login-btn-bottom {
|
||||
margin: 40rpx 0;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #1890ff, #096dd9);
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4rpx 12rpx rgba(24,144,255,0.3);
|
||||
}
|
||||
|
||||
.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: 99999;
|
||||
}
|
||||
|
||||
.popup-card {
|
||||
width: 560rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
height: 72rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.popup-btns {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-btn.cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.popup-btn.confirm {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user