Kaynağa Gözat

优化白名单管理页面

yawuga 3 hafta önce
ebeveyn
işleme
402fd4da74

+ 351 - 99
src/views/base/whitelist/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="90px">
       <el-form-item label="姓名" prop="name">
         <el-input
           v-model="queryParams.name"
@@ -25,50 +25,40 @@
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="人脸照片地址,用于机器人侧照片比对" prop="faceImageUrl">
-        <el-input
-          v-model="queryParams.faceImageUrl"
-          placeholder="请输入人脸照片地址,用于机器人侧照片比对"
-          clearable
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="有效开始时间,不填表示立即生效" prop="validStartTime">
-        <el-date-picker clearable
-          v-model="queryParams.validStartTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择有效开始时间,不填表示立即生效">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="有效结束时间,不填表示长期有效" prop="validEndTime">
-        <el-date-picker clearable
-          v-model="queryParams.validEndTime"
-          type="date"
-          value-format="YYYY-MM-DD"
-          placeholder="请选择有效结束时间,不填表示长期有效">
-        </el-date-picker>
-      </el-form-item>
       <el-form-item label="人员类型" prop="whitelistType">
-        <el-select v-model="queryParams.whitelistType" placeholder="请选择人员类型" clearable>
+        <el-select v-model="queryParams.whitelistType" placeholder="请选择人员类型" clearable style="width: 180px">
           <el-option
             v-for="dict in whitelist__type"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
+            :value="String(dict.value)"
           />
         </el-select>
       </el-form-item>
       <el-form-item label="来源类型" prop="sourceType">
-        <el-select v-model="queryParams.sourceType" placeholder="请选择来源类型" clearable>
+        <el-select v-model="queryParams.sourceType" placeholder="请选择来源类型" clearable style="width: 180px">
           <el-option
             v-for="dict in source_type"
             :key="dict.value"
             :label="dict.label"
-            :value="dict.value"
+            :value="String(dict.value)"
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="启用状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable style="width: 180px">
+          <el-option label="启用" value="1" />
+          <el-option label="停用" value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="有效期" prop="validStatus">
+        <el-select v-model="queryParams.validStatus" placeholder="请选择有效期状态" clearable style="width: 180px">
+          <el-option label="有效" value="valid" />
+          <el-option label="未生效" value="not_started" />
+          <el-option label="已过期" value="expired" />
+          <el-option label="长期有效" value="long_term" />
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
         <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -105,6 +95,15 @@
           v-hasPermi="['base:whitelist:remove']"
         >删除</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="Upload"
+          @click="handleImport"
+          v-hasPermi="['base:whitelist:import']"
+        >导入</el-button>
+      </el-col>
       <el-col :span="1.5">
         <el-button
           type="warning"
@@ -119,35 +118,68 @@
 
     <el-table v-loading="loading" :data="whitelistList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="主键ID" align="center" prop="id" />
-      <el-table-column label="姓名" align="center" prop="name" />
-      <el-table-column label="手机号" align="center" prop="mobile" />
-      <el-table-column label="身份证号" align="center" prop="idCardNo" />
-      <el-table-column label="人脸照片地址,用于机器人侧照片比对" align="center" prop="faceImageUrl" />
-      <el-table-column label="有效开始时间,不填表示立即生效" align="center" prop="validStartTime" width="180">
+      <el-table-column label="姓名" align="center" prop="name" min-width="120" show-overflow-tooltip />
+      <el-table-column label="手机号" align="center" prop="mobile" width="140" />
+      <el-table-column label="身份证号" align="center" prop="idCardNo" min-width="180" show-overflow-tooltip />
+      <el-table-column label="人员类型" align="center" prop="whitelistType" width="120">
         <template #default="scope">
-          <span>{{ parseTime(scope.row.validStartTime, '{y}-{m}-{d}') }}</span>
+          <dict-tag :options="whitelist__type" :value="String(scope.row.whitelistType)"/>
         </template>
       </el-table-column>
-      <el-table-column label="有效结束时间,不填表示长期有效" align="center" prop="validEndTime" width="180">
+      <el-table-column label="人脸照片" align="center" prop="faceImageUrl" width="100">
         <template #default="scope">
-          <span>{{ parseTime(scope.row.validEndTime, '{y}-{m}-{d}') }}</span>
+          <image-preview
+            v-if="scope.row.faceImageUrl"
+            :src="scope.row.faceImageUrl"
+            :width="44"
+            :height="44"
+          />
+          <el-tag v-else type="info">未上传</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="启用状态:0停用,1启用" align="center" prop="status" />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <el-table-column label="人员类型" align="center" prop="whitelistType">
+      <el-table-column label="来源类型" align="center" prop="sourceType" width="120">
         <template #default="scope">
-          <dict-tag :options="whitelist__type" :value="scope.row.whitelistType"/>
+          <dict-tag :options="source_type" :value="String(scope.row.sourceType)"/>
         </template>
       </el-table-column>
-      <el-table-column label="来源类型" align="center" prop="sourceType">
+      <el-table-column label="有效期" align="center" min-width="240" show-overflow-tooltip>
         <template #default="scope">
-          <dict-tag :options="source_type" :value="scope.row.sourceType"/>
+          <div class="valid-time-cell">
+            <span>{{ formatValidTime(scope.row.validStartTime, scope.row.validEndTime) }}</span>
+            <el-tag
+              size="small"
+              :type="getValidStatusTagType(scope.row.validStartTime, scope.row.validEndTime)"
+            >
+              {{ getValidStatusText(scope.row.validStartTime, scope.row.validEndTime) }}
+            </el-tag>
+          </div>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="启用状态" align="center" prop="status" width="100">
         <template #default="scope">
+          <el-tag v-if="String(scope.row.status) === '1'" type="success">启用</el-tag>
+          <el-tag v-else type="info">停用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
+      <el-table-column label="操作" align="center" width="240" fixed="right" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-if="String(scope.row.status) !== '1'"
+            link
+            type="primary"
+            icon="CircleCheck"
+            @click="handleStatusChange(scope.row, '1')"
+            v-hasPermi="['base:whitelist:edit']"
+          >启用</el-button>
+          <el-button
+            v-if="String(scope.row.status) === '1'"
+            link
+            type="warning"
+            icon="CircleClose"
+            @click="handleStatusChange(scope.row, '0')"
+            v-hasPermi="['base:whitelist:edit']"
+          >停用</el-button>
           <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['base:whitelist:edit']">修改</el-button>
           <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['base:whitelist:remove']">删除</el-button>
         </template>
@@ -162,77 +194,105 @@
       @pagination="getList"
     />
 
-    <!-- 添加或修改访客白名单对话框 -->
-    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="whitelistRef" :model="form" :rules="rules" label-width="100px">
+    <!-- 添加或修改白名单对话框 -->
+    <el-dialog :title="title" v-model="open" width="760px" append-to-body>
+      <el-form ref="whitelistRef" :model="form" :rules="rules" label-width="120px">
         <el-row>
           <el-col :span="24">
             <el-form-item label="姓名" prop="name">
-              <el-input v-model="form.name" placeholder="请输入姓名" />
+              <el-input v-model="form.name" maxlength="100" show-word-limit placeholder="请输入姓名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="人员类型" prop="whitelistType">
+              <el-select v-model="form.whitelistType" placeholder="请选择人员类型" style="width: 100%">
+                <el-option
+                  v-for="dict in whitelist__type"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="String(dict.value)"
+                />
+              </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="手机号" prop="mobile">
-              <el-input v-model="form.mobile" placeholder="请输入手机号" />
+              <el-input
+                v-model="form.mobile"
+                maxlength="11"
+                placeholder="请输入手机号"
+                @input="handleMobileInput"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="身份证号" prop="idCardNo">
-              <el-input v-model="form.idCardNo" placeholder="请输入身份证号" />
+              <el-input
+                v-model="form.idCardNo"
+                maxlength="18"
+                placeholder="请输入身份证号"
+                @input="handleIdCardInput"
+              />
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="人脸照片地址,用于机器人侧照片比对" prop="faceImageUrl">
-              <el-input v-model="form.faceImageUrl" placeholder="请输入人脸照片地址,用于机器人侧照片比对" />
+            <el-form-item label="人脸照片" prop="faceImageUrl">
+              <image-upload
+                v-model="form.faceImageUrl"
+                :limit="1"
+                @update:modelValue="handleIdentityInfoChange"
+              />
+              <div class="form-tip">用于机器人侧照片比对。建议上传清晰正脸照片。</div>
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="有效开始时间,不填表示立即生效" prop="validStartTime">
+            <el-form-item label="来源类型" prop="sourceType">
+              <el-tag type="info">{{ formatSourceType(form.sourceType) }}</el-tag>
+              <div class="form-tip">来源类型由系统自动识别,运维后台新增和导入的数据默认为本地。</div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="有效开始时间" prop="validStartTime">
               <el-date-picker clearable
                 v-model="form.validStartTime"
                 type="date"
                 value-format="YYYY-MM-DD"
-                placeholder="请选择有效开始时间,不填表示立即生效">
+                style="width: 100%"
+                placeholder="请选择有效开始时间"
+                @change="handleValidTimeChange">
               </el-date-picker>
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="有效结束时间,不填表示长期有效" prop="validEndTime">
+            <el-form-item label="有效结束时间" prop="validEndTime">
               <el-date-picker clearable
                 v-model="form.validEndTime"
                 type="date"
                 value-format="YYYY-MM-DD"
-                placeholder="请选择有效结束时间,不填表示长期有效">
+                style="width: 100%"
+                placeholder="请选择有效结束时间"
+                @change="handleValidTimeChange">
               </el-date-picker>
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="备注" prop="remark">
-              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="人员类型" prop="whitelistType">
-              <el-select v-model="form.whitelistType" placeholder="请选择人员类型">
-                <el-option
-                  v-for="dict in whitelist__type"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="parseInt(dict.value)"
-                ></el-option>
-              </el-select>
+            <el-form-item label="启用状态" prop="status">
+              <el-radio-group v-model="form.status">
+                <el-radio label="1">启用</el-radio>
+                <el-radio label="0">停用</el-radio>
+              </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="来源类型" prop="sourceType">
-              <el-select v-model="form.sourceType" placeholder="请选择来源类型">
-                <el-option
-                  v-for="dict in source_type"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="parseInt(dict.value)"
-                ></el-option>
-              </el-select>
+            <el-form-item label="备注" prop="remark">
+              <el-input
+                v-model="form.remark"
+                type="textarea"
+                :rows="3"
+                maxlength="500"
+                show-word-limit
+                placeholder="请输入备注"
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -263,6 +323,129 @@ const multiple = ref(true)
 const total = ref(0)
 const title = ref("")
 
+/** 校验手机号、身份证号、人脸照片三者至少填写一种 */
+function validateIdentityInfo(rule, value, callback) {
+  if (form.value.mobile || form.value.idCardNo || form.value.faceImageUrl) {
+    callback()
+  } else {
+    callback(new Error("手机号、身份证号、人脸照片至少填写一种"))
+  }
+}
+
+/** 校验有效结束时间必须大于有效开始时间 */
+function validateValidTime(rule, value, callback) {
+  if (form.value.validStartTime && form.value.validEndTime && form.value.validEndTime <= form.value.validStartTime) {
+    callback(new Error("有效结束时间必须大于有效开始时间"))
+  } else {
+    callback()
+  }
+}
+
+/** 格式化有效期显示 */
+function formatValidTime(startTime, endTime) {
+  const start = startTime ? proxy.parseTime(startTime, "{y}-{m}-{d}") : "立即生效"
+  const end = endTime ? proxy.parseTime(endTime, "{y}-{m}-{d}") : "长期有效"
+  return start + " 至 " + end
+}
+
+/** 获取有效期状态 */
+function getValidStatus(startTime, endTime) {
+  const now = new Date()
+  const start = startTime ? new Date(startTime) : null
+  const end = endTime ? new Date(endTime) : null
+  if (!start && !end) return "long_term"
+  if (start && now < start) return "not_started"
+  if (end && now > end) return "expired"
+  return "valid"
+}
+
+/** 有效期状态文本 */
+function getValidStatusText(startTime, endTime) {
+  const status = getValidStatus(startTime, endTime)
+  const map = {
+    valid: "有效",
+    not_started: "未生效",
+    expired: "已过期",
+    long_term: "长期有效"
+  }
+  return map[status] || "-"
+}
+
+/** 有效期状态标签类型 */
+function getValidStatusTagType(startTime, endTime) {
+  const status = getValidStatus(startTime, endTime)
+  const map = {
+    valid: "success",
+    not_started: "warning",
+    expired: "danger",
+    long_term: "info"
+  }
+  return map[status] || "info"
+}
+
+/** 身份识别信息变化时,主动触发三选一校验 */
+function handleIdentityInfoChange() {
+  nextTick(() => {
+    proxy.$refs["whitelistRef"]?.validateField(["mobile", "idCardNo", "faceImageUrl"])
+  })
+}
+
+/** 格式化来源类型显示 */
+function formatSourceType(value) {
+  const target = source_type.value.find(item => String(item.value) === String(value))
+  return target ? target.label : "-"
+}
+
+/** 手机号输入处理 */
+function handleMobileInput(value) {
+  form.value.mobile = String(value || "").replace(/\D/g, "").slice(0, 11)
+  handleIdentityInfoChange()
+}
+
+/** 身份证号输入处理 */
+function handleIdCardInput(value) {
+  form.value.idCardNo = String(value || "")
+    .toUpperCase()
+    .replace(/[^0-9X]/g, "")
+    .slice(0, 18)
+  handleIdentityInfoChange()
+}
+
+/** 手机号格式校验 */
+function validateMobile(rule, value, callback) {
+  if (!value) {
+    callback()
+    return
+  }
+  const reg = /^1[3-9]\d{9}$/
+  if (!reg.test(value)) {
+    callback(new Error("请输入正确的手机号"))
+    return
+  }
+  callback()
+}
+
+/** 身份证号格式校验 */
+function validateIdCardNo(rule, value, callback) {
+  if (!value) {
+    callback()
+    return
+  }
+  const reg = /^\d{17}[\dX]$/
+  if (!reg.test(value)) {
+    callback(new Error("请输入正确的18位身份证号"))
+    return
+  }
+  callback()
+}
+
+/** 有效期变化时校验结束时间 */
+function handleValidTimeChange() {
+  nextTick(() => {
+    proxy.$refs["whitelistRef"]?.validateField("validEndTime")
+  })
+}
+
 const data = reactive({
   form: {},
   queryParams: {
@@ -271,25 +454,37 @@ const data = reactive({
     name: undefined,
     mobile: undefined,
     idCardNo: undefined,
-    faceImageUrl: undefined,
-    validStartTime: undefined,
-    validEndTime: undefined,
-    status: undefined,
     whitelistType: undefined,
-    sourceType: undefined
+    sourceType: undefined,
+    status: undefined,
+    validStatus: undefined
   },
   rules: {
     name: [
       { required: true, message: "姓名不能为空", trigger: "blur" }
     ],
-    status: [
-      { required: true, message: "启用状态:0停用,1启用不能为空", trigger: "change" }
-    ],
     whitelistType: [
-      { required: true, message: "人员类型不能为空", trigger: "change" }
+      { required: true, message: "请选择人员类型", trigger: "change" }
+    ],
+    mobile: [
+      { validator: validateIdentityInfo, trigger: "blur" },
+      { validator: validateMobile, trigger: "blur" }
+    ],
+    idCardNo: [
+      { validator: validateIdentityInfo, trigger: "blur" },
+      { validator: validateIdCardNo, trigger: "blur" }
+    ],
+    faceImageUrl: [
+      { validator: validateIdentityInfo, trigger: "change" }
     ],
     sourceType: [
       { required: true, message: "来源类型不能为空", trigger: "change" }
+    ],
+    validEndTime: [
+      { validator: validateValidTime, trigger: "change" }
+    ],
+    status: [
+      { required: true, message: "请选择启用状态", trigger: "change" }
     ]
   }
 })
@@ -320,14 +515,14 @@ function reset() {
     mobile: null,
     idCardNo: null,
     faceImageUrl: null,
+    whitelistType: null,
+    sourceType: "1",
     validStartTime: null,
     validEndTime: null,
-    status: null,
+    status: "1",
     remark: null,
     createTime: null,
-    updateTime: null,
-    whitelistType: null,
-    sourceType: null
+    updateTime: null
   }
   proxy.resetForm("whitelistRef")
 }
@@ -355,24 +550,60 @@ function handleSelectionChange(selection) {
 function handleAdd() {
   reset()
   open.value = true
-  title.value = "添加访客白名单"
+  title.value = "添加白名单"
 }
 
 /** 修改按钮操作 */
 function handleUpdate(row) {
   reset()
-  const _id = row.id || ids.value
+  const _id = row.id || ids.value[0]
   getWhitelist(_id).then(response => {
-    form.value = response.data
+    const data = response.data || {}
+    form.value = {
+      ...data,
+      idCardNo: data.idCardNo || data.id_card_no || null,
+      faceImageUrl: data.faceImageUrl || data.face_image_url || null,
+      validStartTime: data.validStartTime || data.valid_start_time || null,
+      validEndTime: data.validEndTime || data.valid_end_time || null,
+      status: data.status != null ? String(data.status) : "1",
+      whitelistType: data.whitelistType != null ? String(data.whitelistType) : null,
+      sourceType: data.sourceType != null ? String(data.sourceType) : "1"
+    }
     open.value = true
-    title.value = "修改访客白名单"
+    title.value = "修改白名单"
   })
 }
 
+/** 启用/停用白名单 */
+function handleStatusChange(row, status) {
+  const actionText = status === "1" ? "启用" : "停用"
+  proxy.$modal.confirm('确认' + actionText + '白名单人员"' + row.name + '"吗?').then(() => {
+    return updateWhitelist({
+      ...row,
+      status
+    })
+  }).then(() => {
+    proxy.$modal.msgSuccess(actionText + "成功")
+    getList()
+  }).catch(() => {})
+}
+
+/** 导入按钮操作 */
+function handleImport() {
+  proxy.$modal.msgWarning("白名单导入功能待后端接口完成后接入")
+}
+
 /** 提交按钮 */
 function submitForm() {
   proxy.$refs["whitelistRef"].validate(valid => {
     if (valid) {
+      if (!form.value.mobile && !form.value.idCardNo && !form.value.faceImageUrl) {
+        proxy.$modal.msgWarning("手机号、身份证号、人脸照片至少填写一种")
+        return
+      }
+      if (!form.value.sourceType) {
+        form.value.sourceType = "1"
+      }
       if (form.value.id != null) {
         updateWhitelist(form.value).then(() => {
           proxy.$modal.msgSuccess("修改成功")
@@ -393,7 +624,7 @@ function submitForm() {
 /** 删除按钮操作 */
 function handleDelete(row) {
   const _ids = row.id || ids.value
-  proxy.$modal.confirm('是否确认删除访客白名单编号为"' + _ids + '"的数据项?').then(function() {
+  proxy.$modal.confirm('确认删除选中的白名单数据吗?删除后不可恢复。').then(function() {
     return delWhitelist(_ids)
   }).then(() => {
     getList()
@@ -405,8 +636,29 @@ function handleDelete(row) {
 function handleExport() {
   proxy.download('base/whitelist/export', {
     ...queryParams.value
-  }, `whitelist_${new Date().getTime()}.xlsx`)
+  }, `白名单_${new Date().getTime()}.xlsx`)
 }
 
 getList()
 </script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+:deep(.el-table .cell) {
+  line-height: 22px;
+}
+.form-tip {
+  margin-top: 6px;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #909399;
+}
+.valid-time-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+</style>

+ 40 - 14
迎宾巡逻安防机器人运维端Web管理系统详细设计开发文档_V2.1.html

@@ -79,7 +79,7 @@
     <table><thead><tr><th>一级菜单</th><th>二级菜单</th><th>页面职责</th><th>一期优先级</th></tr></thead><tbody>
       <tr><td>首页</td><td>首页总览</td><td>展示机器人实时状态、摘要统计、异常告警与快捷操作入口。</td><td>P0</td></tr>
       <tr><td rowspan="7">内容管理</td><td>欢迎语配置</td><td>维护机器人默认欢迎语和触发控制参数。</td><td>P0</td></tr><tr><td>问答库管理</td><td>维护 FAQ 问答数据,支持字典分类、相似问、导入导出;问答分类使用 RuoYi 字典,不单独建设问答分类管理菜单。</td><td>P0</td></tr><tr><td>素材管理</td><td>维护图片、视频素材。</td><td>P0</td></tr><tr><td>播放方案管理</td><td>维护素材播放编排关系、时长、顺序、默认方案。</td><td>P0</td></tr><tr><td>播报内容管理</td><td>维护可被播报任务引用的播报文本模板。</td><td>P0</td></tr><tr><td>播报任务管理</td><td>维护播报时间策略、频率、启停状态。</td><td>P0</td></tr><tr><td>展示主题配置</td><td>维护机器人对外展示界面的品牌与主题风格。</td><td>P1</td></tr>
-      <tr><td rowspan="3">访客管理</td><td>访客记录</td><td>查看访客登记记录,支持查询、详情、导出。</td><td>P1</td></tr><tr><td>预约记录</td><td>查看主控平台同步的预约记录。</td><td>P1</td></tr><tr><td>白名单管理</td><td>维护白名单查看与本地管理能力。</td><td>P1</td></tr>
+      <tr><td rowspan="3">访客管理</td><td>访客记录</td><td>查看访客登记记录,支持查询、详情、导出。</td><td>P1</td></tr><tr><td>预约记录</td><td>查看主控平台同步的预约记录。</td><td>P1</td></tr><tr><td>白名单管理</td><td>维护人员白名单数据,支持通过人脸照片、身份证号、手机号进行白名单匹配。</td><td>P1</td></tr>
       <tr><td rowspan="4">监控管理</td><td>视频预览</td><td>查看机器人摄像头实时画面。</td><td>P1</td></tr><tr><td>远程喊话</td><td>下发喊话内容、查看执行结果。</td><td>P1</td></tr><tr><td>对话日志</td><td>查看人机交互日志。</td><td>P1</td></tr><tr><td>安防告警日志</td><td>查看机器人侧安防告警记录。</td><td>P1</td></tr>
       <tr><td rowspan="6">运维管理</td><td>设备状态</td><td>查看详细设备状态、资源占用、模块状态。</td><td>P0</td></tr><tr><td>设备控制</td><td>提供一键充电、停止充电、重启、关机、服务重启等操作。</td><td>P0</td></tr><tr><td>运行参数配置</td><td>动态读取参数分组与字段,支持编辑与保存。</td><td>P0</td></tr><tr><td>系统诊断</td><td>查看诊断检查结果、自检结果与关键资源状态。</td><td>P1</td></tr><tr><td>日志中心</td><td>统一查看系统、设备、升级、操作等日志。</td><td>P0</td></tr><tr><td>软件版本 / OTA 升级</td><td>查看版本、上传安装包、执行升级、查看升级记录。</td><td>P0</td></tr>
       <tr><td rowspan="3">系统设置</td><td>账号管理</td><td>维护后台账号、角色、状态。</td><td>P1</td></tr><tr><td>修改密码</td><td>当前登录账号修改密码。</td><td>P0</td></tr><tr><td>基础设置</td><td>维护系统名称、Logo、页脚信息等后台基础配置。</td><td>P2</td></tr>
@@ -108,7 +108,7 @@
         <tr><td>展示主题配置</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>基础 CRUD 可生成;Logo/背景上传、颜色选择器、主题预览、设为启用需要定制。</td></tr>
         <tr><td rowspan="3">访客管理</td><td>访客记录</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型查询列表和详情页面,可基于 robot_ops_visitor_record 生成,导出和照片预览需轻微调整。</td></tr>
         <tr><td>预约记录</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型查询列表和详情页面,可基于 robot_ops_appointment_record 生成,数据来源为主控平台同步。</td></tr>
-        <tr><td>白名单管理</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型 CRUD 页面,可基于 robot_ops_whitelist 生成,再补充导入导出和启用/停用快捷操作。</td></tr>
+        <tr><td>白名单管理</td><td>RuoYi 生成后定制</td><td>部分适合</td><td>可基于 robot_ops_whitelist 生成基础 CRUD;需补充身份证号、人脸照片上传/预览、有效期、人员类型、启用/停用、导入导出,以及手机号/身份证号/人脸照片至少填写一种的表单校验。</td></tr>
         <tr><td rowspan="4">监控管理</td><td>视频预览</td><td>定制开发</td><td>否</td><td>实时视频播放、重连、全屏、状态提示依赖视频流接口和播放器组件,需要定制开发。</td></tr>
         <tr><td>远程喊话</td><td>半定制开发</td><td>部分适合</td><td>喊话记录列表可基于 robot_ops_shout_record 生成;喊话输入、预置短语、立即喊话操作需要定制。</td></tr>
         <tr><td>对话日志</td><td>RuoYi 生成后微调</td><td>适合</td><td>典型日志查询和详情页面,可基于 robot_ops_dialogue_log 生成,详情页需展示原始请求和原始响应。</td></tr>
@@ -147,7 +147,19 @@
     <h3>6.4 访客管理</h3>
     <h4>6.4.1 访客记录页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>访客姓名、手机号、证件号、登记方式、登记结果、来访时间范围。</td></tr><tr><td>列表字段</td><td>访客姓名、手机号、证件号、登记方式、被访对象、来访时间、登记结果、来源、操作。</td></tr><tr><td>详情字段</td><td>访客姓名(visitorName)、手机号(mobile)、证件号(idCardNo)、登记方式(registerType)、访客照片(visitorPhoto)、预约单号(appointmentNo)、被访对象(visitedPerson)、来访时间(visitTime)、登记结果(resultStatus)、来源类型(sourceType)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>查看详情、导出。</td></tr><tr><td>导出字段</td><td>导出列表全部字段,不导出照片文件,仅导出照片链接。</td></tr></tbody></table>
     <h4>6.4.2 预约记录页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>预约单号、访客姓名、手机号、预约状态、预约时间范围。</td></tr><tr><td>列表字段</td><td>预约单号、访客姓名、手机号、被访人、预约时间、状态、同步时间、操作。</td></tr><tr><td>详情字段</td><td>预约单号(appointmentNo)、访客姓名(visitorName)、手机号(mobile)、被访人(visitedPerson)、预约时间(appointmentTime)、预约状态(status)、同步时间(syncTime)、来源平台(sourcePlatform)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>查看详情。</td></tr><tr><td>数据来源</td><td>主控平台同步;本地端仅展示,不发起预约流程。</td></tr></tbody></table>
-    <h4>6.4.3 白名单管理页面</h4><table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody><tr><td>查询条件</td><td>姓名、手机号、白名单状态、来源类型。</td></tr><tr><td>列表字段</td><td>姓名、手机号、白名单类型、状态、来源、本地/平台同步标识、更新时间、操作。</td></tr><tr><td>编辑字段</td><td>姓名(name)、手机号(mobile)、白名单类型(whitelistType)、来源类型(sourceType)、启用状态(status)、备注(remark)。</td></tr><tr><td>操作按钮</td><td>新增、编辑、删除、导入、导出、启用/停用。</td></tr><tr><td>业务规则</td><td>一期即按可维护设计,不再仅停留在“预留按钮”。如后续对接平台同步,再增加只读同步标识。</td></tr></tbody></table>
+    <h4>6.4.3 白名单管理页面</h4>
+<table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody>
+<tr><td>页面目标</td><td>维护可被机器人识别为可信人员的白名单数据,支持通过人脸照片、身份证号、手机号等信息进行白名单匹配。</td></tr>
+<tr><td>查询条件</td><td>姓名、手机号、身份证号、人员类型、来源类型、启用状态、有效期状态。</td></tr>
+<tr><td>列表字段</td><td>姓名、手机号、身份证号、人员类型、是否有人脸照片、来源类型、有效期、启用状态、更新时间、操作。</td></tr>
+<tr><td>编辑字段</td><td>姓名(name)、人员类型(whitelistType,页面显示为"人员类型")、手机号(mobile)、身份证号(idCardNo)、人脸照片(faceImageUrl)、来源类型(sourceType)、有效开始时间(validStartTime)、有效结束时间(validEndTime)、启用状态(status)、备注(remark)。</td></tr>
+<tr><td>人员类型</td><td>人员类型用于描述白名单人员身份,建议字典项为:内部人员、访客、VIP、其他。不建议将"人脸白名单"作为人员类型,因为人脸识别属于匹配方式,不属于人员身份类型。</td></tr>
+<tr><td>操作按钮</td><td>新增、编辑、删除、导入、导出、启用/停用。</td></tr>
+<tr><td>表单校验</td><td>姓名、人员类型、来源类型、启用状态必填;手机号、身份证号、人脸照片三者至少填写一种;有效结束时间如填写,必须大于有效开始时间。</td></tr>
+<tr><td>人脸照片规则</td><td>一期仅保存人脸照片地址(faceImageUrl),不保存人脸特征ID。机器人侧按照片进行人脸比对,后续如算法侧升级为特征库比对,再扩展相关字段。</td></tr>
+<tr><td>匹配规则</td><td>白名单不设置单一识别方式字段。机器人或后端根据当前采集到的身份信息进行匹配:人脸识别时通过人脸照片比对;身份证读取或输入时匹配身份证号;手机号输入时匹配手机号。任一方式匹配到启用且在有效期内的白名单人员,即视为白名单命中。</td></tr>
+<tr><td>业务规则</td><td>本地录入、平台同步、机器人采集的数据统一进入白名单管理。后续如对接平台同步,可对平台同步数据增加只读限制或同步标识。</td></tr>
+</tbody></table>
 
     <h3>6.5 监控管理</h3>
     <h4>6.5.1 视频预览页面</h4><table><thead><tr><th>区域</th><th>详细设计</th></tr></thead><tbody><tr><td>播放器区域</td><td>展示实时视频画面,支持播放、暂停、刷新、全屏。</td></tr><tr><td>状态区域</td><td>展示视频状态:未连接/连接中/播放中/失败。</td></tr><tr><td>辅助信息</td><td>显示码流类型、分辨率、最近更新时间。</td></tr><tr><td>操作按钮</td><td>刷新、全屏、重新连接。</td></tr><tr><td>异常提示</td><td>播放失败时展示错误码和建议操作:刷新、检查摄像头服务、检查网络。视频流地址、播放状态等由机器人侧实时接口返回,运维端一期不单独建设视频配置表。</td></tr></tbody></table>
@@ -375,15 +387,18 @@
       <tr><td>/robot-ops/visitor/record/export</td><td>GET</td><td>导出访客记录</td><td>同分页查询条件</td><td>Excel文件</td><td>robot_ops_visitor_record</td></tr>
       <tr><td>/robot-ops/visitor/appointment/page</td><td>GET</td><td>预约记录分页</td><td>appointmentNo、visitorName、mobile、status、appointmentTimeStart、appointmentTimeEnd、pageNum、pageSize</td><td>appointmentNo、visitorName、mobile、visitedPerson、appointmentTime、status、syncTime</td><td>robot_ops_appointment_record</td></tr>
       <tr><td>/robot-ops/visitor/appointment/{id}</td><td>GET</td><td>预约详情</td><td>预约记录ID(id)</td><td>appointmentNo、visitorName、mobile、visitedPerson、appointmentTime、status、syncTime、sourcePlatform、remark</td><td>robot_ops_appointment_record</td></tr>
-      <tr><td>/robot-ops/visitor/whitelist/page</td><td>GET</td><td>白名单分页</td><td>name、mobile、status、sourceType、pageNum、pageSize</td><td>id、name、mobile、whitelistType、sourceType、status、updateTime</td><td>robot_ops_whitelist</td></tr>
-      <tr><td>/robot-ops/visitor/whitelist/{id}</td><td>GET</td><td>白名单详情</td><td>白名单ID(id)</td><td>name、mobile、whitelistType、sourceType、status、remark</td><td>robot_ops_whitelist</td></tr>
-      <tr><td>/robot-ops/visitor/whitelist</td><td>POST</td><td>新增白名单</td><td>name、mobile、whitelistType、sourceType、status、remark</td><td>新增ID(id)</td><td>robot_ops_whitelist</td></tr>
-      <tr><td>/robot-ops/visitor/whitelist</td><td>PUT</td><td>编辑白名单</td><td>id、name、mobile、whitelistType、sourceType、status、remark</td><td>无</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/page</td><td>GET</td><td>白名单分页</td><td>姓名(name)、手机号(mobile)、身份证号(idCardNo)、人员类型(whitelistType)、来源类型(sourceType)、启用状态(status)、有效期状态(validStatus,可选)、pageNum、pageSize</td><td>白名单ID(id)、姓名(name)、手机号(mobile)、身份证号(idCardNo)、人员类型(whitelistType)、人脸照片地址(faceImageUrl)、是否有人脸照片(hasFaceImage)、来源类型(sourceType)、有效开始时间(validStartTime)、有效结束时间(validEndTime)、启用状态(status)、更新时间(updateTime)</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/{id}</td><td>GET</td><td>白名单详情</td><td>白名单ID(id)</td><td>name、mobile、idCardNo、whitelistType、faceImageUrl、sourceType、validStartTime、validEndTime、status、remark</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist</td><td>POST</td><td>新增白名单</td><td>name、mobile、idCardNo、whitelistType、faceImageUrl、sourceType、validStartTime、validEndTime、status、remark</td><td>新增ID(id)</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist</td><td>PUT</td><td>编辑白名单</td><td>id、name、mobile、idCardNo、whitelistType、faceImageUrl、sourceType、validStartTime、validEndTime、status、remark</td><td>无</td><td>robot_ops_whitelist</td></tr>
 <tr><td>/robot-ops/visitor/whitelist/{id}/status</td><td>PUT</td><td>启用/停用白名单</td><td>白名单ID(id)、启用状态(status)</td><td>无</td><td>robot_ops_whitelist</td></tr>
       <tr><td>/robot-ops/visitor/whitelist/{id}</td><td>DELETE</td><td>删除白名单</td><td>白名单ID(id)</td><td>无</td><td>robot_ops_whitelist</td></tr>
-      <tr><td>/robot-ops/visitor/whitelist/import</td><td>POST</td><td>导入白名单</td><td>Excel文件(file)</td><td>导入总数、成功数、失败数、失败明细</td><td>robot_ops_whitelist</td></tr>
-      <tr><td>/robot-ops/visitor/whitelist/export</td><td>GET</td><td>导出白名单</td><td>同分页查询条件</td><td>Excel文件</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/import</td><td>POST</td><td>导入白名单</td><td>Excel文件(file);导入字段包括姓名、人员类型、手机号、身份证号、人脸照片地址、来源类型、有效开始时间、有效结束时间、启用状态、备注</td><td>导入总数、成功数、失败数、失败明细;导入时需校验 mobile、idCardNo、faceImageUrl 三者至少填写一种</td><td>robot_ops_whitelist</td></tr>
+      <tr><td>/robot-ops/visitor/whitelist/export</td><td>GET</td><td>导出白名单</td><td>同分页查询条件</td><td>Excel文件;导出字段包括姓名、手机号、身份证号、人员类型、人脸照片地址、来源类型、有效开始时间、有效结束时间、启用状态、更新时间、备注</td><td>robot_ops_whitelist</td></tr>
     </tbody></table>
+    <div class="note">白名单中的 whitelistType 字段在页面上显示为"人员类型",用于表示人员身份,如内部人员、访客、VIP、其他。人脸识别不作为人员类型,而是白名单匹配方式之一。</div>
+    <div class="note">白名单不单独设置识别方式字段。机器人侧可根据当前采集到的身份信息匹配白名单:人脸识别时通过 faceImageUrl 对应的人脸照片进行比对;刷身份证或输入身份证时匹配 idCardNo;输入手机号时匹配 mobile。任一方式匹配到启用且在有效期内的白名单人员,即视为白名单命中。</div>
+    <div class="note">新增或编辑白名单时,mobile、idCardNo、faceImageUrl 三者至少填写一种;一期不保存人脸特征ID。</div>
 
     <h3>7.6 监控管理接口</h3>
     <table><thead><tr><th>接口</th><th>方法</th><th>说明</th><th>请求参数</th><th>返回/处理字段</th><th>数据库表</th></tr></thead><tbody>
@@ -684,18 +699,29 @@
     <h4>8.3.3 白名单表 robot_ops_whitelist</h4>
     <div class="code">CREATE TABLE `robot_ops_whitelist` (
   `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-  `name` VARCHAR(100) NOT NULL COMMENT '人员姓名',
+  `name` VARCHAR(100) NOT NULL COMMENT '姓名',
   `mobile` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
-  `whitelist_type` VARCHAR(50) DEFAULT NULL COMMENT '白名单类型:访客、内部人员、人脸白名单等',
-  `source_type` VARCHAR(20) DEFAULT NULL COMMENT '来源类型:LOCAL本地,PLATFORM平台同步',
+  `id_card_no` VARCHAR(50) DEFAULT NULL COMMENT '身份证号',
+  `whitelist_type` VARCHAR(50) DEFAULT NULL COMMENT '人员类型字典值,如internal内部人员、visitor访客、vip VIP、other其他',
+  `face_image_url` VARCHAR(255) DEFAULT NULL COMMENT '人脸照片地址,用于机器人侧照片比对',
+  `source_type` VARCHAR(50) DEFAULT NULL COMMENT '来源类型:local本地录入,platform平台同步,robot机器人采集',
+  `valid_start_time` DATETIME DEFAULT NULL COMMENT '有效开始时间,不填表示立即生效',
+  `valid_end_time` DATETIME DEFAULT NULL COMMENT '有效结束时间,不填表示长期有效',
   `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '启用状态:0停用,1启用',
   `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`id`),
   KEY `idx_robot_ops_whitelist_mobile` (`mobile`),
-  KEY `idx_robot_ops_whitelist_status` (`status`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客白名单表';</div>
+  KEY `idx_robot_ops_whitelist_id_card_no` (`id_card_no`),
+  KEY `idx_robot_ops_whitelist_type` (`whitelist_type`),
+  KEY `idx_robot_ops_whitelist_source_type` (`source_type`),
+  KEY `idx_robot_ops_whitelist_status` (`status`),
+  KEY `idx_robot_ops_whitelist_valid_time` (`valid_start_time`, `valid_end_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='白名单表';</div>
+    <div class="note">说明:whitelist_type 当前页面显示为"人员类型",用于描述人员身份,不表示识别方式。建议字典项为 internal=内部人员、visitor=访客、vip=VIP、other=其他。</div>
+    <div class="note">说明:白名单不设置 recognition_type / auth_type 字段,因为同一人员可同时支持人脸照片、身份证号、手机号多种匹配方式。机器人侧根据实际采集到的信息选择对应字段进行匹配。</div>
+    <div class="note">说明:一期不建设 face_feature_id 字段。当前人脸白名单采用照片比对方式,仅保存 face_image_url。</div>
 
     <h3>8.4 监控与日志表</h3>
     <h4>8.4.1 远程喊话记录表 robot_ops_shout_record</h4>