337 lines
30 KiB
JavaScript
337 lines
30 KiB
JavaScript
/* 资产编辑模态框 */
|
||
const AssetModal = {
|
||
template: `
|
||
<div v-if="store.assetModal.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||
<div class="bg-white dark:bg-slate-900 w-full md:max-w-lg md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
|
||
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center sticky top-0 bg-white dark:bg-slate-900">
|
||
<h3 class="font-semibold text-sm">{{ store.assetModal.editing ? '编辑资产' : '新增资产' }}</h3>
|
||
<button @click="store.assetModal.show=false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||
</div>
|
||
<form @submit.prevent="save" class="px-5 py-4 space-y-3" v-if="f">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-500">名称 *</span>
|
||
<input v-model="f.name" required 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>
|
||
<select v-model="f.asset_type" 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">
|
||
<option v-for="(l,k) in Fmt.TYPE_LABELS" :key="k" :value="k">{{ l }}</option>
|
||
</select></label>
|
||
<label class="block"><span class="text-xs text-slate-500">所属平台</span>
|
||
<select v-model="f.provider_id" @change="onProviderChange" 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">
|
||
<option :value="null">未指定</option>
|
||
<option v-for="p in providersForType" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||
</select></label>
|
||
<label class="block"><span class="text-xs text-slate-500">到期时间</span>
|
||
<input type="date" v-model="f.expiry_date" 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>
|
||
<select v-model="f.renewal_cycle" 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">
|
||
<option value="">未设定</option><option v-for="(l,k) in Fmt.CYCLE_LABELS" :key="k" :value="k">{{ l }}</option>
|
||
</select></label>
|
||
<label class="block"><span class="text-xs text-slate-500">费用</span>
|
||
<input type="number" step="0.01" v-model.number="f.cost" 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>
|
||
<select v-model="f.currency" 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">
|
||
<option>USD</option><option>CNY</option><option>JPY</option><option>EUR</option>
|
||
</select></label>
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-500">续费网址</span>
|
||
<input v-model="f.renew_url" placeholder="https://…(去哪续费)" 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="col-span-2 block"><span class="text-xs text-slate-500">取消订阅网址</span>
|
||
<input v-model="f.cancel_url" placeholder="https://…(去哪取消)" 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>
|
||
<select v-model="f.status" 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">
|
||
<option v-for="(l,k) in Fmt.STATUS_LABELS" :key="k" :value="k">{{ l }}</option>
|
||
</select></label>
|
||
<label class="block"><span class="text-xs text-slate-500">所属账号</span>
|
||
<input v-model="f.account" list="account-options" placeholder="在「平台」页管理账号" 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="account-options">
|
||
<option v-for="a in store.accounts" :key="a.id" :value="a.name">{{ accountLabel(a) }}</option>
|
||
</datalist></label>
|
||
<div class="col-span-2 flex gap-5 text-sm text-slate-600 dark:text-slate-300">
|
||
<label class="flex items-center gap-2"><input type="checkbox" v-model="f.auto_renew"> 自动续费</label>
|
||
<label class="flex items-center gap-2"><input type="checkbox" v-model="f.is_archived"> 已归档</label>
|
||
</div>
|
||
</div>
|
||
|
||
<fieldset v-if="f.asset_type==='vps'" class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
||
<legend class="text-xs font-medium text-slate-600 dark:text-slate-300 px-1">VPS 详情</legend>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<label class="block"><span class="text-xs text-slate-400">公网 IP *</span><input v-model="f.vps_detail.ip_address" required class="mt-1 w-full px-2 py-1.5 rounded 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-400">Tailscale IP</span><input v-model="f.vps_detail.tailscale_ip" class="mt-1 w-full px-2 py-1.5 rounded 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-400">区域</span><input v-model="f.vps_detail.region" class="mt-1 w-full px-2 py-1.5 rounded 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-400">系统</span><input v-model="f.vps_detail.os" class="mt-1 w-full px-2 py-1.5 rounded 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-400">CPU核</span><input type="number" v-model.number="f.vps_detail.cpu_cores" class="mt-1 w-full px-2 py-1.5 rounded 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-400">内存GB</span><input type="number" step="0.5" v-model.number="f.vps_detail.memory_gb" class="mt-1 w-full px-2 py-1.5 rounded 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-400">磁盘GB</span><input type="number" v-model.number="f.vps_detail.disk_gb" class="mt-1 w-full px-2 py-1.5 rounded 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-400">SSH端口</span><input type="number" v-model.number="f.vps_detail.ssh_port" class="mt-1 w-full px-2 py-1.5 rounded 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-400">SSH用户</span><input v-model="f.vps_detail.ssh_user" placeholder="root" class="mt-1 w-full px-2 py-1.5 rounded 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-400">登录方式</span><select v-model="f.vps_detail.login_method" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"><option value="key">密钥</option><option value="password">密码</option><option value="other">其他</option></select></label>
|
||
<label class="block"><span class="text-xs text-slate-400">SSH密钥</span><input v-model="f.vps_detail.ssh_key" type="password" :placeholder="store.assetModal.editing ? '留空不修改' : ''" class="mt-1 w-full px-2 py-1.5 rounded 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-400">密码</span><input v-model="f.vps_detail.password" type="password" :placeholder="store.assetModal.editing ? '留空不修改' : ''" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-400">用途/项目</span><input v-model="f.vps_detail.purpose" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<fieldset v-if="f.asset_type==='domain'" class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
||
<legend class="text-xs font-medium text-slate-600 dark:text-slate-300 px-1">域名详情</legend>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-400">域名 *</span><input v-model="f.domain_detail.domain_name" required class="mt-1 w-full px-2 py-1.5 rounded 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-400">注册商</span><input v-model="f.domain_detail.registrar" class="mt-1 w-full px-2 py-1.5 rounded 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-400">DNS托管</span><input v-model="f.domain_detail.dns_provider" class="mt-1 w-full px-2 py-1.5 rounded 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-400">CF账号</span><input v-model="f.domain_detail.cloudflare_account" class="mt-1 w-full px-2 py-1.5 rounded 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-400">绑定资产ID</span><input type="number" v-model.number="f.domain_detail.bind_asset_id" placeholder="关联VPS的ID" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
<label class="col-span-2 flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300"><input type="checkbox" v-model="f.domain_detail.is_using"> 仍在实际使用</label>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<fieldset v-if="f.asset_type==='ai_agent'" class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
||
<legend class="text-xs font-medium text-slate-600 dark:text-slate-300 px-1">AI 账号详情</legend>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<label class="block"><span class="text-xs text-slate-400">服务商 *</span><input v-model="f.ai_detail.provider" required class="mt-1 w-full px-2 py-1.5 rounded 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-400">计划</span><input v-model="f.ai_detail.plan" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-400">API Key</span><input v-model="f.ai_detail.api_key" type="password" class="mt-1 w-full px-2 py-1.5 rounded 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-400">余额</span><input type="number" step="0.01" v-model.number="f.ai_detail.balance" class="mt-1 w-full px-2 py-1.5 rounded 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-400">月限额</span><input type="number" step="0.01" v-model.number="f.ai_detail.monthly_limit" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<fieldset v-if="f.asset_type==='cloudflare'" class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
||
<legend class="text-xs font-medium text-slate-600 dark:text-slate-300 px-1">Cloudflare 子资产详情</legend>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<label class="block"><span class="text-xs text-slate-400">账号邮箱</span><input v-model="f.cloudflare_detail.account_email" placeholder="账号邮箱(区分多账号)" class="mt-1 w-full px-2 py-1.5 rounded 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-400">子资产类型</span><select v-model="f.cloudflare_detail.sub_type" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"><option value="zone">zone(解析)</option><option value="worker">Worker</option><option value="r2">R2</option><option value="tunnel">Tunnel</option><option value="mail">Mail</option><option value="dns_record">DNS记录</option><option value="other">其他</option></select></label>
|
||
<label class="block"><span class="text-xs text-slate-400">子资产名称</span><input v-model="f.cloudflare_detail.sub_name" placeholder="如 worker 名 / bucket 名" class="mt-1 w-full px-2 py-1.5 rounded 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-400">关联 zone/域名</span><input v-model="f.cloudflare_detail.zone_name" placeholder="example.com" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-400">状态</span><input v-model="f.cloudflare_detail.status" placeholder="active / paused 等" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<label class="block"><span class="text-xs text-slate-500">备注</span>
|
||
<textarea v-model="f.remark" rows="2" 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"></textarea></label>
|
||
|
||
<div class="flex justify-end gap-2 pt-1">
|
||
<button type="button" @click="store.assetModal.show=false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-sm text-slate-600 dark:text-slate-300">取消</button>
|
||
<button type="submit" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">{{ store.assetModal.editing ? '保存' : '创建' }}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>`,
|
||
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;
|
||
}
|
||
// 账号候选显示:说明优先,其次平台名(platform 存的是 slug,回退原值)
|
||
function accountLabel(a) {
|
||
if (a.remark) return a.remark + ' · ' + a.name;
|
||
const p = store.providers.find(x => x.slug === a.platform);
|
||
return p ? a.name + '(' + p.name + ')' : a.name;
|
||
}
|
||
return { store, Fmt, f, providersForType, onProviderChange, accountLabel, save: saveAsset };
|
||
},
|
||
};
|
||
|
||
/* 平台编辑模态框 */
|
||
const ProviderModal = {
|
||
template: `
|
||
<div v-if="store.providerModal.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||
<div class="bg-white dark:bg-slate-900 w-full md:max-w-md md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
|
||
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
|
||
<h3 class="font-semibold text-sm">{{ store.providerModal.editing ? '编辑平台' : '新增平台' }}</h3>
|
||
<button @click="store.providerModal.show=false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||
</div>
|
||
<form @submit.prevent="save" class="px-5 py-4 space-y-3" v-if="f">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<label class="block"><span class="text-xs text-slate-500">标识 slug *</span><input v-model="f.slug" required placeholder="aliyun" 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><input v-model="f.name" required 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="col-span-2 block"><span class="text-xs text-slate-500">分类</span><select v-model="f.category" 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"><option v-for="(l,k) in Fmt.CATEGORY_LABELS" :key="k" :value="k">{{ l }}</option></select></label>
|
||
<div class="col-span-2 block"><span class="text-xs text-slate-500">提供服务(可多选,综合平台全选)</span>
|
||
<div class="flex flex-wrap gap-x-3 gap-y-1 mt-1">
|
||
<label v-for="(l,k) in Fmt.SERVICES_LABELS" :key="k" class="flex items-center gap-1 text-xs text-slate-600 dark:text-slate-300"><input type="checkbox" :checked="servicesSet.has(k)" @change="toggleService(k)"> {{ l }}</label>
|
||
</div>
|
||
</div>
|
||
<label class="block"><span class="text-xs text-slate-500">SDK类型</span><select v-model="f.sdk_type" 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"><option value="">未指定</option><option v-for="o in sdkOptions" :key="o.type" :value="o.type">{{ o.type }}</option></select></label>
|
||
<label class="col-span-2 block"><span class="text-xs text-slate-500">官网</span><input v-model="f.website" 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="col-span-2 block"><span class="text-xs text-slate-500">管理面板URL</span><input v-model="f.console_url" 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 v-if="currentFields.length" class="col-span-2 border border-slate-200 dark:border-slate-700 rounded-lg p-3 space-y-2">
|
||
<div class="text-xs font-medium text-slate-600 dark:text-slate-300">API 凭证(加密存储)</div>
|
||
<label v-for="field in currentFields" :key="field" class="block"><span class="text-xs text-slate-400">{{ field }}</span><input v-model="apiFields[field]" type="password" :placeholder="store.providerModal.editing ? '留空不修改' : '填写 ' + field" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||
</div>
|
||
<div v-else-if="f.sdk_type" class="col-span-2 text-xs text-amber-600 dark:text-amber-400">该 SDK 类型暂无预设凭证字段(可后续扩展)</div>
|
||
<label class="col-span-2 flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300"><input type="checkbox" v-model="f.enabled"> 启用</label>
|
||
</div>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" @click="store.providerModal.show=false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-sm text-slate-600 dark:text-slate-300">取消</button>
|
||
<button type="submit" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>`,
|
||
setup() {
|
||
const f = Vue.computed(() => store.providerModal.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 */ }
|
||
});
|
||
const sdkOptions = Vue.computed(() => {
|
||
const opts = [];
|
||
for (const m of adapterMeta.value) for (const t of (m.sdk_types || [])) opts.push({ type: t, fields: m.required_config || [] });
|
||
return opts;
|
||
});
|
||
const currentFields = Vue.computed(() => {
|
||
const opt = sdkOptions.value.find(o => o.type === (f.value && f.value.sdk_type));
|
||
return opt ? opt.fields : [];
|
||
});
|
||
// 提供服务多选: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(',');
|
||
}
|
||
// 模态框每次打开时清空上次残留的凭证输入,避免串号
|
||
Vue.watch(() => store.providerModal.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];
|
||
if (Object.keys(cfg).length) f.value.api_config = JSON.stringify(cfg);
|
||
saveProvider();
|
||
}
|
||
return { store, Fmt, f, sdkOptions, currentFields, servicesSet, toggleService, apiFields, save };
|
||
},
|
||
};
|
||
|
||
/* 账号编辑模态框(z-[60]:可能叠加在账号查看弹窗 z-50 之上打开,需更高层级) */
|
||
const AccountModal = {
|
||
template: `
|
||
<div v-if="store.accountModal.show" class="fixed inset-0 bg-black/40 z-[60] flex items-end md:items-center justify-center p-0 md:p-4">
|
||
<div class="bg-white dark:bg-slate-900 w-full md:max-w-md md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
|
||
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
|
||
<h3 class="font-semibold text-sm">{{ store.accountModal.editing ? '编辑账号' : '新增账号' }}</h3>
|
||
<button @click="store.accountModal.show=false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||
</div>
|
||
<form @submit.prevent="save" class="px-5 py-4 space-y-3" v-if="f">
|
||
<label class="block"><span class="text-xs text-slate-500">账号说明(这个账号是干嘛的)</span>
|
||
<input v-model="f.remark" placeholder="如:XX公司代管账号" 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>
|
||
<input v-model="f.name" required placeholder="如 you@example.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>
|
||
<label class="block"><span class="text-xs text-slate-500">所属平台</span>
|
||
<select v-model="f.platform" 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">
|
||
<option value="">未指定</option>
|
||
<option v-for="p in store.providers" :key="p.id" :value="p.slug">{{ p.name }}</option>
|
||
</select></label>
|
||
<p v-if="store.accountModal.editing" class="text-xs text-slate-400">修改账号标识后,引用该账号的资产会自动同步更新。</p>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" @click="store.accountModal.show=false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-sm text-slate-600 dark:text-slate-300">取消</button>
|
||
<button type="submit" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>`,
|
||
setup() {
|
||
const f = Vue.computed(() => store.accountModal.form);
|
||
return { store, f, save: saveAccount };
|
||
},
|
||
};
|
||
|
||
/* 账号查看弹窗:平台 → 账号 → 资产 三层结构,展开账号看名下资产 */
|
||
const AccountsViewModal = {
|
||
template: `
|
||
<div v-if="store.accountsModal.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||
<div class="bg-white dark:bg-slate-900 w-full md:max-w-lg md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
|
||
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center sticky top-0 bg-white dark:bg-slate-900">
|
||
<h3 class="font-semibold text-sm">👤 {{ title }}({{ accountList.length }})</h3>
|
||
<div class="flex items-center gap-3">
|
||
<button @click="add" class="text-xs text-blue-600 dark:text-blue-400">+ 新增账号</button>
|
||
<button @click="store.accountsModal.show=false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-4">
|
||
<p v-if="!accountList.length" class="text-xs text-slate-400">暂无账号。一个平台可有多个账号(如代管/公司账号),添加后可在资产表单「所属账号」中选择。</p>
|
||
<div v-else class="space-y-4">
|
||
<div v-for="g in groups" :key="g.platform">
|
||
<div v-if="!singleProvider" class="text-xs font-medium text-slate-400 mb-1">{{ g.platform ? platformName(g.platform) : '未指定平台' }}</div>
|
||
<div class="divide-y divide-slate-100 dark:divide-slate-800 border border-slate-100 dark:border-slate-800 rounded-lg">
|
||
<div v-for="a in g.accounts" :key="a.id">
|
||
<div class="px-3 py-2 flex items-center gap-2">
|
||
<button @click="toggle(a.id)" class="text-slate-400 text-xs w-4">{{ expanded[a.id] ? '▾' : '▸' }}</button>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="text-sm truncate">{{ a.remark || a.name }}</div>
|
||
<div class="text-xs text-slate-400 truncate"><span v-if="a.remark">{{ a.name }} · </span>关联 {{ assetsOf(a).length }} 个资产</div>
|
||
</div>
|
||
<button @click="edit(a)" class="text-xs text-blue-600 dark:text-blue-400">编辑</button>
|
||
<button @click="del(a)" class="text-xs text-red-600 dark:text-red-400">删除</button>
|
||
</div>
|
||
<div v-if="expanded[a.id]" class="px-3 pb-2 pl-9 space-y-1">
|
||
<p v-if="!assetsOf(a).length" class="text-xs text-slate-400">该账号下暂无资产(编辑资产时在「所属账号」选择 {{ a.name }} 即可关联)</p>
|
||
<div v-for="asset in assetsOf(a)" :key="asset.id" class="text-xs flex items-center gap-2">
|
||
<span class="px-1.5 py-0.5 rounded-full shrink-0" :class="Fmt.typeBadge(asset.asset_type)">{{ Fmt.TYPE_LABELS[asset.asset_type] }}</span>
|
||
<span class="truncate">{{ asset.name }}</span>
|
||
<span v-if="asset.expiry_date" class="text-slate-400 shrink-0">{{ asset.expiry_date }} 到期</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`,
|
||
setup() {
|
||
const expanded = Vue.reactive({});
|
||
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 === a.name);
|
||
}
|
||
function toggle(id) { expanded[id] = !expanded[id]; }
|
||
// 每次打开重置展开状态
|
||
Vue.watch(() => store.accountsModal.show, (show) => {
|
||
if (show) for (const k of Object.keys(expanded)) delete expanded[k];
|
||
});
|
||
return {
|
||
store, Fmt, expanded, singleProvider, title, accountList, groups,
|
||
platformName, assetsOf, toggle,
|
||
add: () => openAccountCreate(store.accountsModal.providerSlug || ''),
|
||
edit: openAccountEdit, del: deleteAccount,
|
||
};
|
||
},
|
||
};
|