编写 iCat 详情页
iCat 详情页用于展示宠物猫的详细信息以及用户交互功能。
编写相关组件
iCat 详情页应实现的功能包括:
- iCat 基本信息展示,包括昵称、成长阶段、健康度。饥饿度。排泄物、亲密度等
 - iCat 相关操作按钮,包括修改昵称、撸猫、铲屎、治疗、检查、喂食、埋葬等。并根据小猫的不同状态渲染不同的按钮。其中,喂食和修改昵称应该使用单独的
Modal组件进行渲染。 
首先编写喂食组件。
在components文件夹下创建新文件FeedCard.jsx,并填入以下代码:
FeedCard.jsx
import { useAddRecentTransaction } from "@rainbow-me/rainbowkit";
import { Avatar, Card } from "antd";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { ContractFunctionExecutionError } from "viem";
import { useAccount, useContractRead, useContractWrite, usePrepareContractWrite } from "wagmi";
import iCatAbi from "@/lib/abi/catAbi.json";
export const FeedCard = ({ tokenId, style }) => {
  
  const { address } = useAccount();
  const [amount, setAmount] = useState(undefined);
  const food = {
    'leftover': 0,
    'fishchip': 1,
    'tin': 2
  }
  const foodTrans = {
    'leftover': '剩饭',
    'fishchip': '小鱼干',
    'tin': '鱼罐头'
  }
  const { data, isError } = useContractRead({
    address: process.env.NEXT_PUBLIC_ICAT_CONTRACT_ADDRESS,
    abi: iCatAbi,
    functionName: 'foodBalance',
    args: [address, food[style]]
  })
  const { config, error } = usePrepareContractWrite({
    address: process.env.NEXT_PUBLIC_ICAT_CONTRACT_ADDRESS,
    abi: iCatAbi,
    functionName: 'feedCat',
    args: [tokenId, food[style], amount]
  })
  const { data: tx, isLoading, isSuccess, write } = useContractWrite(config);
  const addRecentTransaction = useAddRecentTransaction();
  useEffect(() => {
    // console.log(data, error)
    if (isSuccess) {
      addRecentTransaction({
        hash: tx?.hash || "",
        description: `购买${foodTrans[style]}`
      })
    }
    if (error instanceof ContractFunctionExecutionError) {
      // console.log('message:', error.metaMessages)
      if (error.metaMessages[0] == "Error: foodNotEnough()") {
        toast.error("该种食物不足!");
      }
    }
  }, [data, isError, isSuccess, error])
  return (
    <div className="hover:drop-shadow-lg">
      <Card
        cover={
          <img alt={`${style}`} src="https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png"></img>
        }
        actions={[
          <div className="flex items-center space-x-2 justify-center">
            <input 
              className="bg-gray-100 rounded-full h-[30px] pl-3 w-[130px]"
              type="number"
              placeholder="请输入数量"
              value={amount}
              onChange={a => {setAmount(a.target.value)}}
            />
            <button disabled={!write} onClick={() => write?.()} className={`rounded-xl text-neutral-100 font-[100] transition tracking-wide w-[90px] h-[30px] outline-none ${isLoading ? "bg-emerald-500" : isSuccess ? 'bg-amber-400' : 'bg-blue-600 hover:bg-blue-700'}`}>
              {isLoading ? `喂食中...` : isSuccess ? `喂食成功!` : `投喂${foodTrans[style]}`}
            </button>
          </div>
        ]}
      >
        <Card.Meta 
          avatar={<Avatar src="https://xsgames.co/randomusers/avatar.php?g=pixel" />}
          title={`${foodTrans[style]}`}
          description={`余额:${data}个`}
        />
      </Card>
    </div>
  )
}
然后创建新文件FeedModal.jsx,并填入以下代码:
FeedModal.jsx
import { Card, Modal } from "antd";
import { useState } from "react";
import { FeedCard } from "./FeedCard";
export const FeedModal = ({ tokenId }) => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const showModal = () => {
    setIsModalOpen(true);
  };
  const handleCancel = () => {
    setIsModalOpen(false);
  };
  return (
    <>
      <button onClick={showModal} className={`rounded-xl px-8 py-3 text-neutral-100 font-[500] transition tracking-wide w-[200px] outline-none bg-blue-600 hover:bg-blue-700`}>
        喂食
      </button>
      <Modal 
        title="选择投喂的食物:"
        open={isModalOpen}
        onCancel={handleCancel}
        closable={true}
        maskClosable={true}
        footer={null}
        destroyOnClose={true}
        width={800}
      >
        <div className="flex flex-row space-x-8">
          <FeedCard tokenId={tokenId} style={`leftover`} />
          <FeedCard tokenId={tokenId} style={`fishchip`} />
          <FeedCard tokenId={tokenId} style={`tin`} />
        </div>
      </Modal>
    </>
  )
}
然后创建Assets.jsx,并填入以下代码:
Assets.jsx
import Image from "next/image";
import { useAccount, useContractReads, useContractWrite, usePrepareContractWrite } from "wagmi";
import iCatAbi from "@/lib/abi/catAbi.json";
import eggAbi from "@/lib/abi/eggAbi.json";
import { Loader } from "./Loader";
import Button from "./Button";
import { FeedModal } from "./FeedModal";
import { toast, Toaster } from "react-hot-toast";
import { useEffect, useState } from "react";
import { Modal } from "antd";
import { useAddRecentTransaction } from "@rainbow-me/rainbowkit";
const Assets = ({ tokenId }) => {
  const [isDead, setIsDead] = useState(false);
  const [owner, setOwner] = useState("");
  const { address } = useAccount();
  const [isHovered, setIsHovered] = useState(false);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [newNickname, setNewNickname] = useState("");
  const iCatCA = {
    address: process.env.NEXT_PUBLIC_ICAT_CONTRACT_ADDRESS,
    abi: iCatAbi
  }
  
  const { data , isSuccess } = useContractReads({
    contracts: [
      {
        ...iCatCA,
        functionName: 'tokenURI',
        args: [tokenId]
      },
      {
        ...iCatCA,
        functionName: 'detail',
        args: [tokenId]
      },
      {
        ...iCatCA,
        functionName: 'calculateHealth',
        args: [tokenId]
      },
      {
        ...iCatCA,
        functionName: 'calculateFeces',
        args: [tokenId]
      },
      {
        ...iCatCA,
        functionName: 'calculateHunger',
        args: [tokenId]
      },
      {
        ...iCatCA,
        functionName: 'calculateIntimacy',
        args: [tokenId]
      }, 
      {
        ...iCatCA,
        functionName: 'ownerOf',
        args: [tokenId]
      }
    ]
  })
  // console.log(data, isSuccess)
  const stage = {
    0: "幼生期",
    1: "成长期",
    2: "成熟期"
  }
  const { config } = usePrepareContractWrite({
    address: process.env.NEXT_PUBLIC_ICAT_CONTRACT_ADDRESS,
    abi: iCatAbi,
    functionName: 'changeNickname',
    args: [tokenId, newNickname]
  })
  const { data: tx, isLoading, isSuccess: isWSuccess, write } = useContractWrite(config);
  const addRecentTransaction = useAddRecentTransaction();
  useEffect(() => {
    if (isSuccess) {
      if (data?.[2].result == 0) {
        toast.error("哎呀,由于你的疏忽,这只猫咪已经死亡了,将它埋葬吧!");
        setIsDead(true);
      }
      setOwner(data?.[6].result);
      // console.log('aaa', isModalOpen)
    }
    if (isWSuccess) {
      toast.success("修改成功!");
      setIsModalOpen(false);
      addRecentTransaction({
        hash: tx?.hash || "",
        description: `修改昵称`
      })
    }
  }, [isSuccess, owner, isWSuccess]);
  return (
    isSuccess ?
    <div className="lg:mx-[204px] md:mx-8 my-0 gap-[20px] max-w-[1280px] px-10 pt-[40px] pb-[60px] gap-y-5 flex-col flex">
      <Toaster />
      <div className="grid grid-rows-1 lg:grid-cols-[320px,1fr] container gap-10 relative antialiased justify-center">
        <div className="aspect-square bg-white bg-clip-border bg-opacity-100 bg-origin-padding bg-no-repeat bg-auto rounded-xl box-border text-black block overflow-hidden relative antialiased h-[320px]">
          <Image src={"/images/qr.png"} width={300} height={300}/>
        </div>
        <div className="box-border text-black gap-5 gap-y-5 display-flex flex-col h-[180px] m-0 w-full antialiased">
          <div className="flex flex-row justify-center lg:justify-start items-center gap-1">
            <p className="box-border text-black block font-sans font-extrabold text-3xl md:justify-center h-10 antialiased">
              iCat #{tokenId} {data?.[1]?.result[0]} {isDead && "(已死亡☠️)"}
            </p>
            {
              !isDead && 
              owner == address && 
              <div className="relative">
                <div
                  className="flex flex-row justify-center items-center font-extrabold cursor-pointer"
                  onMouseEnter={() => setIsHovered(true)}
                  onMouseLeave={() => setIsHovered(false)}
                >
                  <svg stroke="currentColor" fill="none" strokeWidth="2" viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" onClick={() => setIsModalOpen(true)}>
                    <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
                    <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
                  </svg>
                  <Modal 
                    title="给你的iCat取个名字吧!"
                    open={isModalOpen}
                    closable={true}
                    maskClosable={true}
                    onCancel={() => setIsModalOpen(false)}
                    footer={null}
                  >
                    <div className="flex flex-col justify-center items-center gap-4 pt-4">
                      <input type='text' placeholder="请输入新昵称" className="ps-3 focus:outline-none w-[50%] h-[30px] drop-shadow rounded-lg" value={newNickname} onChange={a => {setNewNickname(a.target.value)}}/>
                      <button disabled={!write} onClick={() => write?.()} className={`rounded-xl px-8 py-3 text-neutral-100 font-[500] transition tracking-wide w-[200px] outline-none ${isLoading ? "bg-emerald-500" : isWSuccess ? 'bg-amber-400' : 'bg-blue-600 hover:bg-blue-700'}`}>
                        {isLoading ? `修改中...` : isWSuccess ? `修改成功!` : `修改昵称`}
                      </button>
                    </div>
                  </Modal>
                </div>
                {isHovered && (
                  <div className="flex justify-center items-center opacity-50 absolute top-[100%] left-0 w-40 bg-gray-300 p-2 rounded shadow-lg">
                    修改你的iCat名字
                  </div>
                )}
              </div>
            }
          </div>
          <div className="grid lg:grid-cols-2 grid-cols-1 lg:w-auto w-full gap-4 font-mono text-black text-sm text-center font-bold leading-6 rounded-lg pt-10 container">
            <div className="p-4 rounded-lg shadow-lg bg-white drop-shadow-2xl">
              阶段:{stage[data?.[1].result[3]]}
            </div>
            <div className="p-4 rounded-lg shadow-lg bg-white drop-shadow-2xl">
              健康度:{Number(data?.[2].result)}
            </div>
            <div className="p-4 rounded-lg shadow-lg bg-white drop-shadow-2xl">
              饥饿度:{Number(data?.[4].result)}
            </div>
            <div className="p-4 rounded-lg shadow-lg bg-white drop-shadow-2xl">
              排泄物:{Number(data?.[3].result)}
            </div>
            <div className="p-4 rounded-lg shadow-lg bg-white drop-shadow-2xl">
              亲密度:{Number(data?.[5].result)}
            </div>
          </div>
        </div>
      </div>
      <div className="border border-none rounded-xl box-border text-black block font-sans w-full mt-10 antialiased pt-[220px] lg:pt-0">
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-x-20 gap-y-10 font-mono text-white text-sm text-center justify-center items-center font-bold leading-6 bg-stripes-fuchsia rounded-lg p-4">
          {!isDead && <div className="flex flex-col justify-center items-center"> 
            <Button tokenId={tokenId} name={"撸猫"} func={"pet"} />
          </div>}
          {!isDead && <div className="flex flex-col justify-center items-center"> 
            <Button tokenId={tokenId} name={"铲屎"} func={"clearFeces"} />
          </div>}
          {!isDead && <div className="flex flex-col justify-center items-center"> 
            <Button tokenId={tokenId} name={"治疗"} func={"cure"} />
          </div>}
          <div className="flex flex-col justify-center items-center"> 
            <Button tokenId={tokenId} name={"检查"} func={"examCat"} />
          </div>
          {!isDead && <div className="flex flex-col justify-center items-center"> 
            <FeedModal tokenId={tokenId}/>
          </div>}
          <div className="flex flex-col justify-center items-center"> 
            <Button tokenId={tokenId} name={"埋葬"} func={"buryCat"} />
          </div>
        </div>
      </div>
    </div>
    :
    <Loader />
  )
}
export default Assets;
编写前端页面
在app文件夹下创建新文件asset/[tokenId]/page.jsx,并填入以下代码:
"use client"
import Assets from "@/components/Assets";
import FooterApp from "@/components/FooterApp";
import HeaderApp from "@/components/HeaderApp";
import { useContractRead } from "wagmi";
import icatAbi from "@/lib/abi/catAbi";
import { useEffect, useState } from "react";
import { Loader } from "@/components/Loader";
import { NotFound } from "@/components/NotFound";
const AssetPage = ({ params }) => {
  const [isValidTokenId, setIsValidTokenId] = useState(true);
  const { data, isLoading } = useContractRead({
    address: process.env.NEXT_PUBLIC_ICAT_CONTRACT_ADDRESS,
    abi: icatAbi,
    functionName: 'ownerOf',
    args: [params.tokenId]
  })
  useEffect(() => {
    if (data == undefined || data == '0x0000000000000000000000000000000000000000') {
      setIsValidTokenId(false);
    }
    // console.log('asset available?', isValidTokenId)
  }, [isLoading, isValidTokenId])
  return (
    <div>
        <HeaderApp />
        {
          isLoading ?
          <Loader />
          :
          (
            !isValidTokenId ?
            <NotFound />
            :
            <Assets tokenId={params.tokenId} />
          )
        }
        <FooterApp />
    </div>
  )
}
export default AssetPage;
至此,整个项目的前端全部编写完毕!