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
+188 -1
View File
@@ -43,7 +43,9 @@
</view>
<view class="quotation-actions">
<text class="action-btn" @click.stop="copyQuotation(item)">复制</text>
<text class="action-btn" @click.stop="exportPdf(item)">PDF</text>
<text class="action-btn primary" @click.stop="sendQuotation(item)" v-if="item.status === 'draft'">发送</text>
<text class="action-btn purple" @click.stop="showSmartQuote(item)">智能报价</text>
</view>
</view>
</view>
@@ -52,6 +54,9 @@
<text>暂无报价单</text>
</view>
<view class="export-csv-btn" @click="exportCsv">
<text class="export-icon">CSV</text>
</view>
<view class="add-btn" @click="showCreateModal = true">
<text class="add-icon">+</text>
</view>
@@ -144,11 +149,39 @@
</view>
</view>
</view>
<view class="smart-quote-modal" v-if="showSmartQuoteModal" @click="showSmartQuoteModal = false">
<view class="smart-quote-content" @click.stop>
<view class="smart-quote-header">
<text class="smart-quote-title">智能报价</text>
<text class="smart-quote-close" @click="showSmartQuoteModal = false">×</text>
</view>
<view class="smart-quote-body">
<view class="form-item">
<text class="form-label">客户询盘内容</text>
<textarea class="form-textarea" v-model="inquiryText" placeholder="粘贴客户询盘内容,AI自动提取关键信息生成报价单" />
</view>
<view class="form-item">
<text class="form-label">关联客户</text>
<picker :range="customerOptions" range-key="name" @change="onSmartQuoteCustomerChange">
<view class="picker-value">{{ selectedCustomerId ? getCustomerName(selectedCustomerId) : '不关联客户' }}</view>
</picker>
</view>
</view>
<view class="smart-quote-footer">
<button class="cancel-btn" @click="showSmartQuoteModal = false">取消</button>
<button class="submit-btn" @click="generateSmartQuote" :disabled="smartQuoteLoading">
{{ smartQuoteLoading ? '生成中...' : '生成报价单' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onShow } from 'vue'
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { quotationApi, customerApi } from '@/utils/api.js'
const filter = ref('all')
@@ -156,7 +189,11 @@ const quotations = ref([])
const customers = ref([])
const showCreateModal = ref(false)
const showDetailModal = ref(false)
const showSmartQuoteModal = ref(false)
const currentQuotation = ref(null)
const inquiryText = ref('')
const selectedCustomerId = ref(null)
const smartQuoteLoading = ref(false)
const formData = ref({
title: '',
@@ -287,6 +324,79 @@ const sendQuotation = async (item) => {
uni.showToast({ title: err.message || '操作失败', icon: 'none' })
}
}
const showSmartQuote = (item) => {
inquiryText.value = item.text || ''
showSmartQuoteModal.value = true
}
const onSmartQuoteCustomerChange = (e) => {
const c = customerOptions.value[e.detail.value]
selectedCustomerId.value = c?.id || null
}
const generateSmartQuote = async () => {
if (!inquiryText.value.trim()) {
uni.showToast({ title: '请输入询盘内容', icon: 'none' })
return
}
smartQuoteLoading.value = true
try {
await quotationApi.generateFromInquiry(inquiryText.value, selectedCustomerId.value)
uni.showToast({ title: '报价单生成成功', icon: 'success' })
showSmartQuoteModal.value = false
inquiryText.value = ''
selectedCustomerId.value = null
loadQuotations()
} catch (err) {
uni.showToast({ title: err.message || '生成失败', icon: 'none' })
} finally {
smartQuoteLoading.value = false
}
}
const exportCsv = () => {
const url = quotationApi.exportCsv()
const token = uni.getStorageSync('token')
uni.downloadFile({
url,
header: { Authorization: `Bearer ${token}` },
success: (res) => {
if (res.statusCode === 200) {
uni.showToast({ title: '导出成功', icon: 'success' })
} else {
uni.showToast({ title: '导出失败', icon: 'none' })
}
},
fail: () => { uni.showToast({ title: '导出失败', icon: 'none' }) },
})
}
const exportPdf = (item) => {
const url = quotationApi.exportPdf(item.id)
uni.downloadFile({
url,
header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
uni.showToast({ title: '打开成功', icon: 'success' })
},
fail: () => {
uni.showToast({ title: 'PDF预览失败', icon: 'none' })
},
})
} else {
uni.showToast({ title: 'PDF下载失败', icon: 'none' })
}
},
fail: () => {
uni.showToast({ title: 'PDF下载失败', icon: 'none' })
},
})
}
</script>
<style lang="scss" scoped>
@@ -379,12 +489,37 @@ const sendQuotation = async (item) => {
color: #fff;
}
.action-btn.purple {
background: #722ed1;
color: #fff;
}
.empty {
text-align: center;
color: #999;
padding: 100rpx;
}
.export-csv-btn {
position: fixed;
right: 40rpx;
bottom: 160rpx;
width: 100rpx;
height: 100rpx;
background: #722ed1;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(114, 46, 209, 0.4);
}
.export-icon {
font-size: 28rpx;
color: #fff;
font-weight: 600;
}
.add-btn {
position: fixed;
right: 40rpx;
@@ -567,6 +702,58 @@ const sendQuotation = async (item) => {
color: #fff;
}
.smart-quote-modal {
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;
}
.smart-quote-content {
width: 90%;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
max-height: 80%;
}
.smart-quote-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.smart-quote-title {
font-size: 32rpx;
font-weight: 600;
}
.smart-quote-close {
font-size: 44rpx;
color: #999;
}
.smart-quote-body {
padding: 30rpx;
overflow-y: auto;
}
.smart-quote-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 2rpx solid #f5f5f5;
}
.detail-modal {
position: fixed;
top: 0;