Files
goodBaby/web/frontend/src/views/LoginView.vue
T
meiandClaude Fable 5 79774070fc feat: 完善后端并新增 Vue3 WebUI
后端:
- 修复登录校验、DB 并发(单例+WAL+纯Go sqlite)、会话密钥持久化等核心缺陷
- 新增 Timer CRUD/签到/手动触发、执行日志、仪表盘、驱动元数据等 API
- 统一驱动接口: 带 context 的执行器、真实错误返回、敏感字段掩码、连通性测试
- 补全钉钉执行器; Timer 触发/提醒去重; 配置支持默认值与环境变量覆盖

前端:
- Vite+Vue3+TS+Pinia+Element Plus, 由驱动元数据动态渲染配置表单
- 登录/仪表盘(倒计时环)/定时器/规则/账号/日志/设置页面, 支持暗色模式
- 主题色 #66ccff, 信号波纹 logo, go:embed 单二进制部署

构建: Dockerfile 增加前端构建阶段; 补充各驱动配置文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:19:41 +08:00

271 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { siteApi, userApi } from '@/api'
import { ApiError } from '@/api/client'
import type { SiteInfo } from '@/api/types'
import { useUserStore } from '@/stores/user'
import LogoMark from '@/components/LogoMark.vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const site = ref<SiteInfo | null>(null)
const mode = ref<'login' | 'register'>('login')
const loading = ref(false)
const form = ref({
username: '',
password: '',
confirm: '',
})
onMounted(async () => {
try {
site.value = await siteApi.info()
// 全新部署引导创建第一个账号
if (site.value.need_initial_user) {
mode.value = 'register'
}
} catch {
// 站点信息拉取失败不阻塞登录
}
})
async function submit() {
if (!form.value.username || !form.value.password) {
ElMessage.warning('请输入用户名和密码')
return
}
if (mode.value === 'register' && form.value.password !== form.value.confirm) {
ElMessage.warning('两次输入的密码不一致')
return
}
loading.value = true
try {
if (mode.value === 'register') {
await userApi.register(form.value.username, form.value.password)
ElMessage.success('注册成功')
} else {
await userApi.login(form.value.username, form.value.password)
}
await userStore.fetchUser()
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
router.push(redirect)
} catch (error) {
ElMessage.error(error instanceof ApiError ? error.message : '操作失败')
} finally {
loading.value = false
}
}
</script>
<template>
<div class="login-page">
<!-- 左侧:项目介绍 -->
<div class="intro">
<div class="intro-inner gb-rise">
<div class="intro-mark"><LogoMark :size="28" /></div>
<h1 class="intro-title">goodBaby</h1>
<p class="intro-slogan">摇篮系统 · Dead Man's Switch</p>
<p class="intro-desc">
设定签到周期并定期签到;<br />
超时未签到时,系统会自动执行你预设的规则。
</p>
<ul class="intro-points">
<li>到期前通过钉钉机器人提醒签到</li>
<li>支持发送邮件、QQ、钉钉消息、B 站动态</li>
<li>支持自动公开 GitHub 仓库</li>
</ul>
</div>
</div>
<!-- 右侧:登录表单 -->
<div class="panel">
<div class="panel-card gb-rise">
<h2 class="panel-title">
{{ site?.need_initial_user ? '创建初始账号' : mode === 'login' ? '登录' : '注册' }}
</h2>
<p class="panel-sub">
{{
site?.need_initial_user
? '首次使用,请先创建管理员账号'
: mode === 'login'
? '登录你的 goodBaby 账号'
: '创建一个新账号'
}}
</p>
<el-form label-position="top" size="large" @keyup.enter="submit">
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="2-32 个字符" autofocus />
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="form.password"
type="password"
show-password
placeholder="6-64 个字符"
/>
</el-form-item>
<el-form-item v-if="mode === 'register'" label="确认密码">
<el-input v-model="form.confirm" type="password" show-password placeholder="再输入一次" />
</el-form-item>
<el-button type="primary" size="large" class="submit-btn" :loading="loading" @click="submit">
{{ mode === 'login' ? '登 录' : '注 册' }}
</el-button>
</el-form>
<div v-if="site?.enable_registry && !site?.need_initial_user" class="switch-mode">
<el-link v-if="mode === 'login'" type="primary" :underline="false" @click="mode = 'register'">
没有账号?注册
</el-link>
<el-link v-else type="primary" :underline="false" @click="mode = 'login'">
已有账号?登录
</el-link>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.login-page {
height: 100%;
display: flex;
}
/* ---------- 左侧理念区 ---------- */
.intro {
flex: 1.15;
background:
radial-gradient(ellipse at 20% 20%, rgb(102 204 255 / 0.16) 0%, transparent 55%),
radial-gradient(ellipse at 85% 80%, rgb(102 204 255 / 0.12) 0%, transparent 50%),
linear-gradient(160deg, #14161c 0%, #1b1f27 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
}
.intro-inner {
max-width: 420px;
}
.intro-mark {
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, var(--gb-primary) 0%, #3fa9e0 100%);
display: flex;
align-items: center;
justify-content: center;
color: #0b2b3d;
box-shadow: 0 8px 24px rgb(102 204 255 / 0.5);
margin-bottom: 24px;
}
.intro-title {
margin: 0;
color: #fff;
font-size: 34px;
font-weight: 800;
letter-spacing: -0.02em;
}
.intro-slogan {
margin: 10px 0 20px;
color: #66ccff;
font-size: 17px;
font-weight: 600;
}
.intro-desc {
margin: 0 0 26px;
color: rgb(255 255 255 / 0.66);
font-size: 15px;
line-height: 2;
}
.intro-points {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 12px;
}
.intro-points li {
color: rgb(255 255 255 / 0.5);
font-size: 13px;
padding-left: 20px;
position: relative;
}
.intro-points li::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--gb-primary);
}
/* ---------- 右侧表单区 ---------- */
.panel {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--gb-bg);
padding: 32px;
}
.panel-card {
width: 100%;
max-width: 360px;
}
.panel-title {
margin: 0;
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--el-text-color-primary);
}
.panel-sub {
margin: 8px 0 28px;
color: var(--el-text-color-secondary);
font-size: 14px;
}
.submit-btn {
width: 100%;
margin-top: 8px;
font-weight: 600;
letter-spacing: 0.1em;
}
.switch-mode {
text-align: center;
margin-top: 20px;
}
/* 窄屏时隐藏左侧介绍 */
@media (max-width: 860px) {
.intro {
display: none;
}
}
</style>