Files
bid-setup.tootaio.com/prototype/export.html
xiaomai c0ba7ac0ff feat(prototype): add initial prototype for item database tool
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.
2025-10-13 09:51:46 +08:00

747 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>导出配置 - 物品数据库管理工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Noto Sans SC', sans-serif;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
}
.hero-title {
font-family: 'Noto Serif SC', serif;
background: linear-gradient(135deg, #2C3E50, #3498DB);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.btn-primary {
background: linear-gradient(135deg, #3498DB, #2C3E50);
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, #2C3E50, #3498DB);
transform: translateY(-1px);
}
.btn-secondary {
background: linear-gradient(135deg, #E67E22, #D35400);
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #D35400, #E67E22);
transform: translateY(-1px);
}
.item-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
cursor: pointer;
border: 2px solid transparent;
}
.item-card.selected {
border-color: #3498DB;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.item-image {
width: 100%;
height: 120px;
object-fit: cover;
background: linear-gradient(45deg, #f0f0f0, #e0e0e0);
}
.selected-item {
background: white;
border-radius: 12px;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
.drag-handle {
cursor: grab;
color: #9ca3af;
}
.drag-handle:active {
cursor: grabbing;
}
.sortable-ghost {
opacity: 0.5;
}
.csv-preview {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.4;
max-height: 300px;
overflow-y: auto;
}
.fade-in {
opacity: 0;
transform: translateY(20px);
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
transition: all 0.6s ease;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
transform: translateX(400px);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.batch-settings {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="bg-white/90 backdrop-blur-md shadow-sm border-b border-gray-200 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center space-x-3">
<img src="resources/database-icon.png" alt="Database Icon" class="w-8 h-8">
<h1 class="text-xl font-bold text-gray-800">物品数据库</h1>
</div>
<div class="flex space-x-4">
<a href="index.html" class="px-4 py-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">物品管理</a>
<a href="export.html" class="px-4 py-2 text-blue-600 bg-blue-50 rounded-lg font-medium">
<img src="resources/export-icon.png" alt="Export" class="w-4 h-4 inline mr-2">导出配置
</a>
<a href="history.html" class="px-4 py-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<img src="resources/chart-icon.png" alt="History" class="w-4 h-4 inline mr-2">历史记录
</a>
</div>
</div>
</div>
</nav>
<!-- 主要内容区域 -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 标题区域 -->
<div class="text-center mb-8 fade-in">
<h2 class="hero-title text-3xl md:text-4xl font-bold mb-4">导出配置中心</h2>
<p class="text-gray-600 text-lg max-w-2xl mx-auto">选择要导出的物品设置起拍价格和备注信息预览CSV格式</p>
</div>
<!-- 主要操作区域 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- 物品选择区域 -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 fade-in">
<h3 class="text-xl font-bold text-gray-800 mb-4 flex items-center">
<svg class="w-6 h-6 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
选择物品
</h3>
<!-- 搜索框 -->
<div class="mb-4">
<input type="text" id="export-search" placeholder="搜索物品..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<!-- 物品列表 -->
<div id="export-items-list" class="space-y-3 max-h-96 overflow-y-auto">
<!-- 动态生成的物品卡片 -->
</div>
</div>
</div>
<!-- 选中物品配置区域 -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 fade-in">
<h3 class="text-xl font-bold text-gray-800 mb-4 flex items-center">
<svg class="w-6 h-6 mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"></path>
</svg>
导出配置
</h3>
<!-- 批量设置 -->
<div class="batch-settings">
<h4 class="font-medium text-gray-700 mb-3">批量设置</h4>
<div class="grid grid-cols-2 gap-3 mb-3">
<div>
<label class="block text-xs text-gray-600 mb-1">起拍价</label>
<input type="number" id="batch-price" placeholder="0" min="0" step="0.01"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<div>
<label class="block text-xs text-gray-600 mb-1">备注</label>
<input type="text" id="batch-note" placeholder="竞标款项用途"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
</div>
<button id="apply-batch-settings" class="w-full px-4 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors text-sm">
应用到所有选中物品
</button>
</div>
<!-- 选中物品列表 -->
<div class="mb-4">
<h4 class="font-medium text-gray-700 mb-3">已选中物品 (<span id="selected-count">0</span>)</h4>
<div id="selected-items-list" class="space-y-2 max-h-64 overflow-y-auto">
<!-- 动态生成的选中物品 -->
</div>
</div>
<!-- 清空按钮 -->
<button id="clear-selection" class="w-full px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
清空所有选择
</button>
</div>
</div>
<!-- CSV预览和导出区域 -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 fade-in">
<h3 class="text-xl font-bold text-gray-800 mb-4 flex items-center">
<svg class="w-6 h-6 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
CSV预览
</h3>
<!-- 导出设置 -->
<div class="mb-4 p-4 bg-gray-50 rounded-lg">
<h4 class="font-medium text-gray-700 mb-3">导出选项</h4>
<div class="space-y-2">
<label class="flex items-center">
<input type="checkbox" id="include-images" checked class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">包含图片下载</span>
</label>
<label class="flex items-center">
<input type="checkbox" id="include-headers" checked class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">包含列标题</span>
</label>
</div>
</div>
<!-- CSV内容预览 -->
<div class="mb-4">
<h4 class="font-medium text-gray-700 mb-2">内容预览</h4>
<div id="csv-preview" class="csv-preview">
<!-- 动态生成的CSV预览 -->
</div>
</div>
<!-- 导出按钮 -->
<div class="space-y-3">
<button id="export-csv-btn" class="w-full btn-primary text-white font-medium py-3 px-6 rounded-lg">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
导出 CSV 文件
</button>
<button id="download-images-btn" class="w-full btn-secondary text-white font-medium py-3 px-6 rounded-lg">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
下载图片包
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 通知组件 -->
<div id="notification" class="notification bg-white rounded-lg shadow-lg border border-gray-200 p-4 max-w-sm">
<div class="flex items-center">
<div id="notification-icon" class="flex-shrink-0">
<!-- 动态图标 -->
</div>
<div class="ml-3">
<p id="notification-message" class="text-sm font-medium text-gray-800"></p>
</div>
</div>
</div>
<script>
// 导出配置页面逻辑
class ExportManager {
constructor() {
this.items = this.loadItems();
this.selectedItems = new Map(); // 使用Map来存储选中的物品和配置
this.init();
}
init() {
this.setupEventListeners();
this.renderAvailableItems();
this.initAnimations();
}
loadItems() {
const data = localStorage.getItem('itemdb-items');
return data ? JSON.parse(data) : [];
}
setupEventListeners() {
// 搜索框
document.getElementById('export-search').addEventListener('input', (e) => {
this.filterAvailableItems(e.target.value);
});
// 批量设置
document.getElementById('apply-batch-settings').addEventListener('click', () => {
this.applyBatchSettings();
});
// 清空选择
document.getElementById('clear-selection').addEventListener('click', () => {
this.clearSelection();
});
// 导出按钮
document.getElementById('export-csv-btn').addEventListener('click', () => {
this.exportCSV();
});
document.getElementById('download-images-btn').addEventListener('click', () => {
this.downloadImages();
});
// 导出选项变化时更新预览
document.getElementById('include-headers').addEventListener('change', () => {
this.updateCSVPreview();
});
}
renderAvailableItems() {
const container = document.getElementById('export-items-list');
if (this.items.length === 0) {
container.innerHTML = `
<div class="text-center py-8 text-gray-500">
<svg class="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>
</svg>
<p>暂无物品,请先添加物品</p>
</div>
`;
return;
}
container.innerHTML = this.items.map(item => `
<div class="item-card card-hover" data-id="${item.id}" onclick="exportManager.toggleItem('${item.id}')">
<img src="${item.imageUrl}" alt="${item.name}" class="item-image"
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDIwMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTIwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xMDAgNDBDMTA4LjI4NCA0MCA5NS4wMjU4IDQ0LjY4MjMgOTkuMjY5NSA1Mi45NTYyTDg1LjM0NDIgNzguMjY4OUM4Mi43MjI3IDgyLjk1MTkgODQuNjgzMSA4OC43NyA4OS4zNjY5IDkxLjM4OTNMOTkgMTgwLjAwMUwxMDkuNjMzIDkxLjM4OTNDMTE0LjMxNyA4OC43NyAxMTYuMjc3IDgyLjk1MTkgMTEzLjY1NSA3OC4yNjg5TDk5LjczMDUgNTIuOTU2MkMxMDMuOTc0IDQ0LjY4MjMgOTAuNzE1OSA0MCAxMDAgNDBaIiBmaWxsPSIjOUNBM0FGIi8+Cjwvc3ZnPg=='">
<div class="p-3">
<h4 class="font-medium text-gray-800 text-sm mb-1 truncate">${item.name}</h4>
<p class="text-xs text-gray-600 line-clamp-2">${item.description || '暂无描述'}</p>
<div class="flex flex-wrap gap-1 mt-2">
${item.tags ? item.tags.slice(0, 2).map(tag => `
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">${tag}</span>
`).join('') : ''}
</div>
</div>
</div>
`).join('');
}
filterAvailableItems(searchTerm) {
const items = document.querySelectorAll('#export-items-list .item-card');
const term = searchTerm.toLowerCase();
items.forEach(item => {
const name = item.querySelector('h4').textContent.toLowerCase();
const description = item.querySelector('p').textContent.toLowerCase();
if (name.includes(term) || description.includes(term)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
toggleItem(itemId) {
const item = this.items.find(i => i.id === itemId);
if (!item) return;
const itemCard = document.querySelector(`[data-id="${itemId}"]`);
if (this.selectedItems.has(itemId)) {
this.selectedItems.delete(itemId);
itemCard.classList.remove('selected');
} else {
this.selectedItems.set(itemId, {
...item,
price: 0,
note: ''
});
itemCard.classList.add('selected');
}
this.renderSelectedItems();
this.updateCSVPreview();
}
renderSelectedItems() {
const container = document.getElementById('selected-items-list');
const countElement = document.getElementById('selected-count');
countElement.textContent = this.selectedItems.size;
if (this.selectedItems.size === 0) {
container.innerHTML = `
<div class="text-center py-8 text-gray-500">
<svg class="w-8 h-8 mx-auto mb-2 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
<p class="text-sm">请从左侧选择物品</p>
</div>
`;
return;
}
container.innerHTML = Array.from(this.selectedItems.values()).map((item, index) => `
<div class="selected-item" data-id="${item.id}">
<div class="flex items-center space-x-3 mb-3">
<div class="drag-handle">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"></path>
</svg>
</div>
<img src="${item.imageUrl}" alt="${item.name}" class="w-10 h-10 rounded-lg object-cover"
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0yMCAxMEMyMi4yMDkgMTAgMjQuNDc4NSAxMC44ODI5IDI2LjA4MDMgMTIuNjcxNEwyOS40MTQyIDE2LjcxNDNDMzAuMTkzOCAxNy42MDIgMzAuMTkzOCAxOS4wNjgxIDI5LjQxNDIgMTkuOTU2OUwyNi4wODAzIDIzLjMyODhDMjQuNDc4NSAyNS4xMTcxIDIyLjIwOSAyNiAyMCAyNkMxNy43OTEgMjYgMTUuNTIxNSAyNS4xMTcxIDEzLjkxOTcgMjMuMzI4OEwxMC41ODU4IDE5Ljk1NjlDOS44MDYyNSAxOS4wNjgxIDkuODA2MjUgMTcuNjAyIDEwLjU4NTggMTYuNzE0M0wxMy45MTk3IDEyLjY3MTRDMTUuNTIxNSAxMC44ODI5IDE3Ljc5MSAxMCAyMCAxMFoiIGZpbGw9IiM5Q0EzQUYiLz4KPC9zdmc+'">
<div class="flex-1 min-w-0">
<h5 class="font-medium text-gray-800 text-sm truncate">${item.name}</h5>
</div>
<button onclick="exportManager.removeItem('${item.id}')"
class="text-red-500 hover:text-red-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-600 mb-1">起拍价</label>
<input type="number" value="${item.price || 0}" min="0" step="0.01"
onchange="exportManager.updateItemPrice('${item.id}', this.value)"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<div>
<label class="block text-xs text-gray-600 mb-1">备注</label>
<input type="text" value="${item.note || ''}"
onchange="exportManager.updateItemNote('${item.id}', this.value)"
placeholder="竞标款项用途"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
</div>
</div>
`).join('');
// 初始化拖拽排序
this.initSortable();
}
initSortable() {
const container = document.getElementById('selected-items-list');
if (container) {
Sortable.create(container, {
handle: '.drag-handle',
ghostClass: 'sortable-ghost',
onEnd: (evt) => {
this.updateItemOrder();
}
});
}
}
updateItemOrder() {
const items = document.querySelectorAll('#selected-items-list .selected-item');
const newOrder = new Map();
items.forEach(item => {
const itemId = item.dataset.id;
const itemData = this.selectedItems.get(itemId);
if (itemData) {
newOrder.set(itemId, itemData);
}
});
this.selectedItems = newOrder;
this.updateCSVPreview();
}
removeItem(itemId) {
this.selectedItems.delete(itemId);
const itemCard = document.querySelector(`[data-id="${itemId}"]`);
if (itemCard) {
itemCard.classList.remove('selected');
}
this.renderSelectedItems();
this.updateCSVPreview();
}
updateItemPrice(itemId, price) {
const item = this.selectedItems.get(itemId);
if (item) {
item.price = parseFloat(price) || 0;
this.updateCSVPreview();
}
}
updateItemNote(itemId, note) {
const item = this.selectedItems.get(itemId);
if (item) {
item.note = note;
this.updateCSVPreview();
}
}
applyBatchSettings() {
const batchPrice = document.getElementById('batch-price').value;
const batchNote = document.getElementById('batch-note').value;
if (!batchPrice && !batchNote) {
this.showNotification('请至少设置一个批量参数', 'error');
return;
}
this.selectedItems.forEach(item => {
if (batchPrice) {
item.price = parseFloat(batchPrice) || 0;
}
if (batchNote) {
item.note = batchNote;
}
});
this.renderSelectedItems();
this.updateCSVPreview();
this.showNotification('批量设置应用成功', 'success');
// 清空批量设置输入框
document.getElementById('batch-price').value = '';
document.getElementById('batch-note').value = '';
}
clearSelection() {
this.selectedItems.clear();
document.querySelectorAll('#export-items-list .item-card.selected').forEach(card => {
card.classList.remove('selected');
});
this.renderSelectedItems();
this.updateCSVPreview();
}
updateCSVPreview() {
const container = document.getElementById('csv-preview');
const includeHeaders = document.getElementById('include-headers').checked;
if (this.selectedItems.size === 0) {
container.textContent = '请先从左侧选择要导出的物品';
return;
}
let csvContent = '';
if (includeHeaders) {
csvContent += '物品名称,起拍价,备注,图片文件名\n';
}
Array.from(this.selectedItems.values()).forEach(item => {
const imageFileName = this.extractImageFileName(item.imageUrl);
const row = [
`"${item.name}"`,
item.price || 0,
`"${item.note || ''}"`,
imageFileName
].join(',');
csvContent += row + '\n';
});
container.textContent = csvContent;
}
extractImageFileName(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const filename = pathname.split('/').pop();
return filename || 'unknown';
} catch (error) {
return 'unknown';
}
}
exportCSV() {
if (this.selectedItems.size === 0) {
this.showNotification('请先选择要导出的物品', 'error');
return;
}
const includeHeaders = document.getElementById('include-headers').checked;
let csvContent = '';
if (includeHeaders) {
csvContent += '物品名称,起拍价,备注,图片文件名\n';
}
Array.from(this.selectedItems.values()).forEach(item => {
const imageFileName = this.extractImageFileName(item.imageUrl);
const row = [
`"${item.name}"`,
item.price || 0,
`"${item.note || ''}"`,
imageFileName
].join(',');
csvContent += row + '\n';
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `物品清单-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification('CSV文件导出成功', 'success');
}
downloadImages() {
if (this.selectedItems.size === 0) {
this.showNotification('请先选择要导出的物品', 'error');
return;
}
const includeImages = document.getElementById('include-images').checked;
if (!includeImages) {
this.showNotification('请勾选"包含图片下载"选项', 'error');
return;
}
// 收集所有图片URL
const imageUrls = Array.from(this.selectedItems.values())
.filter(item => item.imageUrl)
.map(item => ({
url: item.imageUrl,
filename: this.extractImageFileName(item.imageUrl)
}));
if (imageUrls.length === 0) {
this.showNotification('没有可用的图片链接', 'error');
return;
}
// 由于浏览器安全限制我们不能直接创建ZIP文件
// 这里我们提供一个图片链接列表供用户手动下载
this.showImageDownloadList(imageUrls);
}
showImageDownloadList(imageUrls) {
const list = imageUrls.map((item, index) =>
`<div class="flex justify-between items-center py-2 border-b">
<span class="text-sm">${index + 1}. ${item.filename}</span>
<a href="${item.url}" download="${item.filename}"
class="text-blue-600 hover:text-blue-800 text-sm">下载</a>
</div>`
).join('');
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-white rounded-xl p-6 max-w-md w-full mx-4 max-h-96 overflow-y-auto">
<h3 class="text-lg font-bold text-gray-800 mb-4">图片下载列表</h3>
<div class="mb-4">
<p class="text-sm text-gray-600 mb-3">共找到 ${imageUrls.length} 张图片,请点击下载:</p>
${list}
</div>
<button onclick="this.parentElement.parentElement.remove()"
class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
关闭
</button>
</div>
`;
document.body.appendChild(modal);
}
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
});
}
}
// 初始化导出管理器
const exportManager = new ExportManager();
</script>
</body>
</html>