tmp.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>简易链接管理器</title>
  7. <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.2/papaparse.min.js"></script>
  8. <style>
  9. body {
  10. font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  11. line-height: 1.6;
  12. margin: 0;
  13. padding: 20px;
  14. color: #333;
  15. background-color: #f9f9f9;
  16. }
  17. .container {
  18. max-width: 1200px;
  19. margin: 0 auto;
  20. }
  21. .header {
  22. display: flex;
  23. justify-content: space-between;
  24. align-items: center;
  25. margin-bottom: 20px;
  26. }
  27. h1 {
  28. color: #2c3e50;
  29. margin: 0;
  30. }
  31. .toolbar {
  32. display: flex;
  33. gap: 10px;
  34. margin-bottom: 20px;
  35. flex-wrap: wrap;
  36. }
  37. .tag-list {
  38. display: flex;
  39. flex-wrap: wrap;
  40. gap: 8px;
  41. margin-bottom: 20px;
  42. }
  43. .tag {
  44. background-color: #e0f2fe;
  45. color: #0369a1;
  46. padding: 5px 10px;
  47. border-radius: 15px;
  48. cursor: pointer;
  49. transition: all 0.2s;
  50. user-select: none;
  51. }
  52. .tag.active {
  53. background-color: #0284c7;
  54. color: white;
  55. }
  56. .tag:hover {
  57. opacity: 0.8;
  58. }
  59. input, button, select {
  60. padding: 8px 12px;
  61. border: 1px solid #ddd;
  62. border-radius: 4px;
  63. font-size: 14px;
  64. }
  65. input:focus, select:focus {
  66. outline: none;
  67. border-color: #0284c7;
  68. }
  69. button {
  70. background-color: #0284c7;
  71. color: white;
  72. cursor: pointer;
  73. border: none;
  74. }
  75. button:hover {
  76. background-color: #0369a1;
  77. }
  78. table {
  79. width: 100%;
  80. border-collapse: collapse;
  81. margin-top: 20px;
  82. background-color: white;
  83. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  84. border-radius: 8px;
  85. overflow: hidden;
  86. }
  87. th, td {
  88. padding: 12px 15px;
  89. text-align: left;
  90. border-bottom: 1px solid #eee;
  91. }
  92. th {
  93. background-color: #f1f5f9;
  94. font-weight: 600;
  95. color: #334155;
  96. }
  97. tr:hover {
  98. background-color: #f1f5f9;
  99. }
  100. .preview-img {
  101. max-width: 100px;
  102. max-height: 100px;
  103. object-fit: contain;
  104. cursor: pointer;
  105. }
  106. .modal {
  107. display: none;
  108. position: fixed;
  109. top: 0;
  110. left: 0;
  111. width: 100%;
  112. height: 100%;
  113. background-color: rgba(0,0,0,0.8);
  114. z-index: 1000;
  115. justify-content: center;
  116. align-items: center;
  117. }
  118. .modal-content {
  119. max-width: 90%;
  120. max-height: 90%;
  121. }
  122. .modal-content img {
  123. max-width: 100%;
  124. max-height: 90vh;
  125. object-fit: contain;
  126. }
  127. .close {
  128. position: absolute;
  129. top: 20px;
  130. right: 30px;
  131. color: white;
  132. font-size: 30px;
  133. cursor: pointer;
  134. }
  135. .export-import {
  136. margin-top: 20px;
  137. display: flex;
  138. gap: 10px;
  139. }
  140. #importInput {
  141. display: none;
  142. }
  143. </style>
  144. </head>
  145. <body>
  146. <div class="container">
  147. <div class="header">
  148. <h1>简易链接管理器</h1>
  149. </div>
  150. <div class="toolbar">
  151. <input type="text" id="searchInput" placeholder="搜索...">
  152. <select id="sortSelect">
  153. <option value="dateDesc">日期 (新→旧)</option>
  154. <option value="dateAsc">日期 (旧→新)</option>
  155. <option value="titleAsc">标题 (A→Z)</option>
  156. <option value="titleDesc">标题 (Z→A)</option>
  157. </select>
  158. <button id="addNewBtn">添加新链接</button>
  159. </div>
  160. <div class="tag-list" id="tagList"></div>
  161. <table id="dataTable">
  162. <thead>
  163. <tr>
  164. <th>标题</th>
  165. <th>链接</th>
  166. <th>图片</th>
  167. <th>标签</th>
  168. <th>添加日期</th>
  169. <th>操作</th>
  170. </tr>
  171. </thead>
  172. <tbody id="tableBody"></tbody>
  173. </table>
  174. <div class="export-import">
  175. <button id="exportBtn">导出数据</button>
  176. <button id="importBtn">导入数据</button>
  177. <input type="file" id="importInput" accept=".json">
  178. </div>
  179. </div>
  180. <div id="imageModal" class="modal">
  181. <span class="close">&times;</span>
  182. <div class="modal-content">
  183. <img id="modalImage" src="">
  184. </div>
  185. </div>
  186. <script>
  187. // 初始数据存储
  188. let data = JSON.parse(localStorage.getItem('linkData')) || [];
  189. let allTags = new Set();
  190. let activeTagFilters = new Set();
  191. // 初始化应用
  192. function init() {
  193. renderTags();
  194. renderTable();
  195. setupEventListeners();
  196. }
  197. // 从数据中提取所有唯一标签
  198. function extractAllTags() {
  199. allTags = new Set();
  200. data.forEach(item => {
  201. if (item.tags && Array.isArray(item.tags)) {
  202. item.tags.forEach(tag => allTags.add(tag));
  203. }
  204. });
  205. return Array.from(allTags).sort();
  206. }
  207. // 渲染标签筛选器
  208. function renderTags() {
  209. const tags = extractAllTags();
  210. const tagList = document.getElementById('tagList');
  211. tagList.innerHTML = '';
  212. tags.forEach(tag => {
  213. const tagElement = document.createElement('span');
  214. tagElement.className = `tag ${activeTagFilters.has(tag) ? 'active' : ''}`;
  215. tagElement.textContent = tag;
  216. tagElement.dataset.tag = tag;
  217. tagList.appendChild(tagElement);
  218. });
  219. }
  220. // 渲染数据表格
  221. function renderTable() {
  222. const tableBody = document.getElementById('tableBody');
  223. tableBody.innerHTML = '';
  224. const searchTerm = document.getElementById('searchInput').value.toLowerCase();
  225. const sortMethod = document.getElementById('sortSelect').value;
  226. // 筛选数据
  227. let filteredData = data.filter(item => {
  228. // 标签筛选
  229. if (activeTagFilters.size > 0) {
  230. if (!item.tags || !Array.isArray(item.tags)) return false;
  231. const hasAllTags = Array.from(activeTagFilters).every(tag =>
  232. item.tags.includes(tag)
  233. );
  234. if (!hasAllTags) return false;
  235. }
  236. // 搜索筛选
  237. if (searchTerm) {
  238. const searchFields = [
  239. item.title,
  240. item.url,
  241. (item.tags || []).join(' ')
  242. ].join(' ').toLowerCase();
  243. return searchFields.includes(searchTerm);
  244. }
  245. return true;
  246. });
  247. // 排序数据
  248. filteredData.sort((a, b) => {
  249. switch (sortMethod) {
  250. case 'dateDesc':
  251. return new Date(b.date) - new Date(a.date);
  252. case 'dateAsc':
  253. return new Date(a.date) - new Date(b.date);
  254. case 'titleAsc':
  255. return a.title.localeCompare(b.title);
  256. case 'titleDesc':
  257. return b.title.localeCompare(a.title);
  258. default:
  259. return new Date(b.date) - new Date(a.date);
  260. }
  261. });
  262. // 渲染数据行
  263. filteredData.forEach((item, index) => {
  264. const row = document.createElement('tr');
  265. // 标题列
  266. const titleCell = document.createElement('td');
  267. titleCell.textContent = item.title;
  268. row.appendChild(titleCell);
  269. // 链接列
  270. const urlCell = document.createElement('td');
  271. const urlLink = document.createElement('a');
  272. urlLink.href = item.url;
  273. urlLink.textContent = item.url.length > 30 ? item.url.substring(0, 30) + '...' : item.url;
  274. urlLink.target = '_blank';
  275. urlCell.appendChild(urlLink);
  276. row.appendChild(urlCell);
  277. // 图片列
  278. const imageCell = document.createElement('td');
  279. if (item.imageUrl) {
  280. const img = document.createElement('img');
  281. img.src = item.imageUrl;
  282. img.alt = item.title;
  283. img.className = 'preview-img';
  284. img.dataset.fullImage = item.imageUrl;
  285. imageCell.appendChild(img);
  286. }
  287. row.appendChild(imageCell);
  288. // 标签列
  289. const tagsCell = document.createElement('td');
  290. if (item.tags && Array.isArray(item.tags)) {
  291. item.tags.forEach(tag => {
  292. const tagSpan = document.createElement('span');
  293. tagSpan.className = 'tag';
  294. tagSpan.textContent = tag;
  295. tagsCell.appendChild(tagSpan);
  296. });
  297. }
  298. row.appendChild(tagsCell);
  299. // 日期列
  300. const dateCell = document.createElement('td');
  301. dateCell.textContent = new Date(item.date).toLocaleDateString();
  302. row.appendChild(dateCell);
  303. // 操作列
  304. const actionsCell = document.createElement('td');
  305. const editBtn = document.createElement('button');
  306. editBtn.textContent = '编辑';
  307. editBtn.dataset.index = index;
  308. editBtn.style.marginRight = '5px';
  309. const deleteBtn = document.createElement('button');
  310. deleteBtn.textContent = '删除';
  311. deleteBtn.dataset.index = index;
  312. deleteBtn.style.backgroundColor = '#ef4444';
  313. actionsCell.appendChild(editBtn);
  314. actionsCell.appendChild(deleteBtn);
  315. row.appendChild(actionsCell);
  316. tableBody.appendChild(row);
  317. });
  318. }
  319. // 设置事件监听器
  320. function setupEventListeners() {
  321. // 搜索框变化时更新表格
  322. document.getElementById('searchInput').addEventListener('input', renderTable);
  323. // 排序选择变化时更新表格
  324. document.getElementById('sortSelect').addEventListener('change', renderTable);
  325. // 标签点击事件
  326. document.getElementById('tagList').addEventListener('click', event => {
  327. if (event.target.classList.contains('tag')) {
  328. const tag = event.target.dataset.tag;
  329. if (activeTagFilters.has(tag)) {
  330. activeTagFilters.delete(tag);
  331. } else {
  332. activeTagFilters.add(tag);
  333. }
  334. renderTags();
  335. renderTable();
  336. }
  337. });
  338. // 添加新链接按钮
  339. document.getElementById('addNewBtn').addEventListener('click', () => {
  340. const title = prompt('输入标题:');
  341. if (!title) return;
  342. const url = prompt('输入URL:');
  343. if (!url) return;
  344. const imageUrl = prompt('输入图片URL (可选):');
  345. const tagsInput = prompt('输入标签 (用逗号分隔):');
  346. const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
  347. const newItem = {
  348. title,
  349. url,
  350. imageUrl,
  351. tags,
  352. date: new Date().toISOString()
  353. };
  354. data.push(newItem);
  355. saveData();
  356. renderTags();
  357. renderTable();
  358. });
  359. // 表格行操作按钮
  360. document.getElementById('tableBody').addEventListener('click', event => {
  361. if (event.target.tagName === 'BUTTON') {
  362. const index = parseInt(event.target.dataset.index);
  363. const item = data[index];
  364. if (event.target.textContent === '编辑') {
  365. // 编辑操作
  366. const title = prompt('编辑标题:', item.title);
  367. if (!title) return;
  368. const url = prompt('编辑URL:', item.url);
  369. if (!url) return;
  370. const imageUrl = prompt('编辑图片URL (可选):', item.imageUrl || '');
  371. const tagsInput = prompt('编辑标签 (用逗号分隔):', (item.tags || []).join(', '));
  372. const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
  373. data[index] = {
  374. ...item,
  375. title,
  376. url,
  377. imageUrl,
  378. tags
  379. };
  380. saveData();
  381. renderTags();
  382. renderTable();
  383. } else if (event.target.textContent === '删除') {
  384. // 删除操作
  385. if (confirm(`确定要删除 "${item.title}" 吗?`)) {
  386. data.splice(index, 1);
  387. saveData();
  388. renderTags();
  389. renderTable();
  390. }
  391. }
  392. } else if (event.target.classList.contains('preview-img')) {
  393. // 图片预览
  394. const modal = document.getElementById('imageModal');
  395. const modalImg = document.getElementById('modalImage');
  396. modal.style.display = 'flex';
  397. modalImg.src = event.target.dataset.fullImage;
  398. }
  399. });
  400. // 图片模态框关闭按钮
  401. document.querySelector('.close').addEventListener('click', () => {
  402. document.getElementById('imageModal').style.display = 'none';
  403. });
  404. // 导出数据
  405. document.getElementById('exportBtn').addEventListener('click', () => {
  406. const dataStr = JSON.stringify(data, null, 2);
  407. const blob = new Blob([dataStr], {type: 'application/json'});
  408. const url = URL.createObjectURL(blob);
  409. const a = document.createElement('a');
  410. a.href = url;
  411. a.download = `bookmark-data-${new Date().toISOString().slice(0,10)}.json`;
  412. document.body.appendChild(a);
  413. a.click();
  414. document.body.removeChild(a);
  415. });
  416. // 导入数据点击
  417. document.getElementById('importBtn').addEventListener('click', () => {
  418. document.getElementById('importInput').click();
  419. });
  420. // 导入数据处理
  421. document.getElementById('importInput').addEventListener('change', event => {
  422. const file = event.target.files[0];
  423. if (!file) return;
  424. const reader = new FileReader();
  425. reader.onload = function(e) {
  426. try {
  427. const importedData = JSON.parse(e.target.result);
  428. if (Array.isArray(importedData)) {
  429. if (confirm(`确定要导入 ${importedData.length} 条数据吗? 这将覆盖当前数据。`)) {
  430. data = importedData;
  431. saveData();
  432. renderTags();
  433. renderTable();
  434. alert('数据导入成功!');
  435. }
  436. } else {
  437. alert('导入的文件格式不正确。请提供有效的JSON数组。');
  438. }
  439. } catch (err) {
  440. alert('导入失败: ' + err.message);
  441. }
  442. event.target.value = '';
  443. };
  444. reader.readAsText(file);
  445. });
  446. }
  447. // 保存数据到localStorage
  448. function saveData() {
  449. localStorage.setItem('linkData', JSON.stringify(data));
  450. }
  451. // 初始化应用
  452. init();
  453. </script>
  454. </body>
  455. </html>