208 lines
22 KiB
JavaScript
208 lines
22 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" 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 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;
|
|
}
|
|
return { store, Fmt, f, providersForType, onProviderChange, 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 };
|
|
},
|
|
};
|