/* 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'; }, };