feat: add profile management page with full CRUD UI

- Add frontend API layer, Pinia store, and 5 components (ProfileCard, ProfilesPanel, ProfileCreateModal, ProfileRenameModal, ProfileImportModal)
- Add ProfilesView page with card grid layout and expandable details
- Modify export endpoint to stream file as browser download instead of returning server path
- Add sidebar nav entry, router route, and i18n translations (en/zh)
- Support create, rename, delete, switch (with gateway restart), export, and import profiles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-16 09:40:25 +08:00
parent 57ef171dda
commit 04b80a616e
13 changed files with 1081 additions and 4 deletions
@@ -0,0 +1,96 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import * as profilesApi from '@/api/hermes/profiles'
import type { HermesProfile, HermesProfileDetail } from '@/api/hermes/profiles'
export const useProfilesStore = defineStore('profiles', () => {
const profiles = ref<HermesProfile[]>([])
const activeProfile = ref<HermesProfile | null>(null)
const detailMap = ref<Record<string, HermesProfileDetail>>({})
const loading = ref(false)
const switching = ref(false)
async function fetchProfiles() {
loading.value = true
try {
profiles.value = await profilesApi.fetchProfiles()
activeProfile.value = profiles.value.find(p => p.active) ?? null
} catch (err) {
console.error('Failed to fetch profiles:', err)
} finally {
loading.value = false
}
}
async function fetchProfileDetail(name: string) {
if (detailMap.value[name]) return detailMap.value[name]
try {
const detail = await profilesApi.fetchProfileDetail(name)
detailMap.value[name] = detail
return detail
} catch {
return null
}
}
async function createProfile(name: string, clone?: boolean) {
const ok = await profilesApi.createProfile(name, clone)
if (ok) await fetchProfiles()
return ok
}
async function deleteProfile(name: string) {
const ok = await profilesApi.deleteProfile(name)
if (ok) {
delete detailMap.value[name]
await fetchProfiles()
}
return ok
}
async function renameProfile(name: string, newName: string) {
const ok = await profilesApi.renameProfile(name, newName)
if (ok) {
delete detailMap.value[name]
await fetchProfiles()
}
return ok
}
async function switchProfile(name: string) {
switching.value = true
try {
const ok = await profilesApi.switchProfile(name)
if (ok) await fetchProfiles()
return ok
} finally {
switching.value = false
}
}
async function exportProfile(name: string) {
return profilesApi.exportProfile(name)
}
async function importProfile(archive: string, name?: string) {
const ok = await profilesApi.importProfile(archive, name)
if (ok) await fetchProfiles()
return ok
}
return {
profiles,
activeProfile,
detailMap,
loading,
switching,
fetchProfiles,
fetchProfileDetail,
createProfile,
deleteProfile,
renameProfile,
switchProfile,
exportProfile,
importProfile,
}
})