Browse Source

添加用户管理接口

feature/home
zhangjiming 6 months ago
parent
commit
e943304e97
  1. 4
      .env
  2. 10
      src/App.vue
  3. 1
      src/assets/menuIcon/n_user.svg
  4. 1
      src/assets/menuIcon/s_user.svg
  5. 2
      src/eventBus.ts
  6. 14
      src/router/index.ts
  7. 51
      src/services/axios.ts
  8. 64
      src/services/httpRequest.ts
  9. 21
      src/services/user/userManager.ts
  10. 6
      src/views/components/menu.ts
  11. 54
      src/views/login/index.vue
  12. 63
      src/views/userManage/UserManage.vue
  13. 7
      vite.config.ts

4
.env

@ -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 VITE_WS_PATH=/api/v1/app/ws/state

10
src/App.vue

@ -1,5 +1,13 @@
<script setup lang="ts"> <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> </script>
<template> <template>

1
src/assets/menuIcon/n_user.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="#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>

1
src/assets/menuIcon/s_user.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>

2
src/eventBus.ts

@ -1,6 +1,6 @@
import mitt from 'mitt' import mitt from 'mitt'
type Events = { type Events = {
menuId: number,
invalidToken: void,
} }
export const eventBus = mitt<Events>() export const eventBus = mitt<Events>()

14
src/router/index.ts

@ -1,9 +1,11 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import UserManage from '@/views/userManage/UserManage.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/',
path: '/login',
name: 'login', name: 'login',
//@ts-ignore //@ts-ignore
component: () => import('../views/login/index.vue'), component: () => import('../views/login/index.vue'),
@ -51,13 +53,13 @@ const router = createRouter({
name: 'oreManage', name: 'oreManage',
component: () => import('../views/oreManage/index.vue'), component: () => import('../views/oreManage/index.vue'),
}, },
{
path: '/userManage',
name: 'userManage',
component: UserManage
}
] ]
}, },
], ],
}) })

51
src/services/axios.ts

@ -1,6 +1,7 @@
import axios from "axios";
import { eventBus } from "@/eventBus";
import axios, { AxiosHeaders } from "axios";
const url = `${window.location.protocol}://${import.meta.env.VITE_API_HOST}:${import.meta.env.VITE_API_PORT}`;
const url = `http://${import.meta.env.VITE_API_HOST}:${import.meta.env.VITE_API_PORT}`;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: url, // 设置请求的根路径 baseURL: url, // 设置请求的根路径
@ -11,37 +12,33 @@ const apiClient = axios.create({
}); });
// 请求拦截器 // 请求拦截器
// apiClient.interceptors.request.use(
// (config) => {
// const token = sessionStorage.getItem("token");
// if (!config.headers) {
// config.headers = AxiosHeaders.from({}); // 确保 config.headers 是 AxiosHeaders 类型
// }
apiClient.interceptors.request.use(
config => {
const token = sessionStorage.getItem("token");
if (!config.headers) {
config.headers = AxiosHeaders.from({}); // 确保 config.headers 是 AxiosHeaders 类型
}
// if (token) {
// config.headers.set("Authorization", `Bearer ${encodeURIComponent(token)}`); // 使用 set 方法设置 Authorization
// }
if (token) {
config.headers.set("Authorization", token);
}
// return config;
// },
// error => {
// return Promise.reject(error);
// }
// );
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器 // 响应拦截器
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => {
if (response.data && response.data.dataType === "ZAppPromopt") {
if (response.data.ecode === "USR_NOT_EXIT") {
return Promise.resolve(response.data);
} else if (response.data.ecode === "USR_PASSWORD_ERROR") {
return Promise.resolve(response.data);
} else {
console.log("接口出错", response.data);
// eventBus.emit("show-error-modal", response.data.data);
return Promise.reject(response.data);
response => {
if (response.data) {
if (response.data.code === "A0230") {
// 访问令牌无效或已过期
eventBus.emit("invalidToken");
} }
return Promise.resolve(response);
} }
return response; return response;
}, },

64
src/services/httpRequest.ts

@ -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;
}

21
src/services/user/userManager.ts

@ -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" });
}

6
src/views/components/menu.ts

@ -34,4 +34,10 @@ export const menuIcon = [{
s_icon: '/src/assets/menuIcon/s_ore.svg', s_icon: '/src/assets/menuIcon/s_ore.svg',
n_icon: '/src/assets/menuIcon/n_ore.svg', n_icon: '/src/assets/menuIcon/n_ore.svg',
path: '/oreManage', path: '/oreManage',
},{
id: 6,
name: '用户管理',
s_icon: '/src/assets/menuIcon/s_user.svg',
n_icon: '/src/assets/menuIcon/n_user.svg',
path: '/userManage',
}] }]

54
src/views/login/index.vue

@ -7,45 +7,41 @@
<h2>登录</h2> <h2>登录</h2>
<!-- 用户名输入框 --> <!-- 用户名输入框 -->
<div class="login-user-input"> <div class="login-user-input">
<input
type="text"
v-model="username"
placeholder="用户名"
class="input-field"
/>
<input type="text" v-model="username" placeholder="用户名" class="input-field" />
</div> </div>
<!-- 密码输入框 --> <!-- 密码输入框 -->
<div style="margin-top: 5%;">
<input
type="password"
v-model="password"
placeholder="密码"
class="input-field"
/>
<div style="margin-top: 5%">
<input type="password" v-model="password" placeholder="密码" class="input-field" />
</div> </div>
<!-- 登录按钮 --> <!-- 登录按钮 -->
<button @click="handleLogin" class="login-button">登录</button> <button @click="handleLogin" class="login-button">登录</button>
</div> </div>
</div> </div>
</template>
</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> </script>
<style scoped> <style scoped>
@import './login.css'
@import "./login.css";
</style> </style>

63
src/views/userManage/UserManage.vue

@ -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>

7
vite.config.ts

@ -16,5 +16,12 @@ export default defineConfig({
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 5174, port: 5174,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}, },
}); });
Loading…
Cancel
Save