feat(auth): 登录页(用户名/密码换 API token)+ 前端全链路登录态
后端: - POST /api/auth/login:admin 账密(Settings 默认 admin/admin123, ADMIN_USER/ADMIN_PASSWORD 可覆盖)换取共享 api_token,免 token 中间件 - 防爆破:同 IP 15min 窗口内 5 次失败锁 15min(429),成功登录清零 - 错误信息统一「用户名或密码错误」(防用户名枚举) - 测试 7 个新增(TDD),全量 500 绿 前端: - LoginView + 路由守卫(未登录跳 /login,回跳 redirect) - axios 请求拦截器自动带 Bearer;401 清登录态回登录页 - 媒体直链统一 mediaUrl() 带 ?token=(img/audio 标签发不了 header), 替换 4 处裸拼;downloadBlob 手动带头 - isLoggedIn 按 key 存在判定 — 后端鉴权关闭时空 token 不锁死登录页 - MainLayout 侧栏底部退出登录 compose:两份编排透传 ADMIN_USER/ADMIN_PASSWORD(默认 admin/admin123) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getCallStats, getCalls } from '@/api'
|
||||
import { mediaUrl } from '@/api/client'
|
||||
import type { CallChannel, CallStats, PhoneCall } from '@/api/types'
|
||||
import {
|
||||
callDirectionLabel,
|
||||
@@ -191,7 +192,7 @@ function timeCell(row: PhoneCall, camelKey: string): number | undefined {
|
||||
}
|
||||
|
||||
function playRecord(row: PhoneCall): void {
|
||||
const local = row.record_media_hash ? `/api/media/${row.record_media_hash}` : null
|
||||
const local = row.record_media_hash ? mediaUrl(row.record_media_hash) : null
|
||||
const file =
|
||||
local ??
|
||||
(typeof cell(row, 'recordFile') === 'string' ? (cell(row, 'recordFile') as string) : null) ??
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login } from '@/api'
|
||||
import { setToken } from '@/api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const form = reactive({ username: '', password: '' })
|
||||
const loading = ref(false)
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
if (!form.username || !form.password) {
|
||||
ElMessage.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const { token } = await login(form.username, form.password)
|
||||
setToken(token)
|
||||
ElMessage.success('登录成功')
|
||||
const redirect = route.query.redirect
|
||||
router.push(typeof redirect === 'string' && redirect ? redirect : '/')
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="yk-login">
|
||||
<el-card class="yk-login__card" shadow="always">
|
||||
<h2 class="yk-login__title">云客数据镜像</h2>
|
||||
<el-form label-position="top" @submit.prevent="submit">
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
placeholder="用户名"
|
||||
size="large"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
size="large"
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="yk-login__btn"
|
||||
:loading="loading"
|
||||
@click="submit"
|
||||
>
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.yk-login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(160deg, #1c2431 0%, #2b3a4a 100%);
|
||||
}
|
||||
|
||||
.yk-login__card {
|
||||
width: 360px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.yk-login__title {
|
||||
margin: 4px 0 20px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #1c2431;
|
||||
}
|
||||
|
||||
.yk-login__btn {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user