1242 lines
53 KiB
HTML
1242 lines
53 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN" class="scroll-smooth">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>电子签名归档系统</title>
|
||
<!-- Tailwind CSS CDN -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<!-- Phosphor Icons 图标库 -->
|
||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||
<script>
|
||
// Tailwind 配置
|
||
tailwind.config = {
|
||
darkMode: 'class', // 暗色模式手动控制
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
// Zinc 色系
|
||
gray: {
|
||
50: '#fafafa',
|
||
100: '#f4f4f5',
|
||
200: '#e4e4e7',
|
||
300: '#d4d4d8',
|
||
400: '#a1a1aa',
|
||
500: '#71717a',
|
||
600: '#52525b',
|
||
700: '#3f3f46',
|
||
800: '#27272a',
|
||
900: '#18181b',
|
||
950: '#09090b',
|
||
},
|
||
primary: {
|
||
DEFAULT: '#18181b',
|
||
dark: '#fafafa',
|
||
}
|
||
},
|
||
fontFamily: {
|
||
sans: ['Inter', '-apple-system', 'system-ui', 'sans-serif'],
|
||
mono: ['JetBrains Mono', 'Menlo', 'monospace'],
|
||
},
|
||
boxShadow: {
|
||
'paper': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
||
'paper-hover': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||
'dark-glow': '0 0 20px rgba(255, 255, 255, 0.05)',
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style type="text/tailwindcss">
|
||
@layer utilities {
|
||
/* 签名画布样式 */
|
||
.signature-canvas {
|
||
@apply w-full h-full cursor-crosshair touch-none;
|
||
}
|
||
/* 隐藏滚动条 */
|
||
.no-scrollbar::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
.no-scrollbar {
|
||
-ms-overflow-style: none;
|
||
scrollbar-width: none;
|
||
}
|
||
/* 自定义滑块样式 */
|
||
input[type=range] {
|
||
@apply h-1 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer;
|
||
}
|
||
input[type=range]::-webkit-slider-thumb {
|
||
@apply appearance-none w-3 h-3 rounded-full bg-gray-900 dark:bg-gray-100 shadow-sm transition-transform hover:scale-125;
|
||
}
|
||
/* 视图切换动画 */
|
||
.view-hidden {
|
||
display: none !important;
|
||
}
|
||
}
|
||
body {
|
||
overscroll-behavior: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans min-h-screen flex flex-col transition-colors duration-300 selection:bg-gray-900 selection:text-white dark:selection:bg-white dark:selection:text-gray-900">
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- 顶部导航栏 -->
|
||
<!-- ================================================================== -->
|
||
<nav class="fixed top-0 w-full z-40 bg-gray-50/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 transition-colors duration-300">
|
||
<div class="max-w-3xl mx-auto px-6 h-14 flex items-center justify-between">
|
||
<!-- 左侧:Logo 和标题 -->
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-2 h-2 bg-gray-900 dark:bg-gray-100 rounded-full"></div>
|
||
<h1 class="font-semibold text-sm tracking-wide uppercase text-gray-900 dark:text-gray-100">电子签名<span class="text-gray-400 dark:text-gray-600 font-light">.go</span></h1>
|
||
</div>
|
||
|
||
<!-- 右侧:导航按钮 -->
|
||
<div class="flex items-center gap-4">
|
||
<!-- 签名列表按钮(登录后显示) -->
|
||
<button id="navToList" class="hidden items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors">
|
||
<i class="ph ph-list text-base"></i>
|
||
<span class="hidden sm:inline">签名列表</span>
|
||
</button>
|
||
<!-- 返回签名按钮(列表页面显示) -->
|
||
<button id="navToSign" class="hidden items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors">
|
||
<i class="ph ph-pencil-simple text-base"></i>
|
||
<span class="hidden sm:inline">签名</span>
|
||
</button>
|
||
<!-- 登录/注销按钮 -->
|
||
<button id="authBtn" class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors">
|
||
<i class="ph ph-sign-in text-base" id="authIcon"></i>
|
||
<span id="authText">登录</span>
|
||
</button>
|
||
<!-- 主题切换按钮 -->
|
||
<button id="themeToggle" class="p-2 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors rounded-full focus:outline-none">
|
||
<i class="ph ph-moon text-lg dark:hidden"></i>
|
||
<i class="ph ph-sun text-lg hidden dark:block"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- 登录视图 -->
|
||
<!-- ================================================================== -->
|
||
<div id="loginView" class="view-hidden">
|
||
<main class="flex-grow flex items-center justify-center pt-14 pb-12 px-4">
|
||
<div class="w-full max-w-sm">
|
||
<!-- 登录卡片 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 p-8">
|
||
<!-- 标题 -->
|
||
<div class="text-center mb-8">
|
||
<div class="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i class="ph ph-lock-key text-xl text-gray-600 dark:text-gray-400"></i>
|
||
</div>
|
||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">登录系统</h2>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">请输入账号密码以访问签名列表</p>
|
||
</div>
|
||
|
||
<!-- 登录表单 -->
|
||
<form id="loginForm" class="space-y-5">
|
||
<!-- 账号输入框 -->
|
||
<div>
|
||
<label for="loginAccount" class="block text-xs font-mono text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||
账号
|
||
</label>
|
||
<input type="text" id="loginAccount"
|
||
class="w-full bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm text-gray-900 dark:text-gray-100 focus:border-gray-900 dark:focus:border-gray-100 outline-none transition-colors placeholder-gray-400 dark:placeholder-gray-500"
|
||
placeholder="请输入账号"
|
||
autocomplete="username">
|
||
</div>
|
||
|
||
<!-- 密码输入框 -->
|
||
<div>
|
||
<label for="loginPassword" class="block text-xs font-mono text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||
密码
|
||
</label>
|
||
<input type="password" id="loginPassword"
|
||
class="w-full bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm text-gray-900 dark:text-gray-100 focus:border-gray-900 dark:focus:border-gray-100 outline-none transition-colors placeholder-gray-400 dark:placeholder-gray-500"
|
||
placeholder="请输入密码"
|
||
autocomplete="current-password">
|
||
</div>
|
||
|
||
<!-- 错误提示 -->
|
||
<div id="loginError" class="hidden text-sm text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/20 px-4 py-2 rounded-lg">
|
||
<span id="loginErrorText"></span>
|
||
</div>
|
||
|
||
<!-- 登录按钮 -->
|
||
<button type="submit" id="loginSubmitBtn"
|
||
class="w-full py-2.5 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-sm font-medium rounded-lg shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 flex items-center justify-center gap-2">
|
||
<i class="ph ph-sign-in"></i>
|
||
<span>登录</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- 签名视图 -->
|
||
<!-- ================================================================== -->
|
||
<div id="signView">
|
||
<main class="flex-grow flex flex-col items-center pt-24 pb-12 px-4 sm:px-6 w-full max-w-3xl mx-auto gap-8">
|
||
|
||
<!-- 头部信息输入 -->
|
||
<div class="w-full space-y-6">
|
||
<div class="flex flex-col sm:flex-row items-start sm:items-end justify-between gap-4">
|
||
<div class="w-full sm:w-2/3 space-y-1">
|
||
<label for="signerName" class="block text-xs font-mono text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
签署人姓名
|
||
</label>
|
||
<input type="text" id="signerName"
|
||
class="w-full bg-transparent border-b-2 border-gray-200 dark:border-gray-700 py-2 text-xl font-medium text-gray-900 dark:text-gray-100 focus:border-gray-900 dark:focus:border-gray-100 outline-none transition-colors placeholder-gray-300 dark:placeholder-gray-600 rounded-none"
|
||
placeholder="请输入您的真实姓名">
|
||
</div>
|
||
<div class="font-mono text-xs text-gray-400 dark:text-gray-500 text-right w-full sm:w-auto">
|
||
<div id="currentDate">2026-01-15</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 签名区域容器 -->
|
||
<div class="w-full relative group">
|
||
|
||
<!-- 工具栏 -->
|
||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||
<!-- 左侧:笔触设置 -->
|
||
<div class="flex items-center gap-6 bg-white dark:bg-gray-800 px-4 py-2 rounded-full border border-gray-200 dark:border-gray-700 shadow-sm transition-colors duration-300">
|
||
<!-- 笔触粗细 -->
|
||
<div class="flex items-center gap-2" title="笔触粗细">
|
||
<i class="ph ph-pencil-simple text-gray-400 text-sm"></i>
|
||
<input type="range" id="strokeWidth" min="1" max="8" value="2" class="w-16">
|
||
</div>
|
||
|
||
<div class="w-px h-3 bg-gray-200 dark:bg-gray-600"></div>
|
||
|
||
<!-- 颜色选择 -->
|
||
<div class="flex items-center gap-2">
|
||
<button class="color-btn w-4 h-4 rounded-full bg-black border border-gray-200 ring-offset-2 ring-gray-900 transition-all hover:scale-110 active-color" data-color="#000000"></button>
|
||
<button class="color-btn w-4 h-4 rounded-full bg-blue-600 border border-transparent ring-offset-2 ring-blue-600 transition-all hover:scale-110" data-color="#2563EB"></button>
|
||
<button class="color-btn w-4 h-4 rounded-full bg-red-600 border border-transparent ring-offset-2 ring-red-600 transition-all hover:scale-110" data-color="#DC2626"></button>
|
||
<input type="hidden" id="strokeColor" value="#000000">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:操作按钮 -->
|
||
<div class="flex items-center gap-2">
|
||
<button id="undoBtn" class="p-2 text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 disabled:opacity-30 transition-colors" disabled title="撤销">
|
||
<i class="ph ph-arrow-u-up-left text-lg"></i>
|
||
</button>
|
||
<button id="redoBtn" class="p-2 text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 disabled:opacity-30 transition-colors" disabled title="重做">
|
||
<i class="ph ph-arrow-u-up-right text-lg"></i>
|
||
</button>
|
||
<button id="clearBtn" class="ml-2 px-3 py-1.5 text-xs font-medium text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors">
|
||
清空
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 画布区域 -->
|
||
<div class="relative w-full aspect-[16/9] sm:aspect-[2/1] bg-white rounded-lg shadow-paper hover:shadow-paper-hover dark:shadow-dark-glow transition-all duration-300 overflow-hidden border border-gray-200 dark:border-gray-700/50 dark:brightness-90">
|
||
<!-- 网格背景 -->
|
||
<div class="absolute inset-0 opacity-[0.03] pointer-events-none"
|
||
style="background-image: radial-gradient(#000 1px, transparent 1px); background-size: 20px 20px;">
|
||
</div>
|
||
<!-- 水印提示 -->
|
||
<div id="watermark" class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none transition-opacity duration-300 select-none text-gray-300 dark:text-gray-200">
|
||
<span class="text-xs font-mono border border-dashed border-gray-300 px-3 py-1 rounded text-gray-400">在此区域签名</span>
|
||
</div>
|
||
<!-- 签名画布 -->
|
||
<canvas id="signatureCanvas" class="signature-canvas relative z-10 mix-blend-multiply"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<div class="w-full flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-800">
|
||
<div class="flex items-center gap-2 text-xs text-gray-400">
|
||
<i class="ph ph-shield-check text-base"></i>
|
||
<span class="hidden sm:inline">已启用加密传输 & 安全存储</span>
|
||
</div>
|
||
|
||
<button id="saveToServerBtn" class="group relative px-6 py-2.5 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-sm font-medium rounded-lg shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 overflow-hidden disabled:opacity-70 disabled:cursor-not-allowed">
|
||
<span class="relative z-10 flex items-center gap-2">
|
||
<i class="ph ph-floppy-disk"></i>
|
||
确认归档
|
||
</span>
|
||
<div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
|
||
</button>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- 签名列表视图 -->
|
||
<!-- ================================================================== -->
|
||
<div id="listView" class="view-hidden">
|
||
<main class="flex-grow flex flex-col items-center pt-24 pb-12 px-4 sm:px-6 w-full max-w-3xl mx-auto gap-6">
|
||
|
||
<!-- 页面标题 -->
|
||
<div class="w-full flex items-center justify-between">
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">签名列表</h2>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">共 <span id="signatureCount">0</span> 条签名记录</p>
|
||
</div>
|
||
<button id="refreshListBtn" class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors">
|
||
<i class="ph ph-arrow-clockwise text-base"></i>
|
||
<span>刷新</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 签名卡片容器 -->
|
||
<div id="signatureList" class="w-full space-y-4">
|
||
<!-- 动态生成签名卡片 -->
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div id="emptyState" class="hidden w-full flex flex-col items-center justify-center py-16 text-center">
|
||
<div class="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||
<i class="ph ph-file-dashed text-2xl text-gray-400 dark:text-gray-500"></i>
|
||
</div>
|
||
<p class="text-gray-500 dark:text-gray-400 text-sm">暂无签名记录</p>
|
||
<p class="text-gray-400 dark:text-gray-500 text-xs mt-1">完成签名后将显示在此处</p>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div id="loadingState" class="hidden w-full flex flex-col items-center justify-center py-16">
|
||
<i class="ph ph-spinner animate-spin text-2xl text-gray-400"></i>
|
||
<p class="text-gray-500 dark:text-gray-400 text-sm mt-4">加载中...</p>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- 姓名填写模态框 -->
|
||
<!-- ================================================================== -->
|
||
<div id="nameModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 backdrop-blur-sm">
|
||
<div class="relative w-full max-w-sm mx-4">
|
||
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl overflow-hidden">
|
||
<!-- 头部 -->
|
||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">请输入签署人姓名</h3>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">归档前需要填写签署人姓名</p>
|
||
</div>
|
||
<!-- 输入框 -->
|
||
<div class="px-6 py-4">
|
||
<input type="text" id="modalSignerName"
|
||
class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 text-lg font-medium text-gray-900 dark:text-gray-100 focus:border-gray-900 dark:focus:border-gray-100 outline-none transition-colors placeholder-gray-400 dark:placeholder-gray-500"
|
||
placeholder="请输入姓名">
|
||
</div>
|
||
<!-- 按钮 -->
|
||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-end gap-3">
|
||
<button id="cancelNameBtn" class="px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
|
||
取消
|
||
</button>
|
||
<button id="confirmNameBtn" class="px-6 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-sm font-medium rounded-lg shadow-lg hover:shadow-xl transition-all duration-200">
|
||
确认归档
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- 签名预览模态框 -->
|
||
<!-- ================================================================== -->
|
||
<div id="previewModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 backdrop-blur-sm">
|
||
<div class="relative max-w-4xl w-full mx-4">
|
||
<!-- 关闭按钮 -->
|
||
<button id="closePreviewBtn" class="absolute -top-12 right-0 p-2 text-white/70 hover:text-white transition-colors">
|
||
<i class="ph ph-x text-2xl"></i>
|
||
</button>
|
||
<!-- 图片容器 -->
|
||
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl overflow-hidden">
|
||
<div id="previewHeader" class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||
<h3 id="previewName" class="text-lg font-semibold text-gray-900 dark:text-gray-100"></h3>
|
||
<p id="previewDate" class="text-sm text-gray-500 dark:text-gray-400 mt-1"></p>
|
||
</div>
|
||
<div class="p-4">
|
||
<img id="previewImage" src="" alt="签名预览" class="w-full max-h-[70vh] object-contain bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- Toast 通知 -->
|
||
<!-- ================================================================== -->
|
||
<div id="toast" class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 transition-all duration-300 opacity-0 translate-y-4 pointer-events-none">
|
||
<div class="bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 px-4 py-3 rounded-lg shadow-2xl flex items-center gap-3 min-w-[300px] border border-gray-800 dark:border-gray-200">
|
||
<div id="toastIcon" class="text-lg"></div>
|
||
<div class="flex-1">
|
||
<h4 id="toastTitle" class="text-sm font-semibold"></h4>
|
||
<p id="toastMessage" class="text-xs opacity-80"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================================================================== -->
|
||
<!-- JavaScript -->
|
||
<!-- ================================================================== -->
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// ==================================================================
|
||
// 全局状态
|
||
// ==================================================================
|
||
let authToken = localStorage.getItem('authToken') || ''; // 认证令牌
|
||
let currentView = 'sign'; // 当前视图:'login' | 'sign' | 'list'
|
||
|
||
// ==================================================================
|
||
// DOM 元素引用
|
||
// ==================================================================
|
||
const html = document.documentElement;
|
||
const themeToggle = document.getElementById('themeToggle');
|
||
|
||
// 视图容器
|
||
const loginView = document.getElementById('loginView');
|
||
const signView = document.getElementById('signView');
|
||
const listView = document.getElementById('listView');
|
||
|
||
// 导航按钮
|
||
const navToList = document.getElementById('navToList');
|
||
const navToSign = document.getElementById('navToSign');
|
||
const authBtn = document.getElementById('authBtn');
|
||
const authIcon = document.getElementById('authIcon');
|
||
const authText = document.getElementById('authText');
|
||
|
||
// 登录表单
|
||
const loginForm = document.getElementById('loginForm');
|
||
const loginAccount = document.getElementById('loginAccount');
|
||
const loginPassword = document.getElementById('loginPassword');
|
||
const loginError = document.getElementById('loginError');
|
||
const loginErrorText = document.getElementById('loginErrorText');
|
||
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
|
||
|
||
// 姓名填写模态框
|
||
const nameModal = document.getElementById('nameModal');
|
||
const modalSignerName = document.getElementById('modalSignerName');
|
||
const cancelNameBtn = document.getElementById('cancelNameBtn');
|
||
const confirmNameBtn = document.getElementById('confirmNameBtn');
|
||
|
||
// 签名画布
|
||
const canvas = document.getElementById('signatureCanvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const watermark = document.getElementById('watermark');
|
||
const strokeWidth = document.getElementById('strokeWidth');
|
||
const strokeColorInput = document.getElementById('strokeColor');
|
||
const colorBtns = document.querySelectorAll('.color-btn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const saveBtn = document.getElementById('saveToServerBtn');
|
||
const undoBtn = document.getElementById('undoBtn');
|
||
const redoBtn = document.getElementById('redoBtn');
|
||
const signerNameInput = document.getElementById('signerName');
|
||
|
||
// 签名列表
|
||
const signatureList = document.getElementById('signatureList');
|
||
const signatureCount = document.getElementById('signatureCount');
|
||
const emptyState = document.getElementById('emptyState');
|
||
const loadingState = document.getElementById('loadingState');
|
||
const refreshListBtn = document.getElementById('refreshListBtn');
|
||
|
||
// Toast 提示
|
||
const toast = document.getElementById('toast');
|
||
const toastTitle = document.getElementById('toastTitle');
|
||
const toastMessage = document.getElementById('toastMessage');
|
||
const toastIcon = document.getElementById('toastIcon');
|
||
|
||
// 签名状态
|
||
let isDrawing = false;
|
||
let pathHistory = [];
|
||
let undoHistory = [];
|
||
|
||
// ==================================================================
|
||
// 主题切换
|
||
// ==================================================================
|
||
// 初始化主题
|
||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||
html.classList.add('dark');
|
||
} else {
|
||
html.classList.remove('dark');
|
||
}
|
||
|
||
// 主题切换事件
|
||
themeToggle.addEventListener('click', () => {
|
||
html.classList.toggle('dark');
|
||
localStorage.theme = html.classList.contains('dark') ? 'dark' : 'light';
|
||
});
|
||
|
||
// ==================================================================
|
||
// 日期显示
|
||
// ==================================================================
|
||
document.getElementById('currentDate').textContent = new Date().toISOString().split('T')[0];
|
||
|
||
// ==================================================================
|
||
// 视图切换
|
||
// ==================================================================
|
||
/**
|
||
* 切换显示的视图
|
||
* @param {string} view - 要显示的视图名称:'login' | 'sign' | 'list'
|
||
*/
|
||
function switchView(view) {
|
||
currentView = view;
|
||
|
||
// 隐藏所有视图
|
||
loginView.classList.add('view-hidden');
|
||
signView.classList.add('view-hidden');
|
||
listView.classList.add('view-hidden');
|
||
|
||
// 显示目标视图
|
||
switch (view) {
|
||
case 'login':
|
||
loginView.classList.remove('view-hidden');
|
||
break;
|
||
case 'sign':
|
||
signView.classList.remove('view-hidden');
|
||
break;
|
||
case 'list':
|
||
listView.classList.remove('view-hidden');
|
||
break;
|
||
}
|
||
|
||
// 更新导航按钮显示状态
|
||
updateNavButtons();
|
||
}
|
||
|
||
/**
|
||
* 更新导航按钮的显示状态
|
||
*/
|
||
function updateNavButtons() {
|
||
const isLoggedIn = authToken !== '';
|
||
|
||
// 签名列表按钮始终可见
|
||
navToList.classList.remove('hidden');
|
||
navToList.classList.add('flex');
|
||
|
||
// 更新登录/注销按钮状态
|
||
if (isLoggedIn) {
|
||
authText.textContent = '注销';
|
||
authIcon.classList.remove('ph-sign-in');
|
||
authIcon.classList.add('ph-sign-out');
|
||
} else {
|
||
authText.textContent = '登录';
|
||
authIcon.classList.remove('ph-sign-out');
|
||
authIcon.classList.add('ph-sign-in');
|
||
}
|
||
|
||
// 列表页面显示返回按钮
|
||
if (currentView === 'list') {
|
||
navToSign.classList.remove('hidden');
|
||
navToSign.classList.add('flex');
|
||
} else {
|
||
navToSign.classList.add('hidden');
|
||
navToSign.classList.remove('flex');
|
||
}
|
||
}
|
||
|
||
// ==================================================================
|
||
// 登录/注销
|
||
// ==================================================================
|
||
|
||
/**
|
||
* 显示登录错误信息
|
||
* @param {string} message - 错误信息
|
||
*/
|
||
function showLoginError(message) {
|
||
loginErrorText.textContent = message;
|
||
loginError.classList.remove('hidden');
|
||
}
|
||
|
||
/**
|
||
* 隐藏登录错误信息
|
||
*/
|
||
function hideLoginError() {
|
||
loginError.classList.add('hidden');
|
||
}
|
||
|
||
/**
|
||
* 处理登录表单提交
|
||
*/
|
||
loginForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
hideLoginError();
|
||
|
||
const account = loginAccount.value.trim();
|
||
const password = loginPassword.value.trim();
|
||
|
||
// 验证输入
|
||
if (!account || !password) {
|
||
showLoginError('请输入账号和密码');
|
||
return;
|
||
}
|
||
|
||
// 禁用按钮,显示加载状态
|
||
loginSubmitBtn.disabled = true;
|
||
loginSubmitBtn.innerHTML = '<i class="ph ph-spinner animate-spin"></i> 登录中...';
|
||
|
||
try {
|
||
// 发送登录请求
|
||
const response = await fetch('/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ account, password })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
// 登录成功,保存令牌
|
||
authToken = result.token;
|
||
localStorage.setItem('authToken', authToken);
|
||
showToast('success', '登录成功', '欢迎回来!');
|
||
// 登录成功后跳转到签名列表
|
||
switchView('list');
|
||
loadSignatureList();
|
||
} else {
|
||
// 登录失败
|
||
showLoginError(result.message || '账号或密码错误');
|
||
}
|
||
} catch (error) {
|
||
console.error('登录失败:', error);
|
||
showLoginError('网络错误,请检查连接');
|
||
} finally {
|
||
// 恢复按钮状态
|
||
loginSubmitBtn.disabled = false;
|
||
loginSubmitBtn.innerHTML = '<i class="ph ph-sign-in"></i> <span>登录</span>';
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 处理注销
|
||
*/
|
||
function handleLogout() {
|
||
authToken = '';
|
||
localStorage.removeItem('authToken');
|
||
showToast('success', '已注销', '您已安全退出登录');
|
||
// 注销后回到签名页面
|
||
switchView('sign');
|
||
}
|
||
|
||
/**
|
||
* 验证当前令牌是否有效
|
||
* @returns {Promise<boolean>} 令牌是否有效
|
||
*/
|
||
async function checkAuth() {
|
||
if (!authToken) return false;
|
||
|
||
try {
|
||
const response = await fetch('/api/check-auth', {
|
||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||
});
|
||
const result = await response.json();
|
||
if (!result.authenticated) {
|
||
// 令牌无效,清除本地存储
|
||
authToken = '';
|
||
localStorage.removeItem('authToken');
|
||
}
|
||
return result.authenticated;
|
||
} catch (error) {
|
||
console.error('验证登录状态失败:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ==================================================================
|
||
// 导航按钮事件
|
||
// ==================================================================
|
||
|
||
// 登录/注销按钮
|
||
authBtn.addEventListener('click', async () => {
|
||
if (authToken) {
|
||
// 已登录,执行注销
|
||
handleLogout();
|
||
} else {
|
||
// 未登录,切换到登录视图
|
||
switchView('login');
|
||
}
|
||
});
|
||
|
||
// 签名列表按钮 - 点击时检查登录状态
|
||
navToList.addEventListener('click', async () => {
|
||
if (!authToken) {
|
||
// 未登录,跳转到登录页
|
||
switchView('login');
|
||
showToast('error', '需要登录', '请先登录后再查看签名列表');
|
||
return;
|
||
}
|
||
// 已登录,验证令牌有效性
|
||
const isAuth = await checkAuth();
|
||
if (isAuth) {
|
||
switchView('list');
|
||
loadSignatureList();
|
||
} else {
|
||
// 令牌无效,跳转登录页
|
||
switchView('login');
|
||
showToast('error', '登录已过期', '请重新登录');
|
||
}
|
||
});
|
||
|
||
// 返回签名按钮
|
||
navToSign.addEventListener('click', () => {
|
||
switchView('sign');
|
||
});
|
||
|
||
// 刷新列表按钮
|
||
refreshListBtn.addEventListener('click', () => {
|
||
loadSignatureList();
|
||
});
|
||
|
||
// ==================================================================
|
||
// 签名画布逻辑
|
||
// ==================================================================
|
||
|
||
/**
|
||
* 调整画布大小以适配容器
|
||
*/
|
||
function resizeCanvas() {
|
||
const parent = canvas.parentElement;
|
||
const dpr = window.devicePixelRatio || 2;
|
||
const rect = parent.getBoundingClientRect();
|
||
|
||
canvas.width = rect.width * dpr;
|
||
canvas.height = rect.height * dpr;
|
||
|
||
ctx.scale(dpr, dpr);
|
||
canvas.style.width = `${rect.width}px`;
|
||
canvas.style.height = `${rect.height}px`;
|
||
|
||
redrawCanvas();
|
||
}
|
||
|
||
// 延迟调整画布大小
|
||
let resizeTimeout;
|
||
window.addEventListener('resize', () => {
|
||
clearTimeout(resizeTimeout);
|
||
resizeTimeout = setTimeout(resizeCanvas, 100);
|
||
});
|
||
setTimeout(resizeCanvas, 50);
|
||
|
||
/**
|
||
* 获取触摸/鼠标坐标
|
||
*/
|
||
function getPoint(e) {
|
||
const rect = canvas.getBoundingClientRect();
|
||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||
return {
|
||
x: clientX - rect.left,
|
||
y: clientY - rect.top
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 开始绘制
|
||
*/
|
||
function startDrawing(e) {
|
||
if (e.cancelable) e.preventDefault();
|
||
isDrawing = true;
|
||
watermark.style.opacity = '0';
|
||
|
||
const point = getPoint(e);
|
||
const newPath = {
|
||
points: [point],
|
||
color: strokeColorInput.value,
|
||
width: parseInt(strokeWidth.value)
|
||
};
|
||
|
||
pathHistory.push(newPath);
|
||
undoHistory = [];
|
||
updateCanvasUI();
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(point.x, point.y);
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
ctx.strokeStyle = newPath.color;
|
||
ctx.lineWidth = newPath.width;
|
||
ctx.lineTo(point.x, point.y);
|
||
ctx.stroke();
|
||
}
|
||
|
||
/**
|
||
* 绘制中
|
||
*/
|
||
function draw(e) {
|
||
if (!isDrawing) return;
|
||
if (e.cancelable) e.preventDefault();
|
||
|
||
const point = getPoint(e);
|
||
const currentPath = pathHistory[pathHistory.length - 1];
|
||
currentPath.points.push(point);
|
||
|
||
ctx.lineTo(point.x, point.y);
|
||
ctx.stroke();
|
||
}
|
||
|
||
/**
|
||
* 停止绘制
|
||
*/
|
||
function stopDrawing() {
|
||
if (isDrawing) {
|
||
isDrawing = false;
|
||
ctx.closePath();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重绘画布
|
||
*/
|
||
function redrawCanvas() {
|
||
ctx.save();
|
||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
ctx.restore();
|
||
|
||
if (pathHistory.length === 0) {
|
||
watermark.style.opacity = '1';
|
||
return;
|
||
}
|
||
watermark.style.opacity = '0';
|
||
|
||
pathHistory.forEach(path => {
|
||
if (path.points.length < 1) return;
|
||
|
||
ctx.beginPath();
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
ctx.strokeStyle = path.color;
|
||
ctx.lineWidth = path.width;
|
||
|
||
ctx.moveTo(path.points[0].x, path.points[0].y);
|
||
for (let i = 1; i < path.points.length; i++) {
|
||
ctx.lineTo(path.points[i].x, path.points[i].y);
|
||
}
|
||
ctx.stroke();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新画布 UI(撤销/重做按钮状态)
|
||
*/
|
||
function updateCanvasUI() {
|
||
undoBtn.disabled = pathHistory.length === 0;
|
||
redoBtn.disabled = undoHistory.length === 0;
|
||
}
|
||
|
||
// 画布事件监听
|
||
canvas.addEventListener('mousedown', startDrawing);
|
||
canvas.addEventListener('mousemove', draw);
|
||
window.addEventListener('mouseup', stopDrawing);
|
||
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
||
canvas.addEventListener('touchmove', draw, { passive: false });
|
||
canvas.addEventListener('touchend', stopDrawing);
|
||
|
||
// 清空画布
|
||
clearBtn.addEventListener('click', () => {
|
||
pathHistory = [];
|
||
undoHistory = [];
|
||
redrawCanvas();
|
||
updateCanvasUI();
|
||
});
|
||
|
||
// 撤销
|
||
undoBtn.addEventListener('click', () => {
|
||
if (pathHistory.length > 0) {
|
||
undoHistory.push(pathHistory.pop());
|
||
redrawCanvas();
|
||
updateCanvasUI();
|
||
}
|
||
});
|
||
|
||
// 重做
|
||
redoBtn.addEventListener('click', () => {
|
||
if (undoHistory.length > 0) {
|
||
pathHistory.push(undoHistory.pop());
|
||
redrawCanvas();
|
||
updateCanvasUI();
|
||
}
|
||
});
|
||
|
||
// ==================================================================
|
||
// 颜色选择
|
||
// ==================================================================
|
||
colorBtns.forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
colorBtns.forEach(b => {
|
||
b.classList.remove('ring-2');
|
||
b.style.transform = '';
|
||
});
|
||
strokeColorInput.value = e.target.dataset.color;
|
||
e.target.classList.add('ring-2');
|
||
});
|
||
});
|
||
colorBtns[0].click();
|
||
|
||
// ==================================================================
|
||
// 上传签名
|
||
// ==================================================================
|
||
saveBtn.addEventListener('click', async () => {
|
||
// 验证签名
|
||
if (pathHistory.length === 0) {
|
||
showToast('error', '画布为空', '请先进行签名。');
|
||
return;
|
||
}
|
||
|
||
const name = signerNameInput.value.trim();
|
||
|
||
// 名字为空,弹出模态框让用户填写
|
||
if (!name) {
|
||
openNameModal();
|
||
return;
|
||
}
|
||
|
||
// 执行上传
|
||
await uploadSignature(name);
|
||
});
|
||
|
||
/**
|
||
* 打开姓名填写模态框
|
||
*/
|
||
function openNameModal() {
|
||
modalSignerName.value = '';
|
||
nameModal.classList.remove('hidden');
|
||
nameModal.classList.add('flex');
|
||
document.body.style.overflow = 'hidden';
|
||
setTimeout(() => modalSignerName.focus(), 100);
|
||
}
|
||
|
||
/**
|
||
* 关闭姓名填写模态框
|
||
*/
|
||
function closeNameModal() {
|
||
nameModal.classList.add('hidden');
|
||
nameModal.classList.remove('flex');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
// 取消按钮事件
|
||
cancelNameBtn.addEventListener('click', closeNameModal);
|
||
|
||
// 点击背景关闭
|
||
nameModal.addEventListener('click', (e) => {
|
||
if (e.target === nameModal) {
|
||
closeNameModal();
|
||
}
|
||
});
|
||
|
||
// 确认归档按钮事件
|
||
confirmNameBtn.addEventListener('click', async () => {
|
||
const name = modalSignerName.value.trim();
|
||
if (!name) {
|
||
modalSignerName.classList.add('border-red-500');
|
||
setTimeout(() => modalSignerName.classList.remove('border-red-500'), 2000);
|
||
return;
|
||
}
|
||
signerNameInput.value = name;
|
||
closeNameModal();
|
||
await uploadSignature(name);
|
||
});
|
||
|
||
// 回车确认
|
||
modalSignerName.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
confirmNameBtn.click();
|
||
}
|
||
});
|
||
|
||
// ESC 关闭
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && !nameModal.classList.contains('hidden')) {
|
||
closeNameModal();
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 执行签名上传
|
||
* @param {string} name - 签署人姓名
|
||
*/
|
||
async function uploadSignature(name) {
|
||
// 禁用按钮,显示加载状态
|
||
const originalHTML = saveBtn.innerHTML;
|
||
saveBtn.disabled = true;
|
||
saveBtn.innerHTML = '<i class="ph ph-spinner animate-spin"></i> 处理中...';
|
||
|
||
try {
|
||
const dataURL = canvas.toDataURL('image/png');
|
||
|
||
const response = await fetch('/upload', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, image: dataURL })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast('success', '归档成功', '签名已安全保存至服务器。');
|
||
// 清空画布
|
||
setTimeout(() => {
|
||
pathHistory = [];
|
||
undoHistory = [];
|
||
signerNameInput.value = '';
|
||
redrawCanvas();
|
||
updateCanvasUI();
|
||
}, 1200);
|
||
} else {
|
||
showToast('error', '上传失败', result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
showToast('error', '连接错误', '无法连接到服务器,请检查网络。');
|
||
} finally {
|
||
saveBtn.disabled = false;
|
||
saveBtn.innerHTML = originalHTML;
|
||
}
|
||
}
|
||
|
||
// ==================================================================
|
||
// 签名列表
|
||
// ==================================================================
|
||
|
||
/**
|
||
* 加载签名列表
|
||
*/
|
||
async function loadSignatureList() {
|
||
// 验证登录状态
|
||
if (!authToken) {
|
||
switchView('login');
|
||
return;
|
||
}
|
||
|
||
// 显示加载状态
|
||
signatureList.innerHTML = '';
|
||
emptyState.classList.add('hidden');
|
||
loadingState.classList.remove('hidden');
|
||
|
||
try {
|
||
const response = await fetch('/api/signatures', {
|
||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||
});
|
||
|
||
// 检查是否需要重新登录
|
||
if (response.status === 401) {
|
||
authToken = '';
|
||
localStorage.removeItem('authToken');
|
||
switchView('login');
|
||
showToast('error', '登录已过期', '请重新登录');
|
||
return;
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
renderSignatureList(result.signatures);
|
||
} else {
|
||
showToast('error', '加载失败', result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载签名列表失败:', error);
|
||
showToast('error', '网络错误', '无法连接到服务器');
|
||
} finally {
|
||
loadingState.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染签名列表
|
||
* @param {Array} signatures - 签名数据数组
|
||
*/
|
||
function renderSignatureList(signatures) {
|
||
signatureCount.textContent = signatures.length;
|
||
|
||
if (signatures.length === 0) {
|
||
signatureList.innerHTML = '';
|
||
emptyState.classList.remove('hidden');
|
||
return;
|
||
}
|
||
|
||
emptyState.classList.add('hidden');
|
||
|
||
// 生成签名卡片 HTML
|
||
signatureList.innerHTML = signatures.map(sig => `
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 hover:shadow-lg transition-shadow duration-200">
|
||
<div class="flex items-center justify-between">
|
||
<!-- 左侧:签名信息 -->
|
||
<div class="flex items-center gap-4">
|
||
<!-- 签名预览按钮(点击弹出大图) -->
|
||
<button class="preview-btn w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden hover:ring-2 hover:ring-gray-400 dark:hover:ring-gray-500 transition-all cursor-pointer"
|
||
data-path="${escapeHtml(sig.path)}" data-name="${escapeHtml(sig.name)}" data-date="${escapeHtml(sig.date)}">
|
||
<img src="/download?path=${encodeURIComponent(sig.path)}&token=${encodeURIComponent(authToken)}"
|
||
alt="签名预览"
|
||
class="w-full h-full object-contain pointer-events-none"
|
||
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
|
||
<div class="w-full h-full items-center justify-center hidden pointer-events-none">
|
||
<i class="ph ph-user text-xl text-gray-500 dark:text-gray-400"></i>
|
||
</div>
|
||
</button>
|
||
<!-- 签名详情 -->
|
||
<div class="min-w-0">
|
||
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">${escapeHtml(sig.name)}</h3>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">${escapeHtml(sig.date)}</p>
|
||
<p class="text-xs text-gray-400 dark:text-gray-500 truncate mt-0.5" title="${escapeHtml(sig.path)}">${formatFileSize(sig.size)}</p>
|
||
</div>
|
||
</div>
|
||
<!-- 右侧:下载按钮 -->
|
||
<button class="download-btn flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors flex-shrink-0"
|
||
data-path="${escapeHtml(sig.path)}" data-name="${escapeHtml(sig.name)}_${escapeHtml(sig.date)}.png">
|
||
<i class="ph ph-download text-base"></i>
|
||
<span class="hidden sm:inline">下载</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 绑定下载按钮事件
|
||
document.querySelectorAll('.download-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const path = btn.dataset.path;
|
||
const name = btn.dataset.name;
|
||
downloadSignature(path, name);
|
||
});
|
||
});
|
||
|
||
// 绑定预览按钮事件
|
||
document.querySelectorAll('.preview-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const path = btn.dataset.path;
|
||
const name = btn.dataset.name;
|
||
const date = btn.dataset.date;
|
||
openPreviewModal(path, name, date);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 下载签名文件
|
||
* @param {string} filePath - 文件路径
|
||
* @param {string} fileName - 下载时的文件名
|
||
*/
|
||
function downloadSignature(filePath, fileName) {
|
||
const url = `/download?path=${encodeURIComponent(filePath)}&token=${encodeURIComponent(authToken)}`;
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = fileName;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
}
|
||
|
||
// ==================================================================
|
||
// 签名预览模态框
|
||
// ==================================================================
|
||
const previewModal = document.getElementById('previewModal');
|
||
const previewImage = document.getElementById('previewImage');
|
||
const previewName = document.getElementById('previewName');
|
||
const previewDate = document.getElementById('previewDate');
|
||
|
||
/**
|
||
* 打开预览模态框
|
||
* @param {string} path - 签名文件路径
|
||
* @param {string} name - 签署人姓名
|
||
* @param {string} date - 签名日期
|
||
*/
|
||
function openPreviewModal(path, name, date) {
|
||
previewImage.src = `/download?path=${encodeURIComponent(path)}&token=${encodeURIComponent(authToken)}`;
|
||
previewName.textContent = name;
|
||
previewDate.textContent = date;
|
||
previewModal.classList.remove('hidden');
|
||
previewModal.classList.add('flex');
|
||
document.body.style.overflow = 'hidden'; // 禁止背景滚动
|
||
}
|
||
|
||
/**
|
||
* 关闭预览模态框
|
||
*/
|
||
function closePreviewModal() {
|
||
previewModal.classList.add('hidden');
|
||
previewModal.classList.remove('flex');
|
||
document.body.style.overflow = ''; // 恢复背景滚动
|
||
}
|
||
|
||
// 点击背景关闭模态框
|
||
previewModal.addEventListener('click', (e) => {
|
||
if (e.target === previewModal) {
|
||
closePreviewModal();
|
||
}
|
||
});
|
||
|
||
// 关闭按钮事件
|
||
document.getElementById('closePreviewBtn').addEventListener('click', closePreviewModal);
|
||
|
||
// ESC 键关闭模态框
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && !previewModal.classList.contains('hidden')) {
|
||
closePreviewModal();
|
||
}
|
||
});
|
||
|
||
/**
|
||
* HTML 转义(防止 XSS)
|
||
*/
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/**
|
||
* 格式化文件大小
|
||
*/
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
}
|
||
|
||
// ==================================================================
|
||
// Toast 通知
|
||
// ==================================================================
|
||
let toastTimeout;
|
||
|
||
/**
|
||
* 显示 Toast 提示
|
||
* @param {string} type - 类型:'success' | 'error'
|
||
* @param {string} title - 标题
|
||
* @param {string} message - 内容
|
||
*/
|
||
function showToast(type, title, message) {
|
||
if (toastTimeout) clearTimeout(toastTimeout);
|
||
|
||
const icons = {
|
||
success: '<i class="ph ph-check-circle text-green-400"></i>',
|
||
error: '<i class="ph ph-warning-circle text-red-400"></i>'
|
||
};
|
||
|
||
toastIcon.innerHTML = icons[type] || '';
|
||
toastTitle.textContent = title;
|
||
toastMessage.textContent = message;
|
||
|
||
toast.classList.remove('translate-y-4', 'opacity-0', 'pointer-events-none');
|
||
|
||
toastTimeout = setTimeout(() => {
|
||
toast.classList.add('translate-y-4', 'opacity-0', 'pointer-events-none');
|
||
}, 3500);
|
||
}
|
||
|
||
// ==================================================================
|
||
// 初始化
|
||
// ==================================================================
|
||
function init() {
|
||
// 默认进入签名页面(无需登录)
|
||
switchView('sign');
|
||
}
|
||
|
||
// 启动应用
|
||
init();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|