Commit 8429e4b4ae701e45b3226b13e3e2ed1f03041e24
1 parent
ed7c85aa
Enhance attendance record and punch application features by introducing a new vi…
…ew mode for weekly and monthly attendance, updating UI components for better user experience, and implementing Wi-Fi and geofencing options for attendance verification. Refactor backend services to support new attendance data structures and improve query efficiency for eligible users.
Showing
20 changed files
with
2362 additions
and
944 deletions
antis-ncc-admin/src/views/attendance-record/components/record-detail-dialog.vue
| ... | ... | @@ -44,7 +44,7 @@ |
| 44 | 44 | </div> |
| 45 | 45 | <div class="overview-card info"> |
| 46 | 46 | <div class="overview-card__label">录入方式</div> |
| 47 | - <div class="overview-card__value">{{ detail.isManual === 1 ? '含补卡/手工修正' : '真实打卡' }}</div> | |
| 47 | + <div class="overview-card__value">{{ isNoAttendanceRecord ? '无' : (detail.isManual === 1 ? '含补卡/手工修正' : '真实打卡') }}</div> | |
| 48 | 48 | </div> |
| 49 | 49 | </div> |
| 50 | 50 | |
| ... | ... | @@ -54,7 +54,7 @@ |
| 54 | 54 | <span>上班打卡</span> |
| 55 | 55 | <div class="punch-card__actions"> |
| 56 | 56 | <el-tag size="mini" :type="detail.punchIn && detail.punchIn.type === 2 ? 'warning' : 'success'"> |
| 57 | - {{ detail.punchIn ? detail.punchIn.typeText || '未打卡' : '未打卡' }} | |
| 57 | + {{ punchInTypeLabel }} | |
| 58 | 58 | </el-tag> |
| 59 | 59 | <el-button size="mini" type="text" :disabled="!hasCoordinate(detail.punchIn)" |
| 60 | 60 | @click="openMap(detail.punchIn, '上班打卡地点')">查看地图</el-button> |
| ... | ... | @@ -63,7 +63,7 @@ |
| 63 | 63 | <div class="punch-card__content"> |
| 64 | 64 | <div class="field-item"> |
| 65 | 65 | <label>打卡时间</label> |
| 66 | - <span>{{ detail.punchIn ? detail.punchIn.time || '未打卡' : '未打卡' }}</span> | |
| 66 | + <span>{{ punchInTimeLabel }}</span> | |
| 67 | 67 | </div> |
| 68 | 68 | <div class="field-item"> |
| 69 | 69 | <label>打卡地址</label> |
| ... | ... | @@ -89,7 +89,7 @@ |
| 89 | 89 | <span>下班打卡</span> |
| 90 | 90 | <div class="punch-card__actions"> |
| 91 | 91 | <el-tag size="mini" :type="detail.punchOut && detail.punchOut.type === 2 ? 'warning' : 'success'"> |
| 92 | - {{ detail.punchOut ? detail.punchOut.typeText || '未打卡' : '未打卡' }} | |
| 92 | + {{ punchOutTypeLabel }} | |
| 93 | 93 | </el-tag> |
| 94 | 94 | <el-button size="mini" type="text" :disabled="!hasCoordinate(detail.punchOut)" |
| 95 | 95 | @click="openMap(detail.punchOut, '下班打卡地点')">查看地图</el-button> |
| ... | ... | @@ -98,7 +98,7 @@ |
| 98 | 98 | <div class="punch-card__content"> |
| 99 | 99 | <div class="field-item"> |
| 100 | 100 | <label>打卡时间</label> |
| 101 | - <span>{{ detail.punchOut ? detail.punchOut.time || '未打卡' : '未打卡' }}</span> | |
| 101 | + <span>{{ punchOutTimeLabel }}</span> | |
| 102 | 102 | </div> |
| 103 | 103 | <div class="field-item"> |
| 104 | 104 | <label>打卡地址</label> |
| ... | ... | @@ -120,7 +120,7 @@ |
| 120 | 120 | </div> |
| 121 | 121 | </div> |
| 122 | 122 | |
| 123 | - <el-card shadow="never" class="supplement-card"> | |
| 123 | + <el-card v-if="hasSupplementInfo" shadow="never" class="supplement-card"> | |
| 124 | 124 | <div slot="header" class="supplement-card__header supplement-card__header--row"> |
| 125 | 125 | <span>补卡信息</span> |
| 126 | 126 | <el-button v-if="canShowCancelWorkflowSupplement" type="danger" size="mini" plain |
| ... | ... | @@ -151,7 +151,7 @@ |
| 151 | 151 | </div> |
| 152 | 152 | </el-card> |
| 153 | 153 | |
| 154 | - <el-card v-if="hasRelatedWorkflows || isLeaveOrSickStatus" shadow="never" | |
| 154 | + <el-card v-if="hasAttendanceRecord && (hasRelatedWorkflows || isLeaveOrSickStatus)" shadow="never" | |
| 155 | 155 | class="supplement-card workflow-card"> |
| 156 | 156 | <div slot="header" class="supplement-card__header"> |
| 157 | 157 | <span>关联流程</span> |
| ... | ... | @@ -176,6 +176,17 @@ |
| 176 | 176 | <span>当前为{{ detail.statusText }}状态,暂无关联流程记录</span> |
| 177 | 177 | </div> |
| 178 | 178 | </el-card> |
| 179 | + | |
| 180 | + <el-card v-if="showRecordRemarkOnly" shadow="never" class="supplement-card"> | |
| 181 | + <div slot="header" class="supplement-card__header"> | |
| 182 | + <span>记录备注</span> | |
| 183 | + </div> | |
| 184 | + <div class="supplement-grid"> | |
| 185 | + <div class="field-item field-item--full"> | |
| 186 | + <span>{{ detail.remark || '无' }}</span> | |
| 187 | + </div> | |
| 188 | + </div> | |
| 189 | + </el-card> | |
| 179 | 190 | </template> |
| 180 | 191 | |
| 181 | 192 | <el-empty v-else :image-size="72" description="暂无考勤详情" /> |
| ... | ... | @@ -232,6 +243,7 @@ export default { |
| 232 | 243 | case 2: |
| 233 | 244 | return 'warning' |
| 234 | 245 | case 3: |
| 246 | + case 8: | |
| 235 | 247 | return 'info' |
| 236 | 248 | default: |
| 237 | 249 | return 'danger' |
| ... | ... | @@ -240,12 +252,78 @@ export default { |
| 240 | 252 | hasRelatedWorkflows() { |
| 241 | 253 | return this.detail && Array.isArray(this.detail.relatedWorkflows) && this.detail.relatedWorkflows.length > 0 |
| 242 | 254 | }, |
| 255 | + /** 后端无考勤行时返回 hasAttendanceRecord: false(兼容旧接口:有 id 视为有记录) */ | |
| 256 | + hasAttendanceRecord() { | |
| 257 | + const d = this.detail | |
| 258 | + if (!d) return true | |
| 259 | + const h = d.hasAttendanceRecord | |
| 260 | + if (h === false || h === 0) return false | |
| 261 | + if (d.id) return true | |
| 262 | + return h !== false && h !== 0 | |
| 263 | + }, | |
| 264 | + isNoAttendanceRecord() { | |
| 265 | + return !!(this.detail && (this.detail.hasAttendanceRecord === false || this.detail.hasAttendanceRecord === 0)) | |
| 266 | + }, | |
| 267 | + /** 考勤日晚于今日、尚无打卡(后端 status=8 待考勤) */ | |
| 268 | + isPendingAttendanceDay() { | |
| 269 | + const d = this.detail | |
| 270 | + if (!d || !this.hasAttendanceRecord) return false | |
| 271 | + const s = d.status | |
| 272 | + return s === 8 || s === '8' | |
| 273 | + }, | |
| 274 | + /** 备注已记销假时不再视为「待销假」请假态,避免仍显示确认销假 */ | |
| 243 | 275 | isLeaveOrSickStatus() { |
| 244 | - return this.detail && (this.detail.status === 4 || this.detail.status === 5) | |
| 276 | + if (!this.detail) return false | |
| 277 | + const r = this.detail.remark | |
| 278 | + if (r != null && String(r).indexOf('销假') >= 0) return false | |
| 279 | + return this.detail.status === 4 || this.detail.status === 5 | |
| 280 | + }, | |
| 281 | + /** 无补卡卡片时,若有考勤备注仍单独展示(原备注在补卡卡片内) */ | |
| 282 | + showRecordRemarkOnly() { | |
| 283 | + if (!this.detail || !this.hasAttendanceRecord) return false | |
| 284 | + const r = this.detail.remark | |
| 285 | + if (r == null || String(r).trim() === '') return false | |
| 286 | + return !this.hasSupplementInfo | |
| 287 | + }, | |
| 288 | + /** 无流程补卡数据时不展示补卡信息卡片(避免全是「无」仍占版面) */ | |
| 289 | + hasSupplementInfo() { | |
| 290 | + if (!this.detail || !this.hasAttendanceRecord) return false | |
| 291 | + const s = this.detail.supplement | |
| 292 | + if (!s) return false | |
| 293 | + const wf = s.workflowId != null && s.workflowId !== '' ? s.workflowId : s.WorkflowId | |
| 294 | + if (wf) return true | |
| 295 | + const r = s.remark != null && s.remark !== '' ? s.remark : s.Remark | |
| 296 | + if (r) return true | |
| 297 | + const op = s.operatorName != null && s.operatorName !== '' ? s.operatorName : s.OperatorName | |
| 298 | + if (op) return true | |
| 299 | + const t = s.operateTime != null && s.operateTime !== '' ? s.operateTime : s.OperateTime | |
| 300 | + return !!t | |
| 301 | + }, | |
| 302 | + punchInTypeLabel() { | |
| 303 | + if (this.isNoAttendanceRecord || this.isPendingAttendanceDay) return '暂无打卡' | |
| 304 | + const p = this.detail && this.detail.punchIn | |
| 305 | + return p ? (p.typeText || '未打卡') : '未打卡' | |
| 306 | + }, | |
| 307 | + punchInTimeLabel() { | |
| 308 | + if (this.isNoAttendanceRecord || this.isPendingAttendanceDay) return '暂无打卡' | |
| 309 | + const p = this.detail && this.detail.punchIn | |
| 310 | + return p ? (p.time || '未打卡') : '未打卡' | |
| 311 | + }, | |
| 312 | + punchOutTypeLabel() { | |
| 313 | + if (this.isNoAttendanceRecord || this.isPendingAttendanceDay) return '暂无打卡' | |
| 314 | + const p = this.detail && this.detail.punchOut | |
| 315 | + return p ? (p.typeText || '未打卡') : '未打卡' | |
| 316 | + }, | |
| 317 | + punchOutTimeLabel() { | |
| 318 | + if (this.isNoAttendanceRecord || this.isPendingAttendanceDay) return '暂无打卡' | |
| 319 | + const p = this.detail && this.detail.punchOut | |
| 320 | + return p ? (p.time || '未打卡') : '未打卡' | |
| 245 | 321 | }, |
| 246 | 322 | /** 正常上下班均已打卡,或已有补卡/手工修正,或请假/病假:不显示后台补卡入口 */ |
| 247 | 323 | showSupplementButton() { |
| 248 | 324 | if (!this.detail) return false |
| 325 | + if (!this.detail.id) return false | |
| 326 | + if (this.detail.status === 8 || this.detail.status === '8') return false | |
| 249 | 327 | if (this.detail.isManual === 1) return false |
| 250 | 328 | if (this.detail.status === 4 || this.detail.status === 5) return false |
| 251 | 329 | const hasIn = !!(this.detail.punchIn && this.detail.punchIn.time) |
| ... | ... | @@ -311,6 +389,7 @@ export default { |
| 311 | 389 | workflowTagType(type) { |
| 312 | 390 | if (type === '请假') return 'danger' |
| 313 | 391 | if (type === '补卡') return 'warning' |
| 392 | + if (type === '销假') return 'success' | |
| 314 | 393 | return 'info' |
| 315 | 394 | }, |
| 316 | 395 | canOpenRelatedWorkflow(wf) { | ... | ... |
antis-ncc-admin/src/views/attendance-record/index.vue
| ... | ... | @@ -7,11 +7,11 @@ |
| 7 | 7 | <div class="record-hero__eyebrow">Attendance Record</div> |
| 8 | 8 | <h2 class="record-hero__title">考勤记录</h2> |
| 9 | 9 | <p class="record-hero__desc"> |
| 10 | - 按月查看员工每日考勤状态(含打卡、补卡及审批通过的请假/病假同步),可查看上下班时间、地点、照片与补卡信息;点击有记录的日期可查看详情并后台补卡。 | |
| 10 | + 按月查看员工每日考勤状态(列表含筛选范围内全部在职员工,无记录日期显示为无);含打卡、补卡及审批通过的请假/病假同步,可查看上下班时间、地点、照片与补卡信息;点击日期可查看详情并后台补卡。 | |
| 11 | 11 | </p> |
| 12 | 12 | </div> |
| 13 | 13 | <div class="record-hero__actions"> |
| 14 | - <el-button type="primary" plain icon="el-icon-download" :loading="exportLoading" | |
| 14 | + <el-button v-if="viewMode === 'month'" type="primary" plain icon="el-icon-download" :loading="exportLoading" | |
| 15 | 15 | @click="exportMonthReport">导出月度明细</el-button> |
| 16 | 16 | <el-button icon="el-icon-refresh-right" @click="refreshPage">刷新</el-button> |
| 17 | 17 | </div> |
| ... | ... | @@ -22,10 +22,21 @@ |
| 22 | 22 | |
| 23 | 23 | <el-card class="filter-card" shadow="never"> |
| 24 | 24 | <el-form :inline="true" :model="query" class="filter-form" @submit.native.prevent> |
| 25 | - <el-form-item label="统计月份"> | |
| 25 | + <el-form-item label="视图"> | |
| 26 | + <el-radio-group v-model="viewMode" size="mini" @change="onViewModeChange"> | |
| 27 | + <el-radio-button label="month">月</el-radio-button> | |
| 28 | + <el-radio-button label="week">周</el-radio-button> | |
| 29 | + </el-radio-group> | |
| 30 | + </el-form-item> | |
| 31 | + | |
| 32 | + <el-form-item v-if="viewMode === 'month'" label="统计月份"> | |
| 26 | 33 | <el-date-picker v-model="query.month" type="month" value-format="yyyy-MM" format="yyyy-MM" |
| 27 | 34 | placeholder="选择月份" /> |
| 28 | 35 | </el-form-item> |
| 36 | + <el-form-item v-else label="选择日期"> | |
| 37 | + <el-date-picker v-model="query.weekDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" | |
| 38 | + placeholder="选择任意日期" @change="onWeekDateChange" /> | |
| 39 | + </el-form-item> | |
| 29 | 40 | <el-form-item label="考勤分组"> |
| 30 | 41 | <el-select v-model="query.attendanceGroupId" clearable filterable placeholder="全部分组"> |
| 31 | 42 | <el-option v-for="item in attendanceGroupOptions" :key="item.id" :label="item.fullName" |
| ... | ... | @@ -46,7 +57,7 @@ |
| 46 | 57 | <div class="summary-card"> |
| 47 | 58 | <div class="summary-card__label">员工人数</div> |
| 48 | 59 | <div class="summary-card__value">{{ summary.employeeCount }}</div> |
| 49 | - <div class="summary-card__hint">当前筛选条件下有考勤记录的员工数</div> | |
| 60 | + <div class="summary-card__hint">当前筛选条件下的在职员工数(含当月无考勤记录)</div> | |
| 50 | 61 | </div> |
| 51 | 62 | <div class="summary-card success"> |
| 52 | 63 | <div class="summary-card__label">正常出勤</div> |
| ... | ... | @@ -68,7 +79,7 @@ |
| 68 | 79 | <el-card class="table-card" shadow="never"> |
| 69 | 80 | <div slot="header" class="table-card__header"> |
| 70 | 81 | <div> |
| 71 | - <span>月度考勤明细</span> | |
| 82 | + <span>{{ viewMode === 'month' ? '月度考勤明细' : '周度考勤明细' }}</span> | |
| 72 | 83 | <span class="table-card__sub">{{ headerText }}</span> |
| 73 | 84 | </div> |
| 74 | 85 | <div class="table-card__legend"> |
| ... | ... | @@ -94,10 +105,10 @@ |
| 94 | 105 | </template> |
| 95 | 106 | </el-table-column> |
| 96 | 107 | |
| 97 | - <el-table-column v-for="day in monthDays" :key="day.key" :label="day.label" :min-width="110" align="left"> | |
| 108 | + <el-table-column v-for="day in activeDays" :key="day.key" :label="day.label" :min-width="110" align="left"> | |
| 98 | 109 | <template slot-scope="scope"> |
| 99 | 110 | <div class="day-cell" |
| 100 | - :class="[`status-${scope.row.dayRecords[day.key].statusKey}`, { clickable: scope.row.dayRecords[day.key].statusKey !== 'empty' }]" | |
| 111 | + :class="[`status-${scope.row.dayRecords[day.key].statusKey}`, 'clickable']" | |
| 101 | 112 | @click="handleOpenDetail(scope.row, day.key, scope.row.dayRecords[day.key])"> |
| 102 | 113 | <div class="day-cell__time">{{ scope.row.dayRecords[day.key].timeText }}</div> |
| 103 | 114 | <el-tag :type="scope.row.dayRecords[day.key].tagType" size="mini" effect="plain"> |
| ... | ... | @@ -136,6 +147,7 @@ export default { |
| 136 | 147 | }, |
| 137 | 148 | data() { |
| 138 | 149 | return { |
| 150 | + viewMode: 'month', | |
| 139 | 151 | pageLoading: false, |
| 140 | 152 | tableLoading: false, |
| 141 | 153 | exportLoading: false, |
| ... | ... | @@ -147,7 +159,29 @@ export default { |
| 147 | 159 | } |
| 148 | 160 | }, |
| 149 | 161 | computed: { |
| 150 | - monthDays() { | |
| 162 | + headerText() { | |
| 163 | + if (this.viewMode === 'week') { | |
| 164 | + const anchor = dayjs(this.query.weekDate || dayjs().format('YYYY-MM-DD')) | |
| 165 | + const diff = (anchor.day() + 6) % 7 // Monday-based | |
| 166 | + const start = anchor.subtract(diff, 'day') | |
| 167 | + const end = start.add(6, 'day') | |
| 168 | + return `${start.format('YYYY-MM-DD')} ~ ${end.format('YYYY-MM-DD')}` | |
| 169 | + } | |
| 170 | + return `${this.query.month} · 共 ${this.activeDays.length} 天` | |
| 171 | + }, | |
| 172 | + activeDays() { | |
| 173 | + if (this.viewMode === 'week') { | |
| 174 | + const anchor = dayjs(this.query.weekDate || dayjs().format('YYYY-MM-DD')) | |
| 175 | + const diff = (anchor.day() + 6) % 7 // Monday-based | |
| 176 | + const start = anchor.subtract(diff, 'day') | |
| 177 | + return Array.from({ length: 7 }).map((_, index) => { | |
| 178 | + const date = start.add(index, 'day') | |
| 179 | + return { | |
| 180 | + key: date.format('YYYY-MM-DD'), | |
| 181 | + label: `${index + 1}日\n${this.getWeekLabel(date.day())}` | |
| 182 | + } | |
| 183 | + }) | |
| 184 | + } | |
| 151 | 185 | const base = dayjs(`${this.query.month}-01`) |
| 152 | 186 | const total = base.daysInMonth() |
| 153 | 187 | return Array.from({ length: total }).map((_, index) => { |
| ... | ... | @@ -157,9 +191,6 @@ export default { |
| 157 | 191 | label: `${index + 1}日\n${this.getWeekLabel(date.day())}` |
| 158 | 192 | } |
| 159 | 193 | }) |
| 160 | - }, | |
| 161 | - headerText() { | |
| 162 | - return `${this.query.month} · 共 ${this.monthDays.length} 天` | |
| 163 | 194 | } |
| 164 | 195 | }, |
| 165 | 196 | created() { |
| ... | ... | @@ -169,6 +200,7 @@ export default { |
| 169 | 200 | createDefaultQuery() { |
| 170 | 201 | return { |
| 171 | 202 | month: dayjs().format('YYYY-MM'), |
| 203 | + weekDate: dayjs().format('YYYY-MM-DD'), | |
| 172 | 204 | attendanceGroupId: '', |
| 173 | 205 | keyword: '', |
| 174 | 206 | currentPage: 1, |
| ... | ... | @@ -228,6 +260,9 @@ export default { |
| 228 | 260 | } |
| 229 | 261 | }, |
| 230 | 262 | async loadData() { |
| 263 | + if (this.viewMode === 'week') { | |
| 264 | + this.query.month = dayjs(this.query.weekDate || dayjs().format('YYYY-MM-DD')).format('YYYY-MM') | |
| 265 | + } | |
| 231 | 266 | if (!this.query.month) { |
| 232 | 267 | this.summary = this.createEmptySummary() |
| 233 | 268 | this.total = 0 |
| ... | ... | @@ -252,7 +287,7 @@ export default { |
| 252 | 287 | }, |
| 253 | 288 | normalizeRow(item) { |
| 254 | 289 | const dayRecordMap = {} |
| 255 | - this.monthDays.forEach(day => { | |
| 290 | + this.activeDays.forEach(day => { | |
| 256 | 291 | dayRecordMap[day.key] = this.createEmptyDayRecord() |
| 257 | 292 | }) |
| 258 | 293 | ; (item.dayRecords || []).forEach(record => { |
| ... | ... | @@ -271,10 +306,22 @@ export default { |
| 271 | 306 | this.loadData() |
| 272 | 307 | }, |
| 273 | 308 | search() { |
| 274 | - if (!this.query.month) { | |
| 309 | + if (this.viewMode === 'month' && !this.query.month) { | |
| 275 | 310 | this.$message.warning('请选择统计月份') |
| 276 | 311 | return |
| 277 | 312 | } |
| 313 | + if (this.viewMode === 'week' && !this.query.weekDate) { | |
| 314 | + this.$message.warning('请选择日期') | |
| 315 | + return | |
| 316 | + } | |
| 317 | + this.query.currentPage = 1 | |
| 318 | + this.loadData() | |
| 319 | + }, | |
| 320 | + onViewModeChange() { | |
| 321 | + this.query.currentPage = 1 | |
| 322 | + this.loadData() | |
| 323 | + }, | |
| 324 | + onWeekDateChange() { | |
| 278 | 325 | this.query.currentPage = 1 |
| 279 | 326 | this.loadData() |
| 280 | 327 | }, |
| ... | ... | @@ -283,7 +330,7 @@ export default { |
| 283 | 330 | this.loadData() |
| 284 | 331 | }, |
| 285 | 332 | handleOpenDetail(row, attendanceDate, dayRecord) { |
| 286 | - if (!dayRecord || dayRecord.statusKey === 'empty') return | |
| 333 | + // 即使当天为“空白/无记录”,也允许点开详情(弹窗内会显示暂无详情) | |
| 287 | 334 | this.$refs.recordDetailDialog.open({ |
| 288 | 335 | userId: row.userId, |
| 289 | 336 | attendanceDate |
| ... | ... | @@ -504,7 +551,8 @@ export default { |
| 504 | 551 | } |
| 505 | 552 | |
| 506 | 553 | .status-rest .day-cell__time, |
| 507 | - .status-empty .day-cell__time { | |
| 554 | + .status-empty .day-cell__time, | |
| 555 | + .status-pending .day-cell__time { | |
| 508 | 556 | color: #909399; |
| 509 | 557 | } |
| 510 | 558 | ... | ... |
antis-ncc-admin/src/views/lqMdxx/Form.vue
| ... | ... | @@ -53,6 +53,62 @@ |
| 53 | 53 | </div> |
| 54 | 54 | </el-form-item> |
| 55 | 55 | </el-col> |
| 56 | + <el-col :span="24"> | |
| 57 | + <el-form-item label="正常打卡方式"> | |
| 58 | + <div class="att-punch-check-row"> | |
| 59 | + <el-checkbox :true-label="1" :false-label="0" | |
| 60 | + v-model="dataForm.attendanceCheckFence">范围打卡(电子围栏)</el-checkbox> | |
| 61 | + <el-checkbox :true-label="1" :false-label="0" v-model="dataForm.attendanceCheckWifi">Wi-Fi | |
| 62 | + 打卡</el-checkbox> | |
| 63 | + </div> | |
| 64 | + <div class="att-punch-hint"> | |
| 65 | + 可同时勾选:<strong>两者都勾选</strong>时,员工在打卡<strong>范围内</strong>或满足<strong>门店 | |
| 66 | + Wi-Fi 规则</strong>,<strong>满足其一</strong>即可;仅勾选一项则只校验该项。 | |
| 67 | + Wi-Fi 以<strong>成对表</strong>维护(SSID + BSSID);可选「校验对应」防同名热点,拿不到 BSSID 时需依赖<strong>围栏 | |
| 68 | + + SSID</strong>。 | |
| 69 | + </div> | |
| 70 | + </el-form-item> | |
| 71 | + </el-col> | |
| 72 | + <el-col :span="24" v-if="Number(dataForm.attendanceCheckWifi) === 1"> | |
| 73 | + <el-form-item label="Wi-Fi 白名单"> | |
| 74 | + <div v-if="!isDetail" class="wifi-pair-toolbar"> | |
| 75 | + <el-button type="primary" size="small" icon="el-icon-plus" | |
| 76 | + @click="addWifiPair">添加一行</el-button> | |
| 77 | + </div> | |
| 78 | + <div class="wifi-pair-table"> | |
| 79 | + <div class="wifi-pair-head"> | |
| 80 | + <span class="wifi-pair-col-ssid">Wi-Fi 名称 (SSID)</span> | |
| 81 | + <span class="wifi-pair-col-bssid">BSSID(路由器 MAC)</span> | |
| 82 | + <span v-if="!isDetail" class="wifi-pair-col-act"></span> | |
| 83 | + </div> | |
| 84 | + <div v-for="(row, index) in dataForm.attendanceWifiPairList" :key="'wp-' + index" | |
| 85 | + class="wifi-pair-row"> | |
| 86 | + <el-input v-model="row.ssid" size="small" :disabled="isDetail" maxlength="64" | |
| 87 | + placeholder="与手机显示一致" /> | |
| 88 | + <el-input v-model="row.bssid" size="small" :disabled="isDetail" maxlength="64" | |
| 89 | + placeholder="如 aa:bb:cc:dd:ee:ff" /> | |
| 90 | + <div v-if="!isDetail" class="wifi-pair-col-act"> | |
| 91 | + <el-button type="text" size="small" class="wifi-pair-del" | |
| 92 | + @click="removeWifiPair(index)">删除</el-button> | |
| 93 | + </div> | |
| 94 | + </div> | |
| 95 | + <div v-if="!dataForm.attendanceWifiPairList.length" class="store-tags-empty">请至少添加一行;建议 SSID | |
| 96 | + 与 BSSID 成对填写</div> | |
| 97 | + </div> | |
| 98 | + </el-form-item> | |
| 99 | + </el-col> | |
| 100 | + <el-col :span="24" v-if="Number(dataForm.attendanceCheckWifi) === 1"> | |
| 101 | + <el-form-item label=" "> | |
| 102 | + <el-checkbox :true-label="1" :false-label="0" v-model="dataForm.attendanceWifiVerifyPair"> | |
| 103 | + 校验 SSID 与 BSSID 为同一 AP(防同名热点) | |
| 104 | + </el-checkbox> | |
| 105 | + <div class="att-punch-hint wifi-verify-hint"> | |
| 106 | + <strong>开启</strong>:能获取 BSSID 时必须与表中<strong>同一行</strong>的 SSID、BSSID 同时一致;获取不到 BSSID | |
| 107 | + 时须<strong>在电子围栏内</strong>且 SSID 命中。<strong>关闭</strong>:当前网络的 SSID 或 BSSID | |
| 108 | + 命中表中<strong>任意一行</strong>的任一字段即可。 | |
| 109 | + </div> | |
| 110 | + </el-form-item> | |
| 111 | + </el-col> | |
| 56 | 112 | |
| 57 | 113 | <!-- 其他业务字段 --> |
| 58 | 114 | <el-col :span="12"> |
| ... | ... | @@ -98,70 +154,43 @@ |
| 98 | 154 | </el-col> |
| 99 | 155 | <el-col :span="24"> |
| 100 | 156 | <el-form-item label="门店图片" prop="storeImages"> |
| 101 | - <NCC-UploadImg v-model="dataForm.storeImages" :fileSize="5" sizeUnit="MB" :limit="9" :disabled="!!isDetail" /> | |
| 157 | + <NCC-UploadImg v-model="dataForm.storeImages" :fileSize="5" sizeUnit="MB" :limit="9" | |
| 158 | + :disabled="!!isDetail" /> | |
| 102 | 159 | </el-form-item> |
| 103 | 160 | </el-col> |
| 104 | 161 | <el-col :span="24"> |
| 105 | 162 | <el-form-item label="门店介绍" prop="storeDescription"> |
| 106 | - <el-input v-model="dataForm.storeDescription" type="textarea" :rows="4" maxlength="1000" show-word-limit | |
| 107 | - placeholder="请输入门店介绍" /> | |
| 163 | + <el-input v-model="dataForm.storeDescription" type="textarea" :rows="4" maxlength="1000" | |
| 164 | + show-word-limit placeholder="请输入门店介绍" /> | |
| 108 | 165 | </el-form-item> |
| 109 | 166 | </el-col> |
| 110 | 167 | <el-col :span="24"> |
| 111 | 168 | <el-form-item label="营业时间设置" prop="businessHours"> |
| 112 | - <el-input | |
| 113 | - v-model="dataForm.businessHours" | |
| 114 | - type="textarea" | |
| 115 | - :rows="3" | |
| 116 | - maxlength="500" | |
| 117 | - show-word-limit | |
| 118 | - placeholder="请输入营业时间设置" | |
| 119 | - /> | |
| 169 | + <el-input v-model="dataForm.businessHours" type="textarea" :rows="3" maxlength="500" | |
| 170 | + show-word-limit placeholder="请输入营业时间设置" /> | |
| 120 | 171 | </el-form-item> |
| 121 | 172 | </el-col> |
| 122 | 173 | <el-col :span="24"> |
| 123 | 174 | <el-form-item label="交通提示" prop="trafficTips"> |
| 124 | - <el-input | |
| 125 | - v-model="dataForm.trafficTips" | |
| 126 | - type="textarea" | |
| 127 | - :rows="3" | |
| 128 | - maxlength="500" | |
| 129 | - show-word-limit | |
| 130 | - placeholder="请输入交通提示" | |
| 131 | - /> | |
| 175 | + <el-input v-model="dataForm.trafficTips" type="textarea" :rows="3" maxlength="500" | |
| 176 | + show-word-limit placeholder="请输入交通提示" /> | |
| 132 | 177 | </el-form-item> |
| 133 | 178 | </el-col> |
| 134 | 179 | <el-col :span="24"> |
| 135 | 180 | <el-form-item label="门店标签" prop="storeTags"> |
| 136 | 181 | <div class="store-tags-editor"> |
| 137 | 182 | <div class="store-tags-list"> |
| 138 | - <el-tag | |
| 139 | - v-for="(tag, index) in dataForm.storeTags" | |
| 140 | - :key="`${tag}-${index}`" | |
| 141 | - :disable-transitions="false" | |
| 142 | - :closable="!isDetail" | |
| 143 | - @close="handleRemoveTag(index)"> | |
| 183 | + <el-tag v-for="(tag, index) in dataForm.storeTags" :key="`${tag}-${index}`" | |
| 184 | + :disable-transitions="false" :closable="!isDetail" @close="handleRemoveTag(index)"> | |
| 144 | 185 | {{ tag }} |
| 145 | 186 | </el-tag> |
| 146 | 187 | <span v-if="!dataForm.storeTags.length" class="store-tags-empty">暂无标签</span> |
| 147 | 188 | </div> |
| 148 | 189 | <div class="store-tags-action-row" v-if="!isDetail"> |
| 149 | - <el-input | |
| 150 | - v-if="inputTagVisible" | |
| 151 | - ref="tagInput" | |
| 152 | - v-model="inputTagValue" | |
| 153 | - class="input-new-tag" | |
| 154 | - size="small" | |
| 155 | - maxlength="20" | |
| 156 | - placeholder="请输入标签内容" | |
| 157 | - @keyup.enter.native="handleInputTagConfirm" | |
| 158 | - @blur="handleInputTagConfirm" | |
| 159 | - /> | |
| 160 | - <el-button | |
| 161 | - v-else | |
| 162 | - class="button-new-tag tag-action-btn" | |
| 163 | - size="small" | |
| 164 | - icon="el-icon-plus" | |
| 190 | + <el-input v-if="inputTagVisible" ref="tagInput" v-model="inputTagValue" | |
| 191 | + class="input-new-tag" size="small" maxlength="20" placeholder="请输入标签内容" | |
| 192 | + @keyup.enter.native="handleInputTagConfirm" @blur="handleInputTagConfirm" /> | |
| 193 | + <el-button v-else class="button-new-tag tag-action-btn" size="small" icon="el-icon-plus" | |
| 165 | 194 | @click="showTagInput"> |
| 166 | 195 | 新增标签 |
| 167 | 196 | </el-button> |
| ... | ... | @@ -186,15 +215,10 @@ |
| 186 | 215 | </div> |
| 187 | 216 | </div> |
| 188 | 217 | <div class="location-search-bar"> |
| 189 | - <el-input | |
| 190 | - v-model="locationSearchKeyword" | |
| 191 | - placeholder="输入地址或关键词检索(如:成都市武侯区紫荆北路182号)" | |
| 192 | - clearable | |
| 193 | - size="small" | |
| 194 | - class="search-input" | |
| 195 | - @keyup.enter.native="handleSearchLocation" | |
| 196 | - > | |
| 197 | - <el-button slot="append" icon="el-icon-search" @click="handleSearchLocation" :loading="locationSearchLoading">检索</el-button> | |
| 218 | + <el-input v-model="locationSearchKeyword" placeholder="输入地址或关键词检索(如:成都市武侯区紫荆北路182号)" clearable | |
| 219 | + size="small" class="search-input" @keyup.enter.native="handleSearchLocation"> | |
| 220 | + <el-button slot="append" icon="el-icon-search" @click="handleSearchLocation" | |
| 221 | + :loading="locationSearchLoading">检索</el-button> | |
| 198 | 222 | </el-input> |
| 199 | 223 | </div> |
| 200 | 224 | <div id="location-map" class="map-canvas"></div> |
| ... | ... | @@ -253,22 +277,22 @@ const TMAP_WS_KEY = 'YRXBZ-NEV6T-K7SXH-VJPMF-G5IQF-F3FCJ' |
| 253 | 277 | |
| 254 | 278 | // JSONP 调用(绕过 CORS,腾讯 WebService API 前端需用 JSONP) |
| 255 | 279 | function jsonp(url) { |
| 256 | - return new Promise((resolve, reject) => { | |
| 257 | - const cbName = '_tmap_jsonp_' + Date.now() + '_' + Math.random().toString(36).slice(2); | |
| 258 | - const script = document.createElement('script'); | |
| 259 | - script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'output=jsonp&callback=' + cbName; | |
| 260 | - window[cbName] = function(res) { | |
| 261 | - if (script.parentNode) script.parentNode.removeChild(script); | |
| 262 | - delete window[cbName]; | |
| 263 | - resolve(res); | |
| 264 | - }; | |
| 265 | - script.onerror = function() { | |
| 266 | - if (script.parentNode) script.parentNode.removeChild(script); | |
| 267 | - delete window[cbName]; | |
| 268 | - reject(new Error('JSONP request failed')); | |
| 269 | - }; | |
| 270 | - document.body.appendChild(script); | |
| 271 | - }); | |
| 280 | + return new Promise((resolve, reject) => { | |
| 281 | + const cbName = '_tmap_jsonp_' + Date.now() + '_' + Math.random().toString(36).slice(2); | |
| 282 | + const script = document.createElement('script'); | |
| 283 | + script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'output=jsonp&callback=' + cbName; | |
| 284 | + window[cbName] = function (res) { | |
| 285 | + if (script.parentNode) script.parentNode.removeChild(script); | |
| 286 | + delete window[cbName]; | |
| 287 | + resolve(res); | |
| 288 | + }; | |
| 289 | + script.onerror = function () { | |
| 290 | + if (script.parentNode) script.parentNode.removeChild(script); | |
| 291 | + delete window[cbName]; | |
| 292 | + reject(new Error('JSONP request failed')); | |
| 293 | + }; | |
| 294 | + document.body.appendChild(script); | |
| 295 | + }); | |
| 272 | 296 | } |
| 273 | 297 | |
| 274 | 298 | export default { |
| ... | ... | @@ -296,6 +320,10 @@ export default { |
| 296 | 320 | storeTags: [], |
| 297 | 321 | businessHours: '', |
| 298 | 322 | trafficTips: '', |
| 323 | + attendanceCheckFence: 0, | |
| 324 | + attendanceCheckWifi: 0, | |
| 325 | + attendanceWifiPairList: [], | |
| 326 | + attendanceWifiVerifyPair: 0, | |
| 299 | 327 | }, |
| 300 | 328 | rules: { |
| 301 | 329 | mdbm: [{ required: true, message: '请输入门店编码', trigger: 'blur' }], |
| ... | ... | @@ -383,6 +411,14 @@ export default { |
| 383 | 411 | this.dataForm.storeDescription = raw.storeDescription || ''; |
| 384 | 412 | this.dataForm.businessHours = raw.businessHours || ''; |
| 385 | 413 | this.dataForm.trafficTips = raw.trafficTips || ''; |
| 414 | + const acf = raw.attendanceCheckFence != null ? Number(raw.attendanceCheckFence) : (raw.AttendanceCheckFence != null ? Number(raw.AttendanceCheckFence) : null); | |
| 415 | + const acw = raw.attendanceCheckWifi != null ? Number(raw.attendanceCheckWifi) : (raw.AttendanceCheckWifi != null ? Number(raw.AttendanceCheckWifi) : null); | |
| 416 | + this.dataForm.attendanceCheckFence = acf != null ? acf : 1; | |
| 417 | + this.dataForm.attendanceCheckWifi = acw != null ? acw : 0; | |
| 418 | + const avp = raw.attendanceWifiVerifyPair != null ? Number(raw.attendanceWifiVerifyPair) : (raw.AttendanceWifiVerifyPair != null ? Number(raw.AttendanceWifiVerifyPair) : null); | |
| 419 | + this.dataForm.attendanceWifiVerifyPair = avp != null ? avp : 0; | |
| 420 | + const apRaw = raw.attendanceWifiPairs != null ? raw.attendanceWifiPairs : raw.AttendanceWifiPairs; | |
| 421 | + this.dataForm.attendanceWifiPairList = this.parseWifiPairList(apRaw); | |
| 386 | 422 | }); |
| 387 | 423 | } else { |
| 388 | 424 | // 新建时重置经纬度与围栏 |
| ... | ... | @@ -394,10 +430,38 @@ export default { |
| 394 | 430 | this.dataForm.storeTags = []; |
| 395 | 431 | this.dataForm.businessHours = ''; |
| 396 | 432 | this.dataForm.trafficTips = ''; |
| 433 | + this.dataForm.attendanceCheckFence = 0; | |
| 434 | + this.dataForm.attendanceCheckWifi = 0; | |
| 435 | + this.dataForm.attendanceWifiPairList = []; | |
| 436 | + this.dataForm.attendanceWifiVerifyPair = 0; | |
| 397 | 437 | } |
| 398 | 438 | }) |
| 399 | 439 | }, |
| 400 | 440 | |
| 441 | + addWifiPair() { | |
| 442 | + if (this.isDetail) return; | |
| 443 | + this.dataForm.attendanceWifiPairList.push({ ssid: '', bssid: '' }); | |
| 444 | + }, | |
| 445 | + | |
| 446 | + removeWifiPair(index) { | |
| 447 | + if (this.isDetail) return; | |
| 448 | + this.dataForm.attendanceWifiPairList.splice(index, 1); | |
| 449 | + }, | |
| 450 | + | |
| 451 | + parseWifiPairList(value) { | |
| 452 | + const arr = this.parseJsonArray(value); | |
| 453 | + if (!arr.length) return []; | |
| 454 | + return arr.map((item) => { | |
| 455 | + if (item && typeof item === 'object') { | |
| 456 | + return { | |
| 457 | + ssid: (item.ssid != null ? String(item.ssid) : (item.Ssid != null ? String(item.Ssid) : '')).trim(), | |
| 458 | + bssid: (item.bssid != null ? String(item.bssid) : (item.Bssid != null ? String(item.Bssid) : '')).trim() | |
| 459 | + }; | |
| 460 | + } | |
| 461 | + return { ssid: '', bssid: '' }; | |
| 462 | + }).filter((p) => p.ssid || p.bssid); | |
| 463 | + }, | |
| 464 | + | |
| 401 | 465 | parseJsonArray(value) { |
| 402 | 466 | if (!value) return []; |
| 403 | 467 | if (Array.isArray(value)) return value; |
| ... | ... | @@ -810,6 +874,16 @@ export default { |
| 810 | 874 | dataFormSubmit() { |
| 811 | 875 | this.$refs['elForm'].validate((valid) => { |
| 812 | 876 | if (!valid) return; |
| 877 | + if (Number(this.dataForm.attendanceCheckWifi) === 1) { | |
| 878 | + const pairs = (this.dataForm.attendanceWifiPairList || []).map((r) => ({ | |
| 879 | + ssid: (r.ssid || '').trim(), | |
| 880 | + bssid: (r.bssid || '').trim() | |
| 881 | + })).filter((p) => p.ssid || p.bssid); | |
| 882 | + if (!pairs.length) { | |
| 883 | + this.$message.warning('开启 Wi-Fi 打卡时请至少添加一行 Wi-Fi 白名单'); | |
| 884 | + return; | |
| 885 | + } | |
| 886 | + } | |
| 813 | 887 | const isNew = !this.dataForm.id; |
| 814 | 888 | // 提交前:fencePolygons 若为数组则序列化为 JSON 字符串,与后端约定一致 |
| 815 | 889 | const submitData = { ...this.dataForm }; |
| ... | ... | @@ -822,6 +896,13 @@ export default { |
| 822 | 896 | if (Array.isArray(submitData.storeTags)) { |
| 823 | 897 | submitData.storeTags = JSON.stringify(submitData.storeTags); |
| 824 | 898 | } |
| 899 | + delete submitData.attendanceWifiPairList; | |
| 900 | + const pairRows = (this.dataForm.attendanceWifiPairList || []).map((r) => ({ | |
| 901 | + ssid: (r.ssid || '').trim(), | |
| 902 | + bssid: (r.bssid || '').trim() | |
| 903 | + })).filter((p) => p.ssid || p.bssid); | |
| 904 | + submitData.attendanceWifiPairs = JSON.stringify(pairRows); | |
| 905 | + submitData.attendanceWifiVerifyPair = this.dataForm.attendanceWifiVerifyPair != null ? Number(this.dataForm.attendanceWifiVerifyPair) : 0; | |
| 825 | 906 | request({ |
| 826 | 907 | url: isNew ? '/api/Extend/LqMdxx' : `/api/Extend/LqMdxx/${this.dataForm.id}`, |
| 827 | 908 | method: isNew ? 'POST' : 'PUT', |
| ... | ... | @@ -844,6 +925,65 @@ export default { |
| 844 | 925 | </script> |
| 845 | 926 | |
| 846 | 927 | <style lang="scss" scoped> |
| 928 | +.att-punch-check-row { | |
| 929 | + display: flex; | |
| 930 | + flex-wrap: wrap; | |
| 931 | + align-items: center; | |
| 932 | + gap: 16px 24px; | |
| 933 | +} | |
| 934 | + | |
| 935 | +.att-punch-hint { | |
| 936 | + margin-top: 8px; | |
| 937 | + font-size: 12px; | |
| 938 | + color: #909399; | |
| 939 | + line-height: 1.5; | |
| 940 | +} | |
| 941 | + | |
| 942 | +.wifi-verify-hint { | |
| 943 | + margin-top: 6px; | |
| 944 | +} | |
| 945 | + | |
| 946 | +.wifi-pair-toolbar { | |
| 947 | + margin-bottom: 10px; | |
| 948 | +} | |
| 949 | + | |
| 950 | +.wifi-pair-table { | |
| 951 | + width: 100%; | |
| 952 | + border: 1px solid #ebeef5; | |
| 953 | + border-radius: 4px; | |
| 954 | + overflow: hidden; | |
| 955 | +} | |
| 956 | + | |
| 957 | +.wifi-pair-head { | |
| 958 | + display: grid; | |
| 959 | + grid-template-columns: 1fr 1fr 56px; | |
| 960 | + gap: 8px; | |
| 961 | + align-items: center; | |
| 962 | + padding: 8px 10px; | |
| 963 | + background: #f5f7fa; | |
| 964 | + font-size: 12px; | |
| 965 | + color: #606266; | |
| 966 | + font-weight: 500; | |
| 967 | +} | |
| 968 | + | |
| 969 | +.wifi-pair-row { | |
| 970 | + display: grid; | |
| 971 | + grid-template-columns: 1fr 1fr 56px; | |
| 972 | + gap: 8px; | |
| 973 | + align-items: center; | |
| 974 | + padding: 8px 10px; | |
| 975 | + border-top: 1px solid #ebeef5; | |
| 976 | +} | |
| 977 | + | |
| 978 | +.wifi-pair-col-act { | |
| 979 | + text-align: right; | |
| 980 | +} | |
| 981 | + | |
| 982 | +.wifi-pair-del { | |
| 983 | + color: #f56c6c; | |
| 984 | + padding: 0; | |
| 985 | +} | |
| 986 | + | |
| 847 | 987 | .fence-status-bar { |
| 848 | 988 | display: flex; |
| 849 | 989 | align-items: center; |
| ... | ... | @@ -953,6 +1093,7 @@ export default { |
| 953 | 1093 | } |
| 954 | 1094 | |
| 955 | 1095 | .store-tags-editor { |
| 1096 | + | |
| 956 | 1097 | .input-new-tag, |
| 957 | 1098 | .tag-action-btn { |
| 958 | 1099 | width: 100%; | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqAttendanceRecord/LqAttendanceRecordPunchInput.cs
| ... | ... | @@ -61,5 +61,17 @@ namespace NCC.Extend.Entitys.Dto.LqAttendanceRecord |
| 61 | 61 | /// </summary> |
| 62 | 62 | [Display(Name = "备注")] |
| 63 | 63 | public string Remark { get; set; } |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// 当前连接 Wi-Fi 的 SSID(小程序上报,用于门店 Wi-Fi 打卡校验) | |
| 67 | + /// </summary> | |
| 68 | + [Display(Name = "Wi-Fi SSID")] | |
| 69 | + public string WifiSsid { get; set; } | |
| 70 | + | |
| 71 | + /// <summary> | |
| 72 | + /// 当前连接 Wi-Fi 的 BSSID(可选) | |
| 73 | + /// </summary> | |
| 74 | + [Display(Name = "Wi-Fi BSSID")] | |
| 75 | + public string WifiBssid { get; set; } | |
| 64 | 76 | } |
| 65 | 77 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMdxx/LqMdxxCrInput.cs
| ... | ... | @@ -59,6 +59,26 @@ namespace NCC.Extend.Entitys.Dto.LqMdxx |
| 59 | 59 | public string fencePolygons { get; set; } |
| 60 | 60 | |
| 61 | 61 | /// <summary> |
| 62 | + /// 正常打卡是否启用电子围栏校验(1-是,0-否;null 沿用后端默认) | |
| 63 | + /// </summary> | |
| 64 | + public int? attendanceCheckFence { get; set; } | |
| 65 | + | |
| 66 | + /// <summary> | |
| 67 | + /// 正常打卡是否启用 Wi-Fi 校验(1-是,0-否) | |
| 68 | + /// </summary> | |
| 69 | + public int? attendanceCheckWifi { get; set; } | |
| 70 | + | |
| 71 | + /// <summary> | |
| 72 | + /// Wi-Fi 成对配置(SSID+BSSID),JSON 数组字符串,如 [{"ssid":"x","bssid":"aa:bb:..."}] | |
| 73 | + /// </summary> | |
| 74 | + public string attendanceWifiPairs { get; set; } | |
| 75 | + | |
| 76 | + /// <summary> | |
| 77 | + /// 是否校验 SSID 与 BSSID 对应(1-是,0-否) | |
| 78 | + /// </summary> | |
| 79 | + public int? attendanceWifiVerifyPair { get; set; } | |
| 80 | + | |
| 81 | + /// <summary> | |
| 62 | 82 | /// 姓名 |
| 63 | 83 | /// </summary> |
| 64 | 84 | public string xm { get; set; } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMdxx/LqMdxxInfoOutput.cs
| ... | ... | @@ -61,6 +61,26 @@ namespace NCC.Extend.Entitys.Dto.LqMdxx |
| 61 | 61 | public string fencePolygons { get; set; } |
| 62 | 62 | |
| 63 | 63 | /// <summary> |
| 64 | + /// 正常打卡是否启用电子围栏校验(1-是,0-否) | |
| 65 | + /// </summary> | |
| 66 | + public int? attendanceCheckFence { get; set; } | |
| 67 | + | |
| 68 | + /// <summary> | |
| 69 | + /// 正常打卡是否启用 Wi-Fi 校验(1-是,0-否) | |
| 70 | + /// </summary> | |
| 71 | + public int? attendanceCheckWifi { get; set; } | |
| 72 | + | |
| 73 | + /// <summary> | |
| 74 | + /// Wi-Fi 成对配置(SSID+BSSID),JSON 数组字符串 | |
| 75 | + /// </summary> | |
| 76 | + public string attendanceWifiPairs { get; set; } | |
| 77 | + | |
| 78 | + /// <summary> | |
| 79 | + /// 是否校验 SSID 与 BSSID 对应(1-是,0-否) | |
| 80 | + /// </summary> | |
| 81 | + public int? attendanceWifiVerifyPair { get; set; } | |
| 82 | + | |
| 83 | + /// <summary> | |
| 64 | 84 | /// 姓名 |
| 65 | 85 | /// </summary> |
| 66 | 86 | public string xm { get; set; } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_mdxx/LqMdxxEntity.cs
| ... | ... | @@ -72,6 +72,30 @@ namespace NCC.Extend.Entitys.lq_mdxx |
| 72 | 72 | public string FencePolygons { get; set; } |
| 73 | 73 | |
| 74 | 74 | /// <summary> |
| 75 | + /// 正常打卡是否启用电子围栏校验(1-是,0-否;null 表示沿用旧逻辑:已配置围栏则校验) | |
| 76 | + /// </summary> | |
| 77 | + [SugarColumn(ColumnName = "F_AttendanceCheckFence")] | |
| 78 | + public int? AttendanceCheckFence { get; set; } | |
| 79 | + | |
| 80 | + /// <summary> | |
| 81 | + /// 正常打卡是否启用 Wi-Fi(成对 SSID+BSSID)校验(1-是,0-否;null 视为 0) | |
| 82 | + /// </summary> | |
| 83 | + [SugarColumn(ColumnName = "F_AttendanceCheckWifi")] | |
| 84 | + public int? AttendanceCheckWifi { get; set; } | |
| 85 | + | |
| 86 | + /// <summary> | |
| 87 | + /// 允许打卡的 Wi-Fi 成对配置,JSON 数组,如 [{"ssid":"门店5G","bssid":"aa:bb:cc:dd:ee:ff"}] | |
| 88 | + /// </summary> | |
| 89 | + [SugarColumn(ColumnName = "F_AttendanceWifiPairs", ColumnDataType = "varchar(4000)")] | |
| 90 | + public string AttendanceWifiPairs { get; set; } | |
| 91 | + | |
| 92 | + /// <summary> | |
| 93 | + /// 是否校验 SSID 与 BSSID 为同一 AP(1-是:有 BSSID 时必须整对命中;无 BSSID 时需同时在围栏内且 SSID 命中;0-否:命中任一条的 SSID 或 BSSID 即可) | |
| 94 | + /// </summary> | |
| 95 | + [SugarColumn(ColumnName = "F_AttendanceWifiVerifyPair")] | |
| 96 | + public int? AttendanceWifiVerifyPair { get; set; } | |
| 97 | + | |
| 98 | + /// <summary> | |
| 75 | 99 | /// 姓名 |
| 76 | 100 | /// </summary> |
| 77 | 101 | [SugarColumn(ColumnName = "xm")] | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/wform_leave_cancel_apply/WformLeaveCancelApplyEntityLite.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using NCC.Common.Const; | |
| 3 | +using SqlSugar; | |
| 4 | + | |
| 5 | +namespace NCC.Extend.Entitys.wform_leave_cancel_apply | |
| 6 | +{ | |
| 7 | + /// <summary> | |
| 8 | + /// 销假申请单(精简映射:考勤详情关联流程) | |
| 9 | + /// </summary> | |
| 10 | + [SugarTable("WFORM_LEAVE_CANCEL_APPLY")] | |
| 11 | + [Tenant(ClaimConst.TENANT_ID)] | |
| 12 | + public class WformLeaveCancelApplyEntityLite | |
| 13 | + { | |
| 14 | + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)] | |
| 15 | + public string Id { get; set; } | |
| 16 | + | |
| 17 | + [SugarColumn(ColumnName = "F_FLOWID")] | |
| 18 | + public string FlowId { get; set; } | |
| 19 | + | |
| 20 | + [SugarColumn(ColumnName = "F_BILLNO")] | |
| 21 | + public string BillNo { get; set; } | |
| 22 | + | |
| 23 | + /// <summary> | |
| 24 | + /// 待销假的考勤记录主键(lq_attendance_record.F_Id) | |
| 25 | + /// </summary> | |
| 26 | + [SugarColumn(ColumnName = "F_ATTENDANCERECORDID")] | |
| 27 | + public string AttendanceRecordId { get; set; } | |
| 28 | + | |
| 29 | + [SugarColumn(ColumnName = "F_APPLYDATE")] | |
| 30 | + public DateTime? ApplyDate { get; set; } | |
| 31 | + | |
| 32 | + [SugarColumn(ColumnName = "F_CANCELREASON")] | |
| 33 | + public string CancelReason { get; set; } | |
| 34 | + } | |
| 35 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Enum/AttendanceRecordStatusEnum.cs
| ... | ... | @@ -47,6 +47,12 @@ namespace NCC.Extend.Entitys.Enum |
| 47 | 47 | /// 旷工 |
| 48 | 48 | /// </summary> |
| 49 | 49 | [Description("旷工")] |
| 50 | - 旷工 = 7 | |
| 50 | + 旷工 = 7, | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 考勤日晚于当前日期、尚无打卡数据(界面展示「暂无打卡」,不计入缺卡类统计) | |
| 54 | + /// </summary> | |
| 55 | + [Description("暂无打卡")] | |
| 56 | + 待考勤 = 8 | |
| 51 | 57 | } |
| 52 | 58 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqAttendanceRecordService.cs
| ... | ... | @@ -27,6 +27,7 @@ using NCC.Extend.Entitys.lq_attendance_missing_card_rule; |
| 27 | 27 | using NCC.Extend.Entitys.lq_attendance_record; |
| 28 | 28 | using NCC.Extend.Entitys.lq_attendance_setting; |
| 29 | 29 | using NCC.Extend.Entitys.lq_mdxx; |
| 30 | +using NCC.Extend.Entitys.wform_leave_cancel_apply; | |
| 30 | 31 | using NCC.Extend.Entitys.wform_leaveapply; |
| 31 | 32 | using NCC.Extend.Entitys.flow_task; |
| 32 | 33 | using NCC.Extend.Entitys.flow_engine; |
| ... | ... | @@ -96,7 +97,8 @@ namespace NCC.Extend |
| 96 | 97 | /// 获取月度考勤记录矩阵 |
| 97 | 98 | /// </summary> |
| 98 | 99 | /// <remarks> |
| 99 | - /// 按统计月份返回员工每日考勤状态;含打卡、补卡及请假流程同步后的「请假/病假」记录。 | |
| 100 | + /// 按统计月份返回员工每日考勤状态;列表包含当前筛选条件下所有在职员工(含当月尚无考勤记录者), | |
| 101 | + /// 含打卡、补卡及请假流程同步后的「请假/病假」记录。 | |
| 100 | 102 | /// </remarks> |
| 101 | 103 | /// <param name="input">查询参数</param> |
| 102 | 104 | /// <returns>月度考勤记录矩阵</returns> |
| ... | ... | @@ -227,59 +229,92 @@ namespace NCC.Extend |
| 227 | 229 | |
| 228 | 230 | var monthStart = new DateTime(monthDate.Year, monthDate.Month, 1); |
| 229 | 231 | var monthEnd = monthStart.AddMonths(1); |
| 230 | - var keyword = input.Keyword?.Trim(); | |
| 231 | 232 | |
| 232 | - var matchedRecords = await _db.Queryable<LqAttendanceRecordEntity>() | |
| 233 | - .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 234 | - .Where(x => x.AttendanceDate >= monthStart && x.AttendanceDate < monthEnd) | |
| 235 | - .WhereIF(!string.IsNullOrWhiteSpace(input.AttendanceGroupId), x => x.AttendanceGroupId == input.AttendanceGroupId) | |
| 236 | - .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => | |
| 237 | - x.EmployeeName.Contains(keyword) || | |
| 238 | - x.StoreName.Contains(keyword) || | |
| 239 | - x.AttendanceGroupName.Contains(keyword)) | |
| 240 | - .ToListAsync(); | |
| 233 | + var eligibleUsers = await QueryMonthReportEligibleUsersAsync(input); | |
| 234 | + var eligibleUserIds = eligibleUsers.Select(x => x.UserId).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(); | |
| 235 | + | |
| 236 | + var matchedRecords = eligibleUserIds.Count == 0 | |
| 237 | + ? new List<LqAttendanceRecordEntity>() | |
| 238 | + : await _db.Queryable<LqAttendanceRecordEntity>() | |
| 239 | + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 240 | + .Where(x => x.AttendanceDate >= monthStart && x.AttendanceDate < monthEnd) | |
| 241 | + .Where(x => eligibleUserIds.Contains(x.UserId)) | |
| 242 | + .ToListAsync(); | |
| 241 | 243 | |
| 242 | 244 | matchedRecords = await RecalculateRecordStatusesAsync(matchedRecords); |
| 243 | 245 | |
| 244 | - var summary = BuildSummary(matchedRecords); | |
| 245 | - var groupedEmployees = matchedRecords | |
| 246 | - .GroupBy(x => new | |
| 246 | + var recordsByUserId = matchedRecords | |
| 247 | + .GroupBy(x => x.UserId) | |
| 248 | + .ToDictionary(x => x.Key, x => x.ToList()); | |
| 249 | + | |
| 250 | + var summary = BuildSummary(matchedRecords, eligibleUsers.Count); | |
| 251 | + var groupedEmployees = eligibleUsers | |
| 252 | + .Select(u => | |
| 247 | 253 | { |
| 248 | - x.UserId, | |
| 249 | - x.EmployeeName, | |
| 250 | - x.StoreId, | |
| 251 | - x.StoreName, | |
| 252 | - x.AttendanceGroupId, | |
| 253 | - x.AttendanceGroupName | |
| 254 | - }) | |
| 255 | - .Select(g => new LqAttendanceRecordRowOutput | |
| 256 | - { | |
| 257 | - userId = g.Key.UserId, | |
| 258 | - employeeName = g.Key.EmployeeName, | |
| 259 | - storeId = g.Key.StoreId, | |
| 260 | - storeName = g.Key.StoreName, | |
| 261 | - attendanceGroupId = g.Key.AttendanceGroupId, | |
| 262 | - groupName = g.Key.AttendanceGroupName, | |
| 263 | - monthStats = new LqAttendanceRecordMonthStatOutput | |
| 254 | + recordsByUserId.TryGetValue(u.UserId, out var userRecords); | |
| 255 | + userRecords ??= new List<LqAttendanceRecordEntity>(); | |
| 256 | + return new LqAttendanceRecordRowOutput | |
| 264 | 257 | { |
| 265 | - normal = g.Count(x => x.Status == (int)AttendanceRecordStatusEnum.正常), | |
| 266 | - late = g.Count(x => x.Status == (int)AttendanceRecordStatusEnum.迟到), | |
| 267 | - abnormal = g.Count(x => IsAbnormalStatus(x.Status)) | |
| 268 | - }, | |
| 269 | - dayRecords = g.GroupBy(x => x.AttendanceDate.Date) | |
| 270 | - .Select(dayGroup => BuildDayItem(dayGroup.OrderByDescending(x => x.UpdateTime ?? x.CreateTime ?? DateTime.MinValue).First())) | |
| 271 | - .OrderBy(x => x.date) | |
| 272 | - .ToList() | |
| 258 | + userId = u.UserId, | |
| 259 | + employeeName = u.EmployeeName, | |
| 260 | + storeId = u.StoreId, | |
| 261 | + storeName = u.StoreName, | |
| 262 | + attendanceGroupId = u.AttendanceGroupId, | |
| 263 | + groupName = u.GroupName, | |
| 264 | + monthStats = new LqAttendanceRecordMonthStatOutput | |
| 265 | + { | |
| 266 | + normal = userRecords.Count(x => x.Status == (int)AttendanceRecordStatusEnum.正常), | |
| 267 | + late = userRecords.Count(x => x.Status == (int)AttendanceRecordStatusEnum.迟到), | |
| 268 | + abnormal = userRecords.Count(x => IsAbnormalStatus(x.Status)) | |
| 269 | + }, | |
| 270 | + dayRecords = userRecords | |
| 271 | + .GroupBy(x => x.AttendanceDate.Date) | |
| 272 | + .Select(dayGroup => BuildDayItem(dayGroup.OrderByDescending(x => x.UpdateTime ?? x.CreateTime ?? DateTime.MinValue).First())) | |
| 273 | + .OrderBy(x => x.date) | |
| 274 | + .ToList() | |
| 275 | + }; | |
| 273 | 276 | }) |
| 274 | - .OrderBy(x => x.storeName) | |
| 275 | - .ThenBy(x => x.groupName) | |
| 276 | - .ThenBy(x => x.employeeName) | |
| 277 | 277 | .ToList(); |
| 278 | 278 | |
| 279 | 279 | return (groupedEmployees, summary); |
| 280 | 280 | } |
| 281 | 281 | |
| 282 | 282 | /// <summary> |
| 283 | + /// 月度矩阵:当前筛选条件下的在职员工(主数据),与当月是否有考勤行无关。 | |
| 284 | + /// </summary> | |
| 285 | + private async Task<List<MonthReportUserLine>> QueryMonthReportEligibleUsersAsync(LqAttendanceRecordMonthQueryInput input) | |
| 286 | + { | |
| 287 | + var keyword = input.Keyword?.Trim(); | |
| 288 | + var lines = await _db.Queryable<UserEntity, LqMdxxEntity, LqAttendanceGroupEntity>((u, s, g) => new JoinQueryInfos( | |
| 289 | + JoinType.Left, s.Id == u.Mdid, | |
| 290 | + JoinType.Left, g.Id == u.AttendanceGroupId)) | |
| 291 | + .Where((u, s, g) => u.DeleteMark == null && SqlFunc.IsNull(u.IsOnJob, 1) == 1) | |
| 292 | + .WhereIF(!string.IsNullOrWhiteSpace(input.AttendanceGroupId), | |
| 293 | + (u, s, g) => u.AttendanceGroupId == input.AttendanceGroupId) | |
| 294 | + .WhereIF(!string.IsNullOrWhiteSpace(keyword), (u, s, g) => | |
| 295 | + SqlFunc.Contains(SqlFunc.IsNull(u.RealName, ""), keyword) || | |
| 296 | + SqlFunc.Contains(SqlFunc.IsNull(u.Account, ""), keyword) || | |
| 297 | + SqlFunc.Contains(SqlFunc.IsNull(s.Dm, ""), keyword) || | |
| 298 | + SqlFunc.Contains(SqlFunc.IsNull(g.GroupName, ""), keyword)) | |
| 299 | + .Select((u, s, g) => new MonthReportUserLine | |
| 300 | + { | |
| 301 | + UserId = u.Id, | |
| 302 | + EmployeeName = u.RealName, | |
| 303 | + StoreId = u.Mdid, | |
| 304 | + StoreName = s.Dm, | |
| 305 | + AttendanceGroupId = u.AttendanceGroupId, | |
| 306 | + GroupName = g.GroupName | |
| 307 | + }) | |
| 308 | + .ToListAsync(); | |
| 309 | + | |
| 310 | + return lines | |
| 311 | + .OrderBy(x => x.StoreName ?? "") | |
| 312 | + .ThenBy(x => x.GroupName ?? "") | |
| 313 | + .ThenBy(x => x.EmployeeName ?? "") | |
| 314 | + .ToList(); | |
| 315 | + } | |
| 316 | + | |
| 317 | + /// <summary> | |
| 283 | 318 | /// 提交打卡 |
| 284 | 319 | /// </summary> |
| 285 | 320 | /// <remarks> |
| ... | ... | @@ -327,6 +362,19 @@ namespace NCC.Extend |
| 327 | 362 | |
| 328 | 363 | var punchDirection = (AttendancePunchDirectionEnum)input.PunchDirection; |
| 329 | 364 | var punchType = input.PunchType; |
| 365 | + if (punchType == (int)AttendancePunchTypeEnum.正常上班) | |
| 366 | + { | |
| 367 | + if ((store.AttendanceCheckWifi ?? 0) == 1 && ParseStoreWifiPairs(store).Count == 0) | |
| 368 | + { | |
| 369 | + throw NCCException.Oh("门店已开启 Wi-Fi 打卡但未配置网络白名单,请联系管理员在门店资料中维护"); | |
| 370 | + } | |
| 371 | + | |
| 372 | + if (!IsNormalPunchLocationSatisfied(store, input.Longitude, input.Latitude, input.WifiSsid, input.WifiBssid)) | |
| 373 | + { | |
| 374 | + throw NCCException.Oh("未满足门店打卡要求(需在打卡范围内或连接门店指定 Wi-Fi/路由器),可改用外勤打卡"); | |
| 375 | + } | |
| 376 | + } | |
| 377 | + | |
| 330 | 378 | var fenceValid = EvaluateFenceValid(store, input.Longitude, input.Latitude); |
| 331 | 379 | ApplyPunch(record, punchDirection, punchType, punchTime, input.Longitude, input.Latitude, input.Address, input.PhotoUrl, fenceValid); |
| 332 | 380 | |
| ... | ... | @@ -374,18 +422,33 @@ namespace NCC.Extend |
| 374 | 422 | var record = await GetAttendanceRecordAsync(input); |
| 375 | 423 | if (record == null) |
| 376 | 424 | { |
| 425 | + if (!string.IsNullOrWhiteSpace(input.UserId) && !string.IsNullOrWhiteSpace(input.AttendanceDate)) | |
| 426 | + { | |
| 427 | + return await BuildEmptyAttendanceDetailAsync(input); | |
| 428 | + } | |
| 429 | + | |
| 377 | 430 | throw NCCException.Oh("未找到对应的打卡记录"); |
| 378 | 431 | } |
| 379 | 432 | |
| 433 | + var statusBeforeDetailRecalc = record.Status; | |
| 380 | 434 | var recalculated = await RecalculateRecordStatusesAsync(new List<LqAttendanceRecordEntity> { record }); |
| 381 | 435 | record = recalculated.FirstOrDefault() ?? record; |
| 436 | + // 重算后状态变化时落库(含:备注已销假但库中仍为请假、待考勤已到期应转为缺卡等) | |
| 437 | + if (record.Status != statusBeforeDetailRecalc) | |
| 438 | + { | |
| 439 | + await SaveAttendanceRecordAsync(record); | |
| 440 | + } | |
| 441 | + | |
| 382 | 442 | var group = await GetAttendanceGroupAsync(record.AttendanceGroupId, false); |
| 383 | 443 | |
| 384 | 444 | var relatedWorkflows = ParseRelatedWorkflows(record.RelatedWorkflowsJson); |
| 385 | 445 | |
| 446 | + // 销假后考勤状态可能会从“请假/病假”恢复到“正常/缺卡”等, | |
| 447 | + // 但 record.Remark 仍可能保留“请假审批通过:...;销假:...”信息。 | |
| 448 | + // 因此解析 leaveType/billNo 时不再强依赖 isLeaveStatus。 | |
| 386 | 449 | var isLeaveStatus = record.Status == (int)AttendanceRecordStatusEnum.请假 |
| 387 | 450 | || record.Status == (int)AttendanceRecordStatusEnum.病假; |
| 388 | - if (!relatedWorkflows.Any(x => x.type == "请假") && isLeaveStatus | |
| 451 | + if (!relatedWorkflows.Any(x => x.type == "请假") | |
| 389 | 452 | && TryParseLeaveApplyFromSyncRemark(record.Remark, out var parsedLeaveType, out var parsedBillNo) |
| 390 | 453 | && !string.IsNullOrWhiteSpace(parsedBillNo)) |
| 391 | 454 | { |
| ... | ... | @@ -406,6 +469,31 @@ namespace NCC.Extend |
| 406 | 469 | }); |
| 407 | 470 | } |
| 408 | 471 | |
| 472 | + var cancelApplyRows = await _db.Queryable<WformLeaveCancelApplyEntityLite>() | |
| 473 | + .Where(x => x.AttendanceRecordId == record.Id) | |
| 474 | + .OrderBy(x => x.ApplyDate, OrderByType.Desc) | |
| 475 | + .ToListAsync(); | |
| 476 | + foreach (var cancelApply in cancelApplyRows) | |
| 477 | + { | |
| 478 | + if (cancelApply == null || string.IsNullOrWhiteSpace(cancelApply.Id)) | |
| 479 | + { | |
| 480 | + continue; | |
| 481 | + } | |
| 482 | + | |
| 483 | + if (relatedWorkflows.Any(x => x.type == "销假" && x.id == cancelApply.Id)) | |
| 484 | + { | |
| 485 | + continue; | |
| 486 | + } | |
| 487 | + | |
| 488 | + relatedWorkflows.Add(new RelatedWorkflowItem | |
| 489 | + { | |
| 490 | + type = "销假", | |
| 491 | + id = cancelApply.Id, | |
| 492 | + billNo = cancelApply.BillNo, | |
| 493 | + remark = SafeTrim(cancelApply.CancelReason) | |
| 494 | + }); | |
| 495 | + } | |
| 496 | + | |
| 409 | 497 | if (!string.IsNullOrWhiteSpace(record.SupplementWorkflowId) && !relatedWorkflows.Any(x => x.type == "补卡")) |
| 410 | 498 | { |
| 411 | 499 | relatedWorkflows.Add(new RelatedWorkflowItem |
| ... | ... | @@ -1060,7 +1148,9 @@ namespace NCC.Extend |
| 1060 | 1148 | } |
| 1061 | 1149 | else if (!record.PunchInTime.HasValue && !record.PunchOutTime.HasValue) |
| 1062 | 1150 | { |
| 1063 | - targetStatus = (int)AttendanceRecordStatusEnum.缺卡; | |
| 1151 | + targetStatus = attendanceDate > today | |
| 1152 | + ? (int)AttendanceRecordStatusEnum.待考勤 | |
| 1153 | + : (int)AttendanceRecordStatusEnum.缺卡; | |
| 1064 | 1154 | } |
| 1065 | 1155 | else |
| 1066 | 1156 | { |
| ... | ... | @@ -1472,11 +1562,11 @@ namespace NCC.Extend |
| 1472 | 1562 | }; |
| 1473 | 1563 | } |
| 1474 | 1564 | |
| 1475 | - private static dynamic BuildSummary(List<LqAttendanceRecordEntity> matchedRecords) | |
| 1565 | + private static dynamic BuildSummary(List<LqAttendanceRecordEntity> matchedRecords, int displayedEmployeeCount) | |
| 1476 | 1566 | { |
| 1477 | 1567 | return new |
| 1478 | 1568 | { |
| 1479 | - employeeCount = matchedRecords.Select(x => x.UserId).Distinct().Count(), | |
| 1569 | + employeeCount = displayedEmployeeCount, | |
| 1480 | 1570 | normalCount = matchedRecords.Count(x => x.Status == (int)AttendanceRecordStatusEnum.正常), |
| 1481 | 1571 | lateCount = matchedRecords.Count(x => x.Status == (int)AttendanceRecordStatusEnum.迟到), |
| 1482 | 1572 | leaveAndMissingCount = matchedRecords.Count(x => x.Status == (int)AttendanceRecordStatusEnum.请假 |
| ... | ... | @@ -1667,9 +1757,28 @@ namespace NCC.Extend |
| 1667 | 1757 | |
| 1668 | 1758 | private async Task<LqAttendanceRecordEntity> GetAttendanceRecordByUserDateAsync(string userId, DateTime attendanceDate) |
| 1669 | 1759 | { |
| 1670 | - return await _db.Queryable<LqAttendanceRecordEntity>() | |
| 1671 | - .Where(x => x.UserId == userId && x.AttendanceDate >= attendanceDate.Date && x.AttendanceDate < attendanceDate.Date.AddDays(1) && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1672 | - .FirstAsync(); | |
| 1760 | + var list = await _db.Queryable<LqAttendanceRecordEntity>() | |
| 1761 | + .Where(x => x.UserId == userId | |
| 1762 | + && x.AttendanceDate >= attendanceDate.Date | |
| 1763 | + && x.AttendanceDate < attendanceDate.Date.AddDays(1) | |
| 1764 | + && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1765 | + .Take(1) | |
| 1766 | + .ToListAsync(); | |
| 1767 | + return list.FirstOrDefault(); | |
| 1768 | + } | |
| 1769 | + | |
| 1770 | + /// <summary> | |
| 1771 | + /// 不限制 IsEffective:用于考勤详情兜底展示(例如销假把“未来占位行”置为无效后仍希望看到关联请假/销假历史)。 | |
| 1772 | + /// </summary> | |
| 1773 | + private async Task<LqAttendanceRecordEntity> GetAttendanceRecordByUserDateAnyAsync(string userId, DateTime attendanceDate) | |
| 1774 | + { | |
| 1775 | + var list = await _db.Queryable<LqAttendanceRecordEntity>() | |
| 1776 | + .Where(x => x.UserId == userId | |
| 1777 | + && x.AttendanceDate >= attendanceDate.Date | |
| 1778 | + && x.AttendanceDate < attendanceDate.Date.AddDays(1)) | |
| 1779 | + .Take(1) | |
| 1780 | + .ToListAsync(); | |
| 1781 | + return list.FirstOrDefault(); | |
| 1673 | 1782 | } |
| 1674 | 1783 | |
| 1675 | 1784 | /// <param name="bypassSnapshotPairSkip">为 true 时不过滤「规则快照与全量快照均已存在」的记录(用于历史缺卡批量修正)</param> |
| ... | ... | @@ -1723,31 +1832,60 @@ namespace NCC.Extend |
| 1723 | 1832 | |
| 1724 | 1833 | foreach (var record in records) |
| 1725 | 1834 | { |
| 1726 | - // 流程同步写入的请假/病假(含应休「休假」、年假等):勿按打卡重算覆盖为缺卡/正常,否则详情无法销假、应休/年假额度无法按考勤折算还原 | |
| 1835 | + // 流程同步写入的请假/病假(含应休「休假」、年假等):勿按打卡重算覆盖为缺卡/正常,否则详情无法销假、应休/年假额度无法按考勤折算还原。 | |
| 1836 | + // 备注已含「销假」时不再跳过:否则会出现库中仍为请假/病假但备注已销假、详情与列表长期不一致。 | |
| 1727 | 1837 | if (record.IsManual == 1 |
| 1728 | 1838 | && (record.Status == (int)AttendanceRecordStatusEnum.请假 || record.Status == (int)AttendanceRecordStatusEnum.病假) |
| 1729 | 1839 | && !string.IsNullOrWhiteSpace(record.Remark) |
| 1730 | - && record.Remark.IndexOf("请假审批通过:", StringComparison.Ordinal) >= 0) | |
| 1840 | + && record.Remark.IndexOf("请假审批通过:", StringComparison.Ordinal) >= 0 | |
| 1841 | + && record.Remark.IndexOf("销假", StringComparison.Ordinal) < 0) | |
| 1731 | 1842 | { |
| 1732 | 1843 | continue; |
| 1733 | 1844 | } |
| 1734 | 1845 | |
| 1846 | + var leaveSyncedButCancelledInRemark = !string.IsNullOrWhiteSpace(record.Remark) | |
| 1847 | + && record.Remark.IndexOf("销假", StringComparison.Ordinal) >= 0 | |
| 1848 | + && (record.Status == (int)AttendanceRecordStatusEnum.请假 | |
| 1849 | + || record.Status == (int)AttendanceRecordStatusEnum.病假); | |
| 1850 | + | |
| 1851 | + var attendanceDate = record.AttendanceDate.Date; | |
| 1852 | + var todayDate = DateTime.Today.Date; | |
| 1853 | + // 「待考勤」仅适用于考勤日晚于今日;已到期应重新按打卡情况判定(否则快照非空时会一直被跳过) | |
| 1854 | + var pendingAttendanceExpired = record.Status == (int)AttendanceRecordStatusEnum.待考勤 | |
| 1855 | + && attendanceDate <= todayDate; | |
| 1856 | + // 库中仍是「缺卡」但考勤日晚于今日且无打卡时,应展示为待考勤;否则快照非空会一直跳过重算,永远停留在缺卡 | |
| 1857 | + var futureDayMissingShouldBePending = attendanceDate > todayDate | |
| 1858 | + && record.Status == (int)AttendanceRecordStatusEnum.缺卡 | |
| 1859 | + && !record.PunchInTime.HasValue | |
| 1860 | + && !record.PunchOutTime.HasValue; | |
| 1861 | + | |
| 1735 | 1862 | if (!bypassSnapshotPairSkip |
| 1736 | 1863 | && !string.IsNullOrWhiteSpace(record.RuleSnapshotJson) |
| 1737 | - && !string.IsNullOrWhiteSpace(record.AllRuleSnapshotJson)) | |
| 1864 | + && !string.IsNullOrWhiteSpace(record.AllRuleSnapshotJson) | |
| 1865 | + && !leaveSyncedButCancelledInRemark | |
| 1866 | + && !pendingAttendanceExpired | |
| 1867 | + && !futureDayMissingShouldBePending) | |
| 1738 | 1868 | { |
| 1739 | 1869 | continue; |
| 1740 | 1870 | } |
| 1741 | 1871 | |
| 1742 | - var attendanceDate = record.AttendanceDate.Date; | |
| 1743 | 1872 | var group = ResolveRecordAttendanceGroup(record, userDict, groupDict); |
| 1744 | 1873 | var isHoliday = holidayDates.Contains(attendanceDate); |
| 1745 | 1874 | var isExempt = IsExemptForDate(exemptUsers, record.UserId, attendanceDate); |
| 1875 | + // 备注已销假但状态未落库时,勿把「请假/病假」传入 Resolve(否则会原样保留请假); | |
| 1876 | + // 「待考勤」已到期时同样按正常流程重算为缺卡/正常等; | |
| 1877 | + // 未来日+缺卡无打卡时勿把「缺卡」传入 Resolve(否则会原样保留缺卡)。 | |
| 1878 | + var statusForResolution = record.Status; | |
| 1879 | + if (leaveSyncedButCancelledInRemark || pendingAttendanceExpired || futureDayMissingShouldBePending) | |
| 1880 | + { | |
| 1881 | + statusForResolution = (int)AttendanceRecordStatusEnum.正常; | |
| 1882 | + } | |
| 1883 | + | |
| 1746 | 1884 | var statusContext = ResolveAttendanceStatus( |
| 1747 | 1885 | attendanceDate, |
| 1748 | 1886 | group, |
| 1749 | 1887 | lateRules, |
| 1750 | - record.Status, | |
| 1888 | + statusForResolution, | |
| 1751 | 1889 | record.PunchInTime, |
| 1752 | 1890 | record.PunchOutTime, |
| 1753 | 1891 | null, |
| ... | ... | @@ -1807,9 +1945,19 @@ namespace NCC.Extend |
| 1807 | 1945 | { |
| 1808 | 1946 | if (!string.IsNullOrWhiteSpace(input.Id)) |
| 1809 | 1947 | { |
| 1810 | - return await _db.Queryable<LqAttendanceRecordEntity>() | |
| 1948 | + var effectiveList = await _db.Queryable<LqAttendanceRecordEntity>() | |
| 1811 | 1949 | .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) |
| 1812 | - .FirstAsync(); | |
| 1950 | + .Take(1) | |
| 1951 | + .ToListAsync(); | |
| 1952 | + var effectiveRecord = effectiveList.FirstOrDefault(); | |
| 1953 | + if (effectiveRecord != null) return effectiveRecord; | |
| 1954 | + | |
| 1955 | + // 兜底:允许返回无效行(用于展示请假/销假关联历史) | |
| 1956 | + var anyList = await _db.Queryable<LqAttendanceRecordEntity>() | |
| 1957 | + .Where(x => x.Id == input.Id) | |
| 1958 | + .Take(1) | |
| 1959 | + .ToListAsync(); | |
| 1960 | + return anyList.FirstOrDefault(); | |
| 1813 | 1961 | } |
| 1814 | 1962 | |
| 1815 | 1963 | if (string.IsNullOrWhiteSpace(input.UserId) || string.IsNullOrWhiteSpace(input.AttendanceDate)) |
| ... | ... | @@ -1818,7 +1966,11 @@ namespace NCC.Extend |
| 1818 | 1966 | } |
| 1819 | 1967 | |
| 1820 | 1968 | var attendanceDate = ParseAttendanceDate(input.AttendanceDate); |
| 1821 | - return await GetAttendanceRecordByUserDateAsync(input.UserId, attendanceDate); | |
| 1969 | + var effectiveRecordByDate = await GetAttendanceRecordByUserDateAsync(input.UserId, attendanceDate); | |
| 1970 | + if (effectiveRecordByDate != null) return effectiveRecordByDate; | |
| 1971 | + | |
| 1972 | + // 兜底:允许返回无效行(用于展示请假/销假关联历史) | |
| 1973 | + return await GetAttendanceRecordByUserDateAnyAsync(input.UserId, attendanceDate); | |
| 1822 | 1974 | } |
| 1823 | 1975 | |
| 1824 | 1976 | private static LqAttendanceRecordEntity CreateAttendanceRecord(UserEntity user, LqMdxxEntity store, LqAttendanceGroupEntity group, DateTime attendanceDate, string operatorUserId) |
| ... | ... | @@ -1834,7 +1986,9 @@ namespace NCC.Extend |
| 1834 | 1986 | AttendanceGroupId = group?.Id, |
| 1835 | 1987 | AttendanceGroupName = group?.GroupName, |
| 1836 | 1988 | AttendanceDate = attendanceDate.Date, |
| 1837 | - Status = (int)AttendanceRecordStatusEnum.缺卡, | |
| 1989 | + Status = attendanceDate.Date > DateTime.Today.Date | |
| 1990 | + ? (int)AttendanceRecordStatusEnum.待考勤 | |
| 1991 | + : (int)AttendanceRecordStatusEnum.缺卡, | |
| 1838 | 1992 | LateMinutes = 0, |
| 1839 | 1993 | EarlyLeaveMinutes = 0, |
| 1840 | 1994 | IsManual = 0, |
| ... | ... | @@ -1956,9 +2110,12 @@ namespace NCC.Extend |
| 1956 | 2110 | // 取消流程补卡、销假回退等场景会清空打卡时间;双无打卡不应判为「正常」,与销假后逻辑一致(见 ExecuteCancelLeaveCoreAsync) |
| 1957 | 2111 | if (!punchInTime.HasValue && !punchOutTime.HasValue) |
| 1958 | 2112 | { |
| 2113 | + var noPunchStatus = attendanceDate.Date > DateTime.Today.Date | |
| 2114 | + ? (int)AttendanceRecordStatusEnum.待考勤 | |
| 2115 | + : (int)AttendanceRecordStatusEnum.缺卡; | |
| 1959 | 2116 | return new AttendanceStatusContext |
| 1960 | 2117 | { |
| 1961 | - Status = (int)AttendanceRecordStatusEnum.缺卡, | |
| 2118 | + Status = noPunchStatus, | |
| 1962 | 2119 | LateMinutes = 0, |
| 1963 | 2120 | EarlyLeaveMinutes = 0 |
| 1964 | 2121 | }; |
| ... | ... | @@ -2005,6 +2162,154 @@ namespace NCC.Extend |
| 2005 | 2162 | return lateRules.Any(rule => minutes >= rule.MinMinutes && (!rule.MaxMinutes.HasValue || minutes < rule.MaxMinutes.Value)); |
| 2006 | 2163 | } |
| 2007 | 2164 | |
| 2165 | + private static bool StoreHasFencePolygons(LqMdxxEntity store) => | |
| 2166 | + store != null && !string.IsNullOrWhiteSpace(store.FencePolygons); | |
| 2167 | + | |
| 2168 | + /// <summary> | |
| 2169 | + /// 是否实际启用围栏校验(门店开启且已配置围栏多边形) | |
| 2170 | + /// </summary> | |
| 2171 | + private static bool EffectiveRequireFenceCheck(LqMdxxEntity store) | |
| 2172 | + { | |
| 2173 | + if (!StoreHasFencePolygons(store)) return false; | |
| 2174 | + var f = store.AttendanceCheckFence; | |
| 2175 | + if (!f.HasValue) return true; | |
| 2176 | + return f.Value == 1; | |
| 2177 | + } | |
| 2178 | + | |
| 2179 | + /// <summary> | |
| 2180 | + /// 统一 BSSID 格式:去空白、连字符改冒号、小写;无分隔符的 12 位十六进制转为 aa:bb:cc:dd:ee:ff | |
| 2181 | + /// </summary> | |
| 2182 | + private static string NormalizeAttendanceBssid(string value) | |
| 2183 | + { | |
| 2184 | + if (string.IsNullOrWhiteSpace(value)) return string.Empty; | |
| 2185 | + var s = value.Trim().Replace("-", ":").Replace(" ", string.Empty).ToLowerInvariant(); | |
| 2186 | + if (s.IndexOf(':') < 0 && s.Length == 12) | |
| 2187 | + { | |
| 2188 | + var allHex = true; | |
| 2189 | + foreach (var c in s) | |
| 2190 | + { | |
| 2191 | + if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) | |
| 2192 | + { | |
| 2193 | + allHex = false; | |
| 2194 | + break; | |
| 2195 | + } | |
| 2196 | + } | |
| 2197 | + | |
| 2198 | + if (allHex) | |
| 2199 | + { | |
| 2200 | + return string.Join(":", Enumerable.Range(0, 6).Select(i => s.Substring(i * 2, 2))); | |
| 2201 | + } | |
| 2202 | + } | |
| 2203 | + | |
| 2204 | + return s; | |
| 2205 | + } | |
| 2206 | + | |
| 2207 | + /// <summary> | |
| 2208 | + /// 解析门店 Wi-Fi 成对配置(ssid + bssid) | |
| 2209 | + /// </summary> | |
| 2210 | + private static List<(string Ssid, string Bssid)> ParseStoreWifiPairs(LqMdxxEntity store) | |
| 2211 | + { | |
| 2212 | + var list = new List<(string Ssid, string Bssid)>(); | |
| 2213 | + if (store == null || string.IsNullOrWhiteSpace(store.AttendanceWifiPairs)) return list; | |
| 2214 | + try | |
| 2215 | + { | |
| 2216 | + var token = JToken.Parse(store.AttendanceWifiPairs.Trim()); | |
| 2217 | + if (token is not JArray arr) return list; | |
| 2218 | + foreach (var item in arr) | |
| 2219 | + { | |
| 2220 | + if (item is not JObject jo) continue; | |
| 2221 | + var ssid = jo["ssid"]?.ToString()?.Trim() ?? jo["Ssid"]?.ToString()?.Trim() ?? string.Empty; | |
| 2222 | + var bssid = jo["bssid"]?.ToString()?.Trim() ?? jo["Bssid"]?.ToString()?.Trim() ?? string.Empty; | |
| 2223 | + if (!string.IsNullOrEmpty(ssid) || !string.IsNullOrEmpty(bssid)) | |
| 2224 | + { | |
| 2225 | + list.Add((ssid, bssid)); | |
| 2226 | + } | |
| 2227 | + } | |
| 2228 | + } | |
| 2229 | + catch | |
| 2230 | + { | |
| 2231 | + // ignore invalid json | |
| 2232 | + } | |
| 2233 | + | |
| 2234 | + return list; | |
| 2235 | + } | |
| 2236 | + | |
| 2237 | + /// <summary> | |
| 2238 | + /// 是否启用 Wi-Fi 侧校验(门店勾选 Wi-Fi 打卡即视为需要;白名单是否已配置由 <see cref="IsWifiPunchAllowed"/> 判断) | |
| 2239 | + /// </summary> | |
| 2240 | + /// <remarks> | |
| 2241 | + /// 若此处要求「必须已有成对数据」才为 true,则未配白名单时前端会显示「未要求 Wi-Fi」且可能放过打卡,与门店勾选意图不符。 | |
| 2242 | + /// </remarks> | |
| 2243 | + private static bool EffectiveRequireWifiCheck(LqMdxxEntity store) | |
| 2244 | + { | |
| 2245 | + if (store == null || (store.AttendanceCheckWifi ?? 0) != 1) return false; | |
| 2246 | + return true; | |
| 2247 | + } | |
| 2248 | + | |
| 2249 | + /// <summary> | |
| 2250 | + /// Wi-Fi 是否满足打卡规则(仅 <see cref="ParseStoreWifiPairs"/>) | |
| 2251 | + /// </summary> | |
| 2252 | + private static bool IsWifiPunchAllowed(LqMdxxEntity store, decimal? longitude, decimal? latitude, string wifiSsid, string wifiBssid) | |
| 2253 | + { | |
| 2254 | + var pairs = ParseStoreWifiPairs(store); | |
| 2255 | + if (pairs.Count > 0) | |
| 2256 | + { | |
| 2257 | + var strict = (store.AttendanceWifiVerifyPair ?? 0) == 1; | |
| 2258 | + var clientSsid = (wifiSsid ?? string.Empty).Trim(); | |
| 2259 | + var clientBssidNorm = NormalizeAttendanceBssid(wifiBssid); | |
| 2260 | + var hasClientBssid = !string.IsNullOrEmpty(clientBssidNorm); | |
| 2261 | + | |
| 2262 | + if (strict) | |
| 2263 | + { | |
| 2264 | + if (hasClientBssid) | |
| 2265 | + { | |
| 2266 | + // 微信/iOS 常拿不到 SSID 或名称不准,以 BSSID(AP)为准;配置行须含有效 BSSID | |
| 2267 | + return pairs.Any(p => | |
| 2268 | + { | |
| 2269 | + var pb = NormalizeAttendanceBssid(p.Bssid); | |
| 2270 | + return !string.IsNullOrEmpty(pb) && pb == clientBssidNorm; | |
| 2271 | + }); | |
| 2272 | + } | |
| 2273 | + | |
| 2274 | + if (EvaluateFenceValid(store, longitude, latitude) != 1) return false; | |
| 2275 | + if (string.IsNullOrEmpty(clientSsid)) return false; | |
| 2276 | + return pairs.Any(p => | |
| 2277 | + { | |
| 2278 | + var ps = (p.Ssid ?? string.Empty).Trim(); | |
| 2279 | + return !string.IsNullOrEmpty(ps) && string.Equals(ps, clientSsid, StringComparison.OrdinalIgnoreCase); | |
| 2280 | + }); | |
| 2281 | + } | |
| 2282 | + | |
| 2283 | + return pairs.Any(p => | |
| 2284 | + { | |
| 2285 | + var ps = (p.Ssid ?? string.Empty).Trim(); | |
| 2286 | + var pb = NormalizeAttendanceBssid(p.Bssid); | |
| 2287 | + var ssidHit = !string.IsNullOrEmpty(ps) && !string.IsNullOrEmpty(clientSsid) && | |
| 2288 | + string.Equals(ps, clientSsid, StringComparison.OrdinalIgnoreCase); | |
| 2289 | + var bssidHit = !string.IsNullOrEmpty(pb) && pb == clientBssidNorm; | |
| 2290 | + return ssidHit || bssidHit; | |
| 2291 | + }); | |
| 2292 | + } | |
| 2293 | + | |
| 2294 | + return false; | |
| 2295 | + } | |
| 2296 | + | |
| 2297 | + /// <summary> | |
| 2298 | + /// 正常打卡是否满足门店位置策略:仅围栏 / 仅 Wi-Fi / 两者同时开启时为「围栏或 Wi-Fi 满足其一」 | |
| 2299 | + /// </summary> | |
| 2300 | + private static bool IsNormalPunchLocationSatisfied(LqMdxxEntity store, decimal? longitude, decimal? latitude, string wifiSsid, string wifiBssid) | |
| 2301 | + { | |
| 2302 | + var needFence = EffectiveRequireFenceCheck(store); | |
| 2303 | + var needWifi = EffectiveRequireWifiCheck(store); | |
| 2304 | + if (!needFence && !needWifi) return true; | |
| 2305 | + | |
| 2306 | + var fenceOk = !needFence || EvaluateFenceValid(store, longitude, latitude) == 1; | |
| 2307 | + var wifiOk = !needWifi || IsWifiPunchAllowed(store, longitude, latitude, wifiSsid, wifiBssid); | |
| 2308 | + if (needFence && needWifi) return fenceOk || wifiOk; | |
| 2309 | + if (needFence) return fenceOk; | |
| 2310 | + return wifiOk; | |
| 2311 | + } | |
| 2312 | + | |
| 2008 | 2313 | private static int? EvaluateFenceValid(LqMdxxEntity store, decimal? longitude, decimal? latitude) |
| 2009 | 2314 | { |
| 2010 | 2315 | if (store == null || !longitude.HasValue || !latitude.HasValue || string.IsNullOrWhiteSpace(store.FencePolygons)) |
| ... | ... | @@ -2246,12 +2551,77 @@ namespace NCC.Extend |
| 2246 | 2551 | w.taskNodeId |
| 2247 | 2552 | }).ToList(), |
| 2248 | 2553 | canCancelWorkflowSupplement, |
| 2554 | + hasAttendanceRecord = true, | |
| 2249 | 2555 | ruleSnapshotJson = record.RuleSnapshotJson, |
| 2250 | 2556 | allRuleSnapshotJson = record.AllRuleSnapshotJson |
| 2251 | 2557 | }; |
| 2252 | 2558 | } |
| 2253 | 2559 | |
| 2254 | 2560 | /// <summary> |
| 2561 | + /// 当日无考勤行时仍返回详情结构,便于前端展示「暂无打卡」。 | |
| 2562 | + /// </summary> | |
| 2563 | + private async Task<dynamic> BuildEmptyAttendanceDetailAsync(LqAttendanceRecordDetailQueryInput input) | |
| 2564 | + { | |
| 2565 | + var user = await GetAttendanceUserAsync(input.UserId.Trim(), requireOnJob: false); | |
| 2566 | + var attendanceDate = ParseAttendanceDate(input.AttendanceDate); | |
| 2567 | + var store = await GetStoreAsync(user.Mdid); | |
| 2568 | + var group = await GetAttendanceGroupAsync(user.AttendanceGroupId, required: false); | |
| 2569 | + var missingStatus = attendanceDate.Date > DateTime.Today.Date | |
| 2570 | + ? (int)AttendanceRecordStatusEnum.待考勤 | |
| 2571 | + : (int)AttendanceRecordStatusEnum.缺卡; | |
| 2572 | + | |
| 2573 | + return new | |
| 2574 | + { | |
| 2575 | + id = (string)null, | |
| 2576 | + userId = user.Id, | |
| 2577 | + employeeName = user.RealName, | |
| 2578 | + storeId = user.Mdid, | |
| 2579 | + storeName = store?.Dm, | |
| 2580 | + attendanceDate = attendanceDate.ToString("yyyy-MM-dd"), | |
| 2581 | + attendanceGroupId = user.AttendanceGroupId, | |
| 2582 | + attendanceGroupName = group?.GroupName, | |
| 2583 | + workStartTime = group?.WorkStartTime, | |
| 2584 | + workEndTime = group?.WorkEndTime, | |
| 2585 | + status = missingStatus, | |
| 2586 | + statusText = "暂无打卡", | |
| 2587 | + lateMinutes = 0, | |
| 2588 | + earlyLeaveMinutes = 0, | |
| 2589 | + isManual = 0, | |
| 2590 | + remark = (string)null, | |
| 2591 | + punchIn = new | |
| 2592 | + { | |
| 2593 | + time = (string)null, | |
| 2594 | + type = (int?)null, | |
| 2595 | + typeText = "暂无打卡", | |
| 2596 | + longitude = (decimal?)null, | |
| 2597 | + latitude = (decimal?)null, | |
| 2598 | + address = (string)null, | |
| 2599 | + photoUrl = (string)null, | |
| 2600 | + fenceValid = (int?)null, | |
| 2601 | + fenceValidText = "未校验" | |
| 2602 | + }, | |
| 2603 | + punchOut = new | |
| 2604 | + { | |
| 2605 | + time = (string)null, | |
| 2606 | + type = (int?)null, | |
| 2607 | + typeText = "暂无打卡", | |
| 2608 | + longitude = (decimal?)null, | |
| 2609 | + latitude = (decimal?)null, | |
| 2610 | + address = (string)null, | |
| 2611 | + photoUrl = (string)null, | |
| 2612 | + fenceValid = (int?)null, | |
| 2613 | + fenceValidText = "未校验" | |
| 2614 | + }, | |
| 2615 | + supplement = (object)null, | |
| 2616 | + relatedWorkflows = new List<object>(), | |
| 2617 | + canCancelWorkflowSupplement = false, | |
| 2618 | + hasAttendanceRecord = false, | |
| 2619 | + ruleSnapshotJson = (string)null, | |
| 2620 | + allRuleSnapshotJson = (string)null | |
| 2621 | + }; | |
| 2622 | + } | |
| 2623 | + | |
| 2624 | + /// <summary> | |
| 2255 | 2625 | /// 是否允许「取消流程补卡」:能解析到补卡申请单,且申请目标中存在与本考勤日对应的勾选项(含 PascalCase、日期格式差异、单日申请兜底)。 |
| 2256 | 2626 | /// </summary> |
| 2257 | 2627 | private async Task<bool> ComputeCanCancelWorkflowSupplementAsync(LqAttendanceRecordEntity record) |
| ... | ... | @@ -2770,6 +3140,8 @@ namespace NCC.Extend |
| 2770 | 3140 | return null; |
| 2771 | 3141 | } |
| 2772 | 3142 | |
| 3143 | + var wifiPairRows = ParseStoreWifiPairs(entity); | |
| 3144 | + var wifiPairsPayload = wifiPairRows.Select(p => new { ssid = p.Ssid, bssid = p.Bssid }).ToList(); | |
| 2773 | 3145 | return new |
| 2774 | 3146 | { |
| 2775 | 3147 | storeId = entity.Id, |
| ... | ... | @@ -2781,7 +3153,13 @@ namespace NCC.Extend |
| 2781 | 3153 | fencePolygons = entity.FencePolygons, |
| 2782 | 3154 | hasFence = !string.IsNullOrWhiteSpace(entity.FencePolygons), |
| 2783 | 3155 | businessHours = entity.BusinessHours, |
| 2784 | - trafficTips = entity.TrafficTips | |
| 3156 | + trafficTips = entity.TrafficTips, | |
| 3157 | + attendanceCheckFence = entity.AttendanceCheckFence, | |
| 3158 | + attendanceCheckWifi = entity.AttendanceCheckWifi, | |
| 3159 | + attendanceWifiPairs = wifiPairsPayload, | |
| 3160 | + attendanceWifiVerifyPair = entity.AttendanceWifiVerifyPair ?? 0, | |
| 3161 | + requireFenceCheck = EffectiveRequireFenceCheck(entity), | |
| 3162 | + requireWifiCheck = EffectiveRequireWifiCheck(entity) | |
| 2785 | 3163 | }; |
| 2786 | 3164 | } |
| 2787 | 3165 | |
| ... | ... | @@ -3032,6 +3410,13 @@ namespace NCC.Extend |
| 3032 | 3410 | private static string GetAttendanceRecordStatusDisplayText(LqAttendanceRecordEntity record) |
| 3033 | 3411 | { |
| 3034 | 3412 | var (_, statusText, _) = MapStatus(record.Status); |
| 3413 | + // 备注中已记录销假时,不再用「请假审批通过」里的假种名作为状态 headline(避免仍显示年假等) | |
| 3414 | + if (!string.IsNullOrWhiteSpace(record.Remark) | |
| 3415 | + && record.Remark.IndexOf("销假", StringComparison.Ordinal) >= 0) | |
| 3416 | + { | |
| 3417 | + return statusText; | |
| 3418 | + } | |
| 3419 | + | |
| 3035 | 3420 | if (record.Status == (int)AttendanceRecordStatusEnum.请假) |
| 3036 | 3421 | { |
| 3037 | 3422 | var label = TryGetLeaveTypeLabelFromSyncRemark(record.Remark); |
| ... | ... | @@ -3157,6 +3542,7 @@ namespace NCC.Extend |
| 3157 | 3542 | } |
| 3158 | 3543 | |
| 3159 | 3544 | if (statusKey == "sick") return "病假"; |
| 3545 | + if (statusKey == "pending") return "暂无打卡"; | |
| 3160 | 3546 | if (statusKey == "missing") return "缺卡"; |
| 3161 | 3547 | if (statusKey == "absenteeism") return "旷工"; |
| 3162 | 3548 | return "--"; |
| ... | ... | @@ -3181,6 +3567,8 @@ namespace NCC.Extend |
| 3181 | 3567 | return ("sick", "病假", "danger"); |
| 3182 | 3568 | case AttendanceRecordStatusEnum.缺卡: |
| 3183 | 3569 | return ("missing", "缺卡", "danger"); |
| 3570 | + case AttendanceRecordStatusEnum.待考勤: | |
| 3571 | + return ("pending", "暂无打卡", "info"); | |
| 3184 | 3572 | case AttendanceRecordStatusEnum.旷工: |
| 3185 | 3573 | return ("absenteeism", "旷工", "danger"); |
| 3186 | 3574 | default: |
| ... | ... | @@ -3284,7 +3672,7 @@ namespace NCC.Extend |
| 3284 | 3672 | } |
| 3285 | 3673 | |
| 3286 | 3674 | /// <summary> |
| 3287 | - /// 为「请假」「补卡」等关联项补全打开流程详情所需字段(FLOW_TASK / FLOW_ENGINE) | |
| 3675 | + /// 为「请假」「补卡」「销假」等关联项补全打开流程详情所需字段(FLOW_TASK / FLOW_ENGINE) | |
| 3288 | 3676 | /// </summary> |
| 3289 | 3677 | private async Task FillRelatedWorkflowFlowOpenAsync(List<RelatedWorkflowItem> list) |
| 3290 | 3678 | { |
| ... | ... | @@ -3294,7 +3682,7 @@ namespace NCC.Extend |
| 3294 | 3682 | } |
| 3295 | 3683 | |
| 3296 | 3684 | foreach (var w in list.Where(x => |
| 3297 | - (x.type == "请假" || x.type == "补卡") && !string.IsNullOrWhiteSpace(x.id))) | |
| 3685 | + (x.type == "请假" || x.type == "补卡" || x.type == "销假") && !string.IsNullOrWhiteSpace(x.id))) | |
| 3298 | 3686 | { |
| 3299 | 3687 | if (!string.IsNullOrWhiteSpace(w.flowId) |
| 3300 | 3688 | && !string.IsNullOrWhiteSpace(w.flowCode) |
| ... | ... | @@ -3362,6 +3750,21 @@ namespace NCC.Extend |
| 3362 | 3750 | return JsonConvert.SerializeObject(list); |
| 3363 | 3751 | } |
| 3364 | 3752 | |
| 3753 | + private sealed class MonthReportUserLine | |
| 3754 | + { | |
| 3755 | + public string UserId { get; set; } | |
| 3756 | + | |
| 3757 | + public string EmployeeName { get; set; } | |
| 3758 | + | |
| 3759 | + public string StoreId { get; set; } | |
| 3760 | + | |
| 3761 | + public string StoreName { get; set; } | |
| 3762 | + | |
| 3763 | + public string AttendanceGroupId { get; set; } | |
| 3764 | + | |
| 3765 | + public string GroupName { get; set; } | |
| 3766 | + } | |
| 3767 | + | |
| 3365 | 3768 | private sealed class PunchApplyTargetLine |
| 3366 | 3769 | { |
| 3367 | 3770 | public string date { get; set; } | ... | ... |
绿纤uni-app/pages/attendance-punch/attendance-punch.vue
| ... | ... | @@ -18,29 +18,19 @@ |
| 18 | 18 | <!-- 打卡方式:与小程序白卡片风格一致 --> |
| 19 | 19 | <view class="att-card att-seg-card"> |
| 20 | 20 | <view class="att-seg"> |
| 21 | - <view | |
| 22 | - class="att-seg-item" | |
| 23 | - :class="{ 'is-active': punchType === 1 }" | |
| 24 | - @click="setPunchType(1)" | |
| 25 | - >正常</view> | |
| 26 | - <view | |
| 27 | - class="att-seg-item" | |
| 28 | - :class="{ 'is-active': punchType === 2 }" | |
| 29 | - @click="setPunchType(2)" | |
| 30 | - >外勤</view> | |
| 21 | + <view class="att-seg-item" :class="{ 'is-active': punchType === 1 }" @click="setPunchType(1)">正常 | |
| 22 | + </view> | |
| 23 | + <view class="att-seg-item" :class="{ 'is-active': punchType === 2 }" @click="setPunchType(2)">外勤 | |
| 24 | + </view> | |
| 31 | 25 | </view> |
| 32 | 26 | </view> |
| 33 | 27 | |
| 34 | 28 | <!-- 单一大按钮:按顺序上班 → 下班 → 已完成 --> |
| 35 | 29 | <view class="att-card att-punch-card"> |
| 36 | - <view | |
| 37 | - class="att-big-btn" | |
| 38 | - :class="{ | |
| 39 | - 'is-all-done': allPunchDone, | |
| 40 | - 'is-disabled': !canPressMainButton | |
| 41 | - }" | |
| 42 | - @click="onMainPunch" | |
| 43 | - > | |
| 30 | + <view class="att-big-btn" :class="{ | |
| 31 | + 'is-all-done': allPunchDone, | |
| 32 | + 'is-disabled': !canPressMainButton | |
| 33 | + }" @click="onMainPunch"> | |
| 44 | 34 | <template v-if="allPunchDone"> |
| 45 | 35 | <u-icon name="checkmark-circle-fill" color="#ffffff" size="52"></u-icon> |
| 46 | 36 | <text class="att-big-done-text">今日打卡已完成</text> |
| ... | ... | @@ -61,17 +51,28 @@ |
| 61 | 51 | <text class="att-loc-text">{{ locationText }}</text> |
| 62 | 52 | <text class="att-loc-tip">{{ fenceStatusText }}</text> |
| 63 | 53 | <view class="att-loc-actions"> |
| 64 | - <text | |
| 65 | - v-if="canViewFenceMap" | |
| 66 | - class="att-loc-action att-loc-action--primary" | |
| 67 | - @click.stop="openFenceMap" | |
| 68 | - >查看打卡范围</text> | |
| 54 | + <text v-if="canViewFenceMap" class="att-loc-action att-loc-action--primary" | |
| 55 | + @click.stop="openFenceMap">查看打卡范围</text> | |
| 69 | 56 | <text class="att-loc-action" @click.stop="refreshLocation">重新定位</text> |
| 70 | 57 | </view> |
| 71 | 58 | </view> |
| 72 | 59 | <u-icon name="arrow-right" color="#c8c9cc" size="18"></u-icon> |
| 73 | 60 | </view> |
| 74 | 61 | |
| 62 | + <!-- #ifdef MP-WEIXIN --> | |
| 63 | + <view v-if="Number(punchType) === 1" class="att-card att-loc-card att-wifi-card"> | |
| 64 | + <u-icon name="wifi" color="#43a047" size="22"></u-icon> | |
| 65 | + <view class="att-loc-main"> | |
| 66 | + <text class="att-loc-text">{{ wifiMainText }}</text> | |
| 67 | + <text class="att-loc-tip">{{ wifiTipText }}</text> | |
| 68 | + <view class="att-loc-actions"> | |
| 69 | + <text class="att-loc-action" @click.stop="refreshWifiInfo">刷新 Wi-Fi</text> | |
| 70 | + </view> | |
| 71 | + </view> | |
| 72 | + <u-icon name="arrow-right" color="#c8c9cc" size="18"></u-icon> | |
| 73 | + </view> | |
| 74 | + <!-- #endif --> | |
| 75 | + | |
| 75 | 76 | <view class="att-card"> |
| 76 | 77 | <view class="att-card-head"> |
| 77 | 78 | <text class="att-card-title">今日记录</text> |
| ... | ... | @@ -84,7 +85,9 @@ |
| 84 | 85 | <view class="att-tl-body"> |
| 85 | 86 | <view class="att-tl-row"> |
| 86 | 87 | <text class="att-tl-name">上班</text> |
| 87 | - <text class="att-tl-time">{{ detail.punchIn && detail.punchIn.time ? detail.punchIn.time : '未打卡' }}</text> | |
| 88 | + <text class="att-tl-time">{{ detail.punchIn && detail.punchIn.time ? detail.punchIn.time | |
| 89 | + : '未打卡' | |
| 90 | + }}</text> | |
| 88 | 91 | </view> |
| 89 | 92 | </view> |
| 90 | 93 | </view> |
| ... | ... | @@ -93,7 +96,9 @@ |
| 93 | 96 | <view class="att-tl-body"> |
| 94 | 97 | <view class="att-tl-row"> |
| 95 | 98 | <text class="att-tl-name">下班</text> |
| 96 | - <text class="att-tl-time">{{ detail.punchOut && detail.punchOut.time ? detail.punchOut.time : '未打卡' }}</text> | |
| 99 | + <text class="att-tl-time">{{ detail.punchOut && detail.punchOut.time ? | |
| 100 | + detail.punchOut.time : '未打卡' | |
| 101 | + }}</text> | |
| 97 | 102 | </view> |
| 98 | 103 | </view> |
| 99 | 104 | </view> |
| ... | ... | @@ -107,25 +112,13 @@ |
| 107 | 112 | <view class="att-card att-more-card"> |
| 108 | 113 | <view class="att-more-title">打卡照片(选填)</view> |
| 109 | 114 | <view class="att-upload-wrap"> |
| 110 | - <u-upload | |
| 111 | - :fileList="photoFileList" | |
| 112 | - @afterRead="afterReadPhoto" | |
| 113 | - @delete="deletePhoto" | |
| 114 | - name="photo" | |
| 115 | - :maxCount="1" | |
| 116 | - accept="image" | |
| 117 | - width="120" | |
| 118 | - height="120" | |
| 119 | - ></u-upload> | |
| 115 | + <u-upload :fileList="photoFileList" @afterRead="afterReadPhoto" @delete="deletePhoto" name="photo" | |
| 116 | + :maxCount="1" accept="image" width="120" height="120"></u-upload> | |
| 120 | 117 | </view> |
| 121 | 118 | <template v-if="punchType === 2"> |
| 122 | 119 | <view class="att-more-title att-more-title--second">外勤说明(选填)</view> |
| 123 | - <u-input | |
| 124 | - v-model="remark" | |
| 125 | - placeholder="请填写外勤事由(选填)" | |
| 126 | - border="none" | |
| 127 | - :customStyle="{ background: '#f1f8f4', padding: '16rpx 20rpx', borderRadius: '12rpx' }" | |
| 128 | - ></u-input> | |
| 120 | + <u-input v-model="remark" placeholder="请填写外勤事由(选填)" border="none" | |
| 121 | + :customStyle="{ background: '#f1f8f4', padding: '16rpx 20rpx', borderRadius: '12rpx' }"></u-input> | |
| 129 | 122 | </template> |
| 130 | 123 | </view> |
| 131 | 124 | |
| ... | ... | @@ -140,728 +133,946 @@ |
| 140 | 133 | </template> |
| 141 | 134 | |
| 142 | 135 | <script> |
| 143 | - import { isPointInPolygon, parseFencePolygons } from '@/service/attendance-fence.js' | |
| 144 | - | |
| 145 | - export default { | |
| 146 | - data() { | |
| 147 | - return { | |
| 148 | - loading: false, | |
| 149 | - submitting: false, | |
| 150 | - userInfo: {}, | |
| 151 | - today: '', | |
| 152 | - detail: null, | |
| 153 | - punchConfig: null, | |
| 154 | - storeFence: null, | |
| 155 | - attendanceGroup: null, | |
| 156 | - isInFence: null, | |
| 157 | - punchType: 1, | |
| 158 | - remark: '', | |
| 159 | - longitude: null, | |
| 160 | - latitude: null, | |
| 161 | - address: '', | |
| 162 | - fenceEnabled: false, | |
| 163 | - locating: false, | |
| 164 | - photoUrl: '', | |
| 165 | - photoFileList: [], | |
| 166 | - clockTime: '--:--:--', | |
| 167 | - clockTimer: null | |
| 136 | +import { isPointInPolygon, parseFencePolygons } from '@/service/attendance-fence.js' | |
| 137 | + | |
| 138 | +export default { | |
| 139 | + data() { | |
| 140 | + return { | |
| 141 | + loading: false, | |
| 142 | + submitting: false, | |
| 143 | + userInfo: {}, | |
| 144 | + today: '', | |
| 145 | + detail: null, | |
| 146 | + punchConfig: null, | |
| 147 | + storeFence: null, | |
| 148 | + attendanceGroup: null, | |
| 149 | + isInFence: null, | |
| 150 | + punchType: 1, | |
| 151 | + remark: '', | |
| 152 | + longitude: null, | |
| 153 | + latitude: null, | |
| 154 | + address: '', | |
| 155 | + fenceEnabled: false, | |
| 156 | + locating: false, | |
| 157 | + photoUrl: '', | |
| 158 | + photoFileList: [], | |
| 159 | + clockTime: '--:--:--', | |
| 160 | + clockTimer: null, | |
| 161 | + wifiSSID: '', | |
| 162 | + wifiBSSID: '', | |
| 163 | + wifiLoading: false, | |
| 164 | + wifiError: '' | |
| 165 | + } | |
| 166 | + }, | |
| 167 | + computed: { | |
| 168 | + todayText() { | |
| 169 | + if (!this.today) return '' | |
| 170 | + const [y, m, d] = this.today.split('-') | |
| 171 | + return `${y}年${Number(m)}月${Number(d)}日` | |
| 172 | + }, | |
| 173 | + weekdayText() { | |
| 174 | + if (!this.today) return '' | |
| 175 | + const w = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] | |
| 176 | + const [y, m, d] = this.today.split('-').map(Number) | |
| 177 | + const dt = new Date(y, m - 1, d) | |
| 178 | + return w[dt.getDay()] | |
| 179 | + }, | |
| 180 | + ruleTimeText() { | |
| 181 | + if (this.attendanceGroup && this.attendanceGroup.workStartTime && this.attendanceGroup.workEndTime) { | |
| 182 | + return `${this.attendanceGroup.workStartTime} - ${this.attendanceGroup.workEndTime}` | |
| 183 | + } | |
| 184 | + if (this.detail && this.detail.workStartTime && this.detail.workEndTime) { | |
| 185 | + return `${this.detail.workStartTime} - ${this.detail.workEndTime}` | |
| 168 | 186 | } |
| 187 | + return '' | |
| 169 | 188 | }, |
| 170 | - computed: { | |
| 171 | - todayText() { | |
| 172 | - if (!this.today) return '' | |
| 173 | - const [y, m, d] = this.today.split('-') | |
| 174 | - return `${y}年${Number(m)}月${Number(d)}日` | |
| 175 | - }, | |
| 176 | - weekdayText() { | |
| 177 | - if (!this.today) return '' | |
| 178 | - const w = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] | |
| 179 | - const [y, m, d] = this.today.split('-').map(Number) | |
| 180 | - const dt = new Date(y, m - 1, d) | |
| 181 | - return w[dt.getDay()] | |
| 182 | - }, | |
| 183 | - ruleTimeText() { | |
| 184 | - if (this.attendanceGroup && this.attendanceGroup.workStartTime && this.attendanceGroup.workEndTime) { | |
| 185 | - return `${this.attendanceGroup.workStartTime} - ${this.attendanceGroup.workEndTime}` | |
| 186 | - } | |
| 187 | - if (this.detail && this.detail.workStartTime && this.detail.workEndTime) { | |
| 188 | - return `${this.detail.workStartTime} - ${this.detail.workEndTime}` | |
| 189 | - } | |
| 190 | - return '' | |
| 191 | - }, | |
| 192 | - displayName() { | |
| 193 | - return this.detail && this.detail.employeeName | |
| 194 | - ? this.detail.employeeName | |
| 195 | - : (this.userInfo.userName || this.userInfo.realName || '员工') | |
| 196 | - }, | |
| 197 | - locationText() { | |
| 198 | - if (this.locating) return '正在获取位置…' | |
| 199 | - if (this.address) return this.address | |
| 200 | - if (this.longitude != null && this.latitude != null) { | |
| 201 | - return `已定位 · ${Number(this.latitude).toFixed(4)}, ${Number(this.longitude).toFixed(4)}` | |
| 189 | + displayName() { | |
| 190 | + return this.detail && this.detail.employeeName | |
| 191 | + ? this.detail.employeeName | |
| 192 | + : (this.userInfo.userName || this.userInfo.realName || '员工') | |
| 193 | + }, | |
| 194 | + locationText() { | |
| 195 | + if (this.locating) return '正在获取位置…' | |
| 196 | + if (this.address) return this.address | |
| 197 | + if (this.longitude != null && this.latitude != null) { | |
| 198 | + return `已定位 · ${Number(this.latitude).toFixed(4)}, ${Number(this.longitude).toFixed(4)}` | |
| 199 | + } | |
| 200 | + return '点击获取定位,用于考勤校验' | |
| 201 | + }, | |
| 202 | + requireFenceCheck() { | |
| 203 | + const sf = this.storeFence | |
| 204 | + if (!sf) return false | |
| 205 | + const rf = sf.requireFenceCheck != null ? sf.requireFenceCheck : sf.RequireFenceCheck | |
| 206 | + return rf === true || rf === 1 || rf === '1' | |
| 207 | + }, | |
| 208 | + requireWifiCheck() { | |
| 209 | + const sf = this.storeFence | |
| 210 | + if (!sf) return false | |
| 211 | + const rw = sf.requireWifiCheck != null ? sf.requireWifiCheck : sf.RequireWifiCheck | |
| 212 | + return rw === true || rw === 1 || rw === '1' | |
| 213 | + }, | |
| 214 | + /** 门店是否已配置 Wi-Fi 成对白名单(与接口 attendanceWifiPairs 一致) */ | |
| 215 | + wifiPairRulesCount() { | |
| 216 | + const sf = this.storeFence | |
| 217 | + if (!sf) return 0 | |
| 218 | + const raw = sf.attendanceWifiPairs != null ? sf.attendanceWifiPairs : sf.AttendanceWifiPairs | |
| 219 | + return Array.isArray(raw) ? raw.length : 0 | |
| 220 | + }, | |
| 221 | + wifiVerifyPairStrict() { | |
| 222 | + const sf = this.storeFence | |
| 223 | + if (!sf) return false | |
| 224 | + const v = sf.attendanceWifiVerifyPair != null ? sf.attendanceWifiVerifyPair : sf.AttendanceWifiVerifyPair | |
| 225 | + return Number(v) === 1 | |
| 226 | + }, | |
| 227 | + wifiMatchesStore() { | |
| 228 | + const sf = this.storeFence | |
| 229 | + const rawPairs = sf && (sf.attendanceWifiPairs != null ? sf.attendanceWifiPairs : sf.AttendanceWifiPairs) | |
| 230 | + const pairs = Array.isArray(rawPairs) ? rawPairs : [] | |
| 231 | + const curSsid = (this.wifiSSID || '').trim() | |
| 232 | + const curB = this.normalizeAttendanceBssid(this.wifiBSSID) | |
| 233 | + const hasClientBssid = !!curB | |
| 234 | + if (pairs.length > 0) { | |
| 235 | + const strict = this.wifiVerifyPairStrict | |
| 236 | + if (strict) { | |
| 237 | + if (hasClientBssid) { | |
| 238 | + // 与后端一致:有 BSSID 时以路由器 MAC 为准(避免 iOS/微信 SSID 为空或不准确) | |
| 239 | + return pairs.some((p) => { | |
| 240 | + const pb = this.normalizeAttendanceBssid(p.bssid) | |
| 241 | + return !!pb && pb === curB | |
| 242 | + }) | |
| 243 | + } | |
| 244 | + if (this.isInFence !== true) return false | |
| 245 | + if (!curSsid) return false | |
| 246 | + return pairs.some((p) => { | |
| 247 | + const ps = String(p.ssid || '').trim() | |
| 248 | + return !!ps && ps.toLowerCase() === curSsid.toLowerCase() | |
| 249 | + }) | |
| 202 | 250 | } |
| 203 | - return '点击获取定位,用于考勤校验' | |
| 204 | - }, | |
| 205 | - fenceStatusText() { | |
| 206 | - if (this.locating) return '正在校验打卡范围…' | |
| 207 | - if (Number(this.punchType) === 2) return '外勤打卡不受门店围栏限制' | |
| 208 | - if (!this.fenceEnabled) return '当前门店未配置围栏,正常打卡不受范围限制' | |
| 251 | + return pairs.some((p) => { | |
| 252 | + const ps = String(p.ssid || '').trim() | |
| 253 | + const pb = this.normalizeAttendanceBssid(p.bssid) | |
| 254 | + const ssidHit = | |
| 255 | + !!ps && !!curSsid && ps.toLowerCase() === curSsid.toLowerCase() | |
| 256 | + const bssidHit = !!pb && pb === curB | |
| 257 | + return ssidHit || bssidHit | |
| 258 | + }) | |
| 259 | + } | |
| 260 | + return false | |
| 261 | + }, | |
| 262 | + /** 正常上班时是否满足门店「围栏 / Wi-Fi / 或二者择一」策略(与后端一致) */ | |
| 263 | + normalPunchLocationOk() { | |
| 264 | + if (Number(this.punchType) !== 1) return true | |
| 265 | + const rf = this.requireFenceCheck | |
| 266 | + const rw = this.requireWifiCheck | |
| 267 | + if (!rf && !rw) return true | |
| 268 | + const fenceOk = !rf || this.isInFence === true | |
| 269 | + const wifiOk = !rw || this.wifiMatchesStore | |
| 270 | + if (rf && rw) return fenceOk || wifiOk | |
| 271 | + if (rf) return fenceOk | |
| 272 | + return wifiOk | |
| 273 | + }, | |
| 274 | + fenceStatusText() { | |
| 275 | + if (this.locating) return '正在校验位置…' | |
| 276 | + if (Number(this.punchType) === 2) return '外勤打卡不受门店位置与 Wi-Fi 限制' | |
| 277 | + const rf = this.requireFenceCheck | |
| 278 | + const rw = this.requireWifiCheck | |
| 279 | + if (!rf && !rw) return '当前门店未要求范围或 Wi-Fi,正常打卡可直接进行' | |
| 280 | + const fenceLine = rf | |
| 281 | + ? (this.longitude == null || this.latitude == null | |
| 282 | + ? '需定位后判断是否在打卡范围内' | |
| 283 | + : (this.isInFence === true ? '已在打卡范围内' : '不在打卡范围内')) | |
| 284 | + : '' | |
| 285 | + const hasPairs = this.wifiPairRulesCount > 0 | |
| 286 | + const wifiLine = rw | |
| 287 | + ? (!hasPairs | |
| 288 | + ? '门店已要求 Wi-Fi 打卡,但未配置网络白名单,请联系管理员在门店资料中维护' | |
| 289 | + : (this.wifiMatchesStore | |
| 290 | + ? '已匹配门店 Wi-Fi 规则' | |
| 291 | + : '未匹配门店 Wi-Fi 规则(开启「对应」且无 BSSID 时需同时在围栏内且 SSID 正确)')) | |
| 292 | + : '' | |
| 293 | + if (rf && rw) { | |
| 294 | + return `门店要求:范围或 Wi-Fi 满足其一即可。${fenceLine};${wifiLine}` | |
| 295 | + } | |
| 296 | + if (rf) { | |
| 209 | 297 | if (this.longitude == null || this.latitude == null) return '点击更新定位后校验是否在门店范围内' |
| 210 | 298 | if (this.isInFence === true) return '当前位于门店打卡范围内,可正常上下班打卡' |
| 211 | 299 | if (this.isInFence === false) return '当前不在门店打卡范围内,正常打卡不可用' |
| 212 | 300 | return '定位完成后自动校验门店围栏范围' |
| 213 | - }, | |
| 214 | - punchInDone() { | |
| 215 | - return !!(this.detail && this.detail.punchIn && this.detail.punchIn.time) | |
| 216 | - }, | |
| 217 | - punchOutDone() { | |
| 218 | - return !!(this.detail && this.detail.punchOut && this.detail.punchOut.time) | |
| 219 | - }, | |
| 220 | - /** 下一步应打卡方向:1 上班 → 2 下班;都完成则为 null */ | |
| 221 | - nextPunchDirection() { | |
| 222 | - if (!this.userInfo.userId) return null | |
| 223 | - if (!this.punchInDone) return 1 | |
| 224 | - if (!this.punchOutDone) return 2 | |
| 225 | - return null | |
| 226 | - }, | |
| 227 | - allPunchDone() { | |
| 228 | - return this.punchInDone && this.punchOutDone | |
| 229 | - }, | |
| 230 | - mainButtonTitle() { | |
| 231 | - if (this.allPunchDone) return '今日打卡已完成' | |
| 232 | - if (this.nextPunchDirection === 1) return '上班打卡' | |
| 233 | - if (this.nextPunchDirection === 2) return '下班打卡' | |
| 234 | - return '打卡' | |
| 235 | - }, | |
| 236 | - mainButtonSub() { | |
| 237 | - if (this.allPunchDone) return '' | |
| 238 | - if (this.nextPunchDirection === 1) return '上班签到' | |
| 239 | - if (this.nextPunchDirection === 2) return '下班签退' | |
| 240 | - return '' | |
| 241 | - }, | |
| 242 | - canPressMainButton() { | |
| 243 | - if (this.loading || this.submitting || this.locating) return false | |
| 244 | - if (!this.userInfo.userId) return false | |
| 245 | - if (this.nextPunchDirection == null) return false | |
| 246 | - if (Number(this.punchType) === 1 && this.fenceEnabled && this.isInFence !== true) return false | |
| 247 | - return true | |
| 248 | - }, | |
| 249 | - canViewFenceMap() { | |
| 250 | - return !!( | |
| 251 | - (this.storeFence && this.storeFence.hasFence && this.storeFence.fencePolygons) || | |
| 252 | - (this.storeFence && this.storeFence.longitude != null && this.storeFence.latitude != null) || | |
| 253 | - (this.longitude != null && this.latitude != null) | |
| 254 | - ) | |
| 255 | 301 | } |
| 302 | + return wifiLine || '请连接门店指定 Wi-Fi 后再试' | |
| 256 | 303 | }, |
| 257 | - onLoad() { | |
| 258 | - this.userInfo = uni.getStorageSync('userInfo') || {} | |
| 259 | - this.today = this.formatDate(new Date()) | |
| 304 | + punchInDone() { | |
| 305 | + return !!(this.detail && this.detail.punchIn && this.detail.punchIn.time) | |
| 260 | 306 | }, |
| 261 | - onShow() { | |
| 262 | - this.userInfo = uni.getStorageSync('userInfo') || {} | |
| 263 | - this.startClock() | |
| 264 | - this.initializePage() | |
| 307 | + punchOutDone() { | |
| 308 | + return !!(this.detail && this.detail.punchOut && this.detail.punchOut.time) | |
| 265 | 309 | }, |
| 266 | - onHide() { | |
| 267 | - this.stopClock() | |
| 310 | + /** 下一步应打卡方向:1 上班 → 2 下班;都完成则为 null */ | |
| 311 | + nextPunchDirection() { | |
| 312 | + if (!this.userInfo.userId) return null | |
| 313 | + if (!this.punchInDone) return 1 | |
| 314 | + if (!this.punchOutDone) return 2 | |
| 315 | + return null | |
| 268 | 316 | }, |
| 269 | - onUnload() { | |
| 270 | - this.stopClock() | |
| 317 | + allPunchDone() { | |
| 318 | + return this.punchInDone && this.punchOutDone | |
| 271 | 319 | }, |
| 272 | - onPullDownRefresh() { | |
| 273 | - this.initializePage().finally(() => { | |
| 274 | - uni.stopPullDownRefresh() | |
| 275 | - }) | |
| 320 | + mainButtonTitle() { | |
| 321 | + if (this.allPunchDone) return '今日打卡已完成' | |
| 322 | + if (this.nextPunchDirection === 1) return '上班打卡' | |
| 323 | + if (this.nextPunchDirection === 2) return '下班打卡' | |
| 324 | + return '打卡' | |
| 276 | 325 | }, |
| 277 | - methods: { | |
| 278 | - /** 切回「正常」时只清空外勤说明;照片正常/外勤共用,保留 */ | |
| 279 | - setPunchType(type) { | |
| 280 | - this.punchType = type | |
| 281 | - if (type === 1) { | |
| 282 | - this.remark = '' | |
| 283 | - } | |
| 284 | - }, | |
| 285 | - async initializePage() { | |
| 286 | - await this.loadPunchConfig() | |
| 287 | - await this.refreshLocation() | |
| 288 | - await this.loadDetail() | |
| 289 | - }, | |
| 290 | - onMainPunch() { | |
| 291 | - if (!this.nextPunchDirection) return | |
| 292 | - if (Number(this.punchType) === 1 && this.fenceEnabled && this.isInFence !== true) { | |
| 293 | - uni.showToast({ title: '当前不在门店打卡范围内,无法正常打卡', icon: 'none' }) | |
| 294 | - return | |
| 326 | + mainButtonSub() { | |
| 327 | + if (this.allPunchDone) return '' | |
| 328 | + if (this.nextPunchDirection === 1) return '上班签到' | |
| 329 | + if (this.nextPunchDirection === 2) return '下班签退' | |
| 330 | + return '' | |
| 331 | + }, | |
| 332 | + canPressMainButton() { | |
| 333 | + if (this.loading || this.submitting || this.locating) return false | |
| 334 | + if (!this.userInfo.userId) return false | |
| 335 | + if (this.nextPunchDirection == null) return false | |
| 336 | + if (Number(this.punchType) === 1 && !this.normalPunchLocationOk) return false | |
| 337 | + return true | |
| 338 | + }, | |
| 339 | + canViewFenceMap() { | |
| 340 | + return !!( | |
| 341 | + (this.storeFence && this.storeFence.hasFence && this.storeFence.fencePolygons) || | |
| 342 | + (this.storeFence && this.storeFence.longitude != null && this.storeFence.latitude != null) || | |
| 343 | + (this.longitude != null && this.latitude != null) | |
| 344 | + ) | |
| 345 | + }, | |
| 346 | + wifiMainText() { | |
| 347 | + if (this.wifiLoading) return '正在获取 Wi-Fi 信息…' | |
| 348 | + if (this.wifiError) return this.wifiError | |
| 349 | + if (this.wifiSSID) return `当前网络:${this.wifiSSID}` | |
| 350 | + return '未获取到 Wi-Fi 名称' | |
| 351 | + }, | |
| 352 | + wifiTipText() { | |
| 353 | + if (this.wifiLoading) return '用于核对是否在门店网络环境' | |
| 354 | + if (this.wifiError) { | |
| 355 | + if (this.wifiError.indexOf('模拟器') !== -1 || this.wifiError.indexOf('真机') !== -1) { | |
| 356 | + return 'Wi-Fi 由系统提供,开发者工具无此能力' | |
| 295 | 357 | } |
| 296 | - if (!this.canPressMainButton) return | |
| 297 | - this.doPunch(this.nextPunchDirection) | |
| 298 | - }, | |
| 299 | - startClock() { | |
| 300 | - this.tickClock() | |
| 301 | - this.clockTimer = setInterval(this.tickClock, 1000) | |
| 302 | - }, | |
| 303 | - stopClock() { | |
| 304 | - if (this.clockTimer) { | |
| 305 | - clearInterval(this.clockTimer) | |
| 306 | - this.clockTimer = null | |
| 358 | + return '可点击「刷新 Wi-Fi」重试;部分机型需开启定位权限' | |
| 359 | + } | |
| 360 | + if (Number(this.punchType) === 1 && this.requireWifiCheck) { | |
| 361 | + if (this.wifiPairRulesCount === 0) { | |
| 362 | + return '门店已开启 Wi-Fi 校验,白名单未配置,请联系管理员' | |
| 307 | 363 | } |
| 308 | - }, | |
| 309 | - tickClock() { | |
| 310 | - const now = new Date() | |
| 311 | - const h = `${now.getHours()}`.padStart(2, '0') | |
| 312 | - const m = `${now.getMinutes()}`.padStart(2, '0') | |
| 313 | - const s = `${now.getSeconds()}`.padStart(2, '0') | |
| 314 | - this.clockTime = `${h}:${m}:${s}` | |
| 315 | - }, | |
| 316 | - formatDate(d) { | |
| 317 | - const y = d.getFullYear() | |
| 318 | - const m = `${d.getMonth() + 1}`.padStart(2, '0') | |
| 319 | - const day = `${d.getDate()}`.padStart(2, '0') | |
| 320 | - return `${y}-${m}-${day}` | |
| 321 | - }, | |
| 322 | - async loadDetail() { | |
| 323 | - if (!this.userInfo.userId) { | |
| 324 | - return | |
| 364 | + const strict = this.wifiVerifyPairStrict | |
| 365 | + if (this.wifiMatchesStore) { | |
| 366 | + return strict ? '已满足门店 Wi-Fi(含对应校验)' : '已命中门店 Wi-Fi 白名单' | |
| 325 | 367 | } |
| 326 | - this.loading = true | |
| 327 | - try { | |
| 328 | - const res = await this.API.getAttendanceDetail({ | |
| 329 | - userId: this.userInfo.userId, | |
| 330 | - attendanceDate: this.today | |
| 331 | - }) | |
| 332 | - if (res && Number(res.code) === 200 && res.data) { | |
| 333 | - this.detail = res.data | |
| 334 | - } else { | |
| 335 | - this.detail = null | |
| 336 | - } | |
| 337 | - } catch (e) { | |
| 368 | + return strict | |
| 369 | + ? '未满足:若已开「对应」校验,请连门店 AP 或进入围栏且 SSID 正确' | |
| 370 | + : '未命中白名单(可核对 SSID/BSSID;与围栏组合时满足其一即可)' | |
| 371 | + } | |
| 372 | + if (this.wifiBSSID) return `BSSID:${this.wifiBSSID}` | |
| 373 | + return '名称由系统返回;iOS 常可稳定获取 BSSID' | |
| 374 | + } | |
| 375 | + }, | |
| 376 | + onLoad() { | |
| 377 | + this.userInfo = uni.getStorageSync('userInfo') || {} | |
| 378 | + this.today = this.formatDate(new Date()) | |
| 379 | + }, | |
| 380 | + onShow() { | |
| 381 | + this.userInfo = uni.getStorageSync('userInfo') || {} | |
| 382 | + this.startClock() | |
| 383 | + this.initializePage() | |
| 384 | + }, | |
| 385 | + onHide() { | |
| 386 | + this.stopClock() | |
| 387 | + }, | |
| 388 | + onUnload() { | |
| 389 | + this.stopClock() | |
| 390 | + }, | |
| 391 | + onPullDownRefresh() { | |
| 392 | + this.initializePage().finally(() => { | |
| 393 | + uni.stopPullDownRefresh() | |
| 394 | + }) | |
| 395 | + }, | |
| 396 | + methods: { | |
| 397 | + /** 与后端 NormalizeAttendanceBssid 一致:连字符统一为冒号、小写 */ | |
| 398 | + normalizeAttendanceBssid(s) { | |
| 399 | + if (!s) return '' | |
| 400 | + let t = String(s).trim().replace(/-/g, ':').replace(/\s+/g, '').toLowerCase() | |
| 401 | + if (t.indexOf(':') < 0 && t.length === 12 && /^[0-9a-f]{12}$/.test(t)) { | |
| 402 | + t = t.match(/.{2}/g).join(':') | |
| 403 | + } | |
| 404 | + return t | |
| 405 | + }, | |
| 406 | + /** 切回「正常」时只清空外勤说明;照片正常/外勤共用,保留 */ | |
| 407 | + setPunchType(type) { | |
| 408 | + this.punchType = type | |
| 409 | + if (Number(type) === 1) { | |
| 410 | + this.remark = '' | |
| 411 | + // #ifdef MP-WEIXIN | |
| 412 | + this.refreshWifiInfo() | |
| 413 | + // #endif | |
| 414 | + } | |
| 415 | + }, | |
| 416 | + async initializePage() { | |
| 417 | + await this.loadPunchConfig() | |
| 418 | + const wifiTask = | |
| 419 | + Number(this.punchType) === 1 ? this.refreshWifiInfo() : Promise.resolve() | |
| 420 | + await Promise.all([this.refreshLocation(), wifiTask]) | |
| 421 | + await this.loadDetail() | |
| 422 | + }, | |
| 423 | + onMainPunch() { | |
| 424 | + if (!this.nextPunchDirection) return | |
| 425 | + if (Number(this.punchType) === 1 && !this.normalPunchLocationOk) { | |
| 426 | + uni.showToast({ | |
| 427 | + title: '未满足门店要求(需在范围内或指定 Wi-Fi/BSSID),可改用外勤', | |
| 428 | + icon: 'none' | |
| 429 | + }) | |
| 430 | + return | |
| 431 | + } | |
| 432 | + if (!this.canPressMainButton) return | |
| 433 | + this.doPunch(this.nextPunchDirection) | |
| 434 | + }, | |
| 435 | + startClock() { | |
| 436 | + this.tickClock() | |
| 437 | + this.clockTimer = setInterval(this.tickClock, 1000) | |
| 438 | + }, | |
| 439 | + stopClock() { | |
| 440 | + if (this.clockTimer) { | |
| 441 | + clearInterval(this.clockTimer) | |
| 442 | + this.clockTimer = null | |
| 443 | + } | |
| 444 | + }, | |
| 445 | + tickClock() { | |
| 446 | + const now = new Date() | |
| 447 | + const h = `${now.getHours()}`.padStart(2, '0') | |
| 448 | + const m = `${now.getMinutes()}`.padStart(2, '0') | |
| 449 | + const s = `${now.getSeconds()}`.padStart(2, '0') | |
| 450 | + this.clockTime = `${h}:${m}:${s}` | |
| 451 | + }, | |
| 452 | + formatDate(d) { | |
| 453 | + const y = d.getFullYear() | |
| 454 | + const m = `${d.getMonth() + 1}`.padStart(2, '0') | |
| 455 | + const day = `${d.getDate()}`.padStart(2, '0') | |
| 456 | + return `${y}-${m}-${day}` | |
| 457 | + }, | |
| 458 | + async loadDetail() { | |
| 459 | + if (!this.userInfo.userId) { | |
| 460 | + return | |
| 461 | + } | |
| 462 | + this.loading = true | |
| 463 | + try { | |
| 464 | + const res = await this.API.getAttendanceDetail({ | |
| 465 | + userId: this.userInfo.userId, | |
| 466 | + attendanceDate: this.today | |
| 467 | + }) | |
| 468 | + if (res && Number(res.code) === 200 && res.data) { | |
| 469 | + this.detail = res.data | |
| 470 | + } else { | |
| 338 | 471 | this.detail = null |
| 339 | - } finally { | |
| 340 | - this.loading = false | |
| 341 | 472 | } |
| 342 | - }, | |
| 343 | - async loadPunchConfig() { | |
| 344 | - if (!this.userInfo.userId) { | |
| 473 | + } catch (e) { | |
| 474 | + this.detail = null | |
| 475 | + } finally { | |
| 476 | + this.loading = false | |
| 477 | + } | |
| 478 | + }, | |
| 479 | + async loadPunchConfig() { | |
| 480 | + if (!this.userInfo.userId) { | |
| 481 | + this.punchConfig = null | |
| 482 | + this.storeFence = null | |
| 483 | + this.attendanceGroup = null | |
| 484 | + this.fenceEnabled = false | |
| 485 | + this.isInFence = null | |
| 486 | + return | |
| 487 | + } | |
| 488 | + try { | |
| 489 | + const res = await this.API.getCurrentAttendancePunchConfig({}) | |
| 490 | + if (res && Number(res.code) === 200 && res.data) { | |
| 491 | + this.punchConfig = res.data | |
| 492 | + this.storeFence = res.data.storeFence || null | |
| 493 | + this.attendanceGroup = res.data.attendanceGroup || null | |
| 494 | + this.fenceEnabled = !!(this.storeFence && this.storeFence.hasFence && this.storeFence.fencePolygons) | |
| 495 | + this.evaluateFenceRange() | |
| 496 | + } else { | |
| 345 | 497 | this.punchConfig = null |
| 346 | 498 | this.storeFence = null |
| 347 | 499 | this.attendanceGroup = null |
| 348 | 500 | this.fenceEnabled = false |
| 349 | 501 | this.isInFence = null |
| 350 | - return | |
| 351 | 502 | } |
| 503 | + } catch (e) { | |
| 504 | + this.punchConfig = null | |
| 505 | + this.storeFence = null | |
| 506 | + this.attendanceGroup = null | |
| 507 | + this.fenceEnabled = false | |
| 508 | + this.isInFence = null | |
| 509 | + } | |
| 510 | + }, | |
| 511 | + refreshWifiInfo() { | |
| 512 | + return new Promise((resolve) => { | |
| 513 | + // #ifndef MP-WEIXIN | |
| 514 | + resolve() | |
| 515 | + return | |
| 516 | + // #endif | |
| 517 | + // #ifdef MP-WEIXIN | |
| 352 | 518 | try { |
| 353 | - const res = await this.API.getCurrentAttendancePunchConfig({}) | |
| 354 | - if (res && Number(res.code) === 200 && res.data) { | |
| 355 | - this.punchConfig = res.data | |
| 356 | - this.storeFence = res.data.storeFence || null | |
| 357 | - this.attendanceGroup = res.data.attendanceGroup || null | |
| 358 | - this.fenceEnabled = !!(this.storeFence && this.storeFence.hasFence && this.storeFence.fencePolygons) | |
| 359 | - this.evaluateFenceRange() | |
| 360 | - } else { | |
| 361 | - this.punchConfig = null | |
| 362 | - this.storeFence = null | |
| 363 | - this.attendanceGroup = null | |
| 364 | - this.fenceEnabled = false | |
| 365 | - this.isInFence = null | |
| 519 | + const sys = uni.getSystemInfoSync() | |
| 520 | + if (sys && sys.platform === 'devtools') { | |
| 521 | + this.wifiLoading = false | |
| 522 | + this.wifiSSID = '' | |
| 523 | + this.wifiBSSID = '' | |
| 524 | + this.wifiError = '开发者工具模拟器不支持 Wi-Fi,请用真机预览或扫码调试' | |
| 525 | + resolve() | |
| 526 | + return | |
| 366 | 527 | } |
| 367 | - } catch (e) { | |
| 368 | - this.punchConfig = null | |
| 369 | - this.storeFence = null | |
| 370 | - this.attendanceGroup = null | |
| 371 | - this.fenceEnabled = false | |
| 372 | - this.isInFence = null | |
| 373 | - } | |
| 374 | - }, | |
| 375 | - refreshLocation() { | |
| 376 | - this.locating = true | |
| 377 | - return new Promise((resolve) => { | |
| 378 | - uni.getLocation({ | |
| 379 | - type: 'gcj02', | |
| 380 | - isHighAccuracy: true, | |
| 381 | - success: (res) => { | |
| 382 | - this.longitude = res.longitude | |
| 383 | - this.latitude = res.latitude | |
| 384 | - this.address = '' | |
| 385 | - this.evaluateFenceRange() | |
| 386 | - this.locating = false | |
| 387 | - resolve() | |
| 388 | - }, | |
| 389 | - fail: () => { | |
| 390 | - this.locating = false | |
| 391 | - this.isInFence = null | |
| 392 | - uni.showToast({ title: '定位失败,请检查系统定位权限', icon: 'none' }) | |
| 393 | - resolve() | |
| 528 | + } catch (e) { } | |
| 529 | + this.wifiLoading = true | |
| 530 | + this.wifiError = '' | |
| 531 | + uni.startWifi({ | |
| 532 | + success: () => { | |
| 533 | + uni.getConnectedWifi({ | |
| 534 | + success: (res) => { | |
| 535 | + const w = res && res.wifi | |
| 536 | + this.wifiSSID = w && w.SSID ? String(w.SSID) : '' | |
| 537 | + this.wifiBSSID = w && w.BSSID ? String(w.BSSID) : '' | |
| 538 | + this.wifiLoading = false | |
| 539 | + if (!this.wifiSSID && !this.wifiBSSID) { | |
| 540 | + this.wifiError = '未连接 Wi-Fi' | |
| 541 | + } | |
| 542 | + resolve() | |
| 543 | + }, | |
| 544 | + fail: (err) => { | |
| 545 | + this.wifiLoading = false | |
| 546 | + const msg = (err && err.errMsg) ? String(err.errMsg) : '' | |
| 547 | + if (msg.indexOf('auth deny') !== -1 || msg.indexOf('authorize') !== -1) { | |
| 548 | + this.wifiError = '需授权获取 Wi-Fi 信息' | |
| 549 | + } else if (msg.indexOf('not connected') !== -1 || (err && Number(err.errCode) === 12002)) { | |
| 550 | + this.wifiError = '当前未连接 Wi-Fi' | |
| 551 | + } else { | |
| 552 | + this.wifiError = '无法获取 Wi-Fi 信息' | |
| 553 | + } | |
| 554 | + this.wifiSSID = '' | |
| 555 | + this.wifiBSSID = '' | |
| 556 | + resolve() | |
| 557 | + } | |
| 558 | + }) | |
| 559 | + }, | |
| 560 | + fail: (err) => { | |
| 561 | + this.wifiLoading = false | |
| 562 | + this.wifiSSID = '' | |
| 563 | + this.wifiBSSID = '' | |
| 564 | + const msg = (err && err.errMsg) ? String(err.errMsg) : '' | |
| 565 | + if (msg.indexOf('devtools') !== -1 || msg.indexOf('not support') !== -1) { | |
| 566 | + this.wifiError = '当前环境不支持 Wi-Fi,请用真机预览' | |
| 567 | + } else { | |
| 568 | + this.wifiError = '无法启动 Wi-Fi 模块,请用真机重试' | |
| 394 | 569 | } |
| 395 | - }) | |
| 396 | - }) | |
| 397 | - }, | |
| 398 | - openFenceMap() { | |
| 399 | - if (!this.canViewFenceMap) { | |
| 400 | - uni.showToast({ title: '暂无可展示的打卡范围', icon: 'none' }) | |
| 401 | - return | |
| 402 | - } | |
| 403 | - const query = [] | |
| 404 | - if (this.longitude != null) query.push(`longitude=${encodeURIComponent(this.longitude)}`) | |
| 405 | - if (this.latitude != null) query.push(`latitude=${encodeURIComponent(this.latitude)}`) | |
| 406 | - if (this.address) query.push(`address=${encodeURIComponent(this.address)}`) | |
| 407 | - if (this.isInFence !== null) query.push(`isInFence=${this.isInFence ? 1 : 0}`) | |
| 408 | - uni.navigateTo({ | |
| 409 | - url: `/pages/attendance-fence-map/attendance-fence-map${query.length ? `?${query.join('&')}` : ''}` | |
| 570 | + resolve() | |
| 571 | + } | |
| 410 | 572 | }) |
| 411 | - }, | |
| 412 | - evaluateFenceRange() { | |
| 413 | - if (!this.fenceEnabled) { | |
| 414 | - this.isInFence = true | |
| 415 | - return | |
| 416 | - } | |
| 417 | - if (this.longitude == null || this.latitude == null) { | |
| 418 | - this.isInFence = null | |
| 419 | - return | |
| 420 | - } | |
| 421 | - const polygons = parseFencePolygons(this.storeFence && this.storeFence.fencePolygons) | |
| 422 | - if (!polygons.length) { | |
| 423 | - this.isInFence = true | |
| 424 | - return | |
| 425 | - } | |
| 426 | - this.isInFence = polygons.some((polygon) => this.isPointInPolygon(this.longitude, this.latitude, polygon)) | |
| 427 | - }, | |
| 428 | - isPointInPolygon(lng, lat, polygon) { | |
| 429 | - return isPointInPolygon(lng, lat, polygon) | |
| 430 | - }, | |
| 431 | - async afterReadPhoto(event) { | |
| 432 | - const lists = [].concat(event.file) | |
| 433 | - const file = lists[0] | |
| 434 | - let fileObj = file | |
| 435 | - if (file.url && !file.path && !file.tempFilePath) { | |
| 436 | - fileObj = { tempFilePath: file.url } | |
| 437 | - } | |
| 438 | - try { | |
| 439 | - uni.showLoading({ title: '上传中' }) | |
| 440 | - const result = await this.API.uploadFile(fileObj) | |
| 441 | - uni.hideLoading() | |
| 442 | - if (result && result.code === 200 && result.data && result.data.url) { | |
| 443 | - this.photoUrl = result.data.url | |
| 444 | - this.photoFileList = [{ | |
| 445 | - url: result.data.url, | |
| 446 | - status: 'success' | |
| 447 | - }] | |
| 448 | - } else { | |
| 449 | - uni.showToast({ title: (result && result.msg) || '上传失败', icon: 'none' }) | |
| 573 | + // #endif | |
| 574 | + }) | |
| 575 | + }, | |
| 576 | + refreshLocation() { | |
| 577 | + this.locating = true | |
| 578 | + return new Promise((resolve) => { | |
| 579 | + uni.getLocation({ | |
| 580 | + type: 'gcj02', | |
| 581 | + isHighAccuracy: true, | |
| 582 | + success: (res) => { | |
| 583 | + this.longitude = res.longitude | |
| 584 | + this.latitude = res.latitude | |
| 585 | + this.address = '' | |
| 586 | + this.evaluateFenceRange() | |
| 587 | + this.locating = false | |
| 588 | + resolve() | |
| 589 | + }, | |
| 590 | + fail: () => { | |
| 591 | + this.locating = false | |
| 592 | + this.isInFence = null | |
| 593 | + uni.showToast({ title: '定位失败,请检查系统定位权限', icon: 'none' }) | |
| 594 | + resolve() | |
| 450 | 595 | } |
| 451 | - } catch (err) { | |
| 452 | - uni.hideLoading() | |
| 453 | - uni.showToast({ title: '上传失败', icon: 'none' }) | |
| 454 | - } | |
| 455 | - }, | |
| 456 | - deletePhoto() { | |
| 457 | - this.photoUrl = '' | |
| 458 | - this.photoFileList = [] | |
| 459 | - }, | |
| 460 | - async doPunch(direction) { | |
| 461 | - if (!this.userInfo.userId) { | |
| 462 | - uni.showToast({ title: '请先登录', icon: 'none' }) | |
| 463 | - return | |
| 596 | + }) | |
| 597 | + }) | |
| 598 | + }, | |
| 599 | + openFenceMap() { | |
| 600 | + if (!this.canViewFenceMap) { | |
| 601 | + uni.showToast({ title: '暂无可展示的打卡范围', icon: 'none' }) | |
| 602 | + return | |
| 603 | + } | |
| 604 | + const query = [] | |
| 605 | + if (this.longitude != null) query.push(`longitude=${encodeURIComponent(this.longitude)}`) | |
| 606 | + if (this.latitude != null) query.push(`latitude=${encodeURIComponent(this.latitude)}`) | |
| 607 | + if (this.address) query.push(`address=${encodeURIComponent(this.address)}`) | |
| 608 | + if (this.isInFence !== null) query.push(`isInFence=${this.isInFence ? 1 : 0}`) | |
| 609 | + uni.navigateTo({ | |
| 610 | + url: `/pages/attendance-fence-map/attendance-fence-map${query.length ? `?${query.join('&')}` : ''}` | |
| 611 | + }) | |
| 612 | + }, | |
| 613 | + evaluateFenceRange() { | |
| 614 | + if (!this.fenceEnabled) { | |
| 615 | + this.isInFence = true | |
| 616 | + return | |
| 617 | + } | |
| 618 | + if (this.longitude == null || this.latitude == null) { | |
| 619 | + this.isInFence = null | |
| 620 | + return | |
| 621 | + } | |
| 622 | + const polygons = parseFencePolygons(this.storeFence && this.storeFence.fencePolygons) | |
| 623 | + if (!polygons.length) { | |
| 624 | + this.isInFence = true | |
| 625 | + return | |
| 626 | + } | |
| 627 | + this.isInFence = polygons.some((polygon) => this.isPointInPolygon(this.longitude, this.latitude, polygon)) | |
| 628 | + }, | |
| 629 | + isPointInPolygon(lng, lat, polygon) { | |
| 630 | + return isPointInPolygon(lng, lat, polygon) | |
| 631 | + }, | |
| 632 | + async afterReadPhoto(event) { | |
| 633 | + const lists = [].concat(event.file) | |
| 634 | + const file = lists[0] | |
| 635 | + let fileObj = file | |
| 636 | + if (file.url && !file.path && !file.tempFilePath) { | |
| 637 | + fileObj = { tempFilePath: file.url } | |
| 638 | + } | |
| 639 | + try { | |
| 640 | + uni.showLoading({ title: '上传中' }) | |
| 641 | + const result = await this.API.uploadFile(fileObj) | |
| 642 | + uni.hideLoading() | |
| 643 | + if (result && result.code === 200 && result.data && result.data.url) { | |
| 644 | + this.photoUrl = result.data.url | |
| 645 | + this.photoFileList = [{ | |
| 646 | + url: result.data.url, | |
| 647 | + status: 'success' | |
| 648 | + }] | |
| 649 | + } else { | |
| 650 | + uni.showToast({ title: (result && result.msg) || '上传失败', icon: 'none' }) | |
| 464 | 651 | } |
| 652 | + } catch (err) { | |
| 653 | + uni.hideLoading() | |
| 654 | + uni.showToast({ title: '上传失败', icon: 'none' }) | |
| 655 | + } | |
| 656 | + }, | |
| 657 | + deletePhoto() { | |
| 658 | + this.photoUrl = '' | |
| 659 | + this.photoFileList = [] | |
| 660 | + }, | |
| 661 | + async doPunch(direction) { | |
| 662 | + if (!this.userInfo.userId) { | |
| 663 | + uni.showToast({ title: '请先登录', icon: 'none' }) | |
| 664 | + return | |
| 665 | + } | |
| 666 | + const isNormal = Number(this.punchType) === 1 | |
| 667 | + const needGps = | |
| 668 | + !isNormal || | |
| 669 | + !!(this.storeFence && this.storeFence.requireFenceCheck) | |
| 670 | + if (needGps) { | |
| 465 | 671 | if (this.longitude == null || this.latitude == null) { |
| 466 | 672 | await this.refreshLocation() |
| 467 | - if (this.longitude == null || this.latitude == null) { | |
| 468 | - uni.showToast({ title: '请先完成定位', icon: 'none' }) | |
| 469 | - return | |
| 470 | - } | |
| 471 | 673 | } |
| 472 | - const isFieldWork = Number(this.punchType) === 2 | |
| 473 | - const payload = { | |
| 474 | - userId: this.userInfo.userId, | |
| 475 | - punchDirection: direction, | |
| 476 | - punchType: Number(this.punchType), | |
| 477 | - longitude: this.longitude, | |
| 478 | - latitude: this.latitude, | |
| 479 | - address: this.address || this.locationText, | |
| 480 | - photoUrl: this.photoUrl || undefined, | |
| 481 | - remark: isFieldWork ? ((this.remark || '').trim() || undefined) : undefined | |
| 674 | + if (this.longitude == null || this.latitude == null) { | |
| 675 | + uni.showToast({ title: '请先完成定位', icon: 'none' }) | |
| 676 | + return | |
| 482 | 677 | } |
| 483 | - this.submitting = true | |
| 484 | - try { | |
| 485 | - const res = await this.API.submitAttendancePunch(payload) | |
| 486 | - if (res && Number(res.code) === 200) { | |
| 487 | - uni.showToast({ title: '打卡成功', icon: 'success' }) | |
| 488 | - this.remark = '' | |
| 489 | - this.photoUrl = '' | |
| 490 | - this.photoFileList = [] | |
| 491 | - await this.loadDetail() | |
| 492 | - } else { | |
| 493 | - uni.showToast({ title: (res && (res.msg || res.message)) || '打卡失败', icon: 'none' }) | |
| 494 | - } | |
| 495 | - } catch (e) { | |
| 496 | - uni.showToast({ title: '打卡失败', icon: 'none' }) | |
| 497 | - } finally { | |
| 498 | - this.submitting = false | |
| 678 | + } else if (isNormal && this.longitude == null) { | |
| 679 | + await this.refreshLocation() | |
| 680 | + } | |
| 681 | + const isFieldWork = Number(this.punchType) === 2 | |
| 682 | + const payload = { | |
| 683 | + userId: this.userInfo.userId, | |
| 684 | + punchDirection: direction, | |
| 685 | + punchType: Number(this.punchType), | |
| 686 | + longitude: this.longitude != null ? this.longitude : undefined, | |
| 687 | + latitude: this.latitude != null ? this.latitude : undefined, | |
| 688 | + address: this.address || this.locationText, | |
| 689 | + wifiSsid: (this.wifiSSID || '').trim() || undefined, | |
| 690 | + wifiBssid: (this.wifiBSSID || '').trim() || undefined, | |
| 691 | + photoUrl: this.photoUrl || undefined, | |
| 692 | + remark: isFieldWork ? ((this.remark || '').trim() || undefined) : undefined | |
| 693 | + } | |
| 694 | + this.submitting = true | |
| 695 | + try { | |
| 696 | + const res = await this.API.submitAttendancePunch(payload) | |
| 697 | + if (res && Number(res.code) === 200) { | |
| 698 | + uni.showToast({ title: '打卡成功', icon: 'success' }) | |
| 699 | + this.remark = '' | |
| 700 | + this.photoUrl = '' | |
| 701 | + this.photoFileList = [] | |
| 702 | + await this.loadDetail() | |
| 703 | + } else { | |
| 704 | + uni.showToast({ title: (res && (res.msg || res.message)) || '打卡失败', icon: 'none' }) | |
| 499 | 705 | } |
| 706 | + } catch (e) { | |
| 707 | + uni.showToast({ title: '打卡失败', icon: 'none' }) | |
| 708 | + } finally { | |
| 709 | + this.submitting = false | |
| 500 | 710 | } |
| 501 | 711 | } |
| 502 | 712 | } |
| 713 | +} | |
| 503 | 714 | </script> |
| 504 | 715 | |
| 505 | 716 | <style scoped lang="scss"> |
| 506 | - /* 与 pages/index 首页:浅绿底 #e8f5e9、渐变顶栏、主色 #43a047 */ | |
| 507 | - .att-page { | |
| 508 | - min-height: 100vh; | |
| 509 | - background: #e8f5e9; | |
| 510 | - padding-bottom: constant(safe-area-inset-bottom); | |
| 511 | - padding-bottom: env(safe-area-inset-bottom); | |
| 512 | - box-sizing: border-box; | |
| 513 | - } | |
| 717 | +/* 与 pages/index 首页:浅绿底 #e8f5e9、渐变顶栏、主色 #43a047 */ | |
| 718 | +.att-page { | |
| 719 | + min-height: 100vh; | |
| 720 | + background: #e8f5e9; | |
| 721 | + padding-bottom: constant(safe-area-inset-bottom); | |
| 722 | + padding-bottom: env(safe-area-inset-bottom); | |
| 723 | + box-sizing: border-box; | |
| 724 | +} | |
| 514 | 725 | |
| 515 | - .att-header { | |
| 516 | - background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%); | |
| 517 | - padding: 40rpx 32rpx 48rpx; | |
| 518 | - text-align: center; | |
| 519 | - box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.12); | |
| 520 | - } | |
| 726 | +.att-header { | |
| 727 | + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%); | |
| 728 | + padding: 40rpx 32rpx 48rpx; | |
| 729 | + text-align: center; | |
| 730 | + box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.12); | |
| 731 | +} | |
| 521 | 732 | |
| 522 | - .att-clock { | |
| 523 | - font-size: 88rpx; | |
| 524 | - font-weight: 300; | |
| 525 | - color: #ffffff; | |
| 526 | - letter-spacing: 4rpx; | |
| 527 | - font-variant-numeric: tabular-nums; | |
| 528 | - line-height: 1.1; | |
| 529 | - text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); | |
| 530 | - } | |
| 733 | +.att-clock { | |
| 734 | + font-size: 88rpx; | |
| 735 | + font-weight: 300; | |
| 736 | + color: #ffffff; | |
| 737 | + letter-spacing: 4rpx; | |
| 738 | + font-variant-numeric: tabular-nums; | |
| 739 | + line-height: 1.1; | |
| 740 | + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); | |
| 741 | +} | |
| 531 | 742 | |
| 532 | - .att-meta { | |
| 533 | - margin-top: 16rpx; | |
| 534 | - display: flex; | |
| 535 | - align-items: center; | |
| 536 | - justify-content: center; | |
| 537 | - gap: 16rpx; | |
| 538 | - } | |
| 743 | +.att-meta { | |
| 744 | + margin-top: 16rpx; | |
| 745 | + display: flex; | |
| 746 | + align-items: center; | |
| 747 | + justify-content: center; | |
| 748 | + gap: 16rpx; | |
| 749 | +} | |
| 539 | 750 | |
| 540 | - .att-date { | |
| 541 | - font-size: 28rpx; | |
| 542 | - color: rgba(255, 255, 255, 0.92); | |
| 543 | - } | |
| 751 | +.att-date { | |
| 752 | + font-size: 28rpx; | |
| 753 | + color: rgba(255, 255, 255, 0.92); | |
| 754 | +} | |
| 544 | 755 | |
| 545 | - .att-week { | |
| 546 | - font-size: 28rpx; | |
| 547 | - color: #e8f5e9; | |
| 548 | - font-weight: 500; | |
| 549 | - } | |
| 756 | +.att-week { | |
| 757 | + font-size: 28rpx; | |
| 758 | + color: #e8f5e9; | |
| 759 | + font-weight: 500; | |
| 760 | +} | |
| 550 | 761 | |
| 551 | - .att-user { | |
| 552 | - margin-top: 12rpx; | |
| 553 | - font-size: 26rpx; | |
| 554 | - color: rgba(255, 255, 255, 0.88); | |
| 555 | - } | |
| 762 | +.att-user { | |
| 763 | + margin-top: 12rpx; | |
| 764 | + font-size: 26rpx; | |
| 765 | + color: rgba(255, 255, 255, 0.88); | |
| 766 | +} | |
| 556 | 767 | |
| 557 | - .att-rule { | |
| 558 | - margin-top: 20rpx; | |
| 559 | - display: flex; | |
| 560 | - align-items: center; | |
| 561 | - justify-content: center; | |
| 562 | - gap: 12rpx; | |
| 563 | - font-size: 24rpx; | |
| 564 | - } | |
| 768 | +.att-rule { | |
| 769 | + margin-top: 20rpx; | |
| 770 | + display: flex; | |
| 771 | + align-items: center; | |
| 772 | + justify-content: center; | |
| 773 | + gap: 12rpx; | |
| 774 | + font-size: 24rpx; | |
| 775 | +} | |
| 565 | 776 | |
| 566 | - .att-rule-label { | |
| 567 | - color: rgba(255, 255, 255, 0.75); | |
| 568 | - } | |
| 777 | +.att-rule-label { | |
| 778 | + color: rgba(255, 255, 255, 0.75); | |
| 779 | +} | |
| 569 | 780 | |
| 570 | - .att-rule-val { | |
| 571 | - color: #ffffff; | |
| 572 | - font-weight: 600; | |
| 573 | - } | |
| 781 | +.att-rule-val { | |
| 782 | + color: #ffffff; | |
| 783 | + font-weight: 600; | |
| 784 | +} | |
| 574 | 785 | |
| 575 | - .att-body { | |
| 576 | - margin-top: -28rpx; | |
| 577 | - padding: 0 32rpx 32rpx; | |
| 578 | - position: relative; | |
| 579 | - z-index: 2; | |
| 580 | - } | |
| 786 | +.att-body { | |
| 787 | + margin-top: -28rpx; | |
| 788 | + padding: 0 32rpx 32rpx; | |
| 789 | + position: relative; | |
| 790 | + z-index: 2; | |
| 791 | +} | |
| 581 | 792 | |
| 582 | - .att-card { | |
| 583 | - background: #ffffff; | |
| 584 | - border-radius: 20rpx; | |
| 585 | - padding: 28rpx; | |
| 586 | - margin-bottom: 24rpx; | |
| 587 | - box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.08); | |
| 588 | - } | |
| 793 | +.att-card { | |
| 794 | + background: #ffffff; | |
| 795 | + border-radius: 20rpx; | |
| 796 | + padding: 28rpx; | |
| 797 | + margin-bottom: 24rpx; | |
| 798 | + box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.08); | |
| 799 | +} | |
| 589 | 800 | |
| 590 | - .att-seg-card { | |
| 591 | - padding: 20rpx 24rpx; | |
| 592 | - } | |
| 801 | +.att-seg-card { | |
| 802 | + padding: 20rpx 24rpx; | |
| 803 | +} | |
| 593 | 804 | |
| 594 | - .att-seg { | |
| 595 | - display: flex; | |
| 596 | - background: linear-gradient(135deg, #e8f5e9 60%, #c8e6c9 100%); | |
| 597 | - border-radius: 16rpx; | |
| 598 | - padding: 8rpx; | |
| 599 | - } | |
| 805 | +.att-seg { | |
| 806 | + display: flex; | |
| 807 | + background: linear-gradient(135deg, #e8f5e9 60%, #c8e6c9 100%); | |
| 808 | + border-radius: 16rpx; | |
| 809 | + padding: 8rpx; | |
| 810 | +} | |
| 600 | 811 | |
| 601 | - .att-seg-item { | |
| 602 | - flex: 1; | |
| 603 | - text-align: center; | |
| 604 | - padding: 20rpx 0; | |
| 605 | - font-size: 28rpx; | |
| 606 | - color: #6a9c6a; | |
| 607 | - border-radius: 12rpx; | |
| 608 | - } | |
| 812 | +.att-seg-item { | |
| 813 | + flex: 1; | |
| 814 | + text-align: center; | |
| 815 | + padding: 20rpx 0; | |
| 816 | + font-size: 28rpx; | |
| 817 | + color: #6a9c6a; | |
| 818 | + border-radius: 12rpx; | |
| 819 | +} | |
| 609 | 820 | |
| 610 | - .att-seg-item.is-active { | |
| 611 | - background: #ffffff; | |
| 612 | - color: #43a047; | |
| 613 | - font-weight: 600; | |
| 614 | - box-shadow: 0 2rpx 12rpx rgba(67, 160, 71, 0.15); | |
| 615 | - } | |
| 821 | +.att-seg-item.is-active { | |
| 822 | + background: #ffffff; | |
| 823 | + color: #43a047; | |
| 824 | + font-weight: 600; | |
| 825 | + box-shadow: 0 2rpx 12rpx rgba(67, 160, 71, 0.15); | |
| 826 | +} | |
| 616 | 827 | |
| 617 | - .att-punch-card { | |
| 618 | - text-align: center; | |
| 619 | - padding: 40rpx 28rpx 36rpx; | |
| 620 | - } | |
| 828 | +.att-punch-card { | |
| 829 | + text-align: center; | |
| 830 | + padding: 40rpx 28rpx 36rpx; | |
| 831 | +} | |
| 621 | 832 | |
| 622 | - .att-big-btn { | |
| 623 | - width: 300rpx; | |
| 624 | - height: 300rpx; | |
| 625 | - margin: 0 auto; | |
| 626 | - border-radius: 50%; | |
| 627 | - display: flex; | |
| 628 | - flex-direction: column; | |
| 629 | - align-items: center; | |
| 630 | - justify-content: center; | |
| 631 | - background: linear-gradient(145deg, #66bb6a 0%, #43a047 45%, #388e3c 100%); | |
| 632 | - box-shadow: 0 12rpx 40rpx 0 rgba(67, 160, 71, 0.35); | |
| 633 | - transition: transform 0.15s, opacity 0.15s; | |
| 634 | - } | |
| 833 | +.att-big-btn { | |
| 834 | + width: 300rpx; | |
| 835 | + height: 300rpx; | |
| 836 | + margin: 0 auto; | |
| 837 | + border-radius: 50%; | |
| 838 | + display: flex; | |
| 839 | + flex-direction: column; | |
| 840 | + align-items: center; | |
| 841 | + justify-content: center; | |
| 842 | + background: linear-gradient(145deg, #66bb6a 0%, #43a047 45%, #388e3c 100%); | |
| 843 | + box-shadow: 0 12rpx 40rpx 0 rgba(67, 160, 71, 0.35); | |
| 844 | + transition: transform 0.15s, opacity 0.15s; | |
| 845 | +} | |
| 635 | 846 | |
| 636 | - .att-big-btn:active:not(.is-disabled):not(.is-all-done) { | |
| 637 | - transform: scale(0.97); | |
| 638 | - } | |
| 847 | +.att-big-btn:active:not(.is-disabled):not(.is-all-done) { | |
| 848 | + transform: scale(0.97); | |
| 849 | +} | |
| 639 | 850 | |
| 640 | - .att-big-btn.is-disabled:not(.is-all-done) { | |
| 641 | - opacity: 0.5; | |
| 642 | - box-shadow: none; | |
| 643 | - } | |
| 851 | +.att-big-btn.is-disabled:not(.is-all-done) { | |
| 852 | + opacity: 0.5; | |
| 853 | + box-shadow: none; | |
| 854 | +} | |
| 644 | 855 | |
| 645 | - .att-big-btn.is-all-done { | |
| 646 | - background: linear-gradient(145deg, #81c784 0%, #66bb6a 100%); | |
| 647 | - box-shadow: 0 8rpx 28rpx 0 rgba(67, 160, 71, 0.25); | |
| 648 | - } | |
| 856 | +.att-big-btn.is-all-done { | |
| 857 | + background: linear-gradient(145deg, #81c784 0%, #66bb6a 100%); | |
| 858 | + box-shadow: 0 8rpx 28rpx 0 rgba(67, 160, 71, 0.25); | |
| 859 | +} | |
| 649 | 860 | |
| 650 | - .att-big-title { | |
| 651 | - font-size: 40rpx; | |
| 652 | - font-weight: 700; | |
| 653 | - color: #ffffff; | |
| 654 | - letter-spacing: 2rpx; | |
| 655 | - } | |
| 861 | +.att-big-title { | |
| 862 | + font-size: 40rpx; | |
| 863 | + font-weight: 700; | |
| 864 | + color: #ffffff; | |
| 865 | + letter-spacing: 2rpx; | |
| 866 | +} | |
| 656 | 867 | |
| 657 | - .att-big-sub { | |
| 658 | - margin-top: 16rpx; | |
| 659 | - font-size: 24rpx; | |
| 660 | - color: rgba(255, 255, 255, 0.9); | |
| 661 | - } | |
| 868 | +.att-big-sub { | |
| 869 | + margin-top: 16rpx; | |
| 870 | + font-size: 24rpx; | |
| 871 | + color: rgba(255, 255, 255, 0.9); | |
| 872 | +} | |
| 662 | 873 | |
| 663 | - .att-big-done-text { | |
| 664 | - margin-top: 12rpx; | |
| 665 | - font-size: 28rpx; | |
| 666 | - font-weight: 600; | |
| 667 | - color: #ffffff; | |
| 668 | - } | |
| 874 | +.att-big-done-text { | |
| 875 | + margin-top: 12rpx; | |
| 876 | + font-size: 28rpx; | |
| 877 | + font-weight: 600; | |
| 878 | + color: #ffffff; | |
| 879 | +} | |
| 669 | 880 | |
| 670 | - .att-next-hint { | |
| 671 | - margin-top: 24rpx; | |
| 672 | - font-size: 24rpx; | |
| 673 | - color: #388e3c; | |
| 674 | - font-weight: 500; | |
| 675 | - } | |
| 881 | +.att-next-hint { | |
| 882 | + margin-top: 24rpx; | |
| 883 | + font-size: 24rpx; | |
| 884 | + color: #388e3c; | |
| 885 | + font-weight: 500; | |
| 886 | +} | |
| 676 | 887 | |
| 677 | - .att-loc-card { | |
| 678 | - display: flex; | |
| 679 | - align-items: flex-start; | |
| 680 | - gap: 16rpx; | |
| 681 | - } | |
| 888 | +.att-loc-card { | |
| 889 | + display: flex; | |
| 890 | + align-items: flex-start; | |
| 891 | + gap: 16rpx; | |
| 892 | +} | |
| 682 | 893 | |
| 683 | - .att-loc-main { | |
| 684 | - flex: 1; | |
| 685 | - min-width: 0; | |
| 686 | - } | |
| 894 | +.att-loc-main { | |
| 895 | + flex: 1; | |
| 896 | + min-width: 0; | |
| 897 | +} | |
| 687 | 898 | |
| 688 | - .att-loc-text { | |
| 689 | - font-size: 26rpx; | |
| 690 | - color: #333333; | |
| 691 | - line-height: 1.5; | |
| 692 | - word-break: break-all; | |
| 693 | - } | |
| 899 | +.att-loc-text { | |
| 900 | + font-size: 26rpx; | |
| 901 | + color: #333333; | |
| 902 | + line-height: 1.5; | |
| 903 | + word-break: break-all; | |
| 904 | +} | |
| 694 | 905 | |
| 695 | - .att-loc-tip { | |
| 696 | - display: block; | |
| 697 | - margin-top: 8rpx; | |
| 698 | - font-size: 22rpx; | |
| 699 | - color: #6a9c6a; | |
| 700 | - } | |
| 906 | +.att-loc-tip { | |
| 907 | + display: block; | |
| 908 | + margin-top: 8rpx; | |
| 909 | + font-size: 22rpx; | |
| 910 | + color: #6a9c6a; | |
| 911 | +} | |
| 701 | 912 | |
| 702 | - .att-loc-actions { | |
| 703 | - display: flex; | |
| 704 | - align-items: center; | |
| 705 | - flex-wrap: wrap; | |
| 706 | - gap: 16rpx; | |
| 707 | - margin-top: 16rpx; | |
| 708 | - } | |
| 913 | +.att-loc-actions { | |
| 914 | + display: flex; | |
| 915 | + align-items: center; | |
| 916 | + flex-wrap: wrap; | |
| 917 | + gap: 16rpx; | |
| 918 | + margin-top: 16rpx; | |
| 919 | +} | |
| 709 | 920 | |
| 710 | - .att-loc-action { | |
| 711 | - font-size: 22rpx; | |
| 712 | - color: #7f8c8d; | |
| 713 | - background: #f5f7fa; | |
| 714 | - border-radius: 999rpx; | |
| 715 | - padding: 10rpx 20rpx; | |
| 716 | - line-height: 1; | |
| 717 | - } | |
| 921 | +.att-loc-action { | |
| 922 | + font-size: 22rpx; | |
| 923 | + color: #7f8c8d; | |
| 924 | + background: #f5f7fa; | |
| 925 | + border-radius: 999rpx; | |
| 926 | + padding: 10rpx 20rpx; | |
| 927 | + line-height: 1; | |
| 928 | +} | |
| 718 | 929 | |
| 719 | - .att-loc-action--primary { | |
| 720 | - color: #43a047; | |
| 721 | - background: rgba(67, 160, 71, 0.12); | |
| 722 | - } | |
| 930 | +.att-loc-action--primary { | |
| 931 | + color: #43a047; | |
| 932 | + background: rgba(67, 160, 71, 0.12); | |
| 933 | +} | |
| 723 | 934 | |
| 724 | - .att-card-head { | |
| 725 | - display: flex; | |
| 726 | - align-items: center; | |
| 727 | - justify-content: space-between; | |
| 728 | - margin-bottom: 24rpx; | |
| 729 | - padding-bottom: 20rpx; | |
| 730 | - border-bottom: 1rpx solid #e8f5e9; | |
| 731 | - } | |
| 935 | +.att-card-head { | |
| 936 | + display: flex; | |
| 937 | + align-items: center; | |
| 938 | + justify-content: space-between; | |
| 939 | + margin-bottom: 24rpx; | |
| 940 | + padding-bottom: 20rpx; | |
| 941 | + border-bottom: 1rpx solid #e8f5e9; | |
| 942 | +} | |
| 732 | 943 | |
| 733 | - .att-card-title { | |
| 734 | - font-size: 30rpx; | |
| 735 | - font-weight: bold; | |
| 736 | - color: #388e3c; | |
| 737 | - letter-spacing: 1rpx; | |
| 738 | - } | |
| 944 | +.att-card-title { | |
| 945 | + font-size: 30rpx; | |
| 946 | + font-weight: bold; | |
| 947 | + color: #388e3c; | |
| 948 | + letter-spacing: 1rpx; | |
| 949 | +} | |
| 739 | 950 | |
| 740 | - .att-status-tag { | |
| 741 | - font-size: 22rpx; | |
| 742 | - color: #43a047; | |
| 743 | - background: rgba(67, 160, 71, 0.12); | |
| 744 | - padding: 8rpx 20rpx; | |
| 745 | - border-radius: 8rpx; | |
| 746 | - } | |
| 951 | +.att-status-tag { | |
| 952 | + font-size: 22rpx; | |
| 953 | + color: #43a047; | |
| 954 | + background: rgba(67, 160, 71, 0.12); | |
| 955 | + padding: 8rpx 20rpx; | |
| 956 | + border-radius: 8rpx; | |
| 957 | +} | |
| 747 | 958 | |
| 748 | - .att-timeline { | |
| 749 | - position: relative; | |
| 750 | - padding-left: 8rpx; | |
| 751 | - } | |
| 959 | +.att-timeline { | |
| 960 | + position: relative; | |
| 961 | + padding-left: 8rpx; | |
| 962 | +} | |
| 752 | 963 | |
| 753 | - .att-tl-item { | |
| 754 | - display: flex; | |
| 755 | - flex-direction: row; | |
| 756 | - align-items: flex-start; | |
| 757 | - position: relative; | |
| 758 | - min-height: 100rpx; | |
| 759 | - overflow: visible; | |
| 760 | - } | |
| 964 | +.att-tl-item { | |
| 965 | + display: flex; | |
| 966 | + flex-direction: row; | |
| 967 | + align-items: flex-start; | |
| 968 | + position: relative; | |
| 969 | + min-height: 100rpx; | |
| 970 | + overflow: visible; | |
| 971 | +} | |
| 761 | 972 | |
| 762 | - .att-tl-item--last { | |
| 763 | - min-height: auto; | |
| 764 | - } | |
| 973 | +.att-tl-item--last { | |
| 974 | + min-height: auto; | |
| 975 | +} | |
| 765 | 976 | |
| 766 | - .att-tl-dot { | |
| 767 | - width: 20rpx; | |
| 768 | - height: 20rpx; | |
| 769 | - border-radius: 50%; | |
| 770 | - background: #e0e0e0; | |
| 771 | - margin-top: 8rpx; | |
| 772 | - flex-shrink: 0; | |
| 773 | - z-index: 1; | |
| 774 | - } | |
| 977 | +.att-tl-dot { | |
| 978 | + width: 20rpx; | |
| 979 | + height: 20rpx; | |
| 980 | + border-radius: 50%; | |
| 981 | + background: #e0e0e0; | |
| 982 | + margin-top: 8rpx; | |
| 983 | + flex-shrink: 0; | |
| 984 | + z-index: 1; | |
| 985 | +} | |
| 775 | 986 | |
| 776 | - .att-tl-dot.is-on { | |
| 777 | - background: #43a047; | |
| 778 | - box-shadow: 0 0 0 6rpx rgba(67, 160, 71, 0.2); | |
| 779 | - } | |
| 987 | +.att-tl-dot.is-on { | |
| 988 | + background: #43a047; | |
| 989 | + box-shadow: 0 0 0 6rpx rgba(67, 160, 71, 0.2); | |
| 990 | +} | |
| 780 | 991 | |
| 781 | - .att-tl-line { | |
| 782 | - position: absolute; | |
| 783 | - left: 9rpx; | |
| 784 | - top: 28rpx; | |
| 785 | - width: 2rpx; | |
| 786 | - height: 72rpx; | |
| 787 | - background: #e8f5e9; | |
| 788 | - } | |
| 992 | +.att-tl-line { | |
| 993 | + position: absolute; | |
| 994 | + left: 9rpx; | |
| 995 | + top: 28rpx; | |
| 996 | + width: 2rpx; | |
| 997 | + height: 72rpx; | |
| 998 | + background: #e8f5e9; | |
| 999 | +} | |
| 789 | 1000 | |
| 790 | - .att-tl-body { | |
| 791 | - flex: 1; | |
| 792 | - margin-left: 24rpx; | |
| 793 | - padding-bottom: 28rpx; | |
| 794 | - } | |
| 1001 | +.att-tl-body { | |
| 1002 | + flex: 1; | |
| 1003 | + margin-left: 24rpx; | |
| 1004 | + padding-bottom: 28rpx; | |
| 1005 | +} | |
| 795 | 1006 | |
| 796 | - .att-tl-item--last .att-tl-body { | |
| 797 | - padding-bottom: 0; | |
| 798 | - } | |
| 1007 | +.att-tl-item--last .att-tl-body { | |
| 1008 | + padding-bottom: 0; | |
| 1009 | +} | |
| 799 | 1010 | |
| 800 | - .att-tl-row { | |
| 801 | - display: flex; | |
| 802 | - justify-content: space-between; | |
| 803 | - align-items: center; | |
| 804 | - gap: 16rpx; | |
| 805 | - } | |
| 1011 | +.att-tl-row { | |
| 1012 | + display: flex; | |
| 1013 | + justify-content: space-between; | |
| 1014 | + align-items: center; | |
| 1015 | + gap: 16rpx; | |
| 1016 | +} | |
| 806 | 1017 | |
| 807 | - .att-tl-name { | |
| 808 | - font-size: 28rpx; | |
| 809 | - color: #6a9c6a; | |
| 810 | - } | |
| 1018 | +.att-tl-name { | |
| 1019 | + font-size: 28rpx; | |
| 1020 | + color: #6a9c6a; | |
| 1021 | +} | |
| 811 | 1022 | |
| 812 | - .att-tl-time { | |
| 813 | - font-size: 28rpx; | |
| 814 | - color: #333333; | |
| 815 | - font-weight: 500; | |
| 816 | - text-align: right; | |
| 817 | - word-break: break-all; | |
| 818 | - } | |
| 1023 | +.att-tl-time { | |
| 1024 | + font-size: 28rpx; | |
| 1025 | + color: #333333; | |
| 1026 | + font-weight: 500; | |
| 1027 | + text-align: right; | |
| 1028 | + word-break: break-all; | |
| 1029 | +} | |
| 819 | 1030 | |
| 820 | - .att-empty { | |
| 821 | - font-size: 26rpx; | |
| 822 | - color: #909399; | |
| 823 | - line-height: 1.6; | |
| 824 | - text-align: center; | |
| 825 | - padding: 16rpx 0; | |
| 826 | - } | |
| 1031 | +.att-empty { | |
| 1032 | + font-size: 26rpx; | |
| 1033 | + color: #909399; | |
| 1034 | + line-height: 1.6; | |
| 1035 | + text-align: center; | |
| 1036 | + padding: 16rpx 0; | |
| 1037 | +} | |
| 827 | 1038 | |
| 828 | - .att-more-title { | |
| 829 | - font-size: 26rpx; | |
| 830 | - color: #6a9c6a; | |
| 831 | - margin-bottom: 20rpx; | |
| 832 | - font-weight: 500; | |
| 833 | - } | |
| 1039 | +.att-more-title { | |
| 1040 | + font-size: 26rpx; | |
| 1041 | + color: #6a9c6a; | |
| 1042 | + margin-bottom: 20rpx; | |
| 1043 | + font-weight: 500; | |
| 1044 | +} | |
| 834 | 1045 | |
| 835 | - .att-more-title--second { | |
| 836 | - margin-top: 28rpx; | |
| 837 | - margin-bottom: 16rpx; | |
| 838 | - } | |
| 1046 | +.att-more-title--second { | |
| 1047 | + margin-top: 28rpx; | |
| 1048 | + margin-bottom: 16rpx; | |
| 1049 | +} | |
| 839 | 1050 | |
| 840 | - .att-upload-wrap { | |
| 841 | - margin-top: 20rpx; | |
| 842 | - } | |
| 1051 | +.att-upload-wrap { | |
| 1052 | + margin-top: 20rpx; | |
| 1053 | +} | |
| 843 | 1054 | |
| 844 | - .att-safe { | |
| 845 | - height: 24rpx; | |
| 846 | - } | |
| 1055 | +.att-safe { | |
| 1056 | + height: 24rpx; | |
| 1057 | +} | |
| 847 | 1058 | |
| 848 | - .att-loading-mask { | |
| 849 | - position: fixed; | |
| 850 | - left: 0; | |
| 851 | - right: 0; | |
| 852 | - top: 0; | |
| 853 | - bottom: 0; | |
| 854 | - display: flex; | |
| 855 | - flex-direction: column; | |
| 856 | - align-items: center; | |
| 857 | - justify-content: center; | |
| 858 | - background: rgba(255, 255, 255, 0.75); | |
| 859 | - z-index: 999; | |
| 860 | - } | |
| 1059 | +.att-loading-mask { | |
| 1060 | + position: fixed; | |
| 1061 | + left: 0; | |
| 1062 | + right: 0; | |
| 1063 | + top: 0; | |
| 1064 | + bottom: 0; | |
| 1065 | + display: flex; | |
| 1066 | + flex-direction: column; | |
| 1067 | + align-items: center; | |
| 1068 | + justify-content: center; | |
| 1069 | + background: rgba(255, 255, 255, 0.75); | |
| 1070 | + z-index: 999; | |
| 1071 | +} | |
| 861 | 1072 | |
| 862 | - .att-loading-txt { | |
| 863 | - margin-top: 16rpx; | |
| 864 | - font-size: 26rpx; | |
| 865 | - color: #6a9c6a; | |
| 866 | - } | |
| 1073 | +.att-loading-txt { | |
| 1074 | + margin-top: 16rpx; | |
| 1075 | + font-size: 26rpx; | |
| 1076 | + color: #6a9c6a; | |
| 1077 | +} | |
| 867 | 1078 | </style> | ... | ... |
绿纤uni-app/pagesA/my-application-list/my-application-list.vue
| ... | ... | @@ -5,6 +5,18 @@ |
| 5 | 5 | <view class="header-sub">查看我发起的流程申请,支持进入详情查看表单与审批进度</view> |
| 6 | 6 | </view> |
| 7 | 7 | |
| 8 | + <view class="filter-bar"> | |
| 9 | + <view | |
| 10 | + v-for="tab in statusTabs" | |
| 11 | + :key="tab.key" | |
| 12 | + class="filter-item" | |
| 13 | + :class="statusFilter === tab.key ? 'filter-item--active' : ''" | |
| 14 | + @tap="changeStatusFilter(tab.key)" | |
| 15 | + > | |
| 16 | + {{ tab.label }} | |
| 17 | + </view> | |
| 18 | + </view> | |
| 19 | + | |
| 8 | 20 | <view v-if="loading && !list.length" class="state-card"> |
| 9 | 21 | <text class="state-text">加载中...</text> |
| 10 | 22 | </view> |
| ... | ... | @@ -63,6 +75,13 @@ export default { |
| 63 | 75 | return { |
| 64 | 76 | loading: false, |
| 65 | 77 | list: [], |
| 78 | + statusFilter: 'all', | |
| 79 | + statusTabs: [ | |
| 80 | + { key: 'all', label: '全部' }, | |
| 81 | + { key: 'running', label: '审核中' }, | |
| 82 | + { key: 'passed', label: '已通过' }, | |
| 83 | + { key: 'rejected', label: '不通过' } | |
| 84 | + ], | |
| 66 | 85 | query: { |
| 67 | 86 | currentPage: 1, |
| 68 | 87 | pageSize: 20, |
| ... | ... | @@ -73,8 +92,14 @@ export default { |
| 73 | 92 | } |
| 74 | 93 | }, |
| 75 | 94 | computed: { |
| 95 | + filteredList() { | |
| 96 | + if (!Array.isArray(this.list)) return [] | |
| 97 | + const st = this.getStatusFilterNumber(this.statusFilter) | |
| 98 | + if (st === null) return this.list | |
| 99 | + return this.list.filter(item => Number(item.status) === st) | |
| 100 | + }, | |
| 76 | 101 | displayList() { |
| 77 | - return (this.list || []).map(item => ({ | |
| 102 | + return (this.filteredList || []).map(item => ({ | |
| 78 | 103 | ...item, |
| 79 | 104 | statusClassName: this.statusClass(item.status), |
| 80 | 105 | statusText: this.getStatusText(item.status), |
| ... | ... | @@ -115,6 +140,11 @@ export default { |
| 115 | 140 | uni.stopPullDownRefresh() |
| 116 | 141 | } |
| 117 | 142 | }, |
| 143 | + changeStatusFilter(key) { | |
| 144 | + if (this.statusFilter === key) return | |
| 145 | + this.statusFilter = key | |
| 146 | + this.refresh() | |
| 147 | + }, | |
| 118 | 148 | refresh() { |
| 119 | 149 | this.query.currentPage = 1 |
| 120 | 150 | this.total = 0 |
| ... | ... | @@ -151,6 +181,15 @@ export default { |
| 151 | 181 | } |
| 152 | 182 | return map[Number(status)] || 'status-default' |
| 153 | 183 | }, |
| 184 | + // 业务状态:与 getStatusText / 后端 FlowTaskEntity.status 对齐 | |
| 185 | + // all: 全部;running: 待审核;passed: 已通过;rejected: 已驳回(不通过) | |
| 186 | + getStatusFilterNumber(key) { | |
| 187 | + if (key === 'all') return null | |
| 188 | + if (key === 'running') return 1 | |
| 189 | + if (key === 'passed') return 2 | |
| 190 | + if (key === 'rejected') return 3 | |
| 191 | + return null | |
| 192 | + }, | |
| 154 | 193 | getUrgentText(val) { |
| 155 | 194 | const map = { |
| 156 | 195 | 1: '普通', |
| ... | ... | @@ -192,6 +231,28 @@ export default { |
| 192 | 231 | box-sizing: border-box; |
| 193 | 232 | background: linear-gradient(180deg, #e8f5e9 0%, #f7fff8 100%); |
| 194 | 233 | } |
| 234 | +.filter-bar { | |
| 235 | + display: flex; | |
| 236 | + gap: 16rpx; | |
| 237 | + padding: 0 12rpx; | |
| 238 | + margin-bottom: 22rpx; | |
| 239 | +} | |
| 240 | +.filter-item { | |
| 241 | + flex: 1; | |
| 242 | + text-align: center; | |
| 243 | + padding: 14rpx 0; | |
| 244 | + border-radius: 999rpx; | |
| 245 | + background: rgba(255, 255, 255, 0.7); | |
| 246 | + color: #7a8b7b; | |
| 247 | + font-size: 24rpx; | |
| 248 | + font-weight: 600; | |
| 249 | + border: 1px solid rgba(46, 125, 50, 0.12); | |
| 250 | +} | |
| 251 | +.filter-item--active { | |
| 252 | + background: #e8f5e9; | |
| 253 | + color: #2e7d32; | |
| 254 | + border-color: rgba(46, 125, 50, 0.35); | |
| 255 | +} | |
| 195 | 256 | .header-card, |
| 196 | 257 | .state-card, |
| 197 | 258 | .apply-card { | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/pages/attendance-punch/attendance-punch.js
| ... | ... | @@ -134,6 +134,15 @@ var render = function () { |
| 134 | 134 | var _vm = this |
| 135 | 135 | var _h = _vm.$createElement |
| 136 | 136 | var _c = _vm._self._c || _h |
| 137 | + var m0 = Number(_vm.punchType) | |
| 138 | + _vm.$mp.data = Object.assign( | |
| 139 | + {}, | |
| 140 | + { | |
| 141 | + $root: { | |
| 142 | + m0: m0, | |
| 143 | + }, | |
| 144 | + } | |
| 145 | + ) | |
| 137 | 146 | } |
| 138 | 147 | var recyclableRender = false |
| 139 | 148 | var staticRenderFns = [] |
| ... | ... | @@ -312,13 +321,6 @@ var _attendanceFence = __webpack_require__(/*! @/service/attendance-fence.js */ |
| 312 | 321 | // |
| 313 | 322 | // |
| 314 | 323 | // |
| 315 | -// | |
| 316 | -// | |
| 317 | -// | |
| 318 | -// | |
| 319 | -// | |
| 320 | -// | |
| 321 | -// | |
| 322 | 324 | var _default = { |
| 323 | 325 | data: function data() { |
| 324 | 326 | return { |
| ... | ... | @@ -341,7 +343,11 @@ var _default = { |
| 341 | 343 | photoUrl: '', |
| 342 | 344 | photoFileList: [], |
| 343 | 345 | clockTime: '--:--:--', |
| 344 | - clockTimer: null | |
| 346 | + clockTimer: null, | |
| 347 | + wifiSSID: '', | |
| 348 | + wifiBSSID: '', | |
| 349 | + wifiLoading: false, | |
| 350 | + wifiError: '' | |
| 345 | 351 | }; |
| 346 | 352 | }, |
| 347 | 353 | computed: { |
| ... | ... | @@ -385,14 +391,95 @@ var _default = { |
| 385 | 391 | } |
| 386 | 392 | return '点击获取定位,用于考勤校验'; |
| 387 | 393 | }, |
| 394 | + requireFenceCheck: function requireFenceCheck() { | |
| 395 | + var sf = this.storeFence; | |
| 396 | + if (!sf) return false; | |
| 397 | + var rf = sf.requireFenceCheck != null ? sf.requireFenceCheck : sf.RequireFenceCheck; | |
| 398 | + return rf === true || rf === 1 || rf === '1'; | |
| 399 | + }, | |
| 400 | + requireWifiCheck: function requireWifiCheck() { | |
| 401 | + var sf = this.storeFence; | |
| 402 | + if (!sf) return false; | |
| 403 | + var rw = sf.requireWifiCheck != null ? sf.requireWifiCheck : sf.RequireWifiCheck; | |
| 404 | + return rw === true || rw === 1 || rw === '1'; | |
| 405 | + }, | |
| 406 | + /** 门店是否已配置 Wi-Fi 成对白名单(与接口 attendanceWifiPairs 一致) */wifiPairRulesCount: function wifiPairRulesCount() { | |
| 407 | + var sf = this.storeFence; | |
| 408 | + if (!sf) return 0; | |
| 409 | + var raw = sf.attendanceWifiPairs != null ? sf.attendanceWifiPairs : sf.AttendanceWifiPairs; | |
| 410 | + return Array.isArray(raw) ? raw.length : 0; | |
| 411 | + }, | |
| 412 | + wifiVerifyPairStrict: function wifiVerifyPairStrict() { | |
| 413 | + var sf = this.storeFence; | |
| 414 | + if (!sf) return false; | |
| 415 | + var v = sf.attendanceWifiVerifyPair != null ? sf.attendanceWifiVerifyPair : sf.AttendanceWifiVerifyPair; | |
| 416 | + return Number(v) === 1; | |
| 417 | + }, | |
| 418 | + wifiMatchesStore: function wifiMatchesStore() { | |
| 419 | + var _this = this; | |
| 420 | + var sf = this.storeFence; | |
| 421 | + var rawPairs = sf && (sf.attendanceWifiPairs != null ? sf.attendanceWifiPairs : sf.AttendanceWifiPairs); | |
| 422 | + var pairs = Array.isArray(rawPairs) ? rawPairs : []; | |
| 423 | + var curSsid = (this.wifiSSID || '').trim(); | |
| 424 | + var curB = this.normalizeAttendanceBssid(this.wifiBSSID); | |
| 425 | + var hasClientBssid = !!curB; | |
| 426 | + if (pairs.length > 0) { | |
| 427 | + var strict = this.wifiVerifyPairStrict; | |
| 428 | + if (strict) { | |
| 429 | + if (hasClientBssid) { | |
| 430 | + // 与后端一致:有 BSSID 时以路由器 MAC 为准(避免 iOS/微信 SSID 为空或不准确) | |
| 431 | + return pairs.some(function (p) { | |
| 432 | + var pb = _this.normalizeAttendanceBssid(p.bssid); | |
| 433 | + return !!pb && pb === curB; | |
| 434 | + }); | |
| 435 | + } | |
| 436 | + if (this.isInFence !== true) return false; | |
| 437 | + if (!curSsid) return false; | |
| 438 | + return pairs.some(function (p) { | |
| 439 | + var ps = String(p.ssid || '').trim(); | |
| 440 | + return !!ps && ps.toLowerCase() === curSsid.toLowerCase(); | |
| 441 | + }); | |
| 442 | + } | |
| 443 | + return pairs.some(function (p) { | |
| 444 | + var ps = String(p.ssid || '').trim(); | |
| 445 | + var pb = _this.normalizeAttendanceBssid(p.bssid); | |
| 446 | + var ssidHit = !!ps && !!curSsid && ps.toLowerCase() === curSsid.toLowerCase(); | |
| 447 | + var bssidHit = !!pb && pb === curB; | |
| 448 | + return ssidHit || bssidHit; | |
| 449 | + }); | |
| 450 | + } | |
| 451 | + return false; | |
| 452 | + }, | |
| 453 | + /** 正常上班时是否满足门店「围栏 / Wi-Fi / 或二者择一」策略(与后端一致) */normalPunchLocationOk: function normalPunchLocationOk() { | |
| 454 | + if (Number(this.punchType) !== 1) return true; | |
| 455 | + var rf = this.requireFenceCheck; | |
| 456 | + var rw = this.requireWifiCheck; | |
| 457 | + if (!rf && !rw) return true; | |
| 458 | + var fenceOk = !rf || this.isInFence === true; | |
| 459 | + var wifiOk = !rw || this.wifiMatchesStore; | |
| 460 | + if (rf && rw) return fenceOk || wifiOk; | |
| 461 | + if (rf) return fenceOk; | |
| 462 | + return wifiOk; | |
| 463 | + }, | |
| 388 | 464 | fenceStatusText: function fenceStatusText() { |
| 389 | - if (this.locating) return '正在校验打卡范围…'; | |
| 390 | - if (Number(this.punchType) === 2) return '外勤打卡不受门店围栏限制'; | |
| 391 | - if (!this.fenceEnabled) return '当前门店未配置围栏,正常打卡不受范围限制'; | |
| 392 | - if (this.longitude == null || this.latitude == null) return '点击更新定位后校验是否在门店范围内'; | |
| 393 | - if (this.isInFence === true) return '当前位于门店打卡范围内,可正常上下班打卡'; | |
| 394 | - if (this.isInFence === false) return '当前不在门店打卡范围内,正常打卡不可用'; | |
| 395 | - return '定位完成后自动校验门店围栏范围'; | |
| 465 | + if (this.locating) return '正在校验位置…'; | |
| 466 | + if (Number(this.punchType) === 2) return '外勤打卡不受门店位置与 Wi-Fi 限制'; | |
| 467 | + var rf = this.requireFenceCheck; | |
| 468 | + var rw = this.requireWifiCheck; | |
| 469 | + if (!rf && !rw) return '当前门店未要求范围或 Wi-Fi,正常打卡可直接进行'; | |
| 470 | + var fenceLine = rf ? this.longitude == null || this.latitude == null ? '需定位后判断是否在打卡范围内' : this.isInFence === true ? '已在打卡范围内' : '不在打卡范围内' : ''; | |
| 471 | + var hasPairs = this.wifiPairRulesCount > 0; | |
| 472 | + var wifiLine = rw ? !hasPairs ? '门店已要求 Wi-Fi 打卡,但未配置网络白名单,请联系管理员在门店资料中维护' : this.wifiMatchesStore ? '已匹配门店 Wi-Fi 规则' : '未匹配门店 Wi-Fi 规则(开启「对应」且无 BSSID 时需同时在围栏内且 SSID 正确)' : ''; | |
| 473 | + if (rf && rw) { | |
| 474 | + return "\u95E8\u5E97\u8981\u6C42\uFF1A\u8303\u56F4\u6216 Wi-Fi \u6EE1\u8DB3\u5176\u4E00\u5373\u53EF\u3002".concat(fenceLine, "\uFF1B").concat(wifiLine); | |
| 475 | + } | |
| 476 | + if (rf) { | |
| 477 | + if (this.longitude == null || this.latitude == null) return '点击更新定位后校验是否在门店范围内'; | |
| 478 | + if (this.isInFence === true) return '当前位于门店打卡范围内,可正常上下班打卡'; | |
| 479 | + if (this.isInFence === false) return '当前不在门店打卡范围内,正常打卡不可用'; | |
| 480 | + return '定位完成后自动校验门店围栏范围'; | |
| 481 | + } | |
| 482 | + return wifiLine || '请连接门店指定 Wi-Fi 后再试'; | |
| 396 | 483 | }, |
| 397 | 484 | punchInDone: function punchInDone() { |
| 398 | 485 | return !!(this.detail && this.detail.punchIn && this.detail.punchIn.time); |
| ... | ... | @@ -425,11 +512,38 @@ var _default = { |
| 425 | 512 | if (this.loading || this.submitting || this.locating) return false; |
| 426 | 513 | if (!this.userInfo.userId) return false; |
| 427 | 514 | if (this.nextPunchDirection == null) return false; |
| 428 | - if (Number(this.punchType) === 1 && this.fenceEnabled && this.isInFence !== true) return false; | |
| 515 | + if (Number(this.punchType) === 1 && !this.normalPunchLocationOk) return false; | |
| 429 | 516 | return true; |
| 430 | 517 | }, |
| 431 | 518 | canViewFenceMap: function canViewFenceMap() { |
| 432 | 519 | return !!(this.storeFence && this.storeFence.hasFence && this.storeFence.fencePolygons || this.storeFence && this.storeFence.longitude != null && this.storeFence.latitude != null || this.longitude != null && this.latitude != null); |
| 520 | + }, | |
| 521 | + wifiMainText: function wifiMainText() { | |
| 522 | + if (this.wifiLoading) return '正在获取 Wi-Fi 信息…'; | |
| 523 | + if (this.wifiError) return this.wifiError; | |
| 524 | + if (this.wifiSSID) return "\u5F53\u524D\u7F51\u7EDC\uFF1A".concat(this.wifiSSID); | |
| 525 | + return '未获取到 Wi-Fi 名称'; | |
| 526 | + }, | |
| 527 | + wifiTipText: function wifiTipText() { | |
| 528 | + if (this.wifiLoading) return '用于核对是否在门店网络环境'; | |
| 529 | + if (this.wifiError) { | |
| 530 | + if (this.wifiError.indexOf('模拟器') !== -1 || this.wifiError.indexOf('真机') !== -1) { | |
| 531 | + return 'Wi-Fi 由系统提供,开发者工具无此能力'; | |
| 532 | + } | |
| 533 | + return '可点击「刷新 Wi-Fi」重试;部分机型需开启定位权限'; | |
| 534 | + } | |
| 535 | + if (Number(this.punchType) === 1 && this.requireWifiCheck) { | |
| 536 | + if (this.wifiPairRulesCount === 0) { | |
| 537 | + return '门店已开启 Wi-Fi 校验,白名单未配置,请联系管理员'; | |
| 538 | + } | |
| 539 | + var strict = this.wifiVerifyPairStrict; | |
| 540 | + if (this.wifiMatchesStore) { | |
| 541 | + return strict ? '已满足门店 Wi-Fi(含对应校验)' : '已命中门店 Wi-Fi 白名单'; | |
| 542 | + } | |
| 543 | + return strict ? '未满足:若已开「对应」校验,请连门店 AP 或进入围栏且 SSID 正确' : '未命中白名单(可核对 SSID/BSSID;与围栏组合时满足其一即可)'; | |
| 544 | + } | |
| 545 | + if (this.wifiBSSID) return "BSSID\uFF1A".concat(this.wifiBSSID); | |
| 546 | + return '名称由系统返回;iOS 常可稳定获取 BSSID'; | |
| 433 | 547 | } |
| 434 | 548 | }, |
| 435 | 549 | onLoad: function onLoad() { |
| ... | ... | @@ -453,28 +567,39 @@ var _default = { |
| 453 | 567 | }); |
| 454 | 568 | }, |
| 455 | 569 | methods: { |
| 570 | + /** 与后端 NormalizeAttendanceBssid 一致:连字符统一为冒号、小写 */normalizeAttendanceBssid: function normalizeAttendanceBssid(s) { | |
| 571 | + if (!s) return ''; | |
| 572 | + var t = String(s).trim().replace(/-/g, ':').replace(/\s+/g, '').toLowerCase(); | |
| 573 | + if (t.indexOf(':') < 0 && t.length === 12 && /^[0-9a-f]{12}$/.test(t)) { | |
| 574 | + t = t.match(/.{2}/g).join(':'); | |
| 575 | + } | |
| 576 | + return t; | |
| 577 | + }, | |
| 456 | 578 | /** 切回「正常」时只清空外勤说明;照片正常/外勤共用,保留 */setPunchType: function setPunchType(type) { |
| 457 | 579 | this.punchType = type; |
| 458 | - if (type === 1) { | |
| 580 | + if (Number(type) === 1) { | |
| 459 | 581 | this.remark = ''; |
| 582 | + this.refreshWifiInfo(); | |
| 460 | 583 | } |
| 461 | 584 | }, |
| 462 | 585 | initializePage: function initializePage() { |
| 463 | - var _this = this; | |
| 586 | + var _this2 = this; | |
| 464 | 587 | return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() { |
| 588 | + var wifiTask; | |
| 465 | 589 | return _regenerator.default.wrap(function _callee$(_context) { |
| 466 | 590 | while (1) { |
| 467 | 591 | switch (_context.prev = _context.next) { |
| 468 | 592 | case 0: |
| 469 | 593 | _context.next = 2; |
| 470 | - return _this.loadPunchConfig(); | |
| 594 | + return _this2.loadPunchConfig(); | |
| 471 | 595 | case 2: |
| 472 | - _context.next = 4; | |
| 473 | - return _this.refreshLocation(); | |
| 474 | - case 4: | |
| 475 | - _context.next = 6; | |
| 476 | - return _this.loadDetail(); | |
| 477 | - case 6: | |
| 596 | + wifiTask = Number(_this2.punchType) === 1 ? _this2.refreshWifiInfo() : Promise.resolve(); | |
| 597 | + _context.next = 5; | |
| 598 | + return Promise.all([_this2.refreshLocation(), wifiTask]); | |
| 599 | + case 5: | |
| 600 | + _context.next = 7; | |
| 601 | + return _this2.loadDetail(); | |
| 602 | + case 7: | |
| 478 | 603 | case "end": |
| 479 | 604 | return _context.stop(); |
| 480 | 605 | } |
| ... | ... | @@ -484,9 +609,9 @@ var _default = { |
| 484 | 609 | }, |
| 485 | 610 | onMainPunch: function onMainPunch() { |
| 486 | 611 | if (!this.nextPunchDirection) return; |
| 487 | - if (Number(this.punchType) === 1 && this.fenceEnabled && this.isInFence !== true) { | |
| 612 | + if (Number(this.punchType) === 1 && !this.normalPunchLocationOk) { | |
| 488 | 613 | uni.showToast({ |
| 489 | - title: '当前不在门店打卡范围内,无法正常打卡', | |
| 614 | + title: '未满足门店要求(需在范围内或指定 Wi-Fi/BSSID),可改用外勤', | |
| 490 | 615 | icon: 'none' |
| 491 | 616 | }); |
| 492 | 617 | return; |
| ... | ... | @@ -518,42 +643,42 @@ var _default = { |
| 518 | 643 | return "".concat(y, "-").concat(m, "-").concat(day); |
| 519 | 644 | }, |
| 520 | 645 | loadDetail: function loadDetail() { |
| 521 | - var _this2 = this; | |
| 646 | + var _this3 = this; | |
| 522 | 647 | return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2() { |
| 523 | 648 | var res; |
| 524 | 649 | return _regenerator.default.wrap(function _callee2$(_context2) { |
| 525 | 650 | while (1) { |
| 526 | 651 | switch (_context2.prev = _context2.next) { |
| 527 | 652 | case 0: |
| 528 | - if (_this2.userInfo.userId) { | |
| 653 | + if (_this3.userInfo.userId) { | |
| 529 | 654 | _context2.next = 2; |
| 530 | 655 | break; |
| 531 | 656 | } |
| 532 | 657 | return _context2.abrupt("return"); |
| 533 | 658 | case 2: |
| 534 | - _this2.loading = true; | |
| 659 | + _this3.loading = true; | |
| 535 | 660 | _context2.prev = 3; |
| 536 | 661 | _context2.next = 6; |
| 537 | - return _this2.API.getAttendanceDetail({ | |
| 538 | - userId: _this2.userInfo.userId, | |
| 539 | - attendanceDate: _this2.today | |
| 662 | + return _this3.API.getAttendanceDetail({ | |
| 663 | + userId: _this3.userInfo.userId, | |
| 664 | + attendanceDate: _this3.today | |
| 540 | 665 | }); |
| 541 | 666 | case 6: |
| 542 | 667 | res = _context2.sent; |
| 543 | 668 | if (res && Number(res.code) === 200 && res.data) { |
| 544 | - _this2.detail = res.data; | |
| 669 | + _this3.detail = res.data; | |
| 545 | 670 | } else { |
| 546 | - _this2.detail = null; | |
| 671 | + _this3.detail = null; | |
| 547 | 672 | } |
| 548 | 673 | _context2.next = 13; |
| 549 | 674 | break; |
| 550 | 675 | case 10: |
| 551 | 676 | _context2.prev = 10; |
| 552 | 677 | _context2.t0 = _context2["catch"](3); |
| 553 | - _this2.detail = null; | |
| 678 | + _this3.detail = null; | |
| 554 | 679 | case 13: |
| 555 | 680 | _context2.prev = 13; |
| 556 | - _this2.loading = false; | |
| 681 | + _this3.loading = false; | |
| 557 | 682 | return _context2.finish(13); |
| 558 | 683 | case 16: |
| 559 | 684 | case "end": |
| ... | ... | @@ -564,52 +689,52 @@ var _default = { |
| 564 | 689 | }))(); |
| 565 | 690 | }, |
| 566 | 691 | loadPunchConfig: function loadPunchConfig() { |
| 567 | - var _this3 = this; | |
| 692 | + var _this4 = this; | |
| 568 | 693 | return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() { |
| 569 | 694 | var res; |
| 570 | 695 | return _regenerator.default.wrap(function _callee3$(_context3) { |
| 571 | 696 | while (1) { |
| 572 | 697 | switch (_context3.prev = _context3.next) { |
| 573 | 698 | case 0: |
| 574 | - if (_this3.userInfo.userId) { | |
| 699 | + if (_this4.userInfo.userId) { | |
| 575 | 700 | _context3.next = 7; |
| 576 | 701 | break; |
| 577 | 702 | } |
| 578 | - _this3.punchConfig = null; | |
| 579 | - _this3.storeFence = null; | |
| 580 | - _this3.attendanceGroup = null; | |
| 581 | - _this3.fenceEnabled = false; | |
| 582 | - _this3.isInFence = null; | |
| 703 | + _this4.punchConfig = null; | |
| 704 | + _this4.storeFence = null; | |
| 705 | + _this4.attendanceGroup = null; | |
| 706 | + _this4.fenceEnabled = false; | |
| 707 | + _this4.isInFence = null; | |
| 583 | 708 | return _context3.abrupt("return"); |
| 584 | 709 | case 7: |
| 585 | 710 | _context3.prev = 7; |
| 586 | 711 | _context3.next = 10; |
| 587 | - return _this3.API.getCurrentAttendancePunchConfig({}); | |
| 712 | + return _this4.API.getCurrentAttendancePunchConfig({}); | |
| 588 | 713 | case 10: |
| 589 | 714 | res = _context3.sent; |
| 590 | 715 | if (res && Number(res.code) === 200 && res.data) { |
| 591 | - _this3.punchConfig = res.data; | |
| 592 | - _this3.storeFence = res.data.storeFence || null; | |
| 593 | - _this3.attendanceGroup = res.data.attendanceGroup || null; | |
| 594 | - _this3.fenceEnabled = !!(_this3.storeFence && _this3.storeFence.hasFence && _this3.storeFence.fencePolygons); | |
| 595 | - _this3.evaluateFenceRange(); | |
| 716 | + _this4.punchConfig = res.data; | |
| 717 | + _this4.storeFence = res.data.storeFence || null; | |
| 718 | + _this4.attendanceGroup = res.data.attendanceGroup || null; | |
| 719 | + _this4.fenceEnabled = !!(_this4.storeFence && _this4.storeFence.hasFence && _this4.storeFence.fencePolygons); | |
| 720 | + _this4.evaluateFenceRange(); | |
| 596 | 721 | } else { |
| 597 | - _this3.punchConfig = null; | |
| 598 | - _this3.storeFence = null; | |
| 599 | - _this3.attendanceGroup = null; | |
| 600 | - _this3.fenceEnabled = false; | |
| 601 | - _this3.isInFence = null; | |
| 722 | + _this4.punchConfig = null; | |
| 723 | + _this4.storeFence = null; | |
| 724 | + _this4.attendanceGroup = null; | |
| 725 | + _this4.fenceEnabled = false; | |
| 726 | + _this4.isInFence = null; | |
| 602 | 727 | } |
| 603 | 728 | _context3.next = 21; |
| 604 | 729 | break; |
| 605 | 730 | case 14: |
| 606 | 731 | _context3.prev = 14; |
| 607 | 732 | _context3.t0 = _context3["catch"](7); |
| 608 | - _this3.punchConfig = null; | |
| 609 | - _this3.storeFence = null; | |
| 610 | - _this3.attendanceGroup = null; | |
| 611 | - _this3.fenceEnabled = false; | |
| 612 | - _this3.isInFence = null; | |
| 733 | + _this4.punchConfig = null; | |
| 734 | + _this4.storeFence = null; | |
| 735 | + _this4.attendanceGroup = null; | |
| 736 | + _this4.fenceEnabled = false; | |
| 737 | + _this4.isInFence = null; | |
| 613 | 738 | case 21: |
| 614 | 739 | case "end": |
| 615 | 740 | return _context3.stop(); |
| ... | ... | @@ -618,24 +743,84 @@ var _default = { |
| 618 | 743 | }, _callee3, null, [[7, 14]]); |
| 619 | 744 | }))(); |
| 620 | 745 | }, |
| 746 | + refreshWifiInfo: function refreshWifiInfo() { | |
| 747 | + var _this5 = this; | |
| 748 | + return new Promise(function (resolve) { | |
| 749 | + try { | |
| 750 | + var sys = uni.getSystemInfoSync(); | |
| 751 | + if (sys && sys.platform === 'devtools') { | |
| 752 | + _this5.wifiLoading = false; | |
| 753 | + _this5.wifiSSID = ''; | |
| 754 | + _this5.wifiBSSID = ''; | |
| 755 | + _this5.wifiError = '开发者工具模拟器不支持 Wi-Fi,请用真机预览或扫码调试'; | |
| 756 | + resolve(); | |
| 757 | + return; | |
| 758 | + } | |
| 759 | + } catch (e) {} | |
| 760 | + _this5.wifiLoading = true; | |
| 761 | + _this5.wifiError = ''; | |
| 762 | + uni.startWifi({ | |
| 763 | + success: function success() { | |
| 764 | + uni.getConnectedWifi({ | |
| 765 | + success: function success(res) { | |
| 766 | + var w = res && res.wifi; | |
| 767 | + _this5.wifiSSID = w && w.SSID ? String(w.SSID) : ''; | |
| 768 | + _this5.wifiBSSID = w && w.BSSID ? String(w.BSSID) : ''; | |
| 769 | + _this5.wifiLoading = false; | |
| 770 | + if (!_this5.wifiSSID && !_this5.wifiBSSID) { | |
| 771 | + _this5.wifiError = '未连接 Wi-Fi'; | |
| 772 | + } | |
| 773 | + resolve(); | |
| 774 | + }, | |
| 775 | + fail: function fail(err) { | |
| 776 | + _this5.wifiLoading = false; | |
| 777 | + var msg = err && err.errMsg ? String(err.errMsg) : ''; | |
| 778 | + if (msg.indexOf('auth deny') !== -1 || msg.indexOf('authorize') !== -1) { | |
| 779 | + _this5.wifiError = '需授权获取 Wi-Fi 信息'; | |
| 780 | + } else if (msg.indexOf('not connected') !== -1 || err && Number(err.errCode) === 12002) { | |
| 781 | + _this5.wifiError = '当前未连接 Wi-Fi'; | |
| 782 | + } else { | |
| 783 | + _this5.wifiError = '无法获取 Wi-Fi 信息'; | |
| 784 | + } | |
| 785 | + _this5.wifiSSID = ''; | |
| 786 | + _this5.wifiBSSID = ''; | |
| 787 | + resolve(); | |
| 788 | + } | |
| 789 | + }); | |
| 790 | + }, | |
| 791 | + fail: function fail(err) { | |
| 792 | + _this5.wifiLoading = false; | |
| 793 | + _this5.wifiSSID = ''; | |
| 794 | + _this5.wifiBSSID = ''; | |
| 795 | + var msg = err && err.errMsg ? String(err.errMsg) : ''; | |
| 796 | + if (msg.indexOf('devtools') !== -1 || msg.indexOf('not support') !== -1) { | |
| 797 | + _this5.wifiError = '当前环境不支持 Wi-Fi,请用真机预览'; | |
| 798 | + } else { | |
| 799 | + _this5.wifiError = '无法启动 Wi-Fi 模块,请用真机重试'; | |
| 800 | + } | |
| 801 | + resolve(); | |
| 802 | + } | |
| 803 | + }); | |
| 804 | + }); | |
| 805 | + }, | |
| 621 | 806 | refreshLocation: function refreshLocation() { |
| 622 | - var _this4 = this; | |
| 807 | + var _this6 = this; | |
| 623 | 808 | this.locating = true; |
| 624 | 809 | return new Promise(function (resolve) { |
| 625 | 810 | uni.getLocation({ |
| 626 | 811 | type: 'gcj02', |
| 627 | 812 | isHighAccuracy: true, |
| 628 | 813 | success: function success(res) { |
| 629 | - _this4.longitude = res.longitude; | |
| 630 | - _this4.latitude = res.latitude; | |
| 631 | - _this4.address = ''; | |
| 632 | - _this4.evaluateFenceRange(); | |
| 633 | - _this4.locating = false; | |
| 814 | + _this6.longitude = res.longitude; | |
| 815 | + _this6.latitude = res.latitude; | |
| 816 | + _this6.address = ''; | |
| 817 | + _this6.evaluateFenceRange(); | |
| 818 | + _this6.locating = false; | |
| 634 | 819 | resolve(); |
| 635 | 820 | }, |
| 636 | 821 | fail: function fail() { |
| 637 | - _this4.locating = false; | |
| 638 | - _this4.isInFence = null; | |
| 822 | + _this6.locating = false; | |
| 823 | + _this6.isInFence = null; | |
| 639 | 824 | uni.showToast({ |
| 640 | 825 | title: '定位失败,请检查系统定位权限', |
| 641 | 826 | icon: 'none' |
| ... | ... | @@ -663,7 +848,7 @@ var _default = { |
| 663 | 848 | }); |
| 664 | 849 | }, |
| 665 | 850 | evaluateFenceRange: function evaluateFenceRange() { |
| 666 | - var _this5 = this; | |
| 851 | + var _this7 = this; | |
| 667 | 852 | if (!this.fenceEnabled) { |
| 668 | 853 | this.isInFence = true; |
| 669 | 854 | return; |
| ... | ... | @@ -678,14 +863,14 @@ var _default = { |
| 678 | 863 | return; |
| 679 | 864 | } |
| 680 | 865 | this.isInFence = polygons.some(function (polygon) { |
| 681 | - return _this5.isPointInPolygon(_this5.longitude, _this5.latitude, polygon); | |
| 866 | + return _this7.isPointInPolygon(_this7.longitude, _this7.latitude, polygon); | |
| 682 | 867 | }); |
| 683 | 868 | }, |
| 684 | 869 | isPointInPolygon: function isPointInPolygon(lng, lat, polygon) { |
| 685 | 870 | return (0, _attendanceFence.isPointInPolygon)(lng, lat, polygon); |
| 686 | 871 | }, |
| 687 | 872 | afterReadPhoto: function afterReadPhoto(event) { |
| 688 | - var _this6 = this; | |
| 873 | + var _this8 = this; | |
| 689 | 874 | return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4() { |
| 690 | 875 | var lists, file, fileObj, result; |
| 691 | 876 | return _regenerator.default.wrap(function _callee4$(_context4) { |
| ... | ... | @@ -705,13 +890,13 @@ var _default = { |
| 705 | 890 | title: '上传中' |
| 706 | 891 | }); |
| 707 | 892 | _context4.next = 8; |
| 708 | - return _this6.API.uploadFile(fileObj); | |
| 893 | + return _this8.API.uploadFile(fileObj); | |
| 709 | 894 | case 8: |
| 710 | 895 | result = _context4.sent; |
| 711 | 896 | uni.hideLoading(); |
| 712 | 897 | if (result && result.code === 200 && result.data && result.data.url) { |
| 713 | - _this6.photoUrl = result.data.url; | |
| 714 | - _this6.photoFileList = [{ | |
| 898 | + _this8.photoUrl = result.data.url; | |
| 899 | + _this8.photoFileList = [{ | |
| 715 | 900 | url: result.data.url, |
| 716 | 901 | status: 'success' |
| 717 | 902 | }]; |
| ... | ... | @@ -744,14 +929,14 @@ var _default = { |
| 744 | 929 | this.photoFileList = []; |
| 745 | 930 | }, |
| 746 | 931 | doPunch: function doPunch(direction) { |
| 747 | - var _this7 = this; | |
| 932 | + var _this9 = this; | |
| 748 | 933 | return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5() { |
| 749 | - var isFieldWork, payload, res; | |
| 934 | + var isNormal, needGps, isFieldWork, payload, res; | |
| 750 | 935 | return _regenerator.default.wrap(function _callee5$(_context5) { |
| 751 | 936 | while (1) { |
| 752 | 937 | switch (_context5.prev = _context5.next) { |
| 753 | 938 | case 0: |
| 754 | - if (_this7.userInfo.userId) { | |
| 939 | + if (_this9.userInfo.userId) { | |
| 755 | 940 | _context5.next = 3; |
| 756 | 941 | break; |
| 757 | 942 | } |
| ... | ... | @@ -761,81 +946,99 @@ var _default = { |
| 761 | 946 | }); |
| 762 | 947 | return _context5.abrupt("return"); |
| 763 | 948 | case 3: |
| 764 | - if (!(_this7.longitude == null || _this7.latitude == null)) { | |
| 765 | - _context5.next = 9; | |
| 949 | + isNormal = Number(_this9.punchType) === 1; | |
| 950 | + needGps = !isNormal || !!(_this9.storeFence && _this9.storeFence.requireFenceCheck); | |
| 951 | + if (!needGps) { | |
| 952 | + _context5.next = 14; | |
| 766 | 953 | break; |
| 767 | 954 | } |
| 768 | - _context5.next = 6; | |
| 769 | - return _this7.refreshLocation(); | |
| 770 | - case 6: | |
| 771 | - if (!(_this7.longitude == null || _this7.latitude == null)) { | |
| 955 | + if (!(_this9.longitude == null || _this9.latitude == null)) { | |
| 772 | 956 | _context5.next = 9; |
| 773 | 957 | break; |
| 774 | 958 | } |
| 959 | + _context5.next = 9; | |
| 960 | + return _this9.refreshLocation(); | |
| 961 | + case 9: | |
| 962 | + if (!(_this9.longitude == null || _this9.latitude == null)) { | |
| 963 | + _context5.next = 12; | |
| 964 | + break; | |
| 965 | + } | |
| 775 | 966 | uni.showToast({ |
| 776 | 967 | title: '请先完成定位', |
| 777 | 968 | icon: 'none' |
| 778 | 969 | }); |
| 779 | 970 | return _context5.abrupt("return"); |
| 780 | - case 9: | |
| 781 | - isFieldWork = Number(_this7.punchType) === 2; | |
| 971 | + case 12: | |
| 972 | + _context5.next = 17; | |
| 973 | + break; | |
| 974 | + case 14: | |
| 975 | + if (!(isNormal && _this9.longitude == null)) { | |
| 976 | + _context5.next = 17; | |
| 977 | + break; | |
| 978 | + } | |
| 979 | + _context5.next = 17; | |
| 980 | + return _this9.refreshLocation(); | |
| 981 | + case 17: | |
| 982 | + isFieldWork = Number(_this9.punchType) === 2; | |
| 782 | 983 | payload = { |
| 783 | - userId: _this7.userInfo.userId, | |
| 984 | + userId: _this9.userInfo.userId, | |
| 784 | 985 | punchDirection: direction, |
| 785 | - punchType: Number(_this7.punchType), | |
| 786 | - longitude: _this7.longitude, | |
| 787 | - latitude: _this7.latitude, | |
| 788 | - address: _this7.address || _this7.locationText, | |
| 789 | - photoUrl: _this7.photoUrl || undefined, | |
| 790 | - remark: isFieldWork ? (_this7.remark || '').trim() || undefined : undefined | |
| 986 | + punchType: Number(_this9.punchType), | |
| 987 | + longitude: _this9.longitude != null ? _this9.longitude : undefined, | |
| 988 | + latitude: _this9.latitude != null ? _this9.latitude : undefined, | |
| 989 | + address: _this9.address || _this9.locationText, | |
| 990 | + wifiSsid: (_this9.wifiSSID || '').trim() || undefined, | |
| 991 | + wifiBssid: (_this9.wifiBSSID || '').trim() || undefined, | |
| 992 | + photoUrl: _this9.photoUrl || undefined, | |
| 993 | + remark: isFieldWork ? (_this9.remark || '').trim() || undefined : undefined | |
| 791 | 994 | }; |
| 792 | - _this7.submitting = true; | |
| 793 | - _context5.prev = 12; | |
| 794 | - _context5.next = 15; | |
| 795 | - return _this7.API.submitAttendancePunch(payload); | |
| 796 | - case 15: | |
| 995 | + _this9.submitting = true; | |
| 996 | + _context5.prev = 20; | |
| 997 | + _context5.next = 23; | |
| 998 | + return _this9.API.submitAttendancePunch(payload); | |
| 999 | + case 23: | |
| 797 | 1000 | res = _context5.sent; |
| 798 | 1001 | if (!(res && Number(res.code) === 200)) { |
| 799 | - _context5.next = 25; | |
| 1002 | + _context5.next = 33; | |
| 800 | 1003 | break; |
| 801 | 1004 | } |
| 802 | 1005 | uni.showToast({ |
| 803 | 1006 | title: '打卡成功', |
| 804 | 1007 | icon: 'success' |
| 805 | 1008 | }); |
| 806 | - _this7.remark = ''; | |
| 807 | - _this7.photoUrl = ''; | |
| 808 | - _this7.photoFileList = []; | |
| 809 | - _context5.next = 23; | |
| 810 | - return _this7.loadDetail(); | |
| 811 | - case 23: | |
| 812 | - _context5.next = 26; | |
| 1009 | + _this9.remark = ''; | |
| 1010 | + _this9.photoUrl = ''; | |
| 1011 | + _this9.photoFileList = []; | |
| 1012 | + _context5.next = 31; | |
| 1013 | + return _this9.loadDetail(); | |
| 1014 | + case 31: | |
| 1015 | + _context5.next = 34; | |
| 813 | 1016 | break; |
| 814 | - case 25: | |
| 1017 | + case 33: | |
| 815 | 1018 | uni.showToast({ |
| 816 | 1019 | title: res && (res.msg || res.message) || '打卡失败', |
| 817 | 1020 | icon: 'none' |
| 818 | 1021 | }); |
| 819 | - case 26: | |
| 820 | - _context5.next = 31; | |
| 1022 | + case 34: | |
| 1023 | + _context5.next = 39; | |
| 821 | 1024 | break; |
| 822 | - case 28: | |
| 823 | - _context5.prev = 28; | |
| 824 | - _context5.t0 = _context5["catch"](12); | |
| 1025 | + case 36: | |
| 1026 | + _context5.prev = 36; | |
| 1027 | + _context5.t0 = _context5["catch"](20); | |
| 825 | 1028 | uni.showToast({ |
| 826 | 1029 | title: '打卡失败', |
| 827 | 1030 | icon: 'none' |
| 828 | 1031 | }); |
| 829 | - case 31: | |
| 830 | - _context5.prev = 31; | |
| 831 | - _this7.submitting = false; | |
| 832 | - return _context5.finish(31); | |
| 833 | - case 34: | |
| 1032 | + case 39: | |
| 1033 | + _context5.prev = 39; | |
| 1034 | + _this9.submitting = false; | |
| 1035 | + return _context5.finish(39); | |
| 1036 | + case 42: | |
| 834 | 1037 | case "end": |
| 835 | 1038 | return _context5.stop(); |
| 836 | 1039 | } |
| 837 | 1040 | } |
| 838 | - }, _callee5, null, [[12, 28, 31, 34]]); | |
| 1041 | + }, _callee5, null, [[20, 36, 39, 42]]); | |
| 839 | 1042 | }))(); |
| 840 | 1043 | } |
| 841 | 1044 | } | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/pages/attendance-punch/attendance-punch.wxml
| 1 | -<view class="att-page data-v-f6f89274"><view class="att-header data-v-f6f89274"><view class="att-clock data-v-f6f89274">{{clockTime}}</view><view class="att-meta data-v-f6f89274"><text class="att-date data-v-f6f89274">{{todayText}}</text><text class="att-week data-v-f6f89274">{{weekdayText}}</text></view><view class="att-user data-v-f6f89274">{{displayName}}</view><block wx:if="{{ruleTimeText}}"><view class="att-rule data-v-f6f89274"><text class="att-rule-label data-v-f6f89274">今日班次</text><text class="att-rule-val data-v-f6f89274">{{ruleTimeText}}</text></view></block></view><view class="att-body data-v-f6f89274"><view class="att-card att-seg-card data-v-f6f89274"><view class="att-seg data-v-f6f89274"><view data-event-opts="{{[['tap',[['setPunchType',[1]]]]]}}" class="{{['att-seg-item','data-v-f6f89274',(punchType===1)?'is-active':'']}}" bindtap="__e">正常</view><view data-event-opts="{{[['tap',[['setPunchType',[2]]]]]}}" class="{{['att-seg-item','data-v-f6f89274',(punchType===2)?'is-active':'']}}" bindtap="__e">外勤</view></view></view><view class="att-card att-punch-card data-v-f6f89274"><view data-event-opts="{{[['tap',[['onMainPunch',['$event']]]]]}}" class="{{['att-big-btn','data-v-f6f89274',(allPunchDone)?'is-all-done':'',(!canPressMainButton)?'is-disabled':'']}}" bindtap="__e"><block wx:if="{{allPunchDone}}"><u-icon vue-id="0bf3a300-1" name="checkmark-circle-fill" color="#ffffff" size="52" class="data-v-f6f89274" bind:__l="__l"></u-icon><text class="att-big-done-text data-v-f6f89274">今日打卡已完成</text></block><block wx:else><text class="att-big-title data-v-f6f89274">{{mainButtonTitle}}</text><text class="att-big-sub data-v-f6f89274">{{mainButtonSub}}</text></block></view><block wx:if="{{nextPunchDirection&&!allPunchDone}}"><view class="att-next-hint data-v-f6f89274">{{'当前应打:'+(nextPunchDirection===1?'上班卡':'下班卡')+''}}</view></block></view><view class="att-card att-loc-card data-v-f6f89274"><u-icon vue-id="0bf3a300-2" name="map-fill" color="#43a047" size="22" class="data-v-f6f89274" bind:__l="__l"></u-icon><view class="att-loc-main data-v-f6f89274"><text class="att-loc-text data-v-f6f89274">{{locationText}}</text><text class="att-loc-tip data-v-f6f89274">{{fenceStatusText}}</text><view class="att-loc-actions data-v-f6f89274"><block wx:if="{{canViewFenceMap}}"><text data-event-opts="{{[['tap',[['openFenceMap',['$event']]]]]}}" class="att-loc-action att-loc-action--primary data-v-f6f89274" catchtap="__e">查看打卡范围</text></block><text data-event-opts="{{[['tap',[['refreshLocation',['$event']]]]]}}" class="att-loc-action data-v-f6f89274" catchtap="__e">重新定位</text></view></view><u-icon vue-id="0bf3a300-3" name="arrow-right" color="#c8c9cc" size="18" class="data-v-f6f89274" bind:__l="__l"></u-icon></view><view class="att-card data-v-f6f89274"><view class="att-card-head data-v-f6f89274"><text class="att-card-title data-v-f6f89274">今日记录</text><block wx:if="{{detail&&detail.statusText}}"><view class="att-status-tag data-v-f6f89274">{{detail.statusText}}</view></block></view><block wx:if="{{detail}}"><view class="att-timeline data-v-f6f89274"><view class="att-tl-item data-v-f6f89274"><view class="{{['att-tl-dot','data-v-f6f89274',punchInDone?'is-on':'']}}"></view><view class="att-tl-line data-v-f6f89274"></view><view class="att-tl-body data-v-f6f89274"><view class="att-tl-row data-v-f6f89274"><text class="att-tl-name data-v-f6f89274">上班</text><text class="att-tl-time data-v-f6f89274">{{detail.punchIn&&detail.punchIn.time?detail.punchIn.time:'未打卡'}}</text></view></view></view><view class="att-tl-item att-tl-item--last data-v-f6f89274"><view class="{{['att-tl-dot','data-v-f6f89274',punchOutDone?'is-on':'']}}"></view><view class="att-tl-body data-v-f6f89274"><view class="att-tl-row data-v-f6f89274"><text class="att-tl-name data-v-f6f89274">下班</text><text class="att-tl-time data-v-f6f89274">{{detail.punchOut&&detail.punchOut.time?detail.punchOut.time:'未打卡'}}</text></view></view></view></view></block><block wx:else><block wx:if="{{!loading}}"><view class="att-empty data-v-f6f89274"><text class="data-v-f6f89274">今日尚无记录,打卡后将显示在此</text></view></block></block></view><view class="att-card att-more-card data-v-f6f89274"><view class="att-more-title data-v-f6f89274">打卡照片(选填)</view><view class="att-upload-wrap data-v-f6f89274"><u-upload vue-id="0bf3a300-4" fileList="{{photoFileList}}" name="photo" maxCount="{{1}}" accept="image" width="120" height="120" data-event-opts="{{[['^afterRead',[['afterReadPhoto']]],['^delete',[['deletePhoto']]]]}}" bind:afterRead="__e" bind:delete="__e" class="data-v-f6f89274" bind:__l="__l"></u-upload></view><block wx:if="{{punchType===2}}"><view class="att-more-title att-more-title--second data-v-f6f89274">外勤说明(选填)</view><u-input bind:input="__e" vue-id="0bf3a300-5" placeholder="请填写外勤事由(选填)" border="none" customStyle="{{({background:'#f1f8f4',padding:'16rpx 20rpx',borderRadius:'12rpx'})}}" value="{{remark}}" data-event-opts="{{[['^input',[['__set_model',['','remark','$event',[]]]]]]}}" class="data-v-f6f89274" bind:__l="__l"></u-input></block></view><view class="att-safe data-v-f6f89274"></view></view><block wx:if="{{loading}}"><view class="att-loading-mask data-v-f6f89274"><u-loading-icon vue-id="0bf3a300-6" mode="circle" size="28" color="#43a047" class="data-v-f6f89274" bind:__l="__l"></u-loading-icon><text class="att-loading-txt data-v-f6f89274">加载中</text></view></block></view> | |
| 2 | 1 | \ No newline at end of file |
| 2 | +<view class="att-page data-v-f6f89274"><view class="att-header data-v-f6f89274"><view class="att-clock data-v-f6f89274">{{clockTime}}</view><view class="att-meta data-v-f6f89274"><text class="att-date data-v-f6f89274">{{todayText}}</text><text class="att-week data-v-f6f89274">{{weekdayText}}</text></view><view class="att-user data-v-f6f89274">{{displayName}}</view><block wx:if="{{ruleTimeText}}"><view class="att-rule data-v-f6f89274"><text class="att-rule-label data-v-f6f89274">今日班次</text><text class="att-rule-val data-v-f6f89274">{{ruleTimeText}}</text></view></block></view><view class="att-body data-v-f6f89274"><view class="att-card att-seg-card data-v-f6f89274"><view class="att-seg data-v-f6f89274"><view data-event-opts="{{[['tap',[['setPunchType',[1]]]]]}}" class="{{['att-seg-item','data-v-f6f89274',(punchType===1)?'is-active':'']}}" bindtap="__e">正常 </view><view data-event-opts="{{[['tap',[['setPunchType',[2]]]]]}}" class="{{['att-seg-item','data-v-f6f89274',(punchType===2)?'is-active':'']}}" bindtap="__e">外勤 </view></view></view><view class="att-card att-punch-card data-v-f6f89274"><view data-event-opts="{{[['tap',[['onMainPunch',['$event']]]]]}}" class="{{['att-big-btn','data-v-f6f89274',(allPunchDone)?'is-all-done':'',(!canPressMainButton)?'is-disabled':'']}}" bindtap="__e"><block wx:if="{{allPunchDone}}"><u-icon vue-id="0bf3a300-1" name="checkmark-circle-fill" color="#ffffff" size="52" class="data-v-f6f89274" bind:__l="__l"></u-icon><text class="att-big-done-text data-v-f6f89274">今日打卡已完成</text></block><block wx:else><text class="att-big-title data-v-f6f89274">{{mainButtonTitle}}</text><text class="att-big-sub data-v-f6f89274">{{mainButtonSub}}</text></block></view><block wx:if="{{nextPunchDirection&&!allPunchDone}}"><view class="att-next-hint data-v-f6f89274">{{'当前应打:'+(nextPunchDirection===1?'上班卡':'下班卡')+''}}</view></block></view><view class="att-card att-loc-card data-v-f6f89274"><u-icon vue-id="0bf3a300-2" name="map-fill" color="#43a047" size="22" class="data-v-f6f89274" bind:__l="__l"></u-icon><view class="att-loc-main data-v-f6f89274"><text class="att-loc-text data-v-f6f89274">{{locationText}}</text><text class="att-loc-tip data-v-f6f89274">{{fenceStatusText}}</text><view class="att-loc-actions data-v-f6f89274"><block wx:if="{{canViewFenceMap}}"><text data-event-opts="{{[['tap',[['openFenceMap',['$event']]]]]}}" class="att-loc-action att-loc-action--primary data-v-f6f89274" catchtap="__e">查看打卡范围</text></block><text data-event-opts="{{[['tap',[['refreshLocation',['$event']]]]]}}" class="att-loc-action data-v-f6f89274" catchtap="__e">重新定位</text></view></view><u-icon vue-id="0bf3a300-3" name="arrow-right" color="#c8c9cc" size="18" class="data-v-f6f89274" bind:__l="__l"></u-icon></view><block wx:if="{{$root.m0===1}}"><view class="att-card att-loc-card att-wifi-card data-v-f6f89274"><u-icon vue-id="0bf3a300-4" name="wifi" color="#43a047" size="22" class="data-v-f6f89274" bind:__l="__l"></u-icon><view class="att-loc-main data-v-f6f89274"><text class="att-loc-text data-v-f6f89274">{{wifiMainText}}</text><text class="att-loc-tip data-v-f6f89274">{{wifiTipText}}</text><view class="att-loc-actions data-v-f6f89274"><text data-event-opts="{{[['tap',[['refreshWifiInfo',['$event']]]]]}}" class="att-loc-action data-v-f6f89274" catchtap="__e">刷新 Wi-Fi</text></view></view><u-icon vue-id="0bf3a300-5" name="arrow-right" color="#c8c9cc" size="18" class="data-v-f6f89274" bind:__l="__l"></u-icon></view></block><view class="att-card data-v-f6f89274"><view class="att-card-head data-v-f6f89274"><text class="att-card-title data-v-f6f89274">今日记录</text><block wx:if="{{detail&&detail.statusText}}"><view class="att-status-tag data-v-f6f89274">{{detail.statusText}}</view></block></view><block wx:if="{{detail}}"><view class="att-timeline data-v-f6f89274"><view class="att-tl-item data-v-f6f89274"><view class="{{['att-tl-dot','data-v-f6f89274',punchInDone?'is-on':'']}}"></view><view class="att-tl-line data-v-f6f89274"></view><view class="att-tl-body data-v-f6f89274"><view class="att-tl-row data-v-f6f89274"><text class="att-tl-name data-v-f6f89274">上班</text><text class="att-tl-time data-v-f6f89274">{{detail.punchIn&&detail.punchIn.time?detail.punchIn.time:'未打卡'}}</text></view></view></view><view class="att-tl-item att-tl-item--last data-v-f6f89274"><view class="{{['att-tl-dot','data-v-f6f89274',punchOutDone?'is-on':'']}}"></view><view class="att-tl-body data-v-f6f89274"><view class="att-tl-row data-v-f6f89274"><text class="att-tl-name data-v-f6f89274">下班</text><text class="att-tl-time data-v-f6f89274">{{detail.punchOut&&detail.punchOut.time?detail.punchOut.time:'未打卡'}}</text></view></view></view></view></block><block wx:else><block wx:if="{{!loading}}"><view class="att-empty data-v-f6f89274"><text class="data-v-f6f89274">今日尚无记录,打卡后将显示在此</text></view></block></block></view><view class="att-card att-more-card data-v-f6f89274"><view class="att-more-title data-v-f6f89274">打卡照片(选填)</view><view class="att-upload-wrap data-v-f6f89274"><u-upload vue-id="0bf3a300-6" fileList="{{photoFileList}}" name="photo" maxCount="{{1}}" accept="image" width="120" height="120" data-event-opts="{{[['^afterRead',[['afterReadPhoto']]],['^delete',[['deletePhoto']]]]}}" bind:afterRead="__e" bind:delete="__e" class="data-v-f6f89274" bind:__l="__l"></u-upload></view><block wx:if="{{punchType===2}}"><view class="att-more-title att-more-title--second data-v-f6f89274">外勤说明(选填)</view><u-input bind:input="__e" vue-id="0bf3a300-7" placeholder="请填写外勤事由(选填)" border="none" customStyle="{{({background:'#f1f8f4',padding:'16rpx 20rpx',borderRadius:'12rpx'})}}" value="{{remark}}" data-event-opts="{{[['^input',[['__set_model',['','remark','$event',[]]]]]]}}" class="data-v-f6f89274" bind:__l="__l"></u-input></block></view><view class="att-safe data-v-f6f89274"></view></view><block wx:if="{{loading}}"><view class="att-loading-mask data-v-f6f89274"><u-loading-icon vue-id="0bf3a300-8" mode="circle" size="28" color="#43a047" class="data-v-f6f89274" bind:__l="__l"></u-loading-icon><text class="att-loading-txt data-v-f6f89274">加载中</text></view></block></view> | |
| 3 | 3 | \ No newline at end of file | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/pagesA/my-application-list/my-application-list.js
| ... | ... | @@ -219,11 +219,37 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va |
| 219 | 219 | // |
| 220 | 220 | // |
| 221 | 221 | // |
| 222 | +// | |
| 223 | +// | |
| 224 | +// | |
| 225 | +// | |
| 226 | +// | |
| 227 | +// | |
| 228 | +// | |
| 229 | +// | |
| 230 | +// | |
| 231 | +// | |
| 232 | +// | |
| 233 | +// | |
| 222 | 234 | var _default = { |
| 223 | 235 | data: function data() { |
| 224 | 236 | return { |
| 225 | 237 | loading: false, |
| 226 | 238 | list: [], |
| 239 | + statusFilter: 'all', | |
| 240 | + statusTabs: [{ | |
| 241 | + key: 'all', | |
| 242 | + label: '全部' | |
| 243 | + }, { | |
| 244 | + key: 'running', | |
| 245 | + label: '审核中' | |
| 246 | + }, { | |
| 247 | + key: 'passed', | |
| 248 | + label: '已通过' | |
| 249 | + }, { | |
| 250 | + key: 'rejected', | |
| 251 | + label: '不通过' | |
| 252 | + }], | |
| 227 | 253 | query: { |
| 228 | 254 | currentPage: 1, |
| 229 | 255 | pageSize: 20, |
| ... | ... | @@ -234,9 +260,17 @@ var _default = { |
| 234 | 260 | }; |
| 235 | 261 | }, |
| 236 | 262 | computed: { |
| 263 | + filteredList: function filteredList() { | |
| 264 | + if (!Array.isArray(this.list)) return []; | |
| 265 | + var st = this.getStatusFilterNumber(this.statusFilter); | |
| 266 | + if (st === null) return this.list; | |
| 267 | + return this.list.filter(function (item) { | |
| 268 | + return Number(item.status) === st; | |
| 269 | + }); | |
| 270 | + }, | |
| 237 | 271 | displayList: function displayList() { |
| 238 | 272 | var _this = this; |
| 239 | - return (this.list || []).map(function (item) { | |
| 273 | + return (this.filteredList || []).map(function (item) { | |
| 240 | 274 | return _objectSpread(_objectSpread({}, item), {}, { |
| 241 | 275 | statusClassName: _this.statusClass(item.status), |
| 242 | 276 | statusText: _this.getStatusText(item.status), |
| ... | ... | @@ -305,6 +339,11 @@ var _default = { |
| 305 | 339 | }, _callee, null, [[1, 11, 16, 20]]); |
| 306 | 340 | }))(); |
| 307 | 341 | }, |
| 342 | + changeStatusFilter: function changeStatusFilter(key) { | |
| 343 | + if (this.statusFilter === key) return; | |
| 344 | + this.statusFilter = key; | |
| 345 | + this.refresh(); | |
| 346 | + }, | |
| 308 | 347 | refresh: function refresh() { |
| 309 | 348 | this.query.currentPage = 1; |
| 310 | 349 | this.total = 0; |
| ... | ... | @@ -341,6 +380,15 @@ var _default = { |
| 341 | 380 | }; |
| 342 | 381 | return map[Number(status)] || 'status-default'; |
| 343 | 382 | }, |
| 383 | + // 业务状态:与 getStatusText / 后端 FlowTaskEntity.status 对齐 | |
| 384 | + // all: 全部;running: 待审核;passed: 已通过;rejected: 已驳回(不通过) | |
| 385 | + getStatusFilterNumber: function getStatusFilterNumber(key) { | |
| 386 | + if (key === 'all') return null; | |
| 387 | + if (key === 'running') return 1; | |
| 388 | + if (key === 'passed') return 2; | |
| 389 | + if (key === 'rejected') return 3; | |
| 390 | + return null; | |
| 391 | + }, | |
| 344 | 392 | getUrgentText: function getUrgentText(val) { |
| 345 | 393 | var map = { |
| 346 | 394 | 1: '普通', | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/pagesA/my-application-list/my-application-list.wxml
| 1 | -<view class="page data-v-3bb121e5"><view class="header-card data-v-3bb121e5"><view class="header-title data-v-3bb121e5">我的申请</view><view class="header-sub data-v-3bb121e5">查看我发起的流程申请,支持进入详情查看表单与审批进度</view></view><block wx:if="{{$root.g0}}"><view class="state-card data-v-3bb121e5"><text class="state-text data-v-3bb121e5">加载中...</text></view></block><block wx:else><block wx:if="{{$root.g1}}"><view class="state-card empty-card data-v-3bb121e5"><text class="empty-icon data-v-3bb121e5">无</text><text class="state-text data-v-3bb121e5">暂无申请记录</text></view></block><block wx:else><view class="list-wrap data-v-3bb121e5"><block wx:for="{{displayList}}" wx:for-item="item" wx:for-index="__i0__" wx:key="id"><view data-event-opts="{{[['tap',[['goDetail',['$0'],[[['displayList','id',item.id]]]]]]]}}" class="apply-card data-v-3bb121e5" bindtap="__e"><view class="card-head data-v-3bb121e5"><view class="title-wrap data-v-3bb121e5"><text class="apply-title data-v-3bb121e5">{{item.fullName||'无标题'}}</text><text class="apply-flow data-v-3bb121e5">{{item.flowName||item.flowCode||'流程'}}</text></view><view class="{{['status-tag','data-v-3bb121e5',item.statusClassName]}}">{{''+item.statusText+''}}</view></view><view class="meta-row data-v-3bb121e5"><text class="meta-label data-v-3bb121e5">发起时间</text><text class="meta-value data-v-3bb121e5">{{item.startTimeText}}</text></view><view class="meta-row data-v-3bb121e5"><text class="meta-label data-v-3bb121e5">当前节点</text><text class="meta-value data-v-3bb121e5">{{item.thisStep||'-'}}</text></view><view class="meta-row data-v-3bb121e5"><text class="meta-label data-v-3bb121e5">紧急程度</text><text class="meta-value data-v-3bb121e5">{{item.urgentText}}</text></view><view class="progress-row data-v-3bb121e5"><view class="progress-track data-v-3bb121e5"><view class="progress-inner data-v-3bb121e5" style="{{(item.progressWidthStyle)}}"></view></view><text class="progress-text data-v-3bb121e5">{{item.progressText}}</text></view></view></block></view></block></block><block wx:if="{{$root.g2}}"><view class="footer-tip data-v-3bb121e5">下拉可刷新,点击卡片查看详情</view></block></view> | |
| 2 | 1 | \ No newline at end of file |
| 2 | +<view class="page data-v-3bb121e5"><view class="header-card data-v-3bb121e5"><view class="header-title data-v-3bb121e5">我的申请</view><view class="header-sub data-v-3bb121e5">查看我发起的流程申请,支持进入详情查看表单与审批进度</view></view><view class="filter-bar data-v-3bb121e5"><block wx:for="{{statusTabs}}" wx:for-item="tab" wx:for-index="__i0__" wx:key="key"><view data-event-opts="{{[['tap',[['changeStatusFilter',['$0'],[[['statusTabs','key',tab.key,'key']]]]]]]}}" class="{{['filter-item','data-v-3bb121e5',statusFilter===tab.key?'filter-item--active':'']}}" bindtap="__e">{{''+tab.label+''}}</view></block></view><block wx:if="{{$root.g0}}"><view class="state-card data-v-3bb121e5"><text class="state-text data-v-3bb121e5">加载中...</text></view></block><block wx:else><block wx:if="{{$root.g1}}"><view class="state-card empty-card data-v-3bb121e5"><text class="empty-icon data-v-3bb121e5">无</text><text class="state-text data-v-3bb121e5">暂无申请记录</text></view></block><block wx:else><view class="list-wrap data-v-3bb121e5"><block wx:for="{{displayList}}" wx:for-item="item" wx:for-index="__i1__" wx:key="id"><view data-event-opts="{{[['tap',[['goDetail',['$0'],[[['displayList','id',item.id]]]]]]]}}" class="apply-card data-v-3bb121e5" bindtap="__e"><view class="card-head data-v-3bb121e5"><view class="title-wrap data-v-3bb121e5"><text class="apply-title data-v-3bb121e5">{{item.fullName||'无标题'}}</text><text class="apply-flow data-v-3bb121e5">{{item.flowName||item.flowCode||'流程'}}</text></view><view class="{{['status-tag','data-v-3bb121e5',item.statusClassName]}}">{{''+item.statusText+''}}</view></view><view class="meta-row data-v-3bb121e5"><text class="meta-label data-v-3bb121e5">发起时间</text><text class="meta-value data-v-3bb121e5">{{item.startTimeText}}</text></view><view class="meta-row data-v-3bb121e5"><text class="meta-label data-v-3bb121e5">当前节点</text><text class="meta-value data-v-3bb121e5">{{item.thisStep||'-'}}</text></view><view class="meta-row data-v-3bb121e5"><text class="meta-label data-v-3bb121e5">紧急程度</text><text class="meta-value data-v-3bb121e5">{{item.urgentText}}</text></view><view class="progress-row data-v-3bb121e5"><view class="progress-track data-v-3bb121e5"><view class="progress-inner data-v-3bb121e5" style="{{(item.progressWidthStyle)}}"></view></view><text class="progress-text data-v-3bb121e5">{{item.progressText}}</text></view></view></block></view></block></block><block wx:if="{{$root.g2}}"><view class="footer-tip data-v-3bb121e5">下拉可刷新,点击卡片查看详情</view></block></view> | |
| 3 | 3 | \ No newline at end of file | ... | ... |
绿纤uni-app/unpackage/dist/dev/mp-weixin/pagesA/my-application-list/my-application-list.wxss
| ... | ... | @@ -30,6 +30,28 @@ |
| 30 | 30 | box-sizing: border-box; |
| 31 | 31 | background: linear-gradient(180deg, #e8f5e9 0%, #f7fff8 100%); |
| 32 | 32 | } |
| 33 | +.filter-bar.data-v-3bb121e5 { | |
| 34 | + display: flex; | |
| 35 | + gap: 16rpx; | |
| 36 | + padding: 0 12rpx; | |
| 37 | + margin-bottom: 22rpx; | |
| 38 | +} | |
| 39 | +.filter-item.data-v-3bb121e5 { | |
| 40 | + flex: 1; | |
| 41 | + text-align: center; | |
| 42 | + padding: 14rpx 0; | |
| 43 | + border-radius: 999rpx; | |
| 44 | + background: rgba(255, 255, 255, 0.7); | |
| 45 | + color: #7a8b7b; | |
| 46 | + font-size: 24rpx; | |
| 47 | + font-weight: 600; | |
| 48 | + border: 1px solid rgba(46, 125, 50, 0.12); | |
| 49 | +} | |
| 50 | +.filter-item--active.data-v-3bb121e5 { | |
| 51 | + background: #e8f5e9; | |
| 52 | + color: #2e7d32; | |
| 53 | + border-color: rgba(46, 125, 50, 0.35); | |
| 54 | +} | |
| 33 | 55 | .header-card.data-v-3bb121e5, |
| 34 | 56 | .state-card.data-v-3bb121e5, |
| 35 | 57 | .apply-card.data-v-3bb121e5 { | ... | ... |
项目文档相关/docs/数据库说明.md
| ... | ... | @@ -56,6 +56,11 @@ |
| 56 | 56 | - `longitude` (DECIMAL): 经度 |
| 57 | 57 | - `latitude` (DECIMAL): 纬度 |
| 58 | 58 | - `fence_polygons` (JSON): 电子围栏多边形坐标,格式 `[[{lng,lat},...]]` |
| 59 | + - `F_AttendanceCheckFence` (INT,可空): 正常打卡是否启用电子围栏校验(1-是,0-否;**可空**:空表示沿用历史默认「有围栏则校验」) | |
| 60 | + - `F_AttendanceCheckWifi` (INT,可空): 正常打卡是否启用 Wi-Fi 校验(1-是,0-否) | |
| 61 | + - `F_AttendanceWifiPairs` (VARCHAR): Wi-Fi 成对白名单,JSON 数组,如 `[{"ssid":"门店5G","bssid":"aa:bb:cc:dd:ee:ff"}]` | |
| 62 | + - `F_AttendanceWifiVerifyPair` (INT,可空,默认 0): **是否校验 SSID 与 BSSID 为同一 AP**。`1`:端上能拿到 BSSID 时必须与某行 **SSID+BSSID 同时一致**;端上 **拿不到 BSSID** 时须 **在电子围栏内** 且 SSID 与某行一致(防同名热点)。`0`:不强制同 AP,只要当前 SSID 或 BSSID **命中任意一行中的字段** 即可(仍按行维护,但校验为「或」) | |
| 63 | + - ~~`F_AttendanceWifiSsids` / `F_AttendanceWifiBssids`~~:已删除,统一使用 `F_AttendanceWifiPairs`(脚本:`项目文档相关/sql/2026-4-3/门店考勤_围栏与WiFi打卡配置.sql`) | |
| 59 | 64 | - `F_BusinessHours` (TEXT): 营业时间设置 |
| 60 | 65 | - `F_TrafficTips` (TEXT): 交通提示 |
| 61 | 66 | - **人员信息**: `BASE_USER` (系统用户表,包含门店ID等扩展字段) |
| ... | ... | @@ -161,7 +166,7 @@ |
| 161 | 166 | - `F_PunchOutLongitude` / `F_PunchOutLatitude` / `F_PunchOutAddress`: 下班打卡定位与地址 |
| 162 | 167 | - `F_PunchInPhotoUrl` / `F_PunchOutPhotoUrl`: 上下班打卡照片 |
| 163 | 168 | - `F_IsPunchInFenceValid` / `F_IsPunchOutFenceValid`: 上下班围栏校验结果(1围栏内,0围栏外,null未校验) |
| 164 | - - `F_Status`: 打卡状态(1正常,2迟到,3休息,4请假,5病假,6缺卡,7旷工) | |
| 169 | + - `F_Status`: 打卡状态(1正常,2迟到,3休息,4请假,5病假,6缺卡,7旷工,8待考勤;8 表示考勤日晚于当前日期且尚无打卡,界面展示「暂无打卡」,不计入缺卡类异常统计) | |
| 165 | 170 | - `F_LateMinutes`: 迟到分钟数 |
| 166 | 171 | - `F_EarlyLeaveMinutes`: 早退分钟数 |
| 167 | 172 | - `F_IsManual`: 是否手动补录 | ... | ... |
项目文档相关/sql/2026-4-3/门店考勤_WiFi_BSSID白名单列.sql
0 → 100644
项目文档相关/sql/2026-4-3/门店考勤_围栏与WiFi打卡配置.sql
0 → 100644
| 1 | +-- 门店正常打卡:围栏 + Wi-Fi 成对配置(SSID+BSSID) | |
| 2 | +-- 可重复执行。会删除已废弃列 F_AttendanceWifiSsids、F_AttendanceWifiBssids(数据请先迁移到 F_AttendanceWifiPairs) | |
| 3 | +-- 执行前:USE 你的库名; | |
| 4 | + | |
| 5 | +DELIMITER $$ | |
| 6 | + | |
| 7 | +DROP PROCEDURE IF EXISTS `sp_patch_lq_mdxx_attendance_wifi`$$ | |
| 8 | + | |
| 9 | +CREATE PROCEDURE `sp_patch_lq_mdxx_attendance_wifi`() | |
| 10 | +BEGIN | |
| 11 | + DECLARE dbname VARCHAR(64); | |
| 12 | + SET dbname = DATABASE(); | |
| 13 | + | |
| 14 | + -- 1) 基础列:围栏开关、Wi-Fi 开关 | |
| 15 | + IF NOT EXISTS ( | |
| 16 | + SELECT 1 FROM information_schema.COLUMNS | |
| 17 | + WHERE TABLE_SCHEMA = dbname AND TABLE_NAME = 'lq_mdxx' AND COLUMN_NAME = 'F_AttendanceCheckFence' | |
| 18 | + ) THEN | |
| 19 | + ALTER TABLE `lq_mdxx` | |
| 20 | + ADD COLUMN `F_AttendanceCheckFence` int NULL DEFAULT NULL | |
| 21 | + COMMENT '正常打卡是否启用围栏校验:1是 0否;NULL 表示沿用旧逻辑(有围栏则校验)' | |
| 22 | + AFTER `fence_polygons`; | |
| 23 | + END IF; | |
| 24 | + | |
| 25 | + IF NOT EXISTS ( | |
| 26 | + SELECT 1 FROM information_schema.COLUMNS | |
| 27 | + WHERE TABLE_SCHEMA = dbname AND TABLE_NAME = 'lq_mdxx' AND COLUMN_NAME = 'F_AttendanceCheckWifi' | |
| 28 | + ) THEN | |
| 29 | + ALTER TABLE `lq_mdxx` | |
| 30 | + ADD COLUMN `F_AttendanceCheckWifi` int NULL DEFAULT NULL | |
| 31 | + COMMENT '正常打卡是否启用Wi-Fi校验:1是 0否;NULL 视为0' | |
| 32 | + AFTER `F_AttendanceCheckFence`; | |
| 33 | + END IF; | |
| 34 | + | |
| 35 | + -- 2) 删除废弃列(独立 SSID / BSSID JSON 列,已由 F_AttendanceWifiPairs 替代) | |
| 36 | + IF EXISTS ( | |
| 37 | + SELECT 1 FROM information_schema.COLUMNS | |
| 38 | + WHERE TABLE_SCHEMA = dbname AND TABLE_NAME = 'lq_mdxx' AND COLUMN_NAME = 'F_AttendanceWifiBssids' | |
| 39 | + ) THEN | |
| 40 | + ALTER TABLE `lq_mdxx` DROP COLUMN `F_AttendanceWifiBssids`; | |
| 41 | + END IF; | |
| 42 | + | |
| 43 | + IF EXISTS ( | |
| 44 | + SELECT 1 FROM information_schema.COLUMNS | |
| 45 | + WHERE TABLE_SCHEMA = dbname AND TABLE_NAME = 'lq_mdxx' AND COLUMN_NAME = 'F_AttendanceWifiSsids' | |
| 46 | + ) THEN | |
| 47 | + ALTER TABLE `lq_mdxx` DROP COLUMN `F_AttendanceWifiSsids`; | |
| 48 | + END IF; | |
| 49 | + | |
| 50 | + -- 3) 成对配置列(加在 Wi-Fi 开关后;此时已不存在旧列) | |
| 51 | + IF NOT EXISTS ( | |
| 52 | + SELECT 1 FROM information_schema.COLUMNS | |
| 53 | + WHERE TABLE_SCHEMA = dbname AND TABLE_NAME = 'lq_mdxx' AND COLUMN_NAME = 'F_AttendanceWifiPairs' | |
| 54 | + ) THEN | |
| 55 | + ALTER TABLE `lq_mdxx` | |
| 56 | + ADD COLUMN `F_AttendanceWifiPairs` varchar(4000) NULL DEFAULT NULL | |
| 57 | + COMMENT 'Wi-Fi成对配置 JSON [{"ssid":"","bssid":""}]' | |
| 58 | + AFTER `F_AttendanceCheckWifi`; | |
| 59 | + END IF; | |
| 60 | + | |
| 61 | + IF NOT EXISTS ( | |
| 62 | + SELECT 1 FROM information_schema.COLUMNS | |
| 63 | + WHERE TABLE_SCHEMA = dbname AND TABLE_NAME = 'lq_mdxx' AND COLUMN_NAME = 'F_AttendanceWifiVerifyPair' | |
| 64 | + ) THEN | |
| 65 | + ALTER TABLE `lq_mdxx` | |
| 66 | + ADD COLUMN `F_AttendanceWifiVerifyPair` int NULL DEFAULT 0 | |
| 67 | + COMMENT '1=校验SSID与BSSID为同一AP;0=SSID或BSSID命中任一行即可' | |
| 68 | + AFTER `F_AttendanceWifiPairs`; | |
| 69 | + END IF; | |
| 70 | +END$$ | |
| 71 | + | |
| 72 | +DELIMITER ; | |
| 73 | + | |
| 74 | +CALL `sp_patch_lq_mdxx_attendance_wifi`(); | |
| 75 | + | |
| 76 | +DROP PROCEDURE IF EXISTS `sp_patch_lq_mdxx_attendance_wifi`; | ... | ... |