提交 2c3c723a authored 作者: hejie's avatar hejie

feat: 合并代码

<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"
}
];
/**
* 导出登录操作列表和第三方登录列表
* 方便其他模块引入使用这两个常量
*/
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,51 @@ 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) => {
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 +74,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 +126,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,
name: "",
principal: "",
phone: "",
email: "",
sort: 0,
status: 1,
remark: ""
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
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
......@@ -71,8 +96,10 @@ defineExpose({ getRef });
/>
</el-form-item>
</re-col>
<!-- 部门负责人表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门负责人">
<!-- 输入框,用于输入部门负责人姓名 -->
<el-input
v-model="newFormInline.principal"
clearable
......@@ -81,8 +108,10 @@ defineExpose({ getRef });
</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
......@@ -90,8 +119,10 @@ defineExpose({ getRef });
/>
</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
......@@ -100,8 +131,10 @@ defineExpose({ getRef });
</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!"
......@@ -111,8 +144,10 @@ defineExpose({ getRef });
/>
</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
......@@ -125,8 +160,10 @@ defineExpose({ getRef });
</el-form-item>
</re-col>
<!-- 备注表单项 -->
<re-col>
<el-form-item label="备注">
<!-- 文本域输入框,用于输入备注信息 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
......
// 抽离可公用的工具函数等用于系统管理页面逻辑
// 从 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-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"
......
<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";
/**
* 定义组件选项,设置组件名称为 SystemDept
*/
defineOptions({
name: "SystemDept"
});
// 创建表单引用和表格引用
const formRef = ref();
const tableRef = ref();
// 调用 useDept Hook,获取部门管理相关的数据和方法
const {
form,
loading,
......@@ -27,20 +39,26 @@ const {
handleSelectionChange
} = useDept();
/**
* 处理全屏操作,重置表格高度
*/
function onFullscreen() {
// 重置表格高度
// 调用表格引用的 setAdaptive 方法重置表格高度
tableRef.value.setAdaptive();
}
</script>
<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"
......@@ -49,6 +67,7 @@ function onFullscreen() {
class="w-[180px]!"
/>
</el-form-item>
<!-- 状态搜索项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
......@@ -60,6 +79,7 @@ function onFullscreen() {
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<!-- 搜索和重置按钮项 -->
<el-form-item>
<el-button
type="primary"
......@@ -75,6 +95,7 @@ function onFullscreen() {
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="部门管理(仅演示,操作后不生效)"
:columns="columns"
......@@ -82,6 +103,7 @@ function onFullscreen() {
@refresh="onSearch"
@fullscreen="onFullscreen"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<el-button
type="primary"
......@@ -91,7 +113,9 @@ function onFullscreen() {
新增部门
</el-button>
</template>
<!-- 自定义表格内容插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
adaptive
......@@ -111,7 +135,9 @@ function onFullscreen() {
}"
@selection-change="handleSelectionChange"
>
<!-- 自定义操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮 -->
<el-button
class="reset-margin"
link
......@@ -122,6 +148,7 @@ function onFullscreen() {
>
修改
</el-button>
<!-- 新增子部门按钮 -->
<el-button
class="reset-margin"
link
......@@ -132,10 +159,12 @@ function onFullscreen() {
>
新增
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除部门名称为${row.name}的这条数据`"
@confirm="handleDelete(row)"
>
<!-- 触发确认弹窗的按钮 -->
<template #reference>
<el-button
class="reset-margin"
......@@ -156,14 +185,17 @@ function onFullscreen() {
</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;
......
......@@ -10,23 +10,50 @@ 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();
const columns: TableColumnList = [
/**
* 表格列配置类型
* @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 = [
{
label: "部门名称",
prop: "name",
width: 180,
align: "left"
align: "left" as "center" | "left" | "right"
},
{
label: "排序",
......@@ -37,6 +64,14 @@ export function useDept() {
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 ? "启用" : "停用"}
......@@ -47,6 +82,12 @@ export function useDept() {
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")
},
......@@ -63,39 +104,63 @@ export function useDept() {
}
];
/**
* 处理表格选择变化事件
* @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 => item.name.includes(form.name));
newData = newData.filter((item: { name: string }) =>
item.name.includes(form.name)
);
}
if (!isAllEmpty(form.status)) {
// 前端搜索状态
newData = newData.filter(item => item.status === form.status);
newData = newData.filter(
(item: { status: number | null }) => item.status === form.status
);
}
dataList.value = handleTree(newData); // 处理成树结构
// 处理数据为树结构
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;
......@@ -108,6 +173,11 @@ export function useDept() {
return newTreeList;
}
/**
* 打开新增或编辑部门对话框
* @param {string} [title="新增"] - 对话框标题
* @param {FormItemProps} [row] - 要编辑的部门数据
*/
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}部门`,
......@@ -129,10 +199,23 @@ export function useDept() {
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"
......@@ -153,7 +236,7 @@ export function useDept() {
}
});
} else {
if (!row.id) {
if (!row?.id) {
message("id不能为空", { type: "error" });
return;
}
......@@ -172,6 +255,12 @@ export function useDept() {
});
}
/**
* 处理删除部门操作
* @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 => {
......@@ -182,6 +271,7 @@ export function useDept() {
});
}
// 组件挂载后执行搜索操作
onMounted(() => {
onSearch();
});
......
// 从 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" }],
// 部门名称字段的验证规则
name: [
// 验证部门名称是否为必填项,若为空则提示 "部门名称为必填项",在失去焦点时触发验证
{ required: true, message: "部门名称为必填项", trigger: "blur" }
],
// 联系电话字段的验证规则
phone: [
{
/**
* 自定义验证函数,用于验证联系电话格式
* @param rule - 当前验证规则对象
* @param value - 当前字段的值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
// 若联系电话为空,则直接通过验证
if (value === "") {
callback();
} else if (!isPhone(value)) {
}
// 若联系电话格式不正确,则返回错误信息
else if (!isPhone(value)) {
callback(new Error("请输入正确的手机号码格式"));
} else {
}
// 若联系电话格式正确,则通过验证
else {
callback();
}
},
// 在失去焦点时触发验证
trigger: "blur"
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
// 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
// trigger: "click"
}
],
// 邮箱字段的验证规则
email: [
{
/**
* 自定义验证函数,用于验证邮箱格式
* @param rule - 当前验证规则对象
* @param value - 当前字段的值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
// 若邮箱为空,则直接通过验证
if (value === "") {
callback();
} else if (!isEmail(value)) {
}
// 若邮箱格式不正确,则返回错误信息
else if (!isEmail(value)) {
callback(new Error("请输入正确的邮箱格式"));
} else {
}
// 若邮箱格式正确,则通过验证
else {
callback();
}
},
......
/**
* 部门表单单项属性接口
* 定义了部门表单中每个字段的类型和属性,用于规范部门相关表单数据的结构。
*/
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"
};
};
......
<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";
// 引入删除图标
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";
/**
* 定义组件选项
* 设置组件名称为 SystemMenu
*/
defineOptions({
name: "SystemMenu"
});
// 创建表单引用
const formRef = ref();
// 创建表格引用
const tableRef = ref();
/**
* 从 useMenu hook 中解构出所需的状态和方法
* form: 搜索表单数据
* loading: 加载状态
* columns: 表格列配置
* dataList: 表格数据列表
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* openDialog: 打开新增或修改对话框的方法
* handleDelete: 处理删除操作的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
loading,
......@@ -28,6 +55,10 @@ const {
handleSelectionChange
} = useMenu();
/**
* 处理全屏操作
* 重置表格高度
*/
function onFullscreen() {
// 重置表格高度
tableRef.value.setAdaptive();
......@@ -36,12 +67,14 @@ function onFullscreen() {
<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"
......@@ -51,6 +84,7 @@ function onFullscreen() {
/>
</el-form-item>
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
......@@ -59,12 +93,14 @@ function onFullscreen() {
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="菜单管理(仅演示,操作后不生效)"
:columns="columns"
......@@ -73,7 +109,9 @@ function onFullscreen() {
@refresh="onSearch"
@fullscreen="onFullscreen"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<!-- 新增菜单按钮,点击打开新增对话框 -->
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
......@@ -82,7 +120,9 @@ function onFullscreen() {
新增菜单
</el-button>
</template>
<!-- 表格插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
adaptive
......@@ -101,7 +141,9 @@ function onFullscreen() {
}"
@selection-change="handleSelectionChange"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮,点击打开修改对话框 -->
<el-button
class="reset-margin"
link
......@@ -112,6 +154,7 @@ function onFullscreen() {
>
修改
</el-button>
<!-- 新增子菜单按钮,仅当菜单类型不为 3 时显示 -->
<el-button
v-show="row.menuType !== 3"
class="reset-margin"
......@@ -123,11 +166,13 @@ function onFullscreen() {
>
新增
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
@confirm="handleDelete(row)"
>
<template #reference>
<!-- 删除按钮 -->
<el-button
class="reset-margin"
link
......@@ -147,14 +192,17 @@ function onFullscreen() {
</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;
......
// 从 @/components/ReSegmented 导入 OptionsType 类型,用于规范选项数据的结构
import type { OptionsType } from "@/components/ReSegmented";
/**
* 菜单类型选项列表
* 定义了不同菜单类型的选项,每个选项包含标签和对应的值
*/
const menuTypeOptions: Array<OptionsType> = [
{
label: "菜单",
value: 1
label: "菜单", // 菜单类型的显示标签
value: 1 // 菜单类型对应的值
},
{
label: "目录",
value: 2
label: "目录", // 目录类型的显示标签
value: 2 // 目录类型对应的值
},
{
label: "外链",
value: 3
label: "外链", // 外链类型的显示标签
value: 3 // 外链类型对应的值
},
{
label: "按钮",
value: 4
label: "按钮", // 按钮类型的显示标签
value: 4 // 按钮类型对应的值
}
];
/**
* 显示链接选项列表
* 定义了菜单是否显示链接的选项,每个选项包含标签、提示信息和对应的值
*/
const showLinkOptions: Array<OptionsType> = [
{
label: "显示",
tip: "会在菜单中显示",
value: true
label: "显示", // 显示链接的显示标签
tip: "会在菜单中显示", // 显示链接的提示信息
value: true // 显示链接对应的值
},
{
label: "隐藏",
tip: "不会在菜单中显示",
value: false
label: "隐藏", // 隐藏链接的显示标签
tip: "不会在菜单中显示", // 隐藏链接的提示信息
value: false // 隐藏链接对应的值
}
];
/**
* 固定标签选项列表
* 定义了菜单标签是否固定显示的选项,每个选项包含标签、提示信息和对应的值
*/
const fixedTagOptions: Array<OptionsType> = [
{
label: "固定",
tip: "当前菜单名称固定显示在标签页且不可关闭",
value: true
label: "固定", // 固定标签的显示标签
tip: "当前菜单名称固定显示在标签页且不可关闭", // 固定标签的提示信息
value: true // 固定标签对应的值
},
{
label: "不固定",
tip: "当前菜单名称不固定显示在标签页且可关闭",
value: false
label: "不固定", // 不固定标签的显示标签
tip: "当前菜单名称不固定显示在标签页且可关闭", // 不固定标签的提示信息
value: false // 不固定标签对应的值
}
];
/**
* 缓存选项列表
* 定义了菜单页面是否缓存的选项,每个选项包含标签、提示信息和对应的值
*/
const keepAliveOptions: Array<OptionsType> = [
{
label: "缓存",
tip: "会保存该页面的整体状态,刷新后会清空状态",
value: true
label: "缓存", // 缓存的显示标签
tip: "会保存该页面的整体状态,刷新后会清空状态", // 缓存的提示信息
value: true // 缓存对应的值
},
{
label: "不缓存",
tip: "不会保存该页面的整体状态",
value: false
label: "不缓存", // 不缓存的显示标签
tip: "不会保存该页面的整体状态", // 不缓存的提示信息
value: false // 不缓存对应的值
}
];
/**
* 隐藏标签选项列表
* 定义了菜单名称或自定义信息是否允许添加到标签页的选项,每个选项包含标签、提示信息和对应的值
*/
const hiddenTagOptions: Array<OptionsType> = [
{
label: "允许",
tip: "当前菜单名称或自定义信息允许添加到标签页",
value: false
label: "允许", // 允许添加到标签页的显示标签
tip: "当前菜单名称或自定义信息允许添加到标签页", // 允许添加到标签页的提示信息
value: false // 允许添加到标签页对应的值
},
{
label: "禁止",
tip: "当前菜单名称或自定义信息禁止添加到标签页",
value: true
label: "禁止", // 禁止添加到标签页的显示标签
tip: "当前菜单名称或自定义信息禁止添加到标签页", // 禁止添加到标签页的提示信息
value: true // 禁止添加到标签页对应的值
}
];
/**
* 显示父级菜单选项列表
* 定义了是否显示父级菜单的选项,每个选项包含标签、提示信息和对应的值
*/
const showParentOptions: Array<OptionsType> = [
{
label: "显示",
tip: "会显示父级菜单",
value: true
label: "显示", // 显示父级菜单的显示标签
tip: "会显示父级菜单", // 显示父级菜单的提示信息
value: true // 显示父级菜单对应的值
},
{
label: "隐藏",
tip: "不会显示父级菜单",
value: false
label: "隐藏", // 隐藏父级菜单的显示标签
tip: "不会显示父级菜单", // 隐藏父级菜单的提示信息
value: false // 隐藏父级菜单对应的值
}
];
/**
* 框架加载动画选项列表
* 定义了是否开启首次加载动画的选项,每个选项包含标签、提示信息和对应的值
*/
const frameLoadingOptions: Array<OptionsType> = [
{
label: "开启",
tip: "有首次加载动画",
value: true
label: "开启", // 开启加载动画的显示标签
tip: "有首次加载动画", // 开启加载动画的提示信息
value: true // 开启加载动画对应的值
},
{
label: "关闭",
tip: "无首次加载动画",
value: false
label: "关闭", // 关闭加载动画的显示标签
tip: "无首次加载动画", // 关闭加载动画的提示信息
value: false // 关闭加载动画对应的值
}
];
/**
* 导出所有选项列表
* 方便在其他文件中引用这些选项数据
*/
export {
menuTypeOptions,
showLinkOptions,
......
......@@ -10,15 +10,29 @@ import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
// import { c } from "node_modules/vite/dist/node/moduleRunnerTransport.d-CXw_Ws6P";
/**
* 自定义 Hook,用于管理菜单相关的状态和操作
* @returns 包含菜单相关状态和操作的对象
*/
export function useMenu() {
// 定义表单数据,使用 reactive 创建响应式对象,初始包含菜单名称字段
const form = reactive({
name: ""
});
// 定义表单引用,用于获取表单实例
const formRef = ref();
// 定义菜单数据列表,使用 ref 创建响应式对象,初始为空数组
const dataList = ref([]);
// 定义加载状态,使用 ref 创建响应式对象,初始为 true 表示正在加载
const loading = ref(true);
/**
* 根据菜单类型返回对应的标签类型或文本描述
* @param type - 菜单类型,取值为 1, 2, 3, 4
* @param text - 是否返回文本描述,默认为 false
* @returns 标签类型或文本描述
*/
const getMenuType = (type, text = false) => {
switch (type) {
case 1:
......@@ -32,11 +46,13 @@ export function useMenu() {
}
};
const columns: TableColumnList = [
// 定义表格列配置,用于渲染菜单表格
const columns = [
{
label: "菜单名称",
prop: "name",
align: "left",
// 自定义单元格渲染函数,显示菜单图标和名称
cellRenderer: ({ row }) => (
<>
<span class="inline-block mr-1">
......@@ -52,6 +68,7 @@ export function useMenu() {
label: "菜单类型",
prop: "type",
width: 100,
// 自定义单元格渲染函数,显示菜单类型标签
cellRenderer: ({ row, props }) => (
<el-tag size={props.size} type={getMenuType(row.type)} effect="plain">
{getMenuType(row.type, true)}
......@@ -65,6 +82,7 @@ export function useMenu() {
{
label: "组件路径",
prop: "component",
// 自定义格式化函数,若组件路径为空则显示路由路径
formatter: ({ path, component }) =>
isAllEmpty(component) ? path : component
},
......@@ -80,6 +98,7 @@ export function useMenu() {
{
label: "隐藏",
prop: "visible",
// 自定义格式化函数,将布尔值转换为中文描述
formatter: ({ visible }) => (visible ? "是" : "否"),
width: 100
},
......@@ -91,46 +110,75 @@ export function useMenu() {
}
];
/**
* 处理表格选中项变化的事件
* @param val - 选中项的值
*/
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
/**
* 重置表单数据并重新搜索菜单
* @param formEl - 表单实例
*/
function resetForm(formEl) {
if (!formEl) return;
// 手动清空菜单名称
form.name = "";
// 重置表单字段
formEl.resetFields();
// 重新搜索菜单
onSearch();
}
/**
* 搜索菜单数据
*/
async function onSearch() {
// 设置加载状态为正在加载
loading.value = true;
// 请求菜单列表数据
const { data } = await getMenuList({ name: "", pageNum: 1, pageSize: 100 }); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
// 获取返回数据中的记录列表
let newData = (data as any).records;
if (!isAllEmpty(form.name)) {
// 前端搜索菜单名称
// 前端搜索菜单名称,过滤出包含搜索关键词的菜单
newData = newData.filter(item =>
transformI18n(item.name).includes(form.name)
);
}
dataList.value = handleTree(newData); // 处理成树结构
console.log("dataList", dataList.value);
// 将一维数组数据处理成树结构
dataList.value = handleTree(newData);
// 模拟加载延迟,500ms 后设置加载状态为完成
setTimeout(() => {
loading.value = false;
}, 500);
}
/**
* 格式化上级菜单选项,将菜单数据转换为可用于下拉选择的选项
* @param treeList - 菜单树状数据
* @returns 格式化后的菜单选项数组
*/
function formatHigherMenuOptions(treeList) {
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
// 对菜单名称进行国际化处理
treeList[i].name = transformI18n(treeList[i].name);
// 递归处理子菜单
formatHigherMenuOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
/**
* 打开新增或编辑菜单的对话框
* @param title - 对话框标题,默认为 "新增"
* @param row - 要编辑的菜单数据,可选
*/
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}菜单`,
......@@ -168,10 +216,17 @@ export function useMenu() {
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
// 对话框内容渲染函数,渲染编辑表单
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
// 对话框确认前的回调函数
beforeSure: (done, { options }) => {
// 获取表单实例
const FormRef = formRef.value.getRef();
// 获取表单数据
const curData = options.props.formInline as FormItemProps;
/**
* 处理成功操作后的通用任务,如提示消息、关闭对话框、刷新表格数据
*/
function chores() {
message(
`您${title}了菜单名称为${transformI18n(curData.name)}的这条数据`,
......@@ -179,15 +234,19 @@ export function useMenu() {
type: "success"
}
);
done(); // 关闭弹框
onSearch(); // 刷新表格数据
// 关闭对话框
done();
// 刷新表格数据
onSearch();
}
// 验证表单
FormRef.validate(valid => {
if (valid) {
// 表单规则校验通过
if (title === "新增") {
curData.visible =
curData.visible || curData.visible === 0 ? 1 : 0;
// 调用新增菜单接口
addMenu(curData).then(res => {
if ((res as any).code === "0") {
chores();
......@@ -201,12 +260,12 @@ export function useMenu() {
curData.id = row?.id;
curData.visible =
curData.visible || curData.visible === 0 ? 1 : 0;
// 调用更新菜单接口
updateMenu(curData).then(res => {
if ((res as any).code === "0") {
chores();
}
});
chores();
}
}
});
......@@ -214,17 +273,22 @@ export function useMenu() {
});
}
/**
* 处理删除菜单的操作
* @param row - 要删除的菜单数据
*/
function handleDelete(row) {
console.log("handleDelete", row.id);
if (!row.id) {
message("id不能为空", { type: "error" });
return;
}
// 调用删除菜单接口
deleteMenu({ id: row.id }).then(res => {
if ((res as any).code === "0") {
message(`您删除了菜单名称为${transformI18n(row.title)}的这条数据`, {
type: "success"
});
// 刷新表格数据
onSearch();
} else {
message((res as any).msg, { type: "error" });
......@@ -232,6 +296,7 @@ export function useMenu() {
});
}
// 组件挂载后执行搜索操作
onMounted(() => {
onSearch();
});
......
/**
* 从 Vue 导入 reactive 函数,用于创建响应式对象
*/
import { reactive } from "vue";
/**
* 从 element-plus 导入 FormRules 类型,用于定义表单验证规则
*/
import type { FormRules } from "element-plus";
/** 自定义表单规则校验 */
/**
* 自定义表单规则校验
* 该常量使用 reactive 函数创建一个响应式的表单验证规则对象,
* 包含对菜单名称、路由路径和权限标识的必填项验证。
*/
export const formRules = reactive(<FormRules>{
// 菜单名称的验证规则,要求该项为必填项,失去焦点时触发验证
name: [{ required: true, message: "菜单名称为必填项", trigger: "blur" }],
// 路由路径的验证规则,要求该项为必填项,失去焦点时触发验证
path: [{ required: true, message: "路由路径为必填项", trigger: "blur" }],
// 权限标识的验证规则,要求该项为必填项,失去焦点时触发验证
perm: [{ required: true, message: "权限标识为必填项", trigger: "blur" }]
});
/**
* 定义表单项的属性接口
*/
interface FormItemProps {
/** 菜单类型(0代表菜单、1代表iframe、2代表外链、3代表按钮)*/
type: number;
/** 上级菜单选项数组,每个选项是一个键值对对象 */
higherMenuOptions: Record<string, unknown>[];
/** 父菜单的ID */
parentId: number;
/** 菜单名称 */
name: string;
/** 菜单路径 */
path: string;
/** 菜单对应的组件 */
component: string;
/** 菜单权限 */
perm: string;
/** 菜单是否可见,值可以是数字或布尔类型 */
visible: number | boolean;
/** 菜单排序 */
sort: number;
/** 菜单图标 */
icon: string;
/** iframe 菜单的源地址 */
frameSrc: string;
/** 菜单ID,可选属性 */
id?: any;
// /** 菜单标题 */
// title: string;
// /** 菜单层级 */
// rank: number;
// /** 菜单重定向路径 */
// redirect: string;
// /** 额外图标 */
// extraIcon: string;
// /** 进入过渡动画 */
// enterTransition: string;
// /** 离开过渡动画 */
// leaveTransition: string;
// /** 激活路径 */
// activePath: string;
// /** 菜单权限列表 */
// auths: string;
// /** iframe 菜单加载状态 */
// frameLoading: boolean;
// /** 是否缓存组件 */
// keepAlive: boolean;
// /** 是否隐藏标签页 */
// hiddenTag: boolean;
// /** 是否固定标签页 */
// fixedTag: boolean;
// /** 是否显示链接 */
// showLink: boolean;
// /** 是否显示父菜单 */
// showParent: boolean;
}
/**
* 定义表单属性接口
*/
interface FormProps {
/** 内联表单,其属性遵循 FormItemProps 接口 */
formInline: FormItemProps;
}
/**
* 导出 FormItemProps 和 FormProps 类型,供其他模块使用
*/
export type { FormItemProps, FormProps };
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入当前目录下 utils 文件夹里 rule.ts 文件定义的表单验证规则
import { formRules } from "./utils/rule";
// 引入当前目录下 utils 文件夹里 types.ts 文件定义的表单属性类型
import { FormProps } from "./utils/types";
/**
* 定义组件的 props
* 使用 withDefaults 函数为 props 设置默认值,确保在父组件未传递 props 时,组件仍能正常工作。
* @property formInline - 表单内联数据,包含角色名称、角色标识和备注信息,默认初始值为空字符串。
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
name: "",
......@@ -11,24 +19,34 @@ const props = withDefaults(defineProps<FormProps>(), {
})
});
// 创建一个响应式引用,用于获取表单实例
const ruleFormRef = ref();
// 创建一个响应式引用,存储表单内联数据,初始值来自 props 传入的数据
const newFormInline = ref(props.formInline);
/**
* 获取表单实例
* @returns 返回表单实例,如果实例存在则返回,不存在则返回 undefined。
*/
function getRef() {
return ruleFormRef.value;
}
// 将 getRef 方法暴露给父组件,使得父组件可以调用该方法获取表单实例
defineExpose({ getRef });
</script>
<template>
<!-- 定义一个 Element Plus 的表单组件 -->
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<!-- 角色名称表单项 -->
<el-form-item label="角色名称" prop="name">
<!-- 输入框组件,绑定角色名称数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.name"
clearable
......@@ -36,7 +54,9 @@ defineExpose({ getRef });
/>
</el-form-item>
<!-- 角色标识表单项 -->
<el-form-item label="角色标识" prop="code">
<!-- 输入框组件,绑定角色标识数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.code"
clearable
......@@ -44,7 +64,9 @@ defineExpose({ getRef });
/>
</el-form-item>
<!-- 备注表单项 -->
<el-form-item label="备注">
<!-- 文本域输入框组件,绑定备注信息数据,有占位提示 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
......
<script setup lang="ts">
// 引入当前目录下 utils 文件夹里的 useRole hook,用于处理角色管理相关逻辑
import { useRole } from "./utils/hook";
// 从 Vue 引入响应式 API 和生命周期钩子
import { ref, computed, nextTick, onMounted } from "vue";
// 引入自定义表格栏组件
import { PureTableBar } from "@/components/RePureTableBar";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入工具函数
import {
delay,
subBefore,
......@@ -10,6 +15,7 @@ import {
useResizeObserver
} from "@pureadmin/utils";
// 引入图标
// import Database from "~icons/ri/database-2-line";
// import More from "~icons/ep/more-filled";
import Delete from "~icons/ep/delete";
......@@ -20,10 +26,18 @@ import AddFill from "~icons/ri/add-circle-line";
import Close from "~icons/ep/close";
import Check from "~icons/ep/check";
/**
* 定义组件选项
* 设置组件名称为 SystemRole,方便调试和组件识别
*/
defineOptions({
name: "SystemRole"
});
/**
* 计算图标类名
* 返回一个包含多个 CSS 类名的数组,用于设置图标的样式
*/
const iconClass = computed(() => {
return [
"w-[22px]",
......@@ -41,12 +55,46 @@ const iconClass = computed(() => {
];
});
// 创建树形组件引用
const treeRef = ref();
// 创建表单引用
const formRef = ref();
// 创建表格引用
const tableRef = ref();
// 创建内容区域引用
const contentRef = ref();
// 创建树形组件高度的响应式变量
const treeHeight = ref();
/**
* 从 useRole hook 中解构出所需的状态和方法
* form: 搜索表单数据
* isShow: 是否显示菜单权限弹窗
* curRow: 当前选中的角色行数据
* loading: 表格加载状态
* columns: 表格列配置
* rowStyle: 表格行样式方法
* dataList: 表格数据列表
* treeData: 树形菜单数据
* treeProps: 树形组件属性配置
* isLinkage: 是否联动操作
* pagination: 分页配置
* isExpandAll: 是否展开所有树形节点
* isSelectAll: 是否全选所有树形节点
* treeSearchValue: 树形搜索框的值
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* openDialog: 打开新增或修改对话框的方法
* handleMenu: 处理菜单权限操作的方法
* handleSave: 保存菜单权限的方法
* handleDelete: 处理删除角色操作的方法
* filterMethod: 树形组件过滤方法
* transformI18n: 国际化转换函数
* onQueryChanged: 处理搜索框内容变化的方法
* handleSizeChange: 处理每页显示数量变化的方法
* handleCurrentChange: 处理当前页码变化的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
isShow,
......@@ -78,10 +126,17 @@ const {
handleSelectionChange
} = useRole(treeRef);
/**
* 组件挂载后执行的操作
* 监听内容区域的大小变化,动态设置树形组件的高度
*/
onMounted(() => {
useResizeObserver(contentRef, async () => {
// 等待下一个 DOM 更新周期
await nextTick();
// 延迟 60 毫秒后执行
delay(60).then(() => {
// 获取表格包装器的高度,并转换为数字赋值给树形组件高度变量
treeHeight.value = parseFloat(
subBefore(tableRef.value.getTableDoms().tableWrapper.style.height, "px")
);
......@@ -92,12 +147,14 @@ onMounted(() => {
<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"
......@@ -106,6 +163,7 @@ onMounted(() => {
class="w-[180px]!"
/>
</el-form-item>
<!-- 角色标识输入项 -->
<el-form-item label="角色标识:" prop="code">
<el-input
v-model="form.code"
......@@ -114,6 +172,7 @@ onMounted(() => {
class="w-[180px]!"
/>
</el-form-item>
<!-- 状态选择项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
......@@ -125,7 +184,9 @@ onMounted(() => {
<el-option label="已停用" value="0" />
</el-select>
</el-form-item>
<!-- 操作按钮项 -->
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
......@@ -134,16 +195,19 @@ onMounted(() => {
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 内容区域 -->
<div
ref="contentRef"
:class="['flex', deviceDetection() ? 'flex-wrap' : '']"
>
<!-- 自定义表格栏组件 -->
<PureTableBar
:class="[isShow && !deviceDetection() ? 'w-[60vw]!' : 'w-full']"
style="transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1)"
......@@ -151,7 +215,9 @@ onMounted(() => {
:columns="columns"
@refresh="onSearch"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<!-- 新增角色按钮,点击打开新增对话框 -->
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
......@@ -160,7 +226,9 @@ onMounted(() => {
新增角色
</el-button>
</template>
<!-- 表格插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
align-whole="center"
......@@ -182,7 +250,9 @@ onMounted(() => {
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮,点击打开修改对话框 -->
<el-button
class="reset-margin"
link
......@@ -193,11 +263,13 @@ onMounted(() => {
>
修改
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除角色名称为${row.name}的这条数据`"
@confirm="handleDelete(row)"
>
<template #reference>
<!-- 删除按钮 -->
<el-button
class="reset-margin"
link
......@@ -209,6 +281,7 @@ onMounted(() => {
</el-button>
</template>
</el-popconfirm>
<!-- 权限按钮,点击处理菜单权限操作 -->
<el-button
class="reset-margin"
link
......@@ -219,6 +292,7 @@ onMounted(() => {
>
权限
</el-button>
<!-- 注释掉的下拉菜单,可用于扩展功能 -->
<!-- <el-dropdown>
<el-button
class="ml-3 mt-[2px]"
......@@ -261,12 +335,15 @@ onMounted(() => {
</template>
</PureTableBar>
<!-- 菜单权限弹窗,当 isShow 为 true 时显示 -->
<div
v-if="isShow"
class="min-w-[calc(100vw-60vw-268px)]! w-full mt-2 px-2 pb-2 bg-bg_color ml-2 overflow-auto"
>
<!-- 弹窗头部 -->
<div class="flex justify-between w-full px-3 pt-5 pb-4">
<div class="flex">
<!-- 关闭图标,点击隐藏菜单权限弹窗 -->
<span :class="iconClass">
<IconifyIconOffline
v-tippy="{
......@@ -279,6 +356,7 @@ onMounted(() => {
@click="handleMenu"
/>
</span>
<!-- 保存图标,点击保存菜单权限 -->
<span :class="[iconClass, 'ml-2']">
<IconifyIconOffline
v-tippy="{
......@@ -292,11 +370,13 @@ onMounted(() => {
/>
</span>
</div>
<!-- 弹窗标题 -->
<p class="font-bold truncate">
菜单权限
{{ `${curRow?.name ? `(${curRow.name})` : ""}` }}
</p>
</div>
<!-- 树形搜索框 -->
<el-input
v-model="treeSearchValue"
placeholder="请输入菜单进行搜索"
......@@ -304,11 +384,13 @@ onMounted(() => {
clearable
@input="onQueryChanged"
/>
<!-- 操作复选框 -->
<div class="flex flex-wrap">
<el-checkbox v-model="isExpandAll" label="展开/折叠" />
<el-checkbox v-model="isSelectAll" label="全选/全不选" />
<el-checkbox v-model="isLinkage" label="父子联动" />
</div>
<!-- 树形组件 -->
<el-tree-v2
ref="treeRef"
show-checkbox
......@@ -318,7 +400,9 @@ onMounted(() => {
:check-strictly="!isLinkage"
:filter-method="filterMethod"
>
<!-- 树形节点默认插槽 -->
<template #default="{ node }">
<!-- 显示树形节点标签,进行国际化转换 -->
<span>{{ transformI18n(node.label) }}</span>
</template>
</el-tree-v2>
......@@ -328,14 +412,17 @@ onMounted(() => {
</template>
<style lang="scss" scoped>
/* 设置下拉菜单中图标元素的外边距为 0 */
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
/* 设置主内容区域的外边距 */
.main-content {
margin: 24px 24px 0 !important;
}
/* 设置搜索表单内表单项的底部外边距 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
......
// 从 Vue 中引入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 Element Plus 引入 FormRules 类型,用于定义表单验证规则
import type { FormRules } from "element-plus";
/** 自定义表单规则校验 */
/**
* 自定义表单规则校验
* 此常量包含了角色表单中各个字段的验证规则,使用 reactive 函数使其变为响应式对象,
* 方便在表单验证过程中动态更新规则。
*/
export const formRules = reactive(<FormRules>{
// 角色名称字段的验证规则
name: [{ required: true, message: "角色名称为必填项", trigger: "blur" }],
// 角色标识字段的验证规则
code: [{ required: true, message: "角色标识为必填项", trigger: "blur" }]
});
// 虽然字段很少 但是抽离出来 后续有扩展字段需求就很方便了
/**
* 定义表单单项属性的接口
* 该接口描述了角色表单中单个表单项的属性结构
*/
interface FormItemProps {
/** 角色名称 */
/** 角色名称,必填字段,字符串类型 */
name: string;
/** 角色编号 */
/** 角色编号,必填字段,字符串类型 */
code: string;
/** 备注 */
/** 备注信息,可选字段,字符串类型 */
remark: string;
/** 角色 ID,可选字段,字符串类型 */
id?: string;
}
/**
* 定义表单属性的接口
* 该接口描述了整个角色表单的属性结构,包含一个 FormItemProps 类型的表单内联对象
*/
interface FormProps {
/** 表单内联对象,包含角色表单的各项属性 */
formInline: FormItemProps;
}
/**
* 导出定义的类型
* 导出 FormItemProps 和 FormProps 接口,方便在其他文件中使用这些类型定义
*/
export type { FormItemProps, FormProps };
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入自定义列组件
import ReCol from "@/components/ReCol";
// 引入表单验证规则
import { formRules } from "../utils/rule";
// 引入表单属性类型定义
import { FormProps } from "../utils/types";
// 引入公共 hook
import { usePublicHooks } from "../../hooks";
/**
* 定义组件的 props
* 使用 withDefaults 函数为 props 设置默认值,确保在父组件未传递 props 时,组件能正常初始化。
* @property formInline - 表单内联数据,包含用户相关信息,如昵称、用户名等。
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
title: "新增",
......@@ -22,6 +32,10 @@ const props = withDefaults(defineProps<FormProps>(), {
})
});
/**
* 性别选项数组
* 包含男性和女性两个选项,每个选项有对应的值和显示标签。
*/
const sexOptions = [
{
value: 0,
......@@ -32,27 +46,40 @@ const sexOptions = [
label: "女"
}
];
// 创建一个响应式引用,用于获取表单实例
const ruleFormRef = ref();
// 从公共 hook 中获取开关样式
const { switchStyle } = usePublicHooks();
// 创建一个响应式引用,存储表单内联数据,初始值来自 props 传入的数据
const newFormInline = ref(props.formInline);
/**
* 获取表单实例
* @returns 返回表单实例,如果实例存在则返回,不存在则返回 undefined。
*/
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 :value="12" :xs="24" :sm="24">
<el-form-item label="用户昵称" prop="nickname">
<!-- 输入框组件,绑定用户昵称数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.nickname"
clearable
......@@ -60,8 +87,11 @@ defineExpose({ getRef });
/>
</el-form-item>
</re-col>
<!-- 用户名称表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="用户名称" prop="username">
<!-- 输入框组件,绑定用户名称数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.username"
clearable
......@@ -70,7 +100,9 @@ defineExpose({ getRef });
</el-form-item>
</re-col>
<!-- <re-col
<!-- 用户密码表单项,仅在表单标题为“新增”时显示 -->
<!--
<re-col
v-if="newFormInline.title === '新增'"
:value="12"
:xs="24"
......@@ -83,9 +115,13 @@ defineExpose({ getRef });
placeholder="请输入用户密码"
/>
</el-form-item>
</re-col> -->
</re-col>
-->
<!-- 手机号表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="手机号" prop="mobile">
<!-- 输入框组件,绑定手机号数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.mobile"
clearable
......@@ -94,8 +130,10 @@ defineExpose({ getRef });
</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
......@@ -103,14 +141,18 @@ defineExpose({ getRef });
/>
</el-form-item>
</re-col>
<!-- 用户性别表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="用户性别">
<!-- 下拉选择框组件,绑定用户性别数据,支持清空选择,有占位提示 -->
<el-select
v-model="newFormInline.gender"
placeholder="请选择用户性别"
class="w-full"
clearable
>
<!-- 循环渲染性别选项 -->
<el-option
v-for="(item, index) in sexOptions"
:key="index"
......@@ -121,8 +163,10 @@ defineExpose({ getRef });
</el-form-item>
</re-col>
<!-- 归属部门表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="归属部门">
<!-- 级联选择器组件,绑定归属部门父 ID 数据,支持清空选择、过滤选项,有占位提示 -->
<el-cascader
v-model="newFormInline.parentId"
class="w-full"
......@@ -137,14 +181,19 @@ defineExpose({ getRef });
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
<!-- 用户状态表单项,仅在表单标题为“新增”时显示 -->
<!--
<re-col
v-if="newFormInline.title === '新增'"
:value="12"
:xs="24"
......@@ -161,10 +210,13 @@ defineExpose({ getRef });
:style="switchStyle"
/>
</el-form-item>
</re-col> -->
</re-col>
-->
<!-- 备注表单项 -->
<re-col>
<el-form-item label="备注">
<!-- 文本域输入框组件,绑定备注信息数据,有占位提示 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
......
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入自定义列组件
import ReCol from "@/components/ReCol";
// 引入角色表单属性类型定义
import { RoleFormProps } from "../utils/types";
/**
* 定义组件的 props
* 使用 withDefaults 函数为 props 设置默认值,确保在父组件未传递 props 时,组件能正常初始化。
* @property formInline - 表单内联数据,包含用户名、昵称、角色选项和所选角色 ID 列表。
*/
const props = withDefaults(defineProps<RoleFormProps>(), {
formInline: () => ({
username: "",
......@@ -12,24 +20,32 @@ const props = withDefaults(defineProps<RoleFormProps>(), {
})
});
// 创建一个响应式引用,存储表单内联数据,初始值来自 props 传入的数据
const newFormInline = ref(props.formInline);
</script>
<template>
<!-- 定义一个 Element Plus 的表单组件,绑定表单数据 -->
<el-form :model="newFormInline">
<!-- 定义一个行布局,设置列间距为 30px -->
<el-row :gutter="30">
<!-- 注释掉的用户名称表单项 -->
<!-- <re-col>
<el-form-item label="用户名称" prop="username">
<el-input disabled v-model="newFormInline.username" />
</el-form-item>
</re-col> -->
<!-- 用户昵称表单项 -->
<re-col>
<el-form-item label="用户昵称" prop="nickname">
<!-- 输入框组件,绑定用户昵称数据,设置为禁用状态 -->
<el-input v-model="newFormInline.nickname" disabled />
</el-form-item>
</re-col>
<!-- 角色列表表单项 -->
<re-col>
<el-form-item label="角色列表" prop="ids">
<!-- 下拉选择框组件,支持多选,绑定所选角色 ID 列表,有占位提示,支持清空选择 -->
<el-select
v-model="newFormInline.ids"
placeholder="请选择"
......@@ -37,12 +53,14 @@ const newFormInline = ref(props.formInline);
clearable
multiple
>
<!-- 循环渲染角色选项 -->
<el-option
v-for="(item, index) in newFormInline.roleOptions"
:key="index"
:value="item.id"
:label="item.name"
>
<!-- 显示角色名称 -->
{{ item.name }}
</el-option>
</el-select>
......
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入部门树组件
import tree from "./tree.vue";
// 引入用户管理相关的自定义 hook
import { useUser } from "./utils/hook";
// 引入自定义表格栏组件
import { PureTableBar } from "@/components/RePureTableBar";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入上传图标
import Upload from "~icons/ri/upload-line";
// 引入角色图标
import Role from "~icons/ri/admin-line";
// 引入密码图标
import Password from "~icons/ri/lock-password-line";
// 引入更多操作图标
import More from "~icons/ep/more-filled";
// 引入删除图标
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";
/**
* 定义组件选项
* 设置组件名称为 SystemUser,方便调试和识别
*/
defineOptions({
name: "SystemUser"
});
// 创建部门树组件的引用
const treeRef = ref();
// 创建表单的引用
const formRef = ref();
// 创建表格的引用
const tableRef = ref();
/**
* 从 useUser hook 中解构出所需的状态和方法
* form: 搜索表单数据
* loading: 表格加载状态
* columns: 表格列配置
* dataList: 表格数据列表
* treeData: 部门树数据
* treeLoading: 部门树加载状态
* selectedNum: 表格中选中的记录数量
* pagination: 分页配置
* buttonClass: 按钮样式类
* deviceDetection: 设备检测方法
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* onbatchDel: 批量删除操作的方法
* openDialog: 打开新增或编辑用户对话框的方法
* onTreeSelect: 部门树节点选择的处理方法
* handleUpdate: 更新用户信息的方法
* handleDelete: 删除单个用户的方法
* handleUpload: 上传用户头像的方法
* handleReset: 重置用户密码的方法
* handleRole: 分配用户角色的方法
* handleSizeChange: 处理每页显示数量变化的方法
* onSelectionCancel: 取消表格选中的方法
* handleCurrentChange: 处理当前页码变化的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
loading,
......@@ -51,7 +98,9 @@ const {
</script>
<template>
<!-- 主容器,根据设备检测结果进行不同布局 -->
<div :class="['flex', 'justify-between', deviceDetection() && 'flex-wrap']">
<!-- 部门树组件 -->
<tree
ref="treeRef"
:class="['mr-2', deviceDetection() ? 'w-full' : 'min-w-[200px]']"
......@@ -59,15 +108,19 @@ const {
:treeLoading="treeLoading"
@tree-select="onTreeSelect"
/>
<!-- 右侧内容区域,根据设备检测结果调整宽度 -->
<div
:class="[deviceDetection() ? ['w-full', 'mt-2'] : 'w-[calc(100%-200px)]']"
>
<!-- 搜索表单 -->
<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="username">
<el-input
v-model="form.username"
......@@ -76,6 +129,8 @@ const {
class="w-[180px]!"
/>
</el-form-item>
<!-- 手机号码输入项 -->
<el-form-item label="手机号码:" prop="mobile">
<el-input
v-model="form.mobile"
......@@ -84,6 +139,8 @@ const {
class="w-[180px]!"
/>
</el-form-item>
<!-- 用户状态选择项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
......@@ -95,7 +152,10 @@ const {
<el-option label="已关闭" value="0" />
</el-select>
</el-form-item>
<!-- 操作按钮项 -->
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
......@@ -104,17 +164,20 @@ const {
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="用户管理(仅演示,操作后不生效)"
:columns="columns"
@refresh="onSearch"
>
<!-- 自定义按钮插槽,放置新增用户按钮 -->
<template #buttons>
<el-button
type="primary"
......@@ -124,23 +187,29 @@ const {
新增用户
</el-button>
</template>
<!-- 表格内容插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 当有选中记录时显示提示信息和批量操作按钮 -->
<div
v-if="selectedNum > 0"
v-motion-fade
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
>
<div class="flex-auto">
<!-- 显示已选记录数量 -->
<span
style="font-size: var(--el-font-size-base)"
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
>
已选 {{ selectedNum }}
</span>
<!-- 取消选择按钮,点击触发取消选择操作 -->
<el-button type="primary" text @click="onSelectionCancel">
取消选择
</el-button>
</div>
<!-- 删除确认弹窗,确认后触发批量删除操作 -->
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
<template #reference>
<el-button type="danger" text class="mr-1!">
......@@ -149,6 +218,8 @@ const {
</template>
</el-popconfirm>
</div>
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
row-key="id"
......@@ -169,7 +240,9 @@ const {
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
>
<!-- 操作列插槽,放置针对每条记录的操作按钮 -->
<template #operation="{ row }">
<!-- 编辑按钮,点击打开编辑用户对话框 -->
<el-button
class="reset-margin"
link
......@@ -180,6 +253,8 @@ const {
>
修改
</el-button>
<!-- 删除确认弹窗,确认后触发删除单个用户操作 -->
<!-- <el-popconfirm
:title="`是否确认删除用户编号为${row.id}的这条数据`"
@confirm="handleDelete(row)"
......@@ -208,6 +283,7 @@ const {
/>
<template #dropdown>
<el-dropdown-menu>
<!-- 上传头像菜单项,点击触发上传头像操作 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
......@@ -220,6 +296,7 @@ const {
上传头像
</el-button>
</el-dropdown-item>
<!-- 重置密码菜单项,点击触发重置密码操作 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
......@@ -232,6 +309,7 @@ const {
重置密码
</el-button>
</el-dropdown-item>
<!-- 分配角色菜单项,点击触发分配角色操作 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
......@@ -256,18 +334,22 @@ const {
</template>
<style lang="scss" scoped>
/* 设置下拉菜单中图标元素的外边距为 0 */
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
/* 设置按钮获得焦点时不显示轮廓 */
:deep(.el-button:focus-visible) {
outline: none;
}
/* 主内容区域样式,设置外边距 */
.main-content {
margin: 24px 24px 0 !important;
}
/* 搜索表单样式,设置内部表单项的底部外边距 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
......
<script setup lang="ts">
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 从 Vue 引入响应式 API、监听 API 和获取当前实例的函数
import { ref, computed, watch, getCurrentInstance } from "vue";
// 引入图标组件
import Dept from "~icons/ri/git-branch-line";
// import Reset from "~icons/ri/restart-line";
import More2Fill from "~icons/ri/more-2-fill?width=18&height=18";
......@@ -10,29 +13,56 @@ import LocationCompany from "~icons/ep/add-location";
import ExpandIcon from "./svg/expand.svg?component";
import UnExpandIcon from "./svg/unexpand.svg?component";
/**
* 定义树形结构节点的接口
* 描述了树形结构中每个节点的属性结构
*/
interface Tree {
id: number;
name: string;
highlight?: boolean;
children?: Tree[];
id: number; // 节点唯一标识
name: string; // 节点显示名称
highlight?: boolean; // 节点是否高亮,可选属性
children?: Tree[]; // 子节点数组,可选属性
}
/**
* 定义组件的 props
* 接收父组件传递的部门树加载状态和部门树数据
*/
defineProps({
treeLoading: Boolean,
treeData: Array
treeLoading: Boolean, // 部门树是否正在加载
treeData: Array // 部门树数据数组
});
/**
* 定义组件的自定义事件
* 当部门树节点被选择时触发 `tree-select` 事件
*/
const emit = defineEmits(["tree-select"]);
// 创建树形组件的引用
const treeRef = ref();
// 记录树形组件是否全部展开的状态
const isExpand = ref(true);
// 记录搜索框的输入值
const searchValue = ref("");
// 记录节点高亮状态的映射对象
const highlightMap = ref({});
// 获取当前组件实例
const { proxy } = getCurrentInstance();
/**
* 定义树形组件的默认属性
* 指定树形组件中表示子节点和显示标签的字段名
*/
const defaultProps = {
children: "children",
label: "name"
children: "children", // 表示子节点的字段名
label: "name" // 表示显示标签的字段名
};
/**
* 计算按钮的样式类
* 返回一个包含多个 CSS 类名的数组,用于设置按钮的样式
*/
const buttonClass = computed(() => {
return [
"h-[20px]!",
......@@ -44,13 +74,26 @@ const buttonClass = computed(() => {
];
});
/**
* 过滤树形节点的方法
* 根据搜索框输入值过滤树形节点,包含输入值的节点将显示
* @param value - 搜索框输入的过滤值
* @param data - 当前树形节点的数据
* @returns 如果节点包含过滤值或过滤值为空则返回 true,否则返回 false
*/
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return data.name.includes(value);
if (!value) return true; // 过滤值为空,显示所有节点
return data.name.includes(value); // 节点名称包含过滤值则显示
};
/**
* 处理树形节点点击事件
* 切换点击节点的高亮状态,并将选中状态通过事件传递给父组件
* @param value - 点击的节点数据
*/
function nodeClick(value) {
const nodeId = value.$treeNodeId;
const nodeId = value.$treeNodeId; // 获取节点的唯一标识
// 切换当前节点的高亮状态
highlightMap.value[nodeId] = highlightMap.value[nodeId]?.highlight
? Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
highlight: false
......@@ -58,11 +101,13 @@ function nodeClick(value) {
: Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
highlight: true
});
// 取消其他节点的高亮状态
Object.values(highlightMap.value).forEach((v: Tree) => {
if (v.id !== nodeId) {
v.highlight = false;
}
});
// 触发 `tree-select` 事件,传递选中节点的信息
emit(
"tree-select",
highlightMap.value[nodeId]?.highlight
......@@ -71,35 +116,54 @@ function nodeClick(value) {
);
}
/**
* 展开或折叠所有树形节点
* 根据传入的状态设置所有节点的展开状态
* @param status - 展开或折叠状态,true 为展开,false 为折叠
*/
function toggleRowExpansionAll(status) {
isExpand.value = status;
const nodes = (proxy.$refs["treeRef"] as any).store._getAllNodes();
isExpand.value = status; // 更新展开状态记录
const nodes = (proxy.$refs["treeRef"] as any).store._getAllNodes(); // 获取所有节点
for (let i = 0; i < nodes.length; i++) {
nodes[i].expanded = status;
nodes[i].expanded = status; // 设置节点的展开状态
}
}
/** 重置部门树状态(选中状态、搜索框值、树初始化) */
/**
* 重置部门树状态
* 清空节点高亮状态、搜索框内容,并展开所有节点
*/
function onTreeReset() {
highlightMap.value = {};
searchValue.value = "";
toggleRowExpansionAll(true);
highlightMap.value = {}; // 清空节点高亮状态
searchValue.value = ""; // 清空搜索框内容
toggleRowExpansionAll(true); // 展开所有节点
}
/**
* 监听搜索框输入值的变化
* 当搜索框输入值变化时,触发树形组件的过滤方法
*/
watch(searchValue, val => {
treeRef.value!.filter(val);
treeRef.value!.filter(val); // 调用树形组件的过滤方法
});
/**
* 将重置部门树状态的方法暴露给父组件
* 允许父组件调用 `onTreeReset` 方法重置部门树状态
*/
defineExpose({ onTreeReset });
</script>
<template>
<!-- 容器元素,根据 `treeLoading` 显示加载状态,设置背景、高度等样式 -->
<div
v-loading="treeLoading"
class="h-full bg-bg_color overflow-hidden relative"
:style="{ minHeight: `calc(100vh - 141px)` }"
>
<!-- 搜索框和操作按钮容器 -->
<div class="flex items-center h-[34px]">
<!-- 搜索框组件,绑定搜索框输入值,设置占位提示和清空按钮 -->
<el-input
v-model="searchValue"
class="ml-2"
......@@ -107,6 +171,7 @@ defineExpose({ onTreeReset });
placeholder="请输入部门名称"
clearable
>
<!-- 搜索框后缀图标,输入为空时显示搜索图标 -->
<template #suffix>
<el-icon class="el-input__icon">
<IconifyIconOffline
......@@ -116,10 +181,14 @@ defineExpose({ onTreeReset });
</el-icon>
</template>
</el-input>
<!-- 下拉菜单组件,提供展开/折叠和重置状态操作 -->
<el-dropdown :hide-on-click="false">
<!-- 下拉菜单触发图标 -->
<More2Fill class="w-[28px] cursor-pointer outline-hidden" />
<!-- 下拉菜单内容 -->
<template #dropdown>
<el-dropdown-menu>
<!-- 展开/折叠菜单项 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
......@@ -128,9 +197,11 @@ defineExpose({ onTreeReset });
:icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)"
@click="toggleRowExpansionAll(isExpand ? false : true)"
>
<!-- 根据展开状态显示不同的文本 -->
{{ isExpand ? "折叠全部" : "展开全部" }}
</el-button>
</el-dropdown-item>
<!-- 注释掉的重置状态菜单项 -->
<!-- <el-dropdown-item>
<el-button
:class="buttonClass"
......@@ -146,8 +217,11 @@ defineExpose({ onTreeReset });
</template>
</el-dropdown>
</div>
<!-- 分割线 -->
<el-divider />
<!-- 滚动条组件,设置高度 -->
<el-scrollbar height="calc(90vh - 88px)">
<!-- 树形组件,绑定数据、属性和事件 -->
<el-tree
ref="treeRef"
:data="treeData"
......@@ -159,6 +233,7 @@ defineExpose({ onTreeReset });
:filter-node-method="filterNode"
@node-click="nodeClick"
>
<!-- 树形节点默认内容模板 -->
<template #default="{ node, data }">
<div
:class="[
......@@ -181,6 +256,7 @@ defineExpose({ onTreeReset });
: 'transparent'
}"
>
<!-- 根据节点类型显示不同的图标 -->
<IconifyIconOffline
:icon="
data.type === 1
......@@ -190,6 +266,7 @@ defineExpose({ onTreeReset });
: Dept
"
/>
<!-- 显示节点名称,超出部分用省略号表示 -->
<span class="w-[120px]! truncate!" :title="node.label">
{{ node.label }}
</span>
......@@ -201,10 +278,12 @@ defineExpose({ onTreeReset });
</template>
<style lang="scss" scoped>
/* 设置分割线的外边距为 0 */
:deep(.el-divider) {
margin: 0;
}
/* 设置树形组件节点悬停时的背景色为透明 */
:deep(.el-tree) {
--el-tree-node-hover-bg-color: transparent;
}
......
......@@ -52,7 +52,8 @@ export function useUser(tableRef: Ref, treeRef: Ref) {
mobile: null,
status: null,
pageNum: 1,
pageSize: 10
pageSize: 10,
deptId: ""
});
const formRef = ref();
const ruleFormRef = ref();
......
// 从 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";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
nickname: [{ required: true, message: "用户昵称为必填项", trigger: "blur" }],
username: [{ required: true, message: "用户名称为必填项", trigger: "blur" }],
password: [{ required: true, message: "用户密码为必填项", trigger: "blur" }],
/**
* 自定义表单规则校验
* 该常量定义了用户表单中各个字段的验证规则,使用 reactive 函数将其转换为响应式对象,
* 方便在表单验证过程中动态更新规则。
*/
export const formRules = reactive<FormRules>({
// 昵称字段验证规则
nickname: [
{
required: true, // 该字段为必填项
message: "用户昵称为必填项", // 验证失败时的提示信息
trigger: "blur" // 触发验证的时机为失去焦点时
}
],
// 用户名字段验证规则
username: [
{
required: true, // 该字段为必填项
message: "用户名称为必填项", // 验证失败时的提示信息
trigger: "blur" // 触发验证的时机为失去焦点时
}
],
// 密码字段验证规则
password: [
{
required: true, // 该字段为必填项
message: "用户密码为必填项", // 验证失败时的提示信息
trigger: "blur" // 触发验证的时机为失去焦点时
}
],
// 手机号字段验证规则
phone: [
{
validator: (rule, value, callback) => {
if (value === "") {
// 如果手机号为空,则不进行格式验证,直接通过
callback();
} else if (!isPhone(value)) {
// 如果手机号不为空且格式不正确,返回错误信息
callback(new Error("请输入正确的手机号码格式"));
} else {
// 如果手机号不为空且格式正确,通过验证
callback();
}
},
trigger: "blur"
trigger: "blur" // 触发验证的时机为失去焦点时
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
}
],
// 邮箱字段验证规则
email: [
{
validator: (rule, value, callback) => {
if (value === "") {
// 如果邮箱为空,则不进行格式验证,直接通过
callback();
} else if (!isEmail(value)) {
// 如果邮箱不为空且格式不正确,返回错误信息
callback(new Error("请输入正确的邮箱格式"));
} else {
// 如果邮箱不为空且格式正确,通过验证
callback();
}
},
trigger: "blur"
trigger: "blur" // 触发验证的时机为失去焦点时
}
]
});
/**
* 定义用户表单单项属性的接口
* 此接口描述了用户表单中单个表单项的属性结构
*/
interface FormItemProps {
/** 用户 ID,可选字段,类型为数字 */
id?: number;
/** 用于判断是`新增`还是`修改` */
/** 用于判断是 `新增` 还是 `修改` 操作,类型为字符串 */
title: string;
/** 上级部门选项数组,每个选项是一个键值对对象,类型为 Record<string, unknown> 数组 */
higherDeptOptions: Record<string, unknown>[];
/** 父部门的 ID,类型为数字 */
parentId: number;
/** 用户昵称,类型为字符串 */
nickname: string;
/** 用户名,类型为字符串 */
username: string;
/** 用户密码,类型为字符串 */
password: string;
/** 用户手机号,类型可以是字符串或数字 */
mobile: string | number;
/** 用户邮箱,类型为字符串 */
email: string;
/** 用户性别,类型可以是字符串或数字 */
gender: string | number;
/** 用户状态,类型为数字 */
status: number;
/** 用户所属部门信息,可选字段,包含部门 ID 和部门名称 */
dept?: {
/** 部门 ID,可选字段,类型为数字 */
id?: number;
/** 部门名称,可选字段,类型为字符串 */
name?: string;
};
/** 备注信息,类型为字符串 */
remark: string;
/** 名称,可选字段,类型为字符串 */
name?: string;
/** 部门 ID,可选字段,类型为数字 */
deptId?: number;
/** 用户头像地址,可选字段,类型为字符串 */
avatar?: string;
}
/**
* 定义用户表单属性的接口
* 此接口描述了整个用户表单的属性结构,包含一个 FormItemProps 类型的表单内联对象
*/
interface FormProps {
/** 表单内联对象,包含用户表单的各项属性 */
formInline: FormItemProps;
}
/**
* 定义角色表单单项属性的接口
* 此接口描述了角色表单中单个表单项的属性结构
*/
interface RoleFormItemProps {
/** 用户名,类型为字符串 */
username: string;
/** 用户昵称,类型为字符串 */
nickname: string;
/** 角色列表 */
/** 角色列表,类型为任意类型的数组 */
roleOptions: any[];
/** 选中的角色列表 */
/** 选中的角色列表,每个元素是一个以数字为键的键值对对象数组 */
ids: Record<number, unknown>[];
}
/**
* 定义角色表单属性的接口
* 此接口描述了整个角色表单的属性结构,包含一个 RoleFormItemProps 类型的表单内联对象
*/
interface RoleFormProps {
/** 表单内联对象,包含角色表单的各项属性 */
formInline: RoleFormItemProps;
}
/**
* 导出定义的类型
* 导出 FormItemProps、FormProps、RoleFormItemProps 和 RoleFormProps 接口,
* 方便在其他文件中使用这些类型定义
*/
export type { FormItemProps, FormProps, RoleFormItemProps, RoleFormProps };
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论