/* 资产编辑模态框 */ const AssetModal = { template: `

{{ store.assetModal.editing ? '编辑资产' : '新增资产' }}

VPS 详情
域名详情
AI 账号详情
Cloudflare 子资产详情
`, setup() { const f = Vue.computed(() => store.assetModal.form); const providersForType = Vue.computed(() => { const t = f.value ? f.value.asset_type : null; const map = { vps: 'vps', domain: 'domain', ai_agent: 'ai_agent', cloudflare: 'cloudflare' }; const cat = map[t]; if (!cat) return store.providers; // 综合平台按 services 过滤(如阿里云可同时选 vps/domain);services 为空时回退分类匹配 return store.providers.filter(p => { if (p.services) return p.services.split(',').map(s => s.trim()).includes(cat); return p.category === cat || p.category === 'other'; }); }); function onProviderChange() { const p = store.providers.find(x => x.id === f.value.provider_id); if (p) f.value.provider = p.slug; } // 账号候选显示:平台名 + 账号标识(同名账号靠平台前缀区分),说明优先 function accountLabel(a) { const p = store.providers.find(x => x.slug === a.platform); const pname = p ? p.name : (a.platform || '未指定平台'); if (a.remark) return pname + ' · ' + a.remark + '(' + a.name + ')'; return pname + ' · ' + a.name; } return { store, Fmt, f, providersForType, onProviderChange, accountLabel, save: saveAsset }; }, }; /* 平台编辑模态框 */ const ProviderModal = { template: `

{{ store.providerModal.editing ? '编辑平台' : '新增平台' }}

提供服务(可多选,综合平台全选)
API 凭证请在平台账号中配置:保存后在平台卡片点「账号」→ 新增账号并填写 API 配置,即可测试/同步。
`, setup() { const f = Vue.computed(() => store.providerModal.form); const sdkOptions = Vue.ref([]); Vue.onMounted(async () => { try { const r = await Api.get('/providers/adapters'); const opts = []; for (const m of (r.adapters || [])) for (const t of (m.sdk_types || [])) opts.push(t); sdkOptions.value = opts; } catch (e) { /* ignore */ } }); // 提供服务多选:services 逗号分隔字符串 <-> checkbox 集合 const servicesSet = Vue.computed(() => new Set((f.value && f.value.services || '').split(',').map(s => s.trim()).filter(Boolean))); function toggleService(k) { const arr = (f.value.services || '').split(',').map(s => s.trim()).filter(Boolean); const idx = arr.indexOf(k); if (idx >= 0) arr.splice(idx, 1); else arr.push(k); f.value.services = arr.join(','); } // 凭证已下沉到账号层,平台表单不再收集 API 凭证 return { store, Fmt, f, sdkOptions, servicesSet, toggleService, save: saveProvider }; }, }; /* 账号编辑模态框(z-[60]:可能叠加在账号查看弹窗 z-50 之上打开,需更高层级) */ const AccountModal = { template: `

{{ store.accountModal.editing ? '编辑账号' : '新增账号' }}

API 配置(供 SDK 同步,加密存储)

修改账号标识后,引用该账号的资产会自动同步更新。

`, setup() { const f = Vue.computed(() => store.accountModal.form); const adapterMeta = Vue.ref([]); const apiFields = Vue.reactive({}); Vue.onMounted(async () => { try { const r = await Api.get('/providers/adapters'); adapterMeta.value = r.adapters || []; } catch (e) { /* ignore */ } }); // 按账号所属平台的 sdk_type 动态渲染所需凭证字段 const currentFields = Vue.computed(() => { const p = store.providers.find(x => x.slug === (f.value && f.value.platform)); if (!p || !p.sdk_type) return []; const m = adapterMeta.value.find(x => (x.sdk_types || []).includes(p.sdk_type)); return m ? (m.required_config || []) : []; }); // 每次打开清空残留输入,避免串号 Vue.watch(() => store.accountModal.show, (show) => { if (show) { for (const k of Object.keys(apiFields)) delete apiFields[k]; } }); function save() { const cfg = {}; for (const field of currentFields.value) if (apiFields[field]) cfg[field] = apiFields[field]; f.value.api_config = Object.keys(cfg).length ? JSON.stringify(cfg) : ''; saveAccount(); } return { store, f, currentFields, apiFields, save }; }, }; /* 账号查看弹窗:平台 → 账号 → 资产 三层结构,展开账号看名下资产 */ const AccountsViewModal = { template: `

👤 {{ title }}({{ accountList.length }})

暂无账号。一个平台可有多个账号(如代管/公司账号),添加后可在资产表单「所属账号」中选择。

{{ g.platform ? platformName(g.platform) : '未指定平台' }}
{{ a.remark || a.name }} 🔑登录 ⚙️API
{{ a.name }} · 关联 {{ assetsOf(a).length }} 个资产

该账号下暂无资产(编辑资产时在「所属账号」选择 {{ a.name }} 即可关联)

{{ Fmt.TYPE_LABELS[asset.asset_type] }} {{ asset.name }} {{ asset.expiry_date }} 到期

🔑 {{ pwd.name }} 的登录凭证

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

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

`, setup() { const expanded = Vue.reactive({}); const pwd = Vue.reactive({ show: false, name: '', login_user: '', password: '', copied: false, countdown: 0, _timer: null }); const singleProvider = Vue.computed(() => !!store.accountsModal.providerSlug); const title = Vue.computed(() => { const slug = store.accountsModal.providerSlug; if (!slug) return '全部账号'; const p = store.providers.find(x => x.slug === slug); return (p ? p.name : slug) + ' 的账号'; }); const accountList = Vue.computed(() => { const slug = store.accountsModal.providerSlug; return slug ? store.accounts.filter(a => a.platform === slug) : store.accounts; }); // 全部模式下按平台分组,保持 平台→账号→资产 层级 const groups = Vue.computed(() => { const m = new Map(); for (const a of accountList.value) { const key = a.platform || ''; if (!m.has(key)) m.set(key, []); m.get(key).push(a); } return [...m.entries()].map(([platform, accounts]) => ({ platform, accounts })); }); function platformName(slug) { const p = store.providers.find(x => x.slug === slug); return p ? p.name : slug; } function assetsOf(a) { return store.assets.filter(x => x.account_id === a.id); } function toggle(id) { expanded[id] = !expanded[id]; } // 账号所属平台配了 sdk_type 且账号有 API 配置时,提供测试/同步 function providerOf(a) { return store.providers.find(p => p.slug === a.platform) || store.providers.find(p => p.name === a.platform); } function canSync(a) { const p = providerOf(a); return !!(a.has_api_config && p && p.sdk_type); } async function testAcct(a) { try { const r = await Api.post('/accounts/' + a.id + '/test', {}); alert(r.ok ? '✅ ' + r.message : '❌ ' + r.message); } catch (e) { alert('测试失败:' + e.message); } } async function syncAcct(a) { try { const r = await Api.post('/accounts/' + a.id + '/sync', {}); const lines = ['同步完成']; if (r.vps) lines.push('VPS:新增 ' + r.vps.created + ' / 更新 ' + r.vps.updated); if (r.domains) lines.push('域名:新增 ' + r.domains.created + ' / 更新 ' + r.domains.updated); if (r.vps_error) lines.push('VPS 错误:' + r.vps_error); if (r.domains_error) lines.push('域名错误:' + r.domains_error); alert(lines.join('\n')); await Promise.all([loadAccounts(), loadAssets()]); } catch (e) { alert('同步失败:' + e.message); } } // 每次打开重置展开状态 Vue.watch(() => store.accountsModal.show, (show) => { if (show) for (const k of Object.keys(expanded)) delete expanded[k]; }); // 查看密码:拉取明文并展示,8 秒后自动关闭;关闭时清除定时器 async function viewPwd(a) { try { const r = await Api.get('/accounts/' + a.id + '/password'); pwd.name = a.remark || a.name; pwd.login_user = r.login_user || ''; pwd.password = r.password; pwd.copied = false; pwd.show = true; if (pwd._timer) clearInterval(pwd._timer); pwd.countdown = 8; pwd._timer = setInterval(() => { pwd.countdown--; if (pwd.countdown <= 0) { pwd.show = false; clearInterval(pwd._timer); pwd._timer = null; } }, 1000); } catch (e) { alert('获取密码失败:' + e.message); } } function copyPwd() { const done = () => { pwd.copied = true; setTimeout(() => { pwd.copied = false; }, 1500); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(pwd.password).then(done).catch(() => fallbackCopy()); } else { fallbackCopy(); } function fallbackCopy() { const ta = document.createElement('textarea'); ta.value = pwd.password; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); done(); } catch (e) {} document.body.removeChild(ta); } } // 关闭弹窗时清定时器,避免泄漏 Vue.watch(() => pwd.show, (v) => { if (!v && pwd._timer) { clearInterval(pwd._timer); pwd._timer = null; } }); return { store, Fmt, expanded, pwd, singleProvider, title, accountList, groups, platformName, assetsOf, toggle, canSync, testAcct, syncAcct, viewPwd, copyPwd, add: () => openAccountCreate(store.accountsModal.providerSlug || ''), edit: openAccountEdit, del: deleteAccount, }; }, };