feat: add account center
This commit is contained in:
73
src/App.vue
73
src/App.vue
@@ -1,11 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import {
|
|
||||||
loadGitHubPages,
|
|
||||||
loadGitHubSettings,
|
|
||||||
saveGitHubPage,
|
|
||||||
saveGitHubSettings,
|
|
||||||
} from "./api/githubSite";
|
|
||||||
import { loadCurrentUser, loginWithEmail, logoutCms } from "./api/auth";
|
import { loadCurrentUser, loginWithEmail, logoutCms } from "./api/auth";
|
||||||
import {
|
import {
|
||||||
createGitHubRepository,
|
createGitHubRepository,
|
||||||
@@ -14,12 +8,19 @@ import {
|
|||||||
logoutGitHub,
|
logoutGitHub,
|
||||||
startGitHubLogin,
|
startGitHubLogin,
|
||||||
} from "./api/githubAuth";
|
} from "./api/githubAuth";
|
||||||
|
import {
|
||||||
|
loadGitHubPages,
|
||||||
|
loadGitHubSettings,
|
||||||
|
saveGitHubPage,
|
||||||
|
saveGitHubSettings,
|
||||||
|
} from "./api/githubSite";
|
||||||
import {
|
import {
|
||||||
loadLocalPages,
|
loadLocalPages,
|
||||||
loadLocalSettings,
|
loadLocalSettings,
|
||||||
saveLocalPage,
|
saveLocalPage,
|
||||||
saveLocalSettings,
|
saveLocalSettings,
|
||||||
} from "./api/localSite";
|
} from "./api/localSite";
|
||||||
|
import AccountPanel from "./components/AccountPanel.vue";
|
||||||
import AppSidebar from "./components/AppSidebar.vue";
|
import AppSidebar from "./components/AppSidebar.vue";
|
||||||
import ConfigPanel from "./components/ConfigPanel.vue";
|
import ConfigPanel from "./components/ConfigPanel.vue";
|
||||||
import ConnectPanel from "./components/ConnectPanel.vue";
|
import ConnectPanel from "./components/ConnectPanel.vue";
|
||||||
@@ -31,6 +32,7 @@ import { mockNavItems, mockPages, mockSidebarGroups, mockSiteSettings } from "./
|
|||||||
import type { CmsUser, DataSource, DocPage, GitHubConnection, GitHubRepository, GitHubUser, PanelKey } from "./types";
|
import type { CmsUser, DataSource, DocPage, GitHubConnection, GitHubRepository, GitHubUser, PanelKey } from "./types";
|
||||||
|
|
||||||
const panels: Array<{ key: PanelKey; label: string; icon: string }> = [
|
const panels: Array<{ key: PanelKey; label: string; icon: string }> = [
|
||||||
|
{ key: "account", label: "用户中心", icon: "U" },
|
||||||
{ key: "connect", label: "仓库连接", icon: "G" },
|
{ key: "connect", label: "仓库连接", icon: "G" },
|
||||||
{ key: "content", label: "页面管理", icon: "P" },
|
{ key: "content", label: "页面管理", icon: "P" },
|
||||||
{ key: "structure", label: "站点结构", icon: "S" },
|
{ key: "structure", label: "站点结构", icon: "S" },
|
||||||
@@ -78,7 +80,6 @@ const repoLabel = computed(() => {
|
|||||||
if (dataSource.value === "github" && hasGitHubConnection.value) {
|
if (dataSource.value === "github" && hasGitHubConnection.value) {
|
||||||
return `${githubConnection.owner}/${githubConnection.repo}`;
|
return `${githubConnection.owner}/${githubConnection.repo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "test-vitepress";
|
return "test-vitepress";
|
||||||
});
|
});
|
||||||
const repoBranch = computed(() =>
|
const repoBranch = computed(() =>
|
||||||
@@ -110,17 +111,12 @@ function replacePages(nextPages: DocPage[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPagesForSource(source: DataSource, connection = githubConnection) {
|
async function loadPagesForSource(source: DataSource, connection = githubConnection) {
|
||||||
if (source === "github") {
|
return source === "github" ? loadGitHubPages(connection) : loadLocalPages();
|
||||||
return loadGitHubPages(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadLocalPages();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettingsForSource(source: DataSource, connection = githubConnection) {
|
async function loadSettingsForSource(source: DataSource, connection = githubConnection) {
|
||||||
if (source === "github") {
|
if (source === "github") {
|
||||||
const result = await loadGitHubSettings(connection);
|
return loadGitHubSettings(connection);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -173,6 +169,22 @@ async function reloadSite() {
|
|||||||
await Promise.all([loadPages(), loadSettings()]);
|
await Promise.all([loadPages(), loadSettings()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshRepositories() {
|
||||||
|
isLoadingRepos.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
repositories.value = await loadGitHubRepositories();
|
||||||
|
activityItems.value = [`已读取 ${repositories.value.length} 个 GitHub 仓库`, ...activityItems.value];
|
||||||
|
} catch (error) {
|
||||||
|
activityItems.value = [
|
||||||
|
error instanceof Error ? error.message : "读取 GitHub 仓库列表失败",
|
||||||
|
...activityItems.value,
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
isLoadingRepos.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAuthState() {
|
async function loadAuthState() {
|
||||||
try {
|
try {
|
||||||
currentUser.value = await loadCurrentUser();
|
currentUser.value = await loadCurrentUser();
|
||||||
@@ -183,7 +195,7 @@ async function loadAuthState() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
activityItems.value = [
|
activityItems.value = [
|
||||||
error instanceof Error ? error.message : "读取 GitHub 登录状态失败",
|
error instanceof Error ? error.message : "读取登录状态失败",
|
||||||
...activityItems.value,
|
...activityItems.value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -211,22 +223,6 @@ async function handleLogout() {
|
|||||||
repositories.value = [];
|
repositories.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRepositories() {
|
|
||||||
isLoadingRepos.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
repositories.value = await loadGitHubRepositories();
|
|
||||||
activityItems.value = [`已读取 ${repositories.value.length} 个 GitHub 仓库`, ...activityItems.value];
|
|
||||||
} catch (error) {
|
|
||||||
activityItems.value = [
|
|
||||||
error instanceof Error ? error.message : "读取 GitHub 仓库列表失败",
|
|
||||||
...activityItems.value,
|
|
||||||
];
|
|
||||||
} finally {
|
|
||||||
isLoadingRepos.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectGithub(connection: GitHubConnection) {
|
async function connectGithub(connection: GitHubConnection) {
|
||||||
const nextConnection = normalizeConnection(connection);
|
const nextConnection = normalizeConnection(connection);
|
||||||
|
|
||||||
@@ -390,6 +386,7 @@ onMounted(async () => {
|
|||||||
<div v-else id="app-shell">
|
<div v-else id="app-shell">
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
:active-panel="activePanel"
|
:active-panel="activePanel"
|
||||||
|
:current-user="currentUser"
|
||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
:has-git-hub-connection="hasGitHubConnection"
|
:has-git-hub-connection="hasGitHubConnection"
|
||||||
:panels="panels"
|
:panels="panels"
|
||||||
@@ -406,16 +403,22 @@ onMounted(async () => {
|
|||||||
<h2>{{ activePanelTitle }}</h2>
|
<h2>{{ activePanelTitle }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
<button class="ghost-button" type="button" @click="handleLogout">
|
<button class="ghost-button" type="button" @click="activePanel = 'account'">用户中心</button>
|
||||||
退出
|
|
||||||
</button>
|
|
||||||
<button class="ghost-button" type="button" @click="reloadSite">重新读取</button>
|
<button class="ghost-button" type="button" @click="reloadSite">重新读取</button>
|
||||||
<button class="primary-button" type="button" @click="activePanel = 'publish'">发布</button>
|
<button class="primary-button" type="button" @click="activePanel = 'publish'">发布</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<AccountPanel
|
||||||
|
v-if="activePanel === 'account'"
|
||||||
|
:current-user="currentUser"
|
||||||
|
:github-user="githubUser"
|
||||||
|
@github-login="startGitHubLogin"
|
||||||
|
@github-logout="signOutGithub"
|
||||||
|
@logout="handleLogout"
|
||||||
|
/>
|
||||||
<ConnectPanel
|
<ConnectPanel
|
||||||
v-if="activePanel === 'connect'"
|
v-else-if="activePanel === 'connect'"
|
||||||
:connection="githubConnection"
|
:connection="githubConnection"
|
||||||
:data-source="dataSource"
|
:data-source="dataSource"
|
||||||
:github-user="githubUser"
|
:github-user="githubUser"
|
||||||
|
|||||||
61
src/components/AccountPanel.vue
Normal file
61
src/components/AccountPanel.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CmsUser, GitHubUser } from "../types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
currentUser: CmsUser;
|
||||||
|
githubUser: GitHubUser | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
githubLogin: [];
|
||||||
|
githubLogout: [];
|
||||||
|
logout: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="panel is-visible">
|
||||||
|
<div class="account-layout">
|
||||||
|
<section class="management-card">
|
||||||
|
<p class="eyebrow">account</p>
|
||||||
|
<h3>用户中心</h3>
|
||||||
|
<dl class="source-summary account-summary">
|
||||||
|
<div>
|
||||||
|
<dt>名称</dt>
|
||||||
|
<dd>{{ currentUser.name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>邮箱</dt>
|
||||||
|
<dd>{{ currentUser.email || "-" }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>角色</dt>
|
||||||
|
<dd>{{ currentUser.role }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<button class="ghost-button account-logout" type="button" @click="$emit('logout')">
|
||||||
|
退出 CMS 登录
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="management-card">
|
||||||
|
<p class="eyebrow">github</p>
|
||||||
|
<h3>GitHub 绑定</h3>
|
||||||
|
<div v-if="githubUser" class="github-user">
|
||||||
|
<img v-if="githubUser.avatarUrl" :src="githubUser.avatarUrl" alt="" />
|
||||||
|
<div>
|
||||||
|
<strong>{{ githubUser.name || githubUser.login }}</strong>
|
||||||
|
<span>@{{ githubUser.login }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-button" type="button" @click="$emit('githubLogout')">解绑</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="helper-text">绑定 GitHub 后,可以读取仓库列表、选择站点仓库并提交内容变更。</p>
|
||||||
|
<button class="primary-button" type="button" @click="$emit('githubLogin')">
|
||||||
|
绑定 GitHub
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DataSource, PanelKey } from "../types";
|
import type { CmsUser, DataSource, PanelKey } from "../types";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
activePanel: PanelKey;
|
activePanel: PanelKey;
|
||||||
|
currentUser: CmsUser;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
hasGitHubConnection: boolean;
|
hasGitHubConnection: boolean;
|
||||||
panels: Array<{ key: PanelKey; label: string; icon: string }>;
|
panels: Array<{ key: PanelKey; label: string; icon: string }>;
|
||||||
@@ -26,6 +27,14 @@ defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="user-button" type="button" @click="$emit('changePanel', 'account')">
|
||||||
|
<span class="user-avatar">{{ currentUser.name.slice(0, 1).toUpperCase() }}</span>
|
||||||
|
<span>
|
||||||
|
<strong>{{ currentUser.name }}</strong>
|
||||||
|
<small>{{ currentUser.role }}</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="connect-button" type="button" @click="$emit('changePanel', 'connect')">
|
<button class="connect-button" type="button" @click="$emit('changePanel', 'connect')">
|
||||||
<span>{{ hasGitHubConnection ? "GitHub" : dataSource === "github" ? "GitHub" : "Local" }}</span>
|
<span>{{ hasGitHubConnection ? "GitHub" : dataSource === "github" ? "GitHub" : "Local" }}</span>
|
||||||
<strong>{{ hasGitHubConnection ? "已连接仓库" : "连接仓库" }}</strong>
|
<strong>{{ hasGitHubConnection ? "已连接仓库" : "连接仓库" }}</strong>
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connect-button,
|
.connect-button,
|
||||||
|
.user-button,
|
||||||
.primary-button,
|
.primary-button,
|
||||||
.secondary-button,
|
.secondary-button,
|
||||||
.ghost-button,
|
.ghost-button,
|
||||||
@@ -149,6 +150,42 @@ button:disabled {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-button {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 38px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-color: var(--line);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-button strong,
|
||||||
|
.user-button small {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-button small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
display: grid;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.connect-button span {
|
.connect-button span {
|
||||||
color: #bfc7d1;
|
color: #bfc7d1;
|
||||||
}
|
}
|
||||||
@@ -505,6 +542,7 @@ label {
|
|||||||
|
|
||||||
.two-column,
|
.two-column,
|
||||||
.publish-layout,
|
.publish-layout,
|
||||||
|
.account-layout,
|
||||||
.connect-layout {
|
.connect-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -572,6 +610,14 @@ label {
|
|||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-summary {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-logout {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.source-summary {
|
.source-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -700,6 +746,7 @@ label {
|
|||||||
.content-layout,
|
.content-layout,
|
||||||
.two-column,
|
.two-column,
|
||||||
.publish-layout,
|
.publish-layout,
|
||||||
|
.account-layout,
|
||||||
.connect-layout,
|
.connect-layout,
|
||||||
.settings-grid,
|
.settings-grid,
|
||||||
.frontmatter-grid,
|
.frontmatter-grid,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type PanelKey = "connect" | "content" | "structure" | "config" | "publish";
|
export type PanelKey = "account" | "connect" | "content" | "structure" | "config" | "publish";
|
||||||
export type EditorMode = "write" | "preview";
|
export type EditorMode = "write" | "preview";
|
||||||
export type DataSource = "local" | "github";
|
export type DataSource = "local" | "github";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user