Files
vps-manager/static/js/api.js
T
gouki 49c4dc9af5 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
2026-09-10 09:27:52 +00:00

131 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* API 客户端与格式化工具 */
window.VpsApi = (function () {
const CFG = window.APP_CONFIG || { appName: 'VPS 资产管理系统' };
function getApiKey() { return localStorage.getItem('vps_api_key') || ''; }
function setApiKey(k) { localStorage.setItem('vps_api_key', k || ''); }
async function request(path, options = {}) {
const headers = { 'Content-Type': 'application/json' };
const key = getApiKey();
if (key) headers['X-API-Key'] = key;
const res = await fetch('/api' + path, { ...options, headers: { ...headers, ...(options.headers || {}) } });
if (!res.ok) {
let msg = res.statusText;
try { const e = await res.json(); msg = (typeof e.detail === 'string' ? e.detail : JSON.stringify(e.detail)) || msg; } catch (_) {}
throw new Error(msg);
}
if (res.status === 204) return null;
return res.json();
}
return {
CFG, getApiKey, setApiKey,
get: (p) => request(p),
post: (p, body) => request(p, { method: 'POST', body: JSON.stringify(body) }),
put: (p, body) => request(p, { method: 'PUT', body: JSON.stringify(body) }),
del: (p) => request(p, { method: 'DELETE' }),
};
})();
/* 授权登录来源预设:key = 入库值(oauth_provider,统一英文 slug),label = 展示名,
* group = 表单分组顺序。国内三方登录排在最前(支付宝 / 淘宝 / 微博 等),
* 手填的自定义值由 store.oauthCustom 追加成候选,不需要动后端字典。 */
const OAUTH_PRESETS = [
{ key: 'alipay', label: '支付宝', group: '国内' },
{ key: 'taobao', label: '淘宝', group: '国内' },
{ key: 'wechat', label: '微信', group: '国内' },
{ key: 'qq', label: 'QQ', group: '国内' },
{ key: 'weibo', label: '微博', group: '国内' },
{ key: 'douyin', label: '抖音', group: '国内' },
{ key: 'baidu', label: '百度', group: '国内' },
{ key: 'jd', label: '京东', group: '国内' },
{ key: 'xiaohongshu', label: '小红书', group: '国内' },
{ key: 'bilibili', label: 'B站', group: '国内' },
{ key: 'meituan', label: '美团', group: '国内' },
{ key: 'dingtalk', label: '钉钉', group: '国内' },
{ key: 'feishu', label: '飞书', group: '国内' },
{ key: 'huawei', label: '华为', group: '国内' },
{ key: 'mi', label: '小米', group: '国内' },
{ key: 'google', label: 'Google', group: '国际' },
{ key: 'apple', label: 'Apple', group: '国际' },
{ key: 'microsoft', label: 'Microsoft', group: '国际' },
{ key: 'facebook', label: 'Facebook', group: '国际' },
{ key: 'x', label: 'X (Twitter)', group: '国际' },
{ key: 'yahoo', label: 'Yahoo', group: '国际' },
{ key: 'line', label: 'LINE', group: '国际' },
{ key: 'kakao', label: 'Kakao', group: '国际' },
{ key: 'naver', label: 'NAVER', group: '国际' },
{ key: 'amazon', label: 'Amazon', group: '国际' },
{ key: 'paypal', label: 'PayPal', group: '国际' },
{ key: 'discord', label: 'Discord', group: '国际' },
{ key: 'telegram', label: 'Telegram', group: '国际' },
{ key: 'reddit', label: 'Reddit', group: '国际' },
{ key: 'linkedin', label: 'LinkedIn', group: '国际' },
{ key: 'steam', label: 'Steam', group: '国际' },
{ key: 'epic', label: 'Epic Games', group: '国际' },
{ key: 'github', label: 'GitHub', group: '开发者' },
{ key: 'gitlab', label: 'GitLab', group: '开发者' },
{ key: 'bitbucket', label: 'Bitbucket', group: '开发者' },
{ key: 'cloudflare', label: 'Cloudflare', group: '开发者' },
{ key: 'huggingface', label: 'Hugging Face', group: '开发者' },
];
window.VpsFmt = {
TYPE_LABELS: { vps: 'VPS', domain: '域名', ai_agent: 'AI账号', cloudflare: 'Cloudflare', other: '其他' },
STATUS_LABELS: { active: '使用中', expired: '已过期', stopped: '已停止', cancelled: '已注销', unknown: '未知' },
CATEGORY_LABELS: { vps: 'VPS', domain: '域名', ai_agent: 'AI账号', cloudflare: 'Cloudflare', other: '其他' },
// 平台可提供的服务(综合平台多标签):字段对应 Provider.services 逗号分隔列表
SERVICES_LABELS: { vps: '云服务器', domain: '域名', ai_agent: 'AI服务', cloudflare: 'Cloudflare', ssl_cert: 'SSL证书', cdn: 'CDN', dns: 'DNS', other: '其他' },
CYCLE_LABELS: { monthly: '月付', quarterly: '季付', yearly: '年付' },
// 凭据库登录方式:字段对应 Credential.login_type
LOGIN_TYPE_LABELS: { password: '密码登录', oauth: '授权登录', other: '其他' },
/* 授权登录来源候选(表单 chips + datalist 建议)
*
* 清单在上面 OAUTH_PRESETS;这里只暴露给视图使用。
*/
OAUTH_PRESETS,
// 短展示名:命中预设给中文,否则原样返回(自定义值不加工)
oauthLabel(k) {
const p = OAUTH_PRESETS.find(x => x.key === (k || '').trim().toLowerCase());
return p ? p.label : (k || '');
},
// 详情展示:「支付宝(alipay)」,自定义值只显示原文
oauthDetail(k) {
const p = OAUTH_PRESETS.find(x => x.key === (k || '').trim().toLowerCase());
return p ? p.label + '' + p.key + '' : (k || '');
},
// 归一化:允许输入中文名或大小写不一致的 slug,落到规范 key 上,避免同来源存成多种写法
normalizeOauth(raw) {
const v = (raw || '').trim();
if (!v) return '';
const lower = v.toLowerCase();
const p = OAUTH_PRESETS.find(x => x.key === lower || x.label.toLowerCase() === lower);
return p ? p.key : v;
},
loginTypeBadge(t) {
return { password: 'bg-slate-500/10 text-slate-600 dark:text-slate-400', oauth: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', other: 'bg-amber-500/10 text-amber-600 dark:text-amber-400' }[t] || 'bg-slate-500/10 text-slate-500';
},
typeBadge(t) {
return { vps: 'bg-blue-500/10 text-blue-600 dark:text-blue-400', domain: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', ai_agent: 'bg-violet-500/10 text-violet-600 dark:text-violet-400', cloudflare: 'bg-orange-500/10 text-orange-600 dark:text-orange-400', other: 'bg-slate-500/10 text-slate-600 dark:text-slate-400' }[t] || 'bg-slate-500/10 text-slate-500';
},
statusBadge(s) {
return { active: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', expired: 'bg-red-500/10 text-red-600 dark:text-red-400', stopped: 'bg-slate-500/10 text-slate-500', cancelled: 'bg-slate-500/10 text-slate-400', unknown: 'bg-amber-500/10 text-amber-600 dark:text-amber-400' }[s] || 'bg-slate-500/10 text-slate-500';
},
expiryText(d) {
if (d === null || d === undefined) return 'text-slate-400';
if (d < 0) return 'text-red-600 dark:text-red-400 font-semibold';
if (d <= 7) return 'text-red-600 dark:text-red-400 font-semibold';
if (d <= 30) return 'text-amber-600 dark:text-amber-400';
return 'text-slate-500 dark:text-slate-400';
},
expiryBadge(d) {
if (d !== null && d !== undefined && d <= 7) return 'bg-red-500/10 text-red-600 dark:text-red-400';
if (d !== null && d !== undefined && d <= 30) return 'bg-amber-500/10 text-amber-600 dark:text-amber-400';
return 'bg-slate-500/10 text-slate-500';
},
checkBadge(s) {
return { pass: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-400', fail: 'bg-red-500/10 text-red-600 dark:text-red-400', unknown: 'bg-slate-500/10 text-slate-500' }[s] || 'bg-slate-500/10 text-slate-500';
},
};