The source code for this blog is available on GitHub.

flutter

flutter测试内容

这是代码

"use client";

import { Post } from "@/interfaces/post";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import cn from "classnames";

type Props = {
  posts: Post[];
  tags: string[];
};

export function KnowledgeTree({ posts, tags }: Props) {
  // Group posts by tag
  const postsByTag = tags.reduce<Record<string, Post[]>>((acc, tag) => {
    acc[tag] = posts.filter((post) => post.tags.includes(tag));
    return acc;
  }, {});

  // State to track expanded tags
  const [expandedTags, setExpandedTags] = useState<Record<string, boolean>>({});
  const router = useRouter();

  const toggleTag = (tag: string) => {
    setExpandedTags((prev) => ({
      ...prev,
      [tag]: !prev[tag],
    }));
  };

  return (
    <div className="bg-white dark:bg-slate-900 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-800">
      <h3 className="text-sm font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-4 flex items-center gap-2">
        <svg
          className="w-4 h-4"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
          />
        </svg>
        Knowledge Base
      </h3>
      
      <div className="space-y-1">
        <button
          type="button"
          onClick={() => router.push("/")}
          className="w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg transition-colors cursor-pointer"
        >
            <svg className="w-4 h-4 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
            </svg>
            All Articles
            <span className="ml-auto text-xs text-slate-400">{posts.length}</span>
        </button>

        {tags.map((tag) => {
          const tagPosts = postsByTag[tag];
          if (!tagPosts?.length) return null;
          const isExpanded = expandedTags[tag];

          return (
            <div key={tag} className="group/item">
              <div className="w-full flex items-center gap-1 px-2 py-1 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg transition-colors select-none">
                <button
                  onClick={() => toggleTag(tag)}
                  className="p-1 hover:bg-slate-200 dark:hover:bg-slate-700 rounded transition-colors"
                  aria-label={`Toggle ${tag}`}
                >
                  <svg
                    className={cn(
                      "w-4 h-4 text-slate-400 transition-transform duration-200",
                      { "rotate-90": isExpanded }
                    )}
                    fill="none"
                    viewBox="0 0 24 24"
                    stroke="currentColor"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      strokeWidth={2}
                      d="M9 5l7 7-7 7"
                    />
                  </svg>
                </button>
                
                <Link
                  href={`/?tag=${encodeURIComponent(tag)}`}
                  className="flex-1 flex items-center justify-between py-1 px-1 cursor-pointer"
                >
                  <span className={cn("text-sm font-medium text-slate-700 dark:text-slate-300", { "text-blue-600 dark:text-blue-400": isExpanded })}>
                    {tag}
                  </span>
                  <span className="text-xs text-slate-400 bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded-full">
                    {tagPosts.length}
                  </span>
                </Link>
              </div>
              
              {isExpanded && (
                <div className="pl-4 ml-2 border-l border-slate-200 dark:border-slate-800 my-1 space-y-0.5 animate-in slide-in-from-top-2 duration-200">
                  {tagPosts.map((post) => (
                    <Link
                      key={post.slug}
                      href={`/posts/${post.slug}`}
                      className="block px-3 py-1.5 text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50/50 dark:hover:bg-slate-800/50 rounded-md transition-colors truncate"
                      title={post.title}
                    >
                      {post.title}
                    </Link>
                  ))}
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}