最後更新:2026/10/15
設計工作室寄報價單給客人,要客人「列印、簽名、掃描或拍照回傳」,回傳率大概一半,剩下的要再催。瑜伽教室的新學員要簽健康聲明和課程同意書,櫃台放一疊紙,簽完掃描歸檔,三年累積幾百張。這些事都可以在網頁上做完:客人在手機上用手指簽、電腦上用滑鼠簽,按送出,簽名變成一張圖存在你的網站,後台一筆一筆看得到。
這篇用一段程式碼做出一個 [mimi_signature] 短代碼,放在任何頁面就是一個簽名表單:姓名、簽名板、清除和送出。送出後簽名存成 PNG、建一筆「簽署紀錄」,記下時間和來源 IP,只有管理員看得到。最後講一下電子簽名在法律上的位置,哪些文件適合這樣簽、哪些不適合。
不想自己改?把這篇交給 AI
把這篇的網址(或全文)連同下面這段話貼給 AI:
我的網站是 WordPress。我想照下面這篇文章,在頁面上放一個手寫簽名的表單,簽名存成圖片、後台有紀錄。
請先問我:要簽的文件是什麼、除了姓名還要填哪些欄位、簽完要不要寄信通知我或客人。
問清楚之後,再一步一步告訴我:要用哪一段程式碼、貼在哪裡、短代碼怎麼放、怎麼測試。
如果文章的做法不適合我的網站,請直接告訴我原因和替代做法。
文章:(貼上這篇的網址,或整篇內容)
改之前先備份,密碼不要貼給 AI。
完整程式碼
// 第 98 篇:表單加手寫簽名(同意書、報價確認)
// 1. 短代碼 [mimi_signature doc="2024 春季報價單"]:姓名、簽名板、送出
add_shortcode('mimi_signature', function ($atts) {
$a = shortcode_atts(['doc' => '文件'], $atts);
ob_start();
?>
<form class="mimi-sign" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="mimi_sign">
<input type="hidden" name="doc" value="<?php echo esc_attr($a['doc']); ?>">
<input type="hidden" name="signature" class="mimi-sign-data">
<?php wp_nonce_field('mimi_sign'); ?>
<p><label>姓名 <input type="text" name="name" required></label></p>
<p>請在框內簽名:</p>
<canvas class="mimi-sign-pad" width="600" height="200" style="touch-action:none;border:2px solid #222;border-radius:12px;background:#fff;max-width:100%"></canvas>
<p><button type="button" class="mimi-sign-clear">清除</button> <button type="submit">確認並送出</button></p>
<p style="font-size:.9em;color:#555">送出即表示您已閱讀並同意「<?php echo esc_html($a['doc']); ?>」的內容。</p>
</form>
<script>
document.querySelectorAll('.mimi-sign').forEach(function (form) {
var c = form.querySelector('.mimi-sign-pad'), ctx = c.getContext('2d'), drawing = false, dirty = false;
ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#111';
function pos(e) { var r = c.getBoundingClientRect(); return [(e.clientX - r.left) * c.width / r.width, (e.clientY - r.top) * c.height / r.height]; }
c.addEventListener('pointerdown', function (e) { drawing = true; dirty = true; var p = pos(e); ctx.beginPath(); ctx.moveTo(p[0], p[1]); c.setPointerCapture(e.pointerId); });
c.addEventListener('pointermove', function (e) { if (!drawing) { return; } var p = pos(e); ctx.lineTo(p[0], p[1]); ctx.stroke(); });
c.addEventListener('pointerup', function () { drawing = false; });
c.addEventListener('pointercancel', function () { drawing = false; });
form.querySelector('.mimi-sign-clear').addEventListener('click', function () { ctx.clearRect(0, 0, c.width, c.height); dirty = false; });
form.addEventListener('submit', function (e) {
if (!dirty) { e.preventDefault(); alert('請先簽名。'); return; }
form.querySelector('.mimi-sign-data').value = c.toDataURL('image/png');
});
});
</script>
<?php
return ob_get_clean();
});
// 2. 收表單:把簽名存成 PNG,建一筆「簽署紀錄」
add_action('admin_post_nopriv_mimi_sign', 'mimi_sign_save');
add_action('admin_post_mimi_sign', 'mimi_sign_save');
function mimi_sign_save() {
check_admin_referer('mimi_sign');
$data = $_POST['signature'] ?? '';
if (strpos($data, 'data:image/png;base64,') !== 0) {
wp_die('沒有收到簽名。');
}
$png = base64_decode(substr($data, 22));
if (!$png || strlen($png) > 2 * MB_IN_BYTES) {
wp_die('簽名資料不正確。');
}
$file = wp_upload_bits('sign-' . wp_generate_password(12, false) . '.png', null, $png); // 檔名用亂數,猜不到
if ($file['error']) {
wp_die('存檔失敗:' . esc_html($file['error']));
}
$id = wp_insert_post([
'post_type' => 'mimi_signature',
'post_status' => 'private',
'post_title' => sanitize_text_field(wp_unslash($_POST['name'])) . '|' . sanitize_text_field(wp_unslash($_POST['doc'])),
'meta_input' => ['_mimi_sign_file' => $file['file'], '_mimi_sign_url' => $file['url'], '_mimi_sign_ip' => $_SERVER['REMOTE_ADDR'] ?? '', '_mimi_sign_time' => current_time('mysql')],
]);
wp_safe_redirect(add_query_arg('signed', $id, wp_get_referer() ?: home_url('/')));
exit;
}
// 3. 後台多一個「簽署紀錄」選單,只有管理員看得到
add_action('init', function () {
register_post_type('mimi_signature', ['label' => '簽署紀錄', 'public' => false, 'show_ui' => true, 'supports' => ['title'], 'capability_type' => 'post', 'capabilities' => ['create_posts' => 'do_not_allow'], 'map_meta_cap' => true, 'menu_icon' => 'dashicons-edit']);
});
add_action('add_meta_boxes', function () {
add_meta_box('mimi_sign_view', '簽名', function ($post) {
echo '<p>時間:' . esc_html(get_post_meta($post->ID, '_mimi_sign_time', true)) . ' IP:' . esc_html(get_post_meta($post->ID, '_mimi_sign_ip', true)) . '</p>';
echo '<img src="' . esc_url(get_post_meta($post->ID, '_mimi_sign_url', true)) . '" style="max-width:100%;border:1px solid #ccc">';
}, 'mimi_signature');
});
// 4. 送出後回到原頁,顯示一句完成
add_filter('the_content', function ($content) {
if (!empty($_GET['signed']) && is_singular()) {
$content = '<p class="mimi-signed">已收到您的簽名,謝謝。</p>' . $content;
}
return $content;
});
程式碼放在 Code Snippets 這類外掛或子主題,放法和備份方式見〈WooCommerce 結帳頁改成台灣習慣〉。
四段各做了什麼
第 1 段是短代碼,印出表單和簽名板。簽名板是一個 <canvas>,瀏覽器內建的畫布元素,下面的小程式聽「指標」事件(滑鼠、手指、觸控筆都算同一種),按下去開始畫線、移動就連線、放開就停。touch-action: none 讓手指在板上滑的時候頁面不會跟著捲動。按「清除」就把畫布擦掉;按「送出」時,程式先檢查有沒有簽過,沒簽就擋下並提醒「請先簽名」,有簽就把畫布轉成 PNG 的文字編碼,塞進隱藏欄位一起送出。短代碼的 doc 參數是文件名稱,會顯示在同意的聲明裡,也會記進紀錄。
第 2 段是收表單的程式,用 WordPress 的 admin-post.php 機制,沒登入的訪客也能送。它先驗證安全碼(防止別的網站偽造送出),檢查收到的是 PNG、大小在 2 MB 以內,再用 wp_upload_bits 存到上傳資料夾,檔名用 12 位亂數,別人猜不到網址。然後建一筆「簽署紀錄」:標題是「姓名|文件名稱」,附加資料存檔案位置、來源 IP 和時間,狀態是私密。最後導回原本的頁面並在網址加上 signed 參數。
第 3 段登記「簽署紀錄」這個內容類型,後台左側選單會多一項,只有能編輯文章的人看得到,而且不能從後台「新增」,紀錄只能從表單進來。每一筆打開是姓名、文件、時間、IP 和簽名圖。
第 4 段讓送出後回到原頁時,頁面最上面多一行「已收到您的簽名,謝謝」。
測試結果
我們在本機測試站建一頁「報價單確認」,放 [mimi_signature doc="2024 秋季報價單"]:
- 填姓名、沒簽名就按送出:跳出「請先簽名」,停在原頁。
- 用滑鼠在板上畫一條曲線、按送出:回到原頁,網址多了
signed=,頁面最上面出現「已收到您的簽名,謝謝」。 - 管理員後台多了「簽署紀錄」,列表有一筆「王小明|2024 秋季報價單」、狀態私密,沒有「新增」按鈕;打開那一筆,有時間、IP 和簽名圖,圖片網址是
sign-加 12 位亂數的 PNG,可以正常打開。
這樣的簽名在法律上算什麼
台灣的電子簽章法(2024 年 5 月修正)第 4 條說,符合這部法規定的電子文件和電子簽章,「在功能上等同於實體文件及簽章,不得僅因其電子形式而否認其法律效力」;第 5 條說依法令規定應簽名或蓋章的,可以用電子簽章做,但有相對人的時候,相對人要同意採用電子形式,或者你要先給對方合理的機會反對。法律也允許其他法律排除適用,司法程序另有規定。
所以用在網站上的手寫簽名,適合「同意書、聲明書、報價確認、活動切結」這類雙方都同意線上處理、金額和風險不大的文件:簽名圖、時間、IP 一起留著,就是你證明對方同意過的紀錄。不動產、擔保、大額合約這類文件,要不要用電子簽名、用哪一種(有憑證機構的數位簽章等級更高),先問律師。
另一件要注意的是個資:簽名圖和姓名是個人資料,上傳資料夾的網址雖然是亂數,仍然是公開可存取的檔案,更謹慎的做法是把檔案放在網站根目錄之外或加上存取限制,紀錄用不到了就刪。
少一個「列印簽名回傳」,就多一半的成交
客人願意簽的時候,就是現在;要他回家列印、找筆、拍照、回傳,熱度就過了。簽名板放在報價頁或課程頁的最後,客人看完、同意、簽名,一氣呵成。把它和線上付款接在一起,就是一個完整的接單流程,可以看〈在台灣先收錢再做〉和〈WooCommerce 詢價模式〉。
如果你的工作室想把報價、簽約、付款都搬到網站上,少掉來回的紙本,可以先看〈IG 帳號不是你的資產〉想想網站要做到什麼。MIMI 專做客製化網頁設計,客製功能網站可以依你的接單流程設計表單和紀錄。MIMI 提供臺北網頁設計服務,臺北的工作室可以用電話、Email 直接和設計師討論;費用可以先看首頁的製作費用參考,歡迎聯絡 MIMI。
