feat(login): 前后端联调\n\n- Vite 代理支持环境变量 VITE_API_TARGET\n- 保留 /api 前缀转发后端\n- Pinia 登录改为调用 /api/v1/user/login,并在后端不可达时回退演示账号\n- 登录页提交改为异步并加入 loading\n- 后端登录路由支持演示账号并保留数据库校验\n- 添加 .env.example 与 .env.development

pull/42/head
hnu202326010131 2 months ago
parent 368382b1a0
commit 1ae64fa483

@ -14,17 +14,24 @@ class LoginRequest(BaseModel):
@router.post("/user/login")
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
"""处理登录请求,验证用户名与密码。"""
result = await db.execute(select(User).where(User.username == req.username).limit(1))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=401, detail="invalid_credentials")
if not user.is_active:
raise HTTPException(status_code=403, detail="inactive_user")
if not bcrypt.verify(req.password, user.password_hash):
raise HTTPException(status_code=401, detail="invalid_credentials")
await db.execute(
update(User).where(User.id == user.id).values(last_login=func.now(), updated_at=func.now())
)
await db.commit()
return {"ok": True, "username": user.username, "fullName": user.full_name}
demo = {"admin": "admin123", "ops": "ops123", "obs": "obs123"}
if req.username in demo and req.password == demo[req.username]:
return {"ok": True, "username": req.username, "fullName": req.username}
try:
result = await db.execute(select(User).where(User.username == req.username).limit(1))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=401, detail="invalid_credentials")
if not user.is_active:
raise HTTPException(status_code=403, detail="inactive_user")
if not bcrypt.verify(req.password, user.password_hash):
raise HTTPException(status_code=401, detail="invalid_credentials")
await db.execute(
update(User).where(User.id == user.id).values(last_login=func.now(), updated_at=func.now())
)
await db.commit()
return {"ok": True, "username": user.username, "fullName": user.full_name}
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="server_error")

@ -0,0 +1 @@
VITE_API_TARGET=http://localhost:8000

@ -0,0 +1 @@
VITE_API_TARGET=http://localhost:8000

@ -1411,7 +1411,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -1426,7 +1425,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@ -1486,7 +1484,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",

@ -1,4 +1,6 @@
import { defineStore } from 'pinia'
import axios from 'axios'
const api = axios.create({ baseURL: '/api' })
type User = { username: string; role: 'admin'|'operator'|'observer' }
@ -24,19 +26,29 @@ export const useAuthStore = defineStore('auth', {
if (this.user) localStorage.setItem('cm_user', JSON.stringify(this.user))
else localStorage.removeItem('cm_user')
},
login(username: string, password: string) {
const demo = {
admin: { u: 'admin', p: 'admin123', role: 'admin' },
ops: { u: 'ops', p: 'ops123', role: 'operator' },
obs: { u: 'obs', p: 'obs123', role: 'observer' }
} as const
const m = Object.values(demo).find(d => d.u === username && d.p === password)
if (!m) return { ok: false, message: '账号或密码错误' }
this.user = { username, role: m.role }
this.persist()
return { ok: true, role: m.role }
async login(username: string, password: string) {
try {
const r = await api.post('/v1/user/login', { username, password })
const role = username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : username === 'obs' ? 'observer' : 'observer'
this.user = { username, role }
this.persist()
return { ok: true, role }
} catch (e: any) {
if (!e?.response) {
const demo = { admin: 'admin123', ops: 'ops123', obs: 'obs123' } as const
const pass = demo[username as keyof typeof demo]
if (pass && password === pass) {
const role = username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : 'observer'
this.user = { username, role }
this.persist()
return { ok: true, role }
}
}
const d = e?.response?.data
const message = d?.detail === 'invalid_credentials' ? '账号或密码错误' : d?.detail === 'inactive_user' ? '账号未激活' : '登录失败'
return { ok: false, message }
}
},
logout() { this.user = null; this.persist() }
}
})

@ -21,12 +21,14 @@ import { useAuthStore } from '../stores/auth'
const username = ref('')
const password = ref('')
const msg = ref('')
const loading = ref(false)
const router = useRouter()
const auth = useAuthStore()
function onSubmit() {
const r = auth.login(username.value, password.value)
async function onSubmit() {
loading.value = true
const r = await auth.login(username.value, password.value)
loading.value = false
if (r.ok) router.replace({ name: auth.defaultPage })
else msg.value = r.message || '登录失败'
}
</script>

@ -1,18 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0',
strictPort: true,
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: p => p.replace(/^\/api/, '')
}
}
}
})
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const target = env.VITE_API_TARGET || "http://localhost:8000";
return {
plugins: [vue()],
server: {
host: "0.0.0.0",
strictPort: true,
port: 5173,
proxy: {
"/api": {
target,
changeOrigin: true,
},
},
},
};
});

Loading…
Cancel
Save