feat(web): 布局壳与 NAKU 品牌——侧栏图标菜单、两行品牌区、favicon

This commit is contained in:
2026-10-07 17:06:53 +08:00
parent fbb989c3fb
commit 129ce7e380
19 changed files with 445 additions and 154 deletions
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
/** 审计日志占位——管理操作留痕(交互设计-审计日志) */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>审计日志</h1>
<p>管理操作留痕时间线(占位,待 UI 设计)。</p>
<PageHeader
title="审计日志"
subtitle="管理操作留痕(信息结构见交互设计-审计日志)"
/>
<a-skeleton active :paragraph="{ rows: 4 }" />
</section>
</template>
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
/** Key 管理占位——登记 / 启停 / 归属(交互设计-key管理) */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>Key 管理</h1>
<p>登记、启停、归属(占位,待 UI 设计)。</p>
<PageHeader
title="Key 管理"
subtitle="登记、启停、归属(信息结构见交互设计-key管理)"
/>
<a-skeleton active :paragraph="{ rows: 4 }" />
</section>
</template>
+69 -44
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
/** 登录 + 空库三态分流(交互登录篇);视觉细节待与 antdv-next 工具链一并设计 */
import { computed, ref } from "vue";
/** 登录 + 空库三态分流(交互登录篇) */
import type { FormInstance } from "antdv-next";
import { computed, reactive, ref, shallowRef } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useLogin, useSetup, useSetupState } from "@/queries/session.ts";
@@ -9,30 +10,34 @@ const router = useRouter();
const setupState = useSetupState();
const login = useLogin();
const setup = useSetup();
const formRef = shallowRef<FormInstance>();
const username = ref("");
const password = ref("");
const passwordConfirm = ref("");
const model = reactive({
username: "",
password: "",
passwordConfirm: "",
});
const error = ref("");
const isSetup = computed(() => setupState.data.value?.initialized === false);
const loading = computed(() => login.isPending.value || setup.isPending.value);
async function onSubmit() {
async function onFinish() {
error.value = "";
try {
if (isSetup.value) {
if (password.value !== passwordConfirm.value) {
if (model.password !== model.passwordConfirm) {
error.value = "两次输入的密码不一致";
return;
}
await setup.mutateAsync({
username: username.value,
password: password.value,
username: model.username,
password: model.password,
});
} else {
await login.mutateAsync({
username: username.value,
password: password.value,
username: model.username,
password: model.password,
});
}
const redirect = route.query.redirect;
@@ -44,46 +49,66 @@ async function onSubmit() {
</script>
<template>
<div class="login-view">
<h1>{{ isSetup ? "初始化管理员" : "管理面登录" }}</h1>
<form @submit.prevent="onSubmit">
<label>
用户名
<input v-model="username" name="username" autocomplete="username" required />
</label>
<label>
密码
<input
v-model="password"
<div class="login-shell">
<a-card class="login-shell__card" :title="isSetup ? '初始化管理员' : '管理面登录'">
<a-form
ref="formRef"
:model="model"
layout="vertical"
auto-complete="off"
@finish="onFinish"
>
<a-form-item
name="username"
label="用户名"
:rules="[{ required: true, message: '请输入用户名' }]"
>
<a-input v-model:value="model.username" autocomplete="username" />
</a-form-item>
<a-form-item
name="password"
type="password"
autocomplete="current-password"
required
label="密码"
:rules="[{ required: true, message: '请输入密码' }]"
>
<a-input-password
v-model:value="model.password"
autocomplete="current-password"
/>
</a-form-item>
<a-form-item
v-if="isSetup"
name="passwordConfirm"
label="确认密码"
:rules="[{ required: true, message: '请再次输入密码' }]"
>
<a-input-password v-model:value="model.passwordConfirm" />
</a-form-item>
<a-alert
v-if="error"
type="error"
show-icon
:message="error"
style="margin-bottom: 16px"
/>
</label>
<label v-if="isSetup">
确认密码
<input v-model="passwordConfirm" name="password_confirm" type="password" required />
</label>
<button type="submit" :disabled="login.isPending.value || setup.isPending.value">
{{ isSetup ? "完成设置" : "登录" }}
</button>
</form>
<p v-if="error" role="alert">{{ error }}</p>
<a-button type="primary" html-type="submit" block :loading="loading">
{{ isSetup ? "完成设置" : "登录" }}
</a-button>
</a-form>
</a-card>
</div>
</template>
<style scoped>
.login-view {
max-width: 360px;
margin: 10vh auto;
.login-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
background: #f0f2f5;
padding: 24px;
}
.login-view label {
display: flex;
flex-direction: column;
gap: 4px;
.login-shell__card {
width: 380px;
}
</style>
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
/** 成员管理占位——开户 / 令牌 / 停用(交互设计-成员管理) */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>成员管理</h1>
<p>开户、令牌签发、停用(占位,待 UI 设计)。</p>
<PageHeader
title="成员管理"
subtitle="开户、令牌、停用(信息结构见交互设计-成员管理)"
/>
<a-skeleton active :paragraph="{ rows: 4 }" />
</section>
</template>
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
/** 全员总览占位——信息结构见交互设计-全员总览;UI 待设计阶段 */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>全员总览</h1>
<p>健康带 + 按成员 / key 汇总(占位,待 UI 设计)。</p>
<PageHeader
title="全员总览"
subtitle="健康带 · 按成员 / key 汇总(信息结构见交互设计-全员总览)"
/>
<a-skeleton active :paragraph="{ rows: 4 }" />
</section>
</template>
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
/** 支付记录占位——订阅/充值同构表单(交互设计-支付记录) */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>支付记录</h1>
<p>订阅 / 充值登记与核对(占位,待 UI 设计)。</p>
<PageHeader
title="支付记录"
subtitle="订阅 / 充值登记与核对(信息结构见交互设计-支付记录)"
/>
<a-skeleton active :paragraph="{ rows: 4 }" />
</section>
</template>
+3 -3
View File
@@ -1,10 +1,10 @@
<script setup lang="ts">
/** 账号设置(改密)占位——交互设计-登录与会话 */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>账号设置</h1>
<p>修改密码(占位,待 UI 设计)。</p>
<PageHeader title="账号设置" subtitle="修改密码" />
<a-skeleton active :paragraph="{ rows: 3 }" />
</section>
</template>
+6 -3
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
/** 成员单页占位——聚合 + 来源徽章 + 明细(交互设计-成员视图) */
import PageHeader from "@/components/common/page-header.vue";
</script>
<template>
<section>
<h1>我的用量</h1>
<p>总用量、来源分解、key 窄卡、支付参数与明细(占位,待 UI 设计)。</p>
<PageHeader
title="我的用量"
subtitle="聚合 · 来源徽章 · 明细(信息结构见交互设计-成员视图)"
/>
<a-skeleton active :paragraph="{ rows: 4 }" />
</section>
</template>
+43 -23
View File
@@ -1,21 +1,23 @@
<script setup lang="ts">
/** 成员令牌登录壳——明文仅存 sessionStorage(交互登录篇 §4) */
import { ref } from "vue";
import type { FormInstance } from "antdv-next";
import { reactive, ref, shallowRef } from "vue";
import { useRouter } from "vue-router";
import { useMemberTokenStore } from "@/stores/member-token.ts";
import { fetchMe } from "@/api/member/me.ts";
const router = useRouter();
const tokenStore = useMemberTokenStore();
const token = ref("");
const formRef = shallowRef<FormInstance>();
const model = reactive({ token: "" });
const error = ref("");
const pending = ref(false);
async function onSubmit() {
async function onFinish() {
error.value = "";
pending.value = true;
try {
tokenStore.setToken(token.value.trim());
tokenStore.setToken(model.token.trim());
const me = await fetchMe();
tokenStore.setMemberName(me.name);
void router.replace({ name: "member-home" });
@@ -29,30 +31,48 @@ async function onSubmit() {
</script>
<template>
<div class="member-login">
<h1>成员入口</h1>
<form @submit.prevent="onSubmit">
<label>
令牌
<input v-model="token" name="token" type="password" required />
</label>
<button type="submit" :disabled="pending">进入</button>
</form>
<p v-if="error" role="alert">{{ error }}</p>
<div class="member-login-shell">
<a-card class="member-login-shell__card" title="成员入口">
<a-form
ref="formRef"
:model="model"
layout="vertical"
auto-complete="off"
@finish="onFinish"
>
<a-form-item
name="token"
label="令牌"
:rules="[{ required: true, message: '请输入成员令牌' }]"
>
<a-input-password v-model:value="model.token" placeholder="粘贴成员令牌" />
</a-form-item>
<a-alert
v-if="error"
type="error"
show-icon
:message="error"
style="margin-bottom: 16px"
/>
<a-button type="primary" html-type="submit" block :loading="pending">
进入
</a-button>
</a-form>
</a-card>
</div>
</template>
<style scoped>
.member-login {
max-width: 360px;
margin: 12vh auto;
.member-login-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
background: #f0f2f5;
padding: 24px;
}
.member-login label {
display: flex;
flex-direction: column;
gap: 4px;
.member-login-shell__card {
width: 380px;
}
</style>