640 lines
32 KiB
JavaScript
640 lines
32 KiB
JavaScript
const fs = require('fs');
|
|
const fixedContent = `<template>
|
|
<div class="page-container quote-generate">
|
|
<div :class="['toast', toastType, { show: showToast }]">
|
|
{{ toastMessage }}
|
|
</div>
|
|
|
|
<div class="page-header">
|
|
<div class="page-header-left">
|
|
<h1 class="page-title">
|
|
<i class="fas fa-file-invoice-dollar"></i> 报价表管理
|
|
</h1>
|
|
<p class="text-secondary">
|
|
<i class="fas fa-info-circle"></i> 管理所有报价单,支持创建、编辑、发送
|
|
</p>
|
|
</div>
|
|
<div class="page-header-right">
|
|
<button class="btn btn-primary" @click="openCreateModal">
|
|
<i class="fas fa-plus"></i> 新建报价表
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-tabs">
|
|
<div class="nav-tab" @click="goToQuoteMeasure">测量报价</div>
|
|
<div :class="['nav-tab', { active: true }]">报价表生成</div>
|
|
</div>
|
|
|
|
<div class="search-bar">
|
|
<div class="search-input-group">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" v-model="searchKeyword" placeholder="搜索报价单号、客户名称..." @input="handleSearch">
|
|
</div>
|
|
<div class="filter-group">
|
|
<select v-model="filterStatus" @change="handleSearch">
|
|
<option value="">全部状态</option>
|
|
<option value="draft">草稿</option>
|
|
<option value="sent">已发送</option>
|
|
<option value="accepted">已接受</option>
|
|
<option value="rejected">已拒绝</option>
|
|
<option value="expired">已过期</option>
|
|
</select>
|
|
<select v-model="filterScheme" @change="handleSearch">
|
|
<option value="">全部方案</option>
|
|
<option value="A">方案A (标准型)</option>
|
|
<option value="B">方案B (升级型)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>报价单号</th>
|
|
<th>客户信息</th>
|
|
<th>产品数量</th>
|
|
<th>方案类型</th>
|
|
<th>报价金额</th>
|
|
<th>状态</th>
|
|
<th>创建时间</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="item in paginatedData" :key="item.id">
|
|
<td class="quote-number">
|
|
<i class="fas fa-file-invoice"></i>
|
|
{{ item.quoteNumber }}
|
|
</td>
|
|
<td class="customer-info">
|
|
<div class="customer-name">{{ item.customerName }}</div>
|
|
<div class="customer-phone">{{ item.customerPhone }}</div>
|
|
</td>
|
|
<td>{{ item.productCount }} 套</td>
|
|
<td>
|
|
<span :class="['scheme-badge', item.scheme]">
|
|
方案{{ item.scheme }}
|
|
</span>
|
|
</td>
|
|
<td class="amount">¥{{ item.totalAmount.toFixed(2) }}</td>
|
|
<td>
|
|
<span :class="['status-badge', item.status]">
|
|
{{ getStatusText(item.status) }}
|
|
</span>
|
|
</td>
|
|
<td>{{ item.createdAt }}</td>
|
|
<td class="action-cell">
|
|
<button class="btn-action view" @click="viewDetail(item)" title="查看">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn-action edit" @click="editItem(item)" title="编辑">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn-action copy" @click="copyItem(item)" title="复制">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
<button class="btn-action send" @click="sendQuote(item)" title="发送">
|
|
<i class="fas fa-envelope"></i>
|
|
</button>
|
|
<button class="btn-action print" @click="printQuote(item)" title="打印">
|
|
<i class="fas fa-print"></i>
|
|
</button>
|
|
<button class="btn-action delete" @click="deleteItem(item)" title="删除">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="filteredData.length === 0">
|
|
<td colspan="8" class="empty-text">
|
|
<i class="fas fa-inbox"></i>
|
|
<p>暂无数据</p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pagination-container" v-if="filteredData.length > 0">
|
|
<div class="pagination-info">共 {{ filteredData.length }} 条记录,每页显示 {{ pageSize.value }}</div>
|
|
<div class="pagination">
|
|
<button class="page-btn" :disabled="currentPage.value === 1" @click="currentPage.value = 1">
|
|
<i class="fas fa-angle-double-left"></i>
|
|
</button>
|
|
<button class="page-btn" :disabled="currentPage.value === 1" @click="currentPage.value--">
|
|
<i class="fas fa-angle-left"></i>
|
|
</button>
|
|
<span class="page-info">{{ currentPage.value }} / {{ totalPages.value }}</span>
|
|
<button class="page-btn" :disabled="currentPage.value === totalPages.value" @click="currentPage.value++">
|
|
<i class="fas fa-angle-right"></i>
|
|
</button>
|
|
<button class="page-btn" :disabled="currentPage.value === totalPages.value" @click="currentPage.value = totalPages.value">
|
|
<i class="fas fa-angle-double-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="['modal', { show: modalVisible }]">
|
|
<div class="modal-content modal-form">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">
|
|
<i class="fas fa-file-invoice-dollar"></i>
|
|
{{ isEditMode.value ? '编辑报价表' : '新建报价表' }}
|
|
</h3>
|
|
<button class="modal-close" @click="closeModal">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form @submit.prevent="saveItem">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label><i class="fas fa-user"></i> 客户名称 *</label>
|
|
<input type="text" v-model="formData.customerName" required placeholder="请输入客户名称">
|
|
</div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-phone"></i> 联系电话 *</label>
|
|
<input type="tel" v-model="formData.customerPhone" required placeholder="请输入联系电话">
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label><i class="fas fa-envelope"></i> 邮箱</label>
|
|
<input type="email" v-model="formData.customerEmail" placeholder="请输入邮箱地址">
|
|
</div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-th-large"></i> 方案类型 *</label>
|
|
<select v-model="formData.scheme" required>
|
|
<option value="A">方案A (标准型)</option>
|
|
<option value="B">方案B (升级型)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label><i class="fas fa-boxes"></i> 产品数量 *</label>
|
|
<input type="number" v-model.number="formData.productCount" min="1" required placeholder="产品数量">
|
|
</div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-yen-sign"></i> 报价金额 *</label>
|
|
<input type="number" v-model.number="formData.totalAmount" min="0" step="0.01" required placeholder="报价金额">
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label><i class="fas fa-info-circle"></i> 状态</label>
|
|
<select v-model="formData.status">
|
|
<option value="draft">草稿</option>
|
|
<option value="sent">已发送</option>
|
|
<option value="accepted">已接受</option>
|
|
<option value="rejected">已拒绝</option>
|
|
<option value="expired">已过期</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-calendar-check"></i> 有效期至</label>
|
|
<input type="date" v-model="formData.validUntil">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-sticky-note"></i> 备注</label>
|
|
<textarea v-model="formData.remark" rows="3" placeholder="请输入备注信息"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline" @click="closeModal">取消</button>
|
|
<button class="btn btn-primary" @click="saveItem">
|
|
<i class="fas fa-save"></i> 保存
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="['modal', { show: detailModalVisible }]">
|
|
<div class="modal-content modal-detail">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">
|
|
<i class="fas fa-file-invoice-dollar"></i> 报价表详情
|
|
</h3>
|
|
<button class="modal-close" @click="closeDetailModal">×</button>
|
|
</div>
|
|
<div class="modal-body" v-if="currentItem">
|
|
<div class="detail-section">
|
|
<h4><i class="fas fa-info-circle"></i> 基本信息</h4>
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<span class="label">报价单号</span>
|
|
<span class="value">{{ currentItem.quoteNumber }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">客户名称</span>
|
|
<span class="value">{{ currentItem.customerName }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">联系电话</span>
|
|
<span class="value">{{ currentItem.customerPhone }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">邮箱</span>
|
|
<span class="value">{{ currentItem.customerEmail || '-' }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">产品数量</span>
|
|
<span class="value">{{ currentItem.productCount }} 套</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">方案类型</span>
|
|
<span :class="['scheme-badge', currentItem.scheme]">方案{{ currentItem.scheme }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">报价金额</span>
|
|
<span class="value amount">¥{{ currentItem.totalAmount.toFixed(2) }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">状态</span>
|
|
<span :class="['status-badge', currentItem.status]">{{ getStatusText(currentItem.status) }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">有效期至</span>
|
|
<span class="value">{{ currentItem.validUntil || '-' }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">创建时间</span>
|
|
<span class="value">{{ currentItem.createdAt }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="label">更新时间</span>
|
|
<span class="value">{{ currentItem.updatedAt }}</span>
|
|
</div>
|
|
<div class="detail-item full-width">
|
|
<span class="label">备注</span>
|
|
<span class="value">{{ currentItem.remark || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline" @click="closeDetailModal">关闭</button>
|
|
<button class="btn btn-primary" @click="editFromDetail">
|
|
<i class="fas fa-edit"></i> 编辑
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="['modal', { show: emailModalVisible }]">
|
|
<div class="modal-content modal-email">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">
|
|
<i class="fas fa-envelope"></i> 发送报价表
|
|
</h3>
|
|
<button class="modal-close" @click="closeEmailModal">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="email-details">
|
|
<p><strong>收件人:</strong> <span>{{ emailRecipient }}</span></p>
|
|
<p><strong>主题:</strong> <span>{{ emailSubject }}</span></p>
|
|
<p><strong>附件:</strong> <span>报价表_{{ currentDate.value }}.pdf</span></p>
|
|
<p><strong>邮件内容:</strong></p>
|
|
<p>尊敬的客户,您好!</p>
|
|
<p>根据您的需求,我们已为您准备了详细的产品报价方案,请查看附件中的报价表PDF文件。如果您有任何疑问或需要进一步调整,请随时与我们联系。</p>
|
|
<p>感谢您对我们产品的关注!</p>
|
|
<p>此致<br>销售团队</p>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline" @click="closeEmailModal">关闭</button>
|
|
<button class="btn btn-primary" @click="openEmailClient">
|
|
<i class="fas fa-external-link-alt"></i> 打开邮箱发送
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-nav">
|
|
<button class="btn btn-outline" @click="goToQuoteMeasure">
|
|
<i class="fas fa-arrow-left"></i> 返回测量报价
|
|
</button>
|
|
<button class="btn btn-primary" @click="goToNext">
|
|
下一步:跟单看板 <i class="fas fa-arrow-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
const router = useRouter();
|
|
|
|
const statusMap = {
|
|
draft: '草稿',
|
|
sent: '已发送',
|
|
accepted: '已接受',
|
|
rejected: '已拒绝',
|
|
expired: '已过期'
|
|
};
|
|
|
|
const showToast = ref(false);
|
|
const toastMessage = ref('');
|
|
const toastType = ref('info');
|
|
|
|
const showNotify = (message, type = 'info') => {
|
|
toastMessage.value = message;
|
|
toastType.value = type;
|
|
showToast.value = true;
|
|
setTimeout(() => {
|
|
showToast.value = false;
|
|
}, 3000);
|
|
};
|
|
|
|
const quoteList = ref([]);
|
|
const searchKeyword = ref('');
|
|
const filterStatus = ref('');
|
|
const filterScheme = ref('');
|
|
const currentPage = ref(1);
|
|
const pageSize = ref(10);
|
|
const currentDate = ref('');
|
|
|
|
const generateDemoData = () => {
|
|
const customers = [
|
|
{ name: '王建国', phone: '13800138001', email: 'wang@example.com' },
|
|
{ name: '李美玲', phone: '13900139002', email: 'li@example.com' },
|
|
{ name: '赵明辉', phone: '13700137003', email: 'zhao@example.com' },
|
|
{ name: '陈晓东', phone: '13600136004', email: 'chen@example.com' },
|
|
{ name: '刘志远', phone: '13500135005', email: 'liu@example.com' }
|
|
];
|
|
|
|
const statuses = ['draft', 'sent', 'accepted', 'rejected', 'expired'];
|
|
const schemes = ['A', 'B'];
|
|
|
|
const data = [];
|
|
const now = new Date();
|
|
|
|
for (let i = 1; i <= 15; i++) {
|
|
const customer = customers[(i - 1) % customers.length];
|
|
const daysAgo = Math.floor(Math.random() * 30);
|
|
const createdDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
|
const validDate = new Date(createdDate.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
const productCount = Math.floor(Math.random() * 5) + 1;
|
|
const scheme = schemes[Math.floor(Math.random() * schemes.length)];
|
|
const basePrice = scheme === 'A' ? 100 : 200;
|
|
const totalAmount = productCount * basePrice * (Math.random() * 2 + 1);
|
|
|
|
data.push({
|
|
id: i,
|
|
quoteNumber: 'QT-' + String(2024000 + i).padStart(7, '0'),
|
|
customerName: customer.name,
|
|
customerPhone: customer.phone,
|
|
customerEmail: customer.email,
|
|
productCount,
|
|
scheme,
|
|
totalAmount,
|
|
status: statuses[Math.floor(Math.random() * statuses.length)],
|
|
validUntil: validDate.getFullYear() + '-' + String(validDate.getMonth() + 1).padStart(2, '0') + '-' + String(validDate.getDate()).padStart(2, '0'),
|
|
remark: i % 3 === 0 ? '客户要求加急处理' : i % 5 === 0 ? '需要上门安装' : '',
|
|
createdAt: createdDate.getFullYear() + '-' + String(createdDate.getMonth() + 1).padStart(2, '0') + '-' + String(createdDate.getDate()).padStart(2, '0') + ' ' + String(createdDate.getHours()).padStart(2, '0') + ':' + String(createdDate.getMinutes()).padStart(2, '0'),
|
|
updatedAt: createdDate.getFullYear() + '-' + String(createdDate.getMonth() + 1).padStart(2, '0') + '-' + String(createdDate.getDate()).padStart(2, '0') + ' ' + String(createdDate.getHours()).padStart(2, '0') + ':' + String(createdDate.getMinutes()).padStart(2, '0')
|
|
});
|
|
}
|
|
return data;
|
|
};
|
|
|
|
const getStatusText = status => statusMap[status] || status;
|
|
const filteredData = computed(() => {
|
|
let result = quoteList.value;
|
|
if (searchKeyword.value) {
|
|
const k = searchKeyword.value.toLowerCase();
|
|
result = result.filter(x =>
|
|
x.quoteNumber.toLowerCase().includes(k) || x.customerName.toLowerCase().includes(k)
|
|
);
|
|
}
|
|
if (filterStatus.value) {
|
|
result = result.filter(x => x.status === filterStatus.value);
|
|
}
|
|
if (filterScheme.value) {
|
|
result = result.filter(x => x.scheme === filterScheme.value);
|
|
}
|
|
return result;
|
|
});
|
|
const totalPages = computed(() => Math.ceil(filteredData.value.length / pageSize.value) || 1);
|
|
const paginatedData = computed(() => {
|
|
const start = (currentPage.value - 1) * pageSize.value;
|
|
const end = start + pageSize.value;
|
|
return filteredData.value.slice(start, end);
|
|
});
|
|
const handleSearch = () => { currentPage.value = 1; };
|
|
const modalVisible = ref(false);
|
|
const detailModalVisible = ref(false);
|
|
const emailModalVisible = ref(false);
|
|
const isEditMode = ref(false);
|
|
const currentItem = ref(null);
|
|
const formData = ref({ customerName: '', customerPhone: '', customerEmail: '', productCount: 1, scheme: 'A', totalAmount: 0, status: 'draft', validUntil: '', remark: '' });
|
|
const resetFormData = () => {
|
|
formData.value = { customerName: '', customerPhone: '', customerEmail: '', productCount: 1, scheme: 'A', totalAmount: 0, status: 'draft', validUntil: '', remark: '' };
|
|
};
|
|
const openCreateModal = () => { isEditMode.value = false; resetFormData(); modalVisible.value = true; };
|
|
const editItem = item => { isEditMode.value = true; currentItem.value = item; formData.value = { ...item }; modalVisible.value = true; };
|
|
const closeModal = () => { modalVisible.value = false; currentItem.value = null; };
|
|
const saveItem = () => {
|
|
if (!formData.value.customerName || !formData.value.customerPhone || !formData.value.productCount || formData.value.totalAmount === undefined) {
|
|
showNotify('请填写必填项', 'error');
|
|
return;
|
|
}
|
|
const now = new Date();
|
|
const nowStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
|
|
|
if (isEditMode.value && currentItem.value) {
|
|
const idx = quoteList.value.findIndex(x => x.id === currentItem.value.id);
|
|
if (idx !== -1) {
|
|
quoteList.value[idx] = { ...quoteList.value[idx], ...formData.value, updatedAt: nowStr };
|
|
showNotify('报价表已更新', 'success');
|
|
}
|
|
} else {
|
|
const newId = Math.max(...quoteList.value.map(x => x.id), 0) + 1;
|
|
const newQuoteNumber = 'QT-' + String(2024000 + newId).padStart(7, '0');
|
|
quoteList.value.unshift({
|
|
id: newId,
|
|
quoteNumber: newQuoteNumber,
|
|
...formData.value,
|
|
createdAt: nowStr,
|
|
updatedAt: nowStr
|
|
});
|
|
showNotify('报价表已创建', 'success');
|
|
}
|
|
|
|
closeModal();
|
|
};
|
|
const viewDetail = item => { currentItem.value = item; detailModalVisible.value = true; };
|
|
const closeDetailModal = () => { detailModalVisible.value = false; currentItem.value = null; };
|
|
const editFromDetail = () => { closeDetailModal(); editItem(currentItem.value); };
|
|
const copyItem = item => {
|
|
const now = new Date();
|
|
const nowStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
|
const newId = Math.max(...quoteList.value.map(x => x.id), 0) + 1;
|
|
const newQuoteNumber = 'QT-' + String(2024000 + newId).padStart(7, '0');
|
|
quoteList.value.unshift({ ...item, id: newId, quoteNumber: newQuoteNumber, status: 'draft', createdAt: nowStr, updatedAt: nowStr });
|
|
showNotify('报价表已复制', 'success');
|
|
};
|
|
const deleteItem = item => {
|
|
if (confirm('确定要删除报价表 ' + item.quoteNumber + ' 吗?')) {
|
|
const idx = quoteList.value.findIndex(x => x.id === item.id);
|
|
if (idx !== -1) {
|
|
quoteList.value.splice(idx, 1);
|
|
if (paginatedData.value.length === 0 && currentPage.value > 1) currentPage.value--;
|
|
showNotify('报价表已删除', 'success');
|
|
}
|
|
}
|
|
};
|
|
|
|
const emailRecipient = computed(() => currentItem.value?.customerEmail || 'customer@example.com');
|
|
const emailSubject = computed(() => '报价单#' + (currentItem.value?.quoteNumber || ''));
|
|
|
|
const sendQuote = item => {
|
|
currentItem.value = item;
|
|
const idx = quoteList.value.findIndex(x => x.id === item.id);
|
|
if (idx !== -1) {
|
|
quoteList.value[idx].status = 'sent';
|
|
quoteList.value[idx].updatedAt = new Date().getFullYear() + '-' + String(new Date().getMonth() + 1).padStart(2, '0') + '-' + String(new Date().getDate()).padStart(2, '0') + ' ' + String(new Date().getHours()).padStart(2, '0') + ':' + String(new Date().getMinutes()).padStart(2, '0');
|
|
}
|
|
showNotify('报价表已准备发送', 'success');
|
|
emailModalVisible.value = true;
|
|
};
|
|
const closeEmailModal = () => { emailModalVisible.value = false; };
|
|
const openEmailClient = () => {
|
|
const subject = encodeURIComponent(emailSubject.value);
|
|
const body = encodeURIComponent('尊敬的客户,您好!根据您的需求,我们已为您准备了详细的产品报价方案,请查看附件中的报价表PDF文件。如果您有任何疑问或需要进一步调整,请随时与我们联系。感谢您对我们产品的关注!此致,销售团队');
|
|
window.location.href = 'mailto:' + emailRecipient.value + '?subject=' + subject + '&body=' + body;
|
|
closeEmailModal();
|
|
};
|
|
const printQuote = item => { currentItem.value = item; showNotify('打印预览功能', 'info'); setTimeout(() => { window.print(); }, 500); };
|
|
const goToQuoteMeasure = () => { router.push('/quote'); };
|
|
const goToNext = () => { router.push('/order/tracking'); };
|
|
onMounted(() => { quoteList.value = generateDemoData(); const now = new Date(); currentDate.value = '' + now.getFullYear() + String(now.getMonth() + 1) + String(now.getDate()); });
|
|
</script>
|
|
|
|
<style scoped>
|
|
.quote-generate {}
|
|
.toast { position: fixed; top: 20px; right: 20px; background: #333; color: #fff; padding: 12px 24px; border-radius: 40px; font-size: 14px; z-index: 2000; opacity:0; transition: all .3s; pointer-events:none;}
|
|
.toast.success { background: #28a745; }
|
|
.toast.error { background: #dc3545; }
|
|
.toast.info { background: #2ea2cc; }
|
|
.toast.show { opacity: 1; }
|
|
.page-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom:24px;}
|
|
.page-header-left { display: flex; flex-direction: column; gap:4px; }
|
|
.page-title { font-size:1.5rem; font-weight:700; color:#2d4059; margin:0; display:flex; gap:10px; align-items:center;}
|
|
.page-title i { color:#2ea2cc; }
|
|
.text-secondary { color:#6c757d; display:flex; gap:6px; align-items:center; }
|
|
.nav-tabs { display:flex; border-bottom: 2px solid #d4dfec; margin-bottom:24px; gap:4px;}
|
|
.nav-tab { padding:10px 20px; font-weight:600; color:#4a5f73; border-bottom:3px solid transparent; cursor:pointer;}
|
|
.nav-tab:hover { color:#2ea2cc; }
|
|
.nav-tab.active { color:#2ea2cc; border-bottom-color:#2ea2cc; }
|
|
.search-bar { display:flex; gap:16px; margin-bottom:20px; flex-wrap: wrap; }
|
|
.search-input-group { flex:1; min-width:280px; position:relative; }
|
|
.search-input-group i { position:absolute; left:16px; top:50%; transform: translateY(-50%); color:#6c757d; }
|
|
.search-input-group input { width:100%; padding:12px 16px 12px 44px; border: 2px solid #d4dfec; border-radius:40px; font-size:14px; outline:none;}
|
|
.search-input-group input:focus { border-color:#2ea2cc; box-shadow:0 0 0 3px rgba(46,162,204,0.1);}
|
|
.filter-group { display:flex; gap:12px; }
|
|
.filter-group select { padding:12px 20px; border:2px solid #d4dfec; border-radius:40px; font-size:14px; background:#fff; cursor:pointer; min-width:140px;}
|
|
.filter-group select:focus { outline:none; border-color:#2ea2cc; }
|
|
.card { background:#fff; border-radius:16px; border:1px solid #d4dfec; overflow:hidden; }
|
|
.table-container { overflow-x:auto; }
|
|
table { width:100%; border-collapse:collapse; }
|
|
th, td { padding:14px 16px; text-align:left; border-bottom:1px solid #eef2f7; }
|
|
th { background:#f8fafc; font-weight:600; color:#4a5f73; font-size:0.9rem; white-space:nowrap; position:sticky; top:0; z-index:1; }
|
|
.quote-number { font-weight:600; color:#2ea2cc; display:flex; gap:6px; align-items:center; }
|
|
.customer-info .customer-name { font-weight:600; color:#2d4059; margin-bottom:2px; }
|
|
.customer-info .customer-phone { font-size:0.85rem; color:#6c757d; }
|
|
.amount { font-weight:700; color:#e74c3c; }
|
|
.scheme-badge { display:inline-block; padding:4px 12px; border-radius:20px; font-size:0.85rem; font-weight:600; }
|
|
.scheme-badge.A { background:#e3f2fd; color:#1976d2; }
|
|
.scheme-badge.B { background:#fff3e0; color:#f57c00; }
|
|
.status-badge { display:inline-block; padding:4px 12px; border-radius:20px; font-size:0.85rem; font-weight:500; }
|
|
.status-badge.draft { background:#f5f5f5; color:#616161; }
|
|
.status-badge.sent { background:#e3f2fd; color:#1565c0; }
|
|
.status-badge.accepted { background:#d4edda; color:#155724; }
|
|
.status-badge.rejected { background:#ffebee; color:#c62828; }
|
|
.status-badge.expired { background:#fce4ec; color:#880e4f; }
|
|
.action-cell { white-space:nowrap; display:flex; gap:4px; }
|
|
.btn-action { width:32px; height:32px; border:none; border-radius:8px; cursor:pointer; display:flex; justify-content:center; align-items:center; }
|
|
.btn-action.view { background:#e3f2fd; color:#1976d2; }
|
|
.btn-action.view:hover { background:#1976d2; color:#fff; }
|
|
.btn-action.edit { background:#fff3e0; color:#f57c00; }
|
|
.btn-action.edit:hover { background:#f57c00; color:#fff; }
|
|
.btn-action.copy { background:#f3e5f5; color:#7b1fa2; }
|
|
.btn-action.copy:hover { background:#7b1fa2; color:#fff; }
|
|
.btn-action.send { background:#e8f5e9; color:#388e3c; }
|
|
.btn-action.send:hover { background:#388e3c; color:#fff; }
|
|
.btn-action.print { background:#e0f2f1; color:#00796b; }
|
|
.btn-action.print:hover { background:#00796b; color:#fff; }
|
|
.btn-action.delete { background:#ffebee; color:#d32f2f; }
|
|
.btn-action.delete:hover { background:#d32f2f; color:#fff; }
|
|
.empty-text { text-align:center; padding:60px 20px; color:#6c757d; }
|
|
.empty-text i { font-size:48px; margin-bottom:16px; display:block; opacity:0.5; }
|
|
.empty-text p { margin:0; font-size:16px; }
|
|
.pagination-container { display:flex; justify-content:space-between; align-items:center; padding:20px; border-top:1px solid #eef2f7; gap:12px; flex-wrap: wrap;}
|
|
.pagination-info { color:#6c757d; font-size:14px; }
|
|
.pagination { display:flex; align-items:center; gap:8px; }
|
|
.page-btn { width:36px; height:36px; border:1px solid #d4dfec; background:#fff; border-radius:8px; cursor:pointer; display:flex; justify-content:center; align-items:center; }
|
|
.page-btn:hover:not(:disabled) { border-color:#2ea2cc; color:#2ea2cc; }
|
|
.page-btn:disabled { opacity:0.5; cursor:not-allowed; }
|
|
.page-info { padding:0 12px; font-weight:600; color:#2d4059; }
|
|
.btn { padding:12px 20px; border-radius:10px; font-weight:600; cursor:pointer; transition:all .2s; border:none; display:flex; align-items:center; gap:8px; }
|
|
.btn-primary { background:linear-gradient(135deg, #2ea2cc, #2690b3); color:#fff; }
|
|
.btn-primary:hover { transform: translateY(-1px); }
|
|
.btn-outline { background:#fff; color:#2d4059; border:1px solid #d4dfec; }
|
|
.btn-outline:hover { background:#f8fafc; border-color:#2ea2cc; }
|
|
.modal { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); display:flex; justify-content:center; align-items:center; z-index:1000; opacity:0; visibility:hidden; transition:all .3s; }
|
|
.modal.show { opacity:1; visibility:visible; }
|
|
.modal-content { background:#fff; border-radius:16px; width:90%; max-height:90vh; overflow-y:auto; transform: translateY(-20px); transition:transform .3s; }
|
|
.modal.show .modal-content { transform: translateY(0); }
|
|
.modal-form { max-width:700px; }
|
|
.modal-detail { max-width:800px; }
|
|
.modal-email { max-width:600px; }
|
|
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:20px 24px; border-bottom:1px solid #eef2f7; }
|
|
.modal-title { margin:0; font-size:1.25rem; color:#2d4059; display:flex; gap:8px; align-items:center; }
|
|
.modal-title i { color:#2ea2cc; }
|
|
.modal-close { background:none; border:none; font-size:1.5rem; color:#6c757d; cursor:pointer; padding:0; line-height:1; }
|
|
.modal-close:hover { color:#2d4059; }
|
|
.modal-body { padding:24px; }
|
|
.modal-footer { display:flex; justify-content:flex-end; gap:12px; padding:16px 24px; border-top:1px solid #eef2f7; }
|
|
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
|
|
.form-group { margin-bottom:20px; }
|
|
.form-group label { display:block; margin-bottom:8px; font-weight:600; color:#2d4059; font-size:0.9rem; display:flex; gap:6px; align-items:center; }
|
|
.form-group label i { color:#2ea2cc; }
|
|
.form-group input, .form-group select, .form-group textarea { width:100%; padding:10px 14px; border:1px solid #d4dfec; border-radius:8px; font-size:14px; outline:none; }
|
|
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color:#2ea2cc; box-shadow:0 0 0 3px rgba(46,162,204,0.1); }
|
|
.detail-section { margin-bottom:24px; }
|
|
.detail-section:last-child { margin-bottom:0; }
|
|
.detail-section h4 { margin:0 0 16px; padding-bottom:8px; border-bottom:2px solid #eef2f7; color:#2d4059; font-size:1.1rem; display:flex; gap:8px; align-items:center; }
|
|
.detail-section h4 i { color:#2ea2cc; }
|
|
.detail-grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
|
|
.detail-item { display:flex; flex-direction:column; gap:4px; }
|
|
.detail-item.full-width { grid-column: 1 / -1; }
|
|
.detail-item .label { font-size:0.85rem; color:#6c757d; font-weight:500; }
|
|
.detail-item .value { font-size:1rem; color:#2d4059; font-weight:500; }
|
|
.detail-item .value.amount { font-size:1.2rem; font-weight:700; color:#e74c3c; }
|
|
.email-details { background:#f8f9fa; border-radius:6px; padding:15px; }
|
|
.email-details p { margin-bottom:8px; color:#2d4059; }
|
|
.email-details p:last-child { margin-bottom:0; }
|
|
.flow-nav { display:flex; justify-content:space-between; margin-top:24px; }
|
|
@media (max-width: 768px) {
|
|
.page-header { flex-direction:column; gap:16px; }
|
|
.page-header-right, .search-input-group, .filter-group, .flow-nav { width:100%; }
|
|
.page-header-right .btn, .flow-nav .btn { width:100%; justify-content:center; }
|
|
.search-bar { flex-direction:column; }
|
|
.form-row, .detail-grid { grid-template-columns:1fr; }
|
|
.flow-nav { flex-direction:column; gap:12px; }
|
|
.action-cell { min-width:200px; }
|
|
}
|
|
@media print {
|
|
.flow-nav, .search-bar, .action-cell, .btn, .modal { display:none !important; }
|
|
.card { box-shadow:none; border:1px solid #ddd; }
|
|
}
|
|
</style>`;
|
|
fs.writeFileSync('./src/views/quote/QuoteGenerate.vue', fixedContent, 'utf8');
|
|
console.log('✅ Successfully wrote QuoteGenerate.vue!');
|