13 changed files with 258 additions and 92 deletions
-
4.env
-
22src/App.vue
-
1src/assets/menuIcon/n_user.svg
-
1src/assets/menuIcon/s_user.svg
-
2src/eventBus.ts
-
14src/router/index.ts
-
51src/services/axios.ts
-
64src/services/httpRequest.ts
-
21src/services/user/userManager.ts
-
6src/views/components/menu.ts
-
84src/views/login/index.vue
-
63src/views/userManage/UserManage.vue
-
7vite.config.ts
@ -1,3 +1,3 @@ |
|||
VITE_API_HOST=window.location.hostname |
|||
VITE_API_PORT=80 |
|||
VITE_API_HOST=127.0.0.1 |
|||
VITE_API_PORT=8080 |
|||
VITE_WS_PATH=/api/v1/app/ws/state |
@ -1,18 +1,26 @@ |
|||
<script setup lang="ts"> |
|||
import Header from './views/components/Header.vue' |
|||
import { useRouter } from "vue-router"; |
|||
import { exceptionOb } from "./services/httpRequest"; |
|||
const router = useRouter(); |
|||
|
|||
exceptionOb.subscribe(exp => { |
|||
if (exp === "invalidToken") { |
|||
router.replace("/login"); |
|||
} |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
<router-view></router-view> |
|||
</div> |
|||
<div> |
|||
<router-view></router-view> |
|||
</div> |
|||
</template> |
|||
|
|||
<style> |
|||
html, |
|||
body { |
|||
margin: 0; |
|||
padding: 0; |
|||
box-sizing: border-box; |
|||
margin: 0; |
|||
padding: 0; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="80" height="80" viewBox="0 0 80 80"><g><g><ellipse cx="40" cy="40" rx="40" ry="40" fill="#EEEFF8" fill-opacity="1"/></g><g><path d="M29.44471,32.3043C29.44472,37.9952,34.0687,42.608599999999996,39.772800000000004,42.608599999999996C45.4768,42.608599999999996,50.100899999999996,37.9952,50.100899999999996,32.3043C50.100899999999996,26.6134,45.4768,22,39.772800000000004,22C34.0687,22,29.44472,26.6134,29.44471,32.3043ZM55.678,46.5004L51.0002,46.5004C50.2702,46.5004,49.678200000000004,45.9097,49.678200000000004,45.1814C49.678200000000004,44.4531,50.2702,43.8625,51.0002,43.8625L55.678,43.8625C56.408,43.8625,57,44.4531,57,45.1814C57,45.9097,56.408,46.5004,55.678,46.5004ZM55.678,52.2502L47.580799999999996,52.2502C46.8508,52.2502,46.2588,51.6595,46.2588,50.931200000000004C46.2588,50.2029,46.8508,49.612300000000005,47.580799999999996,49.612300000000005L55.678,49.612300000000005C56.408,49.612300000000005,57,50.2029,57,50.931200000000004C57,51.6595,56.408,52.2502,55.678,52.2502ZM55.678,58L47.580799999999996,58C46.8508,58,46.2588,57.4094,46.2588,56.681C46.2588,55.9527,46.8508,55.3621,47.580799999999996,55.3621L55.678,55.3621C56.408,55.3621,57,55.9527,57,56.681C57,57.4094,56.408,58,55.678,58ZM41.260000000000005,52.9303C41.260000000000005,48.602900000000005,43.934200000000004,44.898700000000005,47.723299999999995,43.3724C48.3662,43.113600000000005,48.4289,42.229,47.825,41.8898C45.435,40.546099999999996,42.6911,39.782,39.772800000000004,39.782C30.50933,39.782,23,47.4797,23,56.9757C23,57.0475,23.000413211,57.1188,23.00123932,57.1901C23.0066098,57.6398,23.376768,58,23.827485,58L41.2753,58C41.865700000000004,58,42.262299999999996,57.4015,42.037099999999995,56.857C41.5364,55.6469,41.260000000000005,54.3209,41.260000000000005,52.9303Z" fill="#4F85FB" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg> |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="80" height="80" viewBox="0 0 80 80"><g><g><ellipse cx="40" cy="40" rx="40" ry="40" fill="#479CF1" fill-opacity="1"/></g><g><path d="M29.44471,32.3043C29.44472,37.9952,34.0687,42.608599999999996,39.772800000000004,42.608599999999996C45.4768,42.608599999999996,50.100899999999996,37.9952,50.100899999999996,32.3043C50.100899999999996,26.6134,45.4768,22,39.772800000000004,22C34.0687,22,29.44472,26.6134,29.44471,32.3043ZM55.678,46.5004L51.0002,46.5004C50.2702,46.5004,49.678200000000004,45.9097,49.678200000000004,45.1814C49.678200000000004,44.4531,50.2702,43.8625,51.0002,43.8625L55.678,43.8625C56.408,43.8625,57,44.4531,57,45.1814C57,45.9097,56.408,46.5004,55.678,46.5004ZM55.678,52.2502L47.580799999999996,52.2502C46.8508,52.2502,46.2588,51.6595,46.2588,50.931200000000004C46.2588,50.2029,46.8508,49.612300000000005,47.580799999999996,49.612300000000005L55.678,49.612300000000005C56.408,49.612300000000005,57,50.2029,57,50.931200000000004C57,51.6595,56.408,52.2502,55.678,52.2502ZM55.678,58L47.580799999999996,58C46.8508,58,46.2588,57.4094,46.2588,56.681C46.2588,55.9527,46.8508,55.3621,47.580799999999996,55.3621L55.678,55.3621C56.408,55.3621,57,55.9527,57,56.681C57,57.4094,56.408,58,55.678,58ZM41.260000000000005,52.9303C41.260000000000005,48.602900000000005,43.934200000000004,44.898700000000005,47.723299999999995,43.3724C48.3662,43.113600000000005,48.4289,42.229,47.825,41.8898C45.435,40.546099999999996,42.6911,39.782,39.772800000000004,39.782C30.50933,39.782,23,47.4797,23,56.9757C23,57.0475,23.000413211,57.1188,23.00123932,57.1901C23.0066098,57.6398,23.376768,58,23.827485,58L41.2753,58C41.865700000000004,58,42.262299999999996,57.4015,42.037099999999995,56.857C41.5364,55.6469,41.260000000000005,54.3209,41.260000000000005,52.9303Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg> |
@ -1,6 +1,6 @@ |
|||
import mitt from 'mitt' |
|||
type Events = { |
|||
menuId: number, |
|||
invalidToken: void, |
|||
} |
|||
|
|||
export const eventBus = mitt<Events>() |
@ -0,0 +1,64 @@ |
|||
import { Subject } from "rxjs"; |
|||
|
|||
export interface BaseResponse<T = unknown> { |
|||
success: boolean; |
|||
code: string; |
|||
msg: string; |
|||
data: T; |
|||
} |
|||
|
|||
type HttpReqParam = { |
|||
url: string; |
|||
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; |
|||
params?: Record<string, any>; |
|||
encode?: "form" | "json"; // 入参编码类型
|
|||
headers?: Record<string, any>; |
|||
}; |
|||
|
|||
export type ApiException = "invalidToken" | "serverError"; |
|||
|
|||
const exceptionSub = new Subject<ApiException>(); |
|||
export const exceptionOb = exceptionSub.asObservable(); |
|||
|
|||
function extHandle(res: BaseResponse) { |
|||
if (res.code === "A0230") { |
|||
// 访问令牌无效或已过期
|
|||
exceptionSub.next("invalidToken"); |
|||
} |
|||
return { |
|||
...res, |
|||
success: res.code === "00000", |
|||
}; |
|||
} |
|||
|
|||
export default async function httpRequest<T>({ url, method = "GET", params = {}, encode = "json", headers = {} }: HttpReqParam) { |
|||
const token = sessionStorage.getItem("token"); |
|||
if (token) { |
|||
headers = { Authorization: token, ...headers }; |
|||
} |
|||
if (method === "GET") { |
|||
const query = urlEncode(params); |
|||
const _url = query ? url + "?" + query : url; |
|||
const res = await fetch(_url, { headers }); |
|||
return res.json().then(res => extHandle(res) as T); |
|||
} else { |
|||
const body = encode === "json" ? JSON.stringify(params) : urlEncode(params); |
|||
const _headers = |
|||
encode === "json" |
|||
? { "Content-Type": "application/json; charset=utf-8", ...headers } |
|||
: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", ...headers }; |
|||
const res = await fetch(url, { method, headers: _headers, body }); |
|||
return res.json().then(res => extHandle(res) as T); |
|||
} |
|||
} |
|||
export function urlEncode(params?: Record<string, any>) { |
|||
let query = ""; |
|||
if (params && Object.keys(params).length > 0) { |
|||
const qs = []; |
|||
for (let attr in params) { |
|||
qs.push(`${attr}=${encodeURIComponent(params[attr])}`); |
|||
} |
|||
query = qs.join("&"); |
|||
} |
|||
return query; |
|||
} |
@ -0,0 +1,21 @@ |
|||
import httpRequest, { type BaseResponse } from "../httpRequest"; |
|||
|
|||
export function login(params: { username: string; password: string }) { |
|||
return httpRequest<BaseResponse<string>>({ url: `/api/auth/login`, method: "POST", params }); |
|||
} |
|||
|
|||
export type User = { |
|||
id: number; |
|||
username: string; |
|||
nickname: string; // 用户显示
|
|||
password: string; |
|||
role: number; // 1: admin
|
|||
}; |
|||
|
|||
export function getUserList(params: { pageNum: number; pageSize: number }) { |
|||
return httpRequest<BaseResponse<{ list: User[]; total: number }>>({ url: "/api/user/list", params }); |
|||
} |
|||
|
|||
export function getCurrentUser() { |
|||
return httpRequest<BaseResponse<User>>({ url: "/api/user/current" }); |
|||
} |
@ -1,51 +1,47 @@ |
|||
<template> |
|||
<div class="login-container"> |
|||
<!-- 背景图 --> |
|||
<div class="background-image"></div> |
|||
<!-- 登录表单 --> |
|||
<div class="login-form"> |
|||
<h2>登录</h2> |
|||
<!-- 用户名输入框 --> |
|||
<div class="login-user-input"> |
|||
<input |
|||
type="text" |
|||
v-model="username" |
|||
placeholder="用户名" |
|||
class="input-field" |
|||
/> |
|||
</div> |
|||
<!-- 密码输入框 --> |
|||
<div style="margin-top: 5%;"> |
|||
<input |
|||
type="password" |
|||
v-model="password" |
|||
placeholder="密码" |
|||
class="input-field" |
|||
/> |
|||
</div> |
|||
<!-- 登录按钮 --> |
|||
<button @click="handleLogin" class="login-button">登录</button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<div class="login-container"> |
|||
<!-- 背景图 --> |
|||
<div class="background-image"></div> |
|||
<!-- 登录表单 --> |
|||
<div class="login-form"> |
|||
<h2>登录</h2> |
|||
<!-- 用户名输入框 --> |
|||
<div class="login-user-input"> |
|||
<input type="text" v-model="username" placeholder="用户名" class="input-field" /> |
|||
</div> |
|||
<!-- 密码输入框 --> |
|||
<div style="margin-top: 5%"> |
|||
<input type="password" v-model="password" placeholder="密码" class="input-field" /> |
|||
</div> |
|||
<!-- 登录按钮 --> |
|||
<button @click="handleLogin" class="login-button">登录</button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router' |
|||
const router = useRouter() |
|||
// 定义用户名和密码的响应式变量 |
|||
const username = ref(''); |
|||
const password = ref(''); |
|||
<script setup lang="ts"> |
|||
import { ref } from "vue"; |
|||
import { useRouter } from "vue-router"; |
|||
import { showToast } from "vant"; |
|||
import { login } from "@/services/user/userManager"; |
|||
|
|||
// 处理登录的函数 |
|||
const handleLogin = () => { |
|||
console.log('用户名:', username.value); |
|||
console.log('密码:', password.value); |
|||
router.push('/home') |
|||
// 这里可以添加实际的登录逻辑,比如发送请求到后端进行验证 |
|||
}; |
|||
const router = useRouter(); |
|||
// 定义用户名和密码的响应式变量 |
|||
const username = ref(""); |
|||
const password = ref(""); |
|||
|
|||
// 处理登录的函数 |
|||
const handleLogin = async () => { |
|||
const res = await login({ username: username.value, password: password.value }); |
|||
if (res.success) { |
|||
sessionStorage.setItem("token", res.data); |
|||
router.push("/home"); |
|||
} else { |
|||
showToast(res.msg); |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
@import './login.css' |
|||
@import "./login.css"; |
|||
</style> |
@ -0,0 +1,63 @@ |
|||
<template> |
|||
<div class="component-page"> |
|||
<section class="flex items-center h-20 gap-3 pl-3"> |
|||
<button class="btn-light px-3 py-1 text-xs" @click="addUser">新增用户</button> |
|||
<button :disabled="opDisable" class="btn-light px-3 py-1 text-xs disabled:btn-light-disabled">删除用户</button> |
|||
</section> |
|||
|
|||
<section> |
|||
<header class="h-10 flex items-center bg-[#000]/[0.02] text-xs pr-3 text-text"> |
|||
<div class="w-10 self-stretch flex justify-center items-center"> |
|||
<img src="@/assets/Icon-unselect.svg" alt="icon" /> |
|||
</div> |
|||
<p class="w-16">用户名称</p> |
|||
<p>权限</p> |
|||
</header> |
|||
|
|||
<div v-for="user in userList" class="h-10 flex items-center text-xs pr-3 text-[#6e6e6e] border-b border-b-[#f8f8f8]"> |
|||
<div class="w-10 self-stretch flex justify-center items-center"> |
|||
<img :src="isSelect ? icon_select : icon_unselect" alt="" /> |
|||
</div> |
|||
<p class="w-16">{{ user.nickname }}</p> |
|||
<p class="flex-auto">{{ user.role === 1 ? "管理员" : "用户" }}</p> |
|||
</div> |
|||
</section> |
|||
|
|||
<van-overlay :show="showEditDialog"> |
|||
<div class="flex justify-center items-center h-full"></div> |
|||
</van-overlay> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import icon_unselect from "@/assets/Icon-unselect.svg"; |
|||
import icon_select from "@/assets/Icon-select.svg"; |
|||
import { showToast } from "vant"; |
|||
import { onMounted, ref } from "vue"; |
|||
import { getCurrentUser, getUserList, type User } from "@/services/user/userManager"; |
|||
|
|||
const isSelect = ref<boolean>(false); |
|||
const opDisable = ref<boolean>(true); |
|||
const showEditDialog = ref<boolean>(false); |
|||
|
|||
const userList = ref<User[]>([]); |
|||
|
|||
onMounted(() => { |
|||
getUserList({ pageNum: 1, pageSize: 9999 }).then(res => { |
|||
if (res.success) { |
|||
userList.value = res.data.list; |
|||
} else { |
|||
showToast(res.msg); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
function addUser() { |
|||
// TEST |
|||
getCurrentUser().then(res => { |
|||
if (res.success) { |
|||
console.log("current user:", res.data) |
|||
} |
|||
}) |
|||
} |
|||
</script> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue