jquery タグ / 検索/ CSV出力つきCRUD


```php:
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>jQuery CRUD LocalStorage</title>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <style>
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ccc; padding: 8px; }
    .tag {
      display: inline-block;
      padding: 3px 6px;
      border-radius: 5px;
      color: white;
      margin: 2px;
      font-size: 0.9em;
    }
    .tag button {
      border: none;
      background: transparent;
      color: white;
      cursor: pointer;
      margin-left: 4px;
    }
  </style>
</head>
<body>

  <form id="addForm">
    <input type="text" name="name" id="name" placeholder="Name" required>
    <input type="text" name="city" id="city" placeholder="City" required>
    <input type="text" name="tags" id="tags" placeholder="Tags (comma-separated)">
    <input type="submit" value="Add">
  </form>

  <br>

  <input type="text" id="search" placeholder="Search by name, city, or tag">
  <br><br>

  <table>
    <thead>
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>City</th>
        <th>Tags</th>
        <th>Edit</th>
        <th>Delete</th>
      </tr>
    </thead>
    <tbody id="load-table"></tbody>
  </table>

  <br>
  <button id="downloadCsvBtn">CSV出力</button>

  <div id="edit-form" style="display: none; margin-top: 20px;">
    <h2>Edit Form</h2>
    <form id="editForm">
      <input type="hidden" id="edit-id">
      <input type="text" id="edit-name" required>
      <input type="text" id="edit-city" required>
      <input type="text" id="edit-tags" placeholder="Tags (comma-separated)">
      <input type="submit" value="Update">
    </form>
  </div>

  <script>
    const tagColors = {};
    function getColorForTag(tag) {
      if (!tagColors[tag]) {
        const hue = Math.floor(Math.random() * 360);
        tagColors[tag] = `hsl(${hue}, 70%, 60%)`;
      }
      return tagColors[tag];
    }

    function getData() {
      return JSON.parse(localStorage.getItem("students") || "[]");
    }

    function setData(data) {
      localStorage.setItem("students", JSON.stringify(data));
    }

    function renderTable(filter = "") {
      const students = getData();
      const lowerFilter = filter.toLowerCase();
      const rows = students
        .filter(s => 
          s.name.toLowerCase().includes(lowerFilter) ||
          s.city.toLowerCase().includes(lowerFilter) ||
          (s.tags && s.tags.some(tag => tag.toLowerCase().includes(lowerFilter)))
        )
        .map((s, i) => `
          <tr>
            <td>${i + 1}</td>
            <td>${s.name}</td>
            <td>${s.city}</td>
            <td>${s.tags ? s.tags.map((tag, tagIdx) => `
              <span class="tag" data-id="${i}" data-tag-idx="${tagIdx}" style="background-color: ${getColorForTag(tag)};">
                ${tag} <button class="remove-tag">×</button>
              </span>
            `).join("") : ""}</td>
            <td><button class="edit-btn" data-id="${i}">Edit</button></td>
            <td><button class="delete-btn" data-id="${i}">Delete</button></td>
          </tr>
        `).join("");
      $("#load-table").html(rows);
    }

    $(document).ready(function(){
      renderTable();

      // Add
      $("#addForm").on("submit", function(e){
        e.preventDefault();
        const name = $("#name").val();
        const city = $("#city").val();
        const tagsInput = $("#tags").val().trim();
        const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : [];
        const data = getData();
        data.push({ name, city, tags });
        setData(data);
        renderTable();
        this.reset();
      });

      // Delete
      $(document).on("click", ".delete-btn", function(){
        if (confirm("Are you sure you want to delete this record?")) {
          const id = $(this).data("id");
          const data = getData();
          data.splice(id, 1);
          setData(data);
          renderTable($("#search").val());
        }
      });

      // Remove individual tag
      $(document).on("click", ".remove-tag", function(e){
        e.stopPropagation();
        const parent = $(this).closest(".tag");
        const id = parent.data("id");
        const tagIdx = parent.data("tag-idx");
        const data = getData();
        data[id].tags.splice(tagIdx, 1);
        setData(data);
        renderTable($("#search").val());
      });

      // Edit: open form
      $(document).on("click", ".edit-btn", function(){
        const id = $(this).data("id");
        const data = getData();
        $("#edit-id").val(id);
        $("#edit-name").val(data[id].name);
        $("#edit-city").val(data[id].city);
        $("#edit-tags").val((data[id].tags || []).join(", "));
        $("#edit-form").show();
      });

      // Edit: submit
      $("#editForm").on("submit", function(e){
        e.preventDefault();
        const id = $("#edit-id").val();
        const name = $("#edit-name").val();
        const city = $("#edit-city").val();
        const tagsInput = $("#edit-tags").val().trim();
        const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : [];
        const data = getData();
        data[id] = { name, city, tags };
        setData(data);
        $("#edit-form").hide();
        renderTable($("#search").val());
      });

      // Search
      $("#search").on("keyup", function(){
        const term = $(this).val();
        renderTable(term);
      });

      // CSV出力ボタン処理
      $("#downloadCsvBtn").on("click", function(){
        const data = getData();
        if (data.length === 0) {
          alert("出力するデータがありません。");
          return;
        }

        const headers = ["Id", "Name", "City", "Tags"];
        const csvRows = [headers.join(",")];

        data.forEach((item, index) => {
          const tags = item.tags ? item.tags.join(" | ") : "";
          csvRows.push([index + 1, item.name, item.city, `"${tags}"`].join(","));
        });

        const csvContent = csvRows.join("\n");
        const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.setAttribute("href", url);
        link.setAttribute("download", "students.csv");
        // link.style.display = "none";
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      });
    });
  </script>
</body>
</html>
```