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:
TradeMate Dev
2026-05-20 14:30:50 +08:00
parent f8a23855d2
commit a60aac4638
12 changed files with 689 additions and 67 deletions
+390
View File
@@ -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>