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.
747 lines
35 KiB
HTML
747 lines
35 KiB
HTML
<!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> |