refactor(ui): 账号改为平台→账号→资产三层交互(平台卡片入口+展开看名下资产,移除平铺账号区)
This commit is contained in:
@@ -84,6 +84,7 @@ const AppRoot = {
|
|||||||
<asset-modal></asset-modal>
|
<asset-modal></asset-modal>
|
||||||
<provider-modal></provider-modal>
|
<provider-modal></provider-modal>
|
||||||
<account-modal></account-modal>
|
<account-modal></account-modal>
|
||||||
|
<accounts-view-modal></accounts-view-modal>
|
||||||
</div>`,
|
</div>`,
|
||||||
setup() {
|
setup() {
|
||||||
const appName = Api.CFG.appName;
|
const appName = Api.CFG.appName;
|
||||||
@@ -126,6 +127,7 @@ app.component('settings-view', SettingsView);
|
|||||||
app.component('asset-modal', AssetModal);
|
app.component('asset-modal', AssetModal);
|
||||||
app.component('provider-modal', ProviderModal);
|
app.component('provider-modal', ProviderModal);
|
||||||
app.component('account-modal', AccountModal);
|
app.component('account-modal', AccountModal);
|
||||||
|
app.component('accounts-view-modal', AccountsViewModal);
|
||||||
applyDark();
|
applyDark();
|
||||||
initRouter();
|
initRouter();
|
||||||
loadAll();
|
loadAll();
|
||||||
|
|||||||
@@ -247,3 +247,90 @@ const AccountModal = {
|
|||||||
return { store, f, save: saveAccount };
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const store = reactive({
|
|||||||
assetModal: { show: false, editing: null, form: null },
|
assetModal: { show: false, editing: null, form: null },
|
||||||
providerModal: { show: false, editing: null, form: null },
|
providerModal: { show: false, editing: null, form: null },
|
||||||
accountModal: { show: false, editing: null, form: null },
|
accountModal: { show: false, editing: null, form: null },
|
||||||
|
accountsModal: { show: false, providerSlug: null }, // 账号查看弹窗:providerSlug 为 null 时看全部
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyDark() {
|
function applyDark() {
|
||||||
@@ -166,6 +167,9 @@ async function deleteProvider(p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 账号操作 ---------------- */
|
/* ---------------- 账号操作 ---------------- */
|
||||||
|
function openAccountsView(providerSlug) {
|
||||||
|
store.accountsModal = { show: true, providerSlug: providerSlug || null };
|
||||||
|
}
|
||||||
function openAccountCreate(platform) {
|
function openAccountCreate(platform) {
|
||||||
store.accountModal = { show: true, editing: null, form: { name: '', platform: platform || '', remark: '' } };
|
store.accountModal = { show: true, editing: null, form: { name: '', platform: platform || '', remark: '' } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,28 +9,7 @@ const ProvidersView = {
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="seed" class="text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800">初始化预设平台</button>
|
<button @click="seed" class="text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800">初始化预设平台</button>
|
||||||
<button @click="create" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700">+ 自定义平台</button>
|
<button @click="create" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700">+ 自定义平台</button>
|
||||||
<button @click="addAccount" class="text-sm px-3 py-1.5 rounded-lg border border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20">+ 账号</button>
|
<button @click="manageAccounts" class="text-sm px-3 py-1.5 rounded-lg border border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20">👤 账号管理</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 账号管理:同一平台多账号时区分资产归属,资产表单「所属账号」引用此处 -->
|
|
||||||
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<h3 class="font-semibold text-sm">👤 我的账号({{ store.accounts.length }})</h3>
|
|
||||||
<button @click="addAccount" class="text-xs text-blue-600 dark:text-blue-400">+ 新增账号</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="!store.accounts.length" class="text-xs text-slate-400">暂无账号。添加后,可在资产表单「所属账号」中快速选择,区分同一平台下的多个账号。</p>
|
|
||||||
<div v-else class="divide-y divide-slate-100 dark:divide-slate-800">
|
|
||||||
<div v-for="a in store.accounts" :key="a.id" class="py-2 flex items-center gap-2">
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="text-sm truncate">{{ a.remark || a.name }}
|
|
||||||
<span v-if="a.platform" class="text-xs text-slate-400">@ {{ platformName(a.platform) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-slate-400 truncate"><span v-if="a.remark">{{ a.name }} · </span>关联 {{ a.asset_count }} 个资产</div>
|
|
||||||
</div>
|
|
||||||
<button @click="editAccount(a)" class="text-xs text-blue-600 dark:text-blue-400">编辑</button>
|
|
||||||
<button @click="delAccount(a)" class="text-xs text-red-600 dark:text-red-400">删除</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
<div v-for="p in store.providers" :key="p.id" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
<div v-for="p in store.providers" :key="p.id" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
||||||
@@ -49,6 +28,7 @@ const ProvidersView = {
|
|||||||
<div v-if="p.console_url"><a :href="p.console_url" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">管理面板 ↗</a></div>
|
<div v-if="p.console_url"><a :href="p.console_url" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">管理面板 ↗</a></div>
|
||||||
<div class="flex items-center gap-2 mt-1">
|
<div class="flex items-center gap-2 mt-1">
|
||||||
<span class="text-slate-500">📎 关联 {{ countOf(p) }} 个资产</span>
|
<span class="text-slate-500">📎 关联 {{ countOf(p) }} 个资产</span>
|
||||||
|
<button @click="openAccounts(p)" class="text-slate-500 hover:text-blue-600 dark:hover:text-blue-400">👤 账号 {{ accountsCountOf(p) }}</button>
|
||||||
<span v-if="p.has_api_config" class="text-emerald-600 dark:text-emerald-400">已配置 API</span>
|
<span v-if="p.has_api_config" class="text-emerald-600 dark:text-emerald-400">已配置 API</span>
|
||||||
<span v-else class="text-slate-400">未配置 API</span>
|
<span v-else class="text-slate-400">未配置 API</span>
|
||||||
<span :class="p.enabled ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-400'">{{ p.enabled ? '启用' : '停用' }}</span>
|
<span :class="p.enabled ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-400'">{{ p.enabled ? '启用' : '停用' }}</span>
|
||||||
@@ -83,14 +63,13 @@ const ProvidersView = {
|
|||||||
function countOf(p) {
|
function countOf(p) {
|
||||||
return (assetCounts.value['id:' + p.id] || 0) + (assetCounts.value['p:' + p.slug] || 0) + (assetCounts.value['p:' + p.name] || 0);
|
return (assetCounts.value['id:' + p.id] || 0) + (assetCounts.value['p:' + p.slug] || 0) + (assetCounts.value['p:' + p.name] || 0);
|
||||||
}
|
}
|
||||||
|
// 平台名下账号数(账号按 platform slug 归属平台)
|
||||||
|
function accountsCountOf(p) {
|
||||||
|
return store.accounts.filter(a => a.platform === p.slug).length;
|
||||||
|
}
|
||||||
function serviceList(p) {
|
function serviceList(p) {
|
||||||
return (p.services || '').split(',').map(s => s.trim()).filter(Boolean);
|
return (p.services || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
// 账号所属平台显示名:platform 存的是 slug,回退显示原值
|
|
||||||
function platformName(slug) {
|
|
||||||
const p = store.providers.find(x => x.slug === slug);
|
|
||||||
return p ? p.name : slug;
|
|
||||||
}
|
|
||||||
Vue.onMounted(async () => {
|
Vue.onMounted(async () => {
|
||||||
try { const r = await Api.get('/providers/adapters'); supported.value = r.supported_types || []; capabilities.value = r.capabilities || {}; } catch (e) { /* ignore */ }
|
try { const r = await Api.get('/providers/adapters'); supported.value = r.supported_types || []; capabilities.value = r.capabilities || {}; } catch (e) { /* ignore */ }
|
||||||
});
|
});
|
||||||
@@ -123,6 +102,6 @@ const ProvidersView = {
|
|||||||
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
|
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
|
||||||
finally { syncing[p.id] = false; }
|
finally { syncing[p.id] = false; }
|
||||||
}
|
}
|
||||||
return { store, Fmt, testing, syncing, results, countOf, serviceList, platformName, isSupported, canSync, fmtTime, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider, addAccount: () => openAccountCreate(''), editAccount: openAccountEdit, delAccount: deleteAccount };
|
return { store, Fmt, testing, syncing, results, countOf, accountsCountOf, serviceList, isSupported, canSync, fmtTime, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider, openAccounts: (p) => openAccountsView(p.slug), manageAccounts: () => openAccountsView(null) };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user