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:
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>
|
||||
Reference in New Issue
Block a user