首页
关于
Search
1
这里是本地笔记空间 - 记录我的笔记
2 阅读
2
HTML账号管理器
2 阅读
3
晓科的博客 - 网站搭建历程碑
2 阅读
4
文章代码块默认最多显示12行,超出自动出现滚动条
1 阅读
5
一款轻量级PHP在线聊天系统源码
1 阅读
默认分类
登录
Search
Typecho
累计撰写
15
篇文章
累计收到
1
条评论
首页
栏目
默认分类
页面
关于
搜索到
15
篇与
的结果
2026-05-11
引流拓客文本加密系统
网站源码文件:{cloud title="文件加密系统(带卡密售卖)" type="lz" url="https://wwbwh.lanzouw.com/ipNif3p803ch" password=""/}文件保存在蓝奏云上面,登录手机号:15310649220 文件保存路径:根目录/2026/05/11/文本加密系统(带卡密售卖){cloud title="夸克网盘" type="default" url="https://pan.quark.cn/s/ff646252d408" password=""/}登录手机号:15310649220 文件保存路径:根目录/2026/05/11/文本加密系统(带卡密售卖)下面是博客文章原文内容一款话术文本加密系统{card-default label="系统介绍" width="100%"}支持市面上大部分平台,某鱼、某音、红薯、某博、某宝等等,几乎涵盖了绝大多数平台的一个工具,可以帮你把文本加密,简单做到防检测,适合做引流、拓客、获客用的一个必备工具,处理之后文本看上去没有变化,实际上每个字都被单独设计重组了,可以避开不敏感的关键词检测。{/card-default}效果演示实际效果可以自己测试{card-default label="下方为加密演示文本:" width="100%"}点击链接、关注我、加V、私信、购买、下单、赚钱、刷单、暴富。{/card-default}闲鱼卖一块的{message type="success" content="可以看到这个需求还是挺大的,而且他的商品发布还不到七天,如果你有空可以走我这低价拿货自己去卖的话,定价多少你自己决定,卖多少都是你自己的"/}{dotted startColor="#099a41" endColor="#1989fa"/}文本加密系统地址{callout color="#0b9bf4"}暂时没有搭建这个系统{/callout}{message type="warning" content="需要先获取卡密,且每个卡密只能使用一次"/}卡密获取方式{callout color="#f0ad4e"}加我微信:K520A6{/callout}{card-default label="售价说明" width="100%"}单卡密:10代理批发(10张起)5块一张{/card-default}{dotted startColor="#6bffb3" endColor="#1989fa"/}{card-describe title="声明:"}此工具仅用作避免关键词的误屏蔽,提高文本安全性。禁止用于传播不良信息。使用不当造成不良后果概不负责。没有永久防检测工具,过于敏感的词语即使加密也有被检测到的可能性(例如人工审查),因此这个工具并不是万能的,低调使用,虽然这是个好东西但是切勿滥用,不然到时候某一天算法可以识别控制符了对谁都不好 {/card-describe}{callout color="#ff0000"}小提示:很多平台会有重复检测,因此每次重复使用可以随意删除几个字符(不会改变你的文本内容)对于过于敏感词还是建议使用一些形象词字母符号表示(例如:加微=> + v )在配个这个工具使用效果会好很多,这个工具原理就是把文本让人看得懂,让系统、检索、查重、AI这些识别不到具体内容。{/callout}{lamp/}
2026年05月11日
1 阅读
0 评论
0 点赞
2026-05-11
一款轻量级PHP在线聊天系统源码
文件管理{cloud title="仿微信聊天系统" type="lz" url="https://wwbwh.lanzouw.com/iN1CE3p82i5c" password=""/}登录手机:15310649220 文件路径:根目录/2026/04/21/仿微信聊天系统/仿微信在线聊天系统(单PHP文件实现).zip{cloud title="夸克网盘" type="default" url="https://pan.quark.cn/s/0d0988f27ed0" password=""/}登录手机:15310649220 文件路径:根目录/2026/04/21/仿微信聊天系统/仿微信在线聊天系统(单PHP文件实现).zip下面是博客网站上的原文章这个聊天工具的设计思路、核心功能以及使用方式。这款系统无需依赖数据库,基于文件存储实现,轻量化易部署,同时兼顾了安全性和用户体验,适合小型场景下的即时沟通需求。演示图片:电脑端聊天页电脑端登录页手机端登录页手机端聊天页手机端个人中心手机端好友列表{alert type="info"}在线聊天工具演示地址:https://a.aszv.top/liaotian/{/alert}{cloud title="在线聊天源码" type="lz" url="https://wwbwh.lanzouw.com/iS2MV3nu34ih" password=""/}一、开发初衷一个“开箱即用”的聊天解决方案——无需配置复杂的数据库环境,无需繁琐的部署步骤,普通开发者甚至新手都能快速搭建起一个可用的在线聊天工具。同时,也要保证基础的安全性和良好的移动端适配,满足不同设备的使用场景。因此,最终选择了以PHP为核心,结合文件加密存储、原生JS/CSS实现前端交互的技术方案。二、核心特性与技术实现1. 无数据库设计,文件加密存储系统摒弃了传统的数据库依赖,所有用户数据、聊天记录均以文件形式存储在服务器本地,且通过AES-256-CBC加密算法对数据进行加密处理:定义32位随机密钥作为加密核心,保证数据安全性;封装encrypt()和decrypt()函数,实现数据的加密存储与解密读取;用户数据存储在data/users.dat,聊天记录按“用户ID_用户ID”命名存储(如chat_1_2.dat),保证聊天记录的隔离性。同时,系统会自动初始化data/和avatar/目录,并通过.htaccess文件做安全防护:data/目录禁止外部访问,avatar/目录允许正常访问,兼顾数据安全与资源访问需求。2. 完整的用户体系系统实现了从注册、登录到个人信息管理的全流程用户功能:注册/登录:校验账号唯一性,密码通过password_hash()加密存储,登录状态基于PHP Session维持;个人信息管理:支持修改用户名、重置密码,以及头像上传(支持jpg/png/gif/webp等多格式,自动居中裁剪为200×200像素,压缩优化);状态校验:所有核心接口均做登录状态验证,未登录用户无法访问聊天相关功能。3. 即时聊天核心功能聊天功能聚焦“简洁实用”,满足基础沟通需求:联系人列表:登录后自动加载除自身外的所有用户,点击即可进入聊天窗口;聊天记录管理:支持增量拉取聊天记录(减少资源消耗)、清空个人聊天记录(仅对当前用户生效,不影响对方记录);消息交互:消息气泡区分“自己/对方”,适配移动端与PC端的布局,支持多行消息、自动换行,消息时间戳清晰展示;输入体验优化:输入框自适应高度,支持回车发送,按钮状态禁用/启用逻辑完善。4. 全端适配的UI设计前端界面采用原生CSS实现响应式布局,兼顾PC端和移动端体验:PC端:居中布局,左侧联系人栏+右侧聊天区的经典双栏设计;移动端:单栏切换布局,点击返回按钮可切换回联系人列表,适配375px及以下超小屏;视觉细节:消息气泡带小尾巴、头像圆角、hover交互效果、弹窗模糊背景+过渡动画,提升使用体验。三、部署与使用说明1. 部署要求服务器支持PHP 7.0及以上版本,建议PHP8.2版本(需开启openssl、gd扩展,用于加密和图片处理);服务器需有目录写入权限(用于创建data/、avatar/目录及存储文件);建议部署在Apache服务器(.htaccess文件需Apache支持,Nginx需自行配置对应规则)。2. 部署步骤将源码上传至服务器网站根目录;修改index.php中ENCRYPT_KEY为32位随机字符串(重要!提升数据安全性);访问域名/IP即可进入登录注册页面,注册账号后即可使用。3. 核心配置说明// 核心配置(部署时务必修改) define('DATA_DIR', __DIR__ . '/data/'); // 用户/聊天记录存储目录 define('AVATAR_DIR', __DIR__ . '/avatar/'); // 头像存储目录 define('ENCRYPT_KEY', 'your_32byte_secret_key_here_1234567890'); // 32位加密密钥 define('ENCRYPT_METHOD', 'AES-256-CBC'); // 加密算法四、扩展与优化方向作为一款轻量级系统,预留了不少扩展空间,开发者可根据需求自行优化:实时消息推送:当前版本为“拉取式”获取消息,可集成WebSocket实现消息实时推送;消息类型扩展:目前仅支持文本消息,可增加图片、表情、文件发送功能;多端同步:可增加Token机制,实现多设备登录状态同步;用户权限管理:增加管理员角色,支持禁用用户、清理全局聊天记录等;性能优化:大用户量场景下,可将文件存储替换为数据库,提升读写效率。五、总结这款PHP在线聊天系统的核心设计理念是“轻量化、易部署、高可用”,剥离了复杂的功能,聚焦聊天的核心需求,同时保证了基础的安全性和用户体验。无论是用于个人学习、小型团队内部沟通,还是作为二次开发的基础框架,都具备一定的实用性。如果你在使用过程中遇到问题,或者有优化建议,欢迎随时交流。希望这款源码能为有需要的开发者提供一点帮助,也期待大家基于此做出更多有趣的扩展和改进。{bilibili bvid="BV1rqoaBiE5H" page="1"/}
2026年05月11日
1 阅读
0 评论
0 点赞
2026-05-11
HTML账号管理器
这是我用AI做的一个可以管理账号的网页工具这是开发时的完整文件(包含历史版本){cloud title="完整开发文件(包含历史版本)" type="lz" url="https://wwbwh.lanzouw.com/iawQD3p80smh" password=""/}文件存在蓝奏云上,登录手机号:15310649220 文件路径:根目录/2026/05/11/账号管理器/完整开发文件(包含历史版本).zip{cloud title="夸克网盘" type="default" url="https://pan.quark.cn/s/c85473aba0a6" password=""/}登录手机号:15310649220 文件路径:根目录/2026/05/11/账号管理器/完整开发文件(包含历史版本).zip原版完整代码:<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9, user-scalable=no"> <title>账号管理器</title> <style> :root { --bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #fafafa; --text-primary: #111111; --text-secondary: #666666; --text-tertiary: #999999; --border: #e5e5e5; --accent: #0066ff; --accent-hover: #0052cc; --danger: #ff3b30; --success: #34c759; --warning: #ff9500; --shadow: rgba(0, 0, 0, 0.08); --card-bg: #ffffff; --input-bg: #ffffff; --modal-overlay: rgba(0, 0, 0, 0.5); } [data-theme="dark"] { --bg-primary: #000000; --bg-secondary: #1c1c1e; --bg-tertiary: #2c2c2e; --text-primary: #ffffff; --text-secondary: #8e8e93; --text-tertiary: #636366; --border: #38383a; --accent: #0a84ff; --accent-hover: #409cff; --danger: #ff453a; --success: #30d158; --warning: #ff9f0a; --shadow: rgba(0, 0, 0, 0.3); --card-bg: #1c1c1e; --input-bg: #2c2c2e; --modal-overlay: rgba(0, 0, 0, 0.8); } * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; } /* Allow text selection in specific elements */ .field-text, .view-value-text, .note-text, .form-input, textarea, input { -webkit-user-select: text; user-select: text; } body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.5; -webkit-font-smoothing: antialiased; transition: background 0.3s, color 0.3s; } /* Sort Icon Button - Stats Bar Style */ .sort-icon-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-secondary); border-radius: 20px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; margin-left: auto; } .sort-icon-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--bg-tertiary); } .sort-icon-btn.active { background: var(--accent); color: white; border-color: var(--accent); } .sort-icon-btn.active:hover { background: var(--accent-hover); border-color: var(--accent-hover); } .sort-text { font-size: 12px; font-weight: 600; } /* Header */ .header { position: sticky; top: 0; background: var(--bg-primary); border-bottom: 1px solid var(--border); z-index: 100; transition: all 0.3s; } .header-content { max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; gap: 20px; } .logo { display: flex; align-items: center; gap: 12px; font-size: 20px; font-weight: 600; letter-spacing: -0.5px; } .logo-icon { width: 32px; height: 32px; background: var(--accent); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; } .header-actions { display: flex; align-items: center; gap: 8px; } .icon-btn { width: 36px; height: 36px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; outline: none; } .icon-btn:hover { background: var(--bg-secondary); color: var(--text-primary); } .icon-btn.danger:hover { background: var(--danger); color: white; } /* Search Bar */ .search-section { max-width: 1200px; margin: 0 auto; padding: 20px 24px; display: flex; gap: 12px; flex-wrap: wrap; } .search-box { flex: 1; min-width: 280px; position: relative; } .search-input { width: 100%; padding: 12px 16px 12px 40px; border: 1px solid var(--border); border-radius: 12px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; transition: all 0.2s; outline: none; } .search-input:focus { outline: none; border-color: var(--accent); } .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); pointer-events: none; } .btn { padding: 12px 20px; border: none; border-radius: 12px; font-size: 15px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: all 0.2s; white-space: nowrap; outline: none; } .btn-primary { background: var(--accent); color: white; } .btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); } .btn-secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border); } .btn-secondary:hover { background: var(--bg-tertiary); } .btn-danger { background: var(--danger); color: white; } .btn-danger:hover { background: #ff2d55; transform: translateY(-1px); } /* Tags Filter */ .tags-section { max-width: 1200px; margin: 0 auto; padding: 0 24px 20px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; position: relative; } .tag-label { font-size: 13px; color: var(--text-tertiary); font-weight: 500; } .tag { padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: 1px solid var(--border); background: var(--bg-primary); color: var(--text-secondary); } .tag:hover { border-color: var(--accent); color: var(--accent); } .tag.active { background: var(--accent); color: white; border-color: var(--accent); } /* Main Content */ .container { max-width: 1200px; margin: 0 auto; padding: 0 24px 40px; } .stats-bar { display: flex; align-items: center; gap: 24px; margin-bottom: 24px; font-size: 13px; color: var(--text-tertiary); } .stat-item { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } .stat-value { color: var(--text-primary); font-weight: 600; } /* Grid Layout */ .accounts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 16px; } /* Card Design */ .account-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px; padding: 20px; transition: all 0.2s; position: relative; cursor: pointer; min-width: 0; outline: none; } .account-card:hover { border-color: var(--accent); box-shadow: 0 4px 20px var(--shadow); transform: translateY(-2px); } .card-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px; min-width: 0; } .platform-info { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1; } .platform-icon { width: 44px; height: 44px; background: var(--bg-secondary); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 600; color: var(--accent); flex-shrink: 0; } .platform-meta { min-width: 0; flex: 1; overflow: hidden; } .platform-name { font-size: 17px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .platform-url { font-size: 13px; color: var(--accent); text-decoration: none; display: flex; align-items: center; gap: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .platform-url:hover { text-decoration: underline; } /* Tags container with flex wrap */ .card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } .card-tag { display: inline-flex; align-items: center; padding: 4px 10px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 12px; font-size: 11px; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; } .card-tag:hover { background: var(--accent); color: white; border-color: var(--accent); } .card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; flex-shrink: 0; } .account-card:hover .card-actions { opacity: 1; } .card-btn { width: 32px; height: 32px; border: none; background: transparent; color: var(--text-tertiary); cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; outline: none; } .card-btn:hover { background: var(--bg-secondary); color: var(--text-primary); } .card-btn.delete:hover { color: var(--danger); } /* Fields */ .fields { display: flex; flex-direction: column; gap: 12px; min-width: 0; } .field { display: flex; flex-direction: column; gap: 4px; min-width: 0; } .field-label { font-size: 12px; color: var(--text-tertiary); font-weight: 500; text-transform: uppercase; letter-spacing: 0.3px; } .field-value { display: flex; align-items: center; gap: 8px; background: var(--bg-secondary); padding: 10px 12px; border-radius: 10px; font-size: 14px; font-family: 'SF Mono', Monaco, monospace; min-width: 0; } .field-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); } .password-text { filter: blur(4px); transition: filter 0.2s; user-select: none; } .password-text.revealed { filter: none; user-select: text; } .field-actions { display: flex; gap: 4px; flex-shrink: 0; } .field-btn { padding: 4px 8px; border: none; background: transparent; color: var(--text-tertiary); cursor: pointer; border-radius: 4px; font-size: 12px; font-weight: 500; transition: all 0.2s; outline: none; } .field-btn:hover { background: var(--bg-primary); color: var(--accent); } .field-btn.copied { color: var(--success); } .note-text { font-size: 13px; color: var(--text-secondary); line-height: 1.5; padding: 8px 12px; background: var(--bg-secondary); border-radius: 8px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; } /* Empty State */ .empty-state { text-align: center; padding: 80px 20px; color: var(--text-tertiary); } .empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: var(--bg-secondary); border-radius: 20px; display: flex; align-items: center; justify-content: center; color: var(--text-tertiary); } .empty-title { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; } .empty-desc { font-size: 14px; max-width: 400px; margin: 0 auto; line-height: 1.6; } /* Modal */ .modal-overlay { position: fixed; inset: 0; background: var(--modal-overlay); backdrop-filter: blur(8px); display: none; justify-content: center; align-items: center; z-index: 1000; padding: 20px; } .modal-overlay.active { display: flex; } .modal { background: var(--bg-primary); border-radius: 20px; width: 100%; max-width: 480px; max-height: 90vh; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); animation: modalIn 0.3s ease; } @keyframes modalIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .modal-header { padding: 24px 24px 0; } .modal-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; } .modal-subtitle { font-size: 14px; color: var(--text-secondary); } .modal-body { padding: 20px 24px; overflow-y: auto; max-height: calc(90vh - 140px); } .form-group { margin-bottom: 16px; } .form-label { display: block; font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; } .form-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; transition: all 0.2s; outline: none; } .form-input:focus { outline: none; border-color: var(--accent); } .form-input::placeholder { color: var(--text-tertiary); } textarea.form-input { min-height: 80px; resize: vertical; font-family: inherit; line-height: 1.5; } .input-hint { font-size: 12px; color: var(--text-tertiary); margin-top: 4px; } /* Password field with generator */ .password-field-wrapper { position: relative; display: flex; gap: 8px; align-items: center; } .password-input-wrapper { position: relative; flex: 1; } .password-actions { display: flex; gap: 4px; align-items: center; } .icon-action-btn { width: 36px; height: 36px; border: none; background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; outline: none; flex-shrink: 0; } .icon-action-btn:hover { background: var(--accent); color: white; } .icon-action-btn svg { width: 18px; height: 18px; } /* Tags input with pipe separator */ .tags-simple-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; transition: all 0.2s; outline: none; } .tags-simple-input:focus { outline: none; border-color: var(--accent); } .modal-footer { padding: 16px 24px 24px; display: flex; justify-content: flex-end; gap: 12px; border-top: 1px solid var(--border); } .btn-text { padding: 10px 20px; border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; font-size: 15px; font-weight: 500; border-radius: 10px; transition: all 0.2s; outline: none; min-width: 80px; text-align: center; } .btn-text:hover { background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--text-tertiary); } .btn-text.danger { color: var(--danger); border-color: var(--danger); background: rgba(255, 59, 48, 0.05); } .btn-text.danger:hover { background: var(--danger); color: white; border-color: var(--danger); } /* View Modal Styles */ .view-modal .modal-body { padding: 24px; } .view-field { margin-bottom: 20px; } .view-field:last-child { margin-bottom: 0; } .view-label { font-size: 12px; color: var(--text-tertiary); font-weight: 500; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 8px; } .view-value-box { background: var(--bg-secondary); padding: 14px 16px; border-radius: 12px; font-family: 'SF Mono', Monaco, monospace; font-size: 15px; color: var(--text-primary); word-break: break-all; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .view-value-text { flex: 1; overflow-wrap: break-word; } .view-actions { display: flex; gap: 8px; flex-shrink: 0; } .view-btn { padding: 6px 12px; border: none; background: var(--bg-primary); color: var(--text-secondary); cursor: pointer; border-radius: 6px; font-size: 13px; font-weight: 500; transition: all 0.2s; outline: none; } .view-btn:hover { background: var(--accent); color: white; } .view-btn.copied { background: var(--success); color: white; } .view-tags { display: flex; flex-wrap: wrap; gap: 8px; } .view-tag { padding: 6px 14px; background: var(--bg-secondary); color: var(--text-secondary); border-radius: 20px; font-size: 13px; font-weight: 500; } /* View note with resize like edit mode */ .view-note-resizable { background: var(--bg-secondary); padding: 14px 16px; border-radius: 12px; font-size: 14px; color: var(--text-secondary); line-height: 1.6; white-space: pre-wrap; width: 100%; min-height: 80px; resize: vertical; border: 1px solid transparent; font-family: inherit; overflow: auto; } .view-note-resizable:focus { outline: none; border-color: var(--accent); } .view-link { display: inline-flex; align-items: center; gap: 6px; color: var(--accent); text-decoration: none; font-size: 14px; margin-top: 8px; } .view-link:hover { text-decoration: underline; } /* Toast */ .toast { position: fixed; bottom: 24px; right: 24px; background: var(--text-primary); color: var(--bg-primary); padding: 14px 20px; border-radius: 12px; font-size: 14px; font-weight: 500; display: none; align-items: center; gap: 10px; box-shadow: 0 10px 30px var(--shadow); z-index: 2000; animation: toastIn 0.3s ease; } .toast.show { display: flex; } @keyframes toastIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } /* Confirm Modal */ .confirm-modal .modal-body { padding: 24px; text-align: center; } .confirm-icon { width: 64px; height: 64px; margin: 0 auto 16px; background: var(--danger); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; } .confirm-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .confirm-desc { font-size: 14px; color: var(--text-secondary); line-height: 1.5; } /* Double confirm input */ .confirm-input-group { margin-top: 20px; text-align: left; } .confirm-input-label { font-size: 13px; color: var(--text-secondary); margin-bottom: 8px; display: block; } .confirm-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; text-align: center; font-weight: 500; } .confirm-input:focus { outline: none; border-color: var(--danger); } .confirm-input.error { border-color: var(--danger); background: rgba(255, 59, 48, 0.1); } /* View Modal Tags */ .view-tags-container { display: flex; flex-wrap: wrap; gap: 8px; } .view-tag-item { padding: 6px 14px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 20px; font-size: 13px; font-weight: 500; color: var(--text-secondary); } /* Password Generator Settings */ .generator-settings { background: var(--bg-secondary); border-radius: 12px; padding: 16px; margin-top: 12px; border: 1px solid var(--border); } .generator-settings-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px; display: flex; align-items: center; gap: 8px; } .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .setting-row:last-child { margin-bottom: 0; } .setting-label { font-size: 14px; color: var(--text-secondary); } .setting-control { display: flex; align-items: center; gap: 8px; } .length-input { width: 60px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--input-bg); color: var(--text-primary); font-size: 14px; text-align: center; } .checkbox-wrapper { display: flex; align-items: center; gap: 6px; cursor: pointer; } .checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; } .checkbox-wrapper span { font-size: 14px; color: var(--text-secondary); } /* Export/Import Options */ .export-options { display: flex; gap: 12px; margin-bottom: 20px; } .export-option { flex: 1; padding: 16px; border: 2px solid var(--border); border-radius: 12px; cursor: pointer; text-align: center; transition: all 0.2s; } .export-option:hover { border-color: var(--accent); } .export-option.selected { border-color: var(--accent); background: rgba(0, 102, 255, 0.05); } .export-option-icon { width: 48px; height: 48px; margin: 0 auto 8px; background: var(--bg-secondary); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--accent); } .export-option-title { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; } .export-option-desc { font-size: 12px; color: var(--text-secondary); } /* Duplicate confirm modal */ .duplicate-info { background: var(--bg-secondary); border-radius: 12px; padding: 16px; margin: 16px 0; text-align: left; } .duplicate-info-item { display: flex; gap: 8px; margin-bottom: 8px; font-size: 14px; } .duplicate-info-item:last-child { margin-bottom: 0; } .duplicate-info-label { color: var(--text-tertiary); min-width: 60px; } .duplicate-info-value { color: var(--text-primary); font-weight: 500; } /* Desktop Styles (default) */ @media (min-width: 769px) { .mobile-only { display: none !important; } } /* Mobile Styles - 90% zoom */ @media (max-width: 768px) { html { zoom: 0.9; -moz-transform: scale(0.9); -moz-transform-origin: 0 0; } @supports not (zoom: 0.9) { body { transform: scale(0.9); transform-origin: 0 0; width: 111.11%; height: 111.11%; } } .desktop-only { display: none !important; } .header-content { padding: 12px 16px; } .search-section { padding: 16px; } .search-section .btn-primary { display: none; } .sort-icon-btn { padding: 4px 10px; margin-left: auto; } .sort-icon-btn svg { width: 12px; height: 12px; } .sort-text { font-size: 11px; } .tags-section { padding: 0 16px 16px; position: relative; padding-right: 60px; } .mobile-add-btn { position: absolute; right: 16px; top: 0; width: 36px; height: 36px; border: none; background: var(--accent); color: white; border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; outline: none; } .mobile-add-btn:hover { background: var(--accent-hover); transform: scale(1.05); } .container { padding: 0 16px 32px; } .accounts-grid { grid-template-columns: 1fr; } .card-actions { opacity: 1; } /* Mobile copy fix */ .field-btn { padding: 8px 12px; font-size: 13px; } /* Ensure card doesn't overflow */ .account-card { min-width: 0; max-width: 100%; } .fields { min-width: 0; } .field-value { min-width: 0; } .note-text { max-width: 100%; } /* Toast position fix for zoomed viewport */ .toast { right: 20px; bottom: 20px; } /* Generator settings on mobile */ .generator-settings { padding: 12px; } .setting-row { flex-direction: column; align-items: flex-start; gap: 8px; } } /* Card Footer with Update Time */ .card-footer { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; } .update-time { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-tertiary); } .update-time svg { opacity: 0.6; } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } </style> <base target="_blank"> </head> <body> <header class="header"> <div class="header-content"> <div class="logo"> <div class="logo-icon"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <rect x="3" y="11" width="18" height="11" rx="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <span>账号管理器</span> </div> <div class="header-actions"> <button class="icon-btn" onclick="toggleTheme()" title="切换主题"> <svg id="themeIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="5"></circle> <line x1="12" y1="1" x2="12" y2="3"></line> <line x1="12" y1="21" x2="12" y2="23"></line> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> <line x1="1" y1="12" x2="3" y2="12"></line> <line x1="21" y1="12" x2="23" y2="12"></line> <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> </svg> </button> <button class="icon-btn" onclick="showExportModal()" title="导出数据"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7 10 12 15 17 10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> </button> <button class="icon-btn" onclick="document.getElementById('importFile').click()" title="导入数据"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="17 8 12 3 7 8"></polyline> <line x1="12" y1="3" x2="12" y2="15"></line> </svg> </button> <button class="icon-btn danger" onclick="confirmClearAll()" title="清空所有数据"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </button> <input type="file" id="importFile" style="display: none" accept=".json" onchange="importData(this)"> </div> </div> </header> <section class="search-section"> <div class="search-box"> <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> <input type="text" class="search-input" id="searchInput" placeholder="搜索平台、账号、标签、链接或备注..." oninput="renderAccounts()"> </div> <button class="btn btn-primary desktop-only" onclick="openModal()"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <line x1="12" y1="5" x2="12" y2="19"></line> <line x1="5" y1="12" x2="19" y2="12"></line> </svg> 添加账号 </button> </section> <section class="tags-section" id="tagsContainer"> <span class="tag-label">筛选:</span> <span class="tag active" onclick="filterByTag('all')">全部</span> </section> <main class="container"> <div class="stats-bar"> <div class="stat-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> <line x1="16" y1="2" x2="16" y2="6"></line> <line x1="8" y1="2" x2="8" y2="6"></line> <line x1="3" y1="10" x2="21" y2="10"></line> </svg> <span>共 <span class="stat-value" id="totalCount">0</span> 个账号</span> </div> <div class="stat-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12 6 12 12 16 14"></polyline> </svg> <span>更新于 <span class="stat-value" id="lastUpdate">-</span></span> </div> <button class="sort-icon-btn" id="sortBtn" onclick="toggleSortOrder()" title="切换排序"> <svg id="sortIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <polyline points="12 6 12 18"></polyline> <polyline points="8 10 12 6 16 10"></polyline> </svg> <span class="sort-text" id="sortText">最新</span> </button> </div> <div id="contentArea"> <div class="empty-state" id="emptyState"> <div class="empty-icon"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <rect x="3" y="11" width="18" height="11" rx="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <div class="empty-title">暂无账号</div> <div class="empty-desc">点击右上角"添加账号"开始使用,或导入已有数据</div> </div> <div class="accounts-grid" id="accountsGrid" style="display: none;"></div> </div> </main> <!-- Add/Edit Modal - No overlay click close --> <div class="modal-overlay" id="modal"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title" id="modalTitle">添加账号</h2> <p class="modal-subtitle">数据仅保存在本地浏览器中</p> </div> <form class="modal-body" id="accountForm" onsubmit="saveAccount(event)"> <input type="hidden" id="editId"> <div class="form-group"> <label class="form-label">平台名称 *</label> <input type="text" class="form-input" id="platform" placeholder="例如:微信、GitHub、支付宝" required> </div> <div class="form-group"> <label class="form-label">用户名 *</label> <input type="text" class="form-input" id="username" placeholder="邮箱/手机号/用户名" required> </div> <div class="form-group"> <label class="form-label">密码 *</label> <div class="password-field-wrapper"> <div class="password-input-wrapper"> <input type="password" class="form-input" id="password" placeholder="密码" required style="padding-right: 40px;"> </div> <div class="password-actions"> <!-- Generate password button --> <button type="button" class="icon-action-btn" onclick="generatePassword()" title="生成随机密码"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 9.9-1"></path> <circle cx="12" cy="16" r="1" fill="currentColor"></circle> <line x1="8" y1="16" x2="8" y2="16"></line> <line x1="16" y1="16" x2="16" y2="16"></line> </svg> </button> <!-- Settings button --> <button type="button" class="icon-action-btn" onclick="toggleGeneratorSettings()" title="密码生成器设置"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.8 17.8l-4.24-4.24M6.34 17.66l-4.24 4.24M23 12h-6m-6 0H1m20.07-4.93l-4.24 4.24M6.34 6.34l-4.24-4.24"></path> </svg> </button> <!-- Toggle visibility --> <button type="button" class="icon-action-btn" onclick="togglePasswordInput()" title="显示/隐藏密码"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <circle cx="12" cy="12" r="3"></circle> </svg> </button> </div> </div> <!-- Generator Settings Panel --> <div class="generator-settings" id="generatorSettings" style="display: none;"> <div class="generator-settings-title"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.8 17.8l-4.24-4.24M6.34 17.66l-4.24 4.24M23 12h-6m-6 0H1m20.07-4.93l-4.24 4.24M6.34 6.34l-4.24-4.24"></path> </svg> 密码生成器设置 </div> <div class="setting-row"> <span class="setting-label">密码长度</span> <div class="setting-control"> <input type="number" class="length-input" id="pwdLength" value="16" min="4" max="64"> </div> </div> <div class="setting-row"> <span class="setting-label">包含大写字母 (A-Z)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdUppercase" checked> <span>启用</span> </label> </div> <div class="setting-row"> <span class="setting-label">包含小写字母 (a-z)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdLowercase" checked> <span>启用</span> </label> </div> <div class="setting-row"> <span class="setting-label">包含数字 (0-9)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdNumbers" checked> <span>启用</span> </label> </div> <div class="setting-row"> <span class="setting-label">包含符号 (!@#$...)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdSymbols"> <span>启用</span> </label> </div> </div> </div> <div class="form-group"> <label class="form-label">网站链接</label> <input type="text" class="form-input" id="url" placeholder="https://..."> <div class="input-hint">可选,方便快速访问网站,自动补全 http://</div> </div> <div class="form-group"> <label class="form-label">标签</label> <input type="text" class="tags-simple-input" id="tagsInput" placeholder="工作|学习|生活"> <div class="input-hint">使用竖杆 | 分隔多个标签</div> </div> <div class="form-group"> <label class="form-label">备注</label> <textarea class="form-input" id="note" placeholder="记录其他信息,如安全问题、绑定手机、备用邮箱等..."></textarea> </div> </form> <div class="modal-footer"> <button type="button" class="btn-text danger" onclick="confirmDeleteCurrent()" id="deleteEditBtn" style="display: none; margin-right: auto;">删除</button> <button type="button" class="btn-text" onclick="closeModal()">取消</button> <button type="button" class="btn btn-primary" onclick="document.getElementById('accountForm').dispatchEvent(new Event('submit'))">保存</button> </div> </div> </div> <!-- View Modal --> <div class="modal-overlay" id="viewModal" onclick="closeViewModalOnOverlay(event)"> <div class="modal view-modal"> <div class="modal-header"> <h2 class="modal-title" id="viewModalTitle">查看账号</h2> <p class="modal-subtitle" id="viewModalSubtitle">查看账号详情</p> </div> <div class="modal-body" id="viewModalBody"> <!-- Dynamic content --> </div> <div class="modal-footer"> <button type="button" class="btn-text danger" onclick="confirmDeleteFromView()" style="margin-right: auto;">删除</button> <button type="button" class="btn-text" onclick="closeViewModal()">关闭</button> <button type="button" class="btn btn-primary" id="viewEditBtn">编辑</button> </div> </div> </div> <!-- Export Modal --> <div class="modal-overlay" id="exportModal" onclick="closeExportModalOnOverlay(event)"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title">导出数据</h2> <p class="modal-subtitle">选择导出方式</p> </div> <div class="modal-body"> <div class="export-options"> <div class="export-option selected" onclick="selectExportType('plain')" id="exportPlain"> <div class="export-option-icon"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> </svg> </div> <div class="export-option-title">普通导出</div> <div class="export-option-desc">明文 JSON 格式</div> </div> <div class="export-option" onclick="selectExportType('encrypted')" id="exportEncrypted"> <div class="export-option-icon"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <div class="export-option-title">加密导出</div> <div class="export-option-desc">AES-256 加密</div> </div> </div> <div id="exportPasswordSection" style="display: none;"> <div class="form-group"> <label class="form-label">设置导出密码 *</label> <input type="password" class="form-input" id="exportPassword" placeholder="输入密码用于加密数据"> <div class="input-hint">请牢记此密码,导入时需要使用</div> </div> <div class="form-group"> <label class="form-label">确认密码 *</label> <input type="password" class="form-input" id="exportPasswordConfirm" placeholder="再次输入密码"> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn-text" onclick="closeExportModal()">取消</button> <button type="button" class="btn btn-primary" onclick="executeExport()">导出</button> </div> </div> </div> <!-- Import Password Modal --> <div class="modal-overlay" id="importPasswordModal"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title">输入导入密码</h2> <p class="modal-subtitle">此数据文件已加密,需要密码才能解密</p> </div> <div class="modal-body"> <div class="form-group"> <label class="form-label">解密密码 *</label> <input type="password" class="form-input" id="importPassword" placeholder="输入导出时设置的密码"> </div> </div> <div class="modal-footer"> <button type="button" class="btn-text" onclick="cancelImport()">取消</button> <button type="button" class="btn btn-primary" onclick="executeImportWithPassword()">导入</button> </div> </div> </div> <!-- Duplicate Confirm Modal --> <div class="modal-overlay" id="duplicateModal"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title">发现重复账号</h2> <p class="modal-subtitle">该账号信息已存在</p> </div> <div class="modal-body"> <div class="confirm-desc">以下账号的所有信息与您要添加的账号完全相同:</div> <div class="duplicate-info" id="duplicateInfo"> <!-- Dynamic content --> </div> <div class="confirm-desc" style="margin-top: 16px; color: var(--warning);"> <strong>提示:</strong>选择"覆盖"将用新信息替换原有账号(保留原ID和创建时间)。 </div> </div> <div class="modal-footer"> <button type="button" class="btn-text" onclick="cancelDuplicate()">取消添加</button> <button type="button" class="btn btn-danger" onclick="forceAddDuplicate()">强制添加(覆盖)</button> </div> </div> </div> <!-- Confirm Clear Modal with Double Confirm --> <div class="modal-overlay" id="confirmClearModal" onclick="closeConfirmClearModalOnOverlay(event)"> <div class="modal confirm-modal"> <div class="modal-body"> <div class="confirm-icon"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </div> <div class="confirm-title">确认清空所有数据?</div> <div class="confirm-desc">此操作不可恢复,所有账号数据将被永久删除。<br>建议先导出数据备份。</div> <div class="confirm-input-group"> <label class="confirm-input-label">请输入 <strong>DELETE</strong> 确认清空:</label> <input type="text" class="confirm-input" id="clearConfirmInput" placeholder="输入 DELETE"> </div> </div> <div class="modal-footer" style="justify-content: center;"> <button type="button" class="btn-text" onclick="closeConfirmClearModal()">取消</button> <button type="button" class="btn btn-danger" onclick="executeClearAll()" id="confirmClearBtn" disabled>确认清空</button> </div> </div> </div> <!-- Simple Confirm Modal for single delete --> <div class="modal-overlay" id="confirmModal" onclick="closeConfirmModalOnOverlay(event)"> <div class="modal confirm-modal"> <div class="modal-body"> <div class="confirm-icon"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </div> <div class="confirm-title" id="simpleConfirmTitle">确认删除?</div> <div class="confirm-desc" id="simpleConfirmDesc">此操作不可恢复。</div> </div> <div class="modal-footer" style="justify-content: center;"> <button type="button" class="btn-text" onclick="closeConfirmModal()">取消</button> <button type="button" class="btn btn-danger" onclick="executeSimpleConfirm()">确认删除</button> </div> </div> </div> <div class="toast" id="toast"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <polyline points="20 6 9 17 4 12"></polyline> </svg> <span id="toastMsg">操作成功</span> </div> <script> // Data let accounts = JSON.parse(localStorage.getItem('pwd_accounts') || '[]'); let currentFilter = 'all'; let editingId = null; let viewingId = null; let simpleConfirmCallback = null; let duplicateCallback = null; let pendingExportType = 'plain'; let pendingImportData = null; let pendingImportFile = null; // Password Generator Settings const defaultPwdSettings = { length: 16, uppercase: true, lowercase: true, numbers: true, symbols: false }; // Check if mobile function isMobile() { return window.innerWidth <= 768; } // Parse tags from pipe-separated string function parseTags(tagsStr) { if (!tagsStr) return []; return tagsStr.split('|').map(t => t.trim()).filter(t => t.length > 0); } // Format tags to pipe-separated string function formatTags(tags) { if (!tags || tags.length === 0) return ''; return tags.join(' | '); } // Auto-fix URL function fixUrl(url) { if (!url) return ''; url = url.trim(); if (!url) return ''; if (!/^https?:\/\//i.test(url)) { url = 'http://' + url; } return url; } // Check if account is exactly the same function isAccountEqual(acc1, acc2) { const normalizeTags = (tags) => { if (!tags) return ''; return [...tags].sort().join('|'); }; return acc1.platform === acc2.platform && acc1.username === acc2.username && acc1.password === acc2.password && (acc1.url || '') === (acc2.url || '') && normalizeTags(acc1.tags) === normalizeTags(acc2.tags) && (acc1.note || '') === (acc2.note || ''); } // Check for duplicate account function findDuplicate(newAcc, excludeId = null) { return accounts.find(acc => { if (excludeId && acc.id === excludeId) return false; return isAccountEqual(acc, newAcc); }); } // Simple XOR encryption for demo (in production use Web Crypto API) async function encryptData(data, password) { const encoder = new TextEncoder(); const dataBytes = encoder.encode(JSON.stringify(data)); const keyBytes = encoder.encode(password); const encrypted = new Uint8Array(dataBytes.length); for (let i = 0; i < dataBytes.length; i++) { encrypted[i] = dataBytes[i] ^ keyBytes[i % keyBytes.length]; } // Convert to base64 const base64 = btoa(String.fromCharCode(...encrypted)); return { encrypted: true, data: base64, salt: Date.now().toString() }; } async function decryptData(encryptedObj, password) { try { const decoder = new TextDecoder(); const encoder = new TextEncoder(); const keyBytes = encoder.encode(password); // Decode base64 const encryptedBytes = Uint8Array.from(atob(encryptedObj.data), c => c.charCodeAt(0)); const decrypted = new Uint8Array(encryptedBytes.length); for (let i = 0; i < encryptedBytes.length; i++) { decrypted[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length]; } const jsonStr = decoder.decode(decrypted); return JSON.parse(jsonStr); } catch (e) { throw new Error('密码错误或数据损坏'); } } // Theme function initTheme() { const saved = localStorage.getItem('pwd_theme') || 'light'; document.documentElement.setAttribute('data-theme', saved); updateThemeIcon(saved); } function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('pwd_theme', next); updateThemeIcon(next); } function updateThemeIcon(theme) { const icon = document.getElementById('themeIcon'); if (theme === 'dark') { icon.innerHTML = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>'; } else { icon.innerHTML = '<circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>'; } } // Tags function getAllTags() { const tags = new Set(); accounts.forEach(a => a.tags?.forEach(t => tags.add(t))); return Array.from(tags).sort(); } function renderTags() { const container = document.getElementById('tagsContainer'); const allTags = getAllTags(); let html = '<span class="tag-label">筛选:</span>'; html += `<span class="tag ${currentFilter === 'all' ? 'active' : ''}" onclick="filterByTag('all')">全部</span>`; allTags.forEach(tag => { html += `<span class="tag ${currentFilter === tag ? 'active' : ''}" onclick="filterByTag('${tag}')">${escapeHtml(tag)}</span>`; }); // Add mobile button html += `<button class="mobile-add-btn mobile-only" onclick="openModal()" title="添加账号"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <line x1="12" y1="5" x2="12" y2="19"></line> <line x1="5" y1="12" x2="19" y2="12"></line> </svg> </button>`; container.innerHTML = html; } function filterByTag(tag) { currentFilter = tag; renderTags(); renderAccounts(); } // Password Generator function toggleGeneratorSettings() { const settings = document.getElementById('generatorSettings'); settings.style.display = settings.style.display === 'none' ? 'block' : 'none'; } function generatePassword() { const length = parseInt(document.getElementById('pwdLength').value) || 16; const useUpper = document.getElementById('pwdUppercase').checked; const useLower = document.getElementById('pwdLowercase').checked; const useNumbers = document.getElementById('pwdNumbers').checked; const useSymbols = document.getElementById('pwdSymbols').checked; let chars = ''; if (useUpper) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (useLower) chars += 'abcdefghijklmnopqrstuvwxyz'; if (useNumbers) chars += '0123456789'; if (useSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (chars === '') { showToast('请至少选择一种字符类型'); return; } let password = ''; const array = new Uint32Array(length); crypto.getRandomValues(array); for (let i = 0; i < length; i++) { password += chars[array[i] % chars.length]; } document.getElementById('password').value = password; document.getElementById('password').type = 'text'; showToast('密码已生成'); } // Modal & Form - No overlay click close function openModal(id = null) { editingId = id; const modal = document.getElementById('modal'); const title = document.getElementById('modalTitle'); const form = document.getElementById('accountForm'); const deleteBtn = document.getElementById('deleteEditBtn'); // Reset generator settings visibility document.getElementById('generatorSettings').style.display = 'none'; if (id) { const acc = accounts.find(a => a.id === id); title.textContent = '编辑账号'; document.getElementById('editId').value = id; document.getElementById('platform').value = acc.platform; document.getElementById('username').value = acc.username; document.getElementById('password').value = acc.password; document.getElementById('url').value = acc.url || ''; document.getElementById('tagsInput').value = formatTags(acc.tags); document.getElementById('note').value = acc.note || ''; deleteBtn.style.display = 'block'; } else { title.textContent = '添加账号'; form.reset(); document.getElementById('editId').value = ''; deleteBtn.style.display = 'none'; // Set default password settings document.getElementById('pwdLength').value = defaultPwdSettings.length; document.getElementById('pwdUppercase').checked = defaultPwdSettings.uppercase; document.getElementById('pwdLowercase').checked = defaultPwdSettings.lowercase; document.getElementById('pwdNumbers').checked = defaultPwdSettings.numbers; document.getElementById('pwdSymbols').checked = defaultPwdSettings.symbols; } modal.classList.add('active'); document.getElementById('platform').focus(); } function closeModal() { document.getElementById('modal').classList.remove('active'); editingId = null; } // Removed: closeModalOnOverlay - now only closes via button function togglePasswordInput() { const input = document.getElementById('password'); const btn = event.target.closest('.icon-action-btn'); if (input.type === 'password') { input.type = 'text'; } else { input.type = 'password'; } } function confirmDeleteCurrent() { if (editingId) { const acc = accounts.find(a => a.id === editingId); showSimpleConfirm( `确认删除 ${acc.platform}?`, '此操作不可恢复,账号将被永久删除。', () => { deleteAccount(editingId); closeModal(); } ); } } // View Modal function openViewModal(id) { viewingId = id; const acc = accounts.find(a => a.id === id); if (!acc) return; document.getElementById('viewModalTitle').textContent = acc.platform; document.getElementById('viewModalSubtitle').textContent = '查看账号详情'; let html = ''; // Username html += ` <div class="view-field"> <div class="view-label">用户名</div> <div class="view-value-box"> <span class="view-value-text">${escapeHtml(acc.username)}</span> <div class="view-actions"> <button class="view-btn" onclick="copyFromView('${escapeHtml(acc.username)}', this)">复制</button> </div> </div> </div> `; // Password html += ` <div class="view-field"> <div class="view-label">密码</div> <div class="view-value-box"> <span class="view-value-text"> <span id="viewPwdText" class="password-text" style="filter: blur(4px); user-select: none;">${escapeHtml(acc.password)}</span> </span> <div class="view-actions"> <button class="view-btn" onclick="toggleViewPassword()" id="viewPwdBtn">显示</button> <button class="view-btn" onclick="copyFromView('${escapeHtml(acc.password)}', this)">复制</button> </div> </div> </div> `; // Tags - container style if (acc.tags && acc.tags.length > 0) { html += ` <div class="view-field"> <div class="view-label">标签</div> <div class="view-tags-container"> ${acc.tags.map(t => `<span class="view-tag-item">${escapeHtml(t)}</span>`).join('')} </div> </div> `; } // URL if (acc.url) { html += ` <div class="view-field"> <div class="view-label">网站链接</div> <a href="${escapeHtml(acc.url)}" target="_blank" class="view-link"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> <polyline points="15 3 21 3 21 9"></polyline> <line x1="10" y1="14" x2="21" y2="3"></line> </svg> ${escapeHtml(acc.url)} </a> </div> `; } // Note - resizable like edit mode if (acc.note) { html += ` <div class="view-field"> <div class="view-label">备注(可拖动调整大小)</div> <textarea class="view-note-resizable" readonly>${escapeHtml(acc.note)}</textarea> </div> `; } document.getElementById('viewModalBody').innerHTML = html; // Set edit button action document.getElementById('viewEditBtn').onclick = () => { closeViewModal(); setTimeout(() => openModal(id), 100); }; document.getElementById('viewModal').classList.add('active'); } function closeViewModal() { document.getElementById('viewModal').classList.remove('active'); viewingId = null; } function closeViewModalOnOverlay(e) { if (e.target === e.currentTarget) closeViewModal(); } function toggleViewPassword() { const pwd = document.getElementById('viewPwdText'); const btn = document.getElementById('viewPwdBtn'); if (pwd.style.filter === 'blur(4px)') { pwd.style.filter = 'none'; pwd.style.userSelect = 'text'; btn.textContent = '隐藏'; } else { pwd.style.filter = 'blur(4px)'; pwd.style.userSelect = 'none'; btn.textContent = '显示'; } } function confirmDeleteFromView() { if (viewingId) { const acc = accounts.find(a => a.id === viewingId); showSimpleConfirm( `确认删除 ${acc.platform}?`, '此操作不可恢复,账号将被永久删除。', () => { deleteAccount(viewingId); closeViewModal(); } ); } } function copyFromView(text, btn) { navigator.clipboard.writeText(text).then(() => { btn.textContent = '已复制'; btn.classList.add('copied'); showToast('已复制到剪贴板'); setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000); }).catch(() => { // Fallback for mobile const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); btn.textContent = '已复制'; btn.classList.add('copied'); showToast('已复制到剪贴板'); setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000); } catch (err) { showToast('复制失败'); } document.body.removeChild(textArea); }); } // Save with duplicate check function saveAccount(e) { e.preventDefault(); const platform = document.getElementById('platform').value.trim(); const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value; const url = fixUrl(document.getElementById('url').value.trim()); const tags = parseTags(document.getElementById('tagsInput').value); const note = document.getElementById('note').value.trim(); if (!platform || !username || !password) return; const newAcc = { platform, username, password, url, tags, note }; // Check for duplicates when adding new account if (!editingId) { const duplicate = findDuplicate(newAcc); if (duplicate) { showDuplicateModal(newAcc, duplicate); return; } } // Save directly if editing or no duplicate doSaveAccount(newAcc); } function doSaveAccount(newAcc, replaceId = null) { const data = { id: replaceId || editingId || Date.now().toString(), ...newAcc, updatedAt: new Date().toISOString() }; if (editingId || replaceId) { const idx = accounts.findIndex(a => a.id === (replaceId || editingId)); if (idx >= 0) { // Preserve original created time if replacing if (replaceId && !editingId) { data.updatedAt = accounts[idx].updatedAt; } accounts[idx] = data; } showToast(replaceId && !editingId ? '账号已覆盖' : '账号已更新'); } else { accounts.push(data); showToast('账号已添加'); } saveData(); closeModal(); renderAccounts(); renderTags(); } function showDuplicateModal(newAcc, duplicate) { const infoHtml = ` <div class="duplicate-info-item"> <span class="duplicate-info-label">平台</span> <span class="duplicate-info-value">${escapeHtml(duplicate.platform)}</span> </div> <div class="duplicate-info-item"> <span class="duplicate-info-label">用户名</span> <span class="duplicate-info-value">${escapeHtml(duplicate.username)}</span> </div> <div class="duplicate-info-item"> <span class="duplicate-info-label">密码</span> <span class="duplicate-info-value">••••••</span> </div> ${duplicate.url ? ` <div class="duplicate-info-item"> <span class="duplicate-info-label">链接</span> <span class="duplicate-info-value">${escapeHtml(duplicate.url)}</span> </div> ` : ''} ${duplicate.tags?.length ? ` <div class="duplicate-info-item"> <span class="duplicate-info-label">标签</span> <span class="duplicate-info-value">${formatTags(duplicate.tags)}</span> </div> ` : ''} ${duplicate.note ? ` <div class="duplicate-info-item"> <span class="duplicate-info-label">备注</span> <span class="duplicate-info-value">${escapeHtml(duplicate.note)}</span> </div> ` : ''} `; document.getElementById('duplicateInfo').innerHTML = infoHtml; document.getElementById('duplicateModal').classList.add('active'); // Store pending data duplicateCallback = { newAcc, replaceId: duplicate.id }; } function cancelDuplicate() { document.getElementById('duplicateModal').classList.remove('active'); duplicateCallback = null; } function forceAddDuplicate() { if (duplicateCallback) { const { newAcc, replaceId } = duplicateCallback; doSaveAccount(newAcc, replaceId); document.getElementById('duplicateModal').classList.remove('active'); duplicateCallback = null; } } function deleteAccount(id) { accounts = accounts.filter(a => a.id !== id); saveData(); renderAccounts(); renderTags(); showToast('账号已删除'); } // Export Functions function showExportModal() { if (!accounts.length) { showToast('暂无数据可导出'); return; } document.getElementById('exportModal').classList.add('active'); selectExportType('plain'); } function closeExportModal() { document.getElementById('exportModal').classList.remove('active'); } function closeExportModalOnOverlay(e) { if (e.target === e.currentTarget) closeExportModal(); } function selectExportType(type) { pendingExportType = type; document.getElementById('exportPlain').classList.toggle('selected', type === 'plain'); document.getElementById('exportEncrypted').classList.toggle('selected', type === 'encrypted'); const pwdSection = document.getElementById('exportPasswordSection'); pwdSection.style.display = type === 'encrypted' ? 'block' : 'none'; if (type === 'encrypted') { setTimeout(() => document.getElementById('exportPassword').focus(), 100); } } async function executeExport() { const type = pendingExportType; if (type === 'encrypted') { const pwd = document.getElementById('exportPassword').value; const pwdConfirm = document.getElementById('exportPasswordConfirm').value; if (!pwd) { showToast('请输入导出密码'); return; } if (pwd !== pwdConfirm) { showToast('两次输入的密码不一致'); return; } if (pwd.length < 6) { showToast('密码长度至少6位'); return; } try { const encrypted = await encryptData(accounts, pwd); downloadFile(JSON.stringify(encrypted, null, 2), `accounts_encrypted_${new Date().toISOString().split('T')[0]}.json`); showToast('加密数据已导出'); closeExportModal(); } catch (e) { showToast('加密失败:' + e.message); } } else { const data = JSON.stringify(accounts, null, 2); downloadFile(data, `accounts_${new Date().toISOString().split('T')[0]}.json`); showToast('数据已导出'); closeExportModal(); } } function downloadFile(content, filename) { const blob = new Blob([content], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // Import Functions async function importData(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { try { const content = e.target.result; let data; try { data = JSON.parse(content); } catch (err) { throw new Error('文件格式错误,不是有效的 JSON'); } // Check if encrypted if (data.encrypted && data.data) { pendingImportData = data; pendingImportFile = file; document.getElementById('importPasswordModal').classList.add('active'); document.getElementById('importPassword').value = ''; setTimeout(() => document.getElementById('importPassword').focus(), 100); return; } // Plain import if (!Array.isArray(data)) { throw new Error('数据格式错误,应为账号数组'); } processImportData(data); } catch (err) { alert('导入失败:' + err.message); } input.value = ''; }; reader.readAsText(file); } async function executeImportWithPassword() { const pwd = document.getElementById('importPassword').value; if (!pwd) { showToast('请输入解密密码'); return; } try { const decrypted = await decryptData(pendingImportData, pwd); if (!Array.isArray(decrypted)) { throw new Error('解密后数据格式错误'); } processImportData(decrypted); document.getElementById('importPasswordModal').classList.remove('active'); pendingImportData = null; pendingImportFile = null; } catch (err) { showToast('解密失败:' + err.message); } } function cancelImport() { document.getElementById('importPasswordModal').classList.remove('active'); pendingImportData = null; pendingImportFile = null; document.getElementById('importFile').value = ''; } function processImportData(data) { // Fix URLs in imported data data.forEach(item => { if (item.url) { item.url = fixUrl(item.url); } }); let added = 0; let skipped = 0; let updated = 0; const skippedItems = []; data.forEach(item => { if (!item.id || !item.platform) return; // Check for exact duplicate const existing = accounts.find(a => a.id === item.id); if (existing) { // Check if exactly the same if (isAccountEqual(existing, item)) { skipped++; skippedItems.push(item.platform); return; } // Different - update if newer if (new Date(item.updatedAt) > new Date(existing.updatedAt)) { accounts[accounts.indexOf(existing)] = item; updated++; } } else { // Check if new item is duplicate of existing (by content) const contentDuplicate = accounts.find(a => isAccountEqual(a, item)); if (contentDuplicate) { skipped++; skippedItems.push(item.platform); return; } accounts.push(item); added++; } }); saveData(); renderAccounts(); renderTags(); let msg = `导入完成:新增 ${added} 条`; if (updated > 0) msg += `,更新 ${updated} 条`; if (skipped > 0) msg += `,跳过 ${skipped} 条重复`; showToast(msg); if (skippedItems.length > 0) { setTimeout(() => { alert(`以下账号因信息完全相同已跳过:\n${skippedItems.join('\n')}`); }, 300); } } // Clear All with Double Confirm function confirmClearAll() { if (accounts.length === 0) { showToast('暂无数据可清空'); return; } document.getElementById('confirmClearModal').classList.add('active'); document.getElementById('clearConfirmInput').value = ''; document.getElementById('clearConfirmInput').classList.remove('error'); document.getElementById('confirmClearBtn').disabled = true; document.getElementById('clearConfirmInput').focus(); } function closeConfirmClearModal() { document.getElementById('confirmClearModal').classList.remove('active'); } function closeConfirmClearModalOnOverlay(e) { if (e.target === e.currentTarget) closeConfirmClearModal(); } // Real-time validation for clear confirm document.getElementById('clearConfirmInput')?.addEventListener('input', function(e) { const input = e.target; const btn = document.getElementById('confirmClearBtn'); if (input.value === 'DELETE') { btn.disabled = false; input.classList.remove('error'); } else { btn.disabled = true; if (input.value.length >= 6) { input.classList.add('error'); } else { input.classList.remove('error'); } } }); function executeClearAll() { const input = document.getElementById('clearConfirmInput'); if (input.value !== 'DELETE') { input.classList.add('error'); return; } accounts = []; saveData(); renderAccounts(); renderTags(); closeConfirmClearModal(); showToast('所有数据已清空'); } // Simple Confirm Modal function showSimpleConfirm(title, desc, callback) { simpleConfirmCallback = callback; document.getElementById('simpleConfirmTitle').textContent = title; document.getElementById('simpleConfirmDesc').textContent = desc; document.getElementById('confirmModal').classList.add('active'); } function closeConfirmModal() { document.getElementById('confirmModal').classList.remove('active'); simpleConfirmCallback = null; } function closeConfirmModalOnOverlay(e) { if (e.target === e.currentTarget) closeConfirmModal(); } function executeSimpleConfirm() { if (simpleConfirmCallback) { simpleConfirmCallback(); } closeConfirmModal(); } // Render function renderAccounts() { const search = document.getElementById('searchInput').value.toLowerCase(); const grid = document.getElementById('accountsGrid'); const empty = document.getElementById('emptyState'); let filtered = accounts.filter(a => { const matchSearch = !search || a.platform.toLowerCase().includes(search) || a.username.toLowerCase().includes(search) || a.tags?.some(t => t.toLowerCase().includes(search)) || (a.url && a.url.toLowerCase().includes(search)) || a.note?.toLowerCase().includes(search); const matchTag = currentFilter === 'all' || a.tags?.includes(currentFilter); return matchSearch && matchTag; }); // Sort accounts by update time filtered.sort((a, b) => { const timeA = new Date(a.updatedAt).getTime(); const timeB = new Date(b.updatedAt).getTime(); if (window.sortOrder === 'oldest') { return timeA - timeB; // Oldest first } return timeB - timeA; // Newest first (default) }); document.getElementById('totalCount').textContent = filtered.length; if (accounts.length > 0) { const last = accounts.reduce((a, b) => new Date(a.updatedAt) > new Date(b.updatedAt) ? a : b ); document.getElementById('lastUpdate').textContent = new Date(last.updatedAt).toLocaleDateString('zh-CN'); } else { document.getElementById('lastUpdate').textContent = '-'; } if (filtered.length === 0) { grid.style.display = 'none'; empty.style.display = 'block'; empty.querySelector('.empty-title').textContent = search || currentFilter !== 'all' ? '无匹配结果' : '暂无账号'; empty.querySelector('.empty-desc').textContent = search || currentFilter !== 'all' ? '尝试其他关键词或筛选条件' : '点击右上角"添加账号"开始使用'; return; } grid.style.display = 'grid'; empty.style.display = 'none'; grid.innerHTML = filtered.map(acc => ` <div class="account-card" onclick="handleCardClick(event, '${acc.id}')"> <div class="card-header"> <div class="platform-info"> <div class="platform-icon">${acc.platform.charAt(0).toUpperCase()}</div> <div class="platform-meta"> <div class="platform-name">${escapeHtml(acc.platform)}</div> ${acc.url ? `<a href="${escapeHtml(acc.url)}" target="_blank" class="platform-url" title="访问网站" onclick="event.stopPropagation()"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> <polyline points="15 3 21 3 21 9"></polyline> <line x1="10" y1="14" x2="21" y2="3"></line> </svg> ${escapeHtml(acc.url.replace(/^https?:\/\//, '').replace(/\/$/, ''))} </a>` : ''} ${acc.tags?.length ? `<div class="card-tags"> ${acc.tags.map(t => `<span class="card-tag">${escapeHtml(t)}</span>`).join('')} </div>` : ''} </div> </div> <div class="card-actions" onclick="event.stopPropagation()"> <button class="card-btn" onclick="openModal('${acc.id}')" title="编辑"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> </button> <button class="card-btn delete" onclick="confirmDeleteAccount('${acc.id}', '${escapeHtml(acc.platform)}'); event.stopPropagation();" title="删除"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </button> </div> </div> <div class="fields"> <div class="field"> <span class="field-label">用户名</span> <div class="field-value"> <span class="field-text">${escapeHtml(acc.username)}</span> <div class="field-actions"> <button class="field-btn" onclick="copyText('${escapeHtml(acc.username)}', this); event.stopPropagation();">复制</button> </div> </div> </div> <div class="field"> <span class="field-label">密码</span> <div class="field-value"> <span class="field-text password-text" id="pwd-${acc.id}">${escapeHtml(acc.password)}</span> <div class="field-actions"> <button class="field-btn" onclick="togglePwd('${acc.id}'); event.stopPropagation();">显示</button> <button class="field-btn" onclick="copyText('${escapeHtml(acc.password)}', this); event.stopPropagation();">复制</button> </div> </div> </div> ${acc.note ? ` <div class="field"> <span class="field-label">备注</span> <div class="note-text">${escapeHtml(acc.note)}</div> </div> ` : ''} </div> <div class="card-footer"> <div class="update-time"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12 6 12 12 16 14"></polyline> </svg> <span>${formatRelativeTime(acc.updatedAt)}</span> </div> </div> </div> `).join(''); } function handleCardClick(event, id) { // Don't open view if clicking on buttons or links if (event.target.closest('.card-actions') || event.target.closest('.field-actions') || event.target.closest('a')) { return; } openViewModal(id); } function togglePwd(id) { const el = document.getElementById(`pwd-${id}`); const btn = event.target; if (el.classList.contains('revealed')) { el.classList.remove('revealed'); btn.textContent = '显示'; } else { el.classList.add('revealed'); btn.textContent = '隐藏'; } } // Utils function copyText(text, btn) { // Try modern clipboard API first if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { showCopied(btn); }).catch(() => { fallbackCopy(text, btn); }); } else { fallbackCopy(text, btn); } } function fallbackCopy(text, btn) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { const successful = document.execCommand('copy'); if (successful) { showCopied(btn); } else { showToast('复制失败'); } } catch (err) { showToast('复制失败'); } document.body.removeChild(textArea); } function showCopied(btn) { btn.textContent = '已复制'; btn.classList.add('copied'); showToast('已复制到剪贴板'); setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000); } function showToast(msg) { const toast = document.getElementById('toast'); document.getElementById('toastMsg').textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 3000); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Format relative time in Chinese function formatRelativeTime(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); const diffMonth = Math.floor(diffDay / 30); const diffYear = Math.floor(diffDay / 365); if (diffSec < 60) { return '刚刚'; } else if (diffMin < 60) { return `${diffMin}分钟前`; } else if (diffHour < 24) { return `${diffHour}小时前`; } else if (diffDay === 1) { return '昨天'; } else if (diffDay < 30) { return `${diffDay}天前`; } else if (diffMonth < 12) { return `${diffMonth}个月前`; } else if (diffYear === 1) { return '1年前'; } else { return `${diffYear}年前`; } } // Confirm delete with modal function confirmDeleteAccount(id, platform) { showSimpleConfirm( `确认删除 ${platform}?`, '此操作不可恢复,账号将被永久删除。', () => { deleteAccount(id); } ); } function saveData() { localStorage.setItem('pwd_accounts', JSON.stringify(accounts)); } // Sort order - default newest first window.sortOrder = 'newest'; // Toggle sort order function toggleSortOrder() { window.sortOrder = window.sortOrder === 'newest' ? 'oldest' : 'newest'; updateSortIcon(); renderAccounts(); showToast(window.sortOrder === 'newest' ? '已切换:最新在前' : '已切换:最早在前'); } // Update sort icon based on current order function updateSortIcon() { const icon = document.getElementById('sortIcon'); const btn = document.getElementById('sortBtn'); const text = document.getElementById('sortText'); if (window.sortOrder === 'newest') { // Newest first icon (arrow up - descending) icon.innerHTML = ` <polyline points="12 6 12 18" style="stroke-width: 2.5"></polyline> <polyline points="8 10 12 6 16 10" style="stroke-width: 2.5"></polyline> `; btn.title = "当前:最新在前,点击切换"; btn.classList.add('active'); if (text) text.textContent = '最新'; } else { // Oldest first icon (arrow down - ascending) icon.innerHTML = ` <polyline points="12 6 12 18" style="stroke-width: 2.5"></polyline> <polyline points="8 14 12 18 16 14" style="stroke-width: 2.5"></polyline> `; btn.title = "当前:最早在前,点击切换"; btn.classList.remove('active'); if (text) text.textContent = '最早'; } } // Init document.addEventListener('DOMContentLoaded', () => { initTheme(); updateSortIcon(); renderAccounts(); renderTags(); }); // Handle resize let lastWidth = window.innerWidth; window.addEventListener('resize', () => { const currentWidth = window.innerWidth; const wasMobile = lastWidth <= 768; const isMobileNow = currentWidth <= 768; if (wasMobile !== isMobileNow) { renderAccounts(); } lastWidth = currentWidth; }); // Keyboard document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(); closeViewModal(); closeConfirmModal(); closeConfirmClearModal(); closeExportModal(); cancelImport(); cancelDuplicate(); } }); </script> </body> </html>这是加了版权信息的完整代码:<!-- 原创代码专属留痕 请勿移除 --> <!-- ============================================ 作者:Yangxiao 作者域名:aszv.top 创作日期:2026-05-08 本代码为本人原创,盗用必究 ============================================ --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta name="author" content="1425202077"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9, user-scalable=no"> <title>账号管理器</title> <style> :root { --bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #fafafa; --text-primary: #111111; --text-secondary: #666666; --text-tertiary: #999999; --border: #e5e5e5; --accent: #0066ff; --accent-hover: #0052cc; --danger: #ff3b30; --success: #34c759; --warning: #ff9500; --shadow: rgba(0, 0, 0, 0.08); --card-bg: #ffffff; --input-bg: #ffffff; --modal-overlay: rgba(0, 0, 0, 0.5); } [data-theme="dark"] { --bg-primary: #000000; --bg-secondary: #1c1c1e; --bg-tertiary: #2c2c2e; --text-primary: #ffffff; --text-secondary: #8e8e93; --text-tertiary: #636366; --border: #38383a; --accent: #0a84ff; --accent-hover: #409cff; --danger: #ff453a; --success: #30d158; --warning: #ff9f0a; --shadow: rgba(0, 0, 0, 0.3); --card-bg: #1c1c1e; --input-bg: #2c2c2e; --modal-overlay: rgba(0, 0, 0, 0.8); } * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; } /* Allow text selection in specific elements */ .field-text, .view-value-text, .note-text, .form-input, textarea, input { -webkit-user-select: text; user-select: text; } body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.5; -webkit-font-smoothing: antialiased; transition: background 0.3s, color 0.3s; } /* Sort Icon Button - Stats Bar Style */ .sort-icon-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-secondary); border-radius: 20px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; margin-left: auto; } .sort-icon-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--bg-tertiary); } .sort-icon-btn.active { background: var(--accent); color: white; border-color: var(--accent); } .sort-icon-btn.active:hover { background: var(--accent-hover); border-color: var(--accent-hover); } .sort-text { font-size: 12px; font-weight: 600; } /* Header */ .header { position: sticky; top: 0; background: var(--bg-primary); border-bottom: 1px solid var(--border); z-index: 100; transition: all 0.3s; } .header-content { max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; gap: 20px; } .logo { display: flex; align-items: center; gap: 12px; font-size: 20px; font-weight: 600; letter-spacing: -0.5px; } .logo-icon { width: 32px; height: 32px; background: var(--accent); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; } .header-actions { display: flex; align-items: center; gap: 8px; } .icon-btn { width: 36px; height: 36px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; outline: none; } .icon-btn:hover { background: var(--bg-secondary); color: var(--text-primary); } .icon-btn.danger:hover { background: var(--danger); color: white; } /* Search Bar */ .search-section { max-width: 1200px; margin: 0 auto; padding: 20px 24px; display: flex; gap: 12px; flex-wrap: wrap; } .search-box { flex: 1; min-width: 280px; position: relative; } .search-input { width: 100%; padding: 12px 16px 12px 40px; border: 1px solid var(--border); border-radius: 12px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; transition: all 0.2s; outline: none; } .search-input:focus { outline: none; border-color: var(--accent); } .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); pointer-events: none; } .btn { padding: 12px 20px; border: none; border-radius: 12px; font-size: 15px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: all 0.2s; white-space: nowrap; outline: none; } .btn-primary { background: var(--accent); color: white; } .btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); } .btn-secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border); } .btn-secondary:hover { background: var(--bg-tertiary); } .btn-danger { background: var(--danger); color: white; } .btn-danger:hover { background: #ff2d55; transform: translateY(-1px); } /* Tags Filter */ .tags-section { max-width: 1200px; margin: 0 auto; padding: 0 24px 20px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; position: relative; } .tag-label { font-size: 13px; color: var(--text-tertiary); font-weight: 500; } .tag { padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: 1px solid var(--border); background: var(--bg-primary); color: var(--text-secondary); } .tag:hover { border-color: var(--accent); color: var(--accent); } .tag.active { background: var(--accent); color: white; border-color: var(--accent); } /* Main Content */ .container { max-width: 1200px; margin: 0 auto; padding: 0 24px 40px; } .stats-bar { display: flex; align-items: center; gap: 24px; margin-bottom: 24px; font-size: 13px; color: var(--text-tertiary); } .stat-item { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } .stat-value { color: var(--text-primary); font-weight: 600; } /* Grid Layout */ .accounts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 16px; } /* Card Design */ .account-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 16px; padding: 20px; transition: all 0.2s; position: relative; cursor: pointer; min-width: 0; outline: none; } .account-card:hover { border-color: var(--accent); box-shadow: 0 4px 20px var(--shadow); transform: translateY(-2px); } .card-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px; min-width: 0; } .platform-info { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1; } .platform-icon { width: 44px; height: 44px; background: var(--bg-secondary); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 600; color: var(--accent); flex-shrink: 0; } .platform-meta { min-width: 0; flex: 1; overflow: hidden; } .platform-name { font-size: 17px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .platform-url { font-size: 13px; color: var(--accent); text-decoration: none; display: flex; align-items: center; gap: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .platform-url:hover { text-decoration: underline; } /* Tags container with flex wrap */ .card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } .card-tag { display: inline-flex; align-items: center; padding: 4px 10px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 12px; font-size: 11px; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; } .card-tag:hover { background: var(--accent); color: white; border-color: var(--accent); } .card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; flex-shrink: 0; } .account-card:hover .card-actions { opacity: 1; } .card-btn { width: 32px; height: 32px; border: none; background: transparent; color: var(--text-tertiary); cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; outline: none; } .card-btn:hover { background: var(--bg-secondary); color: var(--text-primary); } .card-btn.delete:hover { color: var(--danger); } /* Fields */ .fields { display: flex; flex-direction: column; gap: 12px; min-width: 0; } .field { display: flex; flex-direction: column; gap: 4px; min-width: 0; } .field-label { font-size: 12px; color: var(--text-tertiary); font-weight: 500; text-transform: uppercase; letter-spacing: 0.3px; } .field-value { display: flex; align-items: center; gap: 8px; background: var(--bg-secondary); padding: 10px 12px; border-radius: 10px; font-size: 14px; font-family: 'SF Mono', Monaco, monospace; min-width: 0; } .field-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); } .password-text { filter: blur(4px); transition: filter 0.2s; user-select: none; } .password-text.revealed { filter: none; user-select: text; } .field-actions { display: flex; gap: 4px; flex-shrink: 0; } .field-btn { padding: 4px 8px; border: none; background: transparent; color: var(--text-tertiary); cursor: pointer; border-radius: 4px; font-size: 12px; font-weight: 500; transition: all 0.2s; outline: none; } .field-btn:hover { background: var(--bg-primary); color: var(--accent); } .field-btn.copied { color: var(--success); } .note-text { font-size: 13px; color: var(--text-secondary); line-height: 1.5; padding: 8px 12px; background: var(--bg-secondary); border-radius: 8px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; } /* Empty State */ .empty-state { text-align: center; padding: 80px 20px; color: var(--text-tertiary); } .empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: var(--bg-secondary); border-radius: 20px; display: flex; align-items: center; justify-content: center; color: var(--text-tertiary); } .empty-title { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; } .empty-desc { font-size: 14px; max-width: 400px; margin: 0 auto; line-height: 1.6; } /* Modal */ .modal-overlay { position: fixed; inset: 0; background: var(--modal-overlay); backdrop-filter: blur(8px); display: none; justify-content: center; align-items: center; z-index: 1000; padding: 20px; } .modal-overlay.active { display: flex; } .modal { background: var(--bg-primary); border-radius: 20px; width: 100%; max-width: 480px; max-height: 90vh; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); animation: modalIn 0.3s ease; } @keyframes modalIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .modal-header { padding: 24px 24px 0; } .modal-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; } .modal-subtitle { font-size: 14px; color: var(--text-secondary); } .modal-body { padding: 20px 24px; overflow-y: auto; max-height: calc(90vh - 140px); } .form-group { margin-bottom: 16px; } .form-label { display: block; font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; } .form-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; transition: all 0.2s; outline: none; } .form-input:focus { outline: none; border-color: var(--accent); } .form-input::placeholder { color: var(--text-tertiary); } textarea.form-input { min-height: 80px; resize: vertical; font-family: inherit; line-height: 1.5; } .input-hint { font-size: 12px; color: var(--text-tertiary); margin-top: 4px; } /* Password field with generator */ .password-field-wrapper { position: relative; display: flex; gap: 8px; align-items: center; } .password-input-wrapper { position: relative; flex: 1; } .password-actions { display: flex; gap: 4px; align-items: center; } .icon-action-btn { width: 36px; height: 36px; border: none; background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; outline: none; flex-shrink: 0; } .icon-action-btn:hover { background: var(--accent); color: white; } .icon-action-btn svg { width: 18px; height: 18px; } /* Tags input with pipe separator */ .tags-simple-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; transition: all 0.2s; outline: none; } .tags-simple-input:focus { outline: none; border-color: var(--accent); } .modal-footer { padding: 16px 24px 24px; display: flex; justify-content: flex-end; gap: 12px; border-top: 1px solid var(--border); } .btn-text { padding: 10px 20px; border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; font-size: 15px; font-weight: 500; border-radius: 10px; transition: all 0.2s; outline: none; min-width: 80px; text-align: center; } .btn-text:hover { background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--text-tertiary); } .btn-text.danger { color: var(--danger); border-color: var(--danger); background: rgba(255, 59, 48, 0.05); } .btn-text.danger:hover { background: var(--danger); color: white; border-color: var(--danger); } /* View Modal Styles */ .view-modal .modal-body { padding: 24px; } .view-field { margin-bottom: 20px; } .view-field:last-child { margin-bottom: 0; } .view-label { font-size: 12px; color: var(--text-tertiary); font-weight: 500; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 8px; } .view-value-box { background: var(--bg-secondary); padding: 14px 16px; border-radius: 12px; font-family: 'SF Mono', Monaco, monospace; font-size: 15px; color: var(--text-primary); word-break: break-all; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .view-value-text { flex: 1; overflow-wrap: break-word; } .view-actions { display: flex; gap: 8px; flex-shrink: 0; } .view-btn { padding: 6px 12px; border: none; background: var(--bg-primary); color: var(--text-secondary); cursor: pointer; border-radius: 6px; font-size: 13px; font-weight: 500; transition: all 0.2s; outline: none; } .view-btn:hover { background: var(--accent); color: white; } .view-btn.copied { background: var(--success); color: white; } .view-tags { display: flex; flex-wrap: wrap; gap: 8px; } .view-tag { padding: 6px 14px; background: var(--bg-secondary); color: var(--text-secondary); border-radius: 20px; font-size: 13px; font-weight: 500; } /* View note with resize like edit mode */ .view-note-resizable { background: var(--bg-secondary); padding: 14px 16px; border-radius: 12px; font-size: 14px; color: var(--text-secondary); line-height: 1.6; white-space: pre-wrap; width: 100%; min-height: 80px; resize: vertical; border: 1px solid transparent; font-family: inherit; overflow: auto; } .view-note-resizable:focus { outline: none; border-color: var(--accent); } .view-link { display: inline-flex; align-items: center; gap: 6px; color: var(--accent); text-decoration: none; font-size: 14px; margin-top: 8px; } .view-link:hover { text-decoration: underline; } /* Toast */ .toast { position: fixed; bottom: 24px; right: 24px; background: var(--text-primary); color: var(--bg-primary); padding: 14px 20px; border-radius: 12px; font-size: 14px; font-weight: 500; display: none; align-items: center; gap: 10px; box-shadow: 0 10px 30px var(--shadow); z-index: 2000; animation: toastIn 0.3s ease; } .toast.show { display: flex; } @keyframes toastIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } /* Confirm Modal */ .confirm-modal .modal-body { padding: 24px; text-align: center; } .confirm-icon { width: 64px; height: 64px; margin: 0 auto 16px; background: var(--danger); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; } .confirm-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .confirm-desc { font-size: 14px; color: var(--text-secondary); line-height: 1.5; } /* Double confirm input */ .confirm-input-group { margin-top: 20px; text-align: left; } .confirm-input-label { font-size: 13px; color: var(--text-secondary); margin-bottom: 8px; display: block; } .confirm-input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--input-bg); color: var(--text-primary); font-size: 15px; text-align: center; font-weight: 500; } .confirm-input:focus { outline: none; border-color: var(--danger); } .confirm-input.error { border-color: var(--danger); background: rgba(255, 59, 48, 0.1); } /* View Modal Tags */ .view-tags-container { display: flex; flex-wrap: wrap; gap: 8px; } .view-tag-item { padding: 6px 14px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 20px; font-size: 13px; font-weight: 500; color: var(--text-secondary); } /* Password Generator Settings */ .generator-settings { background: var(--bg-secondary); border-radius: 12px; padding: 16px; margin-top: 12px; border: 1px solid var(--border); } .generator-settings-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px; display: flex; align-items: center; gap: 8px; } .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .setting-row:last-child { margin-bottom: 0; } .setting-label { font-size: 14px; color: var(--text-secondary); } .setting-control { display: flex; align-items: center; gap: 8px; } .length-input { width: 60px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--input-bg); color: var(--text-primary); font-size: 14px; text-align: center; } .checkbox-wrapper { display: flex; align-items: center; gap: 6px; cursor: pointer; } .checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; } .checkbox-wrapper span { font-size: 14px; color: var(--text-secondary); } /* Export/Import Options */ .export-options { display: flex; gap: 12px; margin-bottom: 20px; } .export-option { flex: 1; padding: 16px; border: 2px solid var(--border); border-radius: 12px; cursor: pointer; text-align: center; transition: all 0.2s; } .export-option:hover { border-color: var(--accent); } .export-option.selected { border-color: var(--accent); background: rgba(0, 102, 255, 0.05); } .export-option-icon { width: 48px; height: 48px; margin: 0 auto 8px; background: var(--bg-secondary); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--accent); } .export-option-title { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; } .export-option-desc { font-size: 12px; color: var(--text-secondary); } /* Duplicate confirm modal */ .duplicate-info { background: var(--bg-secondary); border-radius: 12px; padding: 16px; margin: 16px 0; text-align: left; } .duplicate-info-item { display: flex; gap: 8px; margin-bottom: 8px; font-size: 14px; } .duplicate-info-item:last-child { margin-bottom: 0; } .duplicate-info-label { color: var(--text-tertiary); min-width: 60px; } .duplicate-info-value { color: var(--text-primary); font-weight: 500; } /* Desktop Styles (default) */ @media (min-width: 769px) { .mobile-only { display: none !important; } } /* Mobile Styles - 90% zoom */ @media (max-width: 768px) { html { zoom: 0.9; -moz-transform: scale(0.9); -moz-transform-origin: 0 0; } @supports not (zoom: 0.9) { body { transform: scale(0.9); transform-origin: 0 0; width: 111.11%; height: 111.11%; } } .desktop-only { display: none !important; } .header-content { padding: 12px 16px; } .search-section { padding: 16px; } .search-section .btn-primary { display: none; } .sort-icon-btn { padding: 4px 10px; margin-left: auto; } .sort-icon-btn svg { width: 12px; height: 12px; } .sort-text { font-size: 11px; } .tags-section { padding: 0 16px 16px; position: relative; padding-right: 60px; } .mobile-add-btn { position: absolute; right: 16px; top: 0; width: 36px; height: 36px; border: none; background: var(--accent); color: white; border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; outline: none; } .mobile-add-btn:hover { background: var(--accent-hover); transform: scale(1.05); } .container { padding: 0 16px 32px; } .accounts-grid { grid-template-columns: 1fr; } .card-actions { opacity: 1; } /* Mobile copy fix */ .field-btn { padding: 8px 12px; font-size: 13px; } /* Ensure card doesn't overflow */ .account-card { min-width: 0; max-width: 100%; } .fields { min-width: 0; } .field-value { min-width: 0; } .note-text { max-width: 100%; } /* Toast position fix for zoomed viewport */ .toast { right: 20px; bottom: 20px; } /* Generator settings on mobile */ .generator-settings { padding: 12px; } .setting-row { flex-direction: column; align-items: flex-start; gap: 8px; } } /* Card Footer with Update Time */ .card-footer { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; } .update-time { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-tertiary); } .update-time svg { opacity: 0.6; } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } </style> <base target="_blank"> </head> <body> <header class="header"> <div class="header-content"> <div class="logo"> <div class="logo-icon"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <rect x="3" y="11" width="18" height="11" rx="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <span>账号管理器</span> </div> <div class="header-actions"> <button class="icon-btn" onclick="toggleTheme()" title="切换主题"> <svg id="themeIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="5"></circle> <line x1="12" y1="1" x2="12" y2="3"></line> <line x1="12" y1="21" x2="12" y2="23"></line> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> <line x1="1" y1="12" x2="3" y2="12"></line> <line x1="21" y1="12" x2="23" y2="12"></line> <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> </svg> </button> <button class="icon-btn" onclick="showExportModal()" title="导出数据"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7 10 12 15 17 10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> </button> <button class="icon-btn" onclick="document.getElementById('importFile').click()" title="导入数据"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="17 8 12 3 7 8"></polyline> <line x1="12" y1="3" x2="12" y2="15"></line> </svg> </button> <button class="icon-btn danger" onclick="confirmClearAll()" title="清空所有数据"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </button> <input type="file" id="importFile" style="display: none" accept=".json" onchange="importData(this)"> </div> </div> </header> <section class="search-section"> <div class="search-box"> <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> <input type="text" class="search-input" id="searchInput" placeholder="搜索平台、账号、标签、链接或备注..." oninput="renderAccounts()"> </div> <button class="btn btn-primary desktop-only" onclick="openModal()"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <line x1="12" y1="5" x2="12" y2="19"></line> <line x1="5" y1="12" x2="19" y2="12"></line> </svg> 添加账号 </button> </section> <section class="tags-section" id="tagsContainer"> <span class="tag-label">筛选:</span> <span class="tag active" onclick="filterByTag('all')">全部</span> </section> <main class="container"> <div class="stats-bar"> <div class="stat-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> <line x1="16" y1="2" x2="16" y2="6"></line> <line x1="8" y1="2" x2="8" y2="6"></line> <line x1="3" y1="10" x2="21" y2="10"></line> </svg> <span>共 <span class="stat-value" id="totalCount">0</span> 个账号</span> </div> <div class="stat-item"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12 6 12 12 16 14"></polyline> </svg> <span>更新于 <span class="stat-value" id="lastUpdate">-</span></span> </div> <button class="sort-icon-btn" id="sortBtn" onclick="toggleSortOrder()" title="切换排序"> <svg id="sortIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <polyline points="12 6 12 18"></polyline> <polyline points="8 10 12 6 16 10"></polyline> </svg> <span class="sort-text" id="sortText">最新</span> </button> </div> <div id="contentArea"> <div class="empty-state" id="emptyState"> <div class="empty-icon"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <rect x="3" y="11" width="18" height="11" rx="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <div class="empty-title">暂无账号</div> <div class="empty-desc">点击右上角"添加账号"开始使用,或导入已有数据</div> </div> <div class="accounts-grid" id="accountsGrid" style="display: none;"></div> </div> </main> <!-- Add/Edit Modal - No overlay click close --> <div class="modal-overlay" id="modal"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title" id="modalTitle">添加账号</h2> <p class="modal-subtitle">数据仅保存在本地浏览器中</p> </div> <form class="modal-body" id="accountForm" onsubmit="saveAccount(event)"> <input type="hidden" id="editId"> <div class="form-group"> <label class="form-label">平台名称 *</label> <input type="text" class="form-input" id="platform" placeholder="例如:微信、GitHub、支付宝" required> </div> <div class="form-group"> <label class="form-label">用户名 *</label> <input type="text" class="form-input" id="username" placeholder="邮箱/手机号/用户名" required> </div> <div class="form-group"> <label class="form-label">密码 *</label> <div class="password-field-wrapper"> <div class="password-input-wrapper"> <input type="password" class="form-input" id="password" placeholder="密码" required style="padding-right: 40px;"> </div> <div class="password-actions"> <!-- Generate password button --> <button type="button" class="icon-action-btn" onclick="generatePassword()" title="生成随机密码"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 9.9-1"></path> <circle cx="12" cy="16" r="1" fill="currentColor"></circle> <line x1="8" y1="16" x2="8" y2="16"></line> <line x1="16" y1="16" x2="16" y2="16"></line> </svg> </button> <!-- Settings button --> <button type="button" class="icon-action-btn" onclick="toggleGeneratorSettings()" title="密码生成器设置"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.8 17.8l-4.24-4.24M6.34 17.66l-4.24 4.24M23 12h-6m-6 0H1m20.07-4.93l-4.24 4.24M6.34 6.34l-4.24-4.24"></path> </svg> </button> <!-- Toggle visibility --> <button type="button" class="icon-action-btn" onclick="togglePasswordInput()" title="显示/隐藏密码"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <circle cx="12" cy="12" r="3"></circle> </svg> </button> </div> </div> <!-- Generator Settings Panel --> <div class="generator-settings" id="generatorSettings" style="display: none;"> <div class="generator-settings-title"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M12 1v6m0 6v6m4.22-10.22l4.24-4.24M6.34 6.34L2.1 2.1m17.8 17.8l-4.24-4.24M6.34 17.66l-4.24 4.24M23 12h-6m-6 0H1m20.07-4.93l-4.24 4.24M6.34 6.34l-4.24-4.24"></path> </svg> 密码生成器设置 </div> <div class="setting-row"> <span class="setting-label">密码长度</span> <div class="setting-control"> <input type="number" class="length-input" id="pwdLength" value="16" min="4" max="64"> </div> </div> <div class="setting-row"> <span class="setting-label">包含大写字母 (A-Z)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdUppercase" checked> <span>启用</span> </label> </div> <div class="setting-row"> <span class="setting-label">包含小写字母 (a-z)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdLowercase" checked> <span>启用</span> </label> </div> <div class="setting-row"> <span class="setting-label">包含数字 (0-9)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdNumbers" checked> <span>启用</span> </label> </div> <div class="setting-row"> <span class="setting-label">包含符号 (!@#$...)</span> <label class="checkbox-wrapper"> <input type="checkbox" id="pwdSymbols"> <span>启用</span> </label> </div> </div> </div> <div class="form-group"> <label class="form-label">网站链接</label> <input type="text" class="form-input" id="url" placeholder="https://..."> <div class="input-hint">可选,方便快速访问网站,自动补全 http://</div> </div> <div class="form-group"> <label class="form-label">标签</label> <input type="text" class="tags-simple-input" id="tagsInput" placeholder="工作|学习|生活"> <div class="input-hint">使用竖杆 | 分隔多个标签</div> </div> <div class="form-group"> <label class="form-label">备注</label> <textarea class="form-input" id="note" placeholder="记录其他信息,如安全问题、绑定手机、备用邮箱等..."></textarea> </div> </form> <div class="modal-footer"> <button type="button" class="btn-text danger" onclick="confirmDeleteCurrent()" id="deleteEditBtn" style="display: none; margin-right: auto;">删除</button> <button type="button" class="btn-text" onclick="closeModal()">取消</button> <button type="button" class="btn btn-primary" onclick="document.getElementById('accountForm').dispatchEvent(new Event('submit'))">保存</button> </div> </div> </div> <!-- View Modal --> <div class="modal-overlay" id="viewModal" onclick="closeViewModalOnOverlay(event)"> <div class="modal view-modal"> <div class="modal-header"> <h2 class="modal-title" id="viewModalTitle">查看账号</h2> <p class="modal-subtitle" id="viewModalSubtitle">查看账号详情</p> </div> <div class="modal-body" id="viewModalBody"> <!-- Dynamic content --> </div> <div class="modal-footer"> <button type="button" class="btn-text danger" onclick="confirmDeleteFromView()" style="margin-right: auto;">删除</button> <button type="button" class="btn-text" onclick="closeViewModal()">关闭</button> <button type="button" class="btn btn-primary" id="viewEditBtn">编辑</button> </div> </div> </div> <!-- Export Modal --> <div class="modal-overlay" id="exportModal" onclick="closeExportModalOnOverlay(event)"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title">导出数据</h2> <p class="modal-subtitle">选择导出方式</p> </div> <div class="modal-body"> <div class="export-options"> <div class="export-option selected" onclick="selectExportType('plain')" id="exportPlain"> <div class="export-option-icon"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> </svg> </div> <div class="export-option-title">普通导出</div> <div class="export-option-desc">明文 JSON 格式</div> </div> <div class="export-option" onclick="selectExportType('encrypted')" id="exportEncrypted"> <div class="export-option-icon"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <div class="export-option-title">加密导出</div> <div class="export-option-desc">AES-256 加密</div> </div> </div> <div id="exportPasswordSection" style="display: none;"> <div class="form-group"> <label class="form-label">设置导出密码 *</label> <input type="password" class="form-input" id="exportPassword" placeholder="输入密码用于加密数据"> <div class="input-hint">请牢记此密码,导入时需要使用</div> </div> <div class="form-group"> <label class="form-label">确认密码 *</label> <input type="password" class="form-input" id="exportPasswordConfirm" placeholder="再次输入密码"> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn-text" onclick="closeExportModal()">取消</button> <button type="button" class="btn btn-primary" onclick="executeExport()">导出</button> </div> </div> </div> <!-- Import Password Modal --> <div class="modal-overlay" id="importPasswordModal"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title">输入导入密码</h2> <p class="modal-subtitle">此数据文件已加密,需要密码才能解密</p> </div> <div class="modal-body"> <div class="form-group"> <label class="form-label">解密密码 *</label> <input type="password" class="form-input" id="importPassword" placeholder="输入导出时设置的密码"> </div> </div> <div class="modal-footer"> <button type="button" class="btn-text" onclick="cancelImport()">取消</button> <button type="button" class="btn btn-primary" onclick="executeImportWithPassword()">导入</button> </div> </div> </div> <!-- Duplicate Confirm Modal --> <div class="modal-overlay" id="duplicateModal"> <div class="modal"> <div class="modal-header"> <h2 class="modal-title">发现重复账号</h2> <p class="modal-subtitle">该账号信息已存在</p> </div> <div class="modal-body"> <div class="confirm-desc">以下账号的所有信息与您要添加的账号完全相同:</div> <div class="duplicate-info" id="duplicateInfo"> <!-- Dynamic content --> </div> <div class="confirm-desc" style="margin-top: 16px; color: var(--warning);"> <strong>提示:</strong>选择"覆盖"将用新信息替换原有账号(保留原ID和创建时间)。 </div> </div> <div class="modal-footer"> <button type="button" class="btn-text" onclick="cancelDuplicate()">取消添加</button> <button type="button" class="btn btn-danger" onclick="forceAddDuplicate()">强制添加(覆盖)</button> </div> </div> </div> <!-- K520A6 --> <!-- Confirm Clear Modal with Double Confirm --> <div class="modal-overlay" id="confirmClearModal" onclick="closeConfirmClearModalOnOverlay(event)"> <div class="modal confirm-modal"> <div class="modal-body"> <div class="confirm-icon"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </div> <div class="confirm-title">确认清空所有数据?</div> <div class="confirm-desc">此操作不可恢复,所有账号数据将被永久删除。<br>建议先导出数据备份。</div> <div class="confirm-input-group"> <label class="confirm-input-label">请输入 <strong>DELETE</strong> 确认清空:</label> <input type="text" class="confirm-input" id="clearConfirmInput" placeholder="输入 DELETE"> </div> </div> <div class="modal-footer" style="justify-content: center;"> <button type="button" class="btn-text" onclick="closeConfirmClearModal()">取消</button> <button type="button" class="btn btn-danger" onclick="executeClearAll()" id="confirmClearBtn" disabled>确认清空</button> </div> </div> </div> <!-- aszv_top --> <!-- Simple Confirm Modal for single delete --> <div class="modal-overlay" id="confirmModal" onclick="closeConfirmModalOnOverlay(event)"> <div class="modal confirm-modal"> <div class="modal-body"> <div class="confirm-icon"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </div> <div class="confirm-title" id="simpleConfirmTitle">确认删除?</div> <div class="confirm-desc" id="simpleConfirmDesc">此操作不可恢复。</div> </div> <div class="modal-footer" style="justify-content: center;"> <button type="button" class="btn-text" onclick="closeConfirmModal()">取消</button> <button type="button" class="btn btn-danger" onclick="executeSimpleConfirm()">确认删除</button> </div> </div> </div> <div class="toast" id="toast"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <polyline points="20 6 9 17 4 12"></polyline> </svg> <span id="toastMsg">操作成功</span> </div> <script> // Data let accounts = JSON.parse(localStorage.getItem('pwd_accounts') || '[]'); let currentFilter = 'all'; let editingId = null; let viewingId = null; let simpleConfirmCallback = null; let duplicateCallback = null; let pendingExportType = 'plain'; let pendingImportData = null; let pendingImportFile = null; // Password Generator Settings const defaultPwdSettings = { length: 16, uppercase: true, lowercase: true, numbers: true, symbols: false }; // Check if mobile function isMobile() { return window.innerWidth <= 768; } // Parse tags from pipe-separated string function parseTags(tagsStr) { if (!tagsStr) return []; return tagsStr.split('|').map(t => t.trim()).filter(t => t.length > 0); } // Format tags to pipe-separated string function formatTags(tags) { if (!tags || tags.length === 0) return ''; return tags.join(' | '); } // Auto-fix URL function fixUrl(url) { if (!url) return ''; url = url.trim(); if (!url) return ''; if (!/^https?:\/\//i.test(url)) { url = 'http://' + url; } return url; } // Check if account is exactly the same function isAccountEqual(acc1, acc2) { const normalizeTags = (tags) => { if (!tags) return ''; return [...tags].sort().join('|'); }; return acc1.platform === acc2.platform && acc1.username === acc2.username && acc1.password === acc2.password && (acc1.url || '') === (acc2.url || '') && normalizeTags(acc1.tags) === normalizeTags(acc2.tags) && (acc1.note || '') === (acc2.note || ''); } // Check for duplicate account function findDuplicate(newAcc, excludeId = null) { return accounts.find(acc => { if (excludeId && acc.id === excludeId) return false; return isAccountEqual(acc, newAcc); }); } // Simple XOR encryption for demo (in production use Web Crypto API) async function encryptData(data, password) { const encoder = new TextEncoder(); const dataBytes = encoder.encode(JSON.stringify(data)); const keyBytes = encoder.encode(password); const encrypted = new Uint8Array(dataBytes.length); for (let i = 0; i < dataBytes.length; i++) { encrypted[i] = dataBytes[i] ^ keyBytes[i % keyBytes.length]; } // Convert to base64 const base64 = btoa(String.fromCharCode(...encrypted)); return { encrypted: true, data: base64, salt: Date.now().toString() }; } async function decryptData(encryptedObj, password) { try { const decoder = new TextDecoder(); const encoder = new TextEncoder(); const keyBytes = encoder.encode(password); // Decode base64 const encryptedBytes = Uint8Array.from(atob(encryptedObj.data), c => c.charCodeAt(0)); const decrypted = new Uint8Array(encryptedBytes.length); for (let i = 0; i < encryptedBytes.length; i++) { decrypted[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length]; } const jsonStr = decoder.decode(decrypted); return JSON.parse(jsonStr); } catch (e) { throw new Error('密码错误或数据损坏'); } } // Theme function initTheme() { const saved = localStorage.getItem('pwd_theme') || 'light'; document.documentElement.setAttribute('data-theme', saved); updateThemeIcon(saved); } function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('pwd_theme', next); updateThemeIcon(next); } function updateThemeIcon(theme) { const icon = document.getElementById('themeIcon'); if (theme === 'dark') { icon.innerHTML = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>'; } else { icon.innerHTML = '<circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>'; } } // Tags function getAllTags() { const tags = new Set(); accounts.forEach(a => a.tags?.forEach(t => tags.add(t))); return Array.from(tags).sort(); } function renderTags() { const container = document.getElementById('tagsContainer'); const allTags = getAllTags(); let html = '<span class="tag-label">筛选:</span>'; html += `<span class="tag ${currentFilter === 'all' ? 'active' : ''}" onclick="filterByTag('all')">全部</span>`; allTags.forEach(tag => { html += `<span class="tag ${currentFilter === tag ? 'active' : ''}" onclick="filterByTag('${tag}')">${escapeHtml(tag)}</span>`; }); // Add mobile button html += `<button class="mobile-add-btn mobile-only" onclick="openModal()" title="添加账号"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <line x1="12" y1="5" x2="12" y2="19"></line> <line x1="5" y1="12" x2="19" y2="12"></line> </svg> </button>`; container.innerHTML = html; } function filterByTag(tag) { currentFilter = tag; renderTags(); renderAccounts(); } // Password Generator function toggleGeneratorSettings() { const settings = document.getElementById('generatorSettings'); settings.style.display = settings.style.display === 'none' ? 'block' : 'none'; } function generatePassword() { const length = parseInt(document.getElementById('pwdLength').value) || 16; const useUpper = document.getElementById('pwdUppercase').checked; const useLower = document.getElementById('pwdLowercase').checked; const useNumbers = document.getElementById('pwdNumbers').checked; const useSymbols = document.getElementById('pwdSymbols').checked; let chars = ''; if (useUpper) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (useLower) chars += 'abcdefghijklmnopqrstuvwxyz'; if (useNumbers) chars += '0123456789'; if (useSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (chars === '') { showToast('请至少选择一种字符类型'); return; } let password = ''; const array = new Uint32Array(length); crypto.getRandomValues(array); for (let i = 0; i < length; i++) { password += chars[array[i] % chars.length]; } document.getElementById('password').value = password; document.getElementById('password').type = 'text'; showToast('密码已生成'); } // Modal & Form - No overlay click close function openModal(id = null) { editingId = id; const modal = document.getElementById('modal'); const title = document.getElementById('modalTitle'); const form = document.getElementById('accountForm'); const deleteBtn = document.getElementById('deleteEditBtn'); // Reset generator settings visibility document.getElementById('generatorSettings').style.display = 'none'; if (id) { const acc = accounts.find(a => a.id === id); title.textContent = '编辑账号'; document.getElementById('editId').value = id; document.getElementById('platform').value = acc.platform; document.getElementById('username').value = acc.username; document.getElementById('password').value = acc.password; document.getElementById('url').value = acc.url || ''; document.getElementById('tagsInput').value = formatTags(acc.tags); document.getElementById('note').value = acc.note || ''; deleteBtn.style.display = 'block'; } else { title.textContent = '添加账号'; form.reset(); document.getElementById('editId').value = ''; deleteBtn.style.display = 'none'; // Set default password settings document.getElementById('pwdLength').value = defaultPwdSettings.length; document.getElementById('pwdUppercase').checked = defaultPwdSettings.uppercase; document.getElementById('pwdLowercase').checked = defaultPwdSettings.lowercase; document.getElementById('pwdNumbers').checked = defaultPwdSettings.numbers; document.getElementById('pwdSymbols').checked = defaultPwdSettings.symbols; } modal.classList.add('active'); document.getElementById('platform').focus(); } function closeModal() { document.getElementById('modal').classList.remove('active'); editingId = null; } // Removed: closeModalOnOverlay - now only closes via button function togglePasswordInput() { const input = document.getElementById('password'); const btn = event.target.closest('.icon-action-btn'); if (input.type === 'password') { input.type = 'text'; } else { input.type = 'password'; } } function confirmDeleteCurrent() { if (editingId) { const acc = accounts.find(a => a.id === editingId); showSimpleConfirm( `确认删除 ${acc.platform}?`, '此操作不可恢复,账号将被永久删除。', () => { deleteAccount(editingId); closeModal(); } ); } } // View Modal function openViewModal(id) { viewingId = id; const acc = accounts.find(a => a.id === id); if (!acc) return; document.getElementById('viewModalTitle').textContent = acc.platform; document.getElementById('viewModalSubtitle').textContent = '查看账号详情'; let html = ''; // Username html += ` <div class="view-field"> <div class="view-label">用户名</div> <div class="view-value-box"> <span class="view-value-text">${escapeHtml(acc.username)}</span> <div class="view-actions"> <button class="view-btn" onclick="copyFromView('${escapeHtml(acc.username)}', this)">复制</button> </div> </div> </div> `; // Password html += ` <div class="view-field"> <div class="view-label">密码</div> <div class="view-value-box"> <span class="view-value-text"> <span id="viewPwdText" class="password-text" style="filter: blur(4px); user-select: none;">${escapeHtml(acc.password)}</span> </span> <div class="view-actions"> <button class="view-btn" onclick="toggleViewPassword()" id="viewPwdBtn">显示</button> <button class="view-btn" onclick="copyFromView('${escapeHtml(acc.password)}', this)">复制</button> </div> </div> </div> `; // Tags - container style if (acc.tags && acc.tags.length > 0) { html += ` <div class="view-field"> <div class="view-label">标签</div> <div class="view-tags-container"> ${acc.tags.map(t => `<span class="view-tag-item">${escapeHtml(t)}</span>`).join('')} </div> </div> `; } // URL if (acc.url) { html += ` <div class="view-field"> <div class="view-label">网站链接</div> <a href="${escapeHtml(acc.url)}" target="_blank" class="view-link"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> <polyline points="15 3 21 3 21 9"></polyline> <line x1="10" y1="14" x2="21" y2="3"></line> </svg> ${escapeHtml(acc.url)} </a> </div> `; } // Note - resizable like edit mode if (acc.note) { html += ` <div class="view-field"> <div class="view-label">备注(可拖动调整大小)</div> <textarea class="view-note-resizable" readonly>${escapeHtml(acc.note)}</textarea> </div> `; } document.getElementById('viewModalBody').innerHTML = html; // Set edit button action document.getElementById('viewEditBtn').onclick = () => { closeViewModal(); setTimeout(() => openModal(id), 100); }; document.getElementById('viewModal').classList.add('active'); } function closeViewModal() { document.getElementById('viewModal').classList.remove('active'); viewingId = null; } function closeViewModalOnOverlay(e) { if (e.target === e.currentTarget) closeViewModal(); } function toggleViewPassword() { const pwd = document.getElementById('viewPwdText'); const btn = document.getElementById('viewPwdBtn'); if (pwd.style.filter === 'blur(4px)') { pwd.style.filter = 'none'; pwd.style.userSelect = 'text'; btn.textContent = '隐藏'; } else { pwd.style.filter = 'blur(4px)'; pwd.style.userSelect = 'none'; btn.textContent = '显示'; } } function confirmDeleteFromView() { if (viewingId) { const acc = accounts.find(a => a.id === viewingId); showSimpleConfirm( `确认删除 ${acc.platform}?`, '此操作不可恢复,账号将被永久删除。', () => { deleteAccount(viewingId); closeViewModal(); } ); } } function copyFromView(text, btn) { navigator.clipboard.writeText(text).then(() => { btn.textContent = '已复制'; btn.classList.add('copied'); showToast('已复制到剪贴板'); setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000); }).catch(() => { // Fallback for mobile const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); btn.textContent = '已复制'; btn.classList.add('copied'); showToast('已复制到剪贴板'); setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000); } catch (err) { showToast('复制失败'); } document.body.removeChild(textArea); }); } // Save with duplicate check function saveAccount(e) { e.preventDefault(); const platform = document.getElementById('platform').value.trim(); const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value; const url = fixUrl(document.getElementById('url').value.trim()); const tags = parseTags(document.getElementById('tagsInput').value); const note = document.getElementById('note').value.trim(); if (!platform || !username || !password) return; const newAcc = { platform, username, password, url, tags, note }; // Check for duplicates when adding new account if (!editingId) { const duplicate = findDuplicate(newAcc); if (duplicate) { showDuplicateModal(newAcc, duplicate); return; } } // Save directly if editing or no duplicate doSaveAccount(newAcc); } function doSaveAccount(newAcc, replaceId = null) { const data = { id: replaceId || editingId || Date.now().toString(), ...newAcc, updatedAt: new Date().toISOString() }; if (editingId || replaceId) { const idx = accounts.findIndex(a => a.id === (replaceId || editingId)); if (idx >= 0) { // Preserve original created time if replacing if (replaceId && !editingId) { data.updatedAt = accounts[idx].updatedAt; } accounts[idx] = data; } showToast(replaceId && !editingId ? '账号已覆盖' : '账号已更新'); } else { accounts.push(data); showToast('账号已添加'); } saveData(); closeModal(); renderAccounts(); renderTags(); } function showDuplicateModal(newAcc, duplicate) { const infoHtml = ` <div class="duplicate-info-item"> <span class="duplicate-info-label">平台</span> <span class="duplicate-info-value">${escapeHtml(duplicate.platform)}</span> </div> <div class="duplicate-info-item"> <span class="duplicate-info-label">用户名</span> <span class="duplicate-info-value">${escapeHtml(duplicate.username)}</span> </div> <div class="duplicate-info-item"> <span class="duplicate-info-label">密码</span> <span class="duplicate-info-value">••••••</span> </div> ${duplicate.url ? ` <div class="duplicate-info-item"> <span class="duplicate-info-label">链接</span> <span class="duplicate-info-value">${escapeHtml(duplicate.url)}</span> </div> ` : ''} ${duplicate.tags?.length ? ` <div class="duplicate-info-item"> <span class="duplicate-info-label">标签</span> <span class="duplicate-info-value">${formatTags(duplicate.tags)}</span> </div> ` : ''} ${duplicate.note ? ` <div class="duplicate-info-item"> <span class="duplicate-info-label">备注</span> <span class="duplicate-info-value">${escapeHtml(duplicate.note)}</span> </div> ` : ''} `; document.getElementById('duplicateInfo').innerHTML = infoHtml; document.getElementById('duplicateModal').classList.add('active'); // Store pending data duplicateCallback = { newAcc, replaceId: duplicate.id }; } function cancelDuplicate() { document.getElementById('duplicateModal').classList.remove('active'); duplicateCallback = null; } function forceAddDuplicate() { if (duplicateCallback) { const { newAcc, replaceId } = duplicateCallback; doSaveAccount(newAcc, replaceId); document.getElementById('duplicateModal').classList.remove('active'); duplicateCallback = null; } } function deleteAccount(id) { accounts = accounts.filter(a => a.id !== id); saveData(); renderAccounts(); renderTags(); showToast('账号已删除'); } // Export Functions function showExportModal() { if (!accounts.length) { showToast('暂无数据可导出'); return; } document.getElementById('exportModal').classList.add('active'); selectExportType('plain'); } function closeExportModal() { document.getElementById('exportModal').classList.remove('active'); } function closeExportModalOnOverlay(e) { if (e.target === e.currentTarget) closeExportModal(); } function selectExportType(type) { pendingExportType = type; document.getElementById('exportPlain').classList.toggle('selected', type === 'plain'); document.getElementById('exportEncrypted').classList.toggle('selected', type === 'encrypted'); const pwdSection = document.getElementById('exportPasswordSection'); pwdSection.style.display = type === 'encrypted' ? 'block' : 'none'; if (type === 'encrypted') { setTimeout(() => document.getElementById('exportPassword').focus(), 100); } } async function executeExport() { const type = pendingExportType; if (type === 'encrypted') { const pwd = document.getElementById('exportPassword').value; const pwdConfirm = document.getElementById('exportPasswordConfirm').value; if (!pwd) { showToast('请输入导出密码'); return; } if (pwd !== pwdConfirm) { showToast('两次输入的密码不一致'); return; } if (pwd.length < 6) { showToast('密码长度至少6位'); return; } try { const encrypted = await encryptData(accounts, pwd); downloadFile(JSON.stringify(encrypted, null, 2), `accounts_encrypted_${new Date().toISOString().split('T')[0]}.json`); showToast('加密数据已导出'); closeExportModal(); } catch (e) { showToast('加密失败:' + e.message); } } else { const data = JSON.stringify(accounts, null, 2); downloadFile(data, `accounts_${new Date().toISOString().split('T')[0]}.json`); showToast('数据已导出'); closeExportModal(); } } function downloadFile(content, filename) { const blob = new Blob([content], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // Import Functions async function importData(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { try { const content = e.target.result; let data; try { data = JSON.parse(content); } catch (err) { throw new Error('文件格式错误,不是有效的 JSON'); } // Check if encrypted if (data.encrypted && data.data) { pendingImportData = data; pendingImportFile = file; document.getElementById('importPasswordModal').classList.add('active'); document.getElementById('importPassword').value = ''; setTimeout(() => document.getElementById('importPassword').focus(), 100); return; } // Plain import if (!Array.isArray(data)) { throw new Error('数据格式错误,应为账号数组'); } processImportData(data); } catch (err) { alert('导入失败:' + err.message); } input.value = ''; }; reader.readAsText(file); } async function executeImportWithPassword() { const pwd = document.getElementById('importPassword').value; if (!pwd) { showToast('请输入解密密码'); return; } try { const decrypted = await decryptData(pendingImportData, pwd); if (!Array.isArray(decrypted)) { throw new Error('解密后数据格式错误'); } processImportData(decrypted); document.getElementById('importPasswordModal').classList.remove('active'); pendingImportData = null; pendingImportFile = null; } catch (err) { showToast('解密失败:' + err.message); } } function cancelImport() { document.getElementById('importPasswordModal').classList.remove('active'); pendingImportData = null; pendingImportFile = null; document.getElementById('importFile').value = ''; } function processImportData(data) { // Fix URLs in imported data data.forEach(item => { if (item.url) { item.url = fixUrl(item.url); } }); let added = 0; let skipped = 0; let updated = 0; const skippedItems = []; data.forEach(item => { if (!item.id || !item.platform) return; // Check for exact duplicate const existing = accounts.find(a => a.id === item.id); if (existing) { // Check if exactly the same if (isAccountEqual(existing, item)) { skipped++; skippedItems.push(item.platform); return; } // Different - update if newer if (new Date(item.updatedAt) > new Date(existing.updatedAt)) { accounts[accounts.indexOf(existing)] = item; updated++; } } else { // Check if new item is duplicate of existing (by content) const contentDuplicate = accounts.find(a => isAccountEqual(a, item)); if (contentDuplicate) { skipped++; skippedItems.push(item.platform); return; } accounts.push(item); added++; } }); saveData(); renderAccounts(); renderTags(); let msg = `导入完成:新增 ${added} 条`; if (updated > 0) msg += `,更新 ${updated} 条`; if (skipped > 0) msg += `,跳过 ${skipped} 条重复`; showToast(msg); if (skippedItems.length > 0) { setTimeout(() => { alert(`以下账号因信息完全相同已跳过:\n${skippedItems.join('\n')}`); }, 300); } } // Clear All with Double Confirm function confirmClearAll() { if (accounts.length === 0) { showToast('暂无数据可清空'); return; } document.getElementById('confirmClearModal').classList.add('active'); document.getElementById('clearConfirmInput').value = ''; document.getElementById('clearConfirmInput').classList.remove('error'); document.getElementById('confirmClearBtn').disabled = true; document.getElementById('clearConfirmInput').focus(); } function closeConfirmClearModal() { document.getElementById('confirmClearModal').classList.remove('active'); } function closeConfirmClearModalOnOverlay(e) { if (e.target === e.currentTarget) closeConfirmClearModal(); } // Real-time validation for clear confirm document.getElementById('clearConfirmInput')?.addEventListener('input', function(e) { const input = e.target; const btn = document.getElementById('confirmClearBtn'); if (input.value === 'DELETE') { btn.disabled = false; input.classList.remove('error'); } else { btn.disabled = true; if (input.value.length >= 6) { input.classList.add('error'); } else { input.classList.remove('error'); } } }); function executeClearAll() { const input = document.getElementById('clearConfirmInput'); if (input.value !== 'DELETE') { input.classList.add('error'); return; } accounts = []; saveData(); renderAccounts(); renderTags(); closeConfirmClearModal(); showToast('所有数据已清空'); } // Simple Confirm Modal function showSimpleConfirm(title, desc, callback) { simpleConfirmCallback = callback; document.getElementById('simpleConfirmTitle').textContent = title; document.getElementById('simpleConfirmDesc').textContent = desc; document.getElementById('confirmModal').classList.add('active'); } function closeConfirmModal() { document.getElementById('confirmModal').classList.remove('active'); simpleConfirmCallback = null; } function closeConfirmModalOnOverlay(e) { if (e.target === e.currentTarget) closeConfirmModal(); } function executeSimpleConfirm() { if (simpleConfirmCallback) { simpleConfirmCallback(); } closeConfirmModal(); } // Render function renderAccounts() { const search = document.getElementById('searchInput').value.toLowerCase(); const grid = document.getElementById('accountsGrid'); const empty = document.getElementById('emptyState'); let filtered = accounts.filter(a => { const matchSearch = !search || a.platform.toLowerCase().includes(search) || a.username.toLowerCase().includes(search) || a.tags?.some(t => t.toLowerCase().includes(search)) || (a.url && a.url.toLowerCase().includes(search)) || a.note?.toLowerCase().includes(search); const matchTag = currentFilter === 'all' || a.tags?.includes(currentFilter); return matchSearch && matchTag; }); // Sort accounts by update time filtered.sort((a, b) => { const timeA = new Date(a.updatedAt).getTime(); const timeB = new Date(b.updatedAt).getTime(); if (window.sortOrder === 'oldest') { return timeA - timeB; // Oldest first } return timeB - timeA; // Newest first (default) }); document.getElementById('totalCount').textContent = filtered.length; if (accounts.length > 0) { const last = accounts.reduce((a, b) => new Date(a.updatedAt) > new Date(b.updatedAt) ? a : b ); document.getElementById('lastUpdate').textContent = new Date(last.updatedAt).toLocaleDateString('zh-CN'); } else { document.getElementById('lastUpdate').textContent = '-'; } if (filtered.length === 0) { grid.style.display = 'none'; empty.style.display = 'block'; empty.querySelector('.empty-title').textContent = search || currentFilter !== 'all' ? '无匹配结果' : '暂无账号'; empty.querySelector('.empty-desc').textContent = search || currentFilter !== 'all' ? '尝试其他关键词或筛选条件' : '点击右上角"添加账号"开始使用'; return; } grid.style.display = 'grid'; empty.style.display = 'none'; grid.innerHTML = filtered.map(acc => ` <div class="account-card" onclick="handleCardClick(event, '${acc.id}')"> <div class="card-header"> <div class="platform-info"> <div class="platform-icon">${acc.platform.charAt(0).toUpperCase()}</div> <div class="platform-meta"> <div class="platform-name">${escapeHtml(acc.platform)}</div> ${acc.url ? `<a href="${escapeHtml(acc.url)}" target="_blank" class="platform-url" title="访问网站" onclick="event.stopPropagation()"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> <polyline points="15 3 21 3 21 9"></polyline> <line x1="10" y1="14" x2="21" y2="3"></line> </svg> ${escapeHtml(acc.url.replace(/^https?:\/\//, '').replace(/\/$/, ''))} </a>` : ''} ${acc.tags?.length ? `<div class="card-tags"> ${acc.tags.map(t => `<span class="card-tag">${escapeHtml(t)}</span>`).join('')} </div>` : ''} </div> </div> <div class="card-actions" onclick="event.stopPropagation()"> <button class="card-btn" onclick="openModal('${acc.id}')" title="编辑"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> </button> <button class="card-btn delete" onclick="confirmDeleteAccount('${acc.id}', '${escapeHtml(acc.platform)}'); event.stopPropagation();" title="删除"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="3 6 5 6 21 6"></polyline> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> </svg> </button> </div> </div> <div class="fields"> <div class="field"> <span class="field-label">用户名</span> <div class="field-value"> <span class="field-text">${escapeHtml(acc.username)}</span> <div class="field-actions"> <button class="field-btn" onclick="copyText('${escapeHtml(acc.username)}', this); event.stopPropagation();">复制</button> </div> </div> </div> <div class="field"> <span class="field-label">密码</span> <div class="field-value"> <span class="field-text password-text" id="pwd-${acc.id}">${escapeHtml(acc.password)}</span> <div class="field-actions"> <button class="field-btn" onclick="togglePwd('${acc.id}'); event.stopPropagation();">显示</button> <button class="field-btn" onclick="copyText('${escapeHtml(acc.password)}', this); event.stopPropagation();">复制</button> </div> </div> </div> ${acc.note ? ` <div class="field"> <span class="field-label">备注</span> <div class="note-text">${escapeHtml(acc.note)}</div> </div> ` : ''} </div> <div class="card-footer"> <div class="update-time"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12 6 12 12 16 14"></polyline> </svg> <span>${formatRelativeTime(acc.updatedAt)}</span> </div> </div> </div> `).join(''); } function handleCardClick(event, id) { // Don't open view if clicking on buttons or links if (event.target.closest('.card-actions') || event.target.closest('.field-actions') || event.target.closest('a')) { return; } openViewModal(id); } function togglePwd(id) { const el = document.getElementById(`pwd-${id}`); const btn = event.target; if (el.classList.contains('revealed')) { el.classList.remove('revealed'); btn.textContent = '显示'; } else { el.classList.add('revealed'); btn.textContent = '隐藏'; } } // Utils function copyText(text, btn) { // Try modern clipboard API first if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { showCopied(btn); }).catch(() => { fallbackCopy(text, btn); }); } else { fallbackCopy(text, btn); } } function fallbackCopy(text, btn) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { const successful = document.execCommand('copy'); if (successful) { showCopied(btn); } else { showToast('复制失败'); } } catch (err) { showToast('复制失败'); } document.body.removeChild(textArea); } function showCopied(btn) { btn.textContent = '已复制'; btn.classList.add('copied'); showToast('已复制到剪贴板'); setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000); } function showToast(msg) { const toast = document.getElementById('toast'); document.getElementById('toastMsg').textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 3000); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Format relative time in Chinese function formatRelativeTime(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); const diffMonth = Math.floor(diffDay / 30); const diffYear = Math.floor(diffDay / 365); if (diffSec < 60) { return '刚刚'; } else if (diffMin < 60) { return `${diffMin}分钟前`; } else if (diffHour < 24) { return `${diffHour}小时前`; } else if (diffDay === 1) { return '昨天'; } else if (diffDay < 30) { return `${diffDay}天前`; } else if (diffMonth < 12) { return `${diffMonth}个月前`; } else if (diffYear === 1) { return '1年前'; } else { return `${diffYear}年前`; } } // Confirm delete with modal function confirmDeleteAccount(id, platform) { showSimpleConfirm( `确认删除 ${platform}?`, '此操作不可恢复,账号将被永久删除。', () => { deleteAccount(id); } ); } function saveData() { localStorage.setItem('pwd_accounts', JSON.stringify(accounts)); } // Sort order - default newest first window.sortOrder = 'newest'; // Toggle sort order function toggleSortOrder() { window.sortOrder = window.sortOrder === 'newest' ? 'oldest' : 'newest'; updateSortIcon(); renderAccounts(); showToast(window.sortOrder === 'newest' ? '已切换:最新在前' : '已切换:最早在前'); } // Update sort icon based on current order function updateSortIcon() { const icon = document.getElementById('sortIcon'); const btn = document.getElementById('sortBtn'); const text = document.getElementById('sortText'); if (window.sortOrder === 'newest') { // Newest first icon (arrow up - descending) icon.innerHTML = ` <polyline points="12 6 12 18" style="stroke-width: 2.5"></polyline> <polyline points="8 10 12 6 16 10" style="stroke-width: 2.5"></polyline> `; btn.title = "当前:最新在前,点击切换"; btn.classList.add('active'); if (text) text.textContent = '最新'; } else { // Oldest first icon (arrow down - ascending) icon.innerHTML = ` <polyline points="12 6 12 18" style="stroke-width: 2.5"></polyline> <polyline points="8 14 12 18 16 14" style="stroke-width: 2.5"></polyline> `; btn.title = "当前:最早在前,点击切换"; btn.classList.remove('active'); if (text) text.textContent = '最早'; } } // Init document.addEventListener('DOMContentLoaded', () => { initTheme(); updateSortIcon(); renderAccounts(); renderTags(); }); // Handle resize let lastWidth = window.innerWidth; window.addEventListener('resize', () => { const currentWidth = window.innerWidth; const wasMobile = lastWidth <= 768; const isMobileNow = currentWidth <= 768; if (wasMobile !== isMobileNow) { renderAccounts(); } lastWidth = currentWidth; }); // Keyboard document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(); closeViewModal(); closeConfirmModal(); closeConfirmClearModal(); closeExportModal(); cancelImport(); cancelDuplicate(); } }); </script> </body> </html>这是说明文本:------------------------------------------------------ 作者:Yangxiao 作者域名:aszv.top 声明:作者不承担任何责任,是否使用取决于你 安全与否取决于你怎么保存的数据,即使做的再好,也不能避免物理层面信息泄露,实在不放心的可断网使用 ------------------------------------------------------ 使用方法: 1、电脑端直接双击打开即可使用,这是纯前端网页工具,无需后端运行 2、手机上可以在文件夹里找到文件,选择浏览器打开也可以使用,收藏到书签方便下次访问 3、有服务器的,可以把 index.html 文件上传到网站根目录下,访问域名即可直接使用 ------------------------------------------------------这里是网盘文件地址:{cloud title="账号管理器" type="lz" url="https://wwbwh.lanzouw.com/iGCwV3p7i2xe" password=""/}文件存在蓝奏云上,登录的是15310649220手机号,文件路径:根目录/2026/05/11/账号管理器/账号管理器.zip{cloud title="夸克网盘(备用)" type="default" url="https://pan.quark.cn/s/2d15962d24d8" password=""/}文件存在夸克网盘上,登录手机号同上,文件路径:根目录/2026/05/11/账号管理器/账号管理器.zip{callout color="#2166c0"}账号管理器演示地址:https://a.aszv.top/zhanghao/{/callout}网站搭建地址同域名演示图片:下面是发布在blog.aszv.top的原文章内容:这是一款账号管理器网页工具,核心作用是帮你集中、安全地管理各类平台的账号密码信息,整体设计兼顾了实用性和易用性,还适配了桌面端和移动端使用场景,具体功能和特点可以分为这些方面:展示图片:{alert type="info"}账号管理器演示地址:https://a.aszv.top/zhanghao/{/alert}{cloud title="账号管理器" type="lz" url="https://wwbwh.lanzouw.com/iGCwV3p7i2xe" password=""/}{cloud title="夸克网盘(备用)" type="default" url="https://pan.quark.cn/s/2d15962d24d8" password=""/}1. 核心功能:账号信息管理集中存储:可以添加并保存不同平台(比如网站、APP)的账号信息,包括平台名称、网址、用户名、密码,还能记录备注、添加标签,把零散的账号密码都汇总在一处,不用再记在本子或随便存在记事本里。便捷查看/编辑:账号信息以卡片形式展示,点击卡片能查看详情,也能随时编辑、删除账号,还支持复制账号/密码(复制后会有提示),密码默认模糊显示,可手动切换显示/隐藏。密码生成:添加账号时,工具自带密码生成功能,还能自定义密码长度、是否包含数字/符号等,帮你创建安全度更高的密码。2. 实用辅助功能搜索与筛选:支持关键词搜索账号,也能通过标签筛选,快速找到需要的账号信息;还能对账号列表进行排序,方便管理。数据备份/恢复:支持导出账号数据(备份)和导入数据(恢复/迁移),不用担心数据丢失;主题切换:有浅色/深色两种主题模式,可根据使用习惯或环境切换,视觉体验更舒适。3. 体验与适配界面设计:整体简洁清晰,卡片式布局一目了然,操作按钮(比如添加、编辑、复制)都很直观,hover(悬停)时还有交互反馈,使用起来很顺手;多端适配:专门做了移动端优化(比如调整布局、缩放适配),手机上也能方便操作,桌面端则保留更完整的操作空间;细节体验:比如密码复制后有 Toast 提示、删除账号有二次确认(防止误删)、账号卡片显示更新时间等,细节上考虑得比较周全。简单来说,这个工具解决了“账号密码太多记不住、找起来麻烦、密码不安全”的问题,相当于一个专属的“数字密码本”,而且是网页形式,不用安装额外软件,打开就能用,还兼顾了使用的便捷性和数据管理的安全性。{bilibili bvid="BV1CeQcBzE6e" page="1"/}
2026年05月11日
2 阅读
0 评论
0 点赞
2026-05-11
文章代码块默认最多显示12行,超出自动出现滚动条
文章代码块默认最多显示12行,超出自动出现滚动条,所有样式完全不变直接复制这段到 Joe 主题 自定义CSS 里即可/* 文章代码块限制最大12行,超出滚动,不影响原有任何样式 */ article pre { max-height: 20em !important; overflow-y: auto !important; overflow-x: auto !important; } /* 行内单行小代码不限制高度 */ article :not(pre) > code { max-height: unset !important; }操作步骤把之前我给你的那段复制按钮JS代码直接删掉(不需要了)只把上面这段 CSS 粘贴到后台 Joe 自定义 CSS 最底部保存 → 清浏览器缓存,立马生效简单调节行数现在 20em 就是刚好12行想多显示几行:改成 28em想少显示几行:改成 16em
2026年05月11日
1 阅读
0 评论
0 点赞
2026-05-11
这里是本地笔记空间 - 记录我的笔记
网站搭建方式:这个网站搭建在机顶盒上,Linux系统,1panel面板,后面复刻可以参考这里的教程数据库地址:/opt/1panel/www/sites/192.168.0.101/index/usr/6a013f8c941b2.db现在没有这个了,之前用的文件数据库,现在不是了搭建的主题博客是:Typecho+Joe主题
2026年05月11日
2 阅读
1 评论
0 点赞
1
2
3