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:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Microphone, VideoPlay, Document, Picture, Paperclip, User } from '@element-plus/icons-vue'
|
||||
import { mediaUrl } from '@/api/client'
|
||||
import type { Message } from '@/api/types'
|
||||
import { formatDuration, formatTime, wechatTypeLabel } from '@/utils/format'
|
||||
|
||||
@@ -103,7 +104,7 @@ const bestMediaUrl = computed<string | undefined>(() => {
|
||||
*/
|
||||
const localSrc = computed<string | undefined>(() => {
|
||||
const hash = props.message.media_hash
|
||||
return hash ? `/api/media/${hash}` : undefined
|
||||
return hash ? mediaUrl(hash) : undefined
|
||||
})
|
||||
|
||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'tif', 'tiff'])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getCustomers, getVoiceClips, matchVoiceClip, search } from '@/api'
|
||||
import { mediaUrl } from '@/api/client'
|
||||
import type { Customer, VoiceClip } from '@/api/types'
|
||||
import { formatCallDuration, formatTime } from '@/utils/format'
|
||||
|
||||
@@ -149,7 +150,7 @@ const displayItems = computed(() =>
|
||||
function playClip(row: VoiceClip): void {
|
||||
// Local-first: 优先本地化的 /api/media/{media_hash},回退到 OSS 直链
|
||||
// (mirrors CallList.playRecord)。
|
||||
const local = row.media_hash ? `/api/media/${row.media_hash}` : null
|
||||
const local = row.media_hash ? mediaUrl(row.media_hash) : null
|
||||
const url = local ?? row.oss_file_name
|
||||
if (!url) {
|
||||
ElMessage.info('该片段无录音文件')
|
||||
|
||||
Reference in New Issue
Block a user