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.
This commit is contained in:
63
prototype/design.md
Normal file
63
prototype/design.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 物品数据库工具 - 设计风格指南
|
||||||
|
|
||||||
|
## 设计理念
|
||||||
|
|
||||||
|
### 核心设计原则
|
||||||
|
- **专业商务风格**:采用现代简约的设计语言,体现工具的专业性和可靠性
|
||||||
|
- **功能优先**:界面设计以实用性为核心,确保每个元素都有明确的功能目的
|
||||||
|
- **清晰易读**:使用高对比度的文字和背景,确保在不同设备上都能清晰阅读
|
||||||
|
- **直观操作**:通过合理的布局和视觉引导,让用户能够快速理解和使用各项功能
|
||||||
|
|
||||||
|
### 色彩方案
|
||||||
|
- **主色调**:深蓝灰 (#2C3E50) - 传达专业性和信任感
|
||||||
|
- **辅助色**:柔和蓝 (#3498DB) - 用于按钮和链接,体现科技感
|
||||||
|
- **强调色**:温暖橙 (#E67E22) - 用于重要操作和状态提示
|
||||||
|
- **中性色**:浅灰 (#ECF0F1) - 用于背景和分隔区域
|
||||||
|
- **文字色**:深灰 (#34495E) - 确保良好的可读性
|
||||||
|
|
||||||
|
### 字体系统
|
||||||
|
- **标题字体**:Noto Serif SC - 用于页面标题和重要标题,体现权威感
|
||||||
|
- **正文字体**:Noto Sans SC - 用于正文内容和界面文字,确保清晰易读
|
||||||
|
- **代码字体**:JetBrains Mono - 用于CSV预览和数据展示,保持等宽特性
|
||||||
|
|
||||||
|
### 布局原则
|
||||||
|
- **网格系统**:采用12列网格布局,确保内容在不同屏幕尺寸下的合理分布
|
||||||
|
- **间距规范**:使用8px基础间距系统,保持视觉节奏的一致性
|
||||||
|
- **卡片设计**:使用圆角卡片来组织内容,提供清晰的视觉层次
|
||||||
|
- **留白运用**:充分利用留白来减少视觉噪音,突出重要信息
|
||||||
|
|
||||||
|
## 视觉效果
|
||||||
|
|
||||||
|
### 使用的核心库
|
||||||
|
- **Anime.js**:用于页面切换和元素动画,提供流畅的交互体验
|
||||||
|
- **ECharts.js**:用于历史价格趋势图和统计图表的数据可视化
|
||||||
|
- **Splide.js**:用于图片轮播和物品展示的滑动效果
|
||||||
|
- **Matter.js**:用于拖拽排序时的物理效果,增强操作反馈
|
||||||
|
- **Pixi.js**:用于背景装饰效果和视觉特效
|
||||||
|
|
||||||
|
### 动画效果
|
||||||
|
- **页面切换**:使用淡入淡出和轻微的位移动画,保持流畅的导航体验
|
||||||
|
- **卡片交互**:悬停时轻微上浮和阴影加深,点击时轻微缩放反馈
|
||||||
|
- **数据加载**:使用骨架屏和进度指示器,提供良好的等待体验
|
||||||
|
- **操作反馈**:成功操作使用绿色提示,错误使用红色提示,持续2秒
|
||||||
|
|
||||||
|
### 背景设计
|
||||||
|
- **主背景**:使用纯色背景 (#FAFBFC),保持内容的清晰度
|
||||||
|
- **装饰元素**:在页面边角使用几何图形装饰,增加视觉趣味
|
||||||
|
- **渐变应用**:仅在按钮和重要元素上使用轻微渐变,避免过度装饰
|
||||||
|
|
||||||
|
### 图标系统
|
||||||
|
- **功能图标**:使用线性图标风格,保持一致的视觉语言
|
||||||
|
- **状态图标**:使用填充式图标来表示不同的状态和操作结果
|
||||||
|
- **尺寸规范**:功能图标使用24px,状态图标使用16px,保持一致性
|
||||||
|
|
||||||
|
### 响应式设计
|
||||||
|
- **桌面端** (1200px+):三列布局,充分利用屏幕空间展示更多信息
|
||||||
|
- **平板端** (768px-1199px):两列布局,保持核心功能的可用性
|
||||||
|
- **移动端** (< 768px):单列布局,优化触摸操作和滚动体验
|
||||||
|
|
||||||
|
### 交互状态
|
||||||
|
- **默认状态**:清晰的边框和适当的内边距
|
||||||
|
- **悬停状态**:轻微的颜色变化和阴影加深,提供视觉反馈
|
||||||
|
- **激活状态**:颜色加深和轻微内阴影,表示正在交互
|
||||||
|
- **禁用状态**:降低透明度和灰度处理,明确不可用状态
|
||||||
747
prototype/export.html
Normal file
747
prototype/export.html
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
<!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>
|
||||||
873
prototype/history.html
Normal file
873
prototype/history.html
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
<!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/echarts@5.4.3/dist/echarts.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);
|
||||||
|
}
|
||||||
|
.chart-container {
|
||||||
|
height: 400px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.stats-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 4px solid #3498DB;
|
||||||
|
}
|
||||||
|
.record-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 3px solid #E67E22;
|
||||||
|
}
|
||||||
|
.record-item.price {
|
||||||
|
border-left-color: #3498DB;
|
||||||
|
}
|
||||||
|
.record-item.sale {
|
||||||
|
border-left-color: #E67E22;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.filter-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.filter-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 16px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.filter-tab.active {
|
||||||
|
background: #3498DB;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);
|
||||||
|
}
|
||||||
|
.filter-tab:not(.active) {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.filter-tab:not(.active):hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
</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-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
|
<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-blue-600 bg-blue-50 rounded-lg font-medium">
|
||||||
|
<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">查看物品价格趋势、成交记录和统计分析</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 fade-in">
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600">总记录数</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="total-records">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-blue-100 rounded-full">
|
||||||
|
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600">价格记录</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="price-records">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-green-100 rounded-full">
|
||||||
|
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600">成交记录</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="sale-records">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-orange-100 rounded-full">
|
||||||
|
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600">平均成交价</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="avg-price">¥0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-purple-100 rounded-full">
|
||||||
|
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 fade-in">
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-4 sm:mb-0 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 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||||
|
</svg>
|
||||||
|
价格趋势分析
|
||||||
|
</h3>
|
||||||
|
<div class="filter-tabs">
|
||||||
|
<div class="filter-tab active" data-period="7">7天</div>
|
||||||
|
<div class="filter-tab" data-period="30">30天</div>
|
||||||
|
<div class="filter-tab" data-period="90">90天</div>
|
||||||
|
<div class="filter-tab" data-period="365">1年</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物品选择 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">选择物品</label>
|
||||||
|
<select id="item-select" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
<option value="">请选择要查看的物品</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表容器 -->
|
||||||
|
<div id="price-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计分析图表 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 mt-6 fade-in">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-6 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 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||||
|
</svg>
|
||||||
|
统计分析
|
||||||
|
</h3>
|
||||||
|
<div id="stats-chart" class="chart-container"></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 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
添加记录
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form id="add-record-form" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">物品</label>
|
||||||
|
<select id="record-item-select" required class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
<option value="">请选择物品</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">记录类型</label>
|
||||||
|
<select id="record-type" required class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
<option value="price">价格记录</option>
|
||||||
|
<option value="sale">成交记录</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">价格 (元)</label>
|
||||||
|
<input type="number" id="record-price" required min="0" step="0.01"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="请输入价格">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">活动名称</label>
|
||||||
|
<input type="text" id="record-event"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="如:春季拍卖会">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">日期</label>
|
||||||
|
<input type="date" id="record-date" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">备注</label>
|
||||||
|
<textarea id="record-notes" rows="3"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="添加备注信息(可选)"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" 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 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
添加记录
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 历史记录列表 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 mt-6 fade-in">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 flex items-center">
|
||||||
|
<svg class="w-6 h-6 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
历史记录
|
||||||
|
</h3>
|
||||||
|
<button id="export-history-btn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-sm">
|
||||||
|
导出记录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选选项 -->
|
||||||
|
<div class="mb-4 space-y-3">
|
||||||
|
<select id="filter-item" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||||
|
<option value="">所有物品</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-type" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||||
|
<option value="">所有类型</option>
|
||||||
|
<option value="price">价格记录</option>
|
||||||
|
<option value="sale">成交记录</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 记录列表 -->
|
||||||
|
<div id="records-list" class="max-h-96 overflow-y-auto">
|
||||||
|
<!-- 动态生成的记录列表 -->
|
||||||
|
</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 HistoryManager {
|
||||||
|
constructor() {
|
||||||
|
this.items = this.loadItems();
|
||||||
|
this.history = this.loadHistory();
|
||||||
|
this.currentPeriod = 7;
|
||||||
|
this.selectedItemId = '';
|
||||||
|
this.priceChart = null;
|
||||||
|
this.statsChart = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.populateItemSelectors();
|
||||||
|
this.updateStats();
|
||||||
|
this.renderRecords();
|
||||||
|
this.initCharts();
|
||||||
|
this.initAnimations();
|
||||||
|
this.loadSampleHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadItems() {
|
||||||
|
const data = localStorage.getItem('itemdb-items');
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadHistory() {
|
||||||
|
const data = localStorage.getItem('itemdb-history');
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSampleHistory() {
|
||||||
|
if (this.history.length === 0 && this.items.length > 0) {
|
||||||
|
const sampleHistory = [
|
||||||
|
{
|
||||||
|
id: this.generateId(),
|
||||||
|
itemId: this.items[0].id,
|
||||||
|
type: 'price',
|
||||||
|
price: 50000,
|
||||||
|
date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
event: '市场估价',
|
||||||
|
notes: '专业评估师估价'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: this.generateId(),
|
||||||
|
itemId: this.items[0].id,
|
||||||
|
type: 'sale',
|
||||||
|
price: 65000,
|
||||||
|
date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
event: '春季拍卖会',
|
||||||
|
notes: '成功拍出'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: this.generateId(),
|
||||||
|
itemId: this.items[0].id,
|
||||||
|
type: 'price',
|
||||||
|
price: 70000,
|
||||||
|
date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
event: '市场估价',
|
||||||
|
notes: '近期市场价格上涨'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.history = sampleHistory;
|
||||||
|
this.saveHistory();
|
||||||
|
this.updateStats();
|
||||||
|
this.renderRecords();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// 表单提交
|
||||||
|
document.getElementById('add-record-form').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.addRecord();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 物品选择
|
||||||
|
document.getElementById('item-select').addEventListener('change', (e) => {
|
||||||
|
this.selectedItemId = e.target.value;
|
||||||
|
this.updatePriceChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 筛选器
|
||||||
|
document.getElementById('filter-item').addEventListener('change', () => {
|
||||||
|
this.renderRecords();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filter-type').addEventListener('change', () => {
|
||||||
|
this.renderRecords();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 时间筛选
|
||||||
|
document.querySelectorAll('.filter-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
e.target.classList.add('active');
|
||||||
|
this.currentPeriod = parseInt(e.target.dataset.period);
|
||||||
|
this.updatePriceChart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出历史记录
|
||||||
|
document.getElementById('export-history-btn').addEventListener('click', () => {
|
||||||
|
this.exportHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置默认日期为今天
|
||||||
|
document.getElementById('record-date').value = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
populateItemSelectors() {
|
||||||
|
const selectors = ['item-select', 'record-item-select', 'filter-item'];
|
||||||
|
|
||||||
|
selectors.forEach(selectorId => {
|
||||||
|
const selector = document.getElementById(selectorId);
|
||||||
|
const defaultOption = selector.querySelector('option[value=""]');
|
||||||
|
|
||||||
|
// 清除现有选项(保留默认选项)
|
||||||
|
selector.innerHTML = '';
|
||||||
|
selector.appendChild(defaultOption);
|
||||||
|
|
||||||
|
this.items.forEach(item => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = item.id;
|
||||||
|
option.textContent = item.name;
|
||||||
|
selector.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addRecord() {
|
||||||
|
const itemId = document.getElementById('record-item-select').value;
|
||||||
|
const type = document.getElementById('record-type').value;
|
||||||
|
const price = parseFloat(document.getElementById('record-price').value);
|
||||||
|
const event = document.getElementById('record-event').value;
|
||||||
|
const date = document.getElementById('record-date').value;
|
||||||
|
const notes = document.getElementById('record-notes').value;
|
||||||
|
|
||||||
|
if (!itemId || !price || !date) {
|
||||||
|
this.showNotification('请填写必填项', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRecord = {
|
||||||
|
id: this.generateId(),
|
||||||
|
itemId,
|
||||||
|
type,
|
||||||
|
price,
|
||||||
|
date,
|
||||||
|
event: event || '未指定',
|
||||||
|
notes: notes || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.history.unshift(newRecord);
|
||||||
|
this.saveHistory();
|
||||||
|
this.updateStats();
|
||||||
|
this.renderRecords();
|
||||||
|
this.updatePriceChart();
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
document.getElementById('add-record-form').reset();
|
||||||
|
document.getElementById('record-date').value = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
this.showNotification('记录添加成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRecord(recordId) {
|
||||||
|
if (confirm('确定要删除这条记录吗?')) {
|
||||||
|
this.history = this.history.filter(record => record.id !== recordId);
|
||||||
|
this.saveHistory();
|
||||||
|
this.updateStats();
|
||||||
|
this.renderRecords();
|
||||||
|
this.updatePriceChart();
|
||||||
|
this.showNotification('记录删除成功', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRecords() {
|
||||||
|
const container = document.getElementById('records-list');
|
||||||
|
const filterItem = document.getElementById('filter-item').value;
|
||||||
|
const filterType = document.getElementById('filter-type').value;
|
||||||
|
|
||||||
|
let filteredRecords = this.history;
|
||||||
|
|
||||||
|
if (filterItem) {
|
||||||
|
filteredRecords = filteredRecords.filter(record => record.itemId === filterItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterType) {
|
||||||
|
filteredRecords = filteredRecords.filter(record => record.type === filterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredRecords.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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<p>暂无历史记录</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = filteredRecords.map(record => {
|
||||||
|
const item = this.items.find(i => i.id === record.itemId);
|
||||||
|
const itemName = item ? item.name : '未知物品';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="record-item ${record.type}">
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-medium text-gray-800 text-sm">${itemName}</h4>
|
||||||
|
<p class="text-xs text-gray-600">${record.event}</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="historyManager.deleteRecord('${record.id}')"
|
||||||
|
class="text-red-500 hover:text-red-700 ml-2">
|
||||||
|
<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="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 class="flex justify-between items-center">
|
||||||
|
<span class="text-lg font-bold ${record.type === 'price' ? 'text-blue-600' : 'text-orange-600'}">
|
||||||
|
¥${record.price.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500">${new Date(record.date).toLocaleDateString('zh-CN')}</span>
|
||||||
|
</div>
|
||||||
|
${record.notes ? `<p class="text-xs text-gray-600 mt-2">${record.notes}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats() {
|
||||||
|
const totalRecords = this.history.length;
|
||||||
|
const priceRecords = this.history.filter(r => r.type === 'price').length;
|
||||||
|
const saleRecords = this.history.filter(r => r.type === 'sale').length;
|
||||||
|
const salePrices = this.history.filter(r => r.type === 'sale').map(r => r.price);
|
||||||
|
const avgPrice = salePrices.length > 0 ? salePrices.reduce((a, b) => a + b, 0) / salePrices.length : 0;
|
||||||
|
|
||||||
|
document.getElementById('total-records').textContent = totalRecords;
|
||||||
|
document.getElementById('price-records').textContent = priceRecords;
|
||||||
|
document.getElementById('sale-records').textContent = saleRecords;
|
||||||
|
document.getElementById('avg-price').textContent = `¥${Math.round(avgPrice).toLocaleString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
initCharts() {
|
||||||
|
// 初始化价格趋势图表
|
||||||
|
this.priceChart = echarts.init(document.getElementById('price-chart'));
|
||||||
|
|
||||||
|
// 初始化统计图表
|
||||||
|
this.statsChart = echarts.init(document.getElementById('stats-chart'));
|
||||||
|
|
||||||
|
// 更新统计图表
|
||||||
|
this.updateStatsChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePriceChart() {
|
||||||
|
if (!this.selectedItemId || !this.priceChart) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const startDate = new Date(now.getTime() - this.currentPeriod * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const itemHistory = this.history
|
||||||
|
.filter(record => record.itemId === this.selectedItemId)
|
||||||
|
.filter(record => new Date(record.date) >= startDate)
|
||||||
|
.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||||
|
|
||||||
|
if (itemHistory.length === 0) {
|
||||||
|
this.priceChart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '暂无数据',
|
||||||
|
left: 'center',
|
||||||
|
top: 'middle',
|
||||||
|
textStyle: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: { show: false },
|
||||||
|
yAxis: { show: false },
|
||||||
|
series: []
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates = itemHistory.map(r => r.date);
|
||||||
|
const prices = itemHistory.map(r => r.price);
|
||||||
|
const types = itemHistory.map(r => r.type);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '价格趋势',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
color: '#2C3E50',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: function(params) {
|
||||||
|
const data = params[0];
|
||||||
|
const record = itemHistory[data.dataIndex];
|
||||||
|
return `
|
||||||
|
<div style="padding: 8px;">
|
||||||
|
<strong>${data.name}</strong><br/>
|
||||||
|
价格: ¥${data.value.toLocaleString()}<br/>
|
||||||
|
类型: ${record.type === 'price' ? '价格记录' : '成交记录'}<br/>
|
||||||
|
活动: ${record.event}<br/>
|
||||||
|
${record.notes ? `备注: ${record.notes}` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['价格记录', '成交记录'],
|
||||||
|
bottom: 10
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: dates,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: function(value) {
|
||||||
|
return new Date(value).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '¥{value}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '价格',
|
||||||
|
type: 'line',
|
||||||
|
data: prices,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
color: '#3498DB'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: function(params) {
|
||||||
|
return types[params.dataIndex] === 'price' ? '#3498DB' : '#E67E22';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
markPoint: {
|
||||||
|
data: [
|
||||||
|
{ type: 'max', name: '最高价' },
|
||||||
|
{ type: 'min', name: '最低价' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.priceChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatsChart() {
|
||||||
|
if (!this.statsChart) return;
|
||||||
|
|
||||||
|
const typeData = [
|
||||||
|
{ name: '价格记录', value: this.history.filter(r => r.type === 'price').length },
|
||||||
|
{ name: '成交记录', value: this.history.filter(r => r.type === 'sale').length }
|
||||||
|
];
|
||||||
|
|
||||||
|
const itemData = this.items.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
value: this.history.filter(r => r.itemId === item.id).length
|
||||||
|
})).filter(item => item.value > 0);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '记录分布统计',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
color: '#2C3E50',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'horizontal',
|
||||||
|
bottom: 10,
|
||||||
|
data: ['记录类型分布', '物品记录分布']
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '记录类型',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['20%', '40%'],
|
||||||
|
center: ['25%', '50%'],
|
||||||
|
data: typeData,
|
||||||
|
itemStyle: {
|
||||||
|
color: function(params) {
|
||||||
|
return params.name === '价格记录' ? '#3498DB' : '#E67E22';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '物品记录',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['20%', '40%'],
|
||||||
|
center: ['75%', '50%'],
|
||||||
|
data: itemData,
|
||||||
|
itemStyle: {
|
||||||
|
color: function(params) {
|
||||||
|
const colors = ['#3498DB', '#E67E22', '#2ECC71', '#F39C12', '#9B59B6', '#1ABC9C'];
|
||||||
|
return colors[params.dataIndex % colors.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.statsChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportHistory() {
|
||||||
|
if (this.history.length === 0) {
|
||||||
|
this.showNotification('暂无历史记录可导出', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let csvContent = '物品名称,记录类型,价格,日期,活动名称,备注\n';
|
||||||
|
|
||||||
|
this.history.forEach(record => {
|
||||||
|
const item = this.items.find(i => i.id === record.itemId);
|
||||||
|
const itemName = item ? item.name : '未知物品';
|
||||||
|
|
||||||
|
const row = [
|
||||||
|
`"${itemName}"`,
|
||||||
|
record.type === 'price' ? '价格记录' : '成交记录',
|
||||||
|
record.price,
|
||||||
|
record.date,
|
||||||
|
`"${record.event}"`,
|
||||||
|
`"${record.notes || ''}"`
|
||||||
|
].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('历史记录导出成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
generateId() {
|
||||||
|
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveHistory() {
|
||||||
|
localStorage.setItem('itemdb-history', JSON.stringify(this.history));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 historyManager = new HistoryManager();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
346
prototype/index.html
Normal file
346
prototype/index.html
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<!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/splide@4.1.4/dist/js/splide.min.js"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/splide@4.1.4/dist/css/splide.min.css" rel="stylesheet">
|
||||||
|
<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(-4px);
|
||||||
|
box-shadow: 0 20px 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-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.item-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.item-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: linear-gradient(45deg, #f0f0f0, #e0e0e0);
|
||||||
|
}
|
||||||
|
.loading-skeleton {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
@keyframes loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: #3498DB;
|
||||||
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
</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-blue-600 bg-blue-50 rounded-lg font-medium">物品管理</a>
|
||||||
|
<a href="export.html" class="px-4 py-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
|
<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-12 fade-in">
|
||||||
|
<h2 class="hero-title text-4xl md:text-5xl 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 md:grid-cols-4 gap-6 mb-8 fade-in">
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-lg bg-blue-100">
|
||||||
|
<svg class="w-6 h-6 text-blue-600" 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>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">总物品数</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="total-items">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-lg bg-green-100">
|
||||||
|
<svg class="w-6 h-6 text-green-600" 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>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">本月新增</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="monthly-items">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-lg bg-orange-100">
|
||||||
|
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">历史记录</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="history-records">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-3 rounded-lg bg-purple-100">
|
||||||
|
<svg class="w-6 h-6 text-purple-600" 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>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">图片总数</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900" id="total-images">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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-6 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="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
添加新物品
|
||||||
|
</h3>
|
||||||
|
<form id="add-item-form" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">物品名称 *</label>
|
||||||
|
<input type="text" id="item-name" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
|
placeholder="请输入物品名称">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">图片 URL *</label>
|
||||||
|
<input type="url" id="item-image" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
|
placeholder="https://example.com/image.jpg">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">请粘贴您的图床图片链接</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">物品描述</label>
|
||||||
|
<textarea id="item-description" rows="3"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
|
placeholder="请输入物品描述(可选)"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">分类标签</label>
|
||||||
|
<input type="text" id="item-tags"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
|
placeholder="用逗号分隔多个标签,如:古董,瓷器">
|
||||||
|
</div>
|
||||||
|
<button type="submit" 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 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
添加物品
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速操作 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 mt-6 fade-in">
|
||||||
|
<h3 class="text-lg font-bold text-gray-800 mb-4">快速操作</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button id="export-data-btn" class="w-full flex items-center justify-center px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-gray-600" 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>
|
||||||
|
导出数据备份
|
||||||
|
</button>
|
||||||
|
<button id="import-data-btn" class="w-full flex items-center justify-center px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
|
||||||
|
</svg>
|
||||||
|
导入数据
|
||||||
|
</button>
|
||||||
|
<input type="file" id="import-file-input" accept=".json" class="hidden">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物品列表 -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 fade-in">
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-4 sm:mb-0">物品列表</h3>
|
||||||
|
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3 w-full sm:w-auto">
|
||||||
|
<input type="text" id="search-input" placeholder="搜索物品名称..."
|
||||||
|
class="search-input px-4 py-2 rounded-lg focus:outline-none w-full sm:w-64">
|
||||||
|
<select id="filter-select" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 w-full sm:w-auto">
|
||||||
|
<option value="">所有分类</option>
|
||||||
|
<option value="古董">古董</option>
|
||||||
|
<option value="艺术品">艺术品</option>
|
||||||
|
<option value="收藏品">收藏品</option>
|
||||||
|
<option value="珠宝">珠宝</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物品网格 -->
|
||||||
|
<div id="items-grid" class="item-grid">
|
||||||
|
<!-- 动态生成的物品卡片将在这里显示 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div id="empty-state" class="text-center py-12 hidden">
|
||||||
|
<svg class="w-16 h-16 text-gray-300 mx-auto mb-4" 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>
|
||||||
|
<h3 class="text-lg font-medium text-gray-600 mb-2">暂无物品</h3>
|
||||||
|
<p class="text-gray-500">点击左侧添加您的第一个物品</p>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 编辑物品模态框 -->
|
||||||
|
<div id="edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-xl p-6 max-w-md w-full mx-4 transform transition-all">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-4">编辑物品</h3>
|
||||||
|
<form id="edit-form" class="space-y-4">
|
||||||
|
<input type="hidden" id="edit-item-id">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">物品名称</label>
|
||||||
|
<input type="text" id="edit-item-name" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">图片 URL</label>
|
||||||
|
<input type="url" id="edit-item-image" required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">物品描述</label>
|
||||||
|
<textarea id="edit-item-description" rows="3"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">分类标签</label>
|
||||||
|
<input type="text" id="edit-item-tags"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3 pt-4">
|
||||||
|
<button type="submit" class="flex-1 btn-primary text-white font-medium py-2 px-4 rounded-lg">
|
||||||
|
保存修改
|
||||||
|
</button>
|
||||||
|
<button type="button" id="cancel-edit" class="flex-1 bg-gray-200 text-gray-800 font-medium py-2 px-4 rounded-lg hover:bg-gray-300 transition-colors">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
prototype/interaction.md
Normal file
46
prototype/interaction.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 物品数据库和构建清单工具 - 交互设计
|
||||||
|
|
||||||
|
## 核心交互功能
|
||||||
|
|
||||||
|
### 1. 物品管理系统
|
||||||
|
- **物品添加界面**:用户可以输入物品名称,粘贴图片URL,添加描述信息
|
||||||
|
- **物品列表展示**:网格布局显示所有物品,包含缩略图、名称、创建日期
|
||||||
|
- **物品编辑功能**:点击物品卡片可编辑名称、图片URL、描述信息
|
||||||
|
- **物品删除功能**:支持单个删除和批量删除操作
|
||||||
|
|
||||||
|
### 2. 导出配置系统
|
||||||
|
- **物品选择界面**:左侧显示所有可用物品,右侧显示已选择的导出物品列表
|
||||||
|
- **拖拽排序功能**:用户可以通过拖拽调整导出物品的顺序
|
||||||
|
- **批量设置功能**:可以为选中的多个物品批量设置起拍价和备注
|
||||||
|
- **导出预览功能**:实时显示将要导出的CSV内容预览
|
||||||
|
|
||||||
|
### 3. 历史记录管理
|
||||||
|
- **价格历史追踪**:记录每个物品的历史价格变化,包含日期和活动名称
|
||||||
|
- **成交记录管理**:记录每次拍卖的成交价、日期和相关活动
|
||||||
|
- **数据可视化**:使用图表展示价格趋势和成交统计
|
||||||
|
- **记录添加界面**:可以手动添加历史价格和成交记录
|
||||||
|
|
||||||
|
### 4. 数据导入导出
|
||||||
|
- **CSV导出功能**:按照指定格式导出物品清单,支持自定义文件名
|
||||||
|
- **图片打包下载**:收集所有选中物品的图片URL,生成压缩包下载
|
||||||
|
- **数据备份功能**:支持将整个数据库导出为JSON文件进行备份
|
||||||
|
- **数据导入功能**:可以从JSON文件恢复数据
|
||||||
|
|
||||||
|
## 用户交互流程
|
||||||
|
|
||||||
|
### 主要操作流程
|
||||||
|
1. **添加物品** → 用户输入物品信息 → 保存到数据库
|
||||||
|
2. **配置导出** → 选择物品 → 设置价格备注 → 预览 → 导出CSV
|
||||||
|
3. **查看历史** → 选择物品 → 查看价格和成交历史 → 添加新记录
|
||||||
|
4. **数据管理** → 备份数据 → 导入数据 → 清理无用记录
|
||||||
|
|
||||||
|
### 界面布局设计
|
||||||
|
- **顶部导航栏**:物品管理、导出配置、历史记录、数据管理
|
||||||
|
- **主工作区域**:根据当前功能显示对应的操作界面
|
||||||
|
- **侧边信息栏**:显示统计信息、快速操作按钮
|
||||||
|
- **底部状态栏**:显示当前操作状态和提示信息
|
||||||
|
|
||||||
|
## 响应式设计考虑
|
||||||
|
- **桌面端**:充分利用屏幕空间,支持多列布局和复杂交互
|
||||||
|
- **平板端**:适配中等屏幕,保持核心功能可用性
|
||||||
|
- **移动端**:简化界面,重点突出常用功能,优化触摸操作
|
||||||
457
prototype/main.js
vendored
Normal file
457
prototype/main.js
vendored
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
// 物品数据库管理工具 - 主要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();
|
||||||
179
prototype/outline.md
Normal file
179
prototype/outline.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# 物品数据库工具 - 项目文件结构大纲
|
||||||
|
|
||||||
|
## 文件结构概述
|
||||||
|
|
||||||
|
```
|
||||||
|
/mnt/okcomputer/output/
|
||||||
|
├── index.html # 主页面 - 物品管理界面
|
||||||
|
├── export.html # 导出配置页面
|
||||||
|
├── history.html # 历史记录页面
|
||||||
|
├── main.js # 主要JavaScript逻辑
|
||||||
|
├── resources/ # 资源文件夹
|
||||||
|
│ ├── hero-bg.jpg # 主页背景图片
|
||||||
|
│ ├── database-icon.png # 数据库图标
|
||||||
|
│ ├── export-icon.png # 导出图标
|
||||||
|
│ └── chart-icon.png # 图表图标
|
||||||
|
└── data/ # 数据文件夹
|
||||||
|
├── items.json # 物品数据库
|
||||||
|
├── history.json # 历史记录数据
|
||||||
|
└── export-config.json # 导出配置数据
|
||||||
|
```
|
||||||
|
|
||||||
|
## 页面功能详细说明
|
||||||
|
|
||||||
|
### 1. index.html - 物品管理主页面
|
||||||
|
**主要功能**:
|
||||||
|
- 物品添加表单(名称、图片URL、描述)
|
||||||
|
- 物品列表展示(网格布局,支持搜索和筛选)
|
||||||
|
- 物品编辑和删除功能
|
||||||
|
- 快速统计信息显示
|
||||||
|
|
||||||
|
**页面结构**:
|
||||||
|
- 顶部导航栏:包含页面切换和功能按钮
|
||||||
|
- 英雄区域:简洁的标题和工具介绍
|
||||||
|
- 主要内容区域:左侧添加表单,右侧物品网格
|
||||||
|
- 底部状态栏:显示当前操作状态和统计信息
|
||||||
|
|
||||||
|
**交互组件**:
|
||||||
|
- 物品添加表单:实时验证和反馈
|
||||||
|
- 物品卡片:悬停效果和点击交互
|
||||||
|
- 搜索和筛选:实时过滤物品列表
|
||||||
|
- 批量操作:支持多选和批量删除
|
||||||
|
|
||||||
|
### 2. export.html - 导出配置页面
|
||||||
|
**主要功能**:
|
||||||
|
- 物品选择界面(左侧可用物品,右侧选中物品)
|
||||||
|
- 价格和备注设置(支持批量设置)
|
||||||
|
- CSV格式预览
|
||||||
|
- 导出文件生成和下载
|
||||||
|
|
||||||
|
**页面结构**:
|
||||||
|
- 导航栏:返回主页和切换页面
|
||||||
|
- 配置区域:双列布局的物品选择器
|
||||||
|
- 设置面板:价格、备注的批量设置
|
||||||
|
- 预览区域:实时CSV内容预览
|
||||||
|
- 操作按钮:导出CSV、下载图片包
|
||||||
|
|
||||||
|
**交互组件**:
|
||||||
|
- 拖拽排序:支持物品顺序调整
|
||||||
|
- 批量设置:一键应用到多个物品
|
||||||
|
- 实时预览:CSV格式实时更新
|
||||||
|
- 文件下载:CSV和图片压缩包
|
||||||
|
|
||||||
|
### 3. history.html - 历史记录页面
|
||||||
|
**主要功能**:
|
||||||
|
- 历史价格记录展示
|
||||||
|
- 成交记录管理
|
||||||
|
- 价格趋势图表
|
||||||
|
- 记录添加和编辑
|
||||||
|
|
||||||
|
**页面结构**:
|
||||||
|
- 导航栏:页面切换和功能选择
|
||||||
|
- 筛选面板:按物品、日期、活动筛选
|
||||||
|
- 图表区域:价格趋势和统计图表
|
||||||
|
- 记录列表:详细的历史记录表格
|
||||||
|
- 操作面板:添加新记录和编辑功能
|
||||||
|
|
||||||
|
**交互组件**:
|
||||||
|
- 图表交互:ECharts图表的缩放和筛选
|
||||||
|
- 记录管理:添加、编辑、删除历史记录
|
||||||
|
- 数据筛选:多条件组合筛选
|
||||||
|
- 导出功能:历史数据导出为CSV
|
||||||
|
|
||||||
|
## JavaScript功能模块
|
||||||
|
|
||||||
|
### main.js 主要功能模块
|
||||||
|
1. **数据管理模块**
|
||||||
|
- 物品数据的CRUD操作
|
||||||
|
- 本地存储管理
|
||||||
|
- 数据导入导出
|
||||||
|
|
||||||
|
2. **UI交互模块**
|
||||||
|
- 页面切换动画
|
||||||
|
- 表单验证和提交
|
||||||
|
- 列表渲染和更新
|
||||||
|
|
||||||
|
3. **导出功能模块**
|
||||||
|
- CSV格式生成
|
||||||
|
- 图片URL收集
|
||||||
|
- 文件下载处理
|
||||||
|
|
||||||
|
4. **图表可视化模块**
|
||||||
|
- ECharts图表初始化
|
||||||
|
- 数据转换和绑定
|
||||||
|
- 交互事件处理
|
||||||
|
|
||||||
|
5. **工具函数模块**
|
||||||
|
- 日期格式化
|
||||||
|
- 数据验证
|
||||||
|
- 文件操作
|
||||||
|
|
||||||
|
## 数据结构设计
|
||||||
|
|
||||||
|
### items.json 物品数据结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "unique-id",
|
||||||
|
"name": "物品名称",
|
||||||
|
"imageUrl": "图片URL",
|
||||||
|
"description": "物品描述",
|
||||||
|
"createdAt": "创建时间",
|
||||||
|
"updatedAt": "更新时间",
|
||||||
|
"category": "分类",
|
||||||
|
"tags": ["标签1", "标签2"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### history.json 历史记录数据结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"id": "record-id",
|
||||||
|
"itemId": "物品ID",
|
||||||
|
"type": "price|sale",
|
||||||
|
"price": 价格,
|
||||||
|
"date": "日期",
|
||||||
|
"event": "活动名称",
|
||||||
|
"notes": "备注"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### export-config.json 导出配置结构
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"selectedItems": ["物品ID列表"],
|
||||||
|
"prices": {"物品ID": 价格},
|
||||||
|
"notes": {"物品ID": "备注"},
|
||||||
|
"exportSettings": {
|
||||||
|
"includeImages": true,
|
||||||
|
"csvFormat": "standard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈和库依赖
|
||||||
|
|
||||||
|
### 核心库使用
|
||||||
|
- **Anime.js**:页面切换动画和元素交互效果
|
||||||
|
- **ECharts.js**:历史价格趋势图表和统计图表
|
||||||
|
- **Splide.js**:物品图片轮播和展示
|
||||||
|
- **Matter.js**:拖拽排序的物理效果
|
||||||
|
- **Pixi.js**:背景装饰效果和视觉特效
|
||||||
|
|
||||||
|
### 样式框架
|
||||||
|
- **Tailwind CSS**:响应式布局和组件样式
|
||||||
|
- **自定义CSS**:特殊效果和主题样式
|
||||||
|
|
||||||
|
### 数据处理
|
||||||
|
- **原生JavaScript**:数据处理逻辑
|
||||||
|
- **LocalStorage API**:数据持久化存储
|
||||||
|
- **File API**:文件导入导出功能
|
||||||
BIN
prototype/resources/chart-icon.png
Normal file
BIN
prototype/resources/chart-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 557 KiB |
BIN
prototype/resources/database-icon.png
Normal file
BIN
prototype/resources/database-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 344 KiB |
BIN
prototype/resources/export-icon.png
Normal file
BIN
prototype/resources/export-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
BIN
prototype/resources/hero-bg.jpg
Normal file
BIN
prototype/resources/hero-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
Reference in New Issue
Block a user