feat: 新增产品处理流程监控功能
- 新增处理会话表(process_sessions)和步骤表(process_steps) - 支持实时查看每个产品的处理步骤进度 - 支持暂停/继续/停止正在进行的处理 - 处理步骤包括:搜索内容库、搜索互联网、抓取网页、提取数据、填充字段、提交审核 - 新增处理监控页面 /process - 每个步骤记录详细数据、耗时、状态 - 历史记录可查看每个步骤的执行详情
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
/* 处理监控页面专用样式 */
|
||||
|
||||
.process-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.process-header {
|
||||
background: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.process-header h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 活动处理列表 */
|
||||
.active-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.active-process-item {
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f5f3ff 0%, #e0e7ff 100%);
|
||||
}
|
||||
|
||||
.active-process-item.paused {
|
||||
border-color: #f59e0b;
|
||||
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
||||
}
|
||||
|
||||
.process-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.process-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.process-meta {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.process-status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* 步骤进度条 */
|
||||
.steps-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-node::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: #e9ecef;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.step-node:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-node.completed .step-circle {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-node.running .step-circle {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.step-node.failed .step-circle {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-node.skipped .step-circle {
|
||||
background: #9ca3af;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.process-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* 步骤定义流程 */
|
||||
.steps-flow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
flex: 0 0 150px;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.step-card:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 历史表格 */
|
||||
.history-section .data-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.step-indicator .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.step-indicator .dot.completed {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.step-indicator .dot.running {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.step-indicator .dot.failed {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
/* 详情模态框 */
|
||||
.detail-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-step {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.detail-step-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-step-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-step-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-step-content {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-step-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.steps-flow {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.steps-progress {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// API基础地址
|
||||
const API_BASE = '';
|
||||
|
||||
// 自动刷新定时器
|
||||
let autoRefreshInterval = null;
|
||||
|
||||
// 页面加载
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadStepDefinitions();
|
||||
loadActiveProcesses();
|
||||
loadHistory();
|
||||
|
||||
// 启动自动刷新(每2秒)
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// 启动自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
|
||||
autoRefreshInterval = setInterval(() => {
|
||||
loadActiveProcesses();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 加载步骤定义
|
||||
async function loadStepDefinitions() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/steps`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayStepDefinitions(data.steps);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载步骤定义失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示步骤定义
|
||||
function displayStepDefinitions(steps) {
|
||||
const container = document.getElementById('steps-flow');
|
||||
|
||||
container.innerHTML = steps.map((step, i) => `
|
||||
<div class="step-card">
|
||||
<div class="step-number">${step.num}</div>
|
||||
<div class="step-title">${escapeHtml(step.name)}</div>
|
||||
<div class="step-desc">${escapeHtml(step.description)}</div>
|
||||
</div>
|
||||
${i < steps.length - 1 ? '<i class="ri-arrow-right-line" style="color: #ccc;"></i>' : ''}
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 加载活动处理
|
||||
async function loadActiveProcesses() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/active`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayActiveProcesses(data.sessions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载活动处理失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示活动处理
|
||||
function displayActiveProcesses(sessions) {
|
||||
const container = document.getElementById('active-list');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = '<div class="empty-text">暂无正在进行的处理</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sessions.map(item => {
|
||||
const session = item.session;
|
||||
const steps = item.steps || [];
|
||||
const isPaused = session.paused || session.status === 'paused';
|
||||
|
||||
return `
|
||||
<div class="active-process-item ${isPaused ? 'paused' : ''}" id="process-${session.session_id}">
|
||||
<div class="process-info">
|
||||
<div>
|
||||
<div class="process-name">${escapeHtml(session.product_name)}</div>
|
||||
<div class="process-meta">
|
||||
${session.category ? `分类: ${escapeHtml(session.category)} | ` : ''}
|
||||
会话ID: ${escapeHtml(session.session_id)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="process-status-badge status-${session.status}">
|
||||
${getStatusText(session.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤进度 -->
|
||||
<div class="steps-progress">
|
||||
${renderStepsProgress(steps, session.current_step)}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="process-actions">
|
||||
${session.status === 'running' ? `
|
||||
<button onclick="pauseProcess('${session.session_id}')" class="btn btn-warning btn-sm">
|
||||
<i class="ri-pause-line"></i> 暂停
|
||||
</button>
|
||||
` : ''}
|
||||
${session.status === 'paused' ? `
|
||||
<button onclick="resumeProcess('${session.session_id}')" class="btn btn-success btn-sm">
|
||||
<i class="ri-play-line"></i> 继续
|
||||
</button>
|
||||
` : ''}
|
||||
${session.status !== 'completed' ? `
|
||||
<button onclick="stopProcess('${session.session_id}')" class="btn btn-danger btn-sm">
|
||||
<i class="ri-stop-line"></i> 停止
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="showProcessDetail('${session.session_id}')" class="btn btn-secondary btn-sm">
|
||||
<i class="ri-eye-line"></i> 详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染步骤进度
|
||||
function renderStepsProgress(steps, currentStep) {
|
||||
const totalSteps = 6;
|
||||
const stepStatuses = {};
|
||||
|
||||
// 构建步骤状态映射
|
||||
steps.forEach(s => {
|
||||
stepStatuses[s.step_number] = s.step_status;
|
||||
});
|
||||
|
||||
const stepNames = ['搜索内容库', '搜索互联网', '抓取网页', '提取数据', '填充字段', '提交审核'];
|
||||
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalSteps; i++) {
|
||||
const status = stepStatuses[i] || (i > currentStep ? 'pending' : '');
|
||||
let className = '';
|
||||
|
||||
if (status === 'completed') className = 'completed';
|
||||
else if (status === 'running') className = 'running';
|
||||
else if (status === 'failed') className = 'failed';
|
||||
else if (status === 'skipped') className = 'skipped';
|
||||
|
||||
html += `
|
||||
<div class="step-node ${className}">
|
||||
<div class="step-circle">${i}</div>
|
||||
<div class="step-label">${stepNames[i-1]}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 加载历史
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/recent?limit=20`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayHistory(data.sessions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示历史
|
||||
function displayHistory(sessions) {
|
||||
const container = document.getElementById('history-table');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = '<tr><td colspan="6" class="empty-text">暂无处理历史</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sessions.map(session => `
|
||||
<tr>
|
||||
<td>${escapeHtml(session.product_name)}</td>
|
||||
<td>
|
||||
<span class="process-status-badge status-${session.status}">
|
||||
${getStatusText(session.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="step-indicator">
|
||||
<span class="dot ${session.status}"></span>
|
||||
步骤 ${session.current_step || 0}/6
|
||||
</div>
|
||||
</td>
|
||||
<td>${formatDate(session.started_at || session.created_at)}</td>
|
||||
<td>${formatDate(session.finished_at) || '-'}</td>
|
||||
<td>
|
||||
<button onclick="showProcessDetail('${session.session_id}')" class="btn btn-sm btn-secondary">
|
||||
<i class="ri-eye-line"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 暂停处理
|
||||
async function pauseProcess(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/pause`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('处理已暂停', 'success');
|
||||
loadActiveProcesses();
|
||||
} else {
|
||||
showToast('暂停失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('暂停失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 继续处理
|
||||
async function resumeProcess(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/resume`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('处理已继续', 'success');
|
||||
loadActiveProcesses();
|
||||
} else {
|
||||
showToast('继续失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('继续失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止处理
|
||||
async function stopProcess(sessionId) {
|
||||
if (!confirm('确定要停止处理吗?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/stop`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('处理已停止', 'success');
|
||||
loadActiveProcesses();
|
||||
loadHistory();
|
||||
} else {
|
||||
showToast('停止失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('停止失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示处理详情
|
||||
async function showProcessDetail(sessionId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/process/${sessionId}/status`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const session = data.data.session;
|
||||
const steps = data.data.steps;
|
||||
|
||||
document.getElementById('detail-title').innerHTML =
|
||||
`<i class="ri-file-list-3-line"></i> ${escapeHtml(session.product_name)} - 处理详情`;
|
||||
|
||||
const body = document.getElementById('detail-body');
|
||||
body.innerHTML = `
|
||||
<div class="detail-meta" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<div><strong>状态:</strong> ${getStatusText(session.status)}</div>
|
||||
<div><strong>分类:</strong> ${escapeHtml(session.category || '未分类')}</div>
|
||||
<div><strong>开始时间:</strong> ${formatDate(session.started_at) || '未开始'}</div>
|
||||
<div><strong>完成时间:</strong> ${formatDate(session.finished_at) || '-'}</div>
|
||||
${session.review_id ? `<div><strong>审核ID:</strong> ${escapeHtml(session.review_id)}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<h4 style="margin-bottom: 15px;"><i class="ri-list-check"></i> 处理步骤</h4>
|
||||
<div class="detail-steps">
|
||||
${steps.map(step => renderDetailStep(step)).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('process-detail-modal').classList.add('active');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('获取详情失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染详情步骤
|
||||
function renderDetailStep(step) {
|
||||
const statusColors = {
|
||||
'completed': '#10b981',
|
||||
'running': '#667eea',
|
||||
'failed': '#ef4444',
|
||||
'skipped': '#9ca3af',
|
||||
'pending': '#e9ecef'
|
||||
};
|
||||
|
||||
let stepDataHtml = '';
|
||||
if (step.step_data) {
|
||||
try {
|
||||
const data = typeof step.step_data === 'string' ? JSON.parse(step.step_data) : step.step_data;
|
||||
stepDataHtml = `<pre style="margin: 0; white-space: pre-wrap;">${escapeHtml(JSON.stringify(data, null, 2))}</pre>`;
|
||||
} catch (e) {
|
||||
stepDataHtml = escapeHtml(step.step_data);
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="detail-step">
|
||||
<div class="detail-step-header">
|
||||
<div class="detail-step-name">
|
||||
<span style="color: ${statusColors[step.step_status] || '#999'}; font-size: 18px;">●</span>
|
||||
步骤${step.step_number}: ${escapeHtml(step.step_name)}
|
||||
</div>
|
||||
<span class="detail-step-status" style="background: ${statusColors[step.step_status] || '#e9ecef'}; color: white;">
|
||||
${step.step_status}
|
||||
</span>
|
||||
</div>
|
||||
${stepDataHtml ? `<div class="detail-step-content">${stepDataHtml}</div>` : ''}
|
||||
${step.error_message ? `<div style="color: #ef4444; font-size: 13px;"><i class="ri-error-warning-line"></i> ${escapeHtml(step.error_message)}</div>` : ''}
|
||||
<div class="detail-step-time">
|
||||
${step.started_at ? `开始: ${formatDate(step.started_at)}` : ''}
|
||||
${step.finished_at ? ` | 完成: ${formatDate(step.finished_at)}` : ''}
|
||||
${step.duration_ms ? ` | 耗时: ${step.duration_ms}ms` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
function refreshData() {
|
||||
loadActiveProcesses();
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'running': '处理中',
|
||||
'paused': '已暂停',
|
||||
'completed': '已完成',
|
||||
'failed': '失败',
|
||||
'stopped': '已停止'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
function showToast(message, type = '') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast active ${type}`;
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('active');
|
||||
}, 3000);
|
||||
}
|
||||
Reference in New Issue
Block a user