This commit is contained in:
hangyu.tao 2026-01-28 14:27:34 +08:00
parent ceb0f74ccc
commit 97d5f51ff2
3 changed files with 819 additions and 45 deletions

View File

@ -2244,4 +2244,395 @@ tr:hover .action-cell {
.table-refreshing tbody tr {
animation: rowHighlight 0.4s ease-out;
}
/* ==================== Customer Tabs Styles ==================== */
.customer-tabs {
display: flex;
gap: 8px;
margin: 0;
flex: 1;
}
.customer-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--white);
color: var(--medium-gray);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.customer-tab:hover {
border-color: var(--primary-orange);
color: var(--primary-orange);
background: rgba(255, 107, 53, 0.05);
}
.customer-tab.active {
background: linear-gradient(135deg, var(--primary-orange), var(--secondary-orange));
color: var(--white);
border-color: transparent;
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
}
.customer-tab.active i {
color: var(--white);
}
.customer-tab i {
font-size: 0.9rem;
}
/* Tab Content */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ==================== Weekly Summary Styles ==================== */
.weekly-summary-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 15px;
padding: 15px 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
margin-bottom: 20px;
}
.week-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 600;
color: var(--dark-gray);
}
.week-info i {
font-size: 1.2rem;
color: var(--primary-orange);
}
.week-navigation {
display: flex;
align-items: center;
gap: 8px;
}
.week-nav-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--white);
color: var(--medium-gray);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.3s ease;
}
.week-nav-btn:hover {
border-color: var(--primary-orange);
color: var(--primary-orange);
background: rgba(255, 107, 53, 0.05);
}
.week-nav-btn:active {
transform: scale(0.98);
}
.weekly-summary-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.weekly-summary-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: var(--medium-gray);
}
.weekly-summary-empty i {
font-size: 4rem;
color: #ddd;
margin-bottom: 15px;
}
.weekly-summary-empty p {
font-size: 1rem;
}
/* Customer Summary Card */
.customer-summary-card {
background: var(--white);
border-radius: 12px;
border: 1px solid var(--border-color);
overflow: hidden;
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.customer-summary-card:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.customer-summary-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: var(--white);
}
.customer-summary-name {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.1rem;
font-weight: 600;
}
.customer-summary-name i {
color: var(--primary-orange);
}
.customer-summary-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: rgba(255, 107, 53, 0.2);
border-radius: 20px;
font-size: 0.8rem;
color: var(--primary-orange);
}
.customer-summary-body {
padding: 15px 20px;
}
.summary-section {
margin-bottom: 0;
}
.summary-section:last-child {
margin-bottom: 0;
}
.summary-section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
font-weight: 600;
color: var(--dark-gray);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border-color);
}
.summary-section-title i {
color: var(--primary-orange);
}
.summary-items {
display: flex;
flex-direction: column;
gap: 6px;
}
.summary-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--light-gray);
border-radius: 6px;
border-left: 3px solid var(--primary-orange);
min-height: auto;
}
.summary-item-date {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
min-width: auto;
padding: 4px 8px;
background: var(--white);
border-radius: 4px;
font-size: 0.75rem;
color: var(--medium-gray);
text-align: center;
white-space: nowrap;
}
.summary-item-date .day {
font-size: 0.9rem;
font-weight: 600;
color: var(--dark-gray);
}
.summary-item-content {
flex: 1;
font-size: 0.85rem;
color: var(--dark-gray);
line-height: 1.5;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.summary-item-content.combined-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.content-row {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 200px;
}
.content-label {
color: var(--primary-orange);
font-size: 0.9rem;
min-width: 18px;
}
.content-text {
flex: 1;
word-break: break-word;
}
.content-divider {
width: 1px;
height: 20px;
background: var(--border-color);
margin: 0 5px;
}
.summary-item-meta {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
font-size: 0.75rem;
color: var(--medium-gray);
flex-shrink: 0;
}
.summary-item-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.summary-item-meta i {
font-size: 0.75rem;
}
/* Customer Statistics */
.customer-summary-stats {
display: flex;
gap: 20px;
padding: 12px 20px;
background: rgba(255, 107, 53, 0.05);
border-top: 1px solid var(--border-color);
}
.stat-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
color: var(--medium-gray);
}
.stat-item i {
color: var(--primary-orange);
}
.stat-item strong {
color: var(--dark-gray);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.customer-tabs {
order: 3;
width: 100%;
margin: 15px 0 0 0;
justify-content: center;
}
.weekly-summary-header {
flex-direction: column;
align-items: stretch;
}
.week-info,
.week-navigation {
justify-content: center;
}
.customer-summary-header {
flex-direction: column;
gap: 10px;
text-align: center;
}
.customer-summary-stats {
flex-wrap: wrap;
justify-content: center;
}
.summary-item {
flex-direction: column;
}
.summary-item-date {
flex-direction: row;
gap: 8px;
min-width: auto;
}
}

View File

@ -141,7 +141,7 @@
<div class="action-bar">
<button id="addCustomerBtn" class="btn-primary">
<i class="fas fa-plus"></i>
添加客户
添加客户进度
</button>
<button id="importBtn" class="btn-secondary">
<i class="fas fa-file-import"></i>
@ -186,7 +186,15 @@
<!-- Customer List -->
<div class="card table-card">
<div class="card-header">
<h3><i class="fas fa-list"></i> 客户进度</h3>
<!-- Tab Navigation (Left Aligned) -->
<div class="customer-tabs">
<button class="customer-tab active" data-tab="progress-list" id="progressListTab">
<i class="fas fa-table"></i> 客户进度
</button>
<button class="customer-tab" data-tab="weekly-summary" id="weeklySummaryTab">
<i class="fas fa-file-alt"></i> 周报汇总
</button>
</div>
<div class="table-actions">
<button id="refreshCustomersBtn" class="icon-btn" title="刷新">
<i class="fas fa-sync-alt"></i>
@ -197,54 +205,88 @@
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="customerTable">
<thead>
<tr>
<th>客户</th>
<th>咨询时间</th>
<th>版本</th>
<th>描述</th>
<th>解决方案</th>
<th>类型</th>
<th>模块</th>
<th>状态与进度</th>
<th>操作</th>
</tr>
</thead>
<tbody id="customerTableBody">
</tbody>
</table>
<!-- Progress List View (Default) -->
<div id="progressListView" class="tab-content active">
<div class="table-responsive">
<table id="customerTable">
<thead>
<tr>
<th>客户</th>
<th>咨询时间</th>
<th>版本</th>
<th>描述</th>
<th>解决方案</th>
<th>类型</th>
<th>模块</th>
<th>状态与进度</th>
<th>操作</th>
</tr>
</thead>
<tbody id="customerTableBody">
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="pagination">
<div class="pagination-info">
<span id="paginationInfo">显示 0-0 共 0 条</span>
</div>
<div class="pagination-controls">
<button id="firstPage" class="pagination-btn" disabled>
<i class="fas fa-angle-double-left"></i>
</button>
<button id="prevPage" class="pagination-btn" disabled>
<i class="fas fa-angle-left"></i>
</button>
<div id="pageNumbers" class="page-numbers">
</div>
<button id="nextPage" class="pagination-btn" disabled>
<i class="fas fa-angle-right"></i>
</button>
<button id="lastPage" class="pagination-btn" disabled>
<i class="fas fa-angle-double-right"></i>
</button>
</div>
<div class="pagination-size">
<select id="pageSizeSelect">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
</div>
</div>
<!-- Pagination -->
<div class="pagination">
<div class="pagination-info">
<span id="paginationInfo">显示 0-0 共 0 条</span>
</div>
<div class="pagination-controls">
<button id="firstPage" class="pagination-btn" disabled>
<i class="fas fa-angle-double-left"></i>
</button>
<button id="prevPage" class="pagination-btn" disabled>
<i class="fas fa-angle-left"></i>
</button>
<div id="pageNumbers" class="page-numbers">
<!-- Weekly Summary View -->
<div id="weeklySummaryView" class="tab-content">
<div class="weekly-summary-header">
<div class="week-info">
<i class="fas fa-calendar-week"></i>
<span id="currentWeekRange">本周</span>
</div>
<button id="nextPage" class="pagination-btn" disabled>
<i class="fas fa-angle-right"></i>
</button>
<button id="lastPage" class="pagination-btn" disabled>
<i class="fas fa-angle-double-right"></i>
<div class="week-navigation">
<button id="prevWeekBtn" class="week-nav-btn" title="上一周">
<i class="fas fa-chevron-left"></i>
</button>
<button id="currentWeekBtn" class="week-nav-btn" title="回到本周">
<i class="fas fa-home"></i> 本周
</button>
<button id="nextWeekBtn" class="week-nav-btn" title="下一周">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<button id="exportWeeklySummaryBtn" class="btn-secondary" title="导出周报">
<i class="fas fa-file-export"></i> 导出周报
</button>
</div>
<div class="pagination-size">
<select id="pageSizeSelect">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
<div id="weeklySummaryContainer" class="weekly-summary-container">
<!-- Weekly summary cards will be rendered here -->
<div class="weekly-summary-empty">
<i class="fas fa-inbox"></i>
<p>本周暂无客户进度数据</p>
</div>
</div>
</div>
</div>

View File

@ -2413,4 +2413,345 @@ document.addEventListener('DOMContentLoaded', function () {
}
});
}
// ==========================================
// Customer Progress Tabs
// ==========================================
const progressListTab = document.getElementById('progressListTab');
const weeklySummaryTab = document.getElementById('weeklySummaryTab');
const progressListView = document.getElementById('progressListView');
const weeklySummaryView = document.getElementById('weeklySummaryView');
// Week offset for navigation (0 = current week, -1 = last week, etc.)
let weekOffset = 0;
// Tab switching
function switchCustomerTab(tabName) {
// Update tab buttons
document.querySelectorAll('.customer-tab').forEach(tab => {
tab.classList.remove('active');
});
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Get the table actions (refresh & export buttons)
const tableActions = document.querySelector('#customerSection .card-header .table-actions');
if (tabName === 'progress-list') {
progressListTab?.classList.add('active');
progressListView?.classList.add('active');
// Show refresh and export buttons for progress list
if (tableActions) tableActions.style.display = 'flex';
} else if (tabName === 'weekly-summary') {
weeklySummaryTab?.classList.add('active');
weeklySummaryView?.classList.add('active');
// Hide refresh and export buttons for weekly summary
if (tableActions) tableActions.style.display = 'none';
// Load weekly summary when switching to this tab
weekOffset = 0;
loadWeeklySummary();
}
}
// Tab click handlers
progressListTab?.addEventListener('click', () => switchCustomerTab('progress-list'));
weeklySummaryTab?.addEventListener('click', () => switchCustomerTab('weekly-summary'));
// Week navigation handlers
document.getElementById('prevWeekBtn')?.addEventListener('click', () => {
weekOffset--;
loadWeeklySummary();
});
document.getElementById('nextWeekBtn')?.addEventListener('click', () => {
weekOffset++;
loadWeeklySummary();
});
document.getElementById('currentWeekBtn')?.addEventListener('click', () => {
weekOffset = 0;
loadWeeklySummary();
});
// Get week date range
function getWeekRange(offset = 0) {
const now = new Date();
const currentDay = now.getDay();
const diff = currentDay === 0 ? 6 : currentDay - 1; // Adjust for Monday start
const monday = new Date(now);
monday.setDate(now.getDate() - diff + (offset * 7));
monday.setHours(0, 0, 0, 0);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
sunday.setHours(23, 59, 59, 999);
return { start: monday, end: sunday };
}
// Format date for display
function formatWeekDate(date) {
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}${day}`;
}
// Format date to YYYY-MM-DD for comparison
function formatDateForCompare(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Parse date string from customer data
function parseCustomerDate(dateStr) {
if (!dateStr) return null;
const cleaned = String(dateStr).trim().replace(/\./g, '-').replace(/\//g, '-');
const parts = cleaned.split('-').filter(Boolean);
if (parts.length < 3) return null;
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
}
// Load and render weekly summary
function loadWeeklySummary() {
const weekRange = getWeekRange(weekOffset);
// Update week range display
const weekRangeEl = document.getElementById('currentWeekRange');
if (weekRangeEl) {
const weekLabel = weekOffset === 0 ? '本周' : (weekOffset === -1 ? '上周' : `${Math.abs(weekOffset)}${weekOffset < 0 ? '前' : '后'}`);
weekRangeEl.textContent = `${weekLabel}: ${formatWeekDate(weekRange.start)} - ${formatWeekDate(weekRange.end)}`;
}
// Filter customers by week
const weekStart = formatDateForCompare(weekRange.start);
const weekEnd = formatDateForCompare(weekRange.end);
const weeklyCustomers = allCustomers.filter(customer => {
const customerDate = parseCustomerDate(customer.intendedProduct);
if (!customerDate) return false;
const dateStr = formatDateForCompare(customerDate);
return dateStr >= weekStart && dateStr <= weekEnd;
});
// Group by customer name
const customerGroups = {};
weeklyCustomers.forEach(customer => {
const name = customer.customerName || '未知客户';
if (!customerGroups[name]) {
customerGroups[name] = [];
}
customerGroups[name].push(customer);
});
// Render weekly summary
renderWeeklySummary(customerGroups);
}
// Render weekly summary cards
function renderWeeklySummary(customerGroups) {
const container = document.getElementById('weeklySummaryContainer');
if (!container) return;
container.innerHTML = '';
const customerNames = Object.keys(customerGroups);
if (customerNames.length === 0) {
container.innerHTML = `
<div class="weekly-summary-empty">
<i class="fas fa-inbox"></i>
<p>本周暂无客户进度数据</p>
</div>
`;
return;
}
// Sort customer names
customerNames.sort();
customerNames.forEach(customerName => {
const records = customerGroups[customerName];
const card = createCustomerSummaryCard(customerName, records);
container.appendChild(card);
});
}
// Create customer summary card
function createCustomerSummaryCard(customerName, records) {
const card = document.createElement('div');
card.className = 'customer-summary-card';
// Sort records by date
records.sort((a, b) => {
const dateA = parseCustomerDate(a.intendedProduct) || new Date(0);
const dateB = parseCustomerDate(b.intendedProduct) || new Date(0);
return dateB - dateA;
});
// Count statistics
const totalRecords = records.length;
// Get unique types and modules
const types = [...new Set(records.map(r => r.type).filter(Boolean))];
const modules = [...new Set(records.map(r => r.module).filter(Boolean))];
// Build combined items (description + solution in one row)
let combinedItems = '';
records.forEach(record => {
const date = parseCustomerDate(record.intendedProduct);
const dateDisplay = date ? formatItemDate(date) : '';
const description = record.description && record.description.trim() ? escapeHtml(record.description) : '-';
const solution = record.solution && record.solution.trim() ? escapeHtml(record.solution) : '-';
combinedItems += `
<div class="summary-item">
${dateDisplay}
<div class="summary-item-content combined-content">
<div class="content-row">
<span class="content-label"><i class="fas fa-clipboard-list"></i></span>
<span class="content-text">${description}</span>
</div>
<div class="content-divider"></div>
<div class="content-row">
<span class="content-label"><i class="fas fa-lightbulb"></i></span>
<span class="content-text">${solution}</span>
</div>
<div class="summary-item-meta">
${record.version ? `<span><i class="fas fa-code-branch"></i> ${escapeHtml(record.version)}</span>` : ''}
${record.type ? `<span><i class="fas fa-tag"></i> ${escapeHtml(record.type)}</span>` : ''}
${record.module ? `<span><i class="fas fa-puzzle-piece"></i> ${escapeHtml(record.module)}</span>` : ''}
${record.statusProgress ? `<span><i class="fas fa-tasks"></i> ${escapeHtml(record.statusProgress)}</span>` : ''}
</div>
</div>
</div>
`;
});
card.innerHTML = `
<div class="customer-summary-header">
<div class="customer-summary-name">
<i class="fas fa-building"></i>
<span>${escapeHtml(customerName)}</span>
</div>
<div class="customer-summary-badge">
<i class="fas fa-file-alt"></i>
<span>${totalRecords} 条记录</span>
</div>
</div>
<div class="customer-summary-body">
<div class="summary-items">
${combinedItems || '<div class="weekly-summary-empty" style="padding: 20px;"><p>暂无数据</p></div>'}
</div>
</div>
<div class="customer-summary-stats">
<div class="stat-item">
<i class="fas fa-tag"></i>
<span>类型: <strong>${types.join(', ') || '-'}</strong></span>
</div>
<div class="stat-item">
<i class="fas fa-puzzle-piece"></i>
<span>模块: <strong>${modules.join(', ') || '-'}</strong></span>
</div>
</div>
`;
return card;
}
// Format date for summary item
function formatItemDate(date) {
const dayNames = ['日', '一', '二', '三', '四', '五', '六'];
const month = date.getMonth() + 1;
const day = date.getDate();
const dayOfWeek = dayNames[date.getDay()];
return `
<div class="summary-item-date">
<span class="day">${month}/${day}</span>
<span>周${dayOfWeek}</span>
</div>
`;
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Export weekly summary
document.getElementById('exportWeeklySummaryBtn')?.addEventListener('click', () => {
const weekRange = getWeekRange(weekOffset);
const weekStart = formatDateForCompare(weekRange.start);
const weekEnd = formatDateForCompare(weekRange.end);
const weeklyCustomers = allCustomers.filter(customer => {
const customerDate = parseCustomerDate(customer.intendedProduct);
if (!customerDate) return false;
const dateStr = formatDateForCompare(customerDate);
return dateStr >= weekStart && dateStr <= weekEnd;
});
// Group by customer name
const customerGroups = {};
weeklyCustomers.forEach(customer => {
const name = customer.customerName || '未知客户';
if (!customerGroups[name]) {
customerGroups[name] = [];
}
customerGroups[name].push(customer);
});
// Build export content
let content = `周报汇总 (${formatWeekDate(weekRange.start)} - ${formatWeekDate(weekRange.end)})\n`;
content += '='.repeat(60) + '\n\n';
Object.keys(customerGroups).sort().forEach(customerName => {
const records = customerGroups[customerName];
content += `${customerName}\n`;
content += '-'.repeat(40) + '\n';
content += '\n▶ 问题描述:\n';
records.forEach(record => {
if (record.description && record.description.trim()) {
const date = parseCustomerDate(record.intendedProduct);
const dateStr = date ? `${date.getMonth() + 1}/${date.getDate()}` : '';
content += ` [${dateStr}] ${record.description}\n`;
if (record.version) content += ` 版本: ${record.version}\n`;
if (record.type) content += ` 类型: ${record.type}\n`;
if (record.statusProgress) content += ` 状态: ${record.statusProgress}\n`;
}
});
content += '\n▶ 解决方案:\n';
records.forEach(record => {
if (record.solution && record.solution.trim()) {
const date = parseCustomerDate(record.intendedProduct);
const dateStr = date ? `${date.getMonth() + 1}/${date.getDate()}` : '';
content += ` [${dateStr}] ${record.solution}\n`;
}
});
content += '\n';
});
// Download as text file
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `周报汇总_${formatDateForCompare(weekRange.start)}_${formatDateForCompare(weekRange.end)}.txt`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
});
});