| | |
| | | @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 = "预约统计") |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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 = |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | if (!StringUtils.hasText(code)) { |
| | | return "-"; |
| | | } |
| | | if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) { |
| | | return "其他"; |
| | | } |
| | | List<BizVisitReason> rows = |
| | | bizVisitReasonMapper.selectList( |
| | | new LambdaQueryWrapper<BizVisitReason>() |
| | |
| | | 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; |
| | |
| | | |
| | | 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; |
| | | } |
| | |
| | | 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/ |
| | |
| | | <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> |
| | |
| | | <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" |
| | |
| | | <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"> |
| | |
| | | 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, |
| | |
| | | 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 || {}), |
| | |
| | | <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"> |
| | |
| | | if (!raw) { |
| | | return '' |
| | | } |
| | | if (raw.toUpperCase() === 'OTHER') { |
| | | return '其他' |
| | | } |
| | | return raw.replace(/^其他[::]\s*/, '') |
| | | } |
| | | |
| | |
| | | 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'] |
| | |
| | | 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'] |
| | |
| | | |
| | | const visible = computed(() => authStore.loginPromptVisible) |
| | | const needPhoneAuthorize = computed(() => authStore.needPhoneAuthorize) |
| | | const shouldRenderPrompt = ref(true) |
| | | const message = computed(() => { |
| | | if (needPhoneAuthorize.value) { |
| | | return '首次登录需授权手机号完成身份识别' |
| | |
| | | }) |
| | | |
| | | function hideLoginPrompt() { |
| | | shouldRenderPrompt.value = true |
| | | authStore.hideLoginPrompt() |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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', |
| | |
| | | }) |
| | | } |
| | | catch (error) { |
| | | authStore.showLoginPrompt() |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '登录失败', |
| | | icon: 'none', |
| | |
| | | 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"> |
| | | 提示 |
| | |
| | | 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 ? '授权中...' : '授权手机号' }} |
| | |
| | | RESPONDENT: 'respondent', |
| | | VISIT_REASON: 'visit_reason', |
| | | VISIT_START_TIME: 'visit_start_time', |
| | | LICENSE_PLATE: 'license_plate', |
| | | DRIVER: 'driver', |
| | | } as const |
| | | |
| | | interface AppointmentVehicle { |
| | |
| | | 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 = { |
| | |
| | | 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) { |
| | |
| | | if (lastFilledIndex < 7) { |
| | | focusNextPlateChar(vehicleIndex, lastFilledIndex + 1) |
| | | } |
| | | } |
| | | |
| | | function handleIntervieweeInput(e: any) { |
| | | formData.interviewee = e?.detail?.value || '' |
| | | } |
| | | |
| | | function handlePlateDelete(vehicleIndex: number, charIndex: number) { |
| | |
| | | async function loadFieldConfigs() { |
| | | try { |
| | | fieldConfigs.value = await fetchMiniFieldConfigs() |
| | | if (!showVehicleEntryOption.value) { |
| | | handleVehicleEntryChange(false) |
| | | } |
| | | } |
| | | catch (error) { |
| | | fieldConfigs.value = [] |
| | |
| | | }, |
| | | ) |
| | | |
| | | watch( |
| | | showVehicleEntryOption, |
| | | (visible) => { |
| | | if (!visible) { |
| | | handleVehicleEntryChange(false) |
| | | } |
| | | }, |
| | | ) |
| | | |
| | | onLoad((options) => { |
| | | uni.$on('selectVisitors', handleVisitorSelection) |
| | | rebookOrderId.value = options?.rebookOrderId ? Number(options.rebookOrderId) : null |
| | |
| | | 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> |
| | |
| | | </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]'"> |
| | |
| | | const ticketUuid = res?.target?.dataset?.ticketUuid as string | undefined |
| | | if (!ticketUuid) { |
| | | return { |
| | | title: '红宇精工访客预约', |
| | | title: '红宇集团访客预约', |
| | | path: '/pages/index/index', |
| | | } |
| | | } |
| | |
| | | <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]"> |
| | |
| | | |
| | | <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]" /> |
| | |
| | | </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]" /> |
| | |
| | | |
| | | <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]" /> |
| | |
| | | </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]" /> |
| | |
| | | </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]" /> |
| | |
| | | </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; |
| | | } |
| | |
| | | :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> |
| | |
| | | status?: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | |
| | | ] |
| | | |
| | | 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) { |
| | |
| | | |
| | | <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"> |
| | |
| | | </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]"> |
| | |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .record-fixed-head { |
| | | z-index: 9999; |
| | | } |
| | | |
| | | .record-back-hit { |
| | | z-index: 10001; |
| | | } |
| | | |
| | | .search-card { |
| | | width: 702rpx; |
| | | height: 288rpx; |
| | |
| | | }, |
| | | }) |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | |
| | | 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() { |
| | |
| | | await cancelMyAppointment(query.value.orderId, query.value.orderUuid) |
| | | uni.showToast({ title: '已取消预约', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | backOrHome() |
| | | }, 600) |
| | | } |
| | | catch (error) { |
| | |
| | | await deleteMyAppointment(query.value.orderId, query.value.orderUuid) |
| | | uni.showToast({ title: '已删除记录', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | backOrHome() |
| | | }, 600) |
| | | } |
| | | catch (error) { |
| | |
| | | }, |
| | | }) |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | |
| | | 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)`, |
| | |
| | | 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 = { |
| | |
| | | }>({}) |
| | | 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 { |
| | |
| | | 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 |
| | | } |
| | |
| | | } |
| | | uni.showToast({ title: approve ? '已通过' : '已驳回', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | backOrHome() |
| | | }, 1000) |
| | | } |
| | | catch (error) { |
| | |
| | | <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> |
| | |
| | | orderUuid: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | |
| | | 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() { |
| | |
| | | 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) { |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | if (!raw) { |
| | | return '' |
| | | } |
| | | if (raw.toUpperCase() === 'OTHER') { |
| | | return '其他' |
| | | } |
| | | return raw.replace(/^其他[::]\s*/, '') |
| | | } |
| | | |