后端: - 修复登录校验、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>
271 lines
6.5 KiB
Vue
271 lines
6.5 KiB
Vue
<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>
|