feat: Phase 1 完成 — 基础设施 + 用户认证 + 管理端 + 端到端验证

Phase 1 (基础设施与用户认证) 全部 11 个 Task 开发完成:

后端 (Go):
- Auth 模块: 注册/登录/JWT(有状态)/Profile/密码修改
- Admin 模块: 用户列表/详情/禁用/启用/角色分配/创建用户
- 中间件: JWT认证 + RBAC角色权限 + 请求日志 + CORS + Panic恢复
- Dockerfile 多阶段构建 + Docker Compose 全栈部署

前台 (uni-app):
- 登录/注册页面 + 自定义 TabBar + 个人中心
- 请求封装 + 状态管理 (Pinia)

管理端 (Vue 3 + Element Plus):
- 登录/仪表盘/用户列表/用户详情
- Axios 封装 + 路由守卫 + Pinia 状态管理

验证:
- 全流程 API 端到端测试通过
- Playwright 页面自动化验证通过
- code-reviewer 代码审查通过

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-03-02 11:31:24 +08:00
parent e9bd4dd204
commit 4d03215fe4
63 changed files with 15908 additions and 102 deletions

View File

@@ -0,0 +1,347 @@
<!--
登录页面
设计系统design-system/echochat/MASTER.md + pages/login.md
色板Primary #2563EB / CTA #F97316 / Background #F8FAFC / Text #1E293B
风格Clean Minimalism单列居中低内容密度
功能
- 账号用户名或邮箱+ 密码登录
- 表单验证label + error feedback不使用 placeholder-only
- Loading 状态反馈禁用按钮防止重复提交
- 登录成功跳转首页
- "没有账号?去注册" 链接
对应 APIPOST /api/v1/auth/login
接口文档docs/api/frontend/auth.md
-->
<template>
<view class="page-container">
<!-- 顶部品牌区域 -->
<view class="brand-section">
<view class="logo-box">
<text class="logo-letter">E</text>
</view>
<text class="brand-name">EchoChat</text>
<text class="brand-slogan">连接无限沟通无界</text>
</view>
<!-- 表单卡片 -->
<view class="form-card">
<text class="form-heading">登录</text>
<!-- 账号 -->
<view class="field-group">
<text class="field-label">账号</text>
<view class="input-box" :class="{ 'input-focused': focusState.account, 'input-error': errors.account }">
<input
class="input-control"
type="text"
placeholder="请输入用户名或邮箱"
v-model="form.account"
@focus="focusState.account = true"
@blur="onBlur('account')"
maxlength="50"
/>
</view>
<text v-if="errors.account" class="field-error">{{ errors.account }}</text>
</view>
<!-- 密码 -->
<view class="field-group">
<text class="field-label">密码</text>
<view class="input-box" :class="{ 'input-focused': focusState.password, 'input-error': errors.password }">
<input
class="input-control"
:type="passwordVisible ? 'text' : 'password'"
placeholder="请输入密码"
v-model="form.password"
@focus="focusState.password = true"
@blur="onBlur('password')"
maxlength="50"
@confirm="submit"
/>
<text class="pwd-toggle" @tap="passwordVisible = !passwordVisible">
{{ passwordVisible ? '隐藏' : '显示' }}
</text>
</view>
<text v-if="errors.password" class="field-error">{{ errors.password }}</text>
</view>
<!-- 登录按钮 -->
<button class="btn-primary" :class="{ 'btn-disabled': loading }" :disabled="loading" @tap="submit">
<text class="btn-label">{{ loading ? '登录中…' : '登录' }}</text>
</button>
<!-- 底部链接 -->
<view class="link-row">
<text class="link-hint">还没有账号</text>
<text class="link-action" @tap="goRegister">立即注册</text>
</view>
</view>
</view>
</template>
<script>
/**
* 登录页面逻辑
*
* 使用 useUserStore 处理登录
* 登录成功后 reLaunch 到首页
*/
import { useUserStore } from '@/store/user'
export default {
name: 'LoginPage',
data() {
return {
form: { account: '', password: '' },
passwordVisible: false,
loading: false,
focusState: { account: false, password: false },
errors: { account: '', password: '' }
}
},
methods: {
/** 失焦时触发对应字段校验 */
onBlur(field) {
this.focusState[field] = false
this.validate(field)
},
/**
* 单字段校验
* @param {string} field
* @returns {boolean}
*/
validate(field) {
if (field === 'account') {
if (!this.form.account.trim()) {
this.errors.account = '请输入用户名或邮箱'
return false
}
this.errors.account = ''
return true
}
if (field === 'password') {
if (!this.form.password) {
this.errors.password = '请输入密码'
return false
}
if (this.form.password.length < 6) {
this.errors.password = '密码至少 6 位'
return false
}
this.errors.password = ''
return true
}
return true
},
/** 提交登录 */
async submit() {
if (this.loading) return
const a = this.validate('account')
const p = this.validate('password')
if (!a || !p) return
this.loading = true
try {
const store = useUserStore()
await store.login({
account: this.form.account.trim(),
password: this.form.password
})
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => uni.reLaunch({ url: '/pages/index/index' }), 800)
} catch (e) {
console.error('登录失败:', e)
} finally {
this.loading = false
}
},
goRegister() {
uni.navigateTo({ url: '/pages/auth/register' })
}
}
}
</script>
<style scoped>
/*
* 设计系统来源design-system/echochat/MASTER.md + pages/login.md
* 色板Primary #2563EB / CTA #F97316 / BG #F8FAFC / Text #1E293B
* 输入框规范border #E2E8F0 / radius 8px(16rpx) / focus border #2563EB + shadow
* 按钮规范radius 8px(16rpx) / transition 200ms / cursor-pointer
* 间距规范:--space-md 16px(32rpx) / --space-lg 24px(48rpx)
*/
.page-container {
min-height: 100vh;
background-color: #F8FAFC;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 48rpx;
}
/* ---- 品牌区域 ---- */
.brand-section {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 120rpx;
margin-bottom: 64rpx;
}
.logo-box {
width: 112rpx;
height: 112rpx;
border-radius: 24rpx;
background-color: #2563EB;
display: flex;
align-items: center;
justify-content: center;
}
.logo-letter {
font-size: 52rpx;
font-weight: 700;
color: #FFFFFF;
}
.brand-name {
font-size: 44rpx;
font-weight: 700;
color: #1E293B;
margin-top: 24rpx;
letter-spacing: 2rpx;
}
.brand-slogan {
font-size: 26rpx;
color: #64748B;
margin-top: 8rpx;
}
/* ---- 表单卡片 ---- */
.form-card {
width: 100%;
max-width: 800rpx;
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 56rpx 40rpx 48rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.form-heading {
font-size: 40rpx;
font-weight: 600;
color: #1E293B;
margin-bottom: 48rpx;
}
/* ---- 输入字段 ---- */
.field-group {
margin-bottom: 36rpx;
}
.field-label {
display: block;
font-size: 26rpx;
font-weight: 500;
color: #475569;
margin-bottom: 12rpx;
}
.input-box {
display: flex;
align-items: center;
height: 96rpx;
background-color: #FFFFFF;
border: 2rpx solid #E2E8F0;
border-radius: 16rpx;
padding: 0 24rpx;
transition: border-color 200ms ease, box-shadow 200ms ease;
}
.input-box.input-focused {
border-color: #2563EB;
box-shadow: 0 0 0 6rpx rgba(37, 99, 235, 0.12);
}
.input-box.input-error {
border-color: #EF4444;
}
.input-control {
flex: 1;
font-size: 30rpx;
color: #1E293B;
}
.pwd-toggle {
font-size: 24rpx;
color: #2563EB;
font-weight: 500;
padding: 12rpx 0 12rpx 16rpx;
}
.field-error {
display: block;
font-size: 22rpx;
color: #EF4444;
margin-top: 8rpx;
padding-left: 4rpx;
}
/* ---- 按钮CTA 色 #F97316 ---- */
.btn-primary {
width: 100%;
height: 96rpx;
background-color: #2563EB;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: 48rpx;
border: none;
transition: opacity 200ms ease;
}
.btn-primary::after {
border: none;
}
.btn-primary.btn-disabled {
opacity: 0.6;
}
.btn-label {
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 2rpx;
}
/* ---- 底部链接 ---- */
.link-row {
display: flex;
justify-content: center;
align-items: center;
margin-top: 40rpx;
}
.link-hint {
font-size: 26rpx;
color: #94A3B8;
}
.link-action {
font-size: 26rpx;
color: #2563EB;
font-weight: 500;
margin-left: 8rpx;
}
</style>