4755cc75ba
- 管理后台用户/统计/日志/配置四页签全部对接真实后端API - auth注册/登录/游客/微信登录事件写入usage_logs表 - 提取信息结果从原始JSON改为卡片式字段列表(中文标签) - 管理后台搜索按钮增加加载态和结果数提示 - 配置WECHAT_APP_ID/WECHAT_APP_SECRET - 客户/产品/报价单CRUD页面完整(导出导入批量操作)
1072 lines
27 KiB
Vue
1072 lines
27 KiB
Vue
<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分隔 格式: 标题,客户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> |