feat(vault): 授权来源候选扩至 37 项并支持手填自动记入
问题:凭据表单的「授权来源」只有 6 项 datalist 建议,缺国内常见的支付宝/淘宝/ 微博等三方登录,且移动端 iOS Safari 对 datalist 支持不稳,导致没法填。 - api.js: OAUTH_PROVIDERS(6) → OAUTH_PRESETS(37,按国内/国际/开发者分组, key 为入库值、label 为展示名);新增 oauthLabel/oauthDetail 展示中文名, normalizeOauth 把中文输入归一化成英文 slug,避免同一来源存成多种写法 - modals.js: 授权来源改为「输入框 + 分组芯片」选择器,随输入过滤(中英文均可 搜)、可一键清空、误填项可划掉;无匹配时提示将按原样保存 - store.js: 手填的非预设值记入 localStorage 候选,并在拉取凭据列表后与服务端 值对账,换设备也能看到以前填过的来源;被划掉的记入 ignore 不再冒回 - credential_service.py: 凭据库搜索纳入 oauth_provider
This commit is contained in:
+59
-7
@@ -487,12 +487,36 @@ const CredentialModal = {
|
||||
<label class="block"><span class="text-xs text-slate-500">用户名/邮箱</span>
|
||||
<input v-model="f.username" placeholder="如 you@gmail.com" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||||
</div>
|
||||
<!-- 授权登录:只需记录来源,本站无独立密码 -->
|
||||
<label v-if="f.login_type==='oauth'" class="block"><span class="text-xs text-slate-500">授权来源</span>
|
||||
<input v-model="f.oauth_provider" list="oauth-provider-options" placeholder="google / apple / github / wechat" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm">
|
||||
<datalist id="oauth-provider-options">
|
||||
<option v-for="p in Fmt.OAUTH_PROVIDERS" :key="p" :value="p"></option>
|
||||
</datalist></label>
|
||||
<!-- 授权登录:只需记录来源,本站无独立密码;候选=预设+手填留存,点一下即填 -->
|
||||
<div v-if="f.login_type==='oauth'" class="space-y-1.5">
|
||||
<label class="block"><span class="text-xs text-slate-500">授权来源(点选或直接输入)</span>
|
||||
<div class="relative mt-1">
|
||||
<input v-model="f.oauth_provider" placeholder="如 支付宝 / alipay,也可以是任意自定义来源"
|
||||
class="w-full px-3 py-2 pr-8 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm">
|
||||
<button v-if="f.oauth_provider" type="button" @click="f.oauth_provider=''" title="清空"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 text-sm">✕</button>
|
||||
</div>
|
||||
</label>
|
||||
<!-- 候选自己渲染(不用 datalist):iOS Safari 对 datalist 支持不稳,且需要展示中文名 -->
|
||||
<div class="max-h-36 overflow-y-auto rounded-lg border border-slate-200 dark:border-slate-800 bg-slate-50/60 dark:bg-slate-800/30 p-2 space-y-1.5">
|
||||
<div v-for="g in oauthGroups" :key="g.name">
|
||||
<div class="text-[10px] text-slate-400 mb-0.5">{{ g.name }}</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<button v-for="o in g.items" :key="o.key" type="button" @click="pickOauth(o.key)"
|
||||
class="text-[11px] pl-2 py-1 rounded-full border flex items-center gap-1"
|
||||
:class="f.oauth_provider===o.key ? 'border-emerald-500 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : 'border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300'">
|
||||
<span>{{ o.label }}</span>
|
||||
<span v-if="o.label!==o.key" class="text-[9px] text-slate-400 pr-1">{{ o.key }}</span>
|
||||
<span v-if="o.custom" @click.stop="dropOauth(o.key)" title="从候选移除"
|
||||
class="px-1 text-slate-400 hover:text-red-500">✕</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="oauthUnmatched" class="text-[11px] text-slate-400 leading-relaxed">
|
||||
「{{ f.oauth_provider }}」不在候选里,会按原样保存,并记入上面的自定义候选
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<label v-else-if="f.login_type==='password'" class="block"><span class="text-xs text-slate-500">登录密码(加密存储)</span>
|
||||
<input v-model="f.password" type="password" autocomplete="new-password" :placeholder="store.credentialModal.editing ? '留空不修改' : '可选'" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||||
<label class="block"><span class="text-xs text-slate-500">登录页地址</span>
|
||||
@@ -519,6 +543,34 @@ const CredentialModal = {
|
||||
</div>`,
|
||||
setup() {
|
||||
const f = Vue.computed(() => store.credentialModal.form);
|
||||
// 候选清单:手填留存值置顶(最近用过),其后按预设的国内/国际/开发者分组
|
||||
const oauthAll = Vue.computed(() => [
|
||||
...store.oauthCustom.map(k => ({ key: k, label: k, group: '自定义(我填过的)', custom: true })),
|
||||
...Fmt.OAUTH_PRESETS,
|
||||
]);
|
||||
function groupOptions(list) {
|
||||
const groups = [];
|
||||
for (const o of list) {
|
||||
let g = groups.find(x => x.name === o.group);
|
||||
if (!g) { g = { name: o.group, items: [] }; groups.push(g); }
|
||||
g.items.push(o);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
// 输入时同步过滤(key / 中文名都能搜);没命中就退回完整清单,避免逼用户先删字
|
||||
const oauthGroups = Vue.computed(() => {
|
||||
const all = oauthAll.value;
|
||||
const kw = ((f.value && f.value.oauth_provider) || '').trim().toLowerCase();
|
||||
if (!kw) return groupOptions(all);
|
||||
const hit = all.filter(o => o.key.toLowerCase().includes(kw) || o.label.toLowerCase().includes(kw));
|
||||
return hit.length ? groupOptions(hit) : groupOptions(all);
|
||||
});
|
||||
// 手填的新值给出明确反馈:会按原样入库并成为候选
|
||||
const oauthUnmatched = Vue.computed(() => {
|
||||
const key = Fmt.normalizeOauth((f.value && f.value.oauth_provider) || '');
|
||||
return !!key && !oauthAll.value.some(o => o.key === key);
|
||||
});
|
||||
function pickOauth(key) { if (f.value) f.value.oauth_provider = key; }
|
||||
// 2FA 前端预校验:填了 secret 就必须填 6 位码(服务端会再校验一次)
|
||||
function save() {
|
||||
const form = f.value;
|
||||
@@ -528,6 +580,6 @@ const CredentialModal = {
|
||||
}
|
||||
saveCredential();
|
||||
}
|
||||
return { store, Fmt, f, save };
|
||||
return { store, Fmt, f, oauthGroups, oauthUnmatched, pickOauth, dropOauth: dropOauthProvider, save };
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user