This commit introduces the initial prototype for an item database and build list tool. It establishes the foundational structure and core features of the application. Key components included: - **Documentation:** Initial design, interaction, and project structure documents (`design.md`, `interaction.md`, `outline.md`). - **Core Pages:** - `index.html`: Item management with CRUD operations. - `export.html`: CSV export configuration with drag-and-drop sorting. - `history.html`: Price history visualization with ECharts. - **Logic:** `main.js` and page-specific scripts handle client-side logic, including data management with `localStorage`, UI interactions, and animations. - **Features:** Implements core functionalities such as item creation, editing, deletion, data backup/restore, and sample data loading.
457 lines
18 KiB
JavaScript
457 lines
18 KiB
JavaScript
// 物品数据库管理工具 - 主要JavaScript逻辑
|
|
|
|
class ItemDatabase {
|
|
constructor() {
|
|
this.items = this.loadItems();
|
|
this.history = this.loadHistory();
|
|
this.init();
|
|
}
|
|
|
|
// 初始化应用
|
|
init() {
|
|
this.setupEventListeners();
|
|
this.renderItems();
|
|
this.updateStats();
|
|
this.initAnimations();
|
|
this.loadSampleData();
|
|
}
|
|
|
|
// 设置事件监听器
|
|
setupEventListeners() {
|
|
// 表单提交
|
|
document.getElementById('add-item-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.addItem();
|
|
});
|
|
|
|
// 搜索和筛选
|
|
document.getElementById('search-input').addEventListener('input', (e) => {
|
|
this.filterItems();
|
|
});
|
|
|
|
document.getElementById('filter-select').addEventListener('change', (e) => {
|
|
this.filterItems();
|
|
});
|
|
|
|
// 数据导入导出
|
|
document.getElementById('export-data-btn').addEventListener('click', () => {
|
|
this.exportData();
|
|
});
|
|
|
|
document.getElementById('import-data-btn').addEventListener('click', () => {
|
|
document.getElementById('import-file-input').click();
|
|
});
|
|
|
|
document.getElementById('import-file-input').addEventListener('change', (e) => {
|
|
this.importData(e.target.files[0]);
|
|
});
|
|
|
|
// 编辑模态框
|
|
document.getElementById('edit-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.updateItem();
|
|
});
|
|
|
|
document.getElementById('cancel-edit').addEventListener('click', () => {
|
|
this.closeEditModal();
|
|
});
|
|
|
|
// 点击模态框外部关闭
|
|
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'edit-modal') {
|
|
this.closeEditModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 加载示例数据
|
|
loadSampleData() {
|
|
if (this.items.length === 0) {
|
|
const sampleItems = [
|
|
{
|
|
id: this.generateId(),
|
|
name: "明代青花瓷瓶",
|
|
imageUrl: "https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=400&h=300&fit=crop",
|
|
description: "明代宣德年间的青花瓷瓶,品相完好,具有极高的收藏价值",
|
|
tags: ["古董", "瓷器", "明代"],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
},
|
|
{
|
|
id: this.generateId(),
|
|
name: "清代玉雕摆件",
|
|
imageUrl: "https://images.unsplash.com/photo-1605883705077-8d3d848f8a9d?w=400&h=300&fit=crop",
|
|
description: "清代乾隆时期的玉雕摆件,雕工精细,玉质温润",
|
|
tags: ["古董", "玉器", "清代"],
|
|
createdAt: new Date(Date.now() - 86400000).toISOString(),
|
|
updatedAt: new Date(Date.now() - 86400000).toISOString()
|
|
},
|
|
{
|
|
id: this.generateId(),
|
|
name: "宋代书法作品",
|
|
imageUrl: "https://images.unsplash.com/photo-1581833971358-2c8b550f87b3?w=400&h=300&fit=crop",
|
|
description: "宋代书法名家作品,纸本墨迹,保存完好",
|
|
tags: ["艺术品", "书法", "宋代"],
|
|
createdAt: new Date(Date.now() - 172800000).toISOString(),
|
|
updatedAt: new Date(Date.now() - 172800000).toISOString()
|
|
},
|
|
{
|
|
id: this.generateId(),
|
|
name: "唐代金银器",
|
|
imageUrl: "https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=400&h=300&fit=crop",
|
|
description: "唐代金银器,工艺精湛,具有重要历史价值",
|
|
tags: ["古董", "金银器", "唐代"],
|
|
createdAt: new Date(Date.now() - 259200000).toISOString(),
|
|
updatedAt: new Date(Date.now() - 259200000).toISOString()
|
|
},
|
|
{
|
|
id: this.generateId(),
|
|
name: "现代艺术画作",
|
|
imageUrl: "https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=400&h=300&fit=crop",
|
|
description: "当代知名艺术家作品,抽象风格,色彩丰富",
|
|
tags: ["艺术品", "现代艺术", "画作"],
|
|
createdAt: new Date(Date.now() - 345600000).toISOString(),
|
|
updatedAt: new Date(Date.now() - 345600000).toISOString()
|
|
},
|
|
{
|
|
id: this.generateId(),
|
|
name: "古代青铜器",
|
|
imageUrl: "https://images.unsplash.com/photo-1605883705077-8d3d848f8a9d?w=400&h=300&fit=crop",
|
|
description: "商周时期青铜器,纹饰精美,保存完好",
|
|
tags: ["古董", "青铜器", "商周"],
|
|
createdAt: new Date(Date.now() - 432000000).toISOString(),
|
|
updatedAt: new Date(Date.now() - 432000000).toISOString()
|
|
}
|
|
];
|
|
|
|
this.items = sampleItems;
|
|
this.saveItems();
|
|
this.renderItems();
|
|
this.updateStats();
|
|
}
|
|
}
|
|
|
|
// 生成唯一ID
|
|
generateId() {
|
|
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
}
|
|
|
|
// 添加物品
|
|
addItem() {
|
|
const name = document.getElementById('item-name').value.trim();
|
|
const imageUrl = document.getElementById('item-image').value.trim();
|
|
const description = document.getElementById('item-description').value.trim();
|
|
const tags = document.getElementById('item-tags').value.trim();
|
|
|
|
if (!name || !imageUrl) {
|
|
this.showNotification('请填写必填项', 'error');
|
|
return;
|
|
}
|
|
|
|
const newItem = {
|
|
id: this.generateId(),
|
|
name,
|
|
imageUrl,
|
|
description,
|
|
tags: tags ? tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
this.items.unshift(newItem);
|
|
this.saveItems();
|
|
this.renderItems();
|
|
this.updateStats();
|
|
|
|
// 清空表单
|
|
document.getElementById('add-item-form').reset();
|
|
|
|
this.showNotification('物品添加成功', 'success');
|
|
}
|
|
|
|
// 编辑物品
|
|
editItem(itemId) {
|
|
const item = this.items.find(i => i.id === itemId);
|
|
if (!item) return;
|
|
|
|
document.getElementById('edit-item-id').value = item.id;
|
|
document.getElementById('edit-item-name').value = item.name;
|
|
document.getElementById('edit-item-image').value = item.imageUrl;
|
|
document.getElementById('edit-item-description').value = item.description || '';
|
|
document.getElementById('edit-item-tags').value = item.tags ? item.tags.join(', ') : '';
|
|
|
|
document.getElementById('edit-modal').classList.remove('hidden');
|
|
document.getElementById('edit-modal').classList.add('flex');
|
|
}
|
|
|
|
// 更新物品
|
|
updateItem() {
|
|
const itemId = document.getElementById('edit-item-id').value;
|
|
const item = this.items.find(i => i.id === itemId);
|
|
if (!item) return;
|
|
|
|
item.name = document.getElementById('edit-item-name').value.trim();
|
|
item.imageUrl = document.getElementById('edit-item-image').value.trim();
|
|
item.description = document.getElementById('edit-item-description').value.trim();
|
|
const tags = document.getElementById('edit-item-tags').value.trim();
|
|
item.tags = tags ? tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
|
item.updatedAt = new Date().toISOString();
|
|
|
|
this.saveItems();
|
|
this.renderItems();
|
|
this.closeEditModal();
|
|
this.showNotification('物品更新成功', 'success');
|
|
}
|
|
|
|
// 关闭编辑模态框
|
|
closeEditModal() {
|
|
document.getElementById('edit-modal').classList.add('hidden');
|
|
document.getElementById('edit-modal').classList.remove('flex');
|
|
}
|
|
|
|
// 删除物品
|
|
deleteItem(itemId) {
|
|
if (confirm('确定要删除这个物品吗?')) {
|
|
this.items = this.items.filter(item => item.id !== itemId);
|
|
this.saveItems();
|
|
this.renderItems();
|
|
this.updateStats();
|
|
this.showNotification('物品删除成功', 'success');
|
|
}
|
|
}
|
|
|
|
// 渲染物品列表
|
|
renderItems() {
|
|
const grid = document.getElementById('items-grid');
|
|
const emptyState = document.getElementById('empty-state');
|
|
|
|
if (this.items.length === 0) {
|
|
grid.innerHTML = '';
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
emptyState.classList.add('hidden');
|
|
|
|
const filteredItems = this.getFilteredItems();
|
|
|
|
grid.innerHTML = filteredItems.map(item => `
|
|
<div class="item-card card-hover fade-in" data-id="${item.id}">
|
|
<div class="relative">
|
|
<img src="${item.imageUrl}" alt="${item.name}" class="item-image"
|
|
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xMDAgNzBDMTA4LjI4NCA3MCA5NS4wMjU4IDc0LjY4MjMgOTkuMjY5NSA4Mi45NTYyTDg1LjM0NDIgMTA4LjI2OEM4Mi43MjI3IDExMi45NTIgODQuNjgzMSAxMTguNzcgODkuMzY2OSAxMjEuMzg5TDk5IDIxMC4wMDFMMTA5LjYzMyAxMjEuMzg5QzExNC4zMTcgMTE4Ljc3IDExNi4yNzcgMTEyLjk1MiAxMTMuNjU1IDEwOC4yNjhMOTkuNzMwNSA4Mi45NTYyQzEwMy45NzQgNzQuNjgyMyA5MC43MTU5IDcwIDEwMCA3MFoiIGZpbGw9IiM5Q0EzQUYiLz4KPC9zdmc+'">
|
|
<div class="absolute top-2 right-2 flex space-x-1">
|
|
<button onclick="itemDB.editItem('${item.id}')"
|
|
class="p-2 bg-white/80 hover:bg-white rounded-lg transition-colors">
|
|
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
</svg>
|
|
</button>
|
|
<button onclick="itemDB.deleteItem('${item.id}')"
|
|
class="p-2 bg-white/80 hover:bg-white rounded-lg transition-colors">
|
|
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-4">
|
|
<h4 class="font-bold text-gray-800 mb-2 truncate">${item.name}</h4>
|
|
<p class="text-sm text-gray-600 mb-3 line-clamp-2">${item.description || '暂无描述'}</p>
|
|
<div class="flex flex-wrap gap-1 mb-3">
|
|
${item.tags ? item.tags.map(tag => `
|
|
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">${tag}</span>
|
|
`).join('') : ''}
|
|
</div>
|
|
<div class="text-xs text-gray-500">
|
|
创建时间: ${new Date(item.createdAt).toLocaleDateString('zh-CN')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// 添加动画效果
|
|
this.animateCards();
|
|
}
|
|
|
|
// 获取筛选后的物品
|
|
getFilteredItems() {
|
|
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
|
const filterTag = document.getElementById('filter-select').value;
|
|
|
|
return this.items.filter(item => {
|
|
const matchesSearch = item.name.toLowerCase().includes(searchTerm) ||
|
|
item.description.toLowerCase().includes(searchTerm);
|
|
const matchesFilter = !filterTag || (item.tags && item.tags.includes(filterTag));
|
|
return matchesSearch && matchesFilter;
|
|
});
|
|
}
|
|
|
|
// 筛选物品
|
|
filterItems() {
|
|
this.renderItems();
|
|
}
|
|
|
|
// 更新统计信息
|
|
updateStats() {
|
|
const totalItems = this.items.length;
|
|
const currentMonth = new Date().getMonth();
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
const monthlyItems = this.items.filter(item => {
|
|
const itemDate = new Date(item.createdAt);
|
|
return itemDate.getMonth() === currentMonth && itemDate.getFullYear() === currentYear;
|
|
}).length;
|
|
|
|
const historyRecords = this.history.length;
|
|
const totalImages = this.items.filter(item => item.imageUrl).length;
|
|
|
|
document.getElementById('total-items').textContent = totalItems;
|
|
document.getElementById('monthly-items').textContent = monthlyItems;
|
|
document.getElementById('history-records').textContent = historyRecords;
|
|
document.getElementById('total-images').textContent = totalImages;
|
|
|
|
// 添加数字动画
|
|
this.animateNumbers();
|
|
}
|
|
|
|
// 导出数据
|
|
exportData() {
|
|
const data = {
|
|
items: this.items,
|
|
history: this.history,
|
|
exportDate: new Date().toISOString()
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `items-backup-${new Date().toISOString().split('T')[0]}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.showNotification('数据导出成功', 'success');
|
|
}
|
|
|
|
// 导入数据
|
|
importData(file) {
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.target.result);
|
|
if (data.items && Array.isArray(data.items)) {
|
|
this.items = data.items;
|
|
this.saveItems();
|
|
}
|
|
if (data.history && Array.isArray(data.history)) {
|
|
this.history = data.history;
|
|
this.saveHistory();
|
|
}
|
|
this.renderItems();
|
|
this.updateStats();
|
|
this.showNotification('数据导入成功', 'success');
|
|
} catch (error) {
|
|
this.showNotification('数据格式错误', 'error');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// 显示通知
|
|
showNotification(message, type = 'info') {
|
|
const notification = document.getElementById('notification');
|
|
const messageEl = document.getElementById('notification-message');
|
|
const iconEl = document.getElementById('notification-icon');
|
|
|
|
// 设置图标
|
|
const icons = {
|
|
success: '<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
|
error: '<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
|
info: '<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'
|
|
};
|
|
|
|
iconEl.innerHTML = icons[type] || icons.info;
|
|
messageEl.textContent = message;
|
|
|
|
notification.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// 初始化动画
|
|
initAnimations() {
|
|
// 页面加载动画
|
|
const fadeElements = document.querySelectorAll('.fade-in');
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
}
|
|
});
|
|
});
|
|
|
|
fadeElements.forEach(el => observer.observe(el));
|
|
|
|
// 标题动画
|
|
anime({
|
|
targets: '.hero-title',
|
|
opacity: [0, 1],
|
|
translateY: [30, 0],
|
|
duration: 1000,
|
|
easing: 'easeOutExpo',
|
|
delay: 300
|
|
});
|
|
}
|
|
|
|
// 卡片动画
|
|
animateCards() {
|
|
anime({
|
|
targets: '.item-card',
|
|
opacity: [0, 1],
|
|
translateY: [20, 0],
|
|
duration: 600,
|
|
easing: 'easeOutExpo',
|
|
delay: anime.stagger(100)
|
|
});
|
|
}
|
|
|
|
// 数字动画
|
|
animateNumbers() {
|
|
anime({
|
|
targets: '#total-items, #monthly-items, #history-records, #total-images',
|
|
scale: [1.2, 1],
|
|
duration: 300,
|
|
easing: 'easeOutBack'
|
|
});
|
|
}
|
|
|
|
// 数据持久化
|
|
saveItems() {
|
|
localStorage.setItem('itemdb-items', JSON.stringify(this.items));
|
|
}
|
|
|
|
loadItems() {
|
|
const data = localStorage.getItem('itemdb-items');
|
|
return data ? JSON.parse(data) : [];
|
|
}
|
|
|
|
saveHistory() {
|
|
localStorage.setItem('itemdb-history', JSON.stringify(this.history));
|
|
}
|
|
|
|
loadHistory() {
|
|
const data = localStorage.getItem('itemdb-history');
|
|
return data ? JSON.parse(data) : [];
|
|
}
|
|
}
|
|
|
|
// 初始化应用
|
|
const itemDB = new ItemDatabase(); |