feat: 修复 H5 底部导航覆盖 + 更新项目进度文档

## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
This commit is contained in:
TradeMate Dev
2026-05-12 20:24:42 +08:00
parent 69e164dcae
commit 7b62c2f8b4
125 changed files with 19725 additions and 728 deletions
+274 -110
View File
@@ -1,74 +1,110 @@
<template>
<view class="login-container">
<view class="logo-section">
<view class="welcome-section">
<text class="logo">TradeMate</text>
<text class="subtitle">外贸小助手</text>
<text class="slogan">让外贸更简单 · 让沟通更高效</text>
</view>
<view class="form-section">
<text class="form-title">{{ isRegister ? '注册' : '登录' }}</text>
<view class="input-group">
<input
class="input"
type="number"
placeholder="手机号"
v-model="phone"
@input="onPhoneInput"
/>
<view class="features-section">
<view class="feature-card">
<text class="feature-icon">🌐</text>
<text class="feature-title">智能翻译</text>
<text class="feature-desc">支持中英双语商务翻译精准理解外贸术语</text>
</view>
<view class="input-group" v-if="isRegister">
<input
class="input"
type="text"
placeholder="用户名"
v-model="username"
@input="onUsernameInput"
/>
<view class="feature-card">
<text class="feature-icon">💬</text>
<text class="feature-title">智能回复</text>
<text class="feature-desc">AI 生成专业商务回复多种风格可选</text>
</view>
<view class="input-group">
<input
class="input"
type="password"
placeholder="密码"
v-model="password"
@input="onPasswordInput"
/>
<view class="feature-card">
<text class="feature-icon">📊</text>
<text class="feature-title">客户管理</text>
<text class="feature-desc">客户信息跟进提醒数据分析一体化</text>
</view>
<view class="feature-card">
<text class="feature-icon">💰</text>
<text class="feature-title">报价单生成</text>
<text class="feature-desc">快速生成专业报价单支持导出 PDF</text>
</view>
</view>
<view class="error" v-if="error">{{ error }}</view>
<button
class="submit-btn"
@click="handleSubmit"
:disabled="loading"
>
{{ loading ? '处理中...' : (isRegister ? '注册' : '登录') }}
<view class="actions-section">
<button class="try-btn" @click="goToQuickTry">
<text class="try-icon">🎯</text>
<text class="try-text">快速体验</text>
</button>
<text class="toggle-mode" @click="toggleMode">
{{ isRegister ? '已有账号立即登录' : '没有账号立即注册' }}
</text>
<view class="divider">
<view class="line"></view>
<text class="text"></text>
<text class="text">已有账号登录继续</text>
<view class="line"></view>
</view>
<button class="wechat-btn" @click="handleWechatLogin">
<text class="wechat-icon">W</text>
微信一键登录
<view class="form-section" v-if="showForm">
<view class="input-group">
<input
class="input"
type="number"
placeholder="手机号"
v-model="phone"
/>
</view>
<view class="input-group" v-if="isRegister">
<input
class="input"
type="text"
placeholder="用户名"
v-model="username"
/>
</view>
<view class="input-group">
<input
class="input"
type="password"
placeholder="密码"
v-model="password"
/>
</view>
<view class="error" v-if="error">{{ error }}</view>
<button
class="submit-btn"
@click="handleSubmit"
:disabled="loading"
>
{{ loading ? '处理中...' : (isRegister ? '注册账号' : '登录') }}
</button>
<text class="toggle-mode" @click="toggleMode">
{{ isRegister ? '已有账号立即登录' : '没有账号立即注册' }}
</text>
<view class="divider" v-if="false">
<view class="line"></view>
<text class="text"></text>
<view class="line"></view>
</view>
<button class="wechat-btn" @click="handleWechatLogin" v-if="false">
<text class="wechat-icon">W</text>
微信一键登录
</button>
</view>
<button class="show-login-btn" @click="toggleShowForm" v-if="!showForm">
登录 / 注册
</button>
</view>
<view class="footer">
<text class="agreement">登录即表示同意</text>
<text class="link">用户协议</text>
<text class="link" @click="goToAgreement('terms')">用户协议</text>
<text class="agreement"></text>
<text class="link">隐私政策</text>
<text class="link" @click="goToAgreement('privacy')">隐私政策</text>
</view>
</view>
</template>
@@ -83,12 +119,17 @@ const username = ref('')
const isRegister = ref(false)
const loading = ref(false)
const error = ref('')
const showForm = ref(false)
const toggleMode = () => {
isRegister.value = !isRegister.value
error.value = ''
}
const toggleShowForm = () => {
showForm.value = !showForm.value
}
const handleSubmit = async () => {
if (!phone.value || !password.value) {
error.value = '请输入手机号和密码'
@@ -108,15 +149,17 @@ const handleSubmit = async () => {
await authApi.register(phone.value, password.value, username.value)
uni.showToast({ title: '注册成功,请登录', icon: 'success' })
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.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
}
} 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.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
}
} catch (err) {
error.value = err.message || '操作失败,请重试'
} finally {
@@ -125,83 +168,220 @@ const handleSubmit = async () => {
}
const handleWechatLogin = () => {
uni.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
console.log('微信登录', res.userInfo)
uni.showToast({ title: '微信登录开发中', icon: 'none' })
uni.login({
provider: 'weixin',
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)
} catch (err) {
error.value = err.message || '微信登录失败'
} finally {
loading.value = false
}
},
fail: (err) => {
console.log('微信登录失败', err)
error.value = '微信登录取消或失败'
}
})
}
const onPhoneInput = (e) => { phone.value = e.detail.value }
const onPasswordInput = (e) => { password.value = e.detail.value }
const onUsernameInput = (e) => { username.value = e.detail.value }
const goToAgreement = (type) => {
uni.navigateTo({ url: `/pages/agreement/${type}` })
}
const goToQuickTry = async () => {
uni.setStorageSync('isGuest', true)
try {
const res = await authApi.guestLogin()
if (res.access_token) {
uni.setStorageSync('token', res.access_token)
if (res.user) {
uni.setStorageSync('userInfo', res.user)
}
}
} catch (e) {
console.log('Guest login failed, continuing without token:', e)
}
uni.switchTab({ url: '/pages/index/index' })
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(180deg, #1890ff 0%, #e6f7ff 100%);
padding: 120rpx 60rpx 60rpx;
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 50%);
padding: 40rpx 40rpx 60rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.logo-section {
.welcome-section {
text-align: center;
margin-bottom: 80rpx;
margin-bottom: 40rpx;
padding-top: 20rpx;
}
.logo {
font-size: 60rpx;
font-size: 56rpx;
font-weight: bold;
color: #fff;
color: #1890ff;
letter-spacing: 4rpx;
display: block;
}
.subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
color: #666;
margin-top: 8rpx;
display: block;
}
.slogan {
font-size: 24rpx;
color: #999;
margin-top: 16rpx;
display: block;
}
.features-section {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 40rpx;
}
.feature-card {
background: #fff;
border-radius: 20rpx;
padding: 32rpx 24rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(24, 144, 255, 0.08);
border: 2rpx solid #e6f7ff;
}
.feature-icon {
font-size: 48rpx;
display: block;
margin-bottom: 16rpx;
}
.feature-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.feature-desc {
font-size: 22rpx;
color: #999;
line-height: 1.5;
display: block;
}
.actions-section {
flex: 1;
display: flex;
flex-direction: column;
}
.try-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3);
margin-bottom: 24rpx;
}
.try-icon {
font-size: 36rpx;
margin-right: 12rpx;
}
.try-text {
color: #fff;
}
.divider {
display: flex;
align-items: center;
margin: 24rpx 0;
}
.divider .line {
flex: 1;
height: 1rpx;
background: #e8e8e8;
}
.divider .text {
padding: 0 24rpx;
color: #bbb;
font-size: 24rpx;
}
.form-section {
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
padding: 40rpx 32rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
}
.form-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 48rpx;
display: block;
.show-login-btn {
width: 100%;
height: 88rpx;
background: #fff;
color: #1890ff;
border: 2rpx solid #1890ff;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;
}
.input-group {
margin-bottom: 32rpx;
margin-bottom: 28rpx;
}
.input {
width: 100%;
height: 96rpx;
background: #f5f5f5;
background: #fafafa;
border-radius: 16rpx;
padding: 0 24rpx;
padding: 0 28rpx;
font-size: 28rpx;
box-sizing: border-box;
border: 2rpx solid transparent;
&:focus {
border-color: #1890ff;
background: #fff;
}
}
.error {
color: #ff4d4f;
font-size: 24rpx;
margin-bottom: 24rpx;
margin-bottom: 20rpx;
text-align: center;
display: block;
}
@@ -227,29 +407,11 @@ const onUsernameInput = (e) => { username.value = e.detail.value }
.toggle-mode {
display: block;
text-align: center;
margin-top: 32rpx;
color: #666;
margin-top: 28rpx;
color: #1890ff;
font-size: 26rpx;
}
.divider {
display: flex;
align-items: center;
margin: 48rpx 0;
}
.divider .line {
flex: 1;
height: 1rpx;
background: #e8e8e8;
}
.divider .text {
padding: 0 24rpx;
color: #999;
font-size: 24rpx;
}
.wechat-btn {
width: 100%;
height: 96rpx;
@@ -262,26 +424,28 @@ const onUsernameInput = (e) => { username.value = e.detail.value }
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.wechat-icon {
font-size: 36rpx;
font-weight: bold;
margin-right: 16rpx;
margin-right: 12rpx;
}
.footer {
text-align: center;
margin-top: 60rpx;
margin-top: 40rpx;
padding-top: 20rpx;
}
.agreement {
color: #999;
font-size: 24rpx;
font-size: 22rpx;
}
.link {
color: #1890ff;
font-size: 24rpx;
font-size: 22rpx;
}
</style>
</style>