diff --git a/.env.development b/.env.development index e15b6ab..05ddc98 100644 --- a/.env.development +++ b/.env.development @@ -4,7 +4,7 @@ NODE_ENV = development # 下面是自定义的环境变量,可以修改(命名必须以 VITE_ 开头) ## 后端接口公共路径(如果解决跨域问题采用反向代理就只需写公共路径) -VITE_BASE_API = '/api/v1' +VITE_BASE_API = 'http://192.168.1.1:8001' ## 路由模式 hash 或 html5 VITE_ROUTER_HISTORY = 'hash' diff --git a/package.json b/package.json index f211aa0..5df27b5 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,10 @@ "path-to-regexp": "^6.2.1", "pinia": "^2.0.33", "screenfull": "^6.0.2", + "ts-md5": "^1.3.1", "vue": "^3.2.47", "vue-router": "^4.1.6", + "vue3-slide-verify": "^1.1.4", "vxe-table": "^4.3.10", "vxe-table-plugin-element": "^3.0.6", "xe-utils": "^3.5.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f47e83..9286366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,12 +37,18 @@ dependencies: screenfull: specifier: ^6.0.2 version: 6.0.2 + ts-md5: + specifier: ^1.3.1 + version: 1.3.1 vue: specifier: ^3.2.47 version: 3.2.47 vue-router: specifier: ^4.1.6 version: 4.1.6(vue@3.2.47) + vue3-slide-verify: + specifier: ^1.1.4 + version: 1.1.4 vxe-table: specifier: ^4.3.10 version: 4.3.10(vue@3.2.47)(xe-utils@3.5.7) @@ -4762,6 +4768,11 @@ packages: resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==} dev: true + /ts-md5@1.3.1: + resolution: {integrity: sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg==} + engines: {node: '>=12'} + dev: false + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true @@ -5151,6 +5162,12 @@ packages: typescript: 4.9.5 dev: true + /vue3-slide-verify@1.1.4: + resolution: {integrity: sha512-er2d9TSPsF5CcmoxBfP6eSkc4IHROwXu6Ytghkwf52apXpXy2ZtW3tOgHnmFWb3GHESKxZ9bsFXqSX+fJ6hjrg==} + dependencies: + vue: 3.2.47 + dev: false + /vue@3.2.47: resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==} dependencies: diff --git a/src/api/hook-demo/use-fetch-select.ts b/src/api/hook-demo/use-fetch-select.ts deleted file mode 100644 index 8815a60..0000000 --- a/src/api/hook-demo/use-fetch-select.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** 模拟接口响应数据 */ -const SELECT_RESPONSE_DATA = { - code: 0, - data: [ - { - label: "苹果", - value: 1 - }, - { - label: "香蕉", - value: 2 - }, - { - label: "橘子", - value: 3, - disabled: true - } - ], - message: "获取 Select 数据成功" -} - -/** 模拟接口 */ -export function getSelectDataApi() { - return new Promise((resolve, reject) => { - // 模拟接口响应时间 2s - setTimeout(() => { - // 模拟接口调用成功 - if (Math.random() < 0.8) { - resolve(SELECT_RESPONSE_DATA) - } else { - // 模拟接口调用出错 - reject(new Error("接口发生错误")) - } - }, 2000) - }) -} diff --git a/src/api/hook-demo/use-fullscreen-loading.ts b/src/api/hook-demo/use-fullscreen-loading.ts deleted file mode 100644 index c8cf7ec..0000000 --- a/src/api/hook-demo/use-fullscreen-loading.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** 模拟接口响应数据 */ -const SUCCESS_RESPONSE_DATA = { - code: 0, - data: {}, - message: "获取成功" -} - -/** 模拟请求接口成功 */ -export function getSuccessApi() { - return new Promise((resolve) => { - setTimeout(() => { - resolve(SUCCESS_RESPONSE_DATA) - }, 1000) - }) -} - -/** 模拟请求接口失败 */ -export function getErrorApi() { - return new Promise((_resolve, reject) => { - setTimeout(() => { - reject(new Error("发生错误")) - }, 1000) - }) -} diff --git a/src/api/login/index.ts b/src/api/login/index.ts index 294f2e4..603f5c0 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -1,5 +1,8 @@ import { request } from "@/utils/service" import type * as Login from "./types/login" +import type * as Requests from "@/api/types/requests" +import { Md5 } from "ts-md5" +import type * as AxiosType from "axios" /** 获取登录验证码 */ export function getLoginCodeApi() { @@ -11,10 +14,20 @@ export function getLoginCodeApi() { /** 登录并返回 Token */ export function loginApi(data: Login.ILoginRequestData) { - return request({ - url: "users/login", + const md5er = new Md5() + md5er.appendStr(data.password) + data.password = md5er.end(false) as string + const urlSearchParams = new URLSearchParams() + urlSearchParams.append("username", data.username) + urlSearchParams.append("password", data.password) + return request>>({ + url: "api/auth/login", method: "post", - data + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + data: urlSearchParams, + withCredentials: true }) } diff --git a/src/api/login/types/login.ts b/src/api/login/types/login.ts index ae2362a..cae301e 100644 --- a/src/api/login/types/login.ts +++ b/src/api/login/types/login.ts @@ -1,14 +1,11 @@ export interface ILoginRequestData { - /** admin 或 editor */ - username: "admin" | "editor" - /** 密码 */ + username: string password: string - /** 验证码 */ - code: string } -export type LoginCodeResponseData = IApiResponseData - -export type LoginResponseData = IApiResponseData<{ token: string }> - -export type UserInfoResponseData = IApiResponseData<{ username: string; roles: string[] }> +export interface ILoginResponseData { + message: string + id: number + role: number + cookie: string +} diff --git a/src/api/register/index.ts b/src/api/register/index.ts new file mode 100644 index 0000000..f70549f --- /dev/null +++ b/src/api/register/index.ts @@ -0,0 +1,26 @@ +import * as Register from "@/api/register/types/register" +import { Md5 } from "ts-md5" +import { request } from "@/utils/service" +import * as AxiosType from "axios/index" +import * as Requests from "@/api/types/requests" + +export function registerApi(data: Register.IRegisterRequestData) { + const md5er = new Md5() + md5er.appendStr(data.password) + data.password = md5er.end(false) as string + const requestData = new URLSearchParams() + requestData.append("username", data.username) + requestData.append("password", data.password) + requestData.append("relName", data.relName) + requestData.append("email", data.email) + requestData.append("roleId", data.roleId.toString()) + return request>>({ + url: "/api/auth/register", + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + data: requestData, + withCredentials: true + }) +} diff --git a/src/api/register/types/register.ts b/src/api/register/types/register.ts new file mode 100644 index 0000000..5bd57ac --- /dev/null +++ b/src/api/register/types/register.ts @@ -0,0 +1,7 @@ +export interface IRegisterRequestData { + username: string + email: string + relName: string + password: string + roleId: number +} diff --git a/src/api/types/requests.ts b/src/api/types/requests.ts new file mode 100644 index 0000000..dc4aac0 --- /dev/null +++ b/src/api/types/requests.ts @@ -0,0 +1,11 @@ +export interface IResponseData { + timestamp: number + message: string + status: number + data: T +} + +export interface IPageResponseData { + total: number + info: T[] +} diff --git a/src/api/users/index.ts b/src/api/users/index.ts new file mode 100644 index 0000000..51f648a --- /dev/null +++ b/src/api/users/index.ts @@ -0,0 +1,51 @@ +import { request } from "@/utils/service" +import * as AxiosType from "axios" +import * as Requests from "@/api/types/requests" +import { + IChangeUserStatus, + IDelUserResponseData, + IExChangeAccountRequestsData, + IExChangeUserResponseData, + IGetAllUserResponseData +} from "@/api/users/types/users" + +export function getAllUsersApi(page: number) { + return request>>({ + url: "/account/getUser", + method: "get", + params: { + page: page, + num: 10 + }, + withCredentials: true + }) +} +export function delUserApi(userId: number) { + return request>>({ + url: "/account/delUser", + method: "get", + params: { + id: userId + }, + withCredentials: true + }) +} +export function exChangeUserDataApi(newUser: IExChangeAccountRequestsData) { + return request>>({ + url: "/account/changeAccount", + method: "post", + data: newUser, + withCredentials: true + }) +} +export function changeUserStatusApi(userid: number, newStatus: boolean) { + return request>>({ + url: "/account/changeStatus", + method: "get", + params: { + userId: userid, + status: newStatus + }, + withCredentials: true + }) +} diff --git a/src/api/users/types/users.ts b/src/api/users/types/users.ts new file mode 100644 index 0000000..46f70c2 --- /dev/null +++ b/src/api/users/types/users.ts @@ -0,0 +1,15 @@ +import { IAccount } from "@/views/user/types" +import { IPageResponseData } from "@/api/types/requests" +export interface IExChangeAccountRequestsData { + id: number + username: string + email: string + relName: string + roleId: number +} + +export type IDelUserResponseData = string +export type IExChangeUserResponseData = string +export type IChangeUserStatus = string + +export type IGetAllUserResponseData = IPageResponseData diff --git a/src/entities/OrderData.ts b/src/entities/OrderData.ts new file mode 100644 index 0000000..0fa4dba --- /dev/null +++ b/src/entities/OrderData.ts @@ -0,0 +1,3 @@ +export interface OrderData { + id: number +} diff --git a/src/router/index.ts b/src/router/index.ts index ec31de4..6240823 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -55,16 +55,41 @@ export const constantRoutes: RouteRecordRaw[] = [ } } ] + } +] + +/** + * 动态路由 + * 用来放置有权限 (Roles 属性) 的路由 + * 必须带有 Name 属性 + */ +export const asyncRoutes: RouteRecordRaw[] = [ + { + path: "/user", + component: Layout, + redirect: "/user", + children: [ + { + path: "user", + component: () => import("@/views/user/index.vue"), + name: "User", + meta: { + title: "用户管理", + elIcon: "User", + roles: ["admin"] + } + } + ] }, { path: "/order", component: Layout, - redirect: "/order/", + redirect: "/order/manager", name: "Order", meta: { title: "订单", elIcon: "Files", - roles: ["admin", "editor"], + roles: ["admin"], alwaysShow: true }, children: [ @@ -74,7 +99,7 @@ export const constantRoutes: RouteRecordRaw[] = [ name: "CreateOrder", meta: { title: "创建订单", - roles: ["editor"], + roles: ["admin", "editor"], elIcon: "FolderAdd" } }, @@ -89,15 +114,7 @@ export const constantRoutes: RouteRecordRaw[] = [ } } ] - } -] - -/** - * 动态路由 - * 用来放置有权限 (Roles 属性) 的路由 - * 必须带有 Name 属性 - */ -export const asyncRoutes: RouteRecordRaw[] = [ + }, { path: "/permission", component: Layout, diff --git a/src/router/permission.ts b/src/router/permission.ts index 290f9a5..42cc5cd 100644 --- a/src/router/permission.ts +++ b/src/router/permission.ts @@ -15,7 +15,7 @@ router.beforeEach(async (to, _from, next) => { const userStore = useUserStoreHook() const permissionStore = usePermissionStoreHook() // 判断该用户是否登录 - if (getToken()) { + if (getToken() && getToken() !== -1) { if (to.path === "/login") { // 如果已经登录,并准备进入 Login 页面,则重定向到主页 next({ path: "/" }) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 97bd4bd..7ec8b64 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -5,13 +5,14 @@ import { usePermissionStore } from "./permission" import { useTagsViewStore } from "./tags-view" import { getToken, removeToken, setToken } from "@/utils/cache/cookies" import router, { resetRouter } from "@/router" -import { loginApi, getUserInfoApi } from "@/api/login" +import { loginApi } from "@/api/login" import { type ILoginRequestData } from "@/api/login/types/login" import { type RouteRecordRaw } from "vue-router" import asyncRouteSettings from "@/config/async-route" export const useUserStore = defineStore("user", () => { - const token = ref(getToken() || "") + /** Token即用户id */ + const token = ref(getToken() || -1) const roles = ref([]) const username = ref("") @@ -25,15 +26,14 @@ export const useUserStore = defineStore("user", () => { /** 登录 */ const login = (loginData: ILoginRequestData) => { return new Promise((resolve, reject) => { - loginApi({ - username: loginData.username, - password: loginData.password, - code: loginData.code - }) + loginApi(loginData) .then((res) => { - setToken(res.data.token) - token.value = res.data.token - resolve(true) + if (res.status == 202) { + setToken(res.data.data.id) + resolve(res.data.data) + return + } + reject(res.data.data) }) .catch((error) => { reject(error) @@ -42,30 +42,31 @@ export const useUserStore = defineStore("user", () => { } /** 获取用户详情 */ const getInfo = () => { - return new Promise((resolve, reject) => { - getUserInfoApi() - .then((res) => { - const data = res.data - username.value = data.username - // 验证返回的 roles 是否是一个非空数组 - if (data.roles && data.roles.length > 0) { - roles.value = data.roles - } else { - // 塞入一个没有任何作用的默认角色,不然路由守卫逻辑会无限循环 - roles.value = asyncRouteSettings.defaultRoles - } - resolve(res) - }) - .catch((error) => { - reject(error) - }) - }) + roles.value = asyncRouteSettings.defaultRoles + roles.value = ["admin"] + console.log("getInfo()") + // return new Promise((resolve, reject) => { + // getUserInfoApi() + // .then((res) => { + // const data = res.data + // username.value = data.username + // // 验证返回的 roles 是否是一个非空数组 + // if (data.roles && data.roles.length > 0) { + // roles.value = data.roles + // } else { + // // 塞入一个没有任何作用的默认角色,不然路由守卫逻辑会无限循环 + // roles.value = asyncRouteSettings.defaultRoles + // } + // resolve(res) + // }) + // .catch((error) => { + // reject(error) + // }) + // }) } /** 切换角色 */ const changeRoles = async (role: string) => { - const newToken = "token-" + role - token.value = newToken - setToken(newToken) + console.log(role) await getInfo() permissionStore.setRoutes(roles.value) resetRouter() diff --git a/src/utils/cache/cookies.ts b/src/utils/cache/cookies.ts index 537d6c6..6f7623d 100644 --- a/src/utils/cache/cookies.ts +++ b/src/utils/cache/cookies.ts @@ -3,11 +3,14 @@ import CacheKey from "@/constants/cacheKey" import Cookies from "js-cookie" -export const getToken = () => { - return Cookies.get(CacheKey.TOKEN) +export const getToken = (): number | undefined => { + const tokenString = Cookies.get(CacheKey.TOKEN) + if (tokenString) { + return Number.parseInt(tokenString) + } } -export const setToken = (token: string) => { - Cookies.set(CacheKey.TOKEN, token) +export const setToken = (token: number) => { + Cookies.set(CacheKey.TOKEN, token.toString()) } export const removeToken = () => { Cookies.remove(CacheKey.TOKEN) diff --git a/src/utils/service.ts b/src/utils/service.ts index bd76b24..7ec1b74 100644 --- a/src/utils/service.ts +++ b/src/utils/service.ts @@ -1,8 +1,8 @@ import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios" -import { useUserStoreHook } from "@/store/modules/user" -import { ElMessage } from "element-plus" import { get } from "lodash-es" -import { getToken } from "./cache/cookies" +import { useRouter } from "vue-router" +import { useUserStore } from "@/store/modules/user" +import { ElMessage } from "element-plus" /** 创建请求实例 */ function createService() { @@ -10,77 +10,25 @@ function createService() { const service = axios.create() // 请求拦截 service.interceptors.request.use( - (config) => config, + (config) => { + return config + }, // 发送失败 (error) => Promise.reject(error) ) // 响应拦截(可根据具体业务作出相应的调整) service.interceptors.response.use( (response) => { - // apiData 是 API 返回的数据 - const apiData = response.data as any - // 这个 Code 是和后端约定的业务 Code - const code = apiData.code - // 如果没有 Code, 代表这不是项目后端开发的 API - if (code === undefined) { - ElMessage.error("非本系统的接口") - return Promise.reject(new Error("非本系统的接口")) - } else { - switch (code) { - case 0: - // code === 0 代表没有错误 - return apiData - default: - // 不是正确的 Code - ElMessage.error(apiData.message || "Error") - return Promise.reject(new Error("Error")) - } - } + return response }, (error) => { - // Status 是 HTTP 状态码 - const status = get(error, "response.status") - switch (status) { - case 400: - error.message = "请求错误" - break - case 401: - // Token 过期时,直接退出登录并强制刷新页面(会重定向到登录页) - useUserStoreHook().logout() - location.reload() - break - case 403: - error.message = "拒绝访问" - break - case 404: - error.message = "请求地址出错" - break - case 408: - error.message = "请求超时" - break - case 500: - error.message = "服务器内部错误" - break - case 501: - error.message = "服务未实现" - break - case 502: - error.message = "网关错误" - break - case 503: - error.message = "服务不可用" - break - case 504: - error.message = "网关超时" - break - case 505: - error.message = "HTTP 版本不受支持" - break - default: - break + if (error.response.status === 555) { + const router = useRouter() + const userStore = useUserStore() + userStore.logout() + router.push("/login").then((_) => ElMessage.warning("登录过期,请重新登录!")) } - ElMessage.error(error.message) - return Promise.reject(error) + return error } ) return service @@ -91,13 +39,12 @@ function createRequestFunction(service: AxiosInstance) { return function (config: AxiosRequestConfig): Promise { const configDefault = { headers: { - // 携带 Token - Authorization: "Bearer " + getToken(), "Content-Type": get(config, "headers.Content-Type", "application/json") }, timeout: 5000, baseURL: import.meta.env.VITE_BASE_API, - data: {} + data: {}, + widthCredentials: true } return service(Object.assign(configDefault, config)) } diff --git a/src/views/login/index.vue b/src/views/login/index.vue index d68f1e0..6366157 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -2,76 +2,89 @@ import { reactive, ref } from "vue" import { useRouter } from "vue-router" import { useUserStore } from "@/store/modules/user" -import { User, Lock, Key, Picture, Loading } from "@element-plus/icons-vue" +import { User, Lock } from "@element-plus/icons-vue" import ThemeSwitch from "@/components/ThemeSwitch/index.vue" -import { type FormInstance, FormRules } from "element-plus" -import { getLoginCodeApi } from "@/api/login" +import { ElMessage, type FormInstance, FormRules } from "element-plus" +import SlideVerify, { SlideVerifyInstance } from "vue3-slide-verify" +import "vue3-slide-verify/dist/style.css" import { type ILoginRequestData } from "@/api/login/types/login" +const isShowVerifyDialog = ref(false) const router = useRouter() const loginFormRef = ref(null) /** 登录按钮 Loading */ const loading = ref(false) -/** 验证码图片 URL */ -const codeUrl = ref("") /** 登录表单数据 */ const loginForm: ILoginRequestData = reactive({ - username: "admin", - password: "12345678", - code: "" + username: "", + password: "" }) /** 登录表单校验规则 */ const loginFormRules: FormRules = { - username: [{ required: true, message: "请输入用户名", trigger: "blur" }], + username: [ + { required: true, message: "请输入用户名", trigger: "blur" }, + { min: 3, max: 10, message: "长度在 3 到 10 个字符", trigger: "blur" }, + { pattern: "[A-Za-z0-9_-一-龥]+", message: "用户名不符合要求" } + ], password: [ { required: true, message: "请输入密码", trigger: "blur" }, { min: 8, max: 16, message: "长度在 8 到 16 个字符", trigger: "blur" } - ], - code: [{ required: true, message: "请输入验证码", trigger: "blur" }] + ] } /** 登录逻辑 */ const handleLogin = () => { loginFormRef.value?.validate((valid: boolean) => { if (valid) { - loading.value = true - useUserStore() - .login({ - username: loginForm.username, - password: loginForm.password, - code: loginForm.code - }) - .then(() => { - router.push({ path: "/" }) - }) - .catch(() => { - createCode() - loginForm.password = "" - }) - .finally(() => { - loading.value = false - }) + sliderVerify.value?.refresh() + isShowVerifyDialog.value = true } else { return false } }) } -/** 创建验证码 */ -const createCode = () => { - // 先清空验证码的输入 - loginForm.code = "" - // 获取验证码 - codeUrl.value = "" - getLoginCodeApi().then((res) => { - codeUrl.value = res.data - }) +const sliderVerify = ref() +const onVerifyRetry = () => { + ElMessage.warning("检测到非人为操作的哦! try again") } -/** 初始化验证码 */ -createCode() +const onVerifySuccess = (_: number) => { + isShowVerifyDialog.value = false + loading.value = true + useUserStore() + .login({ + username: loginForm.username, + password: loginForm.password + }) + .then(() => { + router.push({ path: "/" }) + ElMessage.success("登录成功!") + }) + .catch(() => { + loginForm.password = "" + ElMessage.error("登录失败,用户名或密码错误") + }) + .finally(() => { + loading.value = false + }) +} + +const onVerifyFail = () => { + ElMessage.error("验证失败,请重试!") +}