提交 0b9a11a0 authored 作者: hejie's avatar hejie

refactor: copyEMS系统功能

上级 f8bb5cdd
{
"recommendations": [
"christian-kohler.path-intellisense",
"warmthsea.vscode-custom-code-color",
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"lokalise.i18n-ally",
"redhat.vscode-yaml",
"csstools.postcss",
"mikestead.dotenv",
"eamodio.gitlens",
"antfu.iconify",
"Vue.volar"
]
}
{
"i18n-ally.localesPaths": [
"locales"
]
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "off",
"git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"files.associations": {
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": "locales",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.enabledParsers": [
"yaml",
"js"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": [
"vue"
],
"iconify.excludes": [
"el"
],
"vsmqtt.brokerProfiles": [
{
"name": "broker.emqx.io",
"host": "broker.emqx.io/mqtt",
"port": 1883,
"clientId": "vsmqtt_client_db34"
}
],
"vscodeCustomCodeColor.highlightValue": [
"v-loading",
"v-auth",
"v-copy",
"v-longpress",
"v-optimize",
"v-perms",
"v-ripple"
],
"vscodeCustomCodeColor.highlightValueColor": "#b392f0",
}
\ No newline at end of file
{
"Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0",
"body": [
"<template>",
"\t<div>test</div>",
"</template>\n",
"<script lang='ts'>",
"export default {",
"\tsetup() {",
"\t\treturn {}",
"\t}",
"}",
"</script>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.0"
}
}
{
"Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+",
"body": [
"<script setup lang='ts'>",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.2+"
}
}
{
"Vue3.3+defineOptions快速生成模板": {
"scope": "vue",
"prefix": "Vue3.3+",
"body": [
"<script setup lang='ts'>",
"defineOptions({",
"\tname: ''",
"})",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.3+defineOptions快速生成模板"
}
}
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"feat", //新功能
"fix", //修复错误
"docs", //文档更新
"style", //代码风格 格式化代码、删除多余空格、添加注释等,不影响代码逻辑
"refactor", //代码重构 优化代码结构、重命名变量、简化逻辑等,不添加新功能或修复错误
"perf", //性能优化 提高代码性能(如减少内存占用、提高响应速度等)
"test", //测试代码更改 添加或更新测试用例、修复测试代码、更新测试框架
"build", //构建系统更改 更新项目的构建系统(如 Webpack、Vite、Babel 配置等)
"ci", //持续集成更改 更新 CI 配置文件(如 `.github/workflows`、`.gitlab-ci.yml` 等)或脚本
"chore", //项目维护 进行不属于上述类型的维护性更改,如更新依赖项、清理项目文件等
"revert", //撤销提交 撤销之前的提交
"wip", //工作进行中 表示正在进行的工作,通常用于未完成的功能开发
"workflow", //工作流程改进 优化项目的开发流程、工具链或工作方式
"types", //类型定义文件更改 更新项目的类型定义文件(如 TypeScript 的 `.d.ts` 文件)
"release" //发布相关进行版本发布或更新版本号
]
]
},
prompt: {
messages: {
type: "Select the type of change that you're committing:", //选择提交类型(如 feat、fix 等)。
scope: "Denote the SCOPE of this change (optional):", //填写提交范围,模块、组件、文件名、功能区域等(可选)。
customScope: "Denote the SCOPE of this change:",
subject: "Write a SHORT, IMPERATIVE tense description of the change:\n", //填写简短描述(必填)。
body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', //填写详细描述(可选)。
// breaking:
// 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
// footerPrefixsSelect:
// "Select the ISSUES type of changeList by this change (optional):",
// customFooterPrefixs: "Input ISSUES prefix:",
// footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
confirmCommit: "Are you sure you want to proceed with the commit above?"
},
types: [
{ value: "feat", name: "feat: 🚀 A new feature", emoji: "🚀" },
{ value: "fix", name: "fix: 🧩 A bug fix", emoji: "🧩" },
{
value: "docs",
name: "docs: 📚 Documentation only changes",
emoji: "📚"
},
{
value: "style",
name: "style: 🎨 Changes that do not affect the meaning of the code",
emoji: "🎨"
},
{
value: "refactor",
name: "refactor: ♻️ A code change that neither fixes a bug nor adds a feature",
emoji: "♻️"
},
{
value: "perf",
name: "perf: ⚡️ A code change that improves performance",
emoji: "⚡️"
},
{
value: "test",
name: "test: ✅ Adding missing tests or correcting existing tests",
emoji: "✅"
},
{
value: "build",
name: "build: 📦️ Changes that affect the build system or external dependencies",
emoji: "📦️"
},
{
value: "ci",
name: "ci: 🎡 Changes to our CI configuration files and scripts",
emoji: "🎡"
},
{
value: "chore",
name: "chore: 🔨 Other changes that don't modify src or test files",
emoji: "🔨"
},
{
value: "revert",
name: "revert: ⏪️ Reverts a previous commit",
emoji: "⏪️"
},
{ value: "wip", name: "wip: 🕔 work in process", emoji: "🕔" },
{
value: "workflow",
name: "workflow: 📋 workflow improvements",
emoji: "📋"
},
{
value: "types",
name: "types: 🔰 type definition file changes",
emoji: "🔰"
}
],
useEmoji: true,
allowBreakingChanges: ["feat", "fix"]
}
};
......@@ -20,7 +20,8 @@
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky",
"preinstall": "npx only-allow pnpm",
"plop": "plop --plopfile ./plop-templates/plopfile.cjs"
"plop": "plop --plopfile ./plop-templates/plopfile.cjs",
"commit": "git-cz"
},
"keywords": [
"vue-pure-admin",
......@@ -69,6 +70,7 @@
"codemirror": "^5.65.19",
"codemirror-editor-vue3": "^2.8.0",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"deep-chat": "^2.1.1",
"echarts": "^5.6.0",
......@@ -138,7 +140,9 @@
"@vitejs/plugin-vue-jsx": "^4.1.2",
"boxen": "^8.0.1",
"code-inspector-plugin": "^0.20.7",
"commitizen": "^4.3.1",
"cssnano": "^7.0.6",
"cz-git": "^1.11.1",
"dagre": "^0.8.5",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
......@@ -205,5 +209,10 @@
"typeit",
"vue-demi"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-git"
}
}
}
差异被折叠。
{
"Version": "6.0.0",
"Title": "统一后台权限管理系统",
"Title": "硬件设备管理系统",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,
......
......@@ -53,6 +53,26 @@ export default defineComponent({
}
);
}
// (() => {
// function block() {
// if (
// window.outerHeight - window.innerHeight > 200 ||
// window.outerWidth - window.innerWidth > 200
// ) {
// document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
// }
// setInterval(() => {
// (function () {
// return false;
// })
// ["constructor"]("debugger")
// ["call"]();
// }, 50);
// }
// try {
// block();
// } catch (err) {}
// })();
}
});
</script>
......@@ -3,25 +3,29 @@ import { http } from "@/utils/http";
type Result = {
success: boolean;
data?: Array<any>;
code?: string;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list: Array<any>;
list?: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
records?: Array<any>;
};
};
/** 获取系统管理-用户管理列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("post", "/user", { data });
return http.request<ResultTable>("post", "/api/user/find-user-list-by-page", {
data
});
};
/** 系统管理-用户管理-获取所有角色列表 */
......@@ -41,12 +45,60 @@ export const getRoleList = (data?: object) => {
/** 获取系统管理-菜单管理列表 */
export const getMenuList = (data?: object) => {
return http.request<Result>("post", "/menu", { data });
return http.request<ResultTable>("post", "/api/menu/find-menu-list-by-page", {
data
});
};
/** 新增菜单-菜单管理列表 */
export const addMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/add-menu", { data });
};
/** 修改菜单-菜单管理列表 */
export const updateMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/modify-menu", { data });
};
/** 删除菜单-菜单管理列表 */
export const deleteMenu = (data?: { id: number }) => {
return http.request<Result>("post", `/api/menu/delete-menu/${data.id}`);
};
// 根据角色ID获取菜单列表
export const getMenuListByRoleId = (roleId: number) => {
return http.request<Result>("post", `/api/menu/get-menu-by-role/${roleId}`);
};
// 给角色绑定菜单
export const bindMenuByRoleId = (data?: object) => {
return http.request<Result>("post", "/api/role/add-role-menu", { data });
};
/** 获取系统管理-部门管理列表 */
export const getDeptList = (data?: object) => {
return http.request<Result>("post", "/dept", { data });
return http.request<ResultTable>(
"post",
"/api/depart/get-depart-list-by-page",
{
data
}
);
};
/** 新增部门-部门管理列表 */
export const addDept = (data?: object) => {
return http.request<Result>("post", "/api/depart/add-depart", { data });
};
/** 修改部门-部门管理列表 */
export const updateDept = (data?: object) => {
return http.request<Result>("post", `/api/depart/modify-depart`, { data });
};
/** 删除部门-部门管理列表 */
export const deleteDept = (data?: { id: number }) => {
return http.request<Result>("post", `/api/depart/delete-depart/${data.id}`);
};
/** 获取系统监控-在线用户列表 */
......
import { http } from "@/utils/http";
//#region =============================== 登录模块对应接口 ====================================
/** 登录 */
export const getLogin = (data?: object) => {
return http.request<LoginResult>("post", "/api/auth/login", { data });
};
/** 获取验证码 */
export const getCaptcha = (time?: string) => {
return http.request<CaptchaResult>("get", "/api/auth/captchaNumber/" + time);
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
};
/** 账户设置-个人信息 */
export const getMine = () => {
return http.request<UserInfoResult>("post", "/api/user/user-info");
};
/** 用户管理-重置密码 */
export const resetPassword = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/reset-password", {
data
});
};
//#endregion
//#region =============================== 用户管理模块对应接口 ===============================
/** 用户管理-新增用户 */
export const addUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user", { data });
};
/** 用户管理-删除用户 */
export const deleteUser = (id: number | string) => {
return http.request<UserInfoResult>("post", `/api/user/delete-user/${id}`);
};
/** 用户管理-修改用户 */
export const updateUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/modify-user", {
data
});
};
/** 获取系统管理-用户管理列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("post", "/api/user/find-user-list-by-page", {
data
});
};
/** 用户管理-获取用户详情 */
export const getUserDetail = (data?: object) => {
return http.request<UserInfoResult>("get", "/api/user/user-detail", {
data
});
};
/** 用户管理-给用户绑定上角色 */
export const addUserRole = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user-role", {
data
});
};
//#endregion
//#region =============================== 菜单管理模块对应接口 ===============================
/** 新增菜单-菜单管理列表 */
export const addMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/add-menu", { data });
};
/** 删除菜单-菜单管理列表 */
export const deleteMenu = (data?: { id: number }) => {
return http.request<Result>("post", `/api/menu/delete-menu/${data.id}`);
};
/** 修改菜单-菜单管理列表 */
export const updateMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/modify-menu", { data });
};
/** 获取系统管理-菜单管理列表 */
export const getMenuList = (data?: object) => {
return http.request<ResultTable>("post", "/api/menu/find-menu-list-by-page", {
data
});
};
//#endregion
//#region =============================== 部门管理模块对应接口 ===============================
/** 新增部门-部门管理列表 */
export const addDept = (data?: object) => {
return http.request<Result>("post", "/api/depart/add-depart", { data });
};
/** 删除部门-部门管理列表 */
export const deleteDept = (data?: { id: number }) => {
return http.request<Result>("post", `/api/depart/delete-depart/${data.id}`);
};
/** 修改部门-部门管理列表 */
export const updateDept = (data?: object) => {
return http.request<Result>("post", `/api/depart/modify-depart`, { data });
};
/** 获取系统管理-部门管理列表 */
export const getDeptList = (data?: object) => {
return http.request<ResultTable>(
"post",
"/api/depart/get-depart-list-by-page",
{
data
}
);
};
//#endregion
//#region =============================== 角色管理模块对应接口 ===============================
// 角色管理-添加角色
export const addRole = (data?: object) => {
return http.request<Result>("post", "/api/role/add-role", { data });
};
// 角色管理-删除角色
export const deleteRole = (id: string) => {
return http.request<Result>("post", `/api/role/delete-role/${id}`);
};
// 角色管理-修改角色
export const updateRole = (data?: object) => {
return http.request<Result>("post", "/api/role/modify-role", { data });
};
/** 获取系统管理-角色管理列表 */
export const getRoleList = (data?: object) => {
// return http.request<ResultTable>("post", "/role", { data });
return http.request<ResultTable>("post", "/api/role/find-role-list-by-page", {
data
});
};
/** 获取角色管理-权限-菜单权限 */
// export const getRoleMenu = (data?: object) => {
// return http.request<Result>("post", "/role-menu", { data });
// };
/** 获取角色管理-权限-菜单权限-根据角色 id 查对应菜单 */
// export const getRoleMenuIds = (data?: object) => {
// return http.request<Result>("post", "/role-menu-ids", { data });
// };
/** 获取系统管理-不分页查询角色管理列表 */
export const getRoleListNoPage = () => {
// return http.request<ResultTable>("post", "/role", { data });
return http.request<ResultTable>("post", "/api/role/get-role-list");
};
// 根据角色ID获取菜单列表
export const getMenuListByRoleId = (roleId: number) => {
return http.request<Result>("post", `/api/menu/get-menu-by-role/${roleId}`);
};
// 角色管理-根据用户id查询角色列表
export const getRoleListByUserId = (id: string | number) => {
return http.request<Result>("post", `/api/role/get-role-by-user/${id}`);
};
// 给角色绑定菜单
export const bindMenuByRoleId = (data?: object) => {
return http.request<Result>("post", "/api/role/add-role-menu", { data });
};
// 用户管理 - 根据用户id绑定角色
// export const bindRoleByUserId = (data?: object) => {
// return http.request<Result>("post", "/api/role/bind-role-by-user", { data });
// };
//#endregion
// ========================= 定义ts类型 =========================
type Result = {
success: boolean;
data?: Array<any>;
code?: string;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list?: Array<any>;
[key: string]: any;
list: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
records?: Array<any>;
};
};
/** 获取Systems列表 */
export const getSystemsList = (data?: object) => {
return http.request<Result>("post", "/get-systems-list", { data });
export type UserInfo = {
menuList: Array<any>;
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 邮箱 */
email: string;
/** 联系电话 */
phone: string;
/** 简介 */
description: string;
};
/** 创建Systems */
export const createSystems = (data?: object) => {
return http.request<Result>("post", "/create-systems", { data });
export type UserInfoResult = {
success: boolean;
data?: UserInfo;
code?: number | string;
};
export type LoginResult = {
status: number;
success: boolean;
data: {
token: string;
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
expires: Date;
};
};
/** 更新Systems */
export const updateSystems = (data?: object) => {
return http.request<Result>("post", "/update-systems", { data });
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
expires: Date;
};
};
/** 删除Systems */
export const deleteSystems = (data?: { id: string }) => {
return http.request<Result>("post", "/delete-systems", { data });
export type CaptchaResult = {
code: string;
success: boolean;
data: string;
};
......@@ -4,7 +4,7 @@ export type UserResult = {
status: number;
success: boolean;
data: {
jwt: string;
token: string;
/** 头像 */
avatar: string;
/** 用户名 */
......@@ -27,6 +27,8 @@ export type UserResult = {
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
token?: string;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
......@@ -54,7 +56,8 @@ export type UserInfo = {
export type UserInfoResult = {
success: boolean;
data: UserInfo;
data?: UserInfo;
code?: number | string;
};
type ResultTable = {
......@@ -78,7 +81,9 @@ export const getLogin = (data?: object) => {
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
return http.request<RefreshTokenResult>("post", "/api/auth/refresh-token", {
data
});
};
/** 账户设置-个人信息 */
......@@ -90,3 +95,43 @@ export const getMine = () => {
export const getMineLogs = (data?: object) => {
return http.request<ResultTable>("get", "/mine-logs", { data });
};
/** 系统管理-用户管理-新增用户 */
export const addUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user", { data });
};
/** 系统管理-用户管理-修改用户 */
export const updateUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/modify-user", {
data
});
};
/** 系统管理-用户管理-给用户绑定上角色 */
export const addUserRole = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user-role", {
data
});
};
/** 系统管理-用户管理-删除用户 */
export const deleteUser = (id: number | string) => {
return http.request<UserInfoResult>("post", `/api/user/delete-user/${id}`);
};
/** 系统管理-用户管理-重置密码 */
export const resetPassword = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/reset-password", {
data
});
};
/** 系统管理-用户管理-获取用户列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("get", "/api/user/user-list", { data });
};
/** 系统管理-用户管理-获取用户详情 */
export const getUserDetail = (data?: object) => {
return http.request<UserInfoResult>("get", "/api/user/user-detail", {
data
});
};
import { ref, onMounted } from "vue";
import { getCaptcha } from "@/api/systems";
import { useUserStoreHook } from "@/store/modules/user";
/**
* 绘制图形验证码
* @param width - 图形宽度
......@@ -15,7 +16,9 @@ export const useImageVerify = (width = 120, height = 40) => {
function getImgCode() {
if (!domRef.value) return;
imgCode.value = draw(domRef.value, width, height);
draw(domRef.value, width, height).then(code => {
imgCode.value = code;
});
}
onMounted(() => {
......@@ -41,45 +44,56 @@ function randomColor(min: number, max: number) {
const b = randomNum(min, max);
return `rgb(${r},${g},${b})`;
}
function draw(dom: HTMLCanvasElement, width: number, height: number) {
let imgCode = "";
const NUMBER_STRING = "0123456789";
const ctx = dom.getContext("2d");
if (!ctx) return imgCode;
ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < 4; i += 1) {
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
imgCode += text;
const fontSize = randomNum(18, 41);
const deg = randomNum(-30, 30);
ctx.font = `${fontSize}px Simhei`;
ctx.textBaseline = "top";
ctx.fillStyle = randomColor(80, 150);
ctx.save();
ctx.translate(30 * i + 15, 15);
ctx.rotate((deg * Math.PI) / 180);
ctx.fillText(text, -15 + 5, -15);
ctx.restore();
}
for (let i = 0; i < 5; i += 1) {
ctx.beginPath();
ctx.moveTo(randomNum(0, width), randomNum(0, height));
ctx.lineTo(randomNum(0, width), randomNum(0, height));
ctx.strokeStyle = randomColor(180, 230);
ctx.closePath();
ctx.stroke();
}
for (let i = 0; i < 41; i += 1) {
ctx.beginPath();
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = randomColor(150, 200);
ctx.fill();
}
return imgCode;
function draw(
dom: HTMLCanvasElement,
width: number,
height: number
): Promise<string> {
return new Promise(resolve => {
let imgCode = "";
const now = new Date().getTime().toString();
getCaptcha(now).then(res => {
if (res.code === "0") {
const captchaCode = res.data;
useUserStoreHook().SET_VERIFYCODE(captchaCode);
useUserStoreHook().SET_CAPTCHATIME(now);
const ctx = dom.getContext("2d");
if (!ctx) return resolve(imgCode);
ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < captchaCode.length; i += 1) {
const text = captchaCode[i];
imgCode += text;
const fontSize = randomNum(18, 41);
const deg = randomNum(-30, 30);
ctx.font = `${fontSize}px Simhei`;
ctx.textBaseline = "top";
ctx.fillStyle = randomColor(80, 150);
ctx.save();
ctx.translate(30 * i + 15, 15);
ctx.rotate((deg * Math.PI) / 180);
ctx.fillText(text, -15 + 5, -15);
ctx.restore();
}
for (let i = 0; i < 5; i += 1) {
ctx.beginPath();
ctx.moveTo(randomNum(0, width), randomNum(0, height));
ctx.lineTo(randomNum(0, width), randomNum(0, height));
ctx.strokeStyle = randomColor(180, 230);
ctx.closePath();
ctx.stroke();
}
for (let i = 0; i < 41; i += 1) {
ctx.beginPath();
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = randomColor(150, 200);
ctx.fill();
}
resolve(imgCode);
} else {
resolve("");
}
});
});
}
......@@ -69,6 +69,9 @@ export default defineComponent({
function handleChange({ option, index }, event: Event) {
if (props.disabled || option.disabled) return;
event.preventDefault();
console.log("props.modelValue", props.modelValue);
console.log("props.modelValue.type", typeof props.modelValue);
console.log("handleChange", option, index);
isNumber(props.modelValue)
? emit("update:modelValue", index)
: (curIndex.value = index);
......
......@@ -26,10 +26,21 @@ const {
toggleSideBar,
toAccountSettings,
getDropdownItemStyle,
getDropdownItemClass
getDropdownItemClass,
goLink
} = useNav();
const { t, locale, translationCh, translationEn } = useTranslationLang();
const aa = 123;
// 定义外链数组
const urlList = ref([
{ name: "EMS", url: "https://pure-admin.github.io/vue-pure-admin/#/welcome" },
{
name: "数据大屏",
url: "https://pure-admin.github.io/vue-pure-admin/#/welcome"
},
{ name: "DBSM", url: "https://pure-admin.github.io/vue-pure-admin/#/welcome" }
]);
</script>
<template>
......@@ -87,6 +98,24 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
<LaySidebarFullScreen id="full-screen" />
<!-- 消息通知 -->
<LayNotice id="header-notice" />
<!-- 外链列表 -->
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<p class="dark:text-white">其它系统</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item
v-for="item in urlList"
:key="item.url"
@click="goLink(item)"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
......
......@@ -147,6 +147,10 @@ export function useNav() {
return new URL("/logo.svg", import.meta.url).href;
}
function goLink(item) {
window.open(item.url, "__blank");
}
return {
title,
device,
......@@ -175,6 +179,7 @@ export function useNav() {
tooltipEffect,
toAccountSettings,
getDropdownItemStyle,
getDropdownItemClass
getDropdownItemClass,
goLink
};
}
......@@ -218,8 +218,7 @@ function initRouter() {
} else {
return new Promise(resolve => {
getMine().then(({ data }) => {
console.log("路由数据---:", data);
data.menuList.shift();
// console.log("路由数据---:", data);
const treeData = transformRoutes(data.menuList);
handleAsyncRoutes(cloneDeep(treeData));
......
......@@ -14,7 +14,13 @@ import {
refreshTokenApi
} from "@/api/user";
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
import {
type DataInfo,
setToken,
removeToken,
userKey,
getToken
} from "@/utils/auth";
export const useUserStore = defineStore("pure-user", {
state: (): userType => ({
......@@ -37,8 +43,10 @@ export const useUserStore = defineStore("pure-user", {
isRemembered: false,
// 登录页的免登录存储几天,默认7天
loginDay: 7,
token: localStorage.getItem("jwt") || "",
refreshToken: localStorage.getItem("refreshToken") || ""
token: localStorage.getItem("token") || "",
refreshToken: localStorage.getItem("refreshToken") || "",
// 拿到验证码的时间戳
captchaTime: ""
}),
actions: {
/** 存储头像 */
......@@ -77,6 +85,10 @@ export const useUserStore = defineStore("pure-user", {
SET_LOGINDAY(value: number) {
this.loginDay = Number(value);
},
/** 设置登录时验证码时间戳 */
SET_CAPTCHATIME(value: string) {
this.captchaTime = value;
},
/** 登入 */
async loginByUsername(data) {
return new Promise<UserResult>((resolve, reject) => {
......@@ -106,7 +118,12 @@ export const useUserStore = defineStore("pure-user", {
refreshTokenApi(data)
.then(data => {
if (data) {
if (data.data.token) {
data.data.accessToken = data.data.token;
}
setToken(data.data);
console.log("data", data.data);
console.log(getToken());
resolve(data);
}
})
......
......@@ -49,4 +49,5 @@ export type userType = {
loginDay?: number;
token: string;
refreshToken: string;
captchaTime: string;
};
......@@ -19,6 +19,8 @@ export interface DataInfo<T> {
roles?: Array<string>;
/** 当前登录用户的按钮级别权限 */
permissions?: Array<string>;
/** 用于调用刷新accessToken的接口时所需的token */
token?: string;
}
export const userKey = "user-info";
......@@ -45,11 +47,11 @@ export function getToken(): DataInfo<number> {
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁)
* 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
*/
export function setToken(data: DataInfo<Date>) {
export function setToken(data: DataInfo<number>) {
let expires = 0;
const { accessToken, refreshToken } = data;
const { isRemembered, loginDay } = useUserStoreHook();
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
expires = data.expires; // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
const cookieString = JSON.stringify({ accessToken, expires, refreshToken });
expires > 0
......
......@@ -72,16 +72,12 @@ export function transformRoutes(menuList: MenuItem[]): treeItem[] {
menuList.forEach((item: MenuItem) => {
// 如果有父菜单项,则将其添加到父菜单项的children中
if (item.type === 2 && item.parentId) {
console.log("itemname", item.name, item.parentId);
const parentItem = map.get(item.parentId);
if (parentItem) {
if (!parentItem.children) {
parentItem.children = [];
parentItem.children.push(item);
} else {
parentItem.children.push(item);
}
parentItem.children.push(item);
}
}
if (item.type === 1) {
......@@ -89,8 +85,6 @@ export function transformRoutes(menuList: MenuItem[]): treeItem[] {
tree.push(item);
}
});
// console.log("map", map);
console.log("tree", tree);
// 遍历树,转换每个菜单项为树形结构
const transTree = tree.map((item: MenuItem) => {
......@@ -102,7 +96,7 @@ export function transformRoutes(menuList: MenuItem[]): treeItem[] {
}
return transformData(item);
});
console.log("transTree", transTree);
console.log("tree", tree, transTree);
return transTree;
}
......@@ -13,6 +13,7 @@ import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
import packageJson from "../../../package.json";
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
......@@ -21,7 +22,11 @@ const defaultConfig: AxiosRequestConfig = {
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
"X-Requested-With": "XMLHttpRequest",
System: "PureAdmin",
Version: packageJson.version,
"Request-Id": "",
Device: "PC"
},
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
paramsSerializer: {
......@@ -52,7 +57,6 @@ class PureHttp {
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
debugger;
config.headers["Authorization"] = formatToken(token);
resolve(config);
});
......@@ -80,8 +84,6 @@ class PureHttp {
? config
: new Promise(resolve => {
const data = getToken();
console.log(4444, data);
if (data) {
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
......@@ -106,6 +108,9 @@ class PureHttp {
config.headers["Authorization"] = formatToken(
data.accessToken
);
config.headers["Request-Id"] = crypto
.randomUUID()
.replaceAll("-", "");
resolve(config);
}
} else {
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 从 Vue 引入 ref 和 reactive 函数,用于创建响应式数据
import { ref, reactive } from "vue";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入手机号登录表单验证规则
import { phoneRules } from "../utils/rule";
// 引入 Element Plus 的 FormInstance 类型,用于表单实例的类型定义
import type { FormInstance } from "element-plus";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入验证码相关的自定义 hook
import { useVerifyCode } from "../utils/verifyCode";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入手机图标
import Iphone from "~icons/ep/iphone";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 获取国际化实例
const { t } = useI18n();
// 定义登录加载状态的响应式引用
const loading = ref(false);
// 定义手机号登录表单数据的响应式对象
const ruleForm = reactive({
phone: "",
verifyCode: ""
phone: "", // 手机号,初始为空字符串
verifyCode: "" // 验证码,初始为空字符串
});
// 定义表单实例的响应式引用
const ruleFormRef = ref<FormInstance>();
// 从 useVerifyCode hook 中解构出按钮禁用状态和倒计时文本
const { isDisabled, text } = useVerifyCode();
/**
* 处理手机号登录操作
* @param formEl - 表单实例
*/
const onLogin = async (formEl: FormInstance | undefined) => {
// 设置加载状态为 true,表示正在登录
loading.value = true;
// 如果表单实例不存在,直接返回
if (!formEl) return;
// 对表单进行验证
await formEl.validate(valid => {
if (valid) {
// 模拟登录请求,需根据实际开发进行修改
setTimeout(() => {
// 显示登录成功的消息提示
message(transformI18n($t("login.pureLoginSuccess")), {
type: "success"
});
// 登录完成,设置加载状态为 false
loading.value = false;
}, 2000);
} else {
// 表单验证失败,设置加载状态为 false
loading.value = false;
}
});
};
/**
* 处理返回操作
* 结束验证码倒计时,并将当前页面设置为 0
*/
function onBack() {
// 调用 useVerifyCode 的 end 方法结束验证码倒计时
useVerifyCode().end();
// 调用用户状态管理的方法,将当前页面设置为 0
useUserStoreHook().SET_CURRENTPAGE(0);
}
</script>
<template>
<!-- 手机号登录表单,绑定表单实例、表单数据和验证规则 -->
<el-form ref="ruleFormRef" :model="ruleForm" :rules="phoneRules" size="large">
<!-- 包裹手机号输入项的动画组件,设置默认延迟时间 -->
<Motion>
<!-- 手机号输入项,绑定验证规则中的 phone 属性 -->
<el-form-item prop="phone">
<!-- 手机号输入框,绑定手机号数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.phone"
clearable
......@@ -58,20 +95,26 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹验证码输入项的动画组件,设置延迟时间为 100 毫秒 -->
<Motion :delay="100">
<!-- 验证码输入项,绑定验证规则中的 verifyCode 属性 -->
<el-form-item prop="verifyCode">
<!-- 包含验证码输入框和获取验证码按钮的容器 -->
<div class="w-full flex justify-between">
<!-- 验证码输入框,绑定验证码数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.verifyCode"
clearable
:placeholder="t('login.pureSmsVerifyCode')"
:prefix-icon="useRenderIcon(Keyhole)"
/>
<!-- 获取验证码按钮,根据 isDisabled 状态禁用按钮,点击触发获取验证码操作 -->
<el-button
:disabled="isDisabled"
class="ml-2!"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
<!-- 根据倒计时文本显示不同内容 -->
{{
text.length > 0
? text + t("login.pureInfo")
......@@ -82,8 +125,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹登录按钮的动画组件,设置延迟时间为 150 毫秒 -->
<Motion :delay="150">
<!-- 登录按钮项 -->
<el-form-item>
<!-- 登录按钮,设置为全宽,主按钮样式,根据 loading 状态显示加载状态,点击触发登录操作 -->
<el-button
class="w-full"
size="default"
......@@ -91,14 +137,19 @@ function onBack() {
:loading="loading"
@click="onLogin(ruleFormRef)"
>
<!-- 显示登录按钮文本 -->
{{ t("login.pureLogin") }}
</el-button>
</el-form-item>
</Motion>
<!-- 包裹返回按钮的动画组件,设置延迟时间为 200 毫秒 -->
<Motion :delay="200">
<!-- 返回按钮项 -->
<el-form-item>
<!-- 返回按钮,设置为全宽,点击触发返回操作 -->
<el-button class="w-full" size="default" @click="onBack">
<!-- 显示返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</el-form-item>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义二维码组件
import ReQrcode from "@/components/ReQrcode";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 获取国际化实例,用于翻译文本
const { t } = useI18n();
</script>
<template>
<!-- 使用 Motion 动画组件包裹二维码,设置顶部和底部外边距为 -2 -->
<Motion class="-mt-2 -mb-2">
<!-- 渲染自定义二维码组件,通过 :text 属性绑定国际化后的二维码内容 -->
<ReQrcode :text="t('login.pureTest')" />
</Motion>
<!-- 使用 Motion 动画组件包裹分割线,设置动画延迟时间为 100 毫秒 -->
<Motion :delay="100">
<!-- 渲染 Element Plus 的分割线组件 -->
<el-divider>
<!-- 在分割线中间显示提示文本,设置文本颜色为灰色,字号为 xs -->
<p class="text-gray-500 text-xs">{{ t("login.pureTip") }}</p>
</el-divider>
</Motion>
<!-- 使用 Motion 动画组件包裹返回按钮,设置动画延迟时间为 150 毫秒 -->
<Motion :delay="150">
<!-- 渲染 Element Plus 的按钮组件,设置按钮宽度为全宽,顶部外边距为 4 -->
<el-button
class="w-full mt-4!"
@click="useUserStoreHook().SET_CURRENTPAGE(0)"
>
<!-- 显示国际化后的返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</Motion>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 从 Vue 引入 ref 和 reactive 函数,用于创建响应式数据
import { ref, reactive } from "vue";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入注册表单验证规则
import { updateRules } from "../utils/rule";
// 引入 Element Plus 的 FormInstance 类型,用于表单实例的类型定义
import type { FormInstance } from "element-plus";
// 引入验证码相关的自定义 hook
import { useVerifyCode } from "../utils/verifyCode";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入锁图标
import Lock from "~icons/ri/lock-fill";
// 引入手机图标
import Iphone from "~icons/ep/iphone";
// 引入用户图标
import User from "~icons/ri/user-3-fill";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 获取国际化实例
const { t } = useI18n();
// 定义是否勾选隐私协议的响应式引用,初始值为 false
const checked = ref(false);
// 定义注册加载状态的响应式引用,初始值为 false
const loading = ref(false);
// 定义注册表单数据的响应式对象
const ruleForm = reactive({
username: "",
phone: "",
verifyCode: "",
password: "",
repeatPassword: ""
username: "", // 用户名,初始为空字符串
phone: "", // 手机号,初始为空字符串
verifyCode: "", // 验证码,初始为空字符串
password: "", // 密码,初始为空字符串
repeatPassword: "" // 重复密码,初始为空字符串
});
// 定义表单实例的响应式引用
const ruleFormRef = ref<FormInstance>();
// 从 useVerifyCode hook 中解构出按钮禁用状态和倒计时文本
const { isDisabled, text } = useVerifyCode();
// 定义重复密码的验证规则
const repeatPasswordRule = [
{
/**
* 重复密码验证函数
* @param rule - 当前验证规则
* @param value - 输入的重复密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 重复密码为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordSureReg"))));
} else if (ruleForm.password !== value) {
// 重复密码与密码不一致时,返回错误信息
callback(
new Error(transformI18n($t("login.purePassWordDifferentReg")))
);
} else {
// 重复密码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
];
/**
* 处理注册操作
* @param formEl - 表单实例
*/
const onUpdate = async (formEl: FormInstance | undefined) => {
// 设置加载状态为 true,表示正在注册
loading.value = true;
// 如果表单实例不存在,直接返回
if (!formEl) return;
// 对表单进行验证
await formEl.validate(valid => {
if (valid) {
if (checked.value) {
// 模拟请求,需根据实际开发进行修改
// 表单验证通过且勾选了隐私协议,模拟注册请求,需根据实际开发进行修改
setTimeout(() => {
// 显示注册成功的消息提示
message(transformI18n($t("login.pureRegisterSuccess")), {
type: "success"
});
// 注册完成,设置加载状态为 false
loading.value = false;
}, 2000);
} else {
// 未勾选隐私协议,设置加载状态为 false,并显示提示消息
loading.value = false;
message(transformI18n($t("login.pureTickPrivacy")), {
type: "warning"
});
}
} else {
// 表单验证失败,设置加载状态为 false
loading.value = false;
}
});
};
/**
* 处理返回操作
* 结束验证码倒计时,并将当前页面设置为 0
*/
function onBack() {
// 调用 useVerifyCode 的 end 方法结束验证码倒计时
useVerifyCode().end();
// 调用用户状态管理的方法,将当前页面设置为 0
useUserStoreHook().SET_CURRENTPAGE(0);
}
</script>
<template>
<!-- 注册表单,绑定表单实例、表单数据和验证规则,设置表单尺寸为 large -->
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="updateRules"
size="large"
>
<!-- 包裹用户名输入项的动画组件,设置默认延迟时间 -->
<Motion>
<!-- 用户名输入项,绑定验证规则 -->
<el-form-item
:rules="[
{
......@@ -92,6 +144,7 @@ function onBack() {
]"
prop="username"
>
<!-- 用户名输入框,绑定用户名数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.username"
clearable
......@@ -101,8 +154,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹手机号输入项的动画组件,设置延迟时间为 100 毫秒 -->
<Motion :delay="100">
<!-- 手机号输入项,绑定验证规则中的 phone 属性 -->
<el-form-item prop="phone">
<!-- 手机号输入框,绑定手机号数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.phone"
clearable
......@@ -112,20 +168,26 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹验证码输入项的动画组件,设置延迟时间为 150 毫秒 -->
<Motion :delay="150">
<!-- 验证码输入项,绑定验证规则中的 verifyCode 属性 -->
<el-form-item prop="verifyCode">
<!-- 包含验证码输入框和获取验证码按钮的容器 -->
<div class="w-full flex justify-between">
<!-- 验证码输入框,绑定验证码数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.verifyCode"
clearable
:placeholder="t('login.pureSmsVerifyCode')"
:prefix-icon="useRenderIcon(Keyhole)"
/>
<!-- 获取验证码按钮,根据 isDisabled 状态禁用按钮,点击触发获取验证码操作 -->
<el-button
:disabled="isDisabled"
class="ml-2!"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
<!-- 根据倒计时文本显示不同内容 -->
{{
text.length > 0
? text + t("login.pureInfo")
......@@ -136,8 +198,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹密码输入项的动画组件,设置延迟时间为 200 毫秒 -->
<Motion :delay="200">
<!-- 密码输入项,绑定验证规则中的 password 属性 -->
<el-form-item prop="password">
<!-- 密码输入框,绑定密码数据,可清空,显示密码可见按钮,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.password"
clearable
......@@ -148,8 +213,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹重复密码输入项的动画组件,设置延迟时间为 250 毫秒 -->
<Motion :delay="250">
<!-- 重复密码输入项,绑定重复密码验证规则 -->
<el-form-item :rules="repeatPasswordRule" prop="repeatPassword">
<!-- 重复密码输入框,绑定重复密码数据,可清空,显示密码可见按钮,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.repeatPassword"
clearable
......@@ -160,19 +228,28 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹隐私协议勾选和隐私政策链接的动画组件,设置延迟时间为 300 毫秒 -->
<Motion :delay="300">
<!-- 隐私协议项 -->
<el-form-item>
<!-- 隐私协议勾选框,绑定勾选状态 -->
<el-checkbox v-model="checked">
<!-- 显示隐私协议勾选提示文本 -->
{{ t("login.pureReadAccept") }}
</el-checkbox>
<!-- 隐私政策链接按钮,设置为链接样式,主按钮类型 -->
<el-button link type="primary">
<!-- 显示隐私政策链接文本 -->
{{ t("login.purePrivacyPolicy") }}
</el-button>
</el-form-item>
</Motion>
<!-- 包裹注册按钮的动画组件,设置延迟时间为 350 毫秒 -->
<Motion :delay="350">
<!-- 注册按钮项 -->
<el-form-item>
<!-- 注册按钮,设置为全宽,默认尺寸,主按钮样式,根据 loading 状态显示加载状态,点击触发注册操作 -->
<el-button
class="w-full"
size="default"
......@@ -180,14 +257,19 @@ function onBack() {
:loading="loading"
@click="onUpdate(ruleFormRef)"
>
<!-- 显示注册按钮文本 -->
{{ t("login.pureDefinite") }}
</el-button>
</el-form-item>
</Motion>
<!-- 包裹返回按钮的动画组件,设置延迟时间为 400 毫秒 -->
<Motion :delay="400">
<!-- 返回按钮项 -->
<el-form-item>
<!-- 返回按钮,设置为全宽,默认尺寸,点击触发返回操作 -->
<el-button class="w-full" size="default" @click="onBack">
<!-- 显示返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</el-form-item>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 从 Vue 引入 ref 和 reactive 函数,用于创建响应式数据
import { ref, reactive } from "vue";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入重置密码表单验证规则
import { updateRules } from "../utils/rule";
// 引入 Element Plus 的 FormInstance 类型,用于表单实例的类型定义
import type { FormInstance } from "element-plus";
// 引入验证码相关的自定义 hook
import { useVerifyCode } from "../utils/verifyCode";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入锁图标
import Lock from "~icons/ri/lock-fill";
// 引入手机图标
import Iphone from "~icons/ep/iphone";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 获取国际化实例
const { t } = useI18n();
// 定义重置密码操作的加载状态,初始为 false
const loading = ref(false);
// 定义重置密码表单的数据,使用响应式对象
const ruleForm = reactive({
phone: "",
verifyCode: "",
password: "",
repeatPassword: ""
phone: "", // 手机号,初始为空字符串
verifyCode: "", // 验证码,初始为空字符串
password: "", // 新密码,初始为空字符串
repeatPassword: "" // 重复新密码,初始为空字符串
});
// 定义表单实例的引用
const ruleFormRef = ref<FormInstance>();
// 从验证码 hook 中解构出按钮禁用状态和倒计时文本
const { isDisabled, text } = useVerifyCode();
// 定义重复密码的验证规则
const repeatPasswordRule = [
{
/**
* 验证重复密码是否符合要求
* @param rule - 当前验证规则对象
* @param value - 用户输入的重复密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 若重复密码为空,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordSureReg"))));
} else if (ruleForm.password !== value) {
// 若重复密码与新密码不一致,返回错误信息
callback(
new Error(transformI18n($t("login.purePassWordDifferentReg")))
);
} else {
// 验证通过
callback();
}
},
// 触发验证的时机为失去焦点时
trigger: "blur"
}
];
/**
* 处理重置密码操作
* @param formEl - 表单实例
*/
const onUpdate = async (formEl: FormInstance | undefined) => {
// 开始重置操作,设置加载状态为 true
loading.value = true;
// 若表单实例不存在,直接返回
if (!formEl) return;
// 对表单进行验证
await formEl.validate(valid => {
if (valid) {
// 模拟请求,需根据实际开发进行修改
// 模拟请求,实际开发中需替换为真实接口调用
setTimeout(() => {
// 显示密码重置成功的消息
message(transformI18n($t("login.purePassWordUpdateReg")), {
type: "success"
});
// 重置操作完成,设置加载状态为 false
loading.value = false;
}, 2000);
} else {
// 表单验证失败,设置加载状态为 false
loading.value = false;
}
});
};
/**
* 处理返回操作
* 结束验证码倒计时,并将当前页面设置为 0
*/
function onBack() {
// 调用验证码 hook 的 end 方法结束倒计时
useVerifyCode().end();
// 调用用户状态管理的方法,将当前页面设置为 0
useUserStoreHook().SET_CURRENTPAGE(0);
}
</script>
<template>
<!-- 重置密码表单,绑定表单实例、表单数据、验证规则,并设置表单尺寸为 large -->
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="updateRules"
size="large"
>
<!-- 手机号输入项,使用动画组件包裹,设置默认延迟 -->
<Motion>
<el-form-item prop="phone">
<!-- 手机号输入框,绑定手机号数据,支持清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.phone"
clearable
......@@ -82,20 +131,25 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 验证码输入项,使用动画组件包裹,设置延迟为 100 毫秒 -->
<Motion :delay="100">
<el-form-item prop="verifyCode">
<!-- 包含验证码输入框和获取验证码按钮的容器 -->
<div class="w-full flex justify-between">
<!-- 验证码输入框,绑定验证码数据,支持清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.verifyCode"
clearable
:placeholder="t('login.pureSmsVerifyCode')"
:prefix-icon="useRenderIcon(Keyhole)"
/>
<!-- 获取验证码按钮,根据 isDisabled 状态禁用按钮,点击触发获取验证码操作 -->
<el-button
:disabled="isDisabled"
class="ml-2!"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
<!-- 根据倒计时文本显示不同内容 -->
{{
text.length > 0
? text + t("login.pureInfo")
......@@ -106,8 +160,10 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 新密码输入项,使用动画组件包裹,设置延迟为 150 毫秒 -->
<Motion :delay="150">
<el-form-item prop="password">
<!-- 新密码输入框,绑定新密码数据,支持清空和显示密码,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.password"
clearable
......@@ -118,8 +174,10 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 重复新密码输入项,使用动画组件包裹,设置延迟为 200 毫秒 -->
<Motion :delay="200">
<el-form-item :rules="repeatPasswordRule" prop="repeatPassword">
<!-- 重复新密码输入框,绑定重复新密码数据,支持清空和显示密码,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.repeatPassword"
clearable
......@@ -130,8 +188,10 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 重置密码按钮项,使用动画组件包裹,设置延迟为 250 毫秒 -->
<Motion :delay="250">
<el-form-item>
<!-- 重置密码按钮,设置为全宽,默认尺寸,主按钮样式,根据加载状态显示加载动画,点击触发重置密码操作 -->
<el-button
class="w-full"
size="default"
......@@ -139,14 +199,18 @@ function onBack() {
:loading="loading"
@click="onUpdate(ruleFormRef)"
>
<!-- 显示重置密码按钮文本 -->
{{ t("login.pureDefinite") }}
</el-button>
</el-form-item>
</Motion>
<!-- 返回按钮项,使用动画组件包裹,设置延迟为 300 毫秒 -->
<Motion :delay="300">
<el-form-item>
<!-- 返回按钮,设置为全宽,默认尺寸,点击触发返回操作 -->
<el-button class="w-full" size="default" @click="onBack">
<!-- 显示返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</el-form-item>
......
差异被折叠。
/**
* 引入国际化函数 $t,用于多语言支持
* 该函数从 @/plugins/i18n 模块导出,可将键值转换为对应语言的文本
*/
import { $t } from "@/plugins/i18n";
/**
* 定义登录操作列表
* 包含手机号登录、二维码登录和注册三个操作,每个操作有对应的国际化标题
*/
const operates = [
{
/** 手机号登录操作的国际化标题 */
title: $t("login.purePhoneLogin")
},
{
/** 二维码登录操作的国际化标题 */
title: $t("login.pureQRCodeLogin")
},
{
/** 注册操作的国际化标题 */
title: $t("login.pureRegister")
}
];
/**
* 定义第三方登录列表
* 包含微信、支付宝、QQ 和微博四种第三方登录方式,每种方式有对应的国际化标题和图标标识
*/
const thirdParty = [
{
/** 微信登录的国际化标题 */
title: $t("login.pureWeChatLogin"),
/** 微信登录的图标标识 */
icon: "wechat"
},
{
/** 支付宝登录的国际化标题 */
title: $t("login.pureAlipayLogin"),
/** 支付宝登录的图标标识 */
icon: "alipay"
},
{
/** QQ 登录的国际化标题 */
title: $t("login.pureQQLogin"),
/** QQ 登录的图标标识 */
icon: "qq"
},
{
/** 微博登录的国际化标题 */
title: $t("login.pureWeiBoLogin"),
/** 微博登录的图标标识 */
icon: "weibo"
}
];
const sys = [
{
title: "SMS"
},
{
title: "DBSM"
},
{
title: "ISMS"
}
];
export { operates, thirdParty, sys };
/**
* 导出登录操作列表和第三方登录列表
* 方便其他模块引入使用这两个常量
*/
export { operates, thirdParty };
import { h, defineComponent, withDirectives, resolveDirective } from "vue";
/** 封装@vueuse/motion动画库中的自定义指令v-motion */
/**
* 封装 @vueuse/motion 动画库中的自定义指令 v-motion
* 该组件用于为包裹的内容添加动画效果,通过 props 可设置动画延迟时间
*/
export default defineComponent({
// 组件名称,方便调试和识别
name: "Motion",
// 定义组件接收的 props
props: {
// 动画延迟时间,类型为数字,默认值为 50 毫秒
delay: {
type: Number,
default: 50
}
},
// 组件的渲染函数
render() {
// 从 props 中解构出 delay 属性
const { delay } = this;
// 解析 v-motion 指令
const motion = resolveDirective("motion");
// 使用 withDirectives 函数为元素添加指令
return withDirectives(
// 创建一个 div 元素
h(
"div",
{},
{
// 默认插槽,渲染子组件或内容
default: () => [this.$slots.default()]
}
),
......@@ -24,10 +36,13 @@ export default defineComponent({
[
motion,
{
// 初始状态,元素透明度为 0,在 y 轴向下偏移 100px
initial: { opacity: 0, y: 100 },
// 进入动画状态,元素透明度变为 1,回到 y 轴初始位置
enter: {
opacity: 1,
y: 0,
// 设置动画过渡效果,包含延迟时间
transition: {
delay
}
......
// 从 Vue 引入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 @pureadmin/utils 引入 isPhone 工具函数,用于验证手机号格式
import { isPhone } from "@pureadmin/utils";
// 从 Element Plus 引入 FormRules 类型,用于定义表单验证规则
import type { FormRules } from "element-plus";
// 从 @/plugins/i18n 引入 $t 和 transformI18n 函数,用于国际化文本处理
import { $t, transformI18n } from "@/plugins/i18n";
// 从 @/store/modules/user 引入 useUserStoreHook 函数,用于获取用户状态管理实例
import { useUserStoreHook } from "@/store/modules/user";
/** 6位数字验证码正则 */
export const REGEXP_SIX = /^\d{6}$/;
/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
/** 密码正则(密码格式应为8 - 18位数字、字母、符号的任意两种组合) */
export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
......@@ -15,31 +20,56 @@ export const REGEXP_PWD =
const loginRules = reactive<FormRules>({
password: [
{
/**
* 密码验证函数
* @param rule - 当前验证规则
* @param value - 输入的密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 密码为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordReg"))));
} else if (!REGEXP_PWD.test(value)) {
// 密码不符合正则规则时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordRuleReg"))));
} else {
// 密码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
verifyCode: [
{
/**
* 验证码验证函数
* @param rule - 当前验证规则
* @param value - 输入的验证码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
console.log("value", value);
console.log(
"useUserStoreHook().verifyCode",
useUserStoreHook().verifyCode
);
if (value === "") {
// 验证码为空时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
} else if (useUserStoreHook().verifyCode !== value) {
// 验证码与存储的验证码不一致时,返回错误信息
callback(
new Error(transformI18n($t("login.pureVerifyCodeCorrectReg")))
);
} else {
// 验证码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
]
......@@ -49,29 +79,49 @@ const loginRules = reactive<FormRules>({
const phoneRules = reactive<FormRules>({
phone: [
{
/**
* 手机号验证函数
* @param rule - 当前验证规则
* @param value - 输入的手机号值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 手机号为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneReg"))));
} else if (!isPhone(value)) {
// 手机号格式不正确时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneCorrectReg"))));
} else {
// 手机号验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
verifyCode: [
{
/**
* 手机登录验证码验证函数
* @param rule - 当前验证规则
* @param value - 输入的验证码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 验证码为空时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
} else if (!REGEXP_SIX.test(value)) {
// 验证码不是6位数字时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeSixReg"))));
} else {
// 验证码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
]
......@@ -81,46 +131,80 @@ const phoneRules = reactive<FormRules>({
const updateRules = reactive<FormRules>({
phone: [
{
/**
* 忘记密码时手机号验证函数
* @param rule - 当前验证规则
* @param value - 输入的手机号值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 手机号为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneReg"))));
} else if (!isPhone(value)) {
// 手机号格式不正确时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneCorrectReg"))));
} else {
// 手机号验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
verifyCode: [
{
/**
* 忘记密码时验证码验证函数
* @param rule - 当前验证规则
* @param value - 输入的验证码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 验证码为空时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
} else if (!REGEXP_SIX.test(value)) {
// 验证码不是6位数字时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeSixReg"))));
} else {
// 验证码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
password: [
{
/**
* 忘记密码时新密码验证函数
* @param rule - 当前验证规则
* @param value - 输入的新密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 新密码为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordReg"))));
} else if (!REGEXP_PWD.test(value)) {
// 新密码不符合正则规则时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordRuleReg"))));
} else {
// 新密码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
]
});
/**
* 导出登录、手机登录和忘记密码的表单验证规则
* 方便在登录相关组件中使用这些验证规则
*/
export { loginRules, phoneRules, updateRules };
/**
* 从项目资源目录导入登录页面所需的静态资源
* @description 导入登录背景图、用户头像和插图 SVG 组件,后续会将这些资源导出供其他组件使用
*/
// 导入登录背景图,路径为 @/assets/login/bg.png
import bg from "@/assets/login/bg.png";
// 导入用户头像 SVG 组件,路径为 @/assets/login/avatar.svg,并通过 ?component 标识为组件
import avatar from "@/assets/login/avatar.svg?component";
// 导入登录插图 SVG 组件,路径为 @/assets/login/illustration.svg,并通过 ?component 标识为组件
import illustration from "@/assets/login/illustration.svg?component";
/**
* 导出登录页面所需的静态资源
* @description 将导入的背景图、用户头像和插图 SVG 组件导出,方便其他组件引入使用
*/
export { bg, avatar, illustration };
// 从 element-plus 库中引入 FormInstance 和 FormItemProp 类型,用于处理表单实例和表单项属性
import type { FormInstance, FormItemProp } from "element-plus";
// 从 @pureadmin/utils 工具库中引入 clone 函数,用于深拷贝对象
import { clone } from "@pureadmin/utils";
// 从 Vue 中引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 创建一个响应式引用,用于记录按钮是否处于禁用状态,初始值为 false
const isDisabled = ref(false);
// 创建一个响应式引用,用于存储定时器的 ID,初始值为 null
const timer = ref(null);
// 创建一个响应式引用,用于显示倒计时的文本,初始值为空字符串
const text = ref("");
/**
* 自定义 hook,用于处理验证码倒计时逻辑
* @returns {Object} 包含 isDisabled、timer、text、start 和 end 方法的对象
*/
export const useVerifyCode = () => {
/**
* 开始验证码倒计时
* @param {FormInstance | undefined} formEl - 表单实例,用于验证表单字段
* @param {FormItemProp} props - 需要验证的表单项属性
* @param {number} [time=60] - 倒计时的总秒数,默认值为 60 秒
*/
const start = async (
formEl: FormInstance | undefined,
props: FormItemProp,
time = 60
) => {
// 如果表单实例不存在,直接返回
if (!formEl) return;
// 深拷贝初始的倒计时时间,用于后续重置倒计时
const initTime = clone(time, true);
// 异步验证指定的表单字段
await formEl.validateField(props, isValid => {
if (isValid) {
// 验证通过,清除之前可能存在的定时器
clearInterval(timer.value);
// 禁用按钮,防止重复点击
isDisabled.value = true;
// 显示初始的倒计时时间
text.value = `${time}`;
// 设置定时器,每秒更新一次倒计时
timer.value = setInterval(() => {
if (time > 0) {
// 倒计时未结束,时间减 1 并更新显示文本
time -= 1;
text.value = `${time}`;
} else {
// 倒计时结束,清空显示文本
text.value = "";
// 启用按钮
isDisabled.value = false;
// 清除定时器
clearInterval(timer.value);
// 重置倒计时时间
time = initTime;
}
}, 1000);
......@@ -34,6 +62,10 @@ export const useVerifyCode = () => {
});
};
/**
* 结束验证码倒计时
* 清空显示文本,启用按钮,并清除定时器
*/
const end = () => {
text.value = "";
isDisabled.value = false;
......@@ -41,10 +73,15 @@ export const useVerifyCode = () => {
};
return {
/** 按钮是否禁用的响应式引用 */
isDisabled,
/** 存储定时器 ID 的响应式引用 */
timer,
/** 显示倒计时文本的响应式引用 */
text,
/** 开始验证码倒计时的方法 */
start,
/** 结束验证码倒计时的方法 */
end
};
};
<script setup lang="ts">
// 从 vue 导入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 导入自定义组件 ReCol
import ReCol from "@/components/ReCol";
// 从当前目录的 utils/rule 文件导入表单验证规则
import { formRules } from "./utils/rule";
// 从当前目录的 utils/types 文件导入表单属性类型
import { FormProps } from "./utils/types";
// 从当前目录的 hooks 文件导入公共 Hook
import { usePublicHooks } from "./hooks";
/**
* 定义组件的 props,使用 withDefaults 设置默认值
* @property {FormItemProps} formInline - 表单内联数据,包含部门相关信息
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
higherDeptOptions: [], // 上级部门选项列表,默认为空数组
parentId: 0, // 上级部门 ID,默认为 0
name: "", // 部门名称,默认为空字符串
principal: "", // 部门负责人,默认为空字符串
phone: "", // 联系电话,默认为空字符串
email: "", // 邮箱地址,默认为空字符串
sort: 0, // 排序值,默认为 0
status: 1, // 部门状态,默认为 1(启用)
remark: "" // 备注信息,默认为空字符串
})
});
// 创建一个 ref 引用,用于获取表单实例
const ruleFormRef = ref();
// 调用公共 Hook 获取 switch 样式
const { switchStyle } = usePublicHooks();
// 创建一个 ref 引用,用于存储表单数据
const newFormInline = ref(props.formInline);
/**
* 获取表单实例的引用
* @returns {unknown} 表单实例的引用
*/
function getRef() {
return ruleFormRef.value;
}
// 暴露 getRef 方法,供父组件调用
defineExpose({ getRef });
</script>
<template>
<!-- 定义一个 Element Plus 表单,绑定表单引用、表单数据和验证规则 -->
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<!-- 定义一个行布局,设置列间距为 30px -->
<el-row :gutter="30">
<!-- 上级部门表单项 -->
<re-col>
<el-form-item label="上级部门">
<!-- 级联选择器,用于选择上级部门 -->
<el-cascader
v-model="newFormInline.parentId"
class="w-full"
:options="newFormInline.higherDeptOptions"
:props="{
value: 'id', // 选项的值字段
label: 'name', // 选项的显示文本字段
emitPath: false, // 不返回选中项的完整路径
checkStrictly: true // 可单独选择任意一级选项
}"
clearable
filterable
placeholder="请选择上级部门"
>
<!-- 自定义选项显示内容 -->
<template #default="{ node, data }">
<span>{{ data.name }}</span>
<!-- 如果不是叶子节点,显示子节点数量 -->
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</re-col>
<!-- 部门名称表单项,添加了验证规则 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门名称" prop="name">
<!-- 输入框,用于输入部门名称 -->
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入部门名称"
/>
</el-form-item>
</re-col>
<!-- 部门负责人表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门负责人">
<!-- 输入框,用于输入部门负责人姓名 -->
<el-input
v-model="newFormInline.principal"
clearable
placeholder="请输入部门负责人"
/>
</el-form-item>
</re-col>
<!-- 手机号表单项,添加了验证规则 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="手机号" prop="phone">
<!-- 输入框,用于输入手机号 -->
<el-input
v-model="newFormInline.phone"
clearable
placeholder="请输入手机号"
/>
</el-form-item>
</re-col>
<!-- 邮箱表单项,添加了验证规则 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="邮箱" prop="email">
<!-- 输入框,用于输入邮箱地址 -->
<el-input
v-model="newFormInline.email"
clearable
placeholder="请输入邮箱"
/>
</el-form-item>
</re-col>
<!-- 排序表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="排序">
<!-- 数字输入框,用于输入排序值 -->
<el-input-number
v-model="newFormInline.sort"
class="w-full!"
:min="0"
:max="9999"
controls-position="right"
/>
</el-form-item>
</re-col>
<!-- 部门状态表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门状态">
<!-- 开关组件,用于切换部门状态 -->
<el-switch
v-model="newFormInline.status"
inline-prompt
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="停用"
:style="switchStyle"
/>
</el-form-item>
</re-col>
<!-- 备注表单项 -->
<re-col>
<el-form-item label="备注">
<!-- 文本域输入框,用于输入备注信息 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
type="textarea"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>
// 抽离可公用的工具函数等用于系统管理页面逻辑
// 从 vue 库导入 computed 函数,用于创建计算属性
import { computed } from "vue";
// 从 @pureadmin/utils 库导入 useDark 函数,用于获取当前是否为深色模式
import { useDark } from "@pureadmin/utils";
/**
* 用于系统管理页面的公共 Hook
* 该 Hook 主要用于处理深色模式相关的样式计算
* @returns 包含深色模式状态、开关样式和标签样式的对象
*/
export function usePublicHooks() {
// 调用 useDark 函数获取当前是否为深色模式的状态
const { isDark } = useDark();
/**
* 计算 `el-switch` 组件的样式
* 根据深色模式状态设置开关开启和关闭时的颜色
*/
const switchStyle = computed(() => {
return {
"--el-switch-on-color": "#6abe39", // 开关开启时的颜色
"--el-switch-off-color": "#e84749" // 开关关闭时的颜色
};
});
/**
* 计算 `el-tag` 组件的样式
* 根据传入的状态和深色模式状态设置标签的文本颜色、背景颜色和边框颜色
*/
const tagStyle = computed(() => {
return (status: number) => {
return status === 1
? {
// 状态为 1 时的标签样式
"--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
"--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
"--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
}
: {
// 状态不为 1 时的标签样式
"--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
"--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
"--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
};
};
});
return {
/** 当前网页是否为`dark`模式 */
isDark,
/** 表现更鲜明的`el-switch`组件 */
switchStyle,
/** 表现更鲜明的`el-tag`组件 */
tagStyle
};
}
<template>
<div class="systems">
<h2>Systems</h2>
<slot />
</div>
</template>
<script setup lang="ts">
// 组件逻辑部分
// 从 Vue 导入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 从当前目录的 utils/hook 文件导入 useDept 自定义 Hook
import { useDept } from "./utils/hook";
// 导入自定义组件 PureTableBar
import { PureTableBar } from "@/components/RePureTableBar";
// 从 ReIcon 组件的 hooks 文件导入 useRenderIcon 函数,用于渲染图标
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 导入不同的图标组件
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import AddFill from "~icons/ri/add-circle-line";
import { Column } from "element-plus";
const count = ref(0);
/**
* 定义组件选项,设置组件名称为 SystemDept
*/
defineOptions({
name: "SystemDept"
});
const increment = () => {
count.value++;
};
// 创建表单引用和表格引用
const formRef = ref();
const tableRef = ref();
// 调用 useDept Hook,获取部门管理相关的数据和方法
const {
form,
loading,
columns,
dataList,
onSearch,
resetForm,
openDialog,
handleDelete,
handleSelectionChange
} = useDept();
/**
* 处理全屏操作,重置表格高度
*/
function onFullscreen() {
// 调用表格引用的 setAdaptive 方法重置表格高度
tableRef.value.setAdaptive();
}
</script>
<style scoped lang="scss">
.systems {
padding: 20px;
border: 1px solid #ccc;
<template>
<div class="main">
<!-- 搜索表单 -->
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
>
<!-- 部门名称搜索项 -->
<el-form-item label="部门名称:" prop="name">
<el-input
v-model="form.name"
placeholder="请输入部门名称"
clearable
class="w-[180px]!"
/>
</el-form-item>
<!-- 状态搜索项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
placeholder="请选择状态"
clearable
class="w-[180px]!"
>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<!-- 搜索和重置按钮项 -->
<el-form-item>
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="部门管理(仅演示,操作后不生效)"
:columns="columns"
:tableRef="tableRef?.getTableRef()"
@refresh="onSearch"
@fullscreen="onFullscreen"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增部门
</el-button>
</template>
<!-- 自定义表格内容插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
adaptive
:adaptiveConfig="{ offsetBottom: 45 }"
align-whole="center"
row-key="id"
showOverflowTooltip
table-layout="auto"
default-expand-all
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
>
<!-- 自定义操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<!-- 新增子部门按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
>
新增
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除部门名称为${row.name}的这条数据`"
@confirm="handleDelete(row)"
>
<!-- 触发确认弹窗的按钮 -->
<template #reference>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>
<style lang="scss" scoped>
/* 移除表格内滚动条底部的线 */
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
/* 主内容区域样式 */
.main-content {
margin: 24px 24px 0 !important;
}
/* 搜索表单样式 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { getDeptList, addDept, deleteDept, updateDept } from "@/api/systems";
import { usePublicHooks } from "../hooks";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h } from "vue";
import type { FormItemProps } from "../utils/types";
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
// import { cloneDeep, deviceDetection } from "@pureadmin/utils";
/**
* 使用部门管理相关功能的自定义 Hook
* @returns 包含部门管理相关方法和数据的对象
*/
export function useDept() {
// 定义搜索表单的数据
const form = reactive({
name: "",
status: null
});
// 编辑表单的引用
const formRef = ref();
// 存储部门列表数据
const dataList = ref([]);
// 表格加载状态
const loading = ref(true);
// 获取公共 Hook 中的标签样式方法
const { tagStyle } = usePublicHooks();
/**
* 表格列配置类型
* @typedef {Object} TableColumn
* @property {string} label - 列标题
* @property {string} [prop] - 对应数据的字段名
* @property {number} [width] - 列宽度
* @property {number} [minWidth] - 列最小宽度
* @property {string} [align] - 列内容对齐方式
* @property {string} [fixed] - 列固定位置
* @property {string} [slot] - 自定义插槽名称
* @property {Function} [cellRenderer] - 单元格渲染函数
* @property {Function} [formatter] - 数据格式化函数
*/
/**
* 表格列配置列表
* @type {TableColumn[]}
*/
const columns: TableColumnList = [
{
label: "部门名称",
prop: "name",
width: 180,
align: "left" as "center" | "left" | "right"
},
{
label: "排序",
prop: "sort",
minWidth: 70
},
{
label: "状态",
prop: "status",
minWidth: 100,
align: "center" as "center" | "left" | "right",
/**
* 状态列单元格渲染函数
* @param {Object} param - 包含行数据和属性的对象
* @param {Object} param.row - 行数据
* @param {Object} param.props - 列属性
* @returns {JSX.Element} 渲染后的标签元素
*/
cellRenderer: ({ row, props }) => (
<el-tag size={props.size} style={tagStyle.value(row.status)}>
{row.status === 1 ? "启用" : "停用"}
</el-tag>
)
},
{
label: "创建时间",
minWidth: 200,
prop: "createTime",
/**
* 时间格式化函数
* @param {Object} param - 包含创建时间的对象
* @param {string} param.createTime - 创建时间
* @returns {string} 格式化后的时间字符串
*/
formatter: ({ createTime }) =>
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
},
// {
// label: "备注",
// prop: "remark",
// minWidth: 320
// },
{
label: "操作",
fixed: "right",
width: 210,
slot: "operation"
}
];
/**
* 处理表格选择变化事件
* @param {Array} val - 选中的行数据数组
*/
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
/**
* 重置搜索表单并重新搜索
* @param {Object} formEl - 表单引用对象
*/
function resetForm(formEl) {
if (!formEl) return;
formEl.resetFields();
onSearch();
}
/**
* 执行搜索操作
*/
async function onSearch() {
// loading.value = true;
// 定义搜索参数
const params = {
pageNum: 1,
pageSize: 200
};
// 获取部门列表数据
const { data } = await getDeptList(params);
// 获取记录数据
let newData = (data as any).records;
if (!isAllEmpty(form.name)) {
// 前端搜索部门名称
newData = newData.filter((item: { name: string }) =>
item.name.includes(form.name)
);
}
if (!isAllEmpty(form.status)) {
// 前端搜索状态
newData = newData.filter(
(item: { status: number | null }) => item.status === form.status
);
}
// 处理数据为树结构
dataList.value = handleTree(newData);
console.log("dataList", dataList.value);
setTimeout(() => {
loading.value = false;
}, 500);
}
/**
* 格式化上级部门选项,根据状态添加禁用属性
* @param {Array} treeList - 部门树列表数据
* @returns {Array} 格式化后的部门树列表数据
*/
function formatHigherDeptOptions(treeList) {
// 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
treeList[i].disabled = treeList[i].status === 0 ? true : false;
formatHigherDeptOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
/**
* 打开新增或编辑部门对话框
* @param {string} [title="新增"] - 对话框标题
* @param {FormItemProps} [row] - 要编辑的部门数据
*/
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}部门`,
props: {
formInline: {
higherDeptOptions: formatHigherDeptOptions(cloneDeep(dataList.value)),
parentId: row?.parentId ?? 0,
name: row?.name ?? "",
principal: row?.principal ?? "",
phone: row?.phone ?? "",
email: row?.email ?? "",
sort: row?.sort ?? 0,
status: row?.status ?? 1,
remark: row?.remark ?? ""
}
},
width: "40%",
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
/**
* 对话框内容渲染函数
* @returns {JSX.Element} 渲染后的编辑表单元素
*/
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
/**
* 对话框确认前的回调函数
* @param {Function} done - 完成回调函数,用于关闭对话框
* @param {Object} param - 包含对话框选项的对象
* @param {Object} param.options - 对话框选项
*/
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as FormItemProps;
/**
* 处理成功后的杂项操作
*/
function chores() {
message(`您${title}了部门名称为${curData.name}的这条数据`, {
type: "success"
});
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
if (title === "新增") {
addDept(curData).then(res => {
if ((res as any).code === "0") {
chores();
} else {
message((res as any).msg, { type: "error" });
}
});
} else {
if (!row?.id) {
message("id不能为空", { type: "error" });
return;
}
curData.id = row.id; // 修改时需要传入id
updateDept(curData).then(res => {
if ((res as any).code === "0") {
chores();
} else {
message((res as any).msg, { type: "error" });
}
});
}
}
});
}
});
}
/**
* 处理删除部门操作
* @param {Object} row - 要删除的部门数据
* @param {number} row.id - 部门 ID
* @param {string} row.name - 部门名称
*/
function handleDelete(row) {
console.log("handleDelete", row.id);
deleteDept({ id: row.id }).then(res => {
if ((res as any).code === "0") {
message(`您删除了部门名称为${row.name}的这条数据`, { type: "success" });
onSearch();
}
});
}
// 组件挂载后执行搜索操作
onMounted(() => {
onSearch();
});
return {
form,
loading,
columns,
dataList,
/** 搜索 */
onSearch,
/** 重置 */
resetForm,
/** 新增、修改部门 */
openDialog,
/** 删除部门 */
handleDelete,
handleSelectionChange
};
}
// 从 vue 库导入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 element-plus 库导入 FormRules 类型,用于定义表单验证规则
import type { FormRules } from "element-plus";
// 从 @pureadmin/utils 库导入 isPhone 和 isEmail 函数,用于验证手机号和邮箱格式
import { isPhone, isEmail } from "@pureadmin/utils";
/**
* 自定义表单规则校验
* 此对象包含了部门表单中各个字段的验证规则,使用 reactive 函数将其转换为响应式对象。
*/
export const formRules = reactive(<FormRules>{
// 部门名称字段的验证规则
name: [
// 验证部门名称是否为必填项,若为空则提示 "部门名称为必填项",在失去焦点时触发验证
{ required: true, message: "部门名称为必填项", trigger: "blur" }
],
// 联系电话字段的验证规则
phone: [
{
/**
* 自定义验证函数,用于验证联系电话格式
* @param rule - 当前验证规则对象
* @param value - 当前字段的值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
// 若联系电话为空,则直接通过验证
if (value === "") {
callback();
}
// 若联系电话格式不正确,则返回错误信息
else if (!isPhone(value)) {
callback(new Error("请输入正确的手机号码格式"));
}
// 若联系电话格式正确,则通过验证
else {
callback();
}
},
// 在失去焦点时触发验证
trigger: "blur"
// 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
// trigger: "click"
}
],
// 邮箱字段的验证规则
email: [
{
/**
* 自定义验证函数,用于验证邮箱格式
* @param rule - 当前验证规则对象
* @param value - 当前字段的值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
// 若邮箱为空,则直接通过验证
if (value === "") {
callback();
}
// 若邮箱格式不正确,则返回错误信息
else if (!isEmail(value)) {
callback(new Error("请输入正确的邮箱格式"));
}
// 若邮箱格式正确,则通过验证
else {
callback();
}
},
trigger: "blur"
}
]
});
/**
* 部门表单单项属性接口
* 定义了部门表单中每个字段的类型和属性,用于规范部门相关表单数据的结构。
*/
interface FormItemProps {
/**
* 上级部门选项列表
* 每个选项是一个键值对对象,键为字符串类型,值为任意类型。
*/
higherDeptOptions: Record<string, unknown>[];
/**
* 上级部门 ID
* 用于标识当前部门所属的上级部门,为数字类型。
*/
parentId: number;
/**
* 部门名称
* 用于存储部门的具体名称,为字符串类型。
*/
name: string;
/**
* 部门负责人
* 记录部门负责人的姓名,为字符串类型。
*/
principal: string;
/**
* 联系电话
* 可以是字符串或数字类型,用于存储部门的联系电话。
*/
phone: string | number;
/**
* 邮箱地址
* 用于存储部门的联系邮箱,为字符串类型。
*/
email: string;
/**
* 排序值
* 用于对部门进行排序,为数字类型。
*/
sort: number;
/**
* 部门状态
* 用数字表示部门的状态,如启用、停用等。
*/
status: number;
/**
* 备注信息
* 用于存储关于部门的额外说明信息,为字符串类型。
*/
remark: string;
/**
* 部门 ID
* 可选属性,用于唯一标识一个部门,为数字类型。
*/
id?: number;
}
/**
* 部门表单属性接口
* 包含一个部门表单单项属性对象,用于规范整个部门表单的数据结构。
*/
interface FormProps {
/**
* 表单内联数据
* 包含部门表单各项属性的对象,类型为 FormItemProps。
*/
formInline: FormItemProps;
}
/**
* 导出部门表单单项属性和表单属性类型
* 方便在其他文件中引用这些类型,确保数据类型的一致性。
*/
export type { FormItemProps, FormProps };
// 从 Vue 中引入 computed 函数,用于创建计算属性
import { computed } from "vue";
// 从 @pureadmin/utils 包中引入 useDark 函数,用于检测当前是否为暗黑模式
import { useDark } from "@pureadmin/utils";
/**
* 定义一个组合式函数 usePublicHooks,用于获取系统管理页面所需的公共状态和样式
* 该函数封装了暗黑模式检测、el-switch 组件样式和 el-tag 组件样式的逻辑
* @returns 包含 isDark、switchStyle 和 tagStyle 的对象
*/
export function usePublicHooks() {
// 调用 useDark 函数获取当前是否为暗黑模式的状态
const { isDark } = useDark();
/**
* 计算 el-switch 组件的样式
* 根据设计需求,设置开启和关闭状态的颜色
*/
const switchStyle = computed(() => {
return {
// 开启状态的颜色
"--el-switch-on-color": "#6abe39",
// 关闭状态的颜色
"--el-switch-off-color": "#e84749"
};
});
/**
* 计算 el-tag 组件的样式
* 根据传入的 status 参数和当前的暗黑模式状态,动态设置标签的文本颜色、背景颜色和边框颜色
* @param status - 状态值,根据不同的值应用不同的样式
* @returns 包含 CSS 变量的对象,用于设置 el-tag 组件的样式
*/
const tagStyle = computed(() => {
return (status: number) => {
return status === 1
? {
// 状态为 1 时,根据暗黑模式设置标签的文本颜色
"--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
// 状态为 1 时,根据暗黑模式设置标签的背景颜色
"--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
// 状态为 1 时,根据暗黑模式设置标签的边框颜色
"--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
}
: {
// 状态不为 1 时,根据暗黑模式设置标签的文本颜色
"--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
// 状态不为 1 时,根据暗黑模式设置标签的背景颜色
"--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
// 状态不为 1 时,根据暗黑模式设置标签的边框颜色
"--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
};
};
});
return {
/** 当前网页是否为`dark`模式 */
isDark,
/** 表现更鲜明的`el-switch`组件 */
switchStyle,
/** 表现更鲜明的`el-tag`组件 */
tagStyle
};
}
差异被折叠。
<template>
<div class="systems">
<h2>Systems</h2>
<slot />
</div>
</template>
<script setup lang="ts">
// 组件逻辑部分
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入菜单相关的自定义 hook
import { useMenu } from "./utils/hook";
// 引入国际化转换函数
import { transformI18n } from "@/plugins/i18n";
// 引入自定义表格栏组件
import { PureTableBar } from "@/components/RePureTableBar";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
const count = ref(0);
// 引入删除图标
import Delete from "~icons/ep/delete";
// 引入编辑图标
import EditPen from "~icons/ep/edit-pen";
// 引入刷新图标
import Refresh from "~icons/ep/refresh";
// 引入添加图标
import AddFill from "~icons/ri/add-circle-line";
const increment = () => {
count.value++;
};
/**
* 定义组件选项
* 设置组件名称为 SystemMenu
*/
defineOptions({
name: "SystemMenu"
});
// 创建表单引用
const formRef = ref();
// 创建表格引用
const tableRef = ref();
/**
* 从 useMenu hook 中解构出所需的状态和方法
* form: 搜索表单数据
* loading: 加载状态
* columns: 表格列配置
* dataList: 表格数据列表
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* openDialog: 打开新增或修改对话框的方法
* handleDelete: 处理删除操作的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
loading,
columns,
dataList,
onSearch,
resetForm,
openDialog,
handleDelete,
handleSelectionChange
} = useMenu();
/**
* 处理全屏操作
* 重置表格高度
*/
function onFullscreen() {
// 重置表格高度
tableRef.value.setAdaptive();
}
</script>
<style scoped lang="scss">
.systems {
padding: 20px;
border: 1px solid #ccc;
<template>
<div class="main">
<!-- 搜索表单 -->
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
>
<!-- 菜单名称输入项 -->
<el-form-item label="菜单名称:" prop="title">
<el-input
v-model="form.name"
placeholder="请输入菜单名称"
clearable
class="w-[180px]!"
/>
</el-form-item>
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="菜单管理(仅演示,操作后不生效)"
:columns="columns"
:isExpandAll="false"
:tableRef="tableRef?.getTableRef()"
@refresh="onSearch"
@fullscreen="onFullscreen"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<!-- 新增菜单按钮,点击打开新增对话框 -->
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增菜单
</el-button>
</template>
<!-- 表格插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
adaptive
:adaptiveConfig="{ offsetBottom: 45 }"
align-whole="center"
row-key="id"
showOverflowTooltip
table-layout="auto"
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮,点击打开修改对话框 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<!-- 新增子菜单按钮,仅当菜单类型不为 3 时显示 -->
<el-button
v-show="row.menuType !== 3"
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
>
新增
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
@confirm="handleDelete(row)"
>
<template #reference>
<!-- 删除按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>
<style lang="scss" scoped>
/* 隐藏表格内部包装器的底部边框 */
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
/* 主内容区域样式 */
.main-content {
margin: 24px 24px 0 !important;
}
/* 搜索表单样式 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
// 从 @/components/ReSegmented 导入 OptionsType 类型,用于规范选项数据的结构
import type { OptionsType } from "@/components/ReSegmented";
/**
* 菜单类型选项列表
* 定义了不同菜单类型的选项,每个选项包含标签和对应的值
*/
const menuTypeOptions: Array<OptionsType> = [
{
label: "菜单", // 菜单类型的显示标签
value: 1 // 菜单类型对应的值
},
{
label: "目录", // 目录类型的显示标签
value: 2 // 目录类型对应的值
},
{
label: "外链", // 外链类型的显示标签
value: 3 // 外链类型对应的值
},
{
label: "按钮", // 按钮类型的显示标签
value: 4 // 按钮类型对应的值
}
];
/**
* 显示链接选项列表
* 定义了菜单是否显示链接的选项,每个选项包含标签、提示信息和对应的值
*/
const showLinkOptions: Array<OptionsType> = [
{
label: "显示", // 显示链接的显示标签
tip: "会在菜单中显示", // 显示链接的提示信息
value: true // 显示链接对应的值
},
{
label: "隐藏", // 隐藏链接的显示标签
tip: "不会在菜单中显示", // 隐藏链接的提示信息
value: false // 隐藏链接对应的值
}
];
/**
* 固定标签选项列表
* 定义了菜单标签是否固定显示的选项,每个选项包含标签、提示信息和对应的值
*/
const fixedTagOptions: Array<OptionsType> = [
{
label: "固定", // 固定标签的显示标签
tip: "当前菜单名称固定显示在标签页且不可关闭", // 固定标签的提示信息
value: true // 固定标签对应的值
},
{
label: "不固定", // 不固定标签的显示标签
tip: "当前菜单名称不固定显示在标签页且可关闭", // 不固定标签的提示信息
value: false // 不固定标签对应的值
}
];
/**
* 缓存选项列表
* 定义了菜单页面是否缓存的选项,每个选项包含标签、提示信息和对应的值
*/
const keepAliveOptions: Array<OptionsType> = [
{
label: "缓存", // 缓存的显示标签
tip: "会保存该页面的整体状态,刷新后会清空状态", // 缓存的提示信息
value: true // 缓存对应的值
},
{
label: "不缓存", // 不缓存的显示标签
tip: "不会保存该页面的整体状态", // 不缓存的提示信息
value: false // 不缓存对应的值
}
];
/**
* 隐藏标签选项列表
* 定义了菜单名称或自定义信息是否允许添加到标签页的选项,每个选项包含标签、提示信息和对应的值
*/
const hiddenTagOptions: Array<OptionsType> = [
{
label: "允许", // 允许添加到标签页的显示标签
tip: "当前菜单名称或自定义信息允许添加到标签页", // 允许添加到标签页的提示信息
value: false // 允许添加到标签页对应的值
},
{
label: "禁止", // 禁止添加到标签页的显示标签
tip: "当前菜单名称或自定义信息禁止添加到标签页", // 禁止添加到标签页的提示信息
value: true // 禁止添加到标签页对应的值
}
];
/**
* 显示父级菜单选项列表
* 定义了是否显示父级菜单的选项,每个选项包含标签、提示信息和对应的值
*/
const showParentOptions: Array<OptionsType> = [
{
label: "显示", // 显示父级菜单的显示标签
tip: "会显示父级菜单", // 显示父级菜单的提示信息
value: true // 显示父级菜单对应的值
},
{
label: "隐藏", // 隐藏父级菜单的显示标签
tip: "不会显示父级菜单", // 隐藏父级菜单的提示信息
value: false // 隐藏父级菜单对应的值
}
];
/**
* 框架加载动画选项列表
* 定义了是否开启首次加载动画的选项,每个选项包含标签、提示信息和对应的值
*/
const frameLoadingOptions: Array<OptionsType> = [
{
label: "开启", // 开启加载动画的显示标签
tip: "有首次加载动画", // 开启加载动画的提示信息
value: true // 开启加载动画对应的值
},
{
label: "关闭", // 关闭加载动画的显示标签
tip: "无首次加载动画", // 关闭加载动画的提示信息
value: false // 关闭加载动画对应的值
}
];
/**
* 导出所有选项列表
* 方便在其他文件中引用这些选项数据
*/
export {
menuTypeOptions,
showLinkOptions,
fixedTagOptions,
keepAliveOptions,
hiddenTagOptions,
showParentOptions,
frameLoadingOptions
};
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论