Solidity 互動式開發實戰指南:從基礎到部署的完整教程(含 Remix IDE 實作)

本文提供完整的 Solidity 互動式開發教程,涵蓋從基礎語法到實際部署的全流程。不同於傳統的靜態程式碼範例,本文特別設計了「可直接在 Remix IDE 中運行的」實作章節。包含完整的 ERC-20 代幣合約和質押合約實作,代碼可直接粘貼到 Remix IDE 編譯部署。同時涵蓋 Remix IDE 使用指南、變量類型與數據結構、控制流語句、以及常見錯誤與調試技巧。是區塊鏈開發者入門 Solidity 的最佳實務指南。

Solidity 互動式開發實戰指南:從基礎到部署的完整教程(含 Remix IDE 實作)


文章 metadata

欄位內容
article_type互動式技術教程
fact_checkedtrue
factcheckeddate2026-03-23
difficultyintermediate
prerequisitesJavaScript/Python 基礎、了解區塊鏈基本概念
tools_requiredRemix IDE(瀏覽器版)、MetaMask 錢包
last_updated2026-03-23
version1.0
disclaimer本教程僅供教育目的。智能合約涉及真實資產,部署前請充分測試。

概述

本文提供完整的 Solidity 互動式開發教程,涵蓋從基礎語法到實際部署的全流程。不同於傳統的靜態程式碼範例,本文特別設計了「可直接在 Remix IDE 中運行的」實作章節,讓讀者能夠邊學邊做,真正掌握以太坊智能合約開發的核心技能。

Remix IDE 是目前最流行的以太坊智能合約線上開發環境,無需本地安裝任何軟體,打開瀏覽器即可開始開發。本教程將帶領讀者從零開始,逐步構建一個完整的 ERC-20 代幣合約和一個簡單的質押合約。


第一章:開發環境設置

1.1 Remix IDE 快速上手

Remix IDE 是以太坊官方推薦的智能合約開發環境,提供完整的編譯、部署、測試功能。訪問方式:

訪問地址:https://remix.ethereum.org/

功能特色:
- 線上 Solidity 編譯器(支援多版本)
- 區塊鏈模擬器(JavaScript VM)
- MetaMask 等錢包整合
- 合約調試工具
- 插件系統擴展功能

首次使用 Remix IDE 的讀者,建議按以下步驟熟悉界面:

界面布局說明:

左側面板(文件管理器):
- 顯示項目文件列表
- 可創建、刪除、重命名文件
- 支援資料夾結構管理

中間面板(代碼編輯器):
- Solidity 代碼編寫區域
- 語法高亮與自動補全
- 錯誤提示與代碼格式化

右側面板(編譯與部署):
- 編譯控制(Compile)
- 部署控制(Deploy & Run Transactions)
- 合約 ABI 與 Bytecode 查看

底部面板(日誌輸出):
- 交易詳情
- 錯誤信息
- Console 輸出

1.2 錢包設置(用於真實網絡部署)

若要在 Sepolia 等測試網絡部署合約,需要設置 MetaMask 錢包:

MetaMask 安裝與設置步驟:

第一步:安裝 MetaMask 擴展
- 訪問 https://metamask.io
- 根據瀏覽器類型安裝對應版本
- 創建新錢包或導入已有錢包

第二步:切換到測試網絡
- 打開 MetaMask
- 點擊頂部網絡選擇器
- 選擇「Sepolia Test Network」

第三步:獲取測試網 ETH
- 訪問 Sepolia 水龍頭:https://sepoliafaucet.com/
- 粘貼錢包地址
- 等待 24 小時冷卻後可再次獲取

第二章:Solidity 基礎語法互動實作

2.1 第一個 Solidity 合約:Hello Ethereum

讓我們從最簡單的合約開始,學習 Solidity 的基本結構:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// 這是我們的第一個 Solidity 合約
// 可以在 Remix IDE 中直接編譯和部署

contract HelloEthereum {
    // 狀態變量:用於永久存儲在區塊鏈上
    string private greeting;
    
    // 合約構造函數:部署時執行一次
    constructor(string memory _greeting) {
        greeting = _greeting;
    }
    
    // 讀取 greeting 的函數
    function getGreeting() public view returns (string memory) {
        return greeting;
    }
    
    // 更新 greeting 的函數
    function setGreeting(string memory _newGreeting) public {
        greeting = _newGreeting;
    }
}

Remix IDE 實作步驟

步驟一:在 Remix 中創建文件
- 左側面板點擊「+」按鈕
- 輸入文件名:HelloEthereum.sol
- 粘貼上述代碼

步驟二:編譯合約
- 右側面板點擊「Solidity Compiler」
- 選擇編譯器版本(建議 0.8.24)
- 點擊「Compile HelloEthereum.sol」
- 觀察底部日誌,確認無錯誤

步驟三:部署到 JavaScript VM
- 右側面板點擊「Deploy & Run Transactions」
- 環境選擇「JavaScript VM」
- 在 constructor input 中輸入初始 greeting(如 "Hello, Ethereum!")
- 點擊「Deploy」按鈕
- 觀察合約已部署成功

2.2 變量類型與數據結構

Solidity 是靜態類型語言,每個變量都需要指定類型。以下是常見的變量類型:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract TypesDemo {
    // ===== 值類型 (Value Types) =====
    
    // 布爾值
    bool public boolean = true;
    
    // 有符號整數
    int256 public signedInt = -100;
    
    // 無符號整數(常用於金額和數量)
    uint256 public unsignedInt = 100;
    
    // 地址類型
    address public owner = msg.sender;
    
    // 位元組
    bytes32 public data = "Hello";
    
    // ===== 引用類型 (Reference Types) =====
    
    // 字串
    string public name = "Ethereum";
    
    // 固定大小位元組陣列
    bytes32 public fixedBytes = "Fixed";
    
    // 動態位元組陣列
    bytes public dynamicBytes = hex"010203";
    
    // ===== 數組 (Arrays) =====
    
    // 動態大小數組
    uint256[] public dynamicArray;
    
    // 固定大小數組
    uint256[5] public fixedArray;
    
    // 結構體
    struct Person {
        string name;
        uint256 age;
        address wallet;
    }
    
    // 映射
    mapping(address => uint256) public balances;
    
    // 枚舉
    enum Status { Pending, Active, Completed }
    Status public currentStatus;
    
    // ===== 函數示例 =====
    
    function setStatus(Status _status) public {
        currentStatus = _status;
    }
    
    function addToArray(uint256 _value) public {
        dynamicArray.push(_value);
    }
    
    function getArrayLength() public view returns (uint256) {
        return dynamicArray.length;
    }
    
    function updateBalance(address _addr, uint256 _balance) public {
        balances[_addr] = _balance;
    }
    
    function getBalance(address _addr) public view returns (uint256) {
        return balances[_addr];
    }
}

2.3 控制流與運算符

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract ControlFlowDemo {
    // 狀態變量
    uint256 public result;
    
    // if-else 控制流
    function checkValue(uint256 _value) public returns (string memory) {
        if (_value > 100) {
            return "Large";
        } else if (_value > 50) {
            return "Medium";
        } else {
            return "Small";
        }
    }
    
    // for 迴圈
    function calculateSum(uint256 _n) public returns (uint256) {
        uint256 sum = 0;
        for (uint256 i = 1; i <= _n; i++) {
            sum += i;
        }
        result = sum;
        return sum;
    }
    
    // while 迴圈
    function calculateFactorial(uint256 _n) public returns (uint256) {
        uint256 factorial = 1;
        uint256 i = 1;
        while (i <= _n) {
            factorial *= i;
            i++;
        }
        result = factorial;
        return factorial;
    }
    
    // 三元運算符
    function max(uint256 a, uint256 b) public pure returns (uint256) {
        return a >= b ? a : b;
    }
    
    // 映射遍歷示例(需要搭配數組)
    mapping(address => uint256) public values;
    address[] public valueKeys;
    
    function setValue(address _addr, uint256 _value) public {
        if (values[_addr] == 0) {
            valueKeys.push(_addr);
        }
        values[_addr] = _value;
    }
    
    function getAllValues() public view returns (uint256[] memory) {
        uint256[] memory result = new uint256[](valueKeys.length);
        for (uint256 i = 0; i < valueKeys.length; i++) {
            result[i] = values[valueKeys[i]];
        }
        return result;
    }
}

第三章:ERC-20 代幣合約實作

3.1 ERC-20 標準概述

ERC-20 是以太坊上最廣泛使用的代幣標準,定義了代幣轉帳、餘額查詢、授權等基本接口。一個完整的 ERC-20 合約需要實現以下函數和事件:

ERC-20 標準接口:

事件(Events):
- Transfer(address indexed from, address indexed to, uint256 value)
- Approval(address indexed owner, address indexed spender, uint256 value)

函數(Functions):
- totalSupply() → uint256
- balanceOf(address account) → uint256
- transfer(address to, uint256 amount) → bool
- allowance(address owner, address spender) → uint256
- approve(address spender, uint256 amount) → bool
- transferFrom(address from, address to, uint256 amount) → bool

3.2 完整 ERC-20 合約實作

以下是一個功能完整的 ERC-20 合約,可在 Remix IDE 中直接部署:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title SimpleToken
 * @dev 簡化的 ERC-20 代幣合約
 * 可在 Remix IDE 中直接編譯和部署
 */
contract SimpleToken {
    // ===== ERC-20 標準定義 =====
    
    string public name;         // 代幣名稱
    string public symbol;       // 代幣符號
    uint8 public decimals;      // 小數位數
    uint256 private _totalSupply; // 總供應量
    
    // 餘額映射
    mapping(address => uint256) private _balances;
    
    // 授權額度映射
    mapping(address => mapping(address => uint256)) private _allowances;
    
    // ===== ERC-20 事件 =====
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    
    // ===== 合約構造函數 =====
    
    /**
     * @dev 構造函數,初始化代幣
     * @param name_ 代幣名稱
     * @param symbol_ 代幣符號
     * @param decimals_ 小數位數
     * @param initialSupply_ 初始供應量
     */
    constructor(
        string memory name_,
        string memory symbol_,
        uint8 decimals_,
        uint256 initialSupply_
    ) {
        name = name_;
        symbol = symbol_;
        decimals = decimals_;
        _mint(msg.sender, initialSupply_ * (10 ** uint256(decimals_)));
    }
    
    // ===== ERC-20 標準函數 =====
    
    /**
     * @dev 返回代幣總供應量
     */
    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }
    
    /**
     * @dev 返回指定地址的代幣餘額
     */
    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }
    
    /**
     * @dev 轉帳代幣到指定地址
     */
    function transfer(address to, uint256 amount) public returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }
    
    /**
     * @dev 返回授權額度
     */
    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }
    
    /**
     * @dev 授權第三方使用指定數量的代幣
     */
    function approve(address spender, uint256 amount) public returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }
    
    /**
     * @dev 從指定地址轉帳代幣到目標地址(需要事先授權)
     */
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        _spendAllowance(from, msg.sender, amount);
        _transfer(from, to, amount);
        return true;
    }
    
    // ===== 內部函數 =====
    
    /**
     * @dev 內部轉帳邏輯
     */
    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "Transfer from zero address");
        require(to != address(0), "Transfer to zero address");
        require(_balances[from] >= amount, "Insufficient balance");
        
        _balances[from] -= amount;
        _balances[to] += amount;
        
        emit Transfer(from, to, amount);
    }
    
    /**
     * @dev 內部鑄造函數
     */
    function _mint(address account, uint256 amount) internal {
        require(account != address(0), "Mint to zero address");
        
        _totalSupply += amount;
        _balances[account] += amount;
        
        emit Transfer(address(0), account, amount);
    }
    
    /**
     * @dev 內部銷毀函數
     */
    function _burn(address account, uint256 amount) internal {
        require(account != address(0), "Burn from zero address");
        require(_balances[account] >= amount, "Insufficient balance");
        
        _balances[account] -= amount;
        _totalSupply -= amount;
        
        emit Transfer(account, address(0), amount);
    }
    
    /**
     * @dev 內部授權函數
     */
    function _approve(address owner, address spender, uint256 amount) internal {
        require(owner != address(0), "Approve from zero address");
        require(spender != address(0), "Approve to zero address");
        
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }
    
    /**
     * @dev 消費授權額度
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal {
        uint256 currentAllowance = _allowances[owner][spender];
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "Insufficient allowance");
            _approve(owner, spender, currentAllowance - amount);
        }
    }
    
    // ===== 擴展函數(可選) =====
    
    /**
     * @dev 增加授權額度(安全增加,避免先重置再增加的 race condition)
     */
    function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
        _approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue);
        return true;
    }
    
    /**
     * @dev 減少授權額度
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
        uint256 currentAllowance = _allowances[msg.sender][spender];
        require(currentAllowance >= subtractedValue, "Decreased allowance below zero");
        _approve(msg.sender, spender, currentAllowance - subtractedValue);
        return true;
    }
    
    /**
     * @dev 銷毀代幣(需持有代幣)
     */
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
    
    /**
     * @dev 從指定地址銷毀代幣(需有授權)
     */
    function burnFrom(address account, uint256 amount) public {
        _spendAllowance(account, msg.sender, amount);
        _burn(account, amount);
    }
}

Remix IDE 實作步驟

步驟一:創建 ERC-20 合約文件
- 在 Remix 中新建文件:SimpleToken.sol
- 粘貼上述代碼

步驟二:編譯合約
- 選擇編譯器版本 0.8.24
- 點擊編譯,確認無錯誤

步驟三:部署合約
- 環境:JavaScript VM(測試用)
- Constructor 參數:
  * name_: "MyToken"
  * symbol_: "MTK"
  * decimals_: 18
  * initialSupply_: 1000000

步驟四:交互測試
- 點擊「balanceOf」按鈕,輸入你的錢包地址,確認餘額為 1000000000000000000000000
- 點擊「transfer」按鈕,轉帳 1000000000000000000000(1 個代幣)到其他地址
- 查看交易歷史和餘額變化

第四章:質押合約實作

4.1 質押合約的商業邏輯

質押合約是以太坊 DeFi 生態的重要組成部分。簡單的質押合約邏輯如下:

質押合約商業邏輯:

用戶操作:
1. 用戶將代幣存入質押合約
2. 合約記錄用戶存款量和存款時間
3. 合約根據存款量和時間計算利息
4. 用戶可隨時提取本金和利息

利息來源:
- 質押獎勵(新鑄造的代幣)
- 外部收益(如借貸協議的利息收入)
- 手續費分紅

關鍵風險:
- 智能合約漏洞
- 流動性風險
- 賞沒風險(Slashing)

4.2 完整質押合約實作

以下是一個基礎版的代幣質押合約,採用「利率池」模式:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title SimpleStaking
 * @dev 簡化的代幣質押合約
 * 用戶質押代幣,獲得利息獎勵
 * 
 * 警告:此合約為教學用途,部署前請充分審計
 */
contract SimpleStaking {
    // ===== ERC-20 代幣接口 =====
    IERC20 public stakingToken;
    
    // ===== 質押參數 =====
    uint256 public rewardRate = 100; // 每秒獎勵率(基於 10^18)
    uint256 public constant REWARD_PRECISION = 1e18;
    
    // ===== 狀態變量 =====
    uint256 public totalStaked;           // 總質押量
    uint256 public accRewardPerShare;      // 累計每份獎勵
    uint256 public lastUpdateTime;         // 上次更新時間
    uint256 public rewardPerTokenStored;    // 存儲的每代幣獎勵
    
    // ===== 用戶質押信息 =====
    mapping(address => uint256) public stakedBalance;        // 質押餘額
    mapping(address => uint256) public rewards;              // 待領取獎勵
    mapping(address => uint256) public userRewardPerShare;   // 用戶的累計每份獎勵
    
    // ===== 事件 =====
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardClaimed(address indexed user, uint256 reward);
    event RewardAdded(uint256 amount);
    
    // ===== 構造函數 =====
    constructor(address _stakingToken) {
        stakingToken = IERC20(_stakingToken);
        lastUpdateTime = block.timestamp;
    }
    
    // ===== 質押函數 =====
    /**
     * @dev 質押代幣
     * 必須先調用 approve 授權此合約
     */
    function stake(uint256 amount) external updateReward(msg.sender) {
        require(amount > 0, "Cannot stake 0");
        
        // 從用戶轉移代幣到此合約
        stakingToken.transferFrom(msg.sender, address(this), amount);
        
        // 更新質押餘額
        stakedBalance[msg.sender] += amount;
        totalStaked += amount;
        
        emit Staked(msg.sender, amount);
    }
    
    // ===== 領取獎勵函數 =====
    /**
     * @dev 領取待領取的獎勵
     */
    function claimReward() external updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        require(reward > 0, "No reward to claim");
        
        rewards[msg.sender] = 0;
        
        // 將獎勵代幣轉給用戶
        stakingToken.transfer(msg.sender, reward);
        
        emit RewardClaimed(msg.sender, reward);
    }
    
    // ===== 提取質押函數 =====
    /**
     * @dev 提取全部質押本金
     */
    function withdraw() external updateReward(msg.sender) {
        uint256 amount = stakedBalance[msg.sender];
        require(amount > 0, "Nothing to withdraw");
        
        stakedBalance[msg.sender] = 0;
        totalStaked -= amount;
        
        // 將本金代幣轉給用戶
        stakingToken.transfer(msg.sender, amount);
        
        emit Withdrawn(msg.sender, amount);
    }
    
    // ===== 查詢函數 =====
    
    /**
     * @dev 計算指定地址的待領取獎勵
     */
    function earned(address account) public view returns (uint256) {
        uint256 balance = stakedBalance[account];
        uint256 currentRewardPerToken = rewardPerToken();
        uint256 lastRewardPerShare = userRewardPerShare[account];
        
        return balance * (currentRewardPerToken - lastRewardPerShare) / REWARD_PRECISION + rewards[account];
    }
    
    /**
     * @dev 獲取當前每代幣累計獎勵
     */
    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) {
            return rewardPerTokenStored;
        }
        
        uint256 timeDelta = block.timestamp - lastUpdateTime;
        uint256 pendingRewards = timeDelta * rewardRate * totalStaked / REWARD_PRECISION;
        
        return rewardPerTokenStored + pendingRewards / totalStaked;
    }
    
    // ===== 管理函數 =====
    
    /**
     * @dev 添加獎勵到獎勵池(由管理員調用)
     */
    function addRewards(uint256 amount) external {
        require(amount > 0, "Amount must be positive");
        require(stakingToken.transferFrom(msg.sender, address(this), amount), "Transfer failed");
        emit RewardAdded(amount);
    }
    
    /**
     * @dev 更新獎勵率
     */
    function setRewardRate(uint256 newRate) external {
        require(newRate > 0, "Rate must be positive");
        rewardRate = newRate;
    }
    
    // ===== 修飾符 =====
    
    /**
     * @dev 更新獎勵的修飾符
     */
    modifier updateReward(address account) {
        // 計算到當前時間的獎勵
        uint256 rewardPerToken_ = rewardPerToken();
        rewardPerTokenStored = rewardPerToken_;
        lastUpdateTime = block.timestamp;
        
        // 更新用戶的獎勵
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerShare[account] = rewardPerToken_;
        }
        _;
    }
}

/**
 * @dev ERC-20 代幣接口(精簡版)
 */
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

Remix IDE 完整實作流程

前置準備:
1. 先部署 SimpleToken 合約(第三章的合約)
2. 鑄造一些代幣給測試地址
3. 部署 SimpleStaking 合約,構造函數參數填入 SimpleToken 地址

步驟一:部署 SimpleToken
- Constructor: ("StakingToken", "STK", 18, 1000000)
- 記住部署後的合約地址

步驟二:部署 SimpleStaking
- Constructor: 填入 SimpleToken 合約地址

步驟三:授權 SimpleStaking 合約
- 在 SimpleToken 合約面板
- 點擊「approve」
- spender: SimpleStaking 合約地址
- amount: 1000000000000000000000 (1000 個代幣)

步驟四:質押代幣
- 在 SimpleStaking 合約面板
- 點擊「stake」
- amount: 100000000000000000000 (100 個代幣)
- 確認質押成功

步驟五:等待並查詢獎勵
- 等待一段時間(如 60 秒)
- 點擊「earned」,輸入你的地址
- 查看待領取獎勵

步驟六:領取獎勵
- 點擊「claimReward」
- 確認交易成功
- 檢查代幣餘額變化

第五章:常見錯誤與調試技巧

5.1 Solidity 開發常見錯誤

常見錯誤一:Integer Overflow/Underflow
錯誤描述:Solidity 0.8 之前版本,整數運算超界不會 revert
解決方案:使用 Solidity 0.8+ 版本,或使用 SafeMath 庫

錯誤二:重入攻擊(Reentrancy Attack)
錯誤描述:合約調用外部合約時,外部合約回調原合約造成狀態未更新就被再次調用
解決方案:使用 Checks-Effects-Interactions 模式,或使用 ReentrancyGuard

錯誤三:未檢查返回值
錯誤描述:某些函數(如 transfer、call)的返回值需要檢查
解決方案:總是檢查返回值或使用 SafeERC20 庫

錯誤四:Gas 限制問題
錯誤描述:合約太複雜,超過區塊 Gas 上限
解決方案:優化合約邏輯,使用事件替代存儲,使用修補(Merkle Tree)等

5.2 Remix IDE 調試功能

Remix 調試功能使用指南:

功能一:交易追蹤
- 在「Deploy & Run Transactions」面板
- 點擊任意已部署的交易
- 查看交易詳情和調試信息

功能二:日誌查看
- 底部面板的「Console」標籤
- 查看交易輸入輸出
- 查看事件日誌

功能三:區塊鏈狀態檢查
- 隨時調用 view 函數
- 查看任意地址的狀態

功能四:時間模擬
- 在 JavaScript VM 環境
- 可直接推進區塊時間
- 測試時間相關邏輯

結論

本教程涵蓋了 Solidity 開發的核心內容:

第一,我們介紹了 Remix IDE 這個強大的線上開發工具,讓讀者無需安裝任何軟體即可開始智能合約開發。

第二,我們通過「Hello Ethereum」合約,展示了 Solidity 的基本結構和語法。

第三,我們深入介紹了 Solidity 的類型系統、控制流語句和數據結構。

第四,我們實作了完整的 ERC-20 代幣合約,這是以太坊生態系統的基礎標準。

第五,我們實作了質押合約,展示了如何構建 DeFi 應用的核心功能。

第六,我們分享了常見錯誤和調試技巧,幫助讀者避免開發陷阱。

讀者在完成本教程後,應該能夠:


參考資源

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!