longz
2026-07-08 700f9d0da314a4ea701fe9f68d305b67d72ec5fb
各类修复fix
1个文件已删除
22个文件已修改
405 ■■■■■ 已修改文件
backend/src/main/java/com/sinata/appointment/controller/DashboardController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/sinata/appointment/service/DashboardApplicationService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/sinata/appointment/service/MiniApprovalDetailApplicationService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/application.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
frontend-admin/dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
frontend-admin/index.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
frontend-admin/src/layouts/MainLayout.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
frontend-admin/src/views/DashboardView.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
frontend-admin/src/views/LoginView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
frontend-h5/src/utils/visitReason.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/auto-imports.d.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/components.d.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/components/MiniLoginPrompt.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/appointment/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/index/index.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/mine/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/record/index.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/review/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/reviewDetail/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/pages/reviewList/index.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yuyueMiniProgram/src/utils/visitReason.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/sinata/appointment/controller/DashboardController.java
@@ -27,10 +27,8 @@
    @Operation(summary = "数据总览")
    @GetMapping("/overview")
    @SaCheckPermission("dashboard:page")
    public Result<DashboardOverviewVo> overview(
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
        return ResultUtil.data(dashboardApplicationService.getOverview(startDate, endDate));
    public Result<DashboardOverviewVo> overview() {
        return ResultUtil.data(dashboardApplicationService.getOverview());
    }
    @Operation(summary = "预约统计")
backend/src/main/java/com/sinata/appointment/service/DashboardApplicationService.java
@@ -45,18 +45,17 @@
    private final MpUserMapper mpUserMapper;
    private final SysDeptMapper sysDeptMapper;
    public DashboardOverviewVo getOverview(LocalDate startDate, LocalDate endDate) {
        DateRange range = resolveRange(startDate, endDate);
        LocalDate dailyDate = resolveDailyDate(range);
    public DashboardOverviewVo getOverview() {
        LocalDate dailyDate = LocalDate.now();
        DashboardOverviewVo vo = new DashboardOverviewVo();
        vo.setStartDate(format(range.startDate()));
        vo.setEndDate(format(range.endDate()));
        vo.setStartDate(null);
        vo.setEndDate(null);
        vo.setDailyDate(format(dailyDate));
        vo.setDailyAppointmentCount(countAppointments(dailyDate, dailyDate, null));
        vo.setTotalAppointmentCount(countAppointments(range.startDate(), range.endDate(), null));
        vo.setRegisteredUserCount(countRegisteredUsers(range.startDate(), range.endDate()));
        vo.setEmployeeCount(countEmployees(range.startDate(), range.endDate()));
        vo.setTotalAppointmentCount(countAllAppointments());
        vo.setRegisteredUserCount(countRegisteredUsers());
        vo.setEmployeeCount(countEmployees());
        return vo;
    }
@@ -131,6 +130,11 @@
                appointmentRangeWrapper(startDate, endDate, resolveDeptIds(visitedDeptId), null));
    }
    private long countAllAppointments() {
        return appointmentOrderMapper.selectCount(
                new LambdaQueryWrapper<AppointmentOrder>().eq(AppointmentOrder::getDelFlag, 0));
    }
    private LambdaQueryWrapper<AppointmentOrder> appointmentRangeWrapper(
            LocalDate startDate, LocalDate endDate, List<Long> deptIds, String status) {
        LambdaQueryWrapper<AppointmentOrder> wrapper =
@@ -203,22 +207,18 @@
        return result;
    }
    private long countRegisteredUsers(LocalDate startDate, LocalDate endDate) {
    private long countRegisteredUsers() {
        return mpUserMapper.selectCount(
                new LambdaQueryWrapper<MpUser>()
                        .eq(MpUser::getDelFlag, 0)
                        .eq(MpUser::getUserType, MiniUserType.VISITOR)
                        .ge(MpUser::getCreateTime, atStartOfDay(startDate))
                        .lt(MpUser::getCreateTime, atStartOfNextDay(endDate)));
                        .eq(MpUser::getUserType, MiniUserType.VISITOR));
    }
    private long countEmployees(LocalDate startDate, LocalDate endDate) {
    private long countEmployees() {
        return mpUserMapper.selectCount(
                new LambdaQueryWrapper<MpUser>()
                        .eq(MpUser::getDelFlag, 0)
                        .eq(MpUser::getUserType, MiniUserType.EMPLOYEE)
                        .ge(MpUser::getCreateTime, atStartOfDay(startDate))
                        .lt(MpUser::getCreateTime, atStartOfNextDay(endDate)));
                        .eq(MpUser::getUserType, MiniUserType.EMPLOYEE));
    }
    private DateRange resolveRange(LocalDate startDate, LocalDate endDate) {
@@ -232,14 +232,6 @@
            return new DateRange(resolvedEnd, resolvedStart);
        }
        return new DateRange(resolvedStart, resolvedEnd);
    }
    private LocalDate resolveDailyDate(DateRange range) {
        LocalDate today = LocalDate.now();
        if (!today.isBefore(range.startDate()) && !today.isAfter(range.endDate())) {
            return today;
        }
        return range.endDate();
    }
    private LocalDateTime atStartOfDay(LocalDate date) {
backend/src/main/java/com/sinata/appointment/service/MiniAppointmentApplicationService.java
@@ -1135,8 +1135,12 @@
    }
    private String buildVisitReasonText(AppointmentOrder row, Map<String, String> reasonNameMap) {
        String reasonName = reasonNameMap.get(trimToNull(row.getVisitReasonCode()));
        return StringUtils.hasText(reasonName) ? reasonName : trimToNull(row.getVisitReasonCode());
        String code = trimToNull(row.getVisitReasonCode());
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) {
            return "其他";
        }
        String reasonName = reasonNameMap.get(code);
        return StringUtils.hasText(reasonName) ? reasonName : code;
    }
    private String buildAccessAreaText(String rawJson) {
backend/src/main/java/com/sinata/appointment/service/MiniApprovalDetailApplicationService.java
@@ -393,6 +393,9 @@
        if (!StringUtils.hasText(code)) {
            return "-";
        }
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) {
            return "其他";
        }
        List<BizVisitReason> rows =
                bizVisitReasonMapper.selectList(
                        new LambdaQueryWrapper<BizVisitReason>()
backend/src/main/java/com/sinata/appointment/service/MiniHandlerApprovalApplicationService.java
@@ -6,6 +6,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sinata.appointment.config.MiniStpUtil;
import com.sinata.appointment.constants.AppointmentBizConstants;
import com.sinata.appointment.constants.ApprovalInstanceStatus;
import com.sinata.appointment.constants.ApprovalNodeCode;
import com.sinata.appointment.constants.ApprovalTaskStatus;
@@ -637,6 +638,9 @@
    private String buildVisitReasonText(AppointmentOrder order, Map<String, String> reasonNameMap) {
        String reasonCode = trimToNull(order.getVisitReasonCode());
        if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(reasonCode)) {
            return "其他";
        }
        String reasonName = reasonNameMap.get(reasonCode);
        return StringUtils.hasText(reasonName) ? reasonName : reasonCode;
    }
backend/src/main/resources/application.yml
@@ -91,9 +91,9 @@
integration:
  hikvision:
    qrcode:
      encrypt-enabled: ${HIKVISION_QRCODE_ENCRYPT_ENABLED:false}
      aes-key: ${HIKVISION_QRCODE_AES_KEY:SinataHikvisionQrcodeKey01}
      aes-iv: ${HIKVISION_QRCODE_AES_IV:SinataHikvisionQrcodeIv001}
      encrypt-enabled: ${HIKVISION_QRCODE_ENCRYPT_ENABLED:true}
      aes-key: "WwXxYyZz1234!@#$"
      aes-iv: "AaBbCcDd1234!@#$"
      valid-minutes: ${HIKVISION_QRCODE_VALID_MINUTES:0}
file-upload-path: /opt/web/uploads/
frontend-admin/dist.zip
Binary files differ
frontend-admin/index.html
@@ -4,7 +4,7 @@
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/jpeg" href="/src/static/logo.jpg" />
    <title>访客预约管理后台</title>
    <title>红宇集团访客预约管理系统</title>
  </head>
  <body>
    <div id="app"></div>
frontend-admin/src/layouts/MainLayout.vue
@@ -3,7 +3,7 @@
    <el-aside width="15.98vw" class="side-wrap">
      <div class="brand-block">
        <img :src="siteLogo" alt="logo" class="brand-icon" />
        <div class="brand-name">运维运营管理平台</div>
        <div class="brand-name">红宇集团访客预约管理系统</div>
      </div>
      <el-menu
        :default-active="active"
frontend-admin/src/views/DashboardView.vue
@@ -3,20 +3,6 @@
    <section class="panel overview-panel" v-loading="overviewLoading">
      <div class="section-head">
        <div class="section-title">数据总览</div>
        <div class="filters">
          <el-date-picker
            v-model="overviewDateRange"
            class="range-picker"
            type="daterange"
            unlink-panels
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="YYYY-MM-DD"
            :clearable="false"
            @change="loadOverview"
          />
        </div>
      </div>
      <div class="summary-grid">
@@ -180,13 +166,12 @@
const lineChartRef = ref<HTMLElement | null>(null);
let lineChartInstance: ECharts | null = null;
const overviewDateRange = ref<[string, string]>(getCurrentWeekRange());
const chartDateRange = ref<[string, string]>(getCurrentWeekRange());
const overview = ref<DashboardOverview>({
  startDate: overviewDateRange.value[0],
  endDate: overviewDateRange.value[1],
  dailyDate: overviewDateRange.value[1],
  startDate: "",
  endDate: "",
  dailyDate: formatDate(new Date()),
  dailyAppointmentCount: 0,
  totalAppointmentCount: 0,
  registeredUserCount: 0,
@@ -360,12 +345,7 @@
async function loadOverview() {
  overviewLoading.value = true;
  try {
    const { data } = await http.get("/dashboard/overview", {
      params: {
        startDate: overviewDateRange.value?.[0],
        endDate: overviewDateRange.value?.[1],
      },
    });
    const { data } = await http.get("/dashboard/overview");
    overview.value = {
      ...overview.value,
      ...(data.result || {}),
frontend-admin/src/views/LoginView.vue
@@ -4,7 +4,7 @@
    <div class="bg-glow bg-glow-right"></div>
    <el-card class="card" shadow="never">
      <h2 class="title">宇通行管理平台</h2>
      <h2 class="title">红宇集团访客预约管理系统</h2>
      <p class="subtitle">安全、高效地处理预约、审批与接待流程</p>
      <el-form :model="form" label-position="top" class="login-form" @submit.prevent="onSubmit">
frontend-h5/src/utils/visitReason.ts
@@ -3,6 +3,8 @@
  if (!raw) {
    return ''
  }
  if (raw.toUpperCase() === 'OTHER') {
    return '其他'
  }
  return raw.replace(/^其他[::]\s*/, '')
}
yuyueMiniProgram/src/auto-imports.d.ts
@@ -37,6 +37,7 @@
  const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
  const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
  const createPinia: typeof import('pinia')['createPinia']
  const createQrCodeImage: typeof import('./utils/qrCodeImage')['createQrCodeImage']
  const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
  const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
  const createRouter: typeof import('@wot-ui/router')['createRouter']
yuyueMiniProgram/src/components.d.ts
@@ -19,7 +19,6 @@
    WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default']
    WdGap: typeof import('wot-design-uni/components/wd-gap/wd-gap.vue')['default']
    WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
    WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
    WdKeyboard: typeof import('wot-design-uni/components/wd-keyboard/wd-keyboard.vue')['default']
    WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']
    WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default']
yuyueMiniProgram/src/components/MiniLoginPrompt.vue
@@ -14,6 +14,7 @@
const visible = computed(() => authStore.loginPromptVisible)
const needPhoneAuthorize = computed(() => authStore.needPhoneAuthorize)
const shouldRenderPrompt = ref(true)
const message = computed(() => {
  if (needPhoneAuthorize.value) {
    return '首次登录需授权手机号完成身份识别'
@@ -22,6 +23,7 @@
})
function hideLoginPrompt() {
  shouldRenderPrompt.value = true
  authStore.hideLoginPrompt()
}
@@ -78,11 +80,13 @@
}
async function handlePhoneAuthorize(event: any) {
  shouldRenderPrompt.value = true
  if (loginSubmitting.value) {
    return
  }
  const phoneCode = event?.detail?.code
  if (!phoneCode) {
    authStore.showLoginPrompt()
    uni.showToast({
      title: '未获取到手机号授权',
      icon: 'none',
@@ -107,6 +111,7 @@
    })
  }
  catch (error) {
    authStore.showLoginPrompt()
    uni.showToast({
      title: error instanceof Error ? error.message : '登录失败',
      icon: 'none',
@@ -116,14 +121,27 @@
    loginSubmitting.value = false
  }
}
function handleBeforePhoneAuthorize() {
  if (loginSubmitting.value) {
    return
  }
  shouldRenderPrompt.value = false
}
</script>
<template>
  <view v-if="visible" class="fixed inset-0 z-40 bg-[rgba(0,0,0,0.55)]" @click="!needPhoneAuthorize && hideLoginPrompt()" />
  <view
    v-if="visible"
    class="fixed inset-0 z-40 bg-[rgba(0,0,0,0.55)]"
    :class="shouldRenderPrompt ? '' : 'opacity-0 pointer-events-none'"
    @click="!needPhoneAuthorize && hideLoginPrompt()"
  />
  <view
    v-if="visible"
    class="fixed left-[50%] top-[50%] z-50 w-[520rpx] translate-x-[-50%] translate-y-[-50%] rounded-[28rpx] bg-white px-[40rpx] pb-[36rpx] pt-[28rpx]"
    :class="shouldRenderPrompt ? '' : 'opacity-0 pointer-events-none'"
  >
    <view class="mb-[16rpx] text-center text-[34rpx] text-[#1A1A1A] font-medium">
      提示
@@ -146,6 +164,7 @@
        class="m-0 h-[80rpx] flex flex-1 items-center justify-center rounded-full border-none bg-[#0C6FFF] p-0 text-[30rpx] text-white after:border-none"
        :disabled="loginSubmitting"
        open-type="getPhoneNumber"
        @tap="handleBeforePhoneAuthorize"
        @getphonenumber="handlePhoneAuthorize"
      >
        {{ loginSubmitting ? '授权中...' : '授权手机号' }}
yuyueMiniProgram/src/pages/appointment/index.vue
@@ -40,6 +40,8 @@
  RESPONDENT: 'respondent',
  VISIT_REASON: 'visit_reason',
  VISIT_START_TIME: 'visit_start_time',
  LICENSE_PLATE: 'license_plate',
  DRIVER: 'driver',
} as const
interface AppointmentVehicle {
@@ -93,6 +95,10 @@
const isVisitReasonRequired = computed(() => isFieldRequired(APPOINTMENT_FIELD_CODES.VISIT_REASON, true))
const isRespondentRequired = computed(() => isFieldRequired(APPOINTMENT_FIELD_CODES.RESPONDENT, true))
const isVisitTimeRequired = computed(() => isFieldRequired(APPOINTMENT_FIELD_CODES.VISIT_START_TIME, true))
const showVehicleEntryOption = computed(() =>
  isFieldVisible(APPOINTMENT_FIELD_CODES.LICENSE_PLATE, true)
  || isFieldVisible(APPOINTMENT_FIELD_CODES.DRIVER, true),
)
const showCustomReasonInput = computed(() => Boolean(selectedVisitReason.value))
const showReasonPicker = ref(false)
const appointmentIcons = {
@@ -124,6 +130,14 @@
    return defaultValue
  }
  return config.requiredFlag === 1
}
function isFieldVisible(fieldCode: string, defaultValue = true) {
  const config = findFieldConfig(fieldCode)
  if (!config) {
    return defaultValue
  }
  return config.visible === 1
}
function handlePlateCharClick(vehicleIndex: number, charIndex: number) {
@@ -190,6 +204,10 @@
  if (lastFilledIndex < 7) {
    focusNextPlateChar(vehicleIndex, lastFilledIndex + 1)
  }
}
function handleIntervieweeInput(e: any) {
  formData.interviewee = e?.detail?.value || ''
}
function handlePlateDelete(vehicleIndex: number, charIndex: number) {
@@ -954,6 +972,9 @@
async function loadFieldConfigs() {
  try {
    fieldConfigs.value = await fetchMiniFieldConfigs()
    if (!showVehicleEntryOption.value) {
      handleVehicleEntryChange(false)
    }
  }
  catch (error) {
    fieldConfigs.value = []
@@ -1307,6 +1328,15 @@
  },
)
watch(
  showVehicleEntryOption,
  (visible) => {
    if (!visible) {
      handleVehicleEntryChange(false)
    }
  },
)
onLoad((options) => {
  uni.$on('selectVisitors', handleVisitorSelection)
  rebookOrderId.value = options?.rebookOrderId ? Number(options.rebookOrderId) : null
@@ -1368,15 +1398,24 @@
        class="mb-[48rpx] rounded-[16rpx] bg-white pl-[32rpx] pr-[12rpx] py-[8rpx] shadow-[0px_4px_8px_0px_rgba(6,47,108,0.1608)]">
        <wd-form :model="formData">
          <!-- 被访人 -->
          <wd-input v-model="formData.interviewee" placeholder="请输入要拜访谁" align="right" custom-class="custom-cell"
            placeholder-class="custom-placeholder">
            <template #label>
              <view class="inline-flex items-start">
                <text class="text-[28rpx] text-[#333333]">被访人</text>
                <text v-if="isRespondentRequired" class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
              </view>
            </template>
          </wd-input>
          <view class="custom-cell flex items-center justify-between">
            <view class="inline-flex items-start shrink-0">
              <text class="text-[28rpx] text-[#333333]">被访人</text>
              <text v-if="isRespondentRequired" class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
            </view>
            <input
              :value="formData.interviewee"
              type="text"
              placeholder="请输入要拜访谁"
              placeholder-class="text-[#B3B3B3]"
              class="ml-[32rpx] h-[64rpx] flex-1 text-right text-[28rpx] text-[#333333]"
              confirm-type="search"
              :adjust-position="true"
              :cursor-spacing="120"
              @input="handleIntervieweeInput"
              @confirm="handleSearchVisitedUser"
            />
          </view>
          <view v-if="selectedVisitedUser" class="px-[20rpx] pb-[16rpx] text-right text-[22rpx] text-[#999999]">
            已选:{{ selectedVisitedUser.realName }}{{ selectedVisitedUser.deptName ? ` / ${selectedVisitedUser.deptName}` : '' }}
          </view>
@@ -1517,7 +1556,7 @@
                </view>
                <text class="ml-[12rpx] text-[28rpx] text-[#999999]">人员进场</text>
              </view>
              <view class="flex items-center" @click="handleVehicleEntryChange(!formData.vehicleEntry)">
              <view v-if="showVehicleEntryOption" class="flex items-center" @click="handleVehicleEntryChange(!formData.vehicleEntry)">
                <view
                  class="flex items-center justify-center w-[28rpx] h-[28rpx] rounded-full border-[2rpx] border-solid transition-colors duration-200"
                  :class="formData.vehicleEntry ? 'border-[#1677FF]' : 'border-[#CFCFCF]'">
yuyueMiniProgram/src/pages/index/index.vue
@@ -467,7 +467,7 @@
  const ticketUuid = res?.target?.dataset?.ticketUuid as string | undefined
  if (!ticketUuid) {
    return {
      title: '红宇精工访客预约',
      title: '红宇集团访客预约',
      path: '/pages/index/index',
    }
  }
@@ -502,15 +502,17 @@
<template>
  <view class="relative">
    <image src="/static/home/bg.png" class="absolute left-0 top-0 z-0 h-[1624rpx] w-full" mode="scaleToFill" />
    <view class="home-header-backdrop" />
    <view class="home-content-backdrop" />
    <view class="relative z-4" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }">
      <view class="mb-[64rpx] h-[88rpx] flex items-center px-[32rpx]">
    <view class="relative z-4 pb-[160rpx]" :style="{ paddingTop: `${safeAreaInsets?.top || 44}px` }">
      <view class="mb-[24rpx] h-[88rpx] flex items-center px-[32rpx]">
        <text class="text-[36rpx] text-white font-medium">
          红宇精工访客预约
          红宇集团访客预约
        </text>
      </view>
      <view class="mb-[36rpx] flex items-center pl-[26rpx] pr-[22rpx]">
      <view class="mb-[28rpx] flex items-center pl-[26rpx] pr-[22rpx]">
        <image src="/static/home/hi.png" class="mr-[24rpx] h-[96rpx] w-[212rpx]" mode="aspectFit" />
        <view class="flex flex-col">
          <view class="welcome-title flex items-center text-[56rpx] text-white font-[600]">
@@ -521,15 +523,13 @@
      <view v-if="showEmployeeDashboard" class="mb-[32rpx] w-full pl-[26rpx] pr-[22rpx]" style="box-sizing: border-box;">
        <view
          class="rounded-[16rpx] p-[24rpx] pb-[26rpx]"
          style="background: linear-gradient(180deg, #CCE4FE 0%, #FAFCFD 40%, #FDFDFD 100%); box-shadow: 0 4rpx 8rpx 0 rgba(0, 104, 255, 0.2);border: 1rpx solid #fff;"
          class="dashboard-panel rounded-[16rpx] p-[24rpx] pb-[26rpx]"
        >
          <view class="flex gap-[30rpx]">
            <view
              class="h-[126rpx] w-[312rpx] flex rounded-[20rpx] pl-[38rpx] pt-[20rpx]"
              class="home-feature-card h-[126rpx] w-[312rpx] flex rounded-[20rpx] pl-[38rpx] pt-[20rpx]"
              style="background: url('/static/home/toDo_bg.png') no-repeat center center;
              background-size: 100%;box-sizing: border-box;
              box-shadow: 0 2rpx 8rpx 0 rgba(119, 144, 213, 0.3)"
              background-size: 100%;box-sizing: border-box;"
              @click="handleOpenApprovalTodo"
            >
              <image src="/static/home/toDo.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" />
@@ -543,10 +543,9 @@
              </view>
            </view>
            <view
              class="h-[126rpx] w-[312rpx] flex items-center rounded-[20rpx] pl-[38rpx] pt-[20rpx]"
              class="home-feature-card h-[126rpx] w-[312rpx] flex items-center rounded-[20rpx] pl-[38rpx] pt-[20rpx]"
              style="background: url('/static/home/wait_bg.png') no-repeat center center;
              background-size: 100%;box-sizing: border-box;
              box-shadow: 0 2rpx 8rpx 0 rgba(119, 144, 213, 0.3)"
              background-size: 100%;box-sizing: border-box;"
              @click="handleOpenVisitRecords"
            >
              <image src="/static/home/wait.png" class="mr-[14rpx] h-[100rpx] w-[108rpx]" />
@@ -565,8 +564,8 @@
      <view class="flex gap-[16rpx] pl-[24rpx] pr-[22rpx]">
        <view
          class="h-[240rpx] w-[344rpx] flex flex-1 flex-col items-center justify-center rounded-[20rpx]"
          style="background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
          class="home-action-card h-[240rpx] w-[344rpx] flex flex-1 flex-col items-center justify-center rounded-[20rpx]"
          style="background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 100%);"
          @click="ensureLoggedIn(() => { router.push({ name: 'appointment' }) })"
        >
          <image src="/static/home/appointment.png" class="mb-[12rpx] h-[96rpx] w-[96rpx]" />
@@ -576,8 +575,8 @@
        </view>
        <view class="flex flex-1 flex-col gap-[16rpx]">
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #FFF7E0 0%, #FFFFFF 100%);box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
            class="home-action-card flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #FFF7E0 0%, #FFFFFF 100%);"
            @click="ensureLoggedIn(() => { router.push({ name: 'record' }) })"
          >
            <image src="/static/home/record.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
@@ -586,8 +585,8 @@
            </text>
          </view>
          <view
            class="flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #E0FAEE 0%, #FFFFFF 100%);border: 1rpx solid #fff;box-shadow: 0 4rpx 10rpx 0 rgba(0, 104, 255, 0.16)"
            class="home-action-card flex flex-1 items-center rounded-[20rpx] px-[40rpx]"
            style="background: linear-gradient(180deg, #E0FAEE 0%, #FFFFFF 100%);"
            @click="ensureLoggedIn(() => { uni.navigateTo({ url: '/pages/commonVisitors/index' }) })"
          >
            <image src="/static/home/visitor.png" class="mr-[16rpx] h-[64rpx] w-[64rpx]" />
@@ -745,6 +744,45 @@
</template>
<style scoped>
.home-content-backdrop {
  position: absolute;
  left: 0;
  top: 420rpx;
  z-index: 0;
  width: 100%;
  height: 924rpx;
  background:
    linear-gradient(180deg, rgba(196, 232, 249, 0) 0%, rgba(196, 232, 249, 0.72) 12%, rgba(226, 244, 250, 0.9) 38%, rgba(239, 248, 252, 0.96) 100%);
  pointer-events: none;
}
.home-header-backdrop {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  width: 100%;
  height: 520rpx;
  background: linear-gradient(180deg, rgba(23, 145, 255, 0.96) 0%, rgba(39, 161, 255, 0.82) 62%, rgba(39, 161, 255, 0) 100%);
  pointer-events: none;
}
.dashboard-panel {
  border: 1rpx solid rgba(255, 255, 255, 0.92);
  background: linear-gradient(180deg, rgba(190, 222, 255, 0.98) 0%, rgba(245, 251, 255, 0.96) 48%, rgba(255, 255, 255, 0.98) 100%);
  box-shadow: 0 14rpx 30rpx rgba(31, 111, 206, 0.22), 0 4rpx 10rpx rgba(255, 255, 255, 0.7) inset;
}
.home-feature-card,
.home-action-card {
  border: 1rpx solid rgba(255, 255, 255, 0.92);
  box-shadow: 0 14rpx 28rpx rgba(35, 111, 205, 0.2), 0 4rpx 10rpx rgba(255, 255, 255, 0.65) inset;
}
.home-action-card {
  box-sizing: border-box;
}
.welcome-title {
  font-family: 'YouSheBiaoTiHei', sans-serif;
}
yuyueMiniProgram/src/pages/mine/index.vue
@@ -139,7 +139,7 @@
          :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }">
          <view class="relative z-10 flex h-full w-[88rpx] items-center justify-center" />
          <view class="absolute left-0 z-0 w-full text-center text-[34rpx] font-medium text-white">
            红宇精工访客预约
            红宇集团访客预约
          </view>
          <view class="w-[88rpx]" />
        </view>
yuyueMiniProgram/src/pages/record/index.vue
@@ -16,7 +16,6 @@
  status?: string
}
const router = useRouter()
const sysInfo = uni.getSystemInfoSync()
const statusBarHeight = sysInfo.statusBarHeight || 0
const navBarHeight = 44
@@ -43,7 +42,22 @@
]
function handleBack() {
  router.back()
  const pages = getCurrentPages()
  if (pages.length > 1) {
    uni.navigateBack({
      delta: 1,
      fail: () => {
        uni.switchTab({ url: '/pages/index/index' })
      },
    })
    return
  }
  uni.switchTab({
    url: '/pages/index/index',
    fail: () => {
      uni.reLaunch({ url: '/pages/index/index' })
    },
  })
}
function statusTheme(item: MiniAppointmentListItem) {
@@ -186,16 +200,16 @@
<template>
  <view class="h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col overflow-hidden">
    <view class="fixed left-0 top-0 z-0 h-[500rpx] w-full" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
    <view class="fixed left-0 top-0 z-0 h-[500rpx] w-full pointer-events-none" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
    <view class="fixed top-0 left-0 w-full z-50 flex flex-col">
      <view class="absolute left-0 top-0 z-[-1] w-full overflow-hidden" :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(288) + uni.upx2px(24)) + 'px' }">
    <view class="fixed top-0 left-0 w-full flex flex-col record-fixed-head">
      <view class="absolute left-0 top-0 z-0 w-full overflow-hidden pointer-events-none" :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(288) + uni.upx2px(24)) + 'px' }">
        <view class="w-full h-[500rpx] absolute top-0 left-0" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
      </view>
      <view class="relative w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
        <view class="flex items-center justify-between w-full absolute left-0" :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }">
          <view class="flex h-full w-[88rpx] items-center justify-center relative z-10" @click="handleBack">
      <view class="relative z-20 w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
        <view class="flex items-center justify-between w-full absolute left-0 z-20" :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }">
          <view class="record-back-hit flex h-full w-[120rpx] items-center justify-center relative" @tap.stop="handleBack" @click.stop="handleBack">
            <wd-icon name="arrow-left" size="40rpx" color="#ffffff" />
          </view>
          <view class="absolute left-0 w-full text-center text-[34rpx] font-medium text-white z-0">
@@ -205,7 +219,7 @@
        </view>
      </view>
      <view class="px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border">
      <view class="relative z-10 px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border">
        <view class="bg-white rounded-[16rpx] search-card shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full shrink-0">
          <view class="flex justify-center pt-[32rpx] pb-[16rpx]">
            <view class="w-[622rpx] h-[88rpx] rounded-[44rpx] bg-[#FFFFFF] border-[4rpx] border-solid border-[#D4E6FF] box-border flex items-center px-[32rpx]">
@@ -323,6 +337,14 @@
</template>
<style scoped>
.record-fixed-head {
  z-index: 9999;
}
.record-back-hit {
  z-index: 10001;
}
.search-card {
  width: 702rpx;
  height: 288rpx;
yuyueMiniProgram/src/pages/review/index.vue
@@ -22,7 +22,6 @@
  },
})
const router = useRouter()
const sysInfo = uni.getSystemInfoSync()
const statusBarHeight = sysInfo.statusBarHeight || 0
const navBarHeight = 44
@@ -78,8 +77,27 @@
  orderUuid: '',
})
function backOrHome() {
  const pages = getCurrentPages()
  if (pages.length > 1) {
    uni.navigateBack({
      delta: 1,
      fail: () => {
        uni.switchTab({ url: '/pages/index/index' })
      },
    })
    return
  }
  uni.switchTab({
    url: '/pages/index/index',
    fail: () => {
      uni.reLaunch({ url: '/pages/index/index' })
    },
  })
}
function handleBack() {
  router.back()
  backOrHome()
}
function handleCancel() {
@@ -107,7 +125,7 @@
    await cancelMyAppointment(query.value.orderId, query.value.orderUuid)
    uni.showToast({ title: '已取消预约', icon: 'success' })
    setTimeout(() => {
      router.back()
      backOrHome()
    }, 600)
  }
  catch (error) {
@@ -132,7 +150,7 @@
    await deleteMyAppointment(query.value.orderId, query.value.orderUuid)
    uni.showToast({ title: '已删除记录', icon: 'success' })
    setTimeout(() => {
      router.back()
      backOrHome()
    }, 600)
  }
  catch (error) {
yuyueMiniProgram/src/pages/reviewDetail/index.vue
@@ -58,7 +58,6 @@
  },
})
const router = useRouter()
const sysInfo = uni.getSystemInfoSync()
const statusBarHeight = sysInfo.statusBarHeight || 0
const navBarHeight = 44
@@ -109,7 +108,8 @@
const selectedLocalUnits = ref<MiniLocalUnitItem[]>([])
const unitPopupOffsetY = ref(0)
const unitPickerBaseHeight = uni.upx2px(940)
const unitPickerMinHeight = uni.upx2px(620)
const unitPickerMinHeight = uni.upx2px(480)
const unitKeyboardFallbackOffset = uni.upx2px(520)
const unitPickerPanelStyle = computed(() => ({
  height: `${Math.max(unitPickerMinHeight, unitPickerBaseHeight - unitPopupOffsetY.value)}px`,
  transform: `translateY(-${unitPopupOffsetY.value}px)`,
@@ -117,7 +117,7 @@
const handleUnitKeyboardHeightChange = (res: any) => {
  const height = Number(res?.height || 0)
  const maxOffset = Math.max(0, unitPickerBaseHeight - unitPickerMinHeight)
  unitPopupOffsetY.value = height > 0 ? Math.min(height, maxOffset) : 0
  unitPopupOffsetY.value = height > 0 ? Math.min(height + uni.upx2px(24), maxOffset) : unitPopupOffsetY.value
}
const appointmentIcons = {
@@ -230,8 +230,27 @@
}>({})
const showOtherLocalUnitInVisitInfo = computed(() => (detail.value?.localUnits?.length || 0) > 0)
function backOrHome() {
  const pages = getCurrentPages()
  if (pages.length > 1) {
    uni.navigateBack({
      delta: 1,
      fail: () => {
        uni.switchTab({ url: '/pages/index/index' })
      },
    })
    return
  }
  uni.switchTab({
    url: '/pages/index/index',
    fail: () => {
      uni.reLaunch({ url: '/pages/index/index' })
    },
  })
}
function handleBack() {
  router.back()
  backOrHome()
}
function resolveReviewNode(detailValue: MiniApprovalDetailItem): ReviewNodeType {
@@ -595,6 +614,8 @@
    return
  }
  uni.onKeyboardHeightChange(handleUnitKeyboardHeightChange)
  const maxOffset = Math.max(0, unitPickerBaseHeight - unitPickerMinHeight)
  unitPopupOffsetY.value = Math.min(unitKeyboardFallbackOffset, maxOffset)
  if (unitInput.value) {
    showUnitDropdown.value = filteredUnits.value.length > 0
  }
@@ -795,7 +816,7 @@
    }
    uni.showToast({ title: approve ? '已通过' : '已驳回', icon: 'success' })
    setTimeout(() => {
      router.back()
      backOrHome()
    }, 1000)
  }
  catch (error) {
@@ -1345,7 +1366,7 @@
              <text class="text-[#333333] text-[28rpx] font-medium">单位</text>
              <text class="text-[#FF4D4F] text-[28rpx] ml-[4rpx]">*</text>
            </view>
            <input v-model="unitInput" type="text" placeholder="请输入单位名称" placeholder-class="text-[#CCCCCC] text-[28rpx]" class="flex-1 text-[28rpx] text-[#333333] ml-[32rpx]" :disabled="isHandlerReadOnly()" :adjust-position="true" :cursor-spacing="120" @input="handleUnitInput" @focus="handleUnitFocus" @blur="handleUnitBlur" />
            <input v-model="unitInput" type="text" placeholder="请输入单位名称" placeholder-class="text-[#CCCCCC] text-[28rpx]" class="flex-1 text-[28rpx] text-[#333333] ml-[32rpx]" :disabled="isHandlerReadOnly()" :adjust-position="false" :cursor-spacing="120" @input="handleUnitInput" @focus="handleUnitFocus" @blur="handleUnitBlur" />
            <view class="flex items-center h-full pl-[24rpx] active:opacity-80" :class="isHandlerReadOnly() ? 'opacity-60' : ''" @click="addUnit">
              <image src="/static/review/add.png" class="w-[28rpx] h-[28rpx] mr-[8rpx]" mode="aspectFit" />
              <text class="text-[#0068FF] text-[28rpx]">添加</text>
yuyueMiniProgram/src/pages/reviewList/index.vue
@@ -29,7 +29,6 @@
  orderUuid: string
}
const router = useRouter()
const sysInfo = uni.getSystemInfoSync()
const statusBarHeight = sysInfo.statusBarHeight || 0
const navBarHeight = 44
@@ -62,8 +61,27 @@
const dateColumns = ref<string[][]>([[], [], []])
const datePickerIndex = ref([0, 0, 0])
function backOrHome() {
  const pages = getCurrentPages()
  if (pages.length > 1) {
    uni.navigateBack({
      delta: 1,
      fail: () => {
        uni.switchTab({ url: '/pages/index/index' })
      },
    })
    return
  }
  uni.switchTab({
    url: '/pages/index/index',
    fail: () => {
      uni.reLaunch({ url: '/pages/index/index' })
    },
  })
}
function handleBack() {
  router.back()
  backOrHome()
}
function getCurrentDate() {
@@ -94,6 +112,15 @@
    months.findIndex(item => item === selectedMonth.toString()),
    days.findIndex(item => item === selectedDay.toString()),
  ]
}
function formatVisitReasonDisplay(reasonText?: string | null, reasonCode?: string | null) {
  const normalizedText = normalizeVisitReasonText(reasonText)
  const code = (reasonCode || '').trim().toUpperCase()
  if (code === 'OTHER' || normalizedText === '其他') {
    return '其他'
  }
  return normalizedText || code || '-'
}
function handleDateChange(e: any) {
@@ -135,7 +162,7 @@
    nodeText: item.nodeName || '待审核',
    orderNo: item.orderNo,
    time: normalizeVisitTimeText(item.visitTimeText || `${item.visitStartDate || '-'}${item.visitEndDate && item.visitEndDate !== item.visitStartDate ? ` 至 ${item.visitEndDate}` : ''}`),
    reasonText: normalizeVisitReasonText(item.visitReasonText) || item.visitReasonCode || '-',
    reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode),
    reasonOtherText: (item.visitReasonOther || '').trim() || '-',
    createTime: item.createTime,
    taskId: item.taskId,
@@ -174,7 +201,7 @@
    nodeText,
    orderNo: item.orderNo,
    time: normalizeVisitTimeText(item.visitTimeText),
    reasonText: normalizeVisitReasonText(item.visitReasonText) || item.visitReasonCode || '-',
    reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode),
    reasonOtherText: (item.visitReasonOther || '').trim() || '-',
    createTime: item.createTime,
    orderId: item.id,
@@ -192,7 +219,7 @@
    nodeText: item.nodeText || (item.statusText || '已处理'),
    orderNo: item.orderNo,
    time: normalizeVisitTimeText(item.visitTimeText || '-'),
    reasonText: normalizeVisitReasonText(item.visitReasonText) || item.visitReasonCode || '-',
    reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode),
    reasonOtherText: (item.visitReasonOther || '').trim() || '-',
    createTime: item.createTime,
    taskId: item.taskId || undefined,
yuyueMiniProgram/src/utils/visitReason.ts
@@ -3,6 +3,8 @@
  if (!raw) {
    return ''
  }
  if (raw.toUpperCase() === 'OTHER') {
    return '其他'
  }
  return raw.replace(/^其他[::]\s*/, '')
}