rebuild(stage-4): deliver shared shell and account interfaces

This commit is contained in:
leefer
2026-07-30 02:08:20 +08:00
parent d1c658d8ef
commit 3b75bf2d63
47 changed files with 3042 additions and 99 deletions
+53
View File
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useSessionStore } from "../../shared/stores/session";
const router = useRouter();
const session = useSessionStore();
const mode = ref<"login" | "register">("login");
const username = ref("");
const password = ref("");
const errorMessage = ref("");
async function submit(): Promise<void> {
errorMessage.value = "";
try {
await session.authenticate(mode.value, username.value, password.value);
await router.replace("/workspace/emotion");
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : "登录失败,请稍后重试。";
}
}
</script>
<template>
<main class="auth-view">
<section class="auth-panel card">
<header class="auth-brand">
<span class="brand-mark"></span>
<div><h1>小白复盘</h1><p>进入你的复盘工作台</p></div>
</header>
<div class="auth-tabs" role="tablist">
<button type="button" :class="{ active: mode === 'login' }" role="tab" @click="mode = 'login'">登录</button>
<button type="button" :class="{ active: mode === 'register' }" role="tab" @click="mode = 'register'">注册</button>
</div>
<form class="auth-form" @submit.prevent="submit">
<div class="field">
<label for="username">账号名</label>
<input id="username" v-model="username" class="input" autocomplete="username" placeholder="请输入账号名" required />
</div>
<div class="field">
<label for="password">密码</label>
<input id="password" v-model="password" class="input" type="password" :autocomplete="mode === 'login' ? 'current-password' : 'new-password'" placeholder="请输入密码" required />
</div>
<p v-if="errorMessage" class="field-error" role="alert">{{ errorMessage }}</p>
<button class="btn btn-primary auth-submit" type="submit" :disabled="session.busy">
{{ session.busy ? "请稍候" : mode === "login" ? "登录" : "注册并登录" }}
</button>
</form>
<p class="auth-risk">数据仅供复盘研究不构成投资建议<br />股市有风险投资需谨慎</p>
</section>
</main>
</template>
@@ -1,64 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { api } from "../../shared/api/client";
type Health = {
status: string;
environment: string;
};
const health = ref<Health | null>(null);
const failure = ref("");
onMounted(async () => {
try {
health.value = await api.get<Health>("/health");
} catch (error) {
failure.value = error instanceof Error ? error.message : "服务连接失败";
}
});
</script>
<template>
<main class="bootstrap-view">
<section class="bootstrap-panel" aria-live="polite">
<h1>小白复盘</h1>
<p v-if="health">新系统基础服务已连接{{ health.environment }}</p>
<p v-else-if="failure" class="failure">{{ failure }}</p>
<p v-else>正在连接基础服务</p>
</section>
</main>
</template>
<style scoped>
.bootstrap-view {
min-height: 100vh;
display: grid;
place-items: center;
padding: var(--space-16);
}
.bootstrap-panel {
width: min(100%, 420px);
padding: var(--space-22);
border: 1px solid var(--border);
border-radius: var(--radius-card);
background: var(--surface);
box-shadow: var(--shadow-card);
}
h1 {
margin: 0 0 var(--space-8);
font-size: var(--font-page-title);
}
p {
margin: 0;
color: var(--text-secondary);
}
.failure {
color: var(--warning);
}
</style>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { useSessionStore } from "../../shared/stores/session";
import MarketSettingsPanel from "../system/MarketSettingsPanel.vue";
import MembershipAdminPanel from "../system/MembershipAdminPanel.vue";
import ModelPoolPanel from "../system/ModelPoolPanel.vue";
const router = useRouter();
const session = useSessionStore();
const section = ref<"market" | "models" | "members">("market");
onMounted(() => {
if (!session.isAdmin) router.replace("/workspace/emotion");
});
</script>
<template>
<main v-if="session.isAdmin" class="page-frame">
<header class="page-header system-header">
<div><h1>系统管理</h1><p class="page-subtitle">平台行情模型池与会员配置</p></div>
<div class="system-tabs" role="tablist">
<button type="button" :class="{ active: section === 'market' }" @click="section = 'market'">行情管理</button>
<button type="button" :class="{ active: section === 'models' }" @click="section = 'models'">模型池</button>
<button type="button" :class="{ active: section === 'members' }" @click="section = 'members'">会员管理</button>
</div>
</header>
<MarketSettingsPanel v-if="section === 'market'" />
<ModelPoolPanel v-else-if="section === 'models'" />
<MembershipAdminPanel v-else />
</main>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { computed } from "vue";
import { useRoute } from "vue-router";
import EmptyState from "../../shared/components/EmptyState.vue";
import { useMarketStore } from "../../shared/stores/market";
import { useSessionStore } from "../../shared/stores/session";
import { useUiStore } from "../../shared/stores/ui";
import { findWorkspace } from "../workspaceRegistry";
const route = useRoute();
const market = useMarketStore();
const session = useSessionStore();
const ui = useUiStore();
const workspace = computed(() => findWorkspace(String(route.params.workspace)) ?? findWorkspace("emotion")!);
const locked = computed(
() => ["screener", "mentor", "heaven"].includes(workspace.value.key) && !session.account?.smart_access,
);
</script>
<template>
<main class="page-frame">
<header class="page-header">
<h1>{{ workspace.title }}</h1>
<p class="page-subtitle">{{ workspace.description }} · 数据日期 {{ market.selectedDate }}</p>
</header>
<div v-if="locked" class="notice notice-warning membership-lock">
<span><strong>{{ workspace.title }}仅对会员开放</strong>开通会员后可使用完整智能功能</span>
<button class="btn btn-small" type="button" @click="ui.openDialog('membership')">查看会员状态</button>
</div>
<section class="card" :class="{ 'locked-content': locked }" :aria-disabled="locked">
<EmptyState title="暂无可显示数据" description="当前日期尚未载入该页面的有效行情数据。" />
</section>
</main>
</template>