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.
873 lines
41 KiB
HTML
873 lines
41 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>历史记录 - 物品数据库管理工具</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/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> |