|
|
@@ -7,9 +7,15 @@
|
|
|
:mask-closable="false"
|
|
|
:ok-loading="loading"
|
|
|
@cancel="close"
|
|
|
- @before-ok="submit">
|
|
|
+ @before-ok="submit"
|
|
|
+ >
|
|
|
<!-- 表单信息 start -->
|
|
|
- <a-form ref="formRef" :model="formData" :rules="rules" :auto-label-width="true">
|
|
|
+ <a-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="formData"
|
|
|
+ :rules="rules"
|
|
|
+ :auto-label-width="true"
|
|
|
+ >
|
|
|
<a-row :gutter="16">
|
|
|
<a-col :span="12">
|
|
|
<a-form-item field="username" label="账户">
|
|
|
@@ -23,7 +29,8 @@
|
|
|
:data="deptData"
|
|
|
:field-names="{ key: 'value', title: 'label' }"
|
|
|
allow-clear
|
|
|
- placeholder="请选择所属部门">
|
|
|
+ placeholder="请选择所属部门"
|
|
|
+ >
|
|
|
</a-tree-select>
|
|
|
<template #extra>
|
|
|
<div>如果修改了部门,需要重新设置权限</div>
|
|
|
@@ -34,7 +41,28 @@
|
|
|
<a-row :gutter="16">
|
|
|
<a-col :span="12">
|
|
|
<a-form-item field="password" label="密码">
|
|
|
- <a-input-password v-model="formData.password" placeholder="请输入密码" />
|
|
|
+ <a-input-password
|
|
|
+ v-model="formData.password"
|
|
|
+ :placeholder="
|
|
|
+ mode === 'edit' ? '不填写则不修改密码' : '请输入密码'
|
|
|
+ "
|
|
|
+ @input="checkPasswordSafe"
|
|
|
+ @clear="() => (passwordSafePercent = 0)"
|
|
|
+ allow-clear
|
|
|
+ />
|
|
|
+ <template #extra>
|
|
|
+ <div v-if="formData.password">
|
|
|
+ <a-progress
|
|
|
+ :steps="3"
|
|
|
+ status="success"
|
|
|
+ :percent="passwordSafePercent"
|
|
|
+ animation
|
|
|
+ :show-text="false"
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ <span class="text-xs text-gray-500 ml-1">密码强度</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
<a-col :span="12">
|
|
|
@@ -53,7 +81,8 @@
|
|
|
:tree-check-strictly="true"
|
|
|
allow-clear
|
|
|
tree-checkable
|
|
|
- placeholder="请选择角色">
|
|
|
+ placeholder="请选择角色"
|
|
|
+ >
|
|
|
</a-tree-select>
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
@@ -73,7 +102,11 @@
|
|
|
<a-row :gutter="16">
|
|
|
<a-col :span="24">
|
|
|
<a-form-item label="状态" field="status">
|
|
|
- <sa-radio v-model="formData.status" dict="data_status" placeholder="请选择状态" />
|
|
|
+ <sa-radio
|
|
|
+ v-model="formData.status"
|
|
|
+ dict="data_status"
|
|
|
+ placeholder="请选择状态"
|
|
|
+ />
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
</a-row>
|
|
|
@@ -90,125 +123,247 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, computed } from 'vue'
|
|
|
-import { Message } from '@arco-design/web-vue'
|
|
|
-import api from '@/api/system/user'
|
|
|
-import tool from '@/utils/tool'
|
|
|
-import commonApi from '@/api/common'
|
|
|
+import { ref, reactive, computed } from "vue";
|
|
|
+import { Message } from "@arco-design/web-vue";
|
|
|
+import api from "@/api/system/user";
|
|
|
+import tool from "@/utils/tool";
|
|
|
+import commonApi from "@/api/common";
|
|
|
+import zxcvbn from "zxcvbn";
|
|
|
|
|
|
-const emit = defineEmits(['success'])
|
|
|
+const emit = defineEmits(["success"]);
|
|
|
|
|
|
// 引用定义
|
|
|
-const formRef = ref()
|
|
|
-const mode = ref('')
|
|
|
-const visible = ref(false)
|
|
|
-const loading = ref(false)
|
|
|
-const deptData = ref([])
|
|
|
-const roleData = ref([])
|
|
|
-const postData = ref([])
|
|
|
+const formRef = ref();
|
|
|
+const mode = ref("");
|
|
|
+const visible = ref(false);
|
|
|
+const loading = ref(false);
|
|
|
+const deptData = ref([]);
|
|
|
+const roleData = ref([]);
|
|
|
+const postData = ref([]);
|
|
|
+const passwordSafePercent = ref(0);
|
|
|
|
|
|
let title = computed(() => {
|
|
|
- return '用户管理' + (mode.value == 'add' ? '-新增' : '-编辑')
|
|
|
-})
|
|
|
+ return "用户管理" + (mode.value == "add" ? "-新增" : "-编辑");
|
|
|
+});
|
|
|
|
|
|
// 表单初始值
|
|
|
const initialFormData = {
|
|
|
- id: '',
|
|
|
- avatar: '',
|
|
|
- username: '',
|
|
|
- nickname: '',
|
|
|
- dept_id: '',
|
|
|
- password: '',
|
|
|
+ id: "",
|
|
|
+ avatar: "",
|
|
|
+ username: "",
|
|
|
+ nickname: "",
|
|
|
+ dept_id: "",
|
|
|
+ password: "",
|
|
|
role_ids: [],
|
|
|
- phone: '',
|
|
|
+ phone: "",
|
|
|
post_ids: [],
|
|
|
- email: '',
|
|
|
+ email: "",
|
|
|
status: 1,
|
|
|
- remark: '',
|
|
|
-}
|
|
|
+ remark: "",
|
|
|
+};
|
|
|
|
|
|
// 表单信息
|
|
|
-const formData = reactive({ ...initialFormData })
|
|
|
+const formData = reactive({ ...initialFormData });
|
|
|
+
|
|
|
+// zxcvbn 警告信息中文映射
|
|
|
+const warningTranslations = {
|
|
|
+ "Straight rows of keys are easy to guess": "键盘上连续的按键容易被猜到",
|
|
|
+ "Short keyboard patterns are easy to guess": "简短的键盘模式容易被猜到",
|
|
|
+ "Use a longer keyboard pattern with more turns": "使用更长且更复杂的键盘模式",
|
|
|
+ 'Repeats like "aaa" are easy to guess': '像 "aaa" 这样的重复字符容易被猜到',
|
|
|
+ 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"':
|
|
|
+ '像 "abcabcabc" 这样的重复模式只比 "abc" 稍难猜一点',
|
|
|
+ "Sequences like abc or 6543 are easy to guess":
|
|
|
+ "像 abc 或 6543 这样的序列容易被猜到",
|
|
|
+ "Recent years are easy to guess": "近期的年份容易被猜到",
|
|
|
+ "Dates are often easy to guess": "日期通常容易被猜到",
|
|
|
+ "This is a top-10 common password": "这是最常用的10个密码之一",
|
|
|
+ "This is a top-100 common password": "这是最常用的100个密码之一",
|
|
|
+ "This is a very common password": "这是一个非常常见的密码",
|
|
|
+ "This is similar to a commonly used password": "这与常用密码相似",
|
|
|
+ "A word by itself is easy to guess": "单个单词容易被猜到",
|
|
|
+ "Names and surnames by themselves are easy to guess": "单独的姓名容易被猜到",
|
|
|
+ "Common names and surnames are easy to guess": "常见的姓名容易被猜到",
|
|
|
+};
|
|
|
+
|
|
|
+// zxcvbn 建议信息中文映射
|
|
|
+const suggestionTranslations = {
|
|
|
+ "Use a few words, avoid common phrases": "使用几个单词,避免常见短语",
|
|
|
+ "No need for symbols, digits, or uppercase letters":
|
|
|
+ "不需要符号、数字或大写字母",
|
|
|
+ "Add another word or two. Uncommon words are better.":
|
|
|
+ "添加一两个单词,不常见的单词更好",
|
|
|
+ "Capitalization doesn't help very much": "大写字母帮助不大",
|
|
|
+ "All-uppercase is almost as easy to guess as all-lowercase":
|
|
|
+ "全大写几乎和全小写一样容易猜到",
|
|
|
+ "Reversed words aren't much harder to guess": "颠倒的单词不会更难猜",
|
|
|
+ "Predictable substitutions like '@' instead of 'a' don't help very much":
|
|
|
+ "像用 '@' 代替 'a' 这样的替换帮助不大",
|
|
|
+ "Use a longer keyboard pattern with more turns": "使用更长且更复杂的键盘模式",
|
|
|
+ "Avoid repeated words and characters": "避免重复的单词和字符",
|
|
|
+ "Avoid sequences": "避免使用序列",
|
|
|
+ "Avoid recent years": "避免使用近期年份",
|
|
|
+ "Avoid years that are associated with you": "避免使用与你相关的年份",
|
|
|
+ "Avoid dates and years that are associated with you":
|
|
|
+ "避免使用与你相关的日期和年份",
|
|
|
+};
|
|
|
+
|
|
|
+// 翻译函数
|
|
|
+const translateText = (text, translations) => {
|
|
|
+ return translations[text] || text;
|
|
|
+};
|
|
|
+
|
|
|
+// 密码强度验证器
|
|
|
+const validatePassword = (value, callback) => {
|
|
|
+ // 如果是编辑模式且密码为空,则不验证(表示不修改密码)
|
|
|
+ if (mode.value === "edit" && !value) {
|
|
|
+ callback();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是新增模式,密码必填
|
|
|
+ if (mode.value === "add" && !value) {
|
|
|
+ callback("密码不能为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有值,则进行强度检查
|
|
|
+ if (value) {
|
|
|
+ // 最小长度检查
|
|
|
+ if (value.length < 8) {
|
|
|
+ callback("密码长度至少为8位");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用 zxcvbn 评估密码强度
|
|
|
+ const result = zxcvbn(value);
|
|
|
+
|
|
|
+ // zxcvbn 返回分数 0-4,我们要求至少为 2
|
|
|
+ if (result.score < 2) {
|
|
|
+ // 翻译反馈信息
|
|
|
+ const warningText = result.feedback.warning || "密码过于简单,容易被破解";
|
|
|
+ const feedback = translateText(warningText, warningTranslations);
|
|
|
+
|
|
|
+ // 翻译建议信息
|
|
|
+ let suggestions = "建议使用字母、数字和特殊字符的组合,避免使用常见密码";
|
|
|
+ if (result.feedback.suggestions.length > 0) {
|
|
|
+ const translatedSuggestions = result.feedback.suggestions.map((s) =>
|
|
|
+ translateText(s, suggestionTranslations)
|
|
|
+ );
|
|
|
+ suggestions = "建议:" + translatedSuggestions.join(";");
|
|
|
+ }
|
|
|
+
|
|
|
+ callback(`${feedback}。${suggestions}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ callback();
|
|
|
+};
|
|
|
|
|
|
// 验证规则
|
|
|
const rules = {
|
|
|
- username: [{ required: true, message: '账户不能为空' }],
|
|
|
- dept_id: [{ required: true, message: '部门不能为空' }],
|
|
|
- role_ids: [{ required: true, message: '角色不能为空' }],
|
|
|
-}
|
|
|
+ username: [{ required: true, message: "账户不能为空" }],
|
|
|
+ dept_id: [{ required: true, message: "部门不能为空" }],
|
|
|
+ role_ids: [{ required: true, message: "角色不能为空" }],
|
|
|
+ password: [{ validator: validatePassword }],
|
|
|
+};
|
|
|
|
|
|
// 打开弹框
|
|
|
-const open = async (type = 'add', id = '') => {
|
|
|
- mode.value = type
|
|
|
+const open = async (type = "add", id = "") => {
|
|
|
+ mode.value = type;
|
|
|
// 重置表单数据
|
|
|
- Object.assign(formData, initialFormData)
|
|
|
- formRef.value.clearValidate()
|
|
|
- visible.value = true
|
|
|
- await initPage()
|
|
|
- if (type == 'edit') {
|
|
|
- const { data } = await api.read(id)
|
|
|
+ Object.assign(formData, initialFormData);
|
|
|
+ passwordSafePercent.value = 0;
|
|
|
+ formRef.value.clearValidate();
|
|
|
+ visible.value = true;
|
|
|
+ await initPage();
|
|
|
+ if (type == "edit") {
|
|
|
+ const { data } = await api.read(id);
|
|
|
if (data.postList) {
|
|
|
- const post = data.postList.map((item) => item.id)
|
|
|
- data.post_ids = post
|
|
|
+ const post = data.postList.map((item) => item.id);
|
|
|
+ data.post_ids = post;
|
|
|
}
|
|
|
- const role = data.roleList.map((item) => item.id)
|
|
|
- data.role_ids = role
|
|
|
- data.password = ''
|
|
|
- setFormData(data)
|
|
|
+ const role = data.roleList.map((item) => item.id);
|
|
|
+ data.role_ids = role;
|
|
|
+ data.password = "";
|
|
|
+ setFormData(data);
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
// 初始化页面数据
|
|
|
const initPage = async () => {
|
|
|
- const deptResp = await commonApi.commonGet('/core/dept/accessDept')
|
|
|
- deptData.value = deptResp.data
|
|
|
+ const deptResp = await commonApi.commonGet("/core/dept/accessDept");
|
|
|
+ deptData.value = deptResp.data;
|
|
|
|
|
|
- const roleResp = await commonApi.commonGet('/core/role/accessRole')
|
|
|
- roleData.value = roleResp.data
|
|
|
+ const roleResp = await commonApi.commonGet("/core/role/accessRole");
|
|
|
+ roleData.value = roleResp.data;
|
|
|
|
|
|
- const postResp = await commonApi.commonGet('/core/post/accessPost')
|
|
|
- postData.value = postResp.data
|
|
|
-}
|
|
|
+ const postResp = await commonApi.commonGet("/core/post/accessPost");
|
|
|
+ postData.value = postResp.data;
|
|
|
+};
|
|
|
|
|
|
// 设置数据
|
|
|
const setFormData = async (data) => {
|
|
|
for (const key in formData) {
|
|
|
if (data[key] != null && data[key] != undefined) {
|
|
|
- formData[key] = data[key]
|
|
|
+ formData[key] = data[key];
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
// 数据保存
|
|
|
const submit = async (done) => {
|
|
|
- const validate = await formRef.value?.validate()
|
|
|
+ const validate = await formRef.value?.validate();
|
|
|
if (!validate) {
|
|
|
- loading.value = true
|
|
|
- let data = { ...formData }
|
|
|
- let result = {}
|
|
|
- if (mode.value === 'add') {
|
|
|
+ loading.value = true;
|
|
|
+ let data = { ...formData };
|
|
|
+ let result = {};
|
|
|
+ if (mode.value === "add") {
|
|
|
// 添加数据
|
|
|
- data.id = undefined
|
|
|
- result = await api.save(data)
|
|
|
+ data.id = undefined;
|
|
|
+ result = await api.save(data);
|
|
|
} else {
|
|
|
// 修改数据
|
|
|
- result = await api.update(data.id, data)
|
|
|
+ result = await api.update(data.id, data);
|
|
|
}
|
|
|
if (result.code === 200) {
|
|
|
- Message.success('操作成功')
|
|
|
- emit('success')
|
|
|
- done(true)
|
|
|
+ Message.success("操作成功");
|
|
|
+ emit("success");
|
|
|
+ done(true);
|
|
|
}
|
|
|
// 防止连续点击提交
|
|
|
setTimeout(() => {
|
|
|
- loading.value = false
|
|
|
- }, 500)
|
|
|
+ loading.value = false;
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+ done(false);
|
|
|
+};
|
|
|
+
|
|
|
+// 检查密码安全强度
|
|
|
+const checkPasswordSafe = (password) => {
|
|
|
+ if (password.length < 1) {
|
|
|
+ passwordSafePercent.value = 0;
|
|
|
+ return;
|
|
|
}
|
|
|
- done(false)
|
|
|
-}
|
|
|
+
|
|
|
+ // 使用 zxcvbn 评估密码强度
|
|
|
+ const result = zxcvbn(password);
|
|
|
+
|
|
|
+ // 将 zxcvbn 的分数 (0-4) 转换为百分比
|
|
|
+ // 0: 0%
|
|
|
+ // 1: 0.25 (25%)
|
|
|
+ // 2: 0.5 (50%)
|
|
|
+ // 3: 0.75 (75%)
|
|
|
+ // 4: 1.0 (100%)
|
|
|
+ passwordSafePercent.value = result.score * 0.25;
|
|
|
+};
|
|
|
|
|
|
// 关闭弹窗
|
|
|
-const close = () => (visible.value = false)
|
|
|
+const close = () => {
|
|
|
+ visible.value = false;
|
|
|
+ passwordSafePercent.value = 0;
|
|
|
+};
|
|
|
|
|
|
-defineExpose({ open, setFormData })
|
|
|
+defineExpose({ open, setFormData });
|
|
|
</script>
|