/* 凭据库视图:登录密码 + 2FA 动态码 * * 数据来自 credentials 表(登录凭据唯一事实源):既包含平台账号迁移/联动过来的 * 条目,也包含自行录入的普通网站。列表以「取用快」为先:搜索、一键复制、 * 展开看动态码;移动端单列卡片,桌面端同布局(避免横向溢出)。 */ const VaultView = { template: `
🔐 凭据库 = 各站点的登录方式(用户名 / 密码 / 2FA / 授权登录)。平台账号的登录密码也统一存在这里,只维护一份,不会两头改。

{{ store.vaultSearch || store.vaultOtpOnly ? '没有匹配的凭据' : '暂无凭据。点右上「+ 新增」录入网站账号;平台账号填过登录密码的会自动出现在这里。' }}

{{ c.site }} 2FA 重复?
{{ c.username }}未填用户名 · {{ c.oauth_provider || '授权' }} 登录 · 其他登录方式 · 平台账号
{{ otpOf(c).code || '······' }}
{{ otpOf(c).expiresIn }}s 后刷新
{{ c.note }}
授权登录({{ c.oauth_provider || '未记录来源' }}),本站无独立密码
关联平台账号:{{ c.account_name }}
更新于 {{ fmtTime(c.updated_at) }}

🔑 {{ pwd.site }}

用户名 {{ pwd.username }}
密码 {{ pwd.password }}

{{ pwd.countdown }}s 后自动关闭

`, setup() { const expanded = Vue.reactive({}); const pwd = Vue.reactive({ show: false, site: '', username: '', password: '', countdown: 0, _timer: null }); function otpOf(c) { return otpState[c.id] || { code: '', expiresIn: 0, loading: false }; } // 展开即拉动态码并开始倒计时;收起时停掉定时器 function toggle(c, forceOpen) { const open = forceOpen ? true : !expanded[c.id]; expanded[c.id] = open; if (!c.has_otp) return; if (open && !otpState[c.id]) fetchOtp(c.id); if (!open) stopOtp(c.id); } async function reveal(c) { store.error = ''; try { const r = await Api.get('/credentials/' + c.id + '/password'); pwd.site = c.site; pwd.username = r.username || ''; pwd.password = r.password; pwd.show = true; if (pwd._timer) clearInterval(pwd._timer); pwd.countdown = 8; pwd._timer = setInterval(() => { pwd.countdown--; if (pwd.countdown <= 0) closePwd(); }, 1000); } catch (e) { store.error = e.message; } } function closePwd() { pwd.show = false; pwd.password = ''; if (pwd._timer) { clearInterval(pwd._timer); pwd._timer = null; } } function copyOtp(c) { const s = otpState[c.id]; if (s && s.code) copyText(s.code, '验证码'); } function fmtTime(iso) { if (!iso) return ''; return new Date(iso).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } // 搜索防抖:300ms 内连续输入只发一次请求 let searchTimer = null; function debouncedReload() { if (searchTimer) clearTimeout(searchTimer); searchTimer = setTimeout(() => { loadCredentials(); }, 300); } function toggleOtpFilter() { store.vaultOtpOnly = !store.vaultOtpOnly; loadCredentials(); } // 离开视图时停掉全部倒计时,避免后台定时器泄漏 Vue.onUnmounted(() => { stopAllOtp(); closePwd(); }); return { store, Fmt, expanded, pwd, otpOf, toggle, reveal, closePwd, copyOtp, fmtTime, reload: debouncedReload, toggleOtpFilter, copyText, copyPwd: copyCredentialPassword, edit: openCredentialEdit, del: deleteCredential, unbindOtp: unbindCredentialOtp, }; }, };