Browse Source

Merge remote-tracking branch 'origin/master'

tianyunperfect 4 months ago
parent
commit
aa711e7453

+ 3 - 2
simple-demo/excalidraw-versions.html

@@ -3,7 +3,9 @@
 <head>
     <meta charset="UTF-8">
     <title>所有版本</title>
-    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+
+    <script src="js/jquery.min.js"></script>
+    <script src="js/util.js"></script>
     <script>
         $(document).ready(function () {
             // 获取url中的参数a和title
@@ -67,7 +69,6 @@
             });
         }
     </script>
-    <script src="js/util.js"></script>
 </head>
 <body>
 <p>版本如下:</p>

+ 99 - 65
simple-demo/excalidraw_all.html

@@ -3,89 +3,123 @@
 <head>
     <meta charset="UTF-8">
     <title>excalidraw_all</title>
-    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="js/jquery.min.js"></script>
     <script src="js/login.js"></script>
     <script src="js/util.js"></script>
+    <script src="js/vue3.global.prod.js"></script>
+    <script src="js/axios.min.js"></script>
 </head>
 <body>
-<div id="nameList"></div>
-<ul id="list"></ul>
+<div id="app">
+    <div id="nameList">
+        <span v-for="name in names" :key="name.id" style="margin-right: 1.5em;min-width: 5em">
+            <a  href="#" @click.prevent="loadList(name.id)">
+            {{ name.name }} - ({{ name.count }})
+        </a>
+        </span>
+
+    </div>
+    <ul id="list">
+        <li v-for="item in listItems" :key="item.id">
+            <a :href="getUrl(item)" target="_blank">{{ item.name }}_{{ item.version }}</a>
+            &nbsp;&nbsp;&nbsp;
+            <button @click="confirmDelete(item.id, item.version)">删除</button>
+        </li>
+    </ul>
+</div>
+
 <script>
-    $(function () {
-        if (!checkLogin()) {
-            return;
-        }
-        let urlParams = new URLSearchParams(window.location.search);
-        let idParam = urlParams.get('id');
-        if (idParam) {
-            loadList(idParam);
-        } else {
-            $.getJSON('https://php.tianyunperfect.cn/controller/excalidraw.php?action=getAllName', function (result) {
-                if (result.code === 200) {
-                    let names = result.data;
-                    let nameListHtml = '';
-                    for (let i = 0; i < names.length; i++) {
-                        nameListHtml += `<a href="?id=${names[i].id}">${names[i].name} - (${names[i].count})</a>`;
-                        if (i < names.length - 1) {
-                            nameListHtml += ' &nbsp;|&nbsp; ';
-                        }
+    const { createApp, ref, onMounted } = Vue;
+
+    createApp({
+        setup() {
+            const names = ref([]);
+            const listItems = ref([]);
+            const currentId = ref(null);
+            if (!checkLogin()) {
+                return;
+            }
+
+            const loadNames = async () => {
+                try {
+                    const response = await axios.get('https://php.tianyunperfect.cn/controller/excalidraw.php?action=getAllName');
+                    if (response.data.code === 200) {
+                        names.value = response.data.data;
+                    } else {
+                        alert(response.data.message);
                     }
-                    $('#nameList').html(nameListHtml);
-                    $('#nameList a').click(function (e) {
-                        e.preventDefault();
-                        let id = $(this).attr('href').split('=')[1];
-                        loadList(id);
-                    });
-                } else {
-                    alert(result.message);
+                } catch (error) {
+                    console.error('加载名称列表失败', error);
+                    alert('加载名称列表失败');
                 }
-            });
-        }
+            };
 
-        function loadList(id) {
-            $.getJSON('https://php.tianyunperfect.cn/controller/excalidraw.php?action=get_by_id&id=' + id, function (result) {
-                if (result.code === 200) {
-                    let data = result.data;
-                    let listHtml = '';
-                    for (let i = 0; i < data.length; i++) {
-                        let a = data[i].id;
-                        let title = data[i].name;
-                        let version = data[i].version;
-                        let url = 'https://excalidraw.tianyunperfect.cn/?a=' + a + '&version=' + version;
-                        listHtml += '<li><a href="' + url + '" target="_blank">' + title + '_' + version + '</a>&nbsp;&nbsp;&nbsp;<button class="delete-btn" data-id="' + a + '" data-version="' + version + '">删除</button></li>';
+            const loadList = async (id) => {
+                currentId.value = id;
+                try {
+                    const response = await axios.get(`https://php.tianyunperfect.cn/controller/excalidraw.php?action=get_by_id&id=${id}`);
+                    if (response.data.code === 200) {
+                        listItems.value = response.data.data;
+                        // 如果没有数据,就根据id,重新加载名称列表
+                        if (listItems.value.length === 0) {
+                            await loadNames();
+                        }
+                    } else {
+                        alert(response.data.message);
                     }
-                    $('#list').html(listHtml);
 
-                    $('.delete-btn').click(function () {
-                        if (confirm('确认要删除吗?')) {
-                            let id = $(this).data('id');
-                            let version = $(this).data('version');
-                            deleteData(id, version);
-                        }
-                    });
-                } else {
-                    alert(result.message);
+                } catch (error) {
+                    console.error('加载列表失败', error);
+                    alert('加载列表失败');
                 }
-            });
-        }
+            };
+
+            const getUrl = (item) => {
+                return `https://excalidraw.tianyunperfect.cn/?a=${item.id}&version=${item.version}`;
+            };
+
+            const confirmDelete = (id, version) => {
+                if (confirm('确认要删除吗?')) {
+                    deleteData(id, version);
+                }
+            };
 
-        function deleteData(id, version) {
-            $.ajax({
-                url: 'https://php.tianyunperfect.cn/controller/excalidraw.php?action=delete&id=' + id + '&version=' + version,
-                type: 'DELETE',
-                success: function (result) {
-                    if (result.code === 200) {
-                        loadList(id);
+            const deleteData = async (id, version) => {
+                try {
+                    const response = await axios.delete(`https://php.tianyunperfect.cn/controller/excalidraw.php?action=delete&id=${id}&version=${version}`);
+                    if (response.data.code === 200) {
+                        loadList(currentId.value);
                     } else {
-                        alert(result.message);
+                        alert(response.data.message);
                     }
-                },
-                error: function () {
+                } catch (error) {
+                    console.error('删除失败', error);
                     alert('删除失败');
                 }
+            };
+
+            onMounted(() => {
+                if (!checkLogin()) {
+                    return;
+                }
+                const urlParams = new URLSearchParams(window.location.search);
+                const idParam = urlParams.get('id');
+                if (idParam) {
+                    loadList(idParam);
+                } else {
+                    loadNames();
+                }
             });
+
+            return {
+                names,
+                listItems,
+                loadList,
+                getUrl,
+                confirmDelete
+            };
         }
-    });
+    }).mount('#app');
 </script>
 </body>
 </html>

+ 274 - 0
simple-demo/heart.html

@@ -0,0 +1,274 @@
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>loveHeart</title>
+    <link
+      rel="shortcut icon"
+      href="http://zhouql.vip/images/心.png"
+      type="image/x-icon"
+    />
+    <style>
+      html,
+      body {
+        height: 100%;
+        padding: 0;
+        margin: 0;
+        background: #000;
+      }
+      canvas {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <canvas id="pinkboard"></canvas>
+    <script>
+      var settings = {
+        particles: {
+          length: 500,
+          duration: 2,
+          velocity: 100,
+          effect: -0.75,
+          size: 32,
+        },
+      };
+      (function () {
+        var b = 0;
+        var c = ["ms", "moz", "webkit", "o"];
+        for (var a = 0; a < c.length && !window.requestAnimationFrame; ++a) {
+          window.requestAnimationFrame = window[c[a] + "RequestAnimationFrame"];
+          window.cancelAnimationFrame =
+            window[c[a] + "CancelAnimationFrame"] ||
+            window[c[a] + "CancelRequestAnimationFrame"];
+        }
+        if (!window.requestAnimationFrame) {
+          window.requestAnimationFrame = function (h, e) {
+            var d = new Date().getTime();
+            var f = Math.max(0, 16 - (d - b));
+            var g = window.setTimeout(function () {
+              h(d + f);
+            }, f);
+            b = d + f;
+            return g;
+          };
+        }
+        if (!window.cancelAnimationFrame) {
+          window.cancelAnimationFrame = function (d) {
+            clearTimeout(d);
+          };
+        }
+      })();
+
+      var Point = (function () {
+        function Point(x, y) {
+          this.x = typeof x !== "undefined" ? x : 0;
+          this.y = typeof y !== "undefined" ? y : 0;
+        }
+        Point.prototype.clone = function () {
+          return new Point(this.x, this.y);
+        };
+        Point.prototype.length = function (length) {
+          if (typeof length == "undefined")
+            return Math.sqrt(this.x * this.x + this.y * this.y);
+          this.normalize();
+          this.x *= length;
+          this.y *= length;
+          return this;
+        };
+        Point.prototype.normalize = function () {
+          var length = this.length();
+          this.x /= length;
+          this.y /= length;
+          return this;
+        };
+        return Point;
+      })();
+      var Particle = (function () {
+        function Particle() {
+          this.position = new Point();
+          this.velocity = new Point();
+          this.acceleration = new Point();
+          this.age = 0;
+        }
+        Particle.prototype.initialize = function (x, y, dx, dy) {
+          this.position.x = x;
+          this.position.y = y;
+          this.velocity.x = dx;
+          this.velocity.y = dy;
+          this.acceleration.x = dx * settings.particles.effect;
+          this.acceleration.y = dy * settings.particles.effect;
+          this.age = 0;
+        };
+        Particle.prototype.update = function (deltaTime) {
+          this.position.x += this.velocity.x * deltaTime;
+          this.position.y += this.velocity.y * deltaTime;
+          this.velocity.x += this.acceleration.x * deltaTime;
+          this.velocity.y += this.acceleration.y * deltaTime;
+          this.age += deltaTime;
+        };
+        Particle.prototype.draw = function (context, image) {
+          function ease(t) {
+            return --t * t * t + 1;
+          }
+          var size = image.width * ease(this.age / settings.particles.duration);
+          context.globalAlpha = 1 - this.age / settings.particles.duration;
+          context.drawImage(
+            image,
+            this.position.x - size / 2,
+            this.position.y - size / 2,
+            size,
+            size
+          );
+        };
+        return Particle;
+      })();
+      var ParticlePool = (function () {
+        var particles,
+          firstActive = 0,
+          firstFree = 0,
+          duration = settings.particles.duration;
+        function ParticlePool(length) {
+          // create and populate particle pool
+          particles = new Array(length);
+          for (var i = 0; i < particles.length; i++)
+            particles[i] = new Particle();
+        }
+        ParticlePool.prototype.add = function (x, y, dx, dy) {
+          particles[firstFree].initialize(x, y, dx, dy);
+          // handle circular queue
+          firstFree++;
+          if (firstFree == particles.length) firstFree = 0;
+          if (firstActive == firstFree) firstActive++;
+          if (firstActive == particles.length) firstActive = 0;
+        };
+        ParticlePool.prototype.update = function (deltaTime) {
+          var i;
+          // update active particles
+          if (firstActive < firstFree) {
+            for (i = firstActive; i < firstFree; i++)
+              particles[i].update(deltaTime);
+          }
+          if (firstFree < firstActive) {
+            for (i = firstActive; i < particles.length; i++)
+              particles[i].update(deltaTime);
+            for (i = 0; i < firstFree; i++) particles[i].update(deltaTime);
+          }
+          // remove inactive particles
+          while (
+            particles[firstActive].age >= duration &&
+            firstActive != firstFree
+          ) {
+            firstActive++;
+            if (firstActive == particles.length) firstActive = 0;
+          }
+        };
+        ParticlePool.prototype.draw = function (context, image) {
+          // draw active particles
+          if (firstActive < firstFree) {
+            for (i = firstActive; i < firstFree; i++)
+              particles[i].draw(context, image);
+          }
+          if (firstFree < firstActive) {
+            for (i = firstActive; i < particles.length; i++)
+              particles[i].draw(context, image);
+            for (i = 0; i < firstFree; i++) particles[i].draw(context, image);
+          }
+        };
+        return ParticlePool;
+      })();
+      (function (canvas) {
+        var context = canvas.getContext("2d"),
+          particles = new ParticlePool(settings.particles.length),
+          particleRate =
+            settings.particles.length / settings.particles.duration, // particles/sec
+          time;
+        // get point on heart with -PI <= t <= PI
+        function pointOnHeart(t) {
+          return new Point(
+            160 * Math.pow(Math.sin(t), 3),
+            130 * Math.cos(t) -
+              50 * Math.cos(2 * t) -
+              20 * Math.cos(3 * t) -
+              10 * Math.cos(4 * t) +
+              25
+          );
+        }
+        // creating the particle image using a dummy canvas
+        var image = (function () {
+          var canvas = document.createElement("canvas"),
+            context = canvas.getContext("2d");
+          canvas.width = settings.particles.size;
+          canvas.height = settings.particles.size;
+          // helper function to create the path
+          function to(t) {
+            var point = pointOnHeart(t);
+            point.x =
+              settings.particles.size / 2 +
+              (point.x * settings.particles.size) / 350;
+            point.y =
+              settings.particles.size / 2 -
+              (point.y * settings.particles.size) / 350;
+            return point;
+          }
+          // create the path
+          context.beginPath();
+          var t = -Math.PI;
+          var point = to(t);
+          context.moveTo(point.x, point.y);
+          while (t < Math.PI) {
+            t += 0.01; // baby steps!
+            point = to(t);
+            context.lineTo(point.x, point.y);
+          }
+          context.closePath();
+          // create the fill
+          context.fillStyle = "#ea80b0";
+          context.fill();
+          // create the image
+          var image = new Image();
+          image.src = canvas.toDataURL();
+          return image;
+        })();
+        // render that thing!
+        function render() {
+          // next animation frame
+          requestAnimationFrame(render);
+          // update time
+          var newTime = new Date().getTime() / 1000,
+            deltaTime = newTime - (time || newTime);
+          time = newTime;
+          // clear canvas
+          context.clearRect(0, 0, canvas.width, canvas.height);
+          // create new particles
+          var amount = particleRate * deltaTime;
+          for (var i = 0; i < amount; i++) {
+            var pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random());
+            var dir = pos.clone().length(settings.particles.velocity);
+            particles.add(
+              canvas.width / 2 + pos.x,
+              canvas.height / 2 - pos.y,
+              dir.x,
+              -dir.y
+            );
+          }
+          // update and draw particles
+          particles.update(deltaTime);
+          particles.draw(context, image);
+        }
+        // handle (re-)sizing of the canvas
+        function onResize() {
+          canvas.width = canvas.clientWidth;
+          canvas.height = canvas.clientHeight;
+        }
+        window.onresize = onResize;
+        // delay rendering bootstrap
+        setTimeout(function () {
+          onResize();
+          render();
+        }, 10);
+      })(document.getElementById("pinkboard"));
+    </script>
+  </body>
+</html>

File diff suppressed because it is too large
+ 0 - 0
simple-demo/js/vue3.global.prod.js


+ 489 - 67
tmp/tmp.html

@@ -1,83 +1,505 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="zh-CN">
 <head>
     <meta charset="UTF-8">
-    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
-    <title>可爱登录页面</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>简易链接管理器</title>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.2/papaparse.min.js"></script>
     <style>
-        .wrap {
+        body {
+            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            line-height: 1.6;
+            margin: 0;
+            padding: 20px;
+            color: #333;
+            background-color: #f9f9f9;
+        }
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+        }
+        .header {
             display: flex;
             justify-content: space-between;
-            /*justify-content: center;*/
-            /*align-items: center;*/
-            /*height: 100vh;*/
+            align-items: center;
+            margin-bottom: 20px;
+        }
+        h1 {
+            color: #2c3e50;
+            margin: 0;
+        }
+        .toolbar {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 20px;
+            flex-wrap: wrap;
+        }
+        .tag-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 8px;
+            margin-bottom: 20px;
+        }
+        .tag {
+            background-color: #e0f2fe;
+            color: #0369a1;
+            padding: 5px 10px;
+            border-radius: 15px;
+            cursor: pointer;
+            transition: all 0.2s;
+            user-select: none;
+        }
+        .tag.active {
+            background-color: #0284c7;
+            color: white;
+        }
+        .tag:hover {
+            opacity: 0.8;
+        }
+        input, button, select {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            font-size: 14px;
+        }
+        input:focus, select:focus {
+            outline: none;
+            border-color: #0284c7;
+        }
+        button {
+            background-color: #0284c7;
+            color: white;
+            cursor: pointer;
+            border: none;
+        }
+        button:hover {
+            background-color: #0369a1;
+        }
+        table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-top: 20px;
+            background-color: white;
+            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+            border-radius: 8px;
+            overflow: hidden;
+        }
+        th, td {
+            padding: 12px 15px;
+            text-align: left;
+            border-bottom: 1px solid #eee;
+        }
+        th {
+            background-color: #f1f5f9;
+            font-weight: 600;
+            color: #334155;
+        }
+        tr:hover {
+            background-color: #f1f5f9;
+        }
+        .preview-img {
+            max-width: 100px;
+            max-height: 100px;
+            object-fit: contain;
+            cursor: pointer;
+        }
+        .modal {
+            display: none;
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background-color: rgba(0,0,0,0.8);
+            z-index: 1000;
+            justify-content: center;
+            align-items: center;
         }
-        .item{
-            width: 30vw;
+        .modal-content {
+            max-width: 90%;
+            max-height: 90%;
+        }
+        .modal-content img {
+            max-width: 100%;
+            max-height: 90vh;
+            object-fit: contain;
+        }
+        .close {
+            position: absolute;
+            top: 20px;
+            right: 30px;
+            color: white;
+            font-size: 30px;
+            cursor: pointer;
+        }
+        .export-import {
+            margin-top: 20px;
+            display: flex;
+            gap: 10px;
+        }
+        #importInput {
+            display: none;
         }
     </style>
 </head>
 <body>
-<div class="wrap">
-    <div class="item">123</div>
-    <div class="item">123</div>
-    <div class="item">123</div>
+<div class="container">
+    <div class="header">
+        <h1>简易链接管理器</h1>
+    </div>
+
+    <div class="toolbar">
+        <input type="text" id="searchInput" placeholder="搜索...">
+        <select id="sortSelect">
+            <option value="dateDesc">日期 (新→旧)</option>
+            <option value="dateAsc">日期 (旧→新)</option>
+            <option value="titleAsc">标题 (A→Z)</option>
+            <option value="titleDesc">标题 (Z→A)</option>
+        </select>
+        <button id="addNewBtn">添加新链接</button>
+    </div>
+
+    <div class="tag-list" id="tagList"></div>
+
+    <table id="dataTable">
+        <thead>
+        <tr>
+            <th>标题</th>
+            <th>链接</th>
+            <th>图片</th>
+            <th>标签</th>
+            <th>添加日期</th>
+            <th>操作</th>
+        </tr>
+        </thead>
+        <tbody id="tableBody"></tbody>
+    </table>
+
+    <div class="export-import">
+        <button id="exportBtn">导出数据</button>
+        <button id="importBtn">导入数据</button>
+        <input type="file" id="importInput" accept=".json">
+    </div>
+</div>
+
+<div id="imageModal" class="modal">
+    <span class="close">&times;</span>
+    <div class="modal-content">
+        <img id="modalImage" src="">
+    </div>
 </div>
 
-<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.5.16/vue.min.js"></script>
-<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
-<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
-<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
-</body>
 <script>
-  //   curl 'https://www.suoyoutongxue.cn/biz/video_lesson' \
-  // -H 'Referer: http://localhost:8080/' \
-  // -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36' \
-  // -H 'Token: 4fefa7f2f623b3ab-e2dbfaadc863221a941069830427056a276b8882fed736822ec83102eeefa660aaa055c1003cf4ab0da572' \
-  // -H 'content-type: application/json;charset=UTF-8' \
-  // --data-raw '{"id":"970","token":"4fefa7f2f623b3ab-e2dbfaadc863221a941069830427056a276b8882fed736822ec83102eeefa660aaa055c1003cf4ab0da572"}'
-  // const data = {
-  //     id: "970",
-  //     token: "4fefa7f2f623b3ab-e2dbfaadc863221a941069830427056a276b8882fed736822ec83102eeefa660aaa055c1003cf4ab0da572"
-  // };
-  //
-  // const config = {
-  //     headers: {
-  //         'Referer': 'http://localhost:8080/',
-  //         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
-  //         'Token': '4fefa7f2f623b3ab-e2dbfaadc863221a941069830427056a276b8882fed736822ec83102eeefa660aaa055c1003cf4ab0da572',
-  //         'content-type': 'application/json;charset=UTF-8'
-  //     }
-  // };
-  //
-  // axios.post('https://www.suoyoutongxue.cn/biz/video_lesson', data, config)
-  //     .then(response => {
-  //         console.log(response.data);
-  //     })
-  //     .catch(error => {
-  //         console.error(error);
-  //     });
-  var myHeaders = new Headers();
-  myHeaders.append("Referer", "http://localhost:8080/");
-  myHeaders.append("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36");
-  myHeaders.append("Token", "4fefa7f2f623b3ab-e2dbfaadc863221a941069830427056a276b8882fed736822ec83102eeefa660aaa055c1003cf4ab0da572");
-  myHeaders.append("content-type", "application/json;charset=UTF-8");
-  myHeaders.append("Accept", "*/*");
-  myHeaders.append("Host", "www.suoyoutongxue.cn");
-  myHeaders.append("Connection", "keep-alive");
-
-  var raw = "{\"id\":\"970\",\"token\":\"4fefa7f2f623b3ab-e2dbfaadc863221a941069830427056a276b8882fed736822ec83102eeefa660aaa055c1003cf4ab0da572\"}";
-
-  var requestOptions = {
-      method: 'POST',
-      headers: myHeaders,
-      body: raw,
-      redirect: 'follow'
-  };
-
-  fetch("https://www.suoyoutongxue.cn/biz/video_lesson", requestOptions)
-      .then(response => response.text())
-      .then(result => console.log(result))
-      .catch(error => console.log('error', error));
+    // 初始数据存储
+    let data = JSON.parse(localStorage.getItem('linkData')) || [];
+    let allTags = new Set();
+    let activeTagFilters = new Set();
+
+    // 初始化应用
+    function init() {
+        renderTags();
+        renderTable();
+        setupEventListeners();
+    }
+
+    // 从数据中提取所有唯一标签
+    function extractAllTags() {
+        allTags = new Set();
+        data.forEach(item => {
+            if (item.tags && Array.isArray(item.tags)) {
+                item.tags.forEach(tag => allTags.add(tag));
+            }
+        });
+        return Array.from(allTags).sort();
+    }
+
+    // 渲染标签筛选器
+    function renderTags() {
+        const tags = extractAllTags();
+        const tagList = document.getElementById('tagList');
+        tagList.innerHTML = '';
+
+        tags.forEach(tag => {
+            const tagElement = document.createElement('span');
+            tagElement.className = `tag ${activeTagFilters.has(tag) ? 'active' : ''}`;
+            tagElement.textContent = tag;
+            tagElement.dataset.tag = tag;
+            tagList.appendChild(tagElement);
+        });
+    }
+
+    // 渲染数据表格
+    function renderTable() {
+        const tableBody = document.getElementById('tableBody');
+        tableBody.innerHTML = '';
+
+        const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+        const sortMethod = document.getElementById('sortSelect').value;
+
+        // 筛选数据
+        let filteredData = data.filter(item => {
+            // 标签筛选
+            if (activeTagFilters.size > 0) {
+                if (!item.tags || !Array.isArray(item.tags)) return false;
+                const hasAllTags = Array.from(activeTagFilters).every(tag =>
+                    item.tags.includes(tag)
+                );
+                if (!hasAllTags) return false;
+            }
+
+            // 搜索筛选
+            if (searchTerm) {
+                const searchFields = [
+                    item.title,
+                    item.url,
+                    (item.tags || []).join(' ')
+                ].join(' ').toLowerCase();
+
+                return searchFields.includes(searchTerm);
+            }
+
+            return true;
+        });
+
+        // 排序数据
+        filteredData.sort((a, b) => {
+            switch (sortMethod) {
+                case 'dateDesc':
+                    return new Date(b.date) - new Date(a.date);
+                case 'dateAsc':
+                    return new Date(a.date) - new Date(b.date);
+                case 'titleAsc':
+                    return a.title.localeCompare(b.title);
+                case 'titleDesc':
+                    return b.title.localeCompare(a.title);
+                default:
+                    return new Date(b.date) - new Date(a.date);
+            }
+        });
+
+        // 渲染数据行
+        filteredData.forEach((item, index) => {
+            const row = document.createElement('tr');
+
+            // 标题列
+            const titleCell = document.createElement('td');
+            titleCell.textContent = item.title;
+            row.appendChild(titleCell);
+
+            // 链接列
+            const urlCell = document.createElement('td');
+            const urlLink = document.createElement('a');
+            urlLink.href = item.url;
+            urlLink.textContent = item.url.length > 30 ? item.url.substring(0, 30) + '...' : item.url;
+            urlLink.target = '_blank';
+            urlCell.appendChild(urlLink);
+            row.appendChild(urlCell);
+
+            // 图片列
+            const imageCell = document.createElement('td');
+            if (item.imageUrl) {
+                const img = document.createElement('img');
+                img.src = item.imageUrl;
+                img.alt = item.title;
+                img.className = 'preview-img';
+                img.dataset.fullImage = item.imageUrl;
+                imageCell.appendChild(img);
+            }
+            row.appendChild(imageCell);
+
+            // 标签列
+            const tagsCell = document.createElement('td');
+            if (item.tags && Array.isArray(item.tags)) {
+                item.tags.forEach(tag => {
+                    const tagSpan = document.createElement('span');
+                    tagSpan.className = 'tag';
+                    tagSpan.textContent = tag;
+                    tagsCell.appendChild(tagSpan);
+                });
+            }
+            row.appendChild(tagsCell);
+
+            // 日期列
+            const dateCell = document.createElement('td');
+            dateCell.textContent = new Date(item.date).toLocaleDateString();
+            row.appendChild(dateCell);
+
+            // 操作列
+            const actionsCell = document.createElement('td');
+            const editBtn = document.createElement('button');
+            editBtn.textContent = '编辑';
+            editBtn.dataset.index = index;
+            editBtn.style.marginRight = '5px';
+
+            const deleteBtn = document.createElement('button');
+            deleteBtn.textContent = '删除';
+            deleteBtn.dataset.index = index;
+            deleteBtn.style.backgroundColor = '#ef4444';
+
+            actionsCell.appendChild(editBtn);
+            actionsCell.appendChild(deleteBtn);
+            row.appendChild(actionsCell);
+
+            tableBody.appendChild(row);
+        });
+    }
+
+    // 设置事件监听器
+    function setupEventListeners() {
+        // 搜索框变化时更新表格
+        document.getElementById('searchInput').addEventListener('input', renderTable);
+
+        // 排序选择变化时更新表格
+        document.getElementById('sortSelect').addEventListener('change', renderTable);
+
+        // 标签点击事件
+        document.getElementById('tagList').addEventListener('click', event => {
+            if (event.target.classList.contains('tag')) {
+                const tag = event.target.dataset.tag;
+                if (activeTagFilters.has(tag)) {
+                    activeTagFilters.delete(tag);
+                } else {
+                    activeTagFilters.add(tag);
+                }
+                renderTags();
+                renderTable();
+            }
+        });
+
+        // 添加新链接按钮
+        document.getElementById('addNewBtn').addEventListener('click', () => {
+            const title = prompt('输入标题:');
+            if (!title) return;
+
+            const url = prompt('输入URL:');
+            if (!url) return;
+
+            const imageUrl = prompt('输入图片URL (可选):');
+
+            const tagsInput = prompt('输入标签 (用逗号分隔):');
+            const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
+
+            const newItem = {
+                title,
+                url,
+                imageUrl,
+                tags,
+                date: new Date().toISOString()
+            };
+
+            data.push(newItem);
+            saveData();
+            renderTags();
+            renderTable();
+        });
+
+        // 表格行操作按钮
+        document.getElementById('tableBody').addEventListener('click', event => {
+            if (event.target.tagName === 'BUTTON') {
+                const index = parseInt(event.target.dataset.index);
+                const item = data[index];
+
+                if (event.target.textContent === '编辑') {
+                    // 编辑操作
+                    const title = prompt('编辑标题:', item.title);
+                    if (!title) return;
+
+                    const url = prompt('编辑URL:', item.url);
+                    if (!url) return;
+
+                    const imageUrl = prompt('编辑图片URL (可选):', item.imageUrl || '');
+
+                    const tagsInput = prompt('编辑标签 (用逗号分隔):', (item.tags || []).join(', '));
+                    const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
+
+                    data[index] = {
+                        ...item,
+                        title,
+                        url,
+                        imageUrl,
+                        tags
+                    };
+
+                    saveData();
+                    renderTags();
+                    renderTable();
+                } else if (event.target.textContent === '删除') {
+                    // 删除操作
+                    if (confirm(`确定要删除 "${item.title}" 吗?`)) {
+                        data.splice(index, 1);
+                        saveData();
+                        renderTags();
+                        renderTable();
+                    }
+                }
+            } else if (event.target.classList.contains('preview-img')) {
+                // 图片预览
+                const modal = document.getElementById('imageModal');
+                const modalImg = document.getElementById('modalImage');
+                modal.style.display = 'flex';
+                modalImg.src = event.target.dataset.fullImage;
+            }
+        });
+
+        // 图片模态框关闭按钮
+        document.querySelector('.close').addEventListener('click', () => {
+            document.getElementById('imageModal').style.display = 'none';
+        });
+
+        // 导出数据
+        document.getElementById('exportBtn').addEventListener('click', () => {
+            const dataStr = JSON.stringify(data, null, 2);
+            const blob = new Blob([dataStr], {type: 'application/json'});
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = `bookmark-data-${new Date().toISOString().slice(0,10)}.json`;
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+        });
+
+        // 导入数据点击
+        document.getElementById('importBtn').addEventListener('click', () => {
+            document.getElementById('importInput').click();
+        });
+
+        // 导入数据处理
+        document.getElementById('importInput').addEventListener('change', event => {
+            const file = event.target.files[0];
+            if (!file) return;
+
+            const reader = new FileReader();
+            reader.onload = function(e) {
+                try {
+                    const importedData = JSON.parse(e.target.result);
+                    if (Array.isArray(importedData)) {
+                        if (confirm(`确定要导入 ${importedData.length} 条数据吗? 这将覆盖当前数据。`)) {
+                            data = importedData;
+                            saveData();
+                            renderTags();
+                            renderTable();
+                            alert('数据导入成功!');
+                        }
+                    } else {
+                        alert('导入的文件格式不正确。请提供有效的JSON数组。');
+                    }
+                } catch (err) {
+                    alert('导入失败: ' + err.message);
+                }
+                event.target.value = '';
+            };
+            reader.readAsText(file);
+        });
+    }
+
+    // 保存数据到localStorage
+    function saveData() {
+        localStorage.setItem('linkData', JSON.stringify(data));
+    }
+
+    // 初始化应用
+    init();
 </script>
+</body>
 </html>

Some files were not shown because too many files changed in this diff