Files
trade-assistant/uni-app/src/pages/quotation/quotation.vue
T
TradeMate Dev 4755cc75ba feat: 管理后台完整可用 + 注册登录记日志 + 提取信息结构化展示 + 微信配置就绪
- 管理后台用户/统计/日志/配置四页签全部对接真实后端API
- auth注册/登录/游客/微信登录事件写入usage_logs表
- 提取信息结果从原始JSON改为卡片式字段列表(中文标签)
- 管理后台搜索按钮增加加载态和结果数提示
- 配置WECHAT_APP_ID/WECHAT_APP_SECRET
- 客户/产品/报价单CRUD页面完整(导出导入批量操作)
2026-05-18 23:50:48 +08:00

1072 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="quotation-container">
<view class="filter-tabs">
<view
class="tab-item"
:class="{ active: filter === 'all' }"
@click="filter = 'all'; loadQuotations()"
>
全部
</view>
<view
class="tab-item"
:class="{ active: filter === 'draft' }"
@click="filter = 'draft'; loadQuotations()"
>
草稿
</view>
<view
class="tab-item"
:class="{ active: filter === 'sent' }"
@click="filter = 'sent'; loadQuotations()"
>
已发送
</view>
<view
class="tab-item"
:class="{ active: filter === 'accepted' }"
@click="filter = 'accepted'; loadQuotations()"
>
已成交
</view>
</view>
<view class="quotation-list" v-if="quotations.length > 0">
<view class="quotation-item" v-for="item in quotations" :key="item.id" @click="showDetail(item)">
<view class="quotation-header">
<text class="quotation-title">{{ item.title || '报价单' }}</text>
<text class="quotation-status" :class="item.status">{{ getStatusText(item.status) }}</text>
</view>
<view class="quotation-info">
<text class="info-item">{{ item.currency }} {{ item.total || 0 }}</text>
<text class="info-item">{{ formatTime(item.created_at) }}</text>
</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>
<view class="empty" v-else>
<text>暂无报价单</text>
</view>
<view class="bottom-actions">
<view class="action-btn export-btn" @click="showExportSheet = true">
<text class="btn-icon"></text>
<text class="btn-text">导出</text>
</view>
<view class="action-btn import-btn" @click="showImportSheet = true">
<text class="btn-icon"></text>
<text class="btn-text">导入</text>
</view>
<view class="action-btn add-btn" @click="showCreateModal = true">
<text class="btn-icon">+</text>
<text class="btn-text">新增</text>
</view>
</view>
<view class="action-sheet-overlay" v-if="showExportSheet" @click="showExportSheet = false">
<view class="action-sheet" @click.stop>
<view class="action-sheet-header">导出报价单</view>
<view class="action-sheet-item" @click="exportAsXlsx">
<text class="action-sheet-icon">📊</text>
<text class="action-sheet-text">导出为 Excel (.xlsx)</text>
<text class="action-sheet-arrow"></text>
</view>
<view class="action-sheet-item" @click="exportAsCsv">
<text class="action-sheet-icon">📄</text>
<text class="action-sheet-text">导出为 CSV (.csv)</text>
<text class="action-sheet-arrow"></text>
</view>
<view class="action-sheet-item" @click="exportToClipboard">
<text class="action-sheet-icon">📋</text>
<text class="action-sheet-text">复制到剪贴板分享到微信/WhatsApp</text>
<text class="action-sheet-arrow"></text>
</view>
<view class="action-sheet-cancel" @click="showExportSheet = false">取消</view>
</view>
</view>
<view class="action-sheet-overlay" v-if="showImportSheet" @click="showImportSheet = false">
<view class="action-sheet" @click.stop>
<view class="action-sheet-header">导入报价单</view>
<view class="action-sheet-item" @click="importFromFile">
<text class="action-sheet-icon">📁</text>
<text class="action-sheet-text">从文件导入支持 .xlsx / .csv</text>
<text class="action-sheet-arrow"></text>
</view>
<view class="action-sheet-item" @click="showPasteImport = true; showImportSheet = false">
<text class="action-sheet-icon">📋</text>
<text class="action-sheet-text">从剪贴板导入粘贴报价数据</text>
<text class="action-sheet-arrow"></text>
</view>
<view class="action-sheet-cancel" @click="showImportSheet = false">取消</view>
</view>
</view>
<view class="modal" v-if="showPasteImport" @click="showPasteImport = false">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">从剪贴板导入</text>
<text class="modal-close" @click="showPasteImport = false">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">粘贴报价数据</text>
<textarea class="form-textarea" v-model="pasteData" placeholder="每行一个报价单,用逗号或Tab分隔&#10;格式: 标题,客户ID,货币,金额,状态" rows="6" />
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="showPasteImport = false">取消</button>
<button class="submit-btn" @click="importFromPaste">导入</button>
</view>
</view>
</view>
<view class="modal" v-if="showCreateModal" @click="closeModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">创建报价单</text>
<text class="modal-close" @click="closeModal">×</text>
</view>
<scroll-view class="modal-body" scroll-y>
<view class="form-item">
<text class="form-label">报价标题 *</text>
<input class="form-input" v-model="formData.title" placeholder="报价单标题" />
</view>
<view class="form-item">
<text class="form-label">客户</text>
<picker :range="customerOptions" range-key="name" @change="onCustomerChange">
<view class="picker-value">{{ formData.customer_id ? getCustomerName(formData.customer_id) : '选择客户' }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">货币</text>
<picker :range="['USD', 'EUR', 'GBP', 'CNY']" @change="onCurrencyChange">
<view class="picker-value">{{ formData.currency }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">付款条款</text>
<input class="form-input" v-model="formData.payment_terms" placeholder="T/T 30% deposit" />
</view>
<view class="form-item">
<text class="form-label">交货条款</text>
<input class="form-input" v-model="formData.delivery_terms" placeholder="FOB Shanghai" />
</view>
<view class="form-item">
<text class="form-label">交货周期</text>
<input class="form-input" v-model="formData.lead_time" placeholder="25 days" />
</view>
<view class="form-item">
<text class="form-label">有效期</text>
<input class="form-input" v-model="formData.valid_until" placeholder="2026-06-08" />
</view>
<view class="items-section">
<view class="items-header">
<text class="items-title">产品明细</text>
<text class="add-item-btn" @click="addItem">+ 添加产品</text>
</view>
<view class="item-row" v-for="(item, idx) in formData.items" :key="idx">
<input class="item-input name" v-model="item.product_name" placeholder="产品名称" />
<input class="item-input qty" v-model.number="item.quantity" type="number" placeholder="数量" />
<input class="item-input price" v-model.number="item.unit_price" type="digit" placeholder="单价" />
<text class="item-total">{{ (item.quantity || 0) * (item.unit_price || 0) }}</text>
<text class="item-delete" @click="removeItem(idx)">×</text>
</view>
</view>
<view class="form-item">
<text class="form-label">折扣</text>
<input class="form-input" v-model.number="formData.discount" type="digit" placeholder="0" />
</view>
<view class="form-item">
<text class="form-label">运费</text>
<input class="form-input" v-model.number="formData.shipping" type="digit" placeholder="0" />
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea class="form-textarea" v-model="formData.notes" placeholder="备注信息" />
</view>
</scroll-view>
<view class="modal-footer">
<button class="cancel-btn" @click="closeModal">取消</button>
<button class="submit-btn" @click="createQuotation">创建报价单</button>
</view>
</view>
</view>
<view class="detail-modal" v-if="showDetailModal" @click="showDetailModal = false">
<view class="detail-content" @click.stop>
<view class="detail-header">
<text class="detail-title">{{ currentQuotation.title }}</text>
<text class="detail-status" :class="currentQuotation.status">{{ getStatusText(currentQuotation.status) }}</text>
</view>
<scroll-view class="detail-body" scroll-y>
<view class="detail-text">{{ currentQuotation.text }}</view>
</scroll-view>
<view class="detail-footer">
<button class="close-btn" @click="showDetailModal = false">关闭</button>
</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 } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { quotationApi, customerApi } from '@/utils/api.js'
const filter = ref('all')
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: '',
customer_id: '',
currency: 'USD',
payment_terms: 'T/T 30% deposit',
delivery_terms: 'FOB Shanghai',
lead_time: '',
valid_until: '',
items: [{ product_name: '', quantity: 1, unit_price: 0 }],
discount: 0,
shipping: 0,
notes: '',
})
const customerOptions = ref([])
const showExportSheet = ref(false)
const showImportSheet = ref(false)
const showPasteImport = ref(false)
const pasteData = ref('')
onShow(() => {
loadQuotations()
loadCustomers()
})
const loadQuotations = async () => {
try {
const res = await quotationApi.list()
quotations.value = res.items || []
} catch (err) {
console.error('加载报价单失败', err)
}
}
const loadCustomers = async () => {
try {
const res = await customerApi.list(1, 100)
customers.value = res.items || []
customerOptions.value = customers.value
} catch (err) {
console.error('加载客户失败', err)
}
}
const getStatusText = (status) => {
const map = { draft: '草稿', sent: '已发送', accepted: '已成交', rejected: '已拒绝', expired: '已过期' }
return map[status] || status
}
const formatTime = (time) => {
if (!time) return ''
return time.slice(0, 16).replace('T', ' ')
}
const getCustomerName = (id) => {
const c = customers.value.find(item => item.id === id)
return c?.name || ''
}
const onCustomerChange = (e) => {
const c = customerOptions.value[e.detail.value]
formData.value.customer_id = c?.id || ''
}
const onCurrencyChange = (e) => {
const currencies = ['USD', 'EUR', 'GBP', 'CNY']
formData.value.currency = currencies[e.detail.value]
}
const addItem = () => {
formData.value.items.push({ product_name: '', quantity: 1, unit_price: 0 })
}
const removeItem = (idx) => {
formData.value.items.splice(idx, 1)
}
const closeModal = () => {
showCreateModal.value = false
formData.value = {
title: '',
customer_id: '',
currency: 'USD',
payment_terms: 'T/T 30% deposit',
delivery_terms: 'FOB Shanghai',
lead_time: '',
valid_until: '',
items: [{ product_name: '', quantity: 1, unit_price: 0 }],
discount: 0,
shipping: 0,
notes: '',
}
}
const createQuotation = async () => {
if (!formData.value.title) {
uni.showToast({ title: '请填写标题', icon: 'none' })
return
}
try {
await quotationApi.create(formData.value)
uni.showToast({ title: '创建成功', icon: 'success' })
closeModal()
loadQuotations()
} catch (err) {
uni.showToast({ title: err.message || '创建失败', icon: 'none' })
}
}
const showDetail = (item) => {
currentQuotation.value = item
showDetailModal.value = true
}
const copyQuotation = (item) => {
uni.setClipboardData({
data: item.text,
success: () => {
uni.showToast({ title: '已复制报价单', icon: 'success' })
},
})
}
const sendQuotation = async (item) => {
try {
await quotationApi.updateStatus(item.id, 'sent')
uni.showToast({ title: '已标记为已发送', icon: 'success' })
loadQuotations()
} catch (err) {
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 exportAsXlsx = () => {
showExportSheet.value = false
const url = quotationApi.exportXlsx()
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 exportAsCsv = () => {
showExportSheet.value = false
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 exportToClipboard = async () => {
showExportSheet.value = false
try {
const res = await quotationApi.list(1, 9999)
const items = res.items || []
if (items.length === 0) {
uni.showToast({ title: '暂无报价单可导出', icon: 'none' })
return
}
const headers = ['标题', '客户', '货币', '总计', '状态', '日期']
const rows = items.map(q => [
q.title || '', q.customer_name || '', q.currency || 'USD',
q.total || 0, q.status || '', q.created_at ? q.created_at.slice(0, 16).replace('T', ' ') : '',
])
const text = [headers.join('\t'), ...rows.map(r => r.join('\t'))].join('\n')
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: `已复制 ${items.length} 条报价数据`, icon: 'success' })
},
fail: () => { uni.showToast({ title: '复制失败', icon: 'none' }) },
})
} catch (err) {
uni.showToast({ title: err.message || '导出失败', icon: 'none' })
}
}
const importFromFile = () => {
showImportSheet.value = false
uni.chooseImage({
count: 1,
success: async (res) => {
const file = res.tempFilePaths[0]
uni.showLoading({ title: '导入中...' })
try {
const result = await quotationApi.importQuotations(file)
uni.hideLoading()
uni.showModal({
title: '导入完成',
content: `成功导入 ${result.imported || 0}\n失败 ${(result.errors || []).length}`,
success: () => loadQuotations(),
})
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.message || '导入失败', icon: 'none' })
}
},
})
}
const importFromPaste = async () => {
if (!pasteData.value.trim()) {
uni.showToast({ title: '请粘贴报价数据', icon: 'none' })
return
}
showPasteImport.value = false
uni.showLoading({ title: '导入中...' })
try {
const lines = pasteData.value.trim().split('\n')
const imported = []
const errors = []
for (const line of lines) {
const parts = line.split(/[\t,]/).map(s => s.trim()).filter(Boolean)
if (parts.length < 1) continue
const data = {
title: parts[0] || '',
customer_id: parts[1] || '',
currency: parts[2] && ['USD', 'EUR', 'GBP', 'CNY'].includes(parts[2]) ? parts[2] : 'USD',
items: [{ product_name: 'Imported Item', quantity: 1, unit_price: parseFloat(parts[3]) || 0 }],
status: parts[4] && ['draft', 'sent', 'accepted', 'rejected'].includes(parts[4]) ? parts[4] : 'draft',
}
try {
await quotationApi.create(data)
imported.push(data.title)
} catch (e) {
errors.push(`${data.title}: ${e.message || '创建失败'}`)
}
}
uni.hideLoading()
pasteData.value = ''
uni.showModal({
title: '导入完成',
content: `成功导入 ${imported.length}\n失败 ${errors.length}`,
success: () => loadQuotations(),
})
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.message || '导入失败', 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>
.quotation-container {
min-height: 100vh;
background: #f5f5f5;
padding: 20rpx;
}
.filter-tabs {
display: flex;
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx;
font-size: 26rpx;
color: #666;
}
.tab-item.active {
color: #1890ff;
background: #e6f7ff;
}
.quotation-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.quotation-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
}
.quotation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.quotation-title {
font-size: 30rpx;
font-weight: 600;
}
.quotation-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.quotation-status.draft { background: #f5f5f5; color: #666; }
.quotation-status.sent { background: #e6f7ff; color: #1890ff; }
.quotation-status.accepted { background: #f6ffed; color: #52c41a; }
.quotation-info {
display: flex;
gap: 20rpx;
margin-bottom: 12rpx;
}
.info-item {
font-size: 24rpx;
color: #666;
}
.quotation-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 6rpx;
background: #f5f5f5;
color: #666;
}
.action-btn.primary {
background: #1890ff;
color: #fff;
}
.action-btn.purple {
background: #722ed1;
color: #fff;
}
.empty {
text-align: center;
color: #999;
padding: 100rpx;
}
.bottom-actions {
position: fixed;
right: 40rpx;
bottom: 100px;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.action-btn {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
}
.add-btn {
background: #1890ff;
}
.import-btn {
background: #52c41a;
}
.export-btn {
background: #722ed1;
}
.btn-icon {
font-size: 32rpx;
color: #fff;
line-height: 1;
}
.btn-text {
font-size: 18rpx;
color: #fff;
margin-top: 2rpx;
}
.action-sheet-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: flex-end;
z-index: 999;
}
.action-sheet {
width: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 0 0 60rpx;
}
.action-sheet-header {
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #333;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.action-sheet-item {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.action-sheet-icon {
font-size: 36rpx;
margin-right: 20rpx;
}
.action-sheet-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.action-sheet-arrow {
font-size: 32rpx;
color: #ccc;
}
.action-sheet-cancel {
text-align: center;
font-size: 28rpx;
color: #999;
padding: 24rpx;
margin-top: 12rpx;
}
.add-icon {
font-size: 60rpx;
color: #fff;
}
.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;
}
.modal-content {
width: 90%;
height: 80%;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
}
.modal-close {
font-size: 44rpx;
color: #999;
}
.modal-body {
flex: 1;
padding: 30rpx;
overflow-y: auto;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.form-input {
width: 100%;
height: 72rpx;
background: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.form-textarea {
width: 100%;
min-height: 120rpx;
background: #f5f5f5;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
}
.picker-value {
height: 72rpx;
background: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
font-size: 28rpx;
}
.items-section {
margin: 20rpx 0;
}
.items-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.items-title {
font-size: 28rpx;
font-weight: 600;
}
.add-item-btn {
font-size: 26rpx;
color: #1890ff;
}
.item-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.item-input {
height: 60rpx;
background: #f5f5f5;
border-radius: 8rpx;
padding: 0 12rpx;
font-size: 24rpx;
}
.item-input.name { flex: 2; }
.item-input.qty { width: 100rpx; }
.item-input.price { width: 120rpx; }
.item-total {
font-size: 24rpx;
color: #1890ff;
min-width: 80rpx;
}
.item-delete {
font-size: 36rpx;
color: #ff4d4f;
}
.modal-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 2rpx solid #f5f5f5;
}
.cancel-btn, .submit-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.submit-btn {
background: #1890ff;
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;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.detail-content {
width: 90%;
height: 70%;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.detail-title {
font-size: 32rpx;
font-weight: 600;
}
.detail-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.detail-body {
flex: 1;
padding: 30rpx;
}
.detail-text {
font-size: 24rpx;
line-height: 1.8;
white-space: pre-wrap;
}
.detail-footer {
padding: 30rpx;
border-top: 2rpx solid #f5f5f5;
}
.close-btn {
width: 100%;
height: 80rpx;
background: #1890ff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
}
</style>