feat_data

This commit is contained in:
hangyu.tao 2026-01-28 20:48:50 +08:00
parent 36141679d3
commit c1d1be8e27
9 changed files with 537 additions and 108 deletions

View File

@ -615,6 +615,51 @@ body {
margin-top: 20px; margin-top: 20px;
} }
/* Checkbox Group Styles */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 10px 0;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 14px;
border: 1px solid var(--border-color);
border-radius: 6px;
transition: all 0.2s ease;
background-color: var(--white);
font-size: 0.9rem;
}
.checkbox-label:hover {
border-color: var(--primary-orange);
background-color: rgba(255, 107, 53, 0.05);
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--primary-orange);
cursor: pointer;
}
.checkbox-label input[type="checkbox"]:checked+span {
color: var(--primary-orange);
font-weight: 500;
}
.checkbox-label:has(input[type="checkbox"]:checked) {
border-color: var(--primary-orange);
background-color: rgba(255, 107, 53, 0.1);
}
/* File Upload */ /* File Upload */
.file-upload { .file-upload {
position: relative; position: relative;

View File

@ -298,7 +298,7 @@
<div class="section-title" style="justify-content: flex-end;"> <div class="section-title" style="justify-content: flex-end;">
<button id="addTrialBtn" class="btn-primary"> <button id="addTrialBtn" class="btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
添加试用时间 添加客户
</button> </button>
</div> </div>
@ -328,6 +328,7 @@
<tr> <tr>
<th>客户名称</th> <th>客户名称</th>
<th>来源</th> <th>来源</th>
<th>意向产品</th>
<th>状态</th> <th>状态</th>
<th>开始时间</th> <th>开始时间</th>
<th>结束时间</th> <th>结束时间</th>
@ -438,8 +439,6 @@
<option value="type">类型</option> <option value="type">类型</option>
<option value="module">模块</option> <option value="module">模块</option>
</select> </select>
<input type="text" id="statusChartTitle" class="chart-title-input"
placeholder="自定义标题" value="数据分布">
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -455,8 +454,6 @@
<option value="type">类型</option> <option value="type">类型</option>
<option value="module">模块</option> <option value="module">模块</option>
</select> </select>
<input type="text" id="typeChartTitle" class="chart-title-input" placeholder="自定义标题"
value="客户类型">
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -681,7 +678,10 @@
<option value="模型工坊">模型工坊</option> <option value="模型工坊">模型工坊</option>
<option value="模型广场">模型广场</option> <option value="模型广场">模型广场</option>
<option value="镜像管理">镜像管理</option> <option value="镜像管理">镜像管理</option>
<option value="其他">其他</option>
</select> </select>
<input type="text" id="createModuleOther" name="moduleOther" placeholder="请输入其他模块名称"
style="display: none; margin-top: 8px;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="createStatusProgress">状态与进度</label> <label for="createStatusProgress">状态与进度</label>
@ -792,7 +792,10 @@
<option value="模型工坊">模型工坊</option> <option value="模型工坊">模型工坊</option>
<option value="模型广场">模型广场</option> <option value="模型广场">模型广场</option>
<option value="镜像管理">镜像管理</option> <option value="镜像管理">镜像管理</option>
<option value="其他">其他</option>
</select> </select>
<input type="text" id="editModuleOther" name="moduleOther" placeholder="请输入其他模块名称"
style="display: none; margin-top: 8px;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="editStatusProgress">状态与进度</label> <label for="editStatusProgress">状态与进度</label>
@ -825,7 +828,7 @@
<div id="addTrialPeriodModal" class="modal"> <div id="addTrialPeriodModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3><i class="fas fa-clock"></i> 添加试用时间</h3> <h3><i class="fas fa-user-plus"></i> 添加客户</h3>
<span class="close">&times;</span> <span class="close">&times;</span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -838,6 +841,26 @@
<label for="trialCustomerSource">客户来源</label> <label for="trialCustomerSource">客户来源</label>
<input type="text" id="trialCustomerSource" name="source" placeholder="请输入客户来源(如:官网、展会、推荐等)"> <input type="text" id="trialCustomerSource" name="source" placeholder="请输入客户来源(如:官网、展会、推荐等)">
</div> </div>
<div class="form-group">
<label>意向产品(可多选)</label>
<div class="checkbox-group" id="trialIntendedProductGroup">
<label class="checkbox-label">
<input type="checkbox" name="intendedProduct" value="数据闭环">
<span>数据闭环</span>
</label>
<label class="checkbox-label">
<input type="checkbox" name="intendedProduct" value="robogo">
<span>robogo</span>
</label>
<label class="checkbox-label">
<input type="checkbox" name="intendedProduct" value="其他"
id="trialIntendedProductOtherCheckbox">
<span>其他</span>
</label>
</div>
<input type="text" id="trialIntendedProductOther" name="intendedProductOther"
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px;">
</div>
<div class="form-group"> <div class="form-group">
<label>是否试用</label> <label>是否试用</label>
<div style="display: flex; gap: 20px; align-items: center;"> <div style="display: flex; gap: 20px; align-items: center;">
@ -888,6 +911,26 @@
<label for="editTrialCustomerSource">客户来源</label> <label for="editTrialCustomerSource">客户来源</label>
<input type="text" id="editTrialCustomerSource" name="source" placeholder="请输入客户来源:如社区、销售、推荐等"> <input type="text" id="editTrialCustomerSource" name="source" placeholder="请输入客户来源:如社区、销售、推荐等">
</div> </div>
<div class="form-group">
<label>意向产品(可多选)</label>
<div class="checkbox-group" id="editTrialIntendedProductGroup">
<label class="checkbox-label">
<input type="checkbox" name="editIntendedProduct" value="数据闭环">
<span>数据闭环</span>
</label>
<label class="checkbox-label">
<input type="checkbox" name="editIntendedProduct" value="robogo">
<span>robogo</span>
</label>
<label class="checkbox-label">
<input type="checkbox" name="editIntendedProduct" value="其他"
id="editTrialIntendedProductOtherCheckbox">
<span>其他</span>
</label>
</div>
<input type="text" id="editTrialIntendedProductOther" name="intendedProductOther"
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px;">
</div>
<div class="form-group"> <div class="form-group">
<label>是否试用</label> <label>是否试用</label>
<div style="display: flex; gap: 20px; align-items: center;"> <div style="display: flex; gap: 20px; align-items: center;">
@ -1044,9 +1087,9 @@
</div> </div>
<script src="/static/js/trial-periods.js?v=1.2"></script> <script src="/static/js/trial-periods.js?v=1.3"></script>
<script src="/static/js/trial-periods-page.js?v=1.2"></script> <script src="/static/js/trial-periods-page.js?v=1.3"></script>
<script src="/static/js/main.js?v=1.2"></script> <script src="/static/js/main.js?v=1.4"></script>
</body> </body>
</html> </html>

View File

@ -228,6 +228,72 @@ document.addEventListener('DOMContentLoaded', function () {
bindCellTooltipEvents(); bindCellTooltipEvents();
// Setup module dropdown with "other" option handling
function setupModuleDropdown(selectId, otherInputId) {
const select = document.getElementById(selectId);
const otherInput = document.getElementById(otherInputId);
if (!select || !otherInput) return;
select.addEventListener('change', function () {
if (this.value === '其他') {
otherInput.style.display = 'block';
otherInput.focus();
} else {
otherInput.style.display = 'none';
otherInput.value = '';
}
});
}
// Get module value (handles "other" option)
function getModuleValue(selectId, otherInputId) {
const select = document.getElementById(selectId);
const otherInput = document.getElementById(otherInputId);
if (!select) return '';
if (select.value === '其他' && otherInput && otherInput.value.trim()) {
return otherInput.value.trim();
}
return select.value;
}
// Set module value in form (handles "other" option)
function setModuleValue(selectId, otherInputId, value) {
const select = document.getElementById(selectId);
const otherInput = document.getElementById(otherInputId);
if (!select) return;
// Check if value matches one of the predefined options
const predefinedOptions = ['数据生成', '数据集', '数据空间', '模型工坊', '模型广场', '镜像管理', ''];
if (predefinedOptions.includes(value)) {
select.value = value;
if (otherInput) {
otherInput.style.display = 'none';
otherInput.value = '';
}
} else if (value) {
// Custom value - select "other" and fill input
select.value = '其他';
if (otherInput) {
otherInput.style.display = 'block';
otherInput.value = value;
}
} else {
select.value = '';
if (otherInput) {
otherInput.style.display = 'none';
otherInput.value = '';
}
}
}
// Initialize module dropdowns
setupModuleDropdown('createModule', 'createModuleOther');
setupModuleDropdown('editModule', 'editModuleOther');
function normalizeDateValue(dateValue) { function normalizeDateValue(dateValue) {
const raw = String(dateValue || '').trim(); const raw = String(dateValue || '').trim();
if (!raw) return ''; if (!raw) return '';
@ -932,7 +998,7 @@ document.addEventListener('DOMContentLoaded', function () {
description: document.getElementById('createDescription').value, description: document.getElementById('createDescription').value,
solution: document.getElementById('createSolution').value, solution: document.getElementById('createSolution').value,
type: document.getElementById('createType').value, type: document.getElementById('createType').value,
module: document.getElementById('createModule').value, module: getModuleValue('createModule', 'createModuleOther'),
statusProgress: document.getElementById('createStatusProgress').value, statusProgress: document.getElementById('createStatusProgress').value,
reporter: '' // Trial periods managed separately reporter: '' // Trial periods managed separately
}; };
@ -948,9 +1014,11 @@ document.addEventListener('DOMContentLoaded', function () {
if (response.ok) { if (response.ok) {
createCustomerForm.reset(); createCustomerForm.reset();
// Hide module "other" input
const createModuleOther = document.getElementById('createModuleOther');
if (createModuleOther) createModuleOther.style.display = 'none';
createModal.style.display = 'none'; createModal.style.display = 'none';
loadCustomers(); loadCustomers();
alert('客户创建成功!');
} else { } else {
alert('创建客户时出错'); alert('创建客户时出错');
} }
@ -1010,7 +1078,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('editDescription').value = customer.description || ''; document.getElementById('editDescription').value = customer.description || '';
document.getElementById('editSolution').value = customer.solution || ''; document.getElementById('editSolution').value = customer.solution || '';
document.getElementById('editType').value = customer.type || ''; document.getElementById('editType').value = customer.type || '';
document.getElementById('editModule').value = customer.module || ''; setModuleValue('editModule', 'editModuleOther', customer.module || '');
document.getElementById('editStatusProgress').value = customer.statusProgress || ''; document.getElementById('editStatusProgress').value = customer.statusProgress || '';
// Parse trial period and fill datetime inputs // Parse trial period and fill datetime inputs
@ -1071,7 +1139,7 @@ document.addEventListener('DOMContentLoaded', function () {
description: document.getElementById('editDescription').value, description: document.getElementById('editDescription').value,
solution: document.getElementById('editSolution').value, solution: document.getElementById('editSolution').value,
type: document.getElementById('editType').value, type: document.getElementById('editType').value,
module: document.getElementById('editModule').value, module: getModuleValue('editModule', 'editModuleOther'),
statusProgress: document.getElementById('editStatusProgress').value, statusProgress: document.getElementById('editStatusProgress').value,
reporter: '' // Trial periods managed separately reporter: '' // Trial periods managed separately
}; };
@ -1088,7 +1156,6 @@ document.addEventListener('DOMContentLoaded', function () {
if (response.ok) { if (response.ok) {
editModal.style.display = 'none'; editModal.style.display = 'none';
loadCustomers(); loadCustomers();
alert('客户更新成功!');
} else { } else {
alert('更新客户时出错'); alert('更新客户时出错');
} }
@ -1111,7 +1178,6 @@ document.addEventListener('DOMContentLoaded', function () {
if (response.ok) { if (response.ok) {
loadCustomers(); loadCustomers();
alert('客户删除成功!');
} else { } else {
alert('删除客户时出错'); alert('删除客户时出错');
} }
@ -1817,7 +1883,6 @@ document.addEventListener('DOMContentLoaded', function () {
followupForm.reset(); followupForm.reset();
followupFormCard.style.display = 'none'; followupFormCard.style.display = 'none';
loadFollowUps(); loadFollowUps();
alert('跟进记录创建成功!');
} else { } else {
alert('创建跟进记录时出错'); alert('创建跟进记录时出错');
} }
@ -1928,7 +1993,6 @@ document.addEventListener('DOMContentLoaded', function () {
if (response.ok) { if (response.ok) {
loadFollowUps(); loadFollowUps();
alert('跟进记录删除成功!');
} else { } else {
alert('删除跟进记录时出错'); alert('删除跟进记录时出错');
} }
@ -1971,7 +2035,6 @@ document.addEventListener('DOMContentLoaded', function () {
addFollowupModalForm.reset(); addFollowupModalForm.reset();
addFollowupModal.style.display = 'none'; addFollowupModal.style.display = 'none';
loadFollowUps(); loadFollowUps();
alert('跟进记录创建成功!');
} else { } else {
alert('创建跟进记录时出错'); alert('创建跟进记录时出错');
} }
@ -2049,7 +2112,6 @@ document.addEventListener('DOMContentLoaded', function () {
editFollowupForm.reset(); editFollowupForm.reset();
editFollowupModal.style.display = 'none'; editFollowupModal.style.display = 'none';
loadFollowUps(); loadFollowUps();
alert('跟进记录更新成功!');
} else { } else {
alert('更新跟进记录时出错'); alert('更新跟进记录时出错');
} }
@ -2222,69 +2284,121 @@ document.addEventListener('DOMContentLoaded', function () {
const results = []; const results = [];
// Search in trial periods // Search in trial periods
const trialResponse = await authenticatedFetch('/api/trial-periods/all'); try {
const trialData = await trialResponse.json(); const trialResponse = await authenticatedFetch('/api/trial-periods/all');
const trialPeriods = trialData.trialPeriods || []; if (trialResponse.ok) {
const trialData = await trialResponse.json();
const trialPeriods = trialData.trialPeriods || [];
// Get customers map // Get customers map
const customersResponse = await authenticatedFetch('/api/customers/list'); const customersResponse = await authenticatedFetch('/api/customers/list');
const customersData = await customersResponse.json(); const customersData = customersResponse.ok ? await customersResponse.json() : {};
const customersMap = customersData.customerMap || {}; const customersMap = customersData.customerMap || {};
// Search trial periods // Search trial periods
trialPeriods.forEach(period => { trialPeriods.forEach(period => {
const customerName = customersMap[period.customerId] || period.customerId; const customerName = customersMap[period.customerId] || period.customerId || '';
if (customerName.toLowerCase().includes(query.toLowerCase())) { if (customerName && customerName.toLowerCase().includes(query.toLowerCase())) {
results.push({ results.push({
type: 'trial', type: 'trial',
title: customerName, title: customerName,
meta: `试用时间: ${period.startTime ? period.startTime.split('T')[0] : ''} ~ ${period.endTime ? period.endTime.split('T')[0] : ''}`, meta: `试用时间: ${period.startTime ? period.startTime.split('T')[0] : ''} ~ ${period.endTime ? period.endTime.split('T')[0] : ''}`,
icon: 'fas fa-clock', icon: 'fas fa-clock',
section: 'trialPeriods' section: 'trialPeriods'
});
}
}); });
} }
}); } catch (trialError) {
console.warn('Trial periods search error:', trialError);
}
// Search in customers (weekly progress) // Search in customers (weekly progress) - use local allCustomers if available
const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100'); try {
const progressData = await progressResponse.json(); if (typeof allCustomers !== 'undefined' && allCustomers.length > 0) {
const customers = progressData.customers || []; allCustomers.forEach(customer => {
const searchFields = [
customers.forEach(customer => { customer.customerName || '',
const searchFields = [customer.customerName, customer.description, customer.solution, customer.module].join(' ').toLowerCase(); customer.description || '',
if (searchFields.includes(query.toLowerCase())) { customer.solution || '',
results.push({ customer.module || ''
type: 'progress', ].join(' ').toLowerCase();
title: customer.customerName, if (searchFields.includes(query.toLowerCase())) {
meta: customer.description ? customer.description.substring(0, 50) + '...' : '', results.push({
icon: 'fas fa-user-plus', type: 'progress',
section: 'customer' title: customer.customerName || '未知客户',
meta: customer.description ? customer.description.substring(0, 50) + '...' : '',
icon: 'fas fa-user-plus',
section: 'customer'
});
}
}); });
} else {
const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100');
if (progressResponse.ok) {
const progressData = await progressResponse.json();
const customers = progressData.customers || [];
customers.forEach(customer => {
const searchFields = [
customer.customerName || '',
customer.description || '',
customer.solution || '',
customer.module || ''
].join(' ').toLowerCase();
if (searchFields.includes(query.toLowerCase())) {
results.push({
type: 'progress',
title: customer.customerName || '未知客户',
meta: customer.description ? customer.description.substring(0, 50) + '...' : '',
icon: 'fas fa-user-plus',
section: 'customer'
});
}
});
}
} }
}); } catch (progressError) {
console.warn('Progress search error:', progressError);
}
// Search in followups // Search in followups
const followupResponse = await authenticatedFetch('/api/followups'); try {
const followupData = await followupResponse.json(); const followupResponse = await authenticatedFetch('/api/followups');
const followups = followupData.followups || []; if (followupResponse.ok) {
const followupData = await followupResponse.json();
const followups = followupData.followups || [];
followups.forEach(followup => { followups.forEach(followup => {
if (followup.customerName.toLowerCase().includes(query.toLowerCase())) { if (followup.customerName && followup.customerName.toLowerCase().includes(query.toLowerCase())) {
results.push({ results.push({
type: 'followup', type: 'followup',
title: followup.customerName, title: followup.customerName,
meta: `状态: ${followup.dealStatus || '未知'} | 级别: ${followup.customerLevel || '未知'}`, meta: `状态: ${followup.dealStatus || '未知'} | 级别: ${followup.customerLevel || '未知'}`,
icon: 'fas fa-tasks', icon: 'fas fa-tasks',
section: 'followup' section: 'followup'
});
}
}); });
} }
}); } catch (followupError) {
console.warn('Followup search error:', followupError);
}
// Render results // Render results
if (results.length === 0) { if (results.length === 0) {
searchResultsItems.innerHTML = '<div class="search-empty">未找到匹配结果</div>'; searchResultsItems.innerHTML = '<div class="search-empty">未找到匹配结果</div>';
} else { } else {
searchResultsItems.innerHTML = results.slice(0, 10).map(result => ` // Deduplicate by title
const seen = new Set();
const uniqueResults = results.filter(r => {
const key = r.title + r.type;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
searchResultsItems.innerHTML = uniqueResults.slice(0, 10).map(result => `
<div class="search-result-item" data-section="${result.section}"> <div class="search-result-item" data-section="${result.section}">
<i class="${result.icon}"></i> <i class="${result.icon}"></i>
<div class="search-result-info"> <div class="search-result-info">

View File

@ -76,6 +76,10 @@ function initTrialPeriodsPage() {
applyTrialFiltersAndSort(); applyTrialFiltersAndSort();
}); });
// Intended product checkbox change handlers
setupIntendedProductCheckboxes('trialIntendedProductOtherCheckbox', 'trialIntendedProductOther');
setupIntendedProductCheckboxes('editTrialIntendedProductOtherCheckbox', 'editTrialIntendedProductOther');
// Load customers map for displaying customer names // Load customers map for displaying customer names
loadCustomersMap(); loadCustomersMap();
} }
@ -133,6 +137,120 @@ async function loadCustomersForDropdown() {
return await loadCustomersMap(); return await loadCustomersMap();
} }
// Setup intended product checkboxes with "other" option handling
function setupIntendedProductCheckboxes(otherCheckboxId, otherInputId) {
const otherCheckbox = document.getElementById(otherCheckboxId);
const otherInput = document.getElementById(otherInputId);
if (!otherCheckbox || !otherInput) return;
otherCheckbox.addEventListener('change', function () {
if (this.checked) {
otherInput.style.display = 'block';
otherInput.focus();
} else {
otherInput.style.display = 'none';
otherInput.value = '';
}
});
}
// Get intended product values from checkboxes (returns comma-separated string)
function getIntendedProductValues(checkboxName, otherInputId) {
const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]:checked`);
const otherInput = document.getElementById(otherInputId);
const values = [];
checkboxes.forEach(cb => {
if (cb.value === '其他') {
// If "other" is checked, use the custom input value
if (otherInput && otherInput.value.trim()) {
values.push(otherInput.value.trim());
}
} else {
values.push(cb.value);
}
});
return values.join(', ');
}
// Set intended product values in checkboxes (accepts comma-separated string)
function setIntendedProductValues(checkboxName, otherInputId, value) {
const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]`);
const otherInput = document.getElementById(otherInputId);
// Reset all checkboxes
checkboxes.forEach(cb => {
cb.checked = false;
});
if (otherInput) {
otherInput.style.display = 'none';
otherInput.value = '';
}
if (!value) return;
// Parse comma-separated values
const selectedValues = value.split(',').map(v => v.trim()).filter(v => v);
const predefinedOptions = ['数据闭环', 'robogo'];
let hasOther = false;
const otherValues = [];
selectedValues.forEach(val => {
if (predefinedOptions.includes(val)) {
// Check the corresponding checkbox
checkboxes.forEach(cb => {
if (cb.value === val) {
cb.checked = true;
}
});
} else {
// Custom value - mark as "other"
hasOther = true;
otherValues.push(val);
}
});
// Handle "other" checkbox and input
if (hasOther) {
checkboxes.forEach(cb => {
if (cb.value === '其他') {
cb.checked = true;
}
});
if (otherInput) {
otherInput.style.display = 'block';
otherInput.value = otherValues.join(', ');
}
}
}
// Legacy compatibility functions (for backward compatibility)
function setupIntendedProductDropdown(selectId, otherId) {
// No-op for backward compatibility - now using checkbox groups
}
function getIntendedProductValue(selectId, otherId) {
// Map to new checkbox-based function based on the select ID
if (selectId === 'trialIntendedProduct') {
return getIntendedProductValues('intendedProduct', 'trialIntendedProductOther');
} else if (selectId === 'editTrialIntendedProduct') {
return getIntendedProductValues('editIntendedProduct', 'editTrialIntendedProductOther');
}
return '';
}
function setIntendedProductValue(selectId, otherId, value) {
// Map to new checkbox-based function based on the select ID
if (selectId === 'trialIntendedProduct') {
setIntendedProductValues('intendedProduct', 'trialIntendedProductOther', value);
} else if (selectId === 'editTrialIntendedProduct') {
setIntendedProductValues('editIntendedProduct', 'editTrialIntendedProductOther', value);
}
}
// Load all trial periods // Load all trial periods
async function loadAllTrialPeriods() { async function loadAllTrialPeriods() {
@ -274,10 +392,10 @@ function renderTrialPeriodsTable() {
if (filteredTrialPeriodsData.length === 0) { if (filteredTrialPeriodsData.length === 0) {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.innerHTML = ` row.innerHTML = `
<td colspan="7" class="empty-state"> <td colspan="8" class="empty-state">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<h3>👥 还没有客户试用信息</h3> <h3>👥 还没有客户试用信息</h3>
<p>点击上方添加试用时间开始管理客户试用</p> <p>点击上方添加客户开始管理客户试用</p>
</td> </td>
`; `;
tbody.appendChild(row); tbody.appendChild(row);
@ -325,10 +443,12 @@ function renderTrialPeriodsTable() {
const endTime = formatDateTime(period.endTime); const endTime = formatDateTime(period.endTime);
const createdAt = formatDateTime(period.createdAt); const createdAt = formatDateTime(period.createdAt);
const source = period.source || ''; const source = period.source || '';
const intendedProduct = period.intendedProduct || '';
row.innerHTML = ` row.innerHTML = `
<td><strong>${customerName}</strong></td> <td><strong>${customerName}</strong></td>
<td>${source}</td> <td>${source}</td>
<td>${intendedProduct}</td>
<td>${statusBadge}</td> <td>${statusBadge}</td>
<td>${startTime}</td> <td>${startTime}</td>
<td>${endTime}</td> <td>${endTime}</td>
@ -410,6 +530,9 @@ function openAddTrialModal() {
const sourceEl = document.getElementById('trialCustomerSource'); const sourceEl = document.getElementById('trialCustomerSource');
if (sourceEl) sourceEl.value = ''; if (sourceEl) sourceEl.value = '';
// Reset intended product
setIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther', '');
document.querySelector('input[name="isTrial"][value="true"]').checked = true; document.querySelector('input[name="isTrial"][value="true"]').checked = true;
document.getElementById('trialStartTime').value = ''; document.getElementById('trialStartTime').value = '';
document.getElementById('trialEndTime').value = ''; document.getElementById('trialEndTime').value = '';
@ -427,6 +550,9 @@ function openEditTrialModal(periodId) {
const sourceEl = document.getElementById('editTrialCustomerSource'); const sourceEl = document.getElementById('editTrialCustomerSource');
if (sourceEl) sourceEl.value = period.source || ''; if (sourceEl) sourceEl.value = period.source || '';
// Set intended product
setIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther', period.intendedProduct || '');
const startDate = new Date(period.startTime); const startDate = new Date(period.startTime);
const endDate = new Date(period.endTime); const endDate = new Date(period.endTime);
@ -456,7 +582,6 @@ async function deleteTrialPeriodFromPage(periodId) {
if (response.ok) { if (response.ok) {
await loadAllTrialPeriods(); await loadAllTrialPeriods();
alert('试用时间删除成功!');
} else { } else {
alert('删除试用时间时出错'); alert('删除试用时间时出错');
} }
@ -475,6 +600,8 @@ async function createTrialPeriodFromPage() {
const customerName = inputEl ? inputEl.value.trim() : ''; const customerName = inputEl ? inputEl.value.trim() : '';
// Get customer source from input // Get customer source from input
const source = sourceEl ? sourceEl.value.trim() : ''; const source = sourceEl ? sourceEl.value.trim() : '';
// Get intended product
const intendedProduct = getIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther');
const isTrialValue = document.querySelector('input[name="isTrial"]:checked').value; const isTrialValue = document.querySelector('input[name="isTrial"]:checked').value;
const isTrial = isTrialValue === 'true'; const isTrial = isTrialValue === 'true';
@ -495,6 +622,7 @@ async function createTrialPeriodFromPage() {
const formData = { const formData = {
customerName: customerName, customerName: customerName,
source: source, source: source,
intendedProduct: intendedProduct,
startTime: new Date(startTime).toISOString(), startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(), endTime: new Date(endTime).toISOString(),
isTrial: isTrial isTrial: isTrial
@ -513,7 +641,6 @@ async function createTrialPeriodFromPage() {
document.getElementById('addTrialPeriodModal').style.display = 'none'; document.getElementById('addTrialPeriodModal').style.display = 'none';
document.getElementById('addTrialPeriodForm').reset(); document.getElementById('addTrialPeriodForm').reset();
await loadAllTrialPeriods(); await loadAllTrialPeriods();
alert('试用时间添加成功!');
} else { } else {
alert('添加试用时间时出错'); alert('添加试用时间时出错');
} }

View File

@ -188,7 +188,6 @@ async function createTrialPeriod() {
document.getElementById('addTrialPeriodModal').style.display = 'none'; document.getElementById('addTrialPeriodModal').style.display = 'none';
document.getElementById('addTrialPeriodForm').reset(); document.getElementById('addTrialPeriodForm').reset();
await loadTrialPeriods(customerId); await loadTrialPeriods(customerId);
alert('试用时间添加成功!');
} else { } else {
alert('添加试用时间时出错'); alert('添加试用时间时出错');
} }
@ -236,6 +235,27 @@ async function updateTrialPeriod() {
const sourceEl = document.getElementById('editTrialCustomerSource'); const sourceEl = document.getElementById('editTrialCustomerSource');
const source = sourceEl ? sourceEl.value.trim() : ''; const source = sourceEl ? sourceEl.value.trim() : '';
// Get intended product value (use the function from trial-periods-page.js if available)
let intendedProduct = '';
if (typeof getIntendedProductValue === 'function') {
intendedProduct = getIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther');
} else {
// Fallback: get values from checkboxes directly
const checkboxes = document.querySelectorAll('input[name="editIntendedProduct"]:checked');
const otherInput = document.getElementById('editTrialIntendedProductOther');
const values = [];
checkboxes.forEach(cb => {
if (cb.value === '其他') {
if (otherInput && otherInput.value.trim()) {
values.push(otherInput.value.trim());
}
} else {
values.push(cb.value);
}
});
intendedProduct = values.join(', ');
}
// Get isTrial value from radio buttons // Get isTrial value from radio buttons
const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked'); const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked');
const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true; const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true;
@ -247,6 +267,7 @@ async function updateTrialPeriod() {
const formData = { const formData = {
source: source, source: source,
intendedProduct: intendedProduct,
startTime: new Date(startTime).toISOString(), startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(), endTime: new Date(endTime).toISOString(),
isTrial: isTrial isTrial: isTrial
@ -271,7 +292,6 @@ async function updateTrialPeriod() {
if (typeof loadAllTrialPeriods === 'function') { if (typeof loadAllTrialPeriods === 'function') {
await loadAllTrialPeriods(); await loadAllTrialPeriods();
} }
alert('试用时间更新成功!');
} else { } else {
alert('更新试用时间时出错'); alert('更新试用时间时出错');
} }
@ -294,7 +314,6 @@ async function deleteTrialPeriod(periodId) {
if (response.ok) { if (response.ok) {
await loadTrialPeriods(currentCustomerId); await loadTrialPeriods(currentCustomerId);
alert('试用时间删除成功!');
} else { } else {
alert('删除试用时间时出错'); alert('删除试用时间时出错');
} }

View File

@ -83,12 +83,13 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re
} }
trialPeriod := models.TrialPeriod{ trialPeriod := models.TrialPeriod{
CustomerName: req.CustomerName, CustomerName: req.CustomerName,
Source: req.Source, Source: req.Source,
StartTime: startTime, IntendedProduct: req.IntendedProduct,
EndTime: endTime, StartTime: startTime,
IsTrial: req.IsTrial, EndTime: endTime,
CreatedAt: time.Now(), IsTrial: req.IsTrial,
CreatedAt: time.Now(),
} }
createdPeriod, err := h.storage.CreateTrialPeriod(trialPeriod) createdPeriod, err := h.storage.CreateTrialPeriod(trialPeriod)

View File

@ -23,7 +23,7 @@ func NewMySQLTrialPeriodStorage() TrialPeriodStorage {
func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) { func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) {
query := ` query := `
SELECT id, customer_name, COALESCE(source, '') as source, start_time, end_time, is_trial, created_at SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
FROM trial_periods FROM trial_periods
ORDER BY created_at DESC ORDER BY created_at DESC
` `
@ -40,7 +40,7 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e
var isTrial int var isTrial int
err := rows.Scan( err := rows.Scan(
&tp.ID, &tp.CustomerName, &tp.Source, &tp.StartTime, &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
&tp.EndTime, &isTrial, &tp.CreatedAt, &tp.EndTime, &isTrial, &tp.CreatedAt,
) )
if err != nil { if err != nil {
@ -56,7 +56,7 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) { func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) {
query := ` query := `
SELECT id, customer_name, COALESCE(source, '') as source, start_time, end_time, is_trial, created_at SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
FROM trial_periods FROM trial_periods
WHERE customer_name = ? WHERE customer_name = ?
ORDER BY end_time DESC ORDER BY end_time DESC
@ -74,7 +74,7 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string
var isTrial int var isTrial int
err := rows.Scan( err := rows.Scan(
&tp.ID, &tp.CustomerName, &tp.Source, &tp.StartTime, &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
&tp.EndTime, &isTrial, &tp.CreatedAt, &tp.EndTime, &isTrial, &tp.CreatedAt,
) )
if err != nil { if err != nil {
@ -90,7 +90,7 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) { func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) {
query := ` query := `
SELECT id, customer_name, COALESCE(source, '') as source, start_time, end_time, is_trial, created_at SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
FROM trial_periods FROM trial_periods
WHERE id = ? WHERE id = ?
` `
@ -99,7 +99,7 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialP
var isTrial int var isTrial int
err := ts.db.QueryRow(query, id).Scan( err := ts.db.QueryRow(query, id).Scan(
&tp.ID, &tp.CustomerName, &tp.Source, &tp.StartTime, &tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
&tp.EndTime, &isTrial, &tp.CreatedAt, &tp.EndTime, &isTrial, &tp.CreatedAt,
) )
@ -123,8 +123,8 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer
} }
query := ` query := `
INSERT INTO trial_periods (id, customer_name, source, start_time, end_time, is_trial, created_at) INSERT INTO trial_periods (id, customer_name, source, intended_product, start_time, end_time, is_trial, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
` `
isTrial := 0 isTrial := 0
@ -133,8 +133,8 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer
} }
_, err := ts.db.Exec(query, _, err := ts.db.Exec(query,
trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.StartTime, trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.IntendedProduct,
trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt, trialPeriod.StartTime, trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt,
) )
if err != nil { if err != nil {
@ -158,6 +158,9 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
if updates.Source != nil { if updates.Source != nil {
existing.Source = *updates.Source existing.Source = *updates.Source
} }
if updates.IntendedProduct != nil {
existing.IntendedProduct = *updates.IntendedProduct
}
if updates.StartTime != nil { if updates.StartTime != nil {
startTime, err := time.Parse(time.RFC3339, *updates.StartTime) startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
if err == nil { if err == nil {
@ -176,7 +179,7 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
query := ` query := `
UPDATE trial_periods UPDATE trial_periods
SET customer_name = ?, source = ?, start_time = ?, end_time = ?, is_trial = ? SET customer_name = ?, source = ?, intended_product = ?, start_time = ?, end_time = ?, is_trial = ?
WHERE id = ? WHERE id = ?
` `
@ -186,8 +189,8 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
} }
_, err = ts.db.Exec(query, _, err = ts.db.Exec(query,
existing.CustomerName, existing.Source, existing.StartTime, existing.EndTime, existing.CustomerName, existing.Source, existing.IntendedProduct,
isTrial, id, existing.StartTime, existing.EndTime, isTrial, id,
) )
return err return err

View File

@ -4,29 +4,32 @@ import "time"
// TrialPeriod represents a trial period for a customer (客户信息) // TrialPeriod represents a trial period for a customer (客户信息)
type TrialPeriod struct { type TrialPeriod struct {
ID string `json:"id"` ID string `json:"id"`
CustomerName string `json:"customerName"` // 直接存储客户名称 CustomerName string `json:"customerName"` // 直接存储客户名称
Source string `json:"source"` // 客户来源 Source string `json:"source"` // 客户来源
StartTime time.Time `json:"startTime"` IntendedProduct string `json:"intendedProduct"` // 意向产品
EndTime time.Time `json:"endTime"` StartTime time.Time `json:"startTime"`
IsTrial bool `json:"isTrial"` EndTime time.Time `json:"endTime"`
CreatedAt time.Time `json:"createdAt"` IsTrial bool `json:"isTrial"`
CreatedAt time.Time `json:"createdAt"`
} }
// CreateTrialPeriodRequest represents the request to create a trial period // CreateTrialPeriodRequest represents the request to create a trial period
type CreateTrialPeriodRequest struct { type CreateTrialPeriodRequest struct {
CustomerName string `json:"customerName"` // 直接使用客户名称 CustomerName string `json:"customerName"` // 直接使用客户名称
Source string `json:"source"` // 客户来源 Source string `json:"source"` // 客户来源
StartTime string `json:"startTime"` IntendedProduct string `json:"intendedProduct"` // 意向产品
EndTime string `json:"endTime"` StartTime string `json:"startTime"`
IsTrial bool `json:"isTrial"` EndTime string `json:"endTime"`
IsTrial bool `json:"isTrial"`
} }
// UpdateTrialPeriodRequest represents the request to update a trial period // UpdateTrialPeriodRequest represents the request to update a trial period
type UpdateTrialPeriodRequest struct { type UpdateTrialPeriodRequest struct {
CustomerName *string `json:"customerName,omitempty"` CustomerName *string `json:"customerName,omitempty"`
Source *string `json:"source,omitempty"` // 客户来源 Source *string `json:"source,omitempty"` // 客户来源
StartTime *string `json:"startTime,omitempty"` IntendedProduct *string `json:"intendedProduct,omitempty"` // 意向产品
EndTime *string `json:"endTime,omitempty"` StartTime *string `json:"startTime,omitempty"`
IsTrial *bool `json:"isTrial,omitempty"` EndTime *string `json:"endTime,omitempty"`
IsTrial *bool `json:"isTrial,omitempty"`
} }

View File

@ -0,0 +1,74 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Database connection parameters
host := os.Getenv("DB_HOST")
if host == "" {
host = "localhost"
}
user := os.Getenv("DB_USER")
if user == "" {
user = "root"
}
password := os.Getenv("DB_PASSWORD")
if password == "" {
password = ""
}
dbName := os.Getenv("DB_NAME")
if dbName == "" {
dbName = "crm_db"
}
port := os.Getenv("DB_PORT")
if port == "" {
port = "3306"
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
user, password, host, port, dbName)
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Check if column exists
var columnExists int
err = db.QueryRow(`
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = 'trial_periods'
AND COLUMN_NAME = 'intended_product'
`, dbName).Scan(&columnExists)
if err != nil {
log.Fatal("Failed to check column existence:", err)
}
if columnExists > 0 {
log.Println("✅ Column 'intended_product' already exists in trial_periods table")
return
}
// Add the intended_product column
_, err = db.Exec(`
ALTER TABLE trial_periods
ADD COLUMN intended_product VARCHAR(255) DEFAULT '' AFTER source
`)
if err != nil {
log.Fatal("Failed to add intended_product column:", err)
}
log.Println("✅ Successfully added 'intended_product' column to trial_periods table")
}