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