فهرست منبع

1. 新增/编辑用户,密码强度检测
2. 权限开关添加备案号,CURD

ith5 3 ماه پیش
والد
کامیت
929ccfaf98
3فایلهای تغییر یافته به همراه358 افزوده شده و 168 حذف شده
  1. 232 77
      src/views/system/user/edit.vue
  2. 73 51
      src/views/v1/center/qxSwitch/edit.vue
  3. 53 40
      src/views/v1/center/qxSwitch/index.vue

+ 232 - 77
src/views/system/user/edit.vue

@@ -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>

+ 73 - 51
src/views/v1/center/qxSwitch/edit.vue

@@ -7,20 +7,41 @@
     :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-form-item label="包名" field="package_name">
         <a-input v-model="formData.package_name" placeholder="请输入包名" />
       </a-form-item>
+      <a-form-item label="备案" field="beian">
+        <a-input v-model="formData.beian" placeholder="请输入备案号" />
+      </a-form-item>
       <a-form-item label="咨询允许获得权限" field="qx">
-        <sa-switch v-model="formData.qx" checkedText="开启" uncheckedText="关闭" />
+        <sa-switch
+          v-model="formData.qx"
+          checkedText="开启"
+          uncheckedText="关闭"
+        />
       </a-form-item>
       <a-form-item label="隐私弹窗" field="ystc">
-        <sa-switch v-model="formData.ystc" checkedText="开启" uncheckedText="关闭" />
+        <sa-switch
+          v-model="formData.ystc"
+          checkedText="开启"
+          uncheckedText="关闭"
+        />
       </a-form-item>
       <a-form-item label="登录框同意勾选文案" field="init">
-        <sa-switch v-model="formData.init" checkedText="开启" uncheckedText="关闭" />
+        <sa-switch
+          v-model="formData.init"
+          checkedText="开启"
+          uncheckedText="关闭"
+        />
       </a-form-item>
     </a-form>
     <!-- 表单信息 end -->
@@ -28,94 +49,95 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue'
-import tool from '@/utils/tool'
-import { Message, Modal } from '@arco-design/web-vue'
-import api from '../../api/center/qxSwitch'
+import { ref, reactive, computed } from "vue";
+import tool from "@/utils/tool";
+import { Message, Modal } from "@arco-design/web-vue";
+import api from "../../api/center/qxSwitch";
 
-const emit = defineEmits(['success'])
+const emit = defineEmits(["success"]);
 // 引用定义
-const visible = ref(false)
-const loading = ref(false)
-const formRef = ref()
-const mode = ref('')
+const visible = ref(false);
+const loading = ref(false);
+const formRef = ref();
+const mode = ref("");
 
 let title = computed(() => {
-  return '权限开关' + (mode.value == 'add' ? '-新增' : '-编辑')
-})
+  return "权限开关" + (mode.value == "add" ? "-新增" : "-编辑");
+});
 
 // 表单初始值
 const initialFormData = {
   id: null,
-  package_name: '',
+  beian: "",
+  package_name: "",
   qx: 1,
   ystc: 1,
   init: 1,
-}
+};
 
 // 表单信息
-const formData = reactive({ ...initialFormData })
+const formData = reactive({ ...initialFormData });
 
 // 验证规则
 const rules = {
-  package_name: [{ required: true, message: '包名必需填写' }],
-  qx: [{ required: true, message: '咨询允许获得权限必需填写' }],
-  ystc: [{ required: true, message: '隐私弹窗必需填写' }],
-  init: [{ required: true, message: '登录框同意勾选文案必需填写' }],
-}
+  package_name: [{ required: true, message: "包名必需填写" }],
+  qx: [{ required: true, message: "咨询允许获得权限必需填写" }],
+  ystc: [{ required: true, message: "隐私弹窗必需填写" }],
+  init: [{ required: true, message: "登录框同意勾选文案必需填写" }],
+};
 
 // 打开弹框
-const open = async (type = 'add') => {
-  mode.value = type
+const open = async (type = "add") => {
+  mode.value = type;
   // 重置表单数据
-  Object.assign(formData, initialFormData)
-  formRef.value.clearValidate()
-  visible.value = true
-  await initPage()
-}
+  Object.assign(formData, initialFormData);
+  formRef.value.clearValidate();
+  visible.value = true;
+  await initPage();
+};
 
 // 初始化页面数据
-const initPage = async () => {}
+const initPage = async () => {};
 
 // 设置数据
 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)
-}
+  done(false);
+};
 
 // 关闭弹窗
-const close = () => (visible.value = false)
+const close = () => (visible.value = false);
 
-defineExpose({ open, setFormData })
+defineExpose({ open, setFormData });
 </script>

+ 53 - 40
src/views/v1/center/qxSwitch/index.vue

@@ -1,11 +1,20 @@
 <template>
   <div class="ma-content-block">
-    <sa-table ref="crudRef" :options="options" :columns="columns" :searchForm="searchForm">
+    <sa-table
+      ref="crudRef"
+      :options="options"
+      :columns="columns"
+      :searchForm="searchForm"
+    >
       <!-- 搜索区 tableSearch -->
       <template #tableSearch>
         <a-col :sm="6" :xs="24">
           <a-form-item label="包名" field="package_name">
-            <a-input v-model="searchForm.package_name" placeholder="请输入包名" allow-clear />
+            <a-input
+              v-model="searchForm.package_name"
+              placeholder="请输入包名"
+              allow-clear
+            />
           </a-form-item>
         </a-col>
       </template>
@@ -16,21 +25,24 @@
           checkedText="开启"
           uncheckedText="关闭"
           v-model="record.qx"
-          @change="changeStatus($event, record, 'qx')" />
+          @change="changeStatus($event, record, 'qx')"
+        />
       </template>
       <template #ystc="{ record }">
         <sa-switch
           checkedText="开启"
           uncheckedText="关闭"
           v-model="record.ystc"
-          @change="changeStatus($event, record, 'ystc')" />
+          @change="changeStatus($event, record, 'ystc')"
+        />
       </template>
       <template #init="{ record }">
         <sa-switch
           checkedText="开启"
           uncheckedText="关闭"
           v-model="record.init"
-          @change="changeStatus($event, record, 'init')" />
+          @change="changeStatus($event, record, 'init')"
+        />
       </template>
     </sa-table>
 
@@ -40,32 +52,32 @@
 </template>
 
 <script setup>
-import { onMounted, ref, reactive } from 'vue'
-import { Message } from '@arco-design/web-vue'
-import EditForm from './edit.vue'
-import api from '../../api/center/qxSwitch'
+import { onMounted, ref, reactive } from "vue";
+import { Message } from "@arco-design/web-vue";
+import EditForm from "./edit.vue";
+import api from "../../api/center/qxSwitch";
 
 // 引用定义
-const crudRef = ref()
-const editRef = ref()
-const viewRef = ref()
+const crudRef = ref();
+const editRef = ref();
+const viewRef = ref();
 
 // 搜索表单
 const searchForm = ref({
-  package_name: '',
-})
+  package_name: "",
+});
 
 // 修改状态
 const changeStatus = async (status, record, field) => {
   const response = await api.update(record.id, {
     ...record,
     [field]: status,
-  })
+  });
   if (response.code === 200) {
-    Message.success(response.message)
-    crudRef.value.refresh()
+    Message.success(response.message);
+    crudRef.value.refresh();
   }
-}
+};
 
 // SaTable 基础配置
 const options = reactive({
@@ -74,55 +86,56 @@ const options = reactive({
   rowSelection: { showCheckedAll: true },
   add: {
     show: true,
-    auth: ['/v1/center/QxSwitch/save'],
+    auth: ["/v1/center/QxSwitch/save"],
     func: async () => {
-      editRef.value?.open()
+      editRef.value?.open();
     },
   },
   edit: {
     show: true,
-    auth: ['/v1/center/QxSwitch/update'],
+    auth: ["/v1/center/QxSwitch/update"],
     func: async (record) => {
-      editRef.value?.open('edit')
-      editRef.value?.setFormData(record)
+      editRef.value?.open("edit");
+      editRef.value?.setFormData(record);
     },
   },
   delete: {
     show: true,
-    auth: ['/v1/center/QxSwitch/destroy'],
+    auth: ["/v1/center/QxSwitch/destroy"],
     func: async (params) => {
-      const resp = await api.destroy(params)
+      const resp = await api.destroy(params);
       if (resp.code === 200) {
-        Message.success(`删除成功!`)
-        crudRef.value?.refresh()
+        Message.success(`删除成功!`);
+        crudRef.value?.refresh();
       }
     },
   },
-})
+});
 
 // SaTable 列配置
 const columns = reactive([
-  { title: '包名', dataIndex: 'package_name', width: 180 },
-  { title: '咨询允许获得权限', dataIndex: 'qx', width: 180 },
-  { title: '隐私弹窗', dataIndex: 'ystc', width: 180 },
-  { title: '登录框同意勾选文案', dataIndex: 'init', width: 180 },
-])
+  { title: "包名", dataIndex: "package_name", width: 180 },
+  { title: "备案号", dataIndex: "beian", width: 180 },
+  { title: "咨询允许获得权限", dataIndex: "qx", width: 180 },
+  { title: "隐私弹窗", dataIndex: "ystc", width: 180 },
+  { title: "登录框同意勾选文案", dataIndex: "init", width: 180 },
+]);
 
 // 页面数据初始化
-const initPage = async () => {}
+const initPage = async () => {};
 
 // SaTable 数据请求
 const refresh = async () => {
-  crudRef.value?.refresh()
-}
+  crudRef.value?.refresh();
+};
 
 // 页面加载完成执行
 onMounted(async () => {
-  initPage()
-  refresh()
-})
+  initPage();
+  refresh();
+});
 </script>
 
 <script>
-export default { name: 'v1/center/qxSwitch' }
+export default { name: "v1/center/qxSwitch" };
 </script>