From 78d13d077c3b66a1bd33b96b72ab8d9f7b2f7c3a Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Tue, 16 Dec 2025 20:21:30 +0800 Subject: [PATCH] =?UTF-8?q?Step=20F1=EF=BC=9A=E5=89=8D=E7=AB=AF=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E9=AA=A8=E6=9E=B6=20&=20=E7=99=BB=E5=BD=95=E9=89=B4?= =?UTF-8?q?=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/Dockerfile | 7 +++ frontend/index.html | 12 ++++ frontend/package.json | 26 +++++++++ frontend/src/App.vue | 4 ++ frontend/src/api/auth.ts | 10 ++++ frontend/src/api/axios.ts | 45 +++++++++++++++ frontend/src/components/Layout.vue | 66 +++++++++++++++++++++ frontend/src/env.d.ts | 7 +++ frontend/src/main.ts | 14 +++++ frontend/src/router/index.ts | 48 ++++++++++++++++ frontend/src/store/auth.ts | 44 ++++++++++++++ frontend/src/types/api.ts | 27 +++++++++ frontend/src/utils/auth.ts | 11 ++++ frontend/src/utils/permission.ts | 6 ++ frontend/src/views/Home.vue | 27 +++++++++ frontend/src/views/Login.vue | 73 ++++++++++++++++++++++++ frontend/tsconfig.json | 18 ++++++ frontend/tsconfig.node.json | 10 ++++ frontend/vite.config.ts | 16 ++++++ nginx/nginx.conf | 51 +++++++++++++++++ pg_data/base/16384/pg_internal.init | Bin 160044 -> 160044 bytes pg_data/global/pg_control | Bin 8192 -> 8192 bytes pg_data/global/pg_internal.init | Bin 27484 -> 27484 bytes pg_data/pg_wal/000000010000000000000001 | Bin 16777216 -> 16777216 bytes pg_data/postmaster.pid | 2 +- 25 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/auth.ts create mode 100644 frontend/src/api/axios.ts create mode 100644 frontend/src/components/Layout.vue create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/store/auth.ts create mode 100644 frontend/src/types/api.ts create mode 100644 frontend/src/utils/auth.ts create mode 100644 frontend/src/utils/permission.ts create mode 100644 frontend/src/views/Home.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 nginx/nginx.conf diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..e902a7cd --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "--port", "5173"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..db5f5b13 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + CTMS + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..7a94757b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "ctms-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "type-check": "vue-tsc --noEmit" + }, + "dependencies": { + "axios": "^1.6.8", + "element-plus": "^2.4.4", + "pinia": "^2.1.7", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@vitejs/plugin-vue": "^5.0.2", + "typescript": "^5.3.3", + "vite": "^5.0.8", + "vue-tsc": "^1.8.25" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 00000000..cb517758 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,4 @@ + diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 00000000..4cbfc072 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,10 @@ +import type { AxiosResponse } from "axios"; +import api, { apiGet, apiPost } from "./axios"; +import type { UserMeResponse, LoginRequest, LoginResponse } from "../types/api"; + +export const login = (payload: LoginRequest): Promise> => + apiPost("/api/v1/auth/login", payload); + +export const fetchMe = (): Promise> => apiGet("/api/v1/auth/me"); + +export default api; diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts new file mode 100644 index 00000000..d4a99937 --- /dev/null +++ b/frontend/src/api/axios.ts @@ -0,0 +1,45 @@ +import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; +import { ElMessage } from "element-plus"; +import router from "../router"; +import { getToken, clearToken } from "../utils/auth"; +import type { ApiError } from "../types/api"; + +const instance: AxiosInstance = axios.create({ + baseURL: "/", + timeout: 15000, +}); + +instance.interceptors.request.use((config: AxiosRequestConfig) => { + const token = getToken(); + if (token) { + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +instance.interceptors.response.use( + (response: AxiosResponse) => response, + async (error: AxiosError) => { + const status = error.response?.status; + const data = error.response?.data; + if (status === 401) { + clearToken(); + ElMessage.error(data?.message || "登录已过期,请重新登录"); + router.push("/login"); + } else if (data?.message) { + ElMessage.error(data.message); + } else { + ElMessage.error("请求失败,请稍后重试"); + } + return Promise.reject(error); + } +); + +export const apiGet = (url: string, config?: AxiosRequestConfig) => instance.get(url, config); +export const apiPost = (url: string, data?: unknown, config?: AxiosRequestConfig) => + instance.post(url, data, config); +export const apiPatch = (url: string, data?: unknown, config?: AxiosRequestConfig) => + instance.patch(url, data, config); + +export default instance; diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue new file mode 100644 index 00000000..9e76054a --- /dev/null +++ b/frontend/src/components/Layout.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 00000000..fc812394 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 00000000..18e568a5 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,14 @@ +import { createApp } from "vue"; +import { createPinia } from "pinia"; +import ElementPlus from "element-plus"; +import "element-plus/dist/index.css"; + +import App from "./App.vue"; +import router from "./router"; + +const app = createApp(App); +app.use(createPinia()); +app.use(router); +app.use(ElementPlus); + +app.mount("#app"); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 00000000..aaa4168b --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,48 @@ +import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; +import { useAuthStore } from "../store/auth"; +import Layout from "../components/Layout.vue"; +import Home from "../views/Home.vue"; +import Login from "../views/Login.vue"; + +const routes: RouteRecordRaw[] = [ + { + path: "/login", + name: "Login", + component: Login, + meta: { public: true, title: "登录" }, + }, + { + path: "/", + component: Layout, + redirect: "/home", + children: [ + { + path: "home", + name: "Home", + component: Home, + meta: { title: "首页" }, + }, + ], + }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +router.beforeEach((to, _from, next) => { + const auth = useAuthStore(); + const token = auth.token; + if (!to.meta.public && !token) { + next({ path: "/login" }); + return; + } + if (to.path === "/login" && token) { + next({ path: "/" }); + return; + } + next(); +}); + +export default router; diff --git a/frontend/src/store/auth.ts b/frontend/src/store/auth.ts new file mode 100644 index 00000000..d8ac7c36 --- /dev/null +++ b/frontend/src/store/auth.ts @@ -0,0 +1,44 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { login as apiLogin, fetchMe } from "../api/auth"; +import { setToken, clearToken, getToken } from "../utils/auth"; +import type { UserInfo } from "../types/api"; + +export const useAuthStore = defineStore("auth", () => { + const token = ref(getToken()); + const user = ref(null); + const loading = ref(false); + + const login = async (username: string, password: string) => { + loading.value = true; + try { + const { data } = await apiLogin({ username, password }); + token.value = data.access_token; + setToken(data.access_token); + await fetchMeAction(); + } finally { + loading.value = false; + } + }; + + const fetchMeAction = async () => { + const { data } = await fetchMe(); + user.value = data; + return data; + }; + + const logout = () => { + token.value = null; + user.value = null; + clearToken(); + }; + + return { + token, + user, + loading, + login, + fetchMe: fetchMeAction, + logout, + }; +}); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 00000000..57d04d66 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,27 @@ +export interface ApiListResponse { + items: T[]; + total: number; +} + +export interface ApiError { + code: string; + message: string; +} + +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + access_token: string; + token_type: string; +} + +export interface UserInfo { + id: string; + username: string; + role: string; +} + +export type UserMeResponse = UserInfo; diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts new file mode 100644 index 00000000..dbdf55c6 --- /dev/null +++ b/frontend/src/utils/auth.ts @@ -0,0 +1,11 @@ +const TOKEN_KEY = "ctms_token"; + +export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY); + +export const setToken = (token: string): void => { + localStorage.setItem(TOKEN_KEY, token); +}; + +export const clearToken = (): void => { + localStorage.removeItem(TOKEN_KEY); +}; diff --git a/frontend/src/utils/permission.ts b/frontend/src/utils/permission.ts new file mode 100644 index 00000000..b26b303e --- /dev/null +++ b/frontend/src/utils/permission.ts @@ -0,0 +1,6 @@ +// 预留权限工具,可在后续基于 role 与项目内角色实现细粒度控制 +export const hasPermission = (requiredRoles: string[], userRole?: string): boolean => { + if (!requiredRoles.length) return true; + if (!userRole) return false; + return requiredRoles.includes(userRole); +}; diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 00000000..23139c72 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 00000000..3375ab4e --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..222d10c3 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 00000000..7065ca9a --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 00000000..44bdf546 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + plugins: [vue()], + server: { + host: true, + port: 5173, + proxy: { + "/api": { + target: "http://backend:8000", + changeOrigin: true, + }, + }, + }, +}); diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000..ff3a3c51 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,51 @@ +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + upstream frontend { + server frontend:5173; + } + + upstream backend { + server backend:8000; + } + + server { + listen 80; + server_name _; + + location /api/ { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } + } +} diff --git a/pg_data/base/16384/pg_internal.init b/pg_data/base/16384/pg_internal.init index 12834ae95b319d3c7ebf27735235f90f5eb4d851..7ba709abdc592d5f7b8d40e64b715d132491c4aa 100644 GIT binary patch delta 10253 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 pu!QoLlBHQ>P1q9p7f>FwJWbybu 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 diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index 04c4f8392ddcbf9cd79443c64ff6db10d9f4d9ad..3d19d35b3f093d316c94a1854ffec612ec83f259 100644 GIT binary patch delta 59 zcmZp0XmFSyA++4jF_QrZ90U$CLgvY@9(?+o^F8PO z{+-`BcVk`MSXc82EqdBz-oC9U$z_-Mrv9fZUN-!_SB$lGzt~bEZr>~YayhWrZyI;E zwbsR2`^g=?PeO7{|1@M8!OrdYHOw>2*r;Ew=k|f-7-%tVro#-5`%Odd8q&NpXvP$k z!uXK*tZTRGCaYq@Kc#km2sfpV*jo~!Pc?|NTYy>nhI z*Qd!1<}Quu7py0O0}yJv!JrOTxf#$r0$NO)S($~d9OD>kM%oH=nO#j+}<1v51#;g>`^V~p4 z^*Cf!FukWSkG>Aht;iT7K4S9#)62~{g=j~pIAO9X(!=v=_aYjl{3>{YTIhsqq3{xv zMiz|Siz?>^L30miF>R*93`SZ#Oas}&5JDm|8d2S3U99b+8tFO`otvQ=)yC?H+AIW% z*;q#gsS0Ou6U;JArp+w;PMJ22MSQoguS9d9kL#+(CX3>Oi7C}*Bd6?7bYJVy?f4DM z*IIlVA=lmkol9WgGN;0H2KUI+OtY2iFoZCXE^x@mjCw_Oe~-&emW4T4pq@C;DZgnP z>@#S=hp5c8=qdx<^s!#@?NO@T2I-ouX?!l+3nk_659} zm5VOZC{EWCa|VPk)4GIXa4v)1cflYt%1kr!@A2hYpg}X80#O8YI=>j16fezC(^!w7 zW~IYa#BYw&6Uk`^erE6-YLPwdmj48D(4%ZK{MZ^6v*>zs`;3ZS+V#8 zJ@((BNBt#mg>LGKys7CQpo}nM4#ocStpc#QJ1&Sc`1ylEf8D@@IWLEcckTwmPK{`Y+a(qU0lSQ%oJe^H!0!^StsVlC((CToZ Izo@zX1Ni?*vj6}9 delta 2024 zcmY+FZD?Cn7{~8T()8MdHn6@#P>5RfI$Dgg%pj}F)-d7=<4&eTP>9TF{InFD1Qomi zVL=c&FG$oDqfU*so7qll(@iDSbqlQB(htsjs8HyTd`RU^=azLZ^~w1^{otqHIsbF+ zbN=T!&pCJLqPlcZ9f^2T|BR>C*A*pmWjuY@_|wsS)bjO=S~<5*TvGd;o^uOw*gwCJ zw(e@#7#BJB!B)@h^K#5sKcBV&?W;qM3$mYNnqSPwe)&tVdIYqY+E?g@nDMV?(w5QD z=Z#XM=_w_Jt$kv$<9^jiQN@yPQ0@E|%a*Q!$u>nvTC=-XBGEM{K4xhQakS4se+~>X zqs$aD_c@4b+6a|(4w5EZ(dAWdN-$M%z1Y1 zeNpI%Yu@tVDLJ9~;S}XBP>;kvf}k_wB`j5*1*^Y+HdFf*eg7O7KFc0;YBW8@q(B}E z_l&BpFohMyeM!|@`k^StE7QfaHQ!s0#7;p7Fzvxf1lSHH!l22_GRw^T&Pg0kbq6Mv zq(CdjALvzGAqp!p4{lSvf#X;xc!E~0=o>X7YwDHFr`0ZdAFM0;!Id6GS+On;tVA*i zC99Za;Rahy%dCAk=)X+Iw`uyJxVzmDIrI> zS;|pupdQiZAlRqD!Y(A)9|iR%!7ww%%rFyA^XX)t!bFn^0Jpg@g-_}(MX6hlbpOGxqAPQT z?sZ?(bbGaRJez)HB5f6S*CRRSgmjT$+HSZCX<+mYFwRUd^UO*cZ>LC&=5}gC5iscd zObRL9*h5ug&md>YDN5px&(Awvb&wM)pzN6dQIMf5McVQ6OGE$(u$6g#$ofK6xUK&=r=1>g9GoaQ^UfZqXyMAv~=*Kf7f0$Xz=kT6*`6q|R z4O4|yx?<&ht3w|2D{Wwje{EVGru^T6L1vVhV&=Zv_$5)JX|hNZ!TozwCq)$Ry-H_e VeuE+y$x~I#zqV1~!g$^5_z%$|(a`_^ diff --git a/pg_data/pg_wal/000000010000000000000001 b/pg_data/pg_wal/000000010000000000000001 index 7307df456cc014c901bdcce70ab9dfe658e3d6bf..4a2f36e2618e8090fecb7cd462a8d83a201ecc17 100644 GIT binary patch delta 731 zcmXxbXH*RU0LAfF84uAz5fQROX7(t1COab|d&?f-$)AiUNzdL}Rw7$@_8!@LWRtzg z?VQ|mexL5WX0zF7PK6D&*pZI(WFR9(GLe}q*proPIA9_>Imn44PUIpt&g3C4F61LW zt`wjkg>a)VMJP%!ic^A;xKoPKl);0tl%qTqs7NI|sZ15BQjO}=peD7bO&z@OrY`lU zPXij#h{iObDa~k33w&saFRf@z8`{#2_H@9Hj&!0kUGS$X-RMpa0_aIEdeeu#^rJrm z7|0+7GlZcGBaq>YU?ig$%^1cqj`2(&h>1*MGG?X_%v4|+(+Ob)GnvI~LYc!{!U$&` z5zHr&1w>(CA&Xed5|*-z<*Z;Ot60q%*0PTEY+xgs*vuB9*~&JyvxA+)u!~rBvxhkL zvXA{7;2?)M%n{;A;ApJX%VY_6uo@kWCaaAhEx}3tK`DM}q8+TRwoU<-P0g}jtk@><@=TX`qR j@?Jj3NBJb5<%@ikZ}MG!$WQqtzvYkojq@<2xF!Dswb?K7 delta 651 zcmWl|2ULy$0EW>cA~HTRo3eNICNi^S??SRiMCMl>GO|kcRw^?@_Nt5`viGJ!`pMp} zd(JgY)5iRtOjwbbELfA3Y}k;U9N3bRT;wJX26@RxehN^KLfBE5A{3<<#VJ8aN>Lhn z94JFs%2A#QRHPD>sX|q%QJoq%Qj=QLrVe$fM|~R5kVZ772~BB6bDU^FOPpy%YueD3 zcC^QZ4s@gwo#{eXy3rk1deD}4O3?B@UniQ*84iRK7LImU5L zaFSD;<_u>!7Zu}V_*;w^jnz0kuNU;9Ueb6?(93#76ZNWI(3991 UKlPXX)<4k}