Forráskód Böngészése

提交增加简易账户体系

DESKTOP-HN5QP3V\Administrator 2 hónapja
szülő
commit
ddef3584ff

+ 4 - 1
.env.pro

@@ -35,4 +35,7 @@ VITE_USE_MOCK=true
 VITE_USE_CSS_SPLIT=true
 
 # 是否使用在线图标
-VITE_USE_ONLINE_ICON=true
+VITE_USE_ONLINE_ICON=true
+
+# 打包路径
+VITE_BASE_PATH=/vcenter/

+ 2 - 0
.eslintrc.js

@@ -27,6 +27,8 @@ module.exports = defineConfig({
     '@typescript-eslint/no-empty-function': 'off',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     'vue/multi-word-component-names': 'off',
+    'vue/no-reserved-component-names': 'off',
+    '@typescript-eslint/ban-types': 'off',
     'no-console': 'off'
   }
 })

+ 102 - 0
PROJECT_SPEC.md

@@ -0,0 +1,102 @@
+# 项目规范文档
+
+本文档旨在概述 `vue-element-plus-admin-mini` 项目的技术栈、结构、规范和可用命令,为新加入的开发者提供指引。
+
+## 1. 项目概述
+
+这是一个基于 Vue 3、Vite 4、Element Plus 和 TypeScript 的后台管理系统集成方案。它提供了一套完整的、可复用的后台开发模板。
+
+- **项目名称**: `vue-element-plus-admin`
+- **版本**: `2.5.6`
+- **作者**: Archer
+- **仓库**: [GitHub](https://github.com/kailong321200875/vue-element-plus-admin)
+
+## 2. 技术栈
+
+项目采用了一系列现代前端技术,核心技术栈如下:
+
+- **核心框架**: Vue 3
+- **构建工具**: Vite 4
+- **UI 库**: Element Plus
+- **编程语言**: TypeScript
+- **状态管理**: Pinia
+- **路由**: Vue Router
+- **HTTP 请求**: Axios
+- **CSS 预处理器**: Less
+- **原子化 CSS**: UnoCSS
+
+## 3. 项目结构
+
+以下是项目的主要目录结构及其功能说明:
+
+```
+.
+├── public/              # 静态资源,不会被 Vite 处理
+├── src/
+│   ├── api/             # API 请求模块
+│   ├── assets/          # 静态资源 (图片、SVG)
+│   ├── components/      # 全局公共组件
+│   ├── hooks/           # 自定义 Hooks
+│   ├── layout/          # 布局组件
+│   ├── locales/         # 国际化语言文件
+│   ├── plugins/         # Vite 插件配置
+│   ├── router/          # 路由配置
+│   ├── store/           # Pinia 状态管理
+│   ├── styles/          # 全局样式
+│   ├── utils/           # 工具函数
+│   ├── views/           # 页面视图
+│   ├── App.vue          # 根组件
+│   └── main.ts          # 应用入口文件
+├── .env.*               # 不同环境的配置文件
+├── .eslintrc.js         # ESLint 配置文件
+├── prettier.config.js   # Prettier 配置文件
+├── stylelint.config.js  # Stylelint 配置文件
+├── vite.config.ts       # Vite 配置文件
+└── package.json         # 项目依赖和脚本
+```
+
+## 4. 编码规范
+
+项目强制实施一套编码和风格规范,以确保代码的一致性和可读性。
+
+### JavaScript/TypeScript (ESLint)
+
+- **基础配置**: `plugin:vue/vue3-essential`, `plugin:@typescript-eslint/recommended`
+- **风格集成**: 与 `Prettier` 集成以处理格式问题。
+- **自定义规则**:
+  - 允许使用 `any` 类型 (`@typescript-eslint/no-explicit-any: 'off'`)。
+  - 允许未使用的变量 (`@typescript-eslint/no-unused-vars: 'off'`)。
+  - 允许多词组件名称关闭 (`vue/multi-word-component-names: 'off'`)。
+  - 允许使用 `console` (`no-console: 'off'`)。
+- **目的**: 保持相对宽松的检查,主要关注潜在的逻辑错误而非严格的风格限制。
+
+### 代码格式化 (Prettier)
+
+- **单引号**: `true`
+- **分号**: `false`
+- **Tab 宽度**: `2`
+- **行宽**: `100`
+- **尾随逗号**: `none`
+
+### 样式 (Stylelint)
+
+- **配置**: 使用 `stylelint-config-standard` 和 `stylelint-order`。
+- **目的**: 保证 CSS/Less 代码的顺序和规范性。
+
+### Git 提交 (Husky & Commitlint)
+
+- **提交信息**: 遵循 `conventional-changelog` 规范。
+- **预提交钩子**: 在提交前会自动运行 `lint-staged`,对暂存区的文件执行 ESLint 和 Prettier 检查。
+
+## 5. 可用命令
+
+通过 `pnpm` 运行以下脚本来管理项目:
+
+- **`pnpm i`**: 安装所有项目依赖。
+- **`pnpm dev`**: 启动开发服务器,并开启热更新。
+- **`pnpm ts:check`**: 对整个项目进行 TypeScript 类型检查。
+- **`pnpm build:pro`**: 构建用于生产环境的最终代码。
+- **`pnpm serve:pro`**: 在本地预览生产环境的构建成果。
+- **`pnpm lint:eslint`**: 运行 ESLint 检查 `.js`, `.ts`, `.vue` 文件并尝试自动修复。
+- **`pnpm lint:format`**: 使用 Prettier 格式化所有支持的文件类型。
+- **`pnpm lint:style`**: 运行 Stylelint 检查样式文件并尝试自动修复。

+ 9 - 5
src/api/login/index.ts

@@ -1,16 +1,20 @@
 import request from '@/axios'
-import type { UserType } from './types'
+import type { ChangePasswordType, UserLoginType, UserType } from './types'
 
 interface RoleParams {
   roleName: string
 }
 
-export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
-  return request.post({ url: '/mock/user/login', data })
+export const loginApi = (data: UserLoginType): Promise<IResponse<UserType>> => {
+  return request.post({ url: '/web/v1/auth/login', data })
 }
 
-export const loginOutApi = (): Promise<IResponse> => {
-  return request.get({ url: '/mock/user/loginOut' })
+export const changePasswordApi = (data: ChangePasswordType): Promise<IResponse> => {
+  return request.put({ url: '/web/v1/auth/password', data })
+}
+
+export const loginOutApi = (): Promise<IResponse<null>> => {
+  return Promise.resolve({ code: 200, data: null })
 }
 
 export const getAdminRoleApi = (

+ 6 - 1
src/api/login/types.ts

@@ -5,8 +5,13 @@ export type UserLoginType = {
 
 export type UserType = {
   username: string
-  password: string
   role: string
   roleId: string
   permissions: string | string[]
+  token: string
+}
+
+export type ChangePasswordType = {
+  oldPassword: string
+  newPassword: string
 }

+ 5 - 1
src/axios/config.ts

@@ -6,6 +6,10 @@ import { useUserStoreWithOut } from '@/store/modules/user'
 import { objToFormData } from '@/utils'
 
 const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
+  const userStore = useUserStoreWithOut()
+  if (userStore.getToken) {
+    config.headers.Authorization = `Bearer ${userStore.getToken}`
+  }
   if (
     config.method === 'post' &&
     config.headers['Content-Type'] === 'application/x-www-form-urlencoded'
@@ -41,7 +45,7 @@ const defaultResponseInterceptors = (response: AxiosResponse) => {
   } else if (response.data.code === SUCCESS_CODE) {
     return response.data
   } else {
-    ElMessage.error(response?.data?.message)
+    ElMessage.error(response?.data?.msg || response?.data?.message || '请求失败')
     if (response?.data?.code === 401) {
       const userStore = useUserStoreWithOut()
       userStore.logout()

+ 6 - 0
src/components/UserInfo/src/UserInfo.vue

@@ -7,6 +7,7 @@ import { ref, computed } from 'vue'
 import LockPage from './components/LockPage.vue'
 import { useLockStore } from '@/store/modules/lock'
 import { useUserStore } from '@/store/modules/user'
+import ChangePasswordDialog from './components/ChangePasswordDialog.vue'
 
 const userStore = useUserStore()
 
@@ -25,6 +26,7 @@ const loginOut = () => {
 }
 
 const dialogVisible = ref<boolean>(false)
+const passwordDialogVisible = ref(false)
 
 // 锁定屏幕
 const lockScreen = () => {
@@ -54,6 +56,9 @@ const toDocument = () => {
           <div @click="toDocument">{{ t('common.document') }}</div>
         </ElDropdownItem>
         <ElDropdownItem divided>
+          <div @click="passwordDialogVisible = true">修改密码</div>
+        </ElDropdownItem>
+        <ElDropdownItem>
           <div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
         </ElDropdownItem>
         <ElDropdownItem>
@@ -64,6 +69,7 @@ const toDocument = () => {
   </ElDropdown>
 
   <LockDialog v-if="dialogVisible" v-model="dialogVisible" />
+  <ChangePasswordDialog v-if="passwordDialogVisible" v-model="passwordDialogVisible" />
   <teleport to="body">
     <transition name="fade-bottom" mode="out-in">
       <LockPage v-if="getIsLock" />

+ 94 - 0
src/components/UserInfo/src/components/ChangePasswordDialog.vue

@@ -0,0 +1,94 @@
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import {
+  ElButton,
+  ElDialog,
+  ElForm,
+  ElFormItem,
+  ElInput,
+  ElMessage
+} from 'element-plus'
+import { changePasswordApi } from '@/api/login'
+import { useUserStore } from '@/store/modules/user'
+
+const model = defineModel<boolean>({ required: true })
+const userStore = useUserStore()
+const formRef = ref<FormInstance>()
+const submitting = ref(false)
+const form = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
+
+const validateConfirm = (_rule: unknown, value: string, callback: (error?: Error) => void) => {
+  value === form.newPassword ? callback() : callback(new Error('两次输入的新密码不一致'))
+}
+
+const rules: FormRules = {
+  oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
+  newPassword: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, message: '新密码不能少于 6 位', trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: '请再次输入新密码', trigger: 'blur' },
+    { validator: validateConfirm, trigger: 'blur' }
+  ]
+}
+
+const close = () => {
+  model.value = false
+  formRef.value?.resetFields()
+}
+
+const submit = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+  submitting.value = true
+  try {
+    const res = await changePasswordApi({
+      oldPassword: form.oldPassword,
+      newPassword: form.newPassword
+    })
+    if (res) {
+      ElMessage.success('密码修改成功,请重新登录')
+      close()
+      userStore.reset()
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+</script>
+
+<template>
+  <ElDialog v-model="model" title="修改密码" width="440px" :close-on-click-modal="false">
+    <div class="password-tip">为保障账户安全,修改后需要使用新密码重新登录。</div>
+    <ElForm ref="formRef" :model="form" :rules="rules" label-position="top">
+      <ElFormItem label="原密码" prop="oldPassword">
+        <ElInput v-model="form.oldPassword" type="password" show-password />
+      </ElFormItem>
+      <ElFormItem label="新密码" prop="newPassword">
+        <ElInput v-model="form.newPassword" type="password" show-password />
+      </ElFormItem>
+      <ElFormItem label="确认新密码" prop="confirmPassword">
+        <ElInput v-model="form.confirmPassword" type="password" show-password />
+      </ElFormItem>
+    </ElForm>
+    <template #footer>
+      <ElButton @click="close">取消</ElButton>
+      <ElButton type="primary" :loading="submitting" @click="submit">确认修改</ElButton>
+    </template>
+  </ElDialog>
+</template>
+
+<style scoped>
+.password-tip {
+  margin: -4px 0 20px;
+  padding: 12px 14px;
+  color: var(--el-text-color-regular);
+  font-size: 13px;
+  line-height: 1.6;
+  background: var(--el-fill-color-light);
+  border-left: 3px solid var(--el-color-primary);
+  border-radius: 4px;
+}
+</style>

+ 2 - 6
src/store/modules/user.ts

@@ -3,7 +3,6 @@ import { store } from '../index'
 import { UserLoginType, UserType } from '@/api/login/types'
 import { ElMessageBox } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
-import { loginOutApi } from '@/api/login'
 import { useTagsViewStore } from './tagsView'
 import router from '@/router'
 
@@ -68,11 +67,8 @@ export const useUserStore = defineStore('user', {
         cancelButtonText: t('common.cancel'),
         type: 'warning'
       })
-        .then(async () => {
-          const res = await loginOutApi().catch(() => {})
-          if (res) {
-            this.reset()
-          }
+        .then(() => {
+          this.reset()
         })
         .catch(() => {})
     },

+ 5 - 4
src/views/Login/components/LoginForm.vue

@@ -9,7 +9,7 @@ import { useAppStore } from '@/store/modules/app'
 import { usePermissionStore } from '@/store/modules/permission'
 import { useRouter } from 'vue-router'
 import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
-import { UserType } from '@/api/login/types'
+import { UserLoginType } from '@/api/login/types'
 import { useValidator } from '@/hooks/web/useValidator'
 import { Icon } from '@/components/Icon'
 import { useUserStore } from '@/store/modules/user'
@@ -227,7 +227,7 @@ const signIn = async () => {
   await formRef?.validate(async (isValid) => {
     if (isValid) {
       loading.value = true
-      const formData = await getFormData<UserType>()
+      const formData = await getFormData<UserLoginType>()
 
       try {
         const res = await loginApi(formData)
@@ -237,13 +237,14 @@ const signIn = async () => {
           if (unref(remember)) {
             userStore.setLoginInfo({
               username: formData.username,
-              password: formData.password
+              password: ''
             })
           } else {
             userStore.setLoginInfo(undefined)
           }
           userStore.setRememberMe(unref(remember))
           userStore.setUserInfo(res.data)
+          userStore.setToken(res.data.token)
           // 是否使用动态路由
           if (appStore.getDynamicRouter) {
             getRole()
@@ -265,7 +266,7 @@ const signIn = async () => {
 
 // 获取角色信息
 const getRole = async () => {
-  const formData = await getFormData<UserType>()
+  const formData = await getFormData<UserLoginType>()
   const params = {
     roleName: formData.username
   }