rebuild(stage-4): deliver shared shell and account interfaces
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user