549 lines
24 KiB
JavaScript
549 lines
24 KiB
JavaScript
/**
|
|
* 工单管理页面组件
|
|
*/
|
|
|
|
export default class WorkOrders {
|
|
constructor(container, route) {
|
|
this.container = container;
|
|
this.route = route;
|
|
this.currentPage = 1;
|
|
this.perPage = 20;
|
|
this.currentStatus = 'all';
|
|
this.searchQuery = '';
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
try {
|
|
this.render();
|
|
this.bindEvents();
|
|
await this.loadWorkOrders();
|
|
} catch (error) {
|
|
console.error('WorkOrders init error:', error);
|
|
this.showError(error);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
this.container.innerHTML = `
|
|
<div class="page-container">
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">工单管理</h1>
|
|
<p class="page-subtitle">工单列表与管理</p>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button class="btn btn-primary" id="create-workorder-btn">
|
|
<i class="fas fa-plus me-2"></i>新建工单
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-content">
|
|
|
|
<!-- 筛选和搜索 -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">状态筛选</label>
|
|
<select class="form-select" id="status-filter">
|
|
<option value="all">全部状态</option>
|
|
<option value="open">待处理</option>
|
|
<option value="in_progress">处理中</option>
|
|
<option value="resolved">已解决</option>
|
|
<option value="closed">已关闭</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">搜索</label>
|
|
<input type="text" class="form-control" id="search-input"
|
|
placeholder="搜索工单标题、描述或ID...">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button class="btn btn-outline-secondary w-100" id="search-btn">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 工单列表 -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0">工单列表</h5>
|
|
<div id="workorder-count" class="text-muted small">共 0 个工单</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover" id="workorders-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>标题</th>
|
|
<th>类别</th>
|
|
<th>优先级</th>
|
|
<th>状态</th>
|
|
<th>创建时间</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="workorders-tbody">
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 分页 -->
|
|
<nav id="pagination-nav" class="mt-4" style="display: none;">
|
|
<ul class="pagination justify-content-center" id="pagination-list">
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 工单详情模态框 -->
|
|
<div class="modal fade" id="workorder-detail-modal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-light">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-file-alt text-primary me-2"></i>工单详情
|
|
<span class="badge bg-secondary ms-2" id="modal-status-badge">加载中...</span>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="row g-0 h-100">
|
|
<!-- 左侧:基本信息 -->
|
|
<div class="col-md-7 border-end p-4">
|
|
<h3 id="modal-title" class="mb-3">工单标题</h3>
|
|
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-6 col-md-4">
|
|
<label class="form-label text-muted small">工单ID</label>
|
|
<div class="fw-bold" id="modal-id">-</div>
|
|
</div>
|
|
<div class="col-6 col-md-4">
|
|
<label class="form-label text-muted small">优先级</label>
|
|
<div id="modal-priority">-</div>
|
|
</div>
|
|
<div class="col-6 col-md-4">
|
|
<label class="form-label text-muted small">分类</label>
|
|
<div id="modal-category">-</div>
|
|
</div>
|
|
<div class="col-6 col-md-4">
|
|
<label class="form-label text-muted small">创建时间</label>
|
|
<div id="modal-created-at">-</div>
|
|
</div>
|
|
<div class="col-6 col-md-4">
|
|
<label class="form-label text-muted small">用户/VIN</label>
|
|
<div id="modal-user">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">问题描述</label>
|
|
<div class="bg-light p-3 rounded" id="modal-description" style="min-height: 80px;">
|
|
-
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold text-primary">
|
|
<i class="fas fa-robot me-1"></i>AI 智能分析与建议
|
|
</label>
|
|
<div class="card border-primary-lt">
|
|
<div class="card-body bg-azure-lt" id="modal-ai-analysis">
|
|
暂无 AI 分析
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧:对话历史/详细记录 -->
|
|
<div class="col-md-5 bg-light p-0 d-flex flex-column" style="height: 600px;">
|
|
<div class="p-3 border-bottom bg-white">
|
|
<h6 class="mb-0 fw-bold"><i class="fas fa-history me-2"></i>处理记录 / 对话历史</h6>
|
|
</div>
|
|
<div class="flex-grow-1 p-3 overflow-auto" id="modal-chat-history">
|
|
<!-- 聊天记录将动态插入这里 -->
|
|
<div class="text-center text-muted mt-5">
|
|
<i class="fas fa-comments fa-2x mb-3"></i>
|
|
<p>暂无相关对话记录</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
<button type="button" class="btn btn-primary" id="modal-edit-btn">编辑工单</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
bindEvents() {
|
|
// 状态筛选
|
|
document.getElementById('status-filter').addEventListener('change', () => {
|
|
this.currentStatus = document.getElementById('status-filter').value;
|
|
this.currentPage = 1;
|
|
this.loadWorkOrders();
|
|
});
|
|
|
|
// 搜索
|
|
document.getElementById('search-btn').addEventListener('click', () => {
|
|
this.searchQuery = document.getElementById('search-input').value.trim();
|
|
this.currentPage = 1;
|
|
this.loadWorkOrders();
|
|
});
|
|
|
|
document.getElementById('search-input').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
document.getElementById('search-btn').click();
|
|
}
|
|
});
|
|
|
|
// 新建工单
|
|
document.getElementById('create-workorder-btn').addEventListener('click', () => {
|
|
this.showCreateWorkOrderModal();
|
|
});
|
|
}
|
|
|
|
async loadWorkOrders() {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: this.currentPage,
|
|
per_page: this.perPage,
|
|
status: this.currentStatus,
|
|
search: this.searchQuery
|
|
});
|
|
|
|
const response = await fetch(`/api/workorders?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
this.renderWorkOrders(data.workorders || []);
|
|
this.renderPagination(data.pagination || {});
|
|
document.getElementById('workorder-count').textContent = `共 ${data.total || 0} 个工单`;
|
|
} else {
|
|
this.showErrorInTable(data.message || '加载工单失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('加载工单失败:', error);
|
|
this.showErrorInTable('网络错误');
|
|
}
|
|
}
|
|
|
|
renderWorkOrders(workorders) {
|
|
const tbody = document.getElementById('workorders-tbody');
|
|
|
|
if (workorders.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted">
|
|
<i class="fas fa-inbox me-2"></i>暂无工单
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = workorders.map(workorder => {
|
|
const statusBadge = this.getStatusBadge(workorder.status);
|
|
const priorityBadge = this.getPriorityBadge(workorder.priority);
|
|
const createTime = new Date(workorder.created_at).toLocaleString();
|
|
|
|
return `
|
|
<tr>
|
|
<td>${workorder.order_id || workorder.id}</td>
|
|
<td>
|
|
<div class="fw-bold">${workorder.title}</div>
|
|
<small class="text-muted">${workorder.description?.substring(0, 50) || ''}...</small>
|
|
</td>
|
|
<td><span class="badge bg-secondary">${workorder.category || '未分类'}</span></td>
|
|
<td>${priorityBadge}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${createTime}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="viewWorkOrder(${workorder.id})">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="editWorkOrder(${workorder.id})">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger" onclick="deleteWorkOrder(${workorder.id})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
renderPagination(pagination) {
|
|
const nav = document.getElementById('pagination-nav');
|
|
const list = document.getElementById('pagination-list');
|
|
|
|
if (!pagination || pagination.total_pages <= 1) {
|
|
nav.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
nav.style.display = 'block';
|
|
|
|
let html = '';
|
|
|
|
// 上一页
|
|
if (pagination.has_prev) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="changePage(${pagination.page - 1})">上一页</a></li>`;
|
|
}
|
|
|
|
// 页码
|
|
for (let i = Math.max(1, pagination.page - 2); i <= Math.min(pagination.total_pages, pagination.page + 2); i++) {
|
|
const activeClass = i === pagination.page ? 'active' : '';
|
|
html += `<li class="page-item ${activeClass}"><a class="page-link" href="#" onclick="changePage(${i})">${i}</a></li>`;
|
|
}
|
|
|
|
// 下一页
|
|
if (pagination.has_next) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="changePage(${pagination.page + 1})">下一页</a></li>`;
|
|
}
|
|
|
|
list.innerHTML = html;
|
|
}
|
|
|
|
getStatusBadge(status) {
|
|
const statusMap = {
|
|
'open': '<span class="badge bg-danger">待处理</span>',
|
|
'in_progress': '<span class="badge bg-warning">处理中</span>',
|
|
'resolved': '<span class="badge bg-success">已解决</span>',
|
|
'closed': '<span class="badge bg-secondary">已关闭</span>'
|
|
};
|
|
return statusMap[status] || `<span class="badge bg-light">${status}</span>`;
|
|
}
|
|
|
|
getPriorityBadge(priority) {
|
|
const priorityMap = {
|
|
'low': '<span class="badge bg-info">低</span>',
|
|
'medium': '<span class="badge bg-warning">中</span>',
|
|
'high': '<span class="badge bg-danger">高</span>',
|
|
'urgent': '<span class="badge bg-dark">紧急</span>'
|
|
};
|
|
return priorityMap[priority] || `<span class="badge bg-light">${priority}</span>`;
|
|
}
|
|
|
|
showErrorInTable(message) {
|
|
const tbody = document.getElementById('workorders-tbody');
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="text-center text-danger">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>${message}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
showCreateWorkOrderModal() {
|
|
// 这里应该显示创建工单的模态框
|
|
if (window.showToast) {
|
|
window.showToast('创建工单功能开发中', 'info');
|
|
}
|
|
}
|
|
|
|
showError(error) {
|
|
this.container.innerHTML = `
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body text-center">
|
|
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
|
|
<h4>页面加载失败</h4>
|
|
<p class="text-muted">${error.message || '未知错误'}</p>
|
|
<button class="btn btn-primary" onclick="location.reload()">
|
|
<i class="fas fa-redo me-2"></i>重新加载
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 全局函数供表格操作使用
|
|
window.viewWorkOrder = async function(id) {
|
|
try {
|
|
// 显示模态框(先显示加载状态)
|
|
const modalEl = document.getElementById('workorder-detail-modal');
|
|
const modal = new bootstrap.Modal(modalEl);
|
|
modal.show();
|
|
|
|
// 重置内容
|
|
document.getElementById('modal-status-badge').innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
document.getElementById('modal-ai-analysis').innerHTML = '<div class="spinner-border spinner-border-sm text-primary"></div> 正在分析...';
|
|
document.getElementById('modal-chat-history').innerHTML = '<div class="text-center mt-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
|
|
|
|
// 获取详情
|
|
const response = await fetch(`/api/workorders/${id}`);
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
alert('获取工单详情失败');
|
|
return;
|
|
}
|
|
|
|
const wo = result.workorder;
|
|
|
|
// 填充基本信息
|
|
document.getElementById('modal-title').textContent = wo.title;
|
|
document.getElementById('modal-id').textContent = wo.order_id || wo.id;
|
|
document.getElementById('modal-category').textContent = wo.category || '-';
|
|
document.getElementById('modal-created-at').textContent = new Date(wo.created_at).toLocaleString();
|
|
document.getElementById('modal-user').textContent = wo.user_id || '-';
|
|
document.getElementById('modal-description').textContent = wo.description || '无描述';
|
|
|
|
// 状态徽章
|
|
const statusMap = {
|
|
'open': '<span class="badge bg-danger">待处理</span>',
|
|
'in_progress': '<span class="badge bg-warning">处理中</span>',
|
|
'resolved': '<span class="badge bg-success">已解决</span>',
|
|
'closed': '<span class="badge bg-secondary">已关闭</span>'
|
|
};
|
|
document.getElementById('modal-status-badge').innerHTML = statusMap[wo.status] || wo.status;
|
|
|
|
// 优先级
|
|
const priorityMap = {
|
|
'low': '<span class="badge bg-info">低</span>',
|
|
'medium': '<span class="badge bg-warning">中</span>',
|
|
'high': '<span class="badge bg-danger">高</span>',
|
|
'urgent': '<span class="badge bg-dark">紧急</span>'
|
|
};
|
|
document.getElementById('modal-priority').innerHTML = priorityMap[wo.priority] || wo.priority;
|
|
|
|
// AI 分析/建议
|
|
if (wo.resolution) {
|
|
document.getElementById('modal-ai-analysis').innerHTML = `
|
|
<div style="white-space: pre-wrap; font-family: inherit;">${wo.resolution}</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('modal-ai-analysis').textContent = '暂无 AI 分析建议';
|
|
}
|
|
|
|
// 渲染对话/处理历史 (模拟数据或真实数据)
|
|
const historyContainer = document.getElementById('modal-chat-history');
|
|
// 这里假设后端返回的详情中包含 history 或 timeline
|
|
// 如果没有,暂时显示描述作为第一条记录
|
|
|
|
let historyHtml = '';
|
|
|
|
// 模拟一条初始记录
|
|
historyHtml += `
|
|
<div class="mb-3">
|
|
<div class="d-flex align-items-center mb-1">
|
|
<span class="badge bg-blue-lt me-2">用户</span>
|
|
<small class="text-muted">${new Date(wo.created_at).toLocaleString()}</small>
|
|
</div>
|
|
<div class="bg-white p-3 border rounded">
|
|
${wo.description}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (wo.timeline && wo.timeline.length > 0) {
|
|
// 如果有真实的时间轴数据
|
|
historyHtml = wo.timeline.map(item => `
|
|
<div class="mb-3">
|
|
<div class="d-flex align-items-center mb-1">
|
|
<span class="badge bg-${item.type === 'ai' ? 'purple-lt' : 'blue-lt'} me-2">
|
|
${item.author || (item.type === 'ai' ? 'AI 助手' : '系统')}
|
|
</span>
|
|
<small class="text-muted">${new Date(item.timestamp).toLocaleString()}</small>
|
|
</div>
|
|
<div class="bg-${item.type === 'ai' ? 'azure-lt' : 'white'} p-3 border rounded">
|
|
${item.content}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
historyContainer.innerHTML = historyHtml;
|
|
|
|
// 绑定编辑按钮
|
|
document.getElementById('modal-edit-btn').onclick = () => {
|
|
modal.hide();
|
|
editWorkOrder(id);
|
|
};
|
|
|
|
} catch (e) {
|
|
console.error('查看工单详情失败', e);
|
|
alert('查看详情失败: ' + e.message);
|
|
}
|
|
};
|
|
|
|
window.editWorkOrder = function(id) {
|
|
if (window.showToast) {
|
|
window.showToast(`编辑工单 ${id} 功能开发中`, 'info');
|
|
}
|
|
};
|
|
|
|
window.deleteWorkOrder = function(id) {
|
|
if (!confirm(`确定要删除工单 ${id} 吗?`)) {
|
|
return;
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
const response = await fetch(`/api/workorders/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (response.ok && result.success) {
|
|
if (window.showToast) {
|
|
window.showToast(result.message || `工单 ${id} 删除成功`, 'success');
|
|
}
|
|
// 通知当前页面刷新工单列表(保持当前页)
|
|
const event = new CustomEvent('workorder-deleted', { detail: { id } });
|
|
document.dispatchEvent(event);
|
|
} else {
|
|
if (window.showToast) {
|
|
window.showToast(result.message || '删除工单失败', 'error');
|
|
} else {
|
|
alert(result.message || '删除工单失败');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('删除工单失败:', error);
|
|
if (window.showToast) {
|
|
window.showToast('删除失败,请检查网络或查看控制台日志', 'error');
|
|
} else {
|
|
alert('删除失败,请检查网络或查看控制台日志');
|
|
}
|
|
}
|
|
})();
|
|
};
|
|
|
|
window.changePage = function(page) {
|
|
// 重新加载当前页面实例
|
|
const event = new CustomEvent('changePage', { detail: { page } });
|
|
document.dispatchEvent(event);
|
|
};
|
|
|
|
// 监听删除事件,触发当前 WorkOrders 列表刷新(保持当前页)
|
|
document.addEventListener('workorder-deleted', () => {
|
|
const event = new CustomEvent('reloadWorkOrders');
|
|
document.dispatchEvent(event);
|
|
}); |