Commit d8833b65 by suyuchen

feat(crawler): 添加爬虫网站管理功能

- 实现爬虫网站列表展示,包含网站名称、URL、请求部门等信息 - 添加网站新增、编辑、删除功能,支持表单验证和数据保存 - 实现爬取状态管理,包含正常、失败、暂停、待审核等状态 - 添加负责人字段管理,支持爬虫任务分配和跟踪 - 实现爬取频率设置和下次爬取时间计算功能 - 添加防爬机制配置和反爬策略管理 - 实现数据量统计和成功率监控功能 - 优化界面样式,提升用户体验和操作便捷性
parent 01256ea8
# 客户信息页面字段扩展需求
# 客户信息页面字段扩展需求
## 需求概述
为提升客户信息管理功能,需在客户信息页面增加VIP等级和负责人字段,以支持更精细化的客户分类和责任分配。
## 具体需求
### 列表页面
- 新增 [VIP等级](file:///D:/jimai/奥智项目/1,2,3/客户信息/.md#L9-L9)
- 新增 [负责人](file:///D:/jimai/奥智项目/1,2,3/客户信息/.md#L10-L10)
### 表单页面
- 新增 [VIP等级](file:///D:/jimai/奥智项目/1,2,3/客户信息/.md#L9-L9) 字段
- 新增 [负责人](file:///D:/jimai/奥智项目/1,2,3/客户信息/.md#L10-L10) 字段
## 预期效果
通过新增字段,实现对VIP客户的精准识别和专属负责人分配,提升客户服务水平和管理效率。
\ No newline at end of file
# 工单列表字段扩展需求
# 工单列表字段扩展需求
## 需求概述
为完善工单信息展示和处理流程,需在工单列表及表单中增加多个字段,并实现SLA计时功能,以提升工单处理效率和客户服务质量。
## 具体需求
### 列表页面
新增以下列:
- SLA计时
- 客户名称
- 服务订单号
- 设备信息
- 设备序列号
- 国家/地区
### 表单页面
新增以下字段:
- 客户名称
- 服务订单号
- 设备信息
- 设备序列号
- 国家/地区
### 特殊逻辑需求
1. 在表单原有字段"问题类型"中新增"紧急/停车"类别
2. "紧急/停车"类别需根据登录用户权限控制显示
3. 选择"紧急/停车"类型时自动启用SLA计时功能
4. 默认设置为T1级别,该级别包含以下字段:
- 首次响应时间
- 处理超时时间
- 通知对象
- 升级路径
5. SLA计时规则在"基本设置模块"的"升级规则配置"中维护
## 预期效果
通过新增字段和SLA计时功能,实现工单信息的全面展示和及时处理,确保紧急问题得到优先处理和有效跟踪。
\ No newline at end of file
...@@ -130,7 +130,392 @@ function initBatchUpload() { ...@@ -130,7 +130,392 @@ function initBatchUpload() {
} }
} }
// 爬虫网站管理功能
let crawlerSites = [
{
id: 1,
siteName: '行业资讯网',
siteUrl: 'https://www.industrynews.com',
requestDept: '市场部',
isCollected: '是',
collectionPlan: '定期爬取行业动态和新闻资讯,每周更新一次数据',
crawlFrequency: '每天一次',
lastCrawlTime: '2026-01-05T14:30',
nextCrawlTime: '2026-01-06T14:30',
crawlStatus: '正常',
targetDataType: '新闻标题、发布时间、内容摘要',
pageStructureStable: '是',
antiCrawlMechanism: 'IP限制,需使用代理池',
crawlerScriptId: 'CRAWL_001',
负责人: '张三',
lastUpdater: '李四',
lastUpdateTime: '2026-01-05T10:00',
dailyDataVolume: 500,
successRate: 98.5,
备注: '需使用企业网络访问'
},
{
id: 2,
siteName: '竞品分析网',
siteUrl: 'https://www.competitoranalysis.com',
requestDept: '产品部',
isCollected: '是',
collectionPlan: '每日爬取竞品价格和功能更新,生成对比报告',
crawlFrequency: '每小时一次',
lastCrawlTime: '2026-01-06T10:15',
nextCrawlTime: '2026-01-06T11:15',
crawlStatus: '正常',
targetDataType: '产品价格、功能列表、更新日志',
pageStructureStable: '否',
antiCrawlMechanism: '验证码,需OCR识别',
crawlerScriptId: 'CRAWL_002',
负责人: '王五',
lastUpdater: '赵六',
lastUpdateTime: '2026-01-04T16:20',
dailyDataVolume: 200,
successRate: 92.3,
备注: '仅在工作日运行'
},
{
id: 3,
siteName: '政策法规网',
siteUrl: 'https://www.policyregulation.com',
requestDept: '法务部',
isCollected: '否',
collectionPlan: '网站内容更新频繁但结构复杂,爬取成本过高',
crawlFrequency: '',
lastCrawlTime: '',
nextCrawlTime: '',
crawlStatus: '已停用',
targetDataType: '',
pageStructureStable: '',
antiCrawlMechanism: '',
crawlerScriptId: '',
负责人: '孙七',
lastUpdater: '周八',
lastUpdateTime: '2025-12-20T09:15',
dailyDataVolume: 0,
successRate: 0,
备注: '需人工评估后再决定是否收录'
}
];
let editingId = null;
// 渲染爬虫网站表格
function renderCrawlerSitesTable() {
const tbody = document.getElementById('crawler-sites-tbody');
if (!tbody) return;
tbody.innerHTML = '';
crawlerSites.forEach(site => {
const row = document.createElement('tr');
// 根据是否收录设置状态样式
const statusClass = site.isCollected === '是' ? 'status active' : 'status inactive';
// 根据爬取状态设置状态样式
const crawlStatusClass = site.crawlStatus === '正常' ? 'status active' :
site.crawlStatus === '失败' ? 'status inactive' :
site.crawlStatus === '暂停' ? 'status inactive' :
site.crawlStatus === '待审核' ? 'status' : 'status';
row.innerHTML = `
<td><strong>${site.siteName}</strong></td>
<td><a href="${site.siteUrl}" target="_blank">${site.siteUrl}</a></td>
<td>${site.requestDept}</td>
<td><span class="${statusClass}">${site.isCollected}</span></td>
<td>${site.collectionPlan}</td>
<td>${site.crawlFrequency}</td>
<td>${site.lastCrawlTime || '-'}</td>
<td><span class="${crawlStatusClass}">${site.crawlStatus}</span></td>
<td>${site.targetDataType}</td>
<td>${site.antiCrawlMechanism}</td>
<td>${site.负责人}</td>
<td>
<div class="action-buttons">
<button class="action-btn edit-btn" data-id="${site.id}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="action-btn delete-btn" data-id="${site.id}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
`;
tbody.appendChild(row);
});
// 为编辑和删除按钮添加事件监听器
document.querySelectorAll('.action-btn.edit-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.currentTarget.getAttribute('data-id'));
editCrawlerSite(id);
});
});
document.querySelectorAll('.action-btn.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.currentTarget.getAttribute('data-id'));
deleteCrawlerSite(id);
});
});
}
// 显示新增网站模态框
function showAddSiteModal() {
const modal = document.getElementById('crawler-site-modal');
const modalTitle = document.getElementById('modal-title');
const siteForm = document.getElementById('crawler-site-form');
if (!modal || !modalTitle || !siteForm) return;
modalTitle.textContent = '新增网站';
siteForm.reset();
document.getElementById('site-id').value = '';
editingId = null;
modal.style.display = 'flex';
}
// 显示编辑网站模态框
function showEditSiteModal(site) {
const modal = document.getElementById('crawler-site-modal');
const modalTitle = document.getElementById('modal-title');
const siteForm = document.getElementById('crawler-site-form');
if (!modal || !modalTitle || !siteForm) return;
modalTitle.textContent = '编辑网站';
document.getElementById('site-id').value = site.id;
document.getElementById('site-name').value = site.siteName;
document.getElementById('site-url').value = site.siteUrl;
document.getElementById('request-dept').value = site.requestDept;
document.getElementById('is-collected').value = site.isCollected;
document.getElementById('collection-plan').value = site.collectionPlan;
document.getElementById('crawl-frequency').value = site.crawlFrequency || '';
document.getElementById('crawl-status').value = site.crawlStatus || '';
document.getElementById('target-data-type').value = site.targetDataType || '';
document.getElementById('anti-crawl-mechanism').value = site.antiCrawlMechanism || '';
document.getElementById('负责人').value = site.负责人 || '';
document.getElementById('last-crawl-time').value = site.lastCrawlTime || '';
document.getElementById('备注').value = site.备注 || '';
editingId = site.id;
modal.style.display = 'flex';
}
// 编辑网站
function editCrawlerSite(id) {
const site = crawlerSites.find(s => s.id === id);
if (site) {
showEditSiteModal(site);
}
}
// 删除网站
function deleteCrawlerSite(id) {
if (confirm('确定要删除这个网站吗?此操作不可恢复。')) {
crawlerSites = crawlerSites.filter(site => site.id !== id);
renderCrawlerSitesTable();
}
}
// 计算下次爬取时间的辅助函数
function calculateNextCrawlTime(lastTime, frequency) {
if (!lastTime) {
// 如果没有上次爬取时间,使用当前时间
lastTime = new Date().toISOString().slice(0, 16).replace('T', ' ');
}
// 将字符串转换为日期对象
let date = new Date(lastTime.replace(' ', 'T'));
// 根据频率计算下次爬取时间
switch(frequency) {
case '每天一次':
date.setDate(date.getDate() + 1);
break;
case '每小时一次':
date.setHours(date.getHours() + 1);
break;
case '每周一上午':
// 计算到下一个周一
const day = date.getDay();
const daysUntilMonday = day === 0 ? 1 : (8 - day);
date.setDate(date.getDate() + daysUntilMonday);
date.setHours(9, 0, 0, 0); // 设置为上午9点
break;
case '每30分钟':
date.setMinutes(date.getMinutes() + 30);
break;
case '每月1号':
date.setMonth(date.getMonth() + 1);
date.setDate(1);
date.setHours(9, 0, 0, 0); // 设置为每月1号上午9点
break;
default:
date.setDate(date.getDate() + 1); // 默认每天一次
}
// 返回格式化的日期字符串
return date.toISOString().slice(0, 16).replace('T', ' ');
}
// 保存网站
function saveCrawlerSite() {
const id = document.getElementById('site-id').value;
const siteName = document.getElementById('site-name').value;
const siteUrl = document.getElementById('site-url').value;
const requestDept = document.getElementById('request-dept').value;
const isCollected = document.getElementById('is-collected').value;
const collectionPlan = document.getElementById('collection-plan').value;
const crawlFrequency = document.getElementById('crawl-frequency').value;
const crawlStatus = document.getElementById('crawl-status').value;
const targetDataType = document.getElementById('target-data-type').value;
const antiCrawlMechanism = document.getElementById('anti-crawl-mechanism').value;
const 负责人 = document.getElementById('负责人').value;
const lastCrawlTime = document.getElementById('last-crawl-time').value;
const 备注 = document.getElementById('备注').value;
// 验证必填字段
if (!siteName || !siteUrl || !requestDept || !isCollected || !collectionPlan || !crawlFrequency || !crawlStatus || !targetDataType || !负责人) {
alert('请填写所有必填字段');
return;
}
// 验证网址格式
try {
new URL(siteUrl);
} catch (e) {
alert('请输入有效的网址格式');
return;
}
if (editingId) {
// 编辑现有网站
const index = crawlerSites.findIndex(s => s.id === editingId);
if (index !== -1) {
crawlerSites[index] = {
id: editingId,
siteName,
siteUrl,
requestDept,
isCollected,
collectionPlan,
crawlFrequency,
lastCrawlTime,
// 计算下次爬取时间(这里只是模拟,实际应根据频率计算)
nextCrawlTime: calculateNextCrawlTime(lastCrawlTime, crawlFrequency),
crawlStatus,
targetDataType,
pageStructureStable: '是', // 默认值,实际应用中可能需要从表单获取
antiCrawlMechanism,
crawlerScriptId: `CRAWL_${editingId.toString().padStart(3, '0')}`, // 生成脚本ID
负责人,
lastUpdater: '当前用户', // 实际应用中应从会话获取
lastUpdateTime: new Date().toISOString().slice(0, 16).replace('T', ' '), // 当前时间
dailyDataVolume: 0, // 默认值,实际应用中可能需要从表单获取
successRate: 100, // 默认值,实际应用中可能需要从表单获取
备注
};
}
} else {
// 添加新网站
const newId = crawlerSites.length > 0 ? Math.max(...crawlerSites.map(s => s.id)) + 1 : 1;
const currentTime = new Date().toISOString().slice(0, 16).replace('T', ' ');
crawlerSites.push({
id: newId,
siteName,
siteUrl,
requestDept,
isCollected,
collectionPlan,
crawlFrequency,
lastCrawlTime,
// 计算下次爬取时间(这里只是模拟,实际应根据频率计算)
nextCrawlTime: calculateNextCrawlTime(lastCrawlTime, crawlFrequency),
crawlStatus,
targetDataType,
pageStructureStable: '是', // 默认值
antiCrawlMechanism,
crawlerScriptId: `CRAWL_${newId.toString().padStart(3, '0')}`, // 生成脚本ID
负责人,
lastUpdater: '当前用户', // 实际应用中应从会话获取
lastUpdateTime: currentTime, // 当前时间
dailyDataVolume: 0, // 默认值
successRate: 100, // 默认值
备注
});
}
renderCrawlerSitesTable();
closeSiteModal();
}
// 关闭模态框
function closeSiteModal() {
const modal = document.getElementById('crawler-site-modal');
if (modal) {
modal.style.display = 'none';
}
}
// 初始化爬虫网站管理功能
function initCrawlerSiteManagement() {
// 绑定新增网站按钮事件
const addSiteBtn = document.getElementById('add-site-btn');
if (addSiteBtn) {
addSiteBtn.addEventListener('click', showAddSiteModal);
}
// 绑定模态框相关事件
const closeSiteModalBtn = document.getElementById('close-site-modal');
const cancelSiteBtn = document.getElementById('cancel-site-btn');
const saveSiteBtn = document.getElementById('save-site-btn');
if (closeSiteModalBtn) {
closeSiteModalBtn.addEventListener('click', closeSiteModal);
}
if (cancelSiteBtn) {
cancelSiteBtn.addEventListener('click', closeSiteModal);
}
if (saveSiteBtn) {
saveSiteBtn.addEventListener('click', saveCrawlerSite);
}
// 点击模态框外部区域关闭模态框
const modal = document.getElementById('crawler-site-modal');
if (modal) {
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeSiteModal();
}
});
}
// 绑定回车键事件,当在表单中按下回车时保存数据
const siteForm = document.getElementById('crawler-site-form');
if (siteForm) {
siteForm.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
saveCrawlerSite();
}
});
}
// 渲染表格
renderCrawlerSitesTable();
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadSidebar(); loadSidebar();
initBatchUpload(); initBatchUpload();
initCrawlerSiteManagement();
}); });
\ No newline at end of file
<!DOCTYPE html> <!DOCTYPE html>
...@@ -785,6 +785,7 @@ ...@@ -785,6 +785,7 @@
display: flex; display: flex;
gap: 15px; gap: 15px;
margin-bottom: 15px; margin-bottom: 15px;
/*margin-right: 20px;*/
} }
.add-modal-form .form-row .form-group { .add-modal-form .form-row .form-group {
...@@ -1313,7 +1314,7 @@ ...@@ -1313,7 +1314,7 @@
} }
.container { .container {
max-width: 1000px; /*max-width: 1000px;*/
/*margin: 0 auto;*/ /*margin: 0 auto;*/
} }
...@@ -1382,7 +1383,7 @@ ...@@ -1382,7 +1383,7 @@
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 20px; gap: 100px;
} }
.form-actions { .form-actions {
...@@ -1464,7 +1465,11 @@ ...@@ -1464,7 +1465,11 @@
<div class="container"> <div class="container">
<div class="card" style="padding: 20px;"> <div class="card" style="padding: 20px;">
<div class="page-header">
<div>
<h1 class="page-title" data-i18n="title">{{ isEdit ? '编辑' : '新增' }}人工费用</h1>
</div>
</div>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
......
<!DOCTYPE html> <!DOCTYPE html>
...@@ -480,7 +480,7 @@ ...@@ -480,7 +480,7 @@
/* 按钮样式 */ /* 按钮样式 */
.btn { .btn {
padding: 10px 20px; padding: 4px 14px;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
...@@ -489,7 +489,7 @@ ...@@ -489,7 +489,7 @@
transition: all 0.2s; transition: all 0.2s;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; margin-right: 10px;
} }
.btn-primary { .btn-primary {
...@@ -1331,11 +1331,15 @@ ...@@ -1331,11 +1331,15 @@
<div class="container"> <div class="container">
<div class="card" style="padding: 20px;"> <div class="card" style="padding: 20px;">
<div class="toolbar" style="display:flex;justify-content: end;margin-bottom: 20px;"> <div class="page-header">
<button class="btn btn-primary" style="text-align: right;" @click="handleAdd">+ 新增人工费用 <div>
</button> <h1 class="page-title" data-i18n="title">人工费用</h1>
</div>
<div class="toolbar" style="display:flex;justify-content: end;margin-bottom: 20px;">
<button class="btn btn-primary" style="text-align: right;padding: 8px 28px;" @click="handleAdd">+ 新增人工费用
</button>
</div>
</div> </div>
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <tr>
...@@ -1364,43 +1368,43 @@ ...@@ -1364,43 +1368,43 @@
</td> </td>
<td> <td>
<div class="action-btns"> <div class="action-btns">
<!-- <button class="btn btn-primary btn-small" @click="handleEdit(item)">编辑</button>--> <button class="btn btn-primary btn-small" @click="handleEdit(item)">编辑</button>
<svg t="1767688375858" class="icon" viewBox="0 0 1024 1024" version="1.1" <!-- <svg t="1767688375858" class="icon" viewBox="0 0 1024 1024" version="1.1"
@click="handleEdit(item)" style="cursor: pointer;margin-right: 5px;" @click="handleEdit(item)" style="cursor: pointer;margin-right: 5px;"
xmlns="http://www.w3.org/2000/svg" p-id="4934" width="20" height="20"> xmlns="http://www.w3.org/2000/svg" p-id="4934" width="20" height="20">
<path d="M800.8 956.7H232.7c-89.3 0-162-72.7-162-162V226.6c0-89.3 72.7-162 162-162h218.5c17.7 0 32 14.3 32 32s-14.3 32-32 32H232.7c-54 0-98 44-98 98v568.1c0 54 44 98 98 98h568.1c54 0 98-44 98-98V586.6c0-17.7 14.3-32 32-32s32 14.3 32 32v208.1c0 89.3-72.7 162-162 162z" <path d="M800.8 956.7H232.7c-89.3 0-162-72.7-162-162V226.6c0-89.3 72.7-162 162-162h218.5c17.7 0 32 14.3 32 32s-14.3 32-32 32H232.7c-54 0-98 44-98 98v568.1c0 54 44 98 98 98h568.1c54 0 98-44 98-98V586.6c0-17.7 14.3-32 32-32s32 14.3 32 32v208.1c0 89.3-72.7 162-162 162z"
p-id="4935"></path> p-id="4935"></path>
<path d="M525 669.4c-8.2 0-16.4-3.1-22.6-9.4L368 525.6c-12.5-12.5-12.5-32.8 0-45.3L756.2 92.2c8.7-8.7 33-29 69.9-29 26.5 0 51.4 10.3 70 29l39.8 39.8c38.6 38.6 38.6 101.3 0 139.9L547.6 660c-6.2 6.3-14.4 9.4-22.6 9.4zM435.9 503l89.1 89.1 365.6-365.6c13.6-13.6 13.6-35.7 0-49.3l-39.8-39.8-0.1-0.1c-6.5-6.5-15.2-10.1-24.6-10.1-13.4 0-22.3 7.8-24.7 10.2L435.9 503z" <path d="M525 669.4c-8.2 0-16.4-3.1-22.6-9.4L368 525.6c-12.5-12.5-12.5-32.8 0-45.3L756.2 92.2c8.7-8.7 33-29 69.9-29 26.5 0 51.4 10.3 70 29l39.8 39.8c38.6 38.6 38.6 101.3 0 139.9L547.6 660c-6.2 6.3-14.4 9.4-22.6 9.4zM435.9 503l89.1 89.1 365.6-365.6c13.6-13.6 13.6-35.7 0-49.3l-39.8-39.8-0.1-0.1c-6.5-6.5-15.2-10.1-24.6-10.1-13.4 0-22.3 7.8-24.7 10.2L435.9 503z"
p-id="4936"></path> p-id="4936"></path>
<path d="M350.8 709.2c-8.4 0-16.5-3.3-22.6-9.4-8.1-8.1-11.2-19.9-8.3-30.9L366.2 496c3-11 11.6-19.7 22.6-22.6 11-3 22.8 0.2 30.9 8.3l126.6 126.6c8.1 8.1 11.2 19.9 8.3 30.9-3 11-11.6 19.7-22.6 22.6l-172.9 46.3c-2.8 0.7-5.5 1.1-8.3 1.1z m62.9-143.1L396 632l65.8-17.6-48.1-48.3zM848 316c-8.3 0-16.6-3.2-22.8-9.6L707 186.1c-12.4-12.6-12.2-32.9 0.4-45.3 12.6-12.4 32.9-12.2 45.3 0.4l118.2 120.3c12.4 12.6 12.2 32.9-0.4 45.3-6.3 6.1-14.4 9.2-22.5 9.2z" <path d="M350.8 709.2c-8.4 0-16.5-3.3-22.6-9.4-8.1-8.1-11.2-19.9-8.3-30.9L366.2 496c3-11 11.6-19.7 22.6-22.6 11-3 22.8 0.2 30.9 8.3l126.6 126.6c8.1 8.1 11.2 19.9 8.3 30.9-3 11-11.6 19.7-22.6 22.6l-172.9 46.3c-2.8 0.7-5.5 1.1-8.3 1.1z m62.9-143.1L396 632l65.8-17.6-48.1-48.3zM848 316c-8.3 0-16.6-3.2-22.8-9.6L707 186.1c-12.4-12.6-12.2-32.9 0.4-45.3 12.6-12.4 32.9-12.2 45.3 0.4l118.2 120.3c12.4 12.6 12.2 32.9-0.4 45.3-6.3 6.1-14.4 9.2-22.5 9.2z"
p-id="4937"></path> p-id="4937"></path>
</svg> </svg>-->
<!--<button <button
v-if="item.status === '启用'" v-if="item.status === '启用'"
class="btn btn-warning btn-small" class="btn btn-warning btn-small"
@click="handleToggleStatus(item)"> @click="handleToggleStatus(item)">
停用 停用
</button>--> </button>
<svg t="1767688450917" class="icon" viewBox="0 0 1024 1024" version="1.1" <!-- <svg t="1767688450917" class="icon" viewBox="0 0 1024 1024" version="1.1"
v-if="item.status === '启用'" @click="handleToggleStatus(item)" style="cursor: pointer;" v-if="item.status === '启用'" @click="handleToggleStatus(item)" style="cursor: pointer;"
xmlns="http://www.w3.org/2000/svg" p-id="5940" width="20" height="20"> xmlns="http://www.w3.org/2000/svg" p-id="5940" width="20" height="20">
<path d="M512 938.688A426.688 426.688 0 1 1 512 85.376a426.688 426.688 0 0 1 0 853.312z m0-85.376A341.312 341.312 0 1 0 512 170.688a341.312 341.312 0 0 0 0 682.624z m208.64-489.664l-356.992 357.056a257.728 257.728 0 0 1-60.352-60.352l357.056-357.056c23.296 16.64 43.712 37.056 60.352 60.352z" <path d="M512 938.688A426.688 426.688 0 1 1 512 85.376a426.688 426.688 0 0 1 0 853.312z m0-85.376A341.312 341.312 0 1 0 512 170.688a341.312 341.312 0 0 0 0 682.624z m208.64-489.664l-356.992 357.056a257.728 257.728 0 0 1-60.352-60.352l357.056-357.056c23.296 16.64 43.712 37.056 60.352 60.352z"
fill="#909399" p-id="5941"></path> fill="#909399" p-id="5941"></path>
</svg> </svg>-->
<!--<button <button
v-else v-else
class="btn btn-success btn-small" class="btn btn-success btn-small"
@click="handleToggleStatus(item)"> @click="handleToggleStatus(item)">
启用 启用
</button>--> </button>
<svg t="1767688500016" class="icon" viewBox="0 0 1024 1024" version="1.1" v-else @click="handleToggleStatus(item)" style="cursor: pointer;" <!--<svg t="1767688500016" class="icon" viewBox="0 0 1024 1024" version="1.1" v-else @click="handleToggleStatus(item)" style="cursor: pointer;"
xmlns="http://www.w3.org/2000/svg" p-id="7110" width="20" height="20"> xmlns="http://www.w3.org/2000/svg" p-id="7110" width="20" height="20">
<path d="M806.3 150.2c-5.4-5.4-14.3-8.9-25-7.1-19.6 0-35.7 16.1-35.7 35.7 0 8.9 3.6 17.8 10.7 25 89.2 73.1 146.3 183.7 146.3 306.8 0 217.6-174.8 392.4-392.4 392.4S117.8 728.1 117.8 510.5c0-123.1 55.3-231.9 142.7-303.2 8.9-7.1 16.1-17.8 16.1-30.3 0-19.6-16.1-35.7-35.7-35.7-8.9 0-16.1 3.6-23.2 8.9-103.5 85.6-169.5 214-169.5 358.5 0 256.9 206.9 463.8 463.8 463.8s463.8-206.9 463.8-463.8c0-144.4-66-272.9-169.5-358.5z" <path d="M806.3 150.2c-5.4-5.4-14.3-8.9-25-7.1-19.6 0-35.7 16.1-35.7 35.7 0 8.9 3.6 17.8 10.7 25 89.2 73.1 146.3 183.7 146.3 306.8 0 217.6-174.8 392.4-392.4 392.4S117.8 728.1 117.8 510.5c0-123.1 55.3-231.9 142.7-303.2 8.9-7.1 16.1-17.8 16.1-30.3 0-19.6-16.1-35.7-35.7-35.7-8.9 0-16.1 3.6-23.2 8.9-103.5 85.6-169.5 214-169.5 358.5 0 256.9 206.9 463.8 463.8 463.8s463.8-206.9 463.8-463.8c0-144.4-66-272.9-169.5-358.5z"
p-id="7111"></path> p-id="7111"></path>
<path d="M512 622.9c19.6 0 35.7-16.1 35.7-35.7V87.8c0-19.6-16.1-35.7-35.7-35.7s-35.7 16.1-35.7 35.7v499.4c0 19.6 16.1 35.7 35.7 35.7z" <path d="M512 622.9c19.6 0 35.7-16.1 35.7-35.7V87.8c0-19.6-16.1-35.7-35.7-35.7s-35.7 16.1-35.7 35.7v499.4c0 19.6 16.1 35.7 35.7 35.7z"
p-id="7112"></path> p-id="7112"></path>
</svg> </svg>-->
</div> </div>
</td> </td>
</tr> </tr>
......
<!DOCTYPE html> <!DOCTYPE html>
...@@ -1295,6 +1295,439 @@ ...@@ -1295,6 +1295,439 @@
.top-header { .top-header {
left: var(--sidebar-width); left: var(--sidebar-width);
} }
/* ===== 爬虫调度面板(新增) ===== */
.crawler-page {
/*background: linear-gradient(135deg, #0b1224, #0f172a);*/
/*min-height: calc(100vh - var(--header-height));*/
/*padding: 24px;*/
/*color: #e2e8f0;*/
}
.crawler-card {
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(226, 232, 240, 0.08);
border-radius: 16px;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
.crawler-card .card-header {
background: rgba(255, 255, 255, 0.02);
border-bottom-color: rgba(226, 232, 240, 0.1);
}
.crawler-card .card-title {
color: #e2e8f0;
display: flex;
align-items: center;
gap: 10px;
}
.crawler-card .card-title .dot {
width: 10px;
height: 10px;
background: #22c55e;
border-radius: 50%;
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.15);
}
.crawler-subtitle {
color: #94a3b8;
font-size: 13px;
margin-top: 4px;
}
.crawler-table {
width: 100%;
border-collapse: collapse;
}
.crawler-table th,
.crawler-table td {
border: none;
border-bottom: 1px solid rgba(226, 232, 240, 0.08);
}
.crawler-table th {
background: rgba(255, 255, 255, 0.02);
color: #94a3b8;
font-size: 12px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.crawler-table tbody tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.source-cell {
display: flex;
align-items: center;
gap: 12px;
color: #e2e8f0;
}
.source-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(148, 163, 184, 0.14);
display: grid;
place-items: center;
color: #94a3b8;
font-size: 16px;
}
.source-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
.source-title {
font-weight: 600;
color: #e2e8f0;
}
.source-url {
color: #94a3b8;
font-size: 12px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
color: #0f172a;
}
.badge.green { background: #22c55e; color: #052e16; }
.badge.blue { background: #38bdf8; color: #0b1c2c; }
.badge.orange { background: #f59e0b; color: #3f2a00; }
.next-run {
display: flex;
flex-direction: column;
gap: 4px;
color: #e2e8f0;
}
.next-run .sub {
font-size: 12px;
color: #94a3b8;
}
.status-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 10px;
font-weight: 600;
color: #e2e8f0;
background: rgba(226, 232, 240, 0.05);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot-green { background: #22c55e; box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.15); }
.dot-amber { background: #f59e0b; box-shadow: 0 0 0 6px rgba(245, 158, 11, 0.15); }
.dot-red { background: #ef4444; box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.15); }
.table-actions {
display: flex;
gap: 8px;
align-items: center;
}
.btn-ghost {
padding: 8px 14px;
border-radius: 10px;
border: 1px solid rgba(226, 232, 240, 0.14);
background: rgba(255, 255, 255, 0.03);
color: #e2e8f0;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-ghost:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(226, 232, 240, 0.25);
}
.btn-icon {
width: 38px;
height: 38px;
border-radius: 12px;
border: 1px solid rgba(226, 232, 240, 0.14);
background: rgba(255, 255, 255, 0.03);
color: #e2e8f0;
display: grid;
place-items: center;
cursor: pointer;
transition: all 0.2s;
}
.btn-icon:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(226, 232, 240, 0.25);
}
/* 右侧抽屉 */
.drawer {
position: fixed;
inset: 0;
display: none;
z-index: 2000;
}
.drawer.show { display: block; }
.drawer-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(2px);
}
.drawer-panel {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 480px;
max-width: 90vw;
background: #0b1224;
border-left: 1px solid rgba(226, 232, 240, 0.08);
box-shadow: -12px 0 48px rgba(0, 0, 0, 0.35);
transform: translateX(100%);
transition: transform 0.35s ease;
display: flex;
flex-direction: column;
}
.drawer.show .drawer-panel { transform: translateX(0); }
.drawer-header {
padding: 20px 24px 12px;
border-bottom: 1px solid rgba(226, 232, 240, 0.08);
}
.drawer-title {
color: #e2e8f0;
font-size: 18px;
font-weight: 700;
}
.drawer-sub {
color: #94a3b8;
font-size: 13px;
margin-top: 6px;
word-break: break-all;
}
.drawer-body {
padding: 18px 24px;
overflow-y: auto;
flex: 1;
}
.tab-switch {
display: inline-flex;
background: rgba(226, 232, 240, 0.06);
border: 1px solid rgba(226, 232, 240, 0.1);
border-radius: 12px;
overflow: hidden;
margin-bottom: 18px;
}
.tab-pill {
padding: 10px 16px;
color: #94a3b8;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.tab-pill.active {
background: rgba(56, 189, 248, 0.16);
color: #e0f2fe;
}
.form-block {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(226, 232, 240, 0.08);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 14px;
}
.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.form-row.single {
grid-template-columns: 1fr;
}
.form-label {
color: #cbd5e1;
font-size: 13px;
margin-bottom: 6px;
}
.input, .select, .textarea, .slider {
width: 100%;
background: rgba(15, 23, 42, 0.9);
color: #e2e8f0;
border: 1px solid rgba(226, 232, 240, 0.12);
border-radius: 10px;
padding: 10px 12px;
font-size: 14px;
}
.slider {
padding: 0;
height: 4px;
accent-color: #22c55e;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.toggle input { display: none; }
.toggle-pill {
width: 44px;
height: 24px;
background: rgba(226, 232, 240, 0.2);
border-radius: 999px;
position: relative;
transition: background 0.2s;
}
.toggle-pill::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
background: #e2e8f0;
top: 3px;
left: 4px;
transition: transform 0.2s;
}
.toggle input:checked + .toggle-pill {
background: #22c55e;
}
.toggle input:checked + .toggle-pill::after {
transform: translateX(18px);
background: #0b1224;
}
.risk-card {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.12), rgba(15, 23, 42, 0.9));
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 16px;
padding: 16px;
margin: 12px 0;
display: grid;
grid-template-columns: 160px 1fr;
gap: 14px;
align-items: center;
}
.gauge {
position: relative;
width: 140px;
height: 140px;
margin: 0 auto;
}
.gauge svg {
width: 100%;
height: 100%;
}
.needle {
transform-origin: 70px 70px;
transition: transform 0.3s ease;
}
.risk-info {
color: #e2e8f0;
}
.risk-title {
font-weight: 700;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.risk-desc {
color: #cbd5e1;
font-size: 13px;
line-height: 1.5;
}
.drawer-footer {
padding: 16px 24px;
border-top: 1px solid rgba(226, 232, 240, 0.08);
display: flex;
gap: 10px;
justify-content: flex-end;
background: rgba(11, 18, 36, 0.95);
}
.btn-primary-strong {
background: linear-gradient(90deg, #22c55e, #16a34a);
color: #0b1224;
border: none;
border-radius: 10px;
padding: 10px 16px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 10px 30px rgba(34, 197, 94, 0.35);
}
.btn-secondary-ghost {
background: rgba(255, 255, 255, 0.04);
color: #e2e8f0;
border: 1px solid rgba(226, 232, 240, 0.15);
border-radius: 10px;
padding: 10px 16px;
font-weight: 600;
cursor: pointer;
}
@media (max-width: 960px) {
.risk-card {
grid-template-columns: 1fr;
}
}
</style> </style>
</head> </head>
<body> <body>
...@@ -1325,12 +1758,143 @@ ...@@ -1325,12 +1758,143 @@
</header> </header>
<div class="main-content" style="position: relative;"> <div class="main-content" style="position: relative;">
<div class="page-header">
<div class="page-title-section">
<h1 class="page-title-main">爬虫网站管理</h1>
<p class="page-title-sub">管理网站爬虫收录与方案配置</p>
</div>
<button class="btn btn-primary" id="add-site-btn">
<i class="fas fa-plus"></i>
新增网站
</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-container">
<table id="crawler-sites-table">
<thead>
<tr>
<th>网站名称</th>
<th>网站网址</th>
<th>需求部门</th>
<th>是否收录</th>
<th>收录方案/不收录原因</th>
<th>爬取频率</th>
<th>上次爬取时间</th>
<th>爬取状态</th>
<th>目标数据类型</th>
<th>反爬机制</th>
<th>负责人</th>
<th>操作</th>
</tr>
</thead>
<tbody id="crawler-sites-tbody">
<!-- 动态数据将插入这里 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 新增/编辑模态框 -->
<div class="modal" id="crawler-site-modal">
<div class="modal-content add-modal-content">
<div class="modal-header">
<h3 class="modal-title" id="modal-title">新增网站</h3>
<button class="close-modal" id="close-site-modal">&times;</button>
</div>
<div class="modal-body">
<form class="add-modal-form" id="crawler-site-form">
<input type="hidden" id="site-id">
<div class="form-row">
<div class="form-group">
<label for="site-name">网站名称 *</label>
<input type="text" id="site-name" required placeholder="请输入网站名称">
</div>
<div class="form-group">
<label for="site-url">网站网址 *</label>
<input type="url" id="site-url" required placeholder="请输入完整网址,如 https://www.example.com">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="request-dept">需求部门 *</label>
<input type="text" id="request-dept" required placeholder="请输入需求部门">
</div>
<div class="form-group">
<label for="is-collected">是否收录 *</label>
<select id="is-collected" required>
<option value="">请选择</option>
<option value="是"></option>
<option value="否"></option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="crawl-frequency">爬取频率 *</label>
<select id="crawl-frequency" required>
<option value="">请选择</option>
<option value="每天一次">每天一次</option>
<option value="每小时一次">每小时一次</option>
<option value="每周一上午">每周一上午</option>
<option value="每30分钟">每30分钟</option>
<option value="每月1号">每月1号</option>
</select>
</div>
<div class="form-group">
<label for="crawl-status">爬取状态 *</label>
<select id="crawl-status" required>
<option value="">请选择</option>
<option value="正常">正常</option>
<option value="暂停">暂停</option>
<option value="失败">失败</option>
<option value="待审核">待审核</option>
<option value="已停用">已停用</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="target-data-type">目标数据类型 *</label>
<input type="text" id="target-data-type" required placeholder="如:新闻标题、价格、评论等">
</div>
<div class="form-group">
<label for="anti-crawl-mechanism">反爬机制</label>
<input type="text" id="anti-crawl-mechanism" placeholder="如:验证码、IP限制、动态加载等">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="负责人">负责人 *</label>
<input type="text" id="负责人" required placeholder="请输入负责人姓名">
</div>
<div class="form-group">
<label for="last-crawl-time">上次爬取时间</label>
<input type="datetime-local" id="last-crawl-time" placeholder="选择时间">
</div>
</div>
<div class="form-group">
<label for="collection-plan">收录方案/不收录原因 *</label>
<textarea id="collection-plan" required rows="4" placeholder="请输入收录方案或不收录的具体原因"></textarea>
</div>
<div class="form-group">
<label for="备注">备注</label>
<textarea id="备注" rows="3" placeholder="如:需登录后抓取、仅限工作日抓取等"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-site-btn">取消</button>
<button class="btn btn-primary" id="save-site-btn">保存</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</body> </body>
<script src="./js.js"></script> <script src="./js.js"></script>
......
<!DOCTYPE html> <!DOCTYPE html>
...@@ -1316,7 +1316,7 @@ ...@@ -1316,7 +1316,7 @@
} }
.container { .container {
max-width: 1400px; /*max-width: 1400px;*/
margin: 0 auto; margin: 0 auto;
} }
...@@ -1537,35 +1537,83 @@ ...@@ -1537,35 +1537,83 @@
.stats-bar { .stats-bar {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px; gap: 20px;
margin-bottom: 20px; margin-bottom: 25px;
} }
.stat-card { .stat-card {
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); background: white;
color: white; color: var(--text-primary);
padding: 20px; padding: 24px;
border-radius: 8px; border-radius: 12px;
text-align: center; text-align: center;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
} }
.stat-card.success { .stat-card.success {
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); border-left: 4px solid var(--success-color);
} }
.stat-card.warning { .stat-card.warning {
background: linear-gradient(135deg, #e6a23c 0%, #ebb563 100%); border-left: 4px solid var(--warning-color);
}
.stat-card.primary {
border-left: 4px solid var(--primary-color);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
font-size: 20px;
}
.stat-card.primary .stat-icon {
background: rgba(27, 100, 243, 0.1);
color: var(--primary-color);
}
.stat-card.success .stat-icon {
background: rgba(34, 197, 94, 0.1);
color: var(--success-color);
}
.stat-card.warning .stat-icon {
background: rgba(234, 179, 8, 0.1);
color: var(--warning-color);
}
.stat-card .stat-icon {
background: rgba(148, 163, 184, 0.1);
color: var(--text-tertiary);
} }
.stat-value { .stat-value {
font-size: 32px; font-size: 28px;
font-weight: bold; font-weight: 700;
margin: 10px 0; margin: 8px 0;
color: var(--text-primary);
} }
.stat-label { .stat-label {
font-size: 14px; font-size: 14px;
opacity: 0.9; color: var(--text-secondary);
font-weight: 500;
} }
</style> </style>
...@@ -1599,27 +1647,40 @@ ...@@ -1599,27 +1647,40 @@
<div class="main-content" style="position: relative;"> <div class="main-content" style="position: relative;">
<div id="app"> <div id="app">
<div class="container card" style="margin: 0;"> <!-- 统计卡片 -->
<div class="stats-bar">
<!-- 统计卡片 --> <div class="stat-card primary">
<div class="stats-bar"> <div class="stat-icon">
<div class="stat-card"> <i class="fas fa-file-alt"></i>
<div class="stat-label">总报价单数</div>
<div class="stat-value">{{ stats.total }}</div>
</div> </div>
<div class="stat-card success"> <div class="stat-label">总报价单数</div>
<div class="stat-label">有效报价单</div> <div class="stat-value">{{ stats.total }}</div>
<div class="stat-value">{{ stats.active }}</div> </div>
<div class="stat-card success">
<div class="stat-icon">
<i class="fas fa-check-circle"></i>
</div> </div>
<div class="stat-card warning"> <div class="stat-label">有效报价单</div>
<div class="stat-label">已过期</div> <div class="stat-value">{{ stats.active }}</div>
<div class="stat-value">{{ stats.expired }}</div> </div>
<div class="stat-card warning">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div> </div>
<div class="stat-card"> <div class="stat-label">已过期</div>
<div class="stat-label">总金额</div> <div class="stat-value">{{ stats.expired }}</div>
<div class="stat-value">¥{{ stats.totalAmount.toLocaleString() }}</div> </div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-yen-sign"></i>
</div> </div>
<div class="stat-label">总金额</div>
<div class="stat-value">¥{{ stats.totalAmount.toLocaleString() }}</div>
</div> </div>
</div>
<div class="container card" style="margin: 0;flex: 1;">
<div class=""> <div class="">
<div class="card-title"> <div class="card-title">
......
<!DOCTYPE html> <!DOCTYPE html>
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
} }
.container { .container {
max-width: 1200px; /*max-width: 1200px;*/
/*margin: 0 auto;*/ /*margin: 0 auto;*/
} }
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
background: white; background: white;
padding: 20px 30px; padding: 20px 30px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px; margin-bottom: 20px;
} }
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
background: white; background: white;
border-radius: 8px; border-radius: 8px;
padding: 30px; padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px; margin-bottom: 20px;
} }
...@@ -1552,7 +1552,7 @@ ...@@ -1552,7 +1552,7 @@
<div class="breadcrumb"> <div class="breadcrumb">
<span>首页</span> <span>首页</span>
<span class="breadcrumb-separator">/</span> <span class="breadcrumb-separator">/</span>
<span class="breadcrumb-current">设备档案</span> <span class="breadcrumb-current">报价管理</span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">
...@@ -1575,6 +1575,12 @@ ...@@ -1575,6 +1575,12 @@
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<div class="page-header">
<div>
<h1 class="page-title" data-i18n="title">报价管理</h1>
</div>
</div>
<form @submit.prevent="handleGenerate"> <form @submit.prevent="handleGenerate">
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
...@@ -1588,6 +1594,29 @@ ...@@ -1588,6 +1594,29 @@
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">客户所在地区 <span class="required">*</span></label>
<input
type="text"
v-model="form.customerRegion"
class="form-control"
placeholder="请输入客户所在地区"
required
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">币种 <span class="required">*</span></label>
<select v-model="form.currency" class="form-control" required>
<option value="">请选择币种</option>
<option value="CNY">人民币 (CNY)</option>
<option value="USD">美元 (USD)</option>
<option value="EUR">欧元 (EUR)</option>
<option value="JPY">日元 (JPY)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">是否停机 <span class="required">*</span></label> <label class="form-label">是否停机 <span class="required">*</span></label>
<select v-model="form.isDowntime" class="form-control" required @change="updateRule"> <select v-model="form.isDowntime" class="form-control" required @change="updateRule">
<option value="">请选择</option> <option value="">请选择</option>
...@@ -1597,6 +1626,46 @@ ...@@ -1597,6 +1626,46 @@
</div> </div>
</div> </div>
<div class="form-row">
<div class="form-group">
<label class="form-label">工厂</label>
<input
type="text"
v-model="form.factory"
class="form-control"
placeholder="请输入工厂名称"
/>
</div>
<div class="form-group">
<label class="form-label">工厂地区</label>
<select v-model="form.factoryRegion" class="form-control" required>
<option value="">请选择</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">工厂地址</label>
<input
type="text"
v-model="form.factoryAddress"
class="form-control"
placeholder="请输入工厂地址"
/>
</div>
<div class="form-group">
<label class="form-label">联系人</label>
<input
type="text"
v-model="form.contactPerson"
class="form-control"
placeholder="请输入联系人"
/>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label">服务内容描述 <span class="required">*</span></label> <label class="form-label">服务内容描述 <span class="required">*</span></label>
<textarea <textarea
...@@ -1646,21 +1715,29 @@ ...@@ -1646,21 +1715,29 @@
<tbody> <tbody>
<tr v-for="(item, index) in laborDetails" :key="index"> <tr v-for="(item, index) in laborDetails" :key="index">
<td> <td>
<select v-model="item.serviceType" class="form-control" @change="updateLaborInfo(index)"> <select v-model="item.serviceType" class="form-control"
@change="updateLaborInfo(index)">
<option value="">请选择服务类型</option> <option value="">请选择服务类型</option>
<option v-for="type in serviceTypes" :key="type" :value="type">{{ type }}</option> <option v-for="type in serviceTypes" :key="type" :value="type">{{ type }}
</option>
</select> </select>
</td> </td>
<td> <td>
<select v-model="item.techLevel" class="form-control" @change="updateLaborInfo(index)"> <select v-model="item.techLevel" class="form-control"
@change="updateLaborInfo(index)">
<option value="">请选择技术等级</option> <option value="">请选择技术等级</option>
<option v-for="level in techLevels" :key="level" :value="level">{{ level }}</option> <option v-for="level in techLevels" :key="level" :value="level">{{ level
}}
</option>
</select> </select>
</td> </td>
<td> <td>
<select v-model="item.billingMethod" class="form-control" @change="updateLaborInfo(index)"> <select v-model="item.billingMethod" class="form-control"
@change="updateLaborInfo(index)">
<option value="">请选择计费方式</option> <option value="">请选择计费方式</option>
<option v-for="method in billingMethods" :key="method" :value="method">{{ method }}</option> <option v-for="method in billingMethods" :key="method" :value="method">
{{ method }}
</option>
</select> </select>
</td> </td>
<td> <td>
...@@ -1673,17 +1750,25 @@ ...@@ -1673,17 +1750,25 @@
step="0.5" step="0.5"
@input="calculateTotal" @input="calculateTotal"
/> />
<span style="margin-left: 5px;">{{ item.billingMethod === '按小时' ? '小时' : (item.billingMethod === '按次' ? '次' : '') }}</span> <span style="margin-left: 5px;">{{ item.billingMethod === '按小时' ? '小时' : (item.billingMethod === '按次' ? '次' : '')
}}</span>
</td>
<td>¥{{ item.price
}}/{{ item.billingMethod === '按小时' ? '小时' : (item.billingMethod === '按次' ? '次' : '')
}}
</td> </td>
<td>¥{{ item.price }}/{{ item.billingMethod === '按小时' ? '小时' : (item.billingMethod === '按次' ? '次' : '') }}</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td> <td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
<td> <td>
<button type="button" class="btn btn-danger btn-small" @click="removeLabor(index)">删除</button> <button type="button" class="btn btn-danger btn-small"
@click="removeLabor(index)">删除
</button>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="7"> <td colspan="7">
<button type="button" class="btn btn-primary btn-small" @click="addLabor">+ 添加人工费用</button> <button type="button" class="btn btn-primary btn-small" @click="addLabor">+
添加人工费用
</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
...@@ -1707,7 +1792,8 @@ ...@@ -1707,7 +1792,8 @@
<tbody> <tbody>
<tr v-for="(item, index) in partDetails" :key="index"> <tr v-for="(item, index) in partDetails" :key="index">
<td> <td>
<select v-model="item.partId" class="form-control" @change="updatePartInfo(index)"> <select v-model="item.partId" class="form-control"
@change="updatePartInfo(index)">
<option value="">请选择零件</option> <option value="">请选择零件</option>
<option v-for="part in availableParts" :key="part.id" :value="part.id"> <option v-for="part in availableParts" :key="part.id" :value="part.id">
{{ part.partName }} {{ part.partName }}
...@@ -1728,12 +1814,16 @@ ...@@ -1728,12 +1814,16 @@
<td>¥{{ item.price }}</td> <td>¥{{ item.price }}</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td> <td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
<td> <td>
<button type="button" class="btn btn-danger btn-small" @click="removePart(index)">删除</button> <button type="button" class="btn btn-danger btn-small"
@click="removePart(index)">删除
</button>
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="6"> <td colspan="6">
<button type="button" class="btn btn-primary btn-small" @click="addPart">+ 添加零件</button> <button type="button" class="btn btn-primary btn-small" @click="addPart">+
添加零件
</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
...@@ -1770,33 +1860,39 @@ ...@@ -1770,33 +1860,39 @@
</div> </div>
<script> <script>
const { createApp } = Vue; const {createApp} = Vue;
createApp({ createApp({
data() { data() {
return { return {
form: { form: {
customerName: '', customerName: '',
customerRegion: '',
currency: '',
serviceDescription: '', serviceDescription: '',
isDowntime: '', isDowntime: '',
isOutOfWarranty: true isOutOfWarranty: true,
factory: '',
factoryRegion: '',
factoryAddress: '',
contactPerson: ''
}, },
matchedRule: null, matchedRule: null,
laborDetails: [], laborDetails: [],
partDetails: [], partDetails: [],
availableParts: [ availableParts: [
{ id: 1, partName: '服务器硬盘', specification: '2TB SATA', price: 800 }, {id: 1, partName: '服务器硬盘', specification: '2TB SATA', price: 800},
{ id: 2, partName: '内存条', specification: '16GB DDR4', price: 600 }, {id: 2, partName: '内存条', specification: '16GB DDR4', price: 600},
{ id: 3, partName: '网络交换机', specification: '24口千兆', price: 2000 } {id: 3, partName: '网络交换机', specification: '24口千兆', price: 2000}
], ],
serviceTypes: ['上门', '远程'], serviceTypes: ['上门', '远程'],
techLevels: ['普通', '高级'], techLevels: ['普通', '高级'],
billingMethods: ['按小时', '按次'], billingMethods: ['按小时', '按次'],
availableLaborStandards: [ availableLaborStandards: [
{ serviceType: '上门', techLevel: '普通', billingMethod: '按小时', price: 150 }, {serviceType: '上门', techLevel: '普通', billingMethod: '按小时', price: 150},
{ serviceType: '上门', techLevel: '高级', billingMethod: '按小时', price: 250 }, {serviceType: '上门', techLevel: '高级', billingMethod: '按小时', price: 250},
{ serviceType: '远程', techLevel: '普通', billingMethod: '按次', price: 200 }, {serviceType: '远程', techLevel: '普通', billingMethod: '按次', price: 200},
{ serviceType: '远程', techLevel: '高级', billingMethod: '按次', price: 300 } {serviceType: '远程', techLevel: '高级', billingMethod: '按次', price: 300}
], ],
availableRules: [ availableRules: [
{ {
...@@ -1804,8 +1900,14 @@ ...@@ -1804,8 +1900,14 @@
ruleName: '停机服务标准报价规则', ruleName: '停机服务标准报价规则',
scenario: '停机', scenario: '停机',
laborRules: [ laborRules: [
{ id: 1, serviceType: '上门', techLevel: '普通', billingMethod: '按小时', price: 150 }, {
{ id: 2, serviceType: '上门', techLevel: '高级', billingMethod: '按小时', price: 250 } id: 1,
serviceType: '上门',
techLevel: '普通',
billingMethod: '按小时',
price: 150
},
{id: 2, serviceType: '上门', techLevel: '高级', billingMethod: '按小时', price: 250}
] ]
}, },
{ {
...@@ -1813,7 +1915,7 @@ ...@@ -1813,7 +1915,7 @@
ruleName: '非停机服务标准报价规则', ruleName: '非停机服务标准报价规则',
scenario: '非停机', scenario: '非停机',
laborRules: [ laborRules: [
{ id: 3, serviceType: '远程', techLevel: '普通', billingMethod: '按次', price: 200 } {id: 3, serviceType: '远程', techLevel: '普通', billingMethod: '按次', price: 200}
] ]
} }
] ]
...@@ -1924,6 +2026,8 @@ ...@@ -1924,6 +2026,8 @@
const quoteData = { const quoteData = {
quoteNo: quoteNo, quoteNo: quoteNo,
customerName: this.form.customerName, customerName: this.form.customerName,
customerRegion: this.form.customerRegion,
currency: this.form.currency,
serviceDescription: this.form.serviceDescription, serviceDescription: this.form.serviceDescription,
quoteDate: new Date().toISOString().split('T')[0], quoteDate: new Date().toISOString().split('T')[0],
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
...@@ -1947,7 +2051,11 @@ ...@@ -1947,7 +2051,11 @@
ruleName: this.matchedRule.ruleName, ruleName: this.matchedRule.ruleName,
scenario: this.matchedRule.scenario, scenario: this.matchedRule.scenario,
whitebookVersion: 'V1.0', whitebookVersion: 'V1.0',
remark: '' remark: '',
factory: this.form.factory,
factoryRegion: this.form.factoryRegion,
factoryAddress: this.form.factoryAddress,
contactPerson: this.form.contactPerson
}; };
// 保存到localStorage // 保存到localStorage
......
<!DOCTYPE html> <!DOCTYPE html>
...@@ -1312,8 +1312,8 @@ ...@@ -1312,8 +1312,8 @@
} }
.container { .container {
max-width: 1200px; /*max-width: 1200px;*/
margin: 0 auto; /*margin: 0 auto;*/
} }
.header { .header {
...@@ -1551,7 +1551,7 @@ ...@@ -1551,7 +1551,7 @@
<div class="breadcrumb"> <div class="breadcrumb">
<span>首页</span> <span>首页</span>
<span class="breadcrumb-separator">/</span> <span class="breadcrumb-separator">/</span>
<span class="breadcrumb-current">设备档案</span> <span class="breadcrumb-current">报价单详情</span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">
...@@ -1574,6 +1574,11 @@ ...@@ -1574,6 +1574,11 @@
<div class="container" style="margin: 0;"> <div class="container" style="margin: 0;">
<div class="card"> <div class="card">
<div class="page-header">
<div>
<h1 class="page-title" data-i18n="title">报价单详情</h1>
</div>
</div>
<!-- 报价单头部信息 --> <!-- 报价单头部信息 -->
<div class="quote-header"> <div class="quote-header">
<div> <div>
...@@ -1586,6 +1591,14 @@ ...@@ -1586,6 +1591,14 @@
<div class="quote-info-value">{{ quoteInfo.customerName }}</div> <div class="quote-info-value">{{ quoteInfo.customerName }}</div>
</div> </div>
<div class="quote-info-item"> <div class="quote-info-item">
<div class="quote-info-label">客户所在地区</div>
<div class="quote-info-value">{{ quoteInfo.customerRegion }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">币种</div>
<div class="quote-info-value">{{ quoteInfo.currency }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">服务内容</div> <div class="quote-info-label">服务内容</div>
<div class="quote-info-value">{{ quoteInfo.serviceDescription }}</div> <div class="quote-info-value">{{ quoteInfo.serviceDescription }}</div>
</div> </div>
...@@ -1608,6 +1621,33 @@ ...@@ -1608,6 +1621,33 @@
</div> </div>
</div> </div>
<!-- 工厂信息 -->
<div class="detail-section">
<div class="detail-section-title">工厂信息</div>
<div class="quote-header">
<div>
<div class="quote-info-item">
<div class="quote-info-label">工厂</div>
<div class="quote-info-value">{{ quoteInfo.factory }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">工厂地区</div>
<div class="quote-info-value">{{ quoteInfo.factoryRegion }}</div>
</div>
</div>
<div>
<div class="quote-info-item">
<div class="quote-info-label">工厂地址</div>
<div class="quote-info-value">{{ quoteInfo.factoryAddress }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">联系人</div>
<div class="quote-info-value">{{ quoteInfo.contactPerson }}</div>
</div>
</div>
</div>
</div>
<!-- 人工费用明细 --> <!-- 人工费用明细 -->
<div class="detail-section" v-if="laborDetails.length > 0"> <div class="detail-section" v-if="laborDetails.length > 0">
<div class="detail-section-title">人工费用明细</div> <div class="detail-section-title">人工费用明细</div>
...@@ -1722,6 +1762,8 @@ ...@@ -1722,6 +1762,8 @@
quoteInfo: { quoteInfo: {
quoteNo: quoteNo, quoteNo: quoteNo,
customerName: '', customerName: '',
customerRegion: '',
currency: '',
serviceDescription: '', serviceDescription: '',
quoteDate: new Date().toLocaleDateString('zh-CN'), quoteDate: new Date().toLocaleDateString('zh-CN'),
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('zh-CN'), validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('zh-CN'),
...@@ -1729,7 +1771,11 @@ ...@@ -1729,7 +1771,11 @@
whitebookVersion: 'V1.0', whitebookVersion: 'V1.0',
ruleName: '', ruleName: '',
scenario: '', scenario: '',
remark: '' remark: '',
factory: '',
factoryRegion: '',
factoryAddress: '',
contactPerson: ''
}, },
laborDetails: [], laborDetails: [],
partDetails: [] partDetails: []
...@@ -1760,6 +1806,8 @@ ...@@ -1760,6 +1806,8 @@
this.quoteInfo = { this.quoteInfo = {
quoteNo: quote.quoteNo, quoteNo: quote.quoteNo,
customerName: quote.customerName, customerName: quote.customerName,
customerRegion: quote.customerRegion || '',
currency: quote.currency || '',
serviceDescription: quote.serviceDescription, serviceDescription: quote.serviceDescription,
quoteDate: quote.quoteDate, quoteDate: quote.quoteDate,
validUntil: quote.validUntil, validUntil: quote.validUntil,
...@@ -1767,7 +1815,11 @@ ...@@ -1767,7 +1815,11 @@
whitebookVersion: quote.whitebookVersion || 'V1.0', whitebookVersion: quote.whitebookVersion || 'V1.0',
ruleName: quote.ruleName || '', ruleName: quote.ruleName || '',
scenario: quote.scenario || '', scenario: quote.scenario || '',
remark: quote.remark || '本报价单有效期30天,最终价格以实际服务为准。' remark: quote.remark || '本报价单有效期30天,最终价格以实际服务为准。',
factory: quote.factory || '',
factoryRegion: quote.factoryRegion || '',
factoryAddress: quote.factoryAddress || '',
contactPerson: quote.contactPerson || ''
}; };
this.laborDetails = quote.laborDetails || []; this.laborDetails = quote.laborDetails || [];
this.partDetails = quote.partDetails || []; this.partDetails = quote.partDetails || [];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment