hn_platform_h5/src/pages/leader/leave-request/index.vue

1610 lines
48 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-container">
<NavBarModal navBarTitle="请假">
<template #left>
<view class="back-btn" @tap="handleBack">
<up-icon name="arrow-left" size="20" color="#fff" />
</view>
</template>
</NavBarModal>
<view class="content-wrapper" :style="contentStyle">
<!-- Tab切换 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'form' }"
@tap="handleTabChange('form')"
>
<text class="tab-text">请假填写</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'record' }"
@tap="handleTabChange('record')"
>
<text class="tab-text">请假记录</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'approval' }"
@tap="handleTabChange('approval')"
>
<text class="tab-text">请假审批</text>
</view>
</view>
<!-- 请假填写 -->
<scroll-view v-if="activeTab === 'form'" class="scroll-container" scroll-y>
<up-form :model="formData" :rules="rules" ref="formRef" labelWidth="100">
<!-- 请假类型 -->
<up-form-item label="请假类型" prop="leaveTypeId" required :borderBottom="true">
<view class="form-picker-wrapper" @tap="handleOpenLeaveTypePicker">
<text
class="picker-text"
:class="{ placeholder: !formData.leaveTypeId }"
>
{{ formData.leaveTypeName || '请选择请假类型' }}
</text>
<up-icon name="arrow-right" size="16" color="#999" />
</view>
</up-form-item>
<!-- 请假日期 -->
<up-form-item label="请假日期" prop="leaveDate" required :borderBottom="true">
<view class="form-picker-wrapper" @tap="handleOpenDatePicker">
<text class="picker-text" :class="{ placeholder: !leaveDateText }">
{{ leaveDateText || '请选择请假日期范围' }}
</text>
<up-icon name="arrow-right" size="16" color="#999" />
</view>
</up-form-item>
<!-- 请假事由 -->
<up-form-item label="请假事由" prop="reason" :borderBottom="true">
<up-textarea
count
:maxlength="140"
:autoHeight="true"
v-model="formData.reason"
placeholder="根据xx安排,参加xx"
/>
</up-form-item>
<!-- 请假时长 -->
<up-form-item label="请假时长" prop="duration" :borderBottom="true">
<up-input v-model="formData.duration" type="number" placeholder="0" />
</up-form-item>
<!-- 请假前需履职天数 -->
<up-form-item label="请假前需履职天数" prop="beforeDays" :borderBottom="true">
<up-input
v-model="formData.beforeDays"
type="number"
placeholder="0"
:disabled="true"
/>
</up-form-item>
<!-- 请假后需履职天数 -->
<up-form-item label="请假后需履职天数" prop="afterDays" :borderBottom="true">
<up-input
v-model="formData.afterDays"
type="number"
placeholder="0"
:disabled="true"
/>
</up-form-item>
<!-- 审批人 -->
<up-form-item label="审批人" prop="approverId" :borderBottom="true">
<view class="form-picker-wrapper" @tap="handleOpenApproverPicker">
<text
class="picker-text"
:class="{ placeholder: !formData.approverId }"
>
{{ approverText || '请选择审批人' }}
</text>
<up-icon name="arrow-right" size="16" color="#999" />
</view>
</up-form-item>
<!-- 上传附件 -->
<up-form-item label="上传附件" prop="attachments" :borderBottom="true">
<up-upload
accept="file"
:fileList="fileList"
@afterRead="handleAfterRead"
@delete="handleDelete"
name="attachments"
multiple
:maxCount="9"
/>
</up-form-item>
</up-form>
<!-- 提交按钮 -->
<view class="submit-section">
<up-button
:text="submitButtonText"
type="primary"
:loading="submitting"
@tap="handleSubmit"
/>
</view>
</scroll-view>
<!-- 请假记录/请假审批 -->
<view v-if="activeTab === 'record' || activeTab === 'approval'" class="list-wrapper">
<!-- 查询条件 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<CommonPicker
:options="yearOptions"
:modelValue="queryParams.year"
placeholder="选择年份"
@change="handleYearChange"
/>
</view>
<view class="filter-item">
<CommonPicker
:options="monthOptions"
:modelValue="queryParams.month"
placeholder="全部月"
@change="handleMonthChange"
/>
</view>
</view>
</view>
<!-- 记录列表 -->
<scroll-view
class="list-container"
scroll-y
@scrolltolower="onHandleScrollToLower"
v-if="recordList.length > 0"
>
<view
v-for="(item, index) in recordList"
:key="item.id || index"
class="record-item"
@tap="handleRecordClick(item)"
>
<view class="record-header">
<view class="record-user">
<up-icon name="account" size="18" color="#07c160" />
<text class="record-title">{{ item.title || '请假记录' }}</text>
</view>
<text class="record-date">{{ item.createTime || '--' }}</text>
</view>
<view class="record-type">
<text>请假类型:{{ item.typeText || '暂无类型' }}</text>
</view>
<view class="record-body">
<view class="record-range">
<up-icon name="calendar" size="16" color="#999" />
<text class="record-range-text">
{{ item.dateRange || '暂无日期' }}
</text>
</view>
<text class="record-days" v-if="item.leaveDays || item.leaveCnt">
{{ item.leaveDays || item.leaveCnt }}天
</text>
</view>
<view class="record-status">
<view class="status-left">
<text
class="status-tag"
:class="`status-${item.statusType || 'default'}`"
>
{{ item.statusText || '待审批' }}
</text>
<text
v-if="item.statusType === 'success'"
class="status-tag status-success status-outline"
>
结束
</text>
</view>
<view class="record-actions">
<!-- 请假审批tab显示同意和拒绝按钮 -->
<template v-if="activeTab === 'approval'">
<up-button
text="同意"
size="small"
type="primary"
@tap.stop="handleApprove(item)"
/>
<up-button
text="拒绝"
size="small"
type="error"
@tap.stop="handleReject(item)"
/>
</template>
<!-- 请假记录tab显示修改、撤销或结束按钮 -->
<template v-else>
<template v-if="item.canEdit">
<up-button
text="修改"
size="small"
type="primary"
plain
@tap.stop="handleModify(item)"
/>
<up-button
text="撤销"
size="small"
type="warning"
plain
@tap.stop="handleRevoke(item)"
/>
</template>
<up-button
v-else
text="结束"
size="small"
type="success"
plain
@tap.stop="handleFinish(item)"
/>
</template>
</view>
</view>
</view>
<view class="loading-text">
{{ !hasMore ? '没有更多数据了~' : '正在加载...' }}
</view>
</scroll-view>
<!-- 空状态 -->
<view v-if="recordList.length === 0 && !loading" class="empty-state">
<ReviewEmptyState text="数据为空" />
</view>
</view>
</view>
<!-- 请假类型选择器 -->
<up-picker
:show="showLeaveTypePicker"
:columns="leaveTypeColumns"
closeOnClickOverlay
@cancel="showLeaveTypePicker = false"
@confirm="handleLeaveTypeConfirm"
@close="showLeaveTypePicker = false"
/>
<!-- 开始日期选择器 -->
<up-datetime-picker
:show="showStartDatePicker"
mode="date"
v-model="startDatePickerValue"
@cancel="showStartDatePicker = false"
@confirm="handleStartDateConfirm"
/>
<!-- 结束日期选择器 -->
<up-datetime-picker
:show="showEndDatePicker"
mode="date"
v-model="endDatePickerValue"
:minDate="computedEndDateMinDate"
@cancel="showEndDatePicker = false"
@confirm="handleEndDateConfirm"
/>
<!-- 审批人选择器 -->
<up-picker
:show="showApproverPicker"
:columns="approverColumns"
closeOnClickOverlay
@cancel="showApproverPicker = false"
@confirm="handleApproverConfirm"
@close="showApproverPicker = false"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { debounce } from 'lodash-es'
import dayjs from 'dayjs'
import NavBarModal from '@/components/NavBarModal/index.vue'
import ReviewEmptyState from '@/components/ReviewEmptyState/index.vue'
import CommonPicker from '@/components/CommonPicker/index.vue'
import { getContentStyle } from '@/utils/safeArea'
import { useMemberStore } from '@/stores'
import {
getLeaveTypeListApi,
getApproverListApi,
submitLeaveRequestApi,
getMyLeaveRecordListApi,
getLeaveApprovalListApi,
getLeaveDetailApi,
getPersonPerformanceDaysApi,
checkHasUnfinishedLeaveApi,
endLeaveRequestApi,
approveLeaveRequestApi,
rejectLeaveRequestApi,
} from '@/services/leader/leave-request'
/**
* 请假页面
* 业务背景:请假申请、请假记录查看、请假审批
* 设计决策:
* 1. 支持三个Tab请假填写、请假记录、请假审批
* 2. 请假记录和请假审批使用相同的查询条件(年份、月份)
* 3. 支持滚动触底分页加载
*/
const contentStyle = computed(() => {
return getContentStyle({
includeNavBar: true,
includeStatusBar: true,
includeBottomSafeArea: true,
})
})
// 当前激活的Tab
const activeTab = ref('form') // 'form' | 'record' | 'approval'
const isEditing = ref(false)
const editingRecord = ref(null)
// 表单相关
const formRef = ref(null)
const formData = ref({
leaveTypeId: '',
leaveTypeName: '',
startDate: null,
endDate: null,
reason: '',
duration: '0',
beforeDays: '0',
afterDays: '0',
approverId: '',
approverName: '',
attachments: [],
})
// 表单验证规则
const rules = ref({
leaveTypeId: [
{
type: 'number',
required: true,
message: '请选择请假类型',
trigger: 'change',
},
],
leaveDate: [
{
validator: (rule, value, callback) => {
if (!formData.value.startDate || !formData.value.endDate) {
callback(new Error('请选择请假日期范围'))
} else {
callback()
}
},
trigger: 'change',
},
],
})
// 提交状态
const submitting = ref(false)
// 文件列表
const fileList = ref([])
// 请假类型相关
const leaveTypeList = ref([])
const showLeaveTypePicker = ref(false)
const leaveTypeColumns = computed(() => {
return [
leaveTypeList.value.map((item) => ({
text: item.dictLabel,
value: item.dictCode || item.dictValue,
})),
]
})
// 日期相关
const showStartDatePicker = ref(false)
const showEndDatePicker = ref(false)
const startDatePickerValue = ref(Date.now())
const endDatePickerValue = ref(Date.now())
/**
* 计算结束日期选择器的最小日期
* 业务背景:确保结束日期不早于开始日期
* 设计决策:如果已选择开始日期,则结束日期的最小值为开始日期;否则为当前日期
*/
const computedEndDateMinDate = computed(() => {
if (formData.value.startDate) {
return typeof formData.value.startDate === 'number'
? formData.value.startDate
: new Date(formData.value.startDate).getTime()
}
return Date.now()
})
const leaveDateText = computed(() => {
if (!formData.value.startDate || !formData.value.endDate) return ''
const start = dayjs(formData.value.startDate).format('YYYY-MM-DD')
const end = dayjs(formData.value.endDate).format('YYYY-MM-DD')
return `${start}${end}`
})
// 审批人相关
const approverList = ref([])
const showApproverPicker = ref(false)
const approverColumns = computed(() => {
return [
approverList.value.map((item) => ({
text: item.nickName || item.name,
value: item.userId || item.id,
})),
]
})
const approverText = computed(() => {
if (!formData.value.approverId) return ''
return formData.value.approverName || ''
})
// 记录列表相关
const recordList = ref([])
const total = ref(0)
const loading = ref(false)
// 查询参数
const queryParams = ref({
pageNum: 1,
year: new Date().getFullYear().toString(),
month: '',
})
/**
* 计算请假记录查询的时间范围
* 业务背景:后端列表接口要求按起止日期筛选,不接受 pageSize
* 设计决策:保留年/月选择体验,将其转换为 startTime、endTime
*/
const computedRecordRange = computed(() => {
const year = queryParams.value.year
const month = queryParams.value.month
if (month) {
const start = dayjs(`${year}-${month}-01`)
return {
startTime: start.format('YYYY-MM-DD'),
endTime: start.endOf('month').format('YYYY-MM-DD'),
}
}
return {
startTime: dayjs(`${year}-01-01`).format('YYYY-MM-DD'),
endTime: dayjs(`${year}-12-31`).format('YYYY-MM-DD'),
}
})
// 年份选项当前年份前后5年
const yearOptions = computed(() => {
const currentYear = new Date().getFullYear()
const years = []
for (let i = currentYear - 5; i <= currentYear + 5; i++) {
years.push({
label: `${i}`,
value: i.toString(),
})
}
return years
})
// 月份选项
const monthOptions = ref([
{ label: '全部月', value: '' },
{ label: '1月', value: '1' },
{ label: '2月', value: '2' },
{ label: '3月', value: '3' },
{ label: '4月', value: '4' },
{ label: '5月', value: '5' },
{ label: '6月', value: '6' },
{ label: '7月', value: '7' },
{ label: '8月', value: '8' },
{ label: '9月', value: '9' },
{ label: '10月', value: '10' },
{ label: '11月', value: '11' },
{ label: '12月', value: '12' },
])
/**
* 计算提交按钮文案
* 业务背景:编辑模式需要提示“保存修改”,新增则为“提交请假”
*/
const submitButtonText = computed(() => {
return isEditing.value ? '保存修改' : '提交请假'
})
/**
* 计算是否还有更多数据
*/
const hasMore = computed(() => {
return recordList.value.length < total.value
})
/**
* 处理Tab切换
* @param {String} tab - Tab类型
*/
const handleTabChange = (tab) => {
activeTab.value = tab
if (tab === 'record' || tab === 'approval') {
// 重置分页和列表
queryParams.value.pageNum = 1
recordList.value = []
total.value = 0
loadRecordList()
}
}
/**
* 打开请假类型选择器
*/
const handleOpenLeaveTypePicker = () => {
if (leaveTypeList.value.length === 0) {
loadLeaveTypeList()
}
showLeaveTypePicker.value = true
}
/**
* 处理请假类型确认
* 业务背景保存请假类型ID和名称
*/
const handleLeaveTypeConfirm = (e) => {
const selectedValue = e.value[0]?.value ?? e.value[0]
const selectedItem = leaveTypeList.value.find(
(item) => (item.dictCode || item.dictValue) === selectedValue,
)
formData.value.leaveTypeId = selectedValue
formData.value.leaveTypeName = selectedItem?.dictLabel || e.value[0]?.text || ''
showLeaveTypePicker.value = false
}
/**
* 打开日期选择器
* 业务背景:打开日期范围选择器,先选择开始日期,再选择结束日期
* 设计决策:分两步选择,确保结束日期不早于开始日期
*/
const handleOpenDatePicker = () => {
if (formData.value.startDate) {
startDatePickerValue.value = formData.value.startDate
} else {
startDatePickerValue.value = Date.now()
}
showStartDatePicker.value = true
}
/**
* 处理开始日期确认
* 业务背景:选择开始日期后,自动打开结束日期选择器
*/
const handleStartDateConfirm = (e) => {
formData.value.startDate = e.value
startDatePickerValue.value = e.value
showStartDatePicker.value = false
// 如果结束日期早于开始日期,重置结束日期
if (formData.value.endDate && formData.value.endDate < e.value) {
formData.value.endDate = null
}
// 打开结束日期选择器
if (formData.value.endDate) {
endDatePickerValue.value = formData.value.endDate
} else {
// 默认结束日期为开始日期
endDatePickerValue.value = e.value
}
showEndDatePicker.value = true
}
/**
* 处理结束日期确认
* 业务背景:选择结束日期后,调用接口获取请假详情数据并回显
*/
const handleEndDateConfirm = async (e) => {
formData.value.endDate = e.value
endDatePickerValue.value = e.value
showEndDatePicker.value = false
// 调用接口获取请假详情
await loadLeaveDetail()
}
/**
* 打开审批人选择器
*/
const handleOpenApproverPicker = () => {
if (approverList.value.length === 0) {
loadApproverList()
}
showApproverPicker.value = true
}
/**
* 处理审批人确认
* 业务背景保存审批人ID和名称
*/
const handleApproverConfirm = (e) => {
const selectedValue = e.value[0]?.value ?? e.value[0]
const selectedItem = approverList.value.find(
(item) => (item.userId || item.id) === selectedValue,
)
formData.value.approverId = selectedValue
formData.value.approverName = selectedItem?.nickName || selectedItem?.name || ''
showApproverPicker.value = false
}
/**
* 上传文件到服务器
* 业务背景将文件上传到服务器返回文件URL
* @param {String} filePath - 文件路径
* @returns {Promise<String>} 返回文件URL
*/
const uploadFilePromise = (filePath) => {
console.log(filePath, 'filePath')
return new Promise((resolve, reject) => {
uni.uploadFile({
url: '/common/upload',
filePath: filePath,
name: 'file',
formData: {
user: 'test',
},
success: (res) => {
if (res.data) {
try {
const data = JSON.parse(res.data)
if (data.code === 200) {
const fileName = data.fileName || data.data
resolve(fileName)
} else {
reject(new Error(data.msg || '上传失败'))
}
} catch (err) {
reject(new Error('解析响应失败'))
}
} else {
reject(new Error('上传失败'))
}
},
fail: (err) => {
reject(err)
},
})
})
}
/**
* 处理文件上传
* 业务背景:上传文件到服务器,保存完整的文件信息对象
* 设计决策:保存文件信息对象,包含 url, size, name, type, status, message 等字段
*/
const handleAfterRead = async (event) => {
const { file } = event
console.log(file, 'file')
try {
const fileName = await uploadFilePromise(file[0].url)
// 构建文件信息对象
const fileInfo = {
url: fileName,
size: file.size || 0,
name: file.name || '',
type: file.type || '',
status: 'success',
message: '',
}
fileList.value.push({
...file,
url: fileName,
status: 'success',
})
formData.value.attachments.push(fileInfo)
} catch (error) {
console.error('文件上传失败:', error)
uni.showToast({
title: '上传失败',
icon: 'none',
})
}
}
/**
* 处理文件删除
*/
const handleDelete = (event) => {
const { index } = event
fileList.value.splice(index, 1)
formData.value.attachments.splice(index, 1)
}
/**
* 处理提交
* 业务背景:按照后台接口要求的参数格式组装提交数据
* 设计决策:
* 1. 将日期格式化为 YYYY-MM-DD
* 2. 将文件信息数组转换为 JSON 字符串
* 3. 将数字字段转换为数字类型
* 4. 组装 startEndTime 格式为 "YYYY-MM-DD / YYYY-MM-DD"
*/
const handleSubmit = async () => {
console.log(formData.value, 'formData.value.attachments')
try {
await formRef.value.validate()
submitting.value = true
const startTime = dayjs(formData.value.startDate).format('YYYY-MM-DD')
const endTime = dayjs(formData.value.endDate).format('YYYY-MM-DD')
const startEndTime = `${startTime} / ${endTime}`
const submitData = {
approverId: formData.value.approverId,
approverName: formData.value.approverName,
endTime: endTime,
fileInfo: JSON.stringify(formData.value.attachments),
leaveDays: Number(formData.value.duration) || 0,
leaveTypeId: formData.value.leaveTypeId,
leaveTypeName: formData.value.leaveTypeName,
lzDaysAfterLeave: Number(formData.value.afterDays) || 0,
lzDaysBeforeLeave: Number(formData.value.beforeDays) || 0,
reason: formData.value.reason,
startEndTime: startEndTime,
startTime: startTime,
}
await submitLeaveRequestApi(submitData)
uni.showToast({
title: '提交成功',
icon: 'success',
})
// 重置表单
formData.value = {
leaveTypeId: '',
leaveTypeName: '',
startDate: null,
endDate: null,
reason: '',
duration: '0',
beforeDays: '0',
afterDays: '0',
approverId: '',
approverName: '',
attachments: [],
}
fileList.value = []
} catch (error) {
console.error('提交失败:', error)
if (error.message) {
uni.showToast({
title: error.message,
icon: 'none',
})
}
} finally {
submitting.value = false
}
}
/**
* 加载请假类型列表
*/
const loadLeaveTypeList = async () => {
try {
const res = await getLeaveTypeListApi()
leaveTypeList.value = res?.rows || res?.data || []
} catch (error) {
console.error('加载请假类型列表失败:', error)
}
}
/**
* 加载审批人列表
*/
const loadApproverList = async () => {
try {
const res = await getApproverListApi()
approverList.value = res?.rows || res?.data || []
} catch (error) {
console.error('加载审批人列表失败:', error)
}
}
/**
* 加载请假详情数据
* 业务背景:根据选择的开始日期和结束日期,调用接口获取请假时长、履职天数等信息
* 设计决策:在用户选择完日期范围后自动调用,回显相关字段
*/
const loadLeaveDetail = async () => {
if (!formData.value.startDate || !formData.value.endDate) {
return
}
try {
const startDate = dayjs(formData.value.startDate).format('YYYY-MM-DD')
const endDate = dayjs(formData.value.endDate).format('YYYY-MM-DD')
const res = await getLeaveDetailApi({
startDate,
endDate,
})
// 回显数据
if (res && res.data) {
// 请假时长
if (res.data.leaveDays !== undefined) {
formData.value.duration = String(res.data.leaveDays)
}
// 请假前需履职天数
if (res.data.lzDaysBeforeLeave !== undefined) {
formData.value.beforeDays = String(res.data.lzDaysBeforeLeave)
}
// 请假后需履职天数
if (res.data.lzDaysAfterLeave !== undefined) {
formData.value.afterDays = String(res.data.lzDaysAfterLeave)
}
}
} catch (error) {
console.error('加载请假详情失败:', error)
uni.showToast({
title: '获取请假详情失败',
icon: 'none',
})
}
}
/**
* 处理年份变更
*/
const handleYearChange = (item) => {
queryParams.value.year = item.value
queryParams.value.pageNum = 1
recordList.value = []
loadRecordList()
}
/**
* 处理月份变更
*/
const handleMonthChange = (item) => {
queryParams.value.month = item.value
queryParams.value.pageNum = 1
recordList.value = []
loadRecordList()
}
/**
* 根据状态值映射展示样式
* 业务背景:后端可能返回多种状态文案,需要统一前端展示颜色
* 设计决策:简单映射几类常见状态,其余走默认灰色
*/
const mapStatusType = (statusText = '') => {
const text = statusText.toString()
if (['待审批', '审批中', '审核中'].some((v) => text.includes(v))) return 'pending'
if (['同意', '通过', '已审批', '完成', '结束'].some((v) => text.includes(v))) return 'success'
if (['拒绝', '驳回', '不通过'].some((v) => text.includes(v))) return 'error'
return 'default'
}
/**
* 判断是否可修改/撤销
* 业务背景:仅未来的请假可修改和撤销
* 设计决策:使用开始日期与今日对比,精确到天
*/
const canEditLeave = (startDate) => {
if (!startDate) return false
return dayjs(startDate).isAfter(dayjs(), 'day')
}
/**
* 计算规范化的开始、结束日期文本
* 业务背景:后端字段命名不统一,需前端兜底
*/
const normalizeStartEnd = (item) => {
const startRaw =
item.startTime || item.startDate || item.beginDate || item.startDateStr || item.start_at
const endRaw = item.endTime || item.endDate || item.finishDate || item.endDateStr || item.end_at
const start = startRaw ? dayjs(startRaw) : null
const end = endRaw ? dayjs(endRaw) : null
return {
startRaw,
endRaw,
startText: start?.format('YYYY-MM-DD') || '',
endText: end?.format('YYYY-MM-DD') || '',
}
}
/**
* 加载记录列表
* 业务背景:根据时间范围与页码获取请假记录/审批列表
* 设计决策:请求参数使用 startTime/endTime移除 pageSize前端保留分页 pageNum
* 注意:请假审批和请假记录使用同一个接口
*/
const loadRecordList = async () => {
try {
loading.value = true
// 请假审批和请假记录使用同一个接口
const res = await getMyLeaveRecordListApi({
pageNum: queryParams.value.pageNum,
startTime: computedRecordRange.value.startTime,
endTime: computedRecordRange.value.endTime,
})
total.value = res?.total || 0
if (res?.rows && res.rows.length > 0) {
// 处理数据,添加占位字段
const processedData = res.rows.map((item) => {
const normalized = normalizeStartEnd(item)
return {
...item,
title:
item.userName || item.nickName
? `${item.userName || item.nickName}的请假`
: item.title || '请假记录',
typeText: item.leaveTypeName || item.typeName || item.leaveType || '请假',
createTime: item.createTime || item.createTimeStr || item.date || '',
dateRange:
(item.startTime && item.endTime && `${item.startTime}~${item.endTime}`) ||
(normalized.startText && normalized.endText
? `${normalized.startText}~${normalized.endText}`
: '') ||
item.startEndTime ||
'',
daysText:
(item.leaveDays && `${item.leaveDays}`) ||
(item.duration && `${item.duration}`) ||
'',
statusText:
item.statusText ||
item.statusName ||
item.statusLabel ||
item.status ||
'待审批',
statusType: mapStatusType(
item.statusText || item.statusName || item.statusLabel || item.status || '',
),
...normalized,
canEdit: canEditLeave(normalized.startRaw || normalized.startText),
}
})
recordList.value = [...recordList.value, ...processedData]
}
} catch (error) {
console.error('加载记录列表失败:', error)
// 模拟数据
if (queryParams.value.pageNum === 1) {
recordList.value = [
{
id: 1,
title: '请假记录示例1',
subtitle: '这是请假记录的描述信息',
createTime: '2025-01-15 10:30:00',
},
{
id: 2,
title: '请假记录示例2',
subtitle: '这是请假记录的描述信息',
createTime: '2025-01-14 14:20:00',
},
]
total.value = 2
}
} finally {
loading.value = false
}
}
/**
* 处理滚动到底部
*/
const onHandleScrollToLower = debounce(() => {
if (hasMore.value && !loading.value) {
queryParams.value.pageNum++
loadRecordList()
}
}, 1000)
/**
* 处理记录点击
*/
const handleRecordClick = (item) => {
console.log('点击记录:', item)
// TODO: 跳转到记录详情页面
}
/**
* 处理修改操作
* 业务背景:点击记录的“修改”按钮后进入表单回显并重新计算履职天数
*/
const handleModify = async (item) => {
// 回显表单数据
prefillFormData(item)
// 切换到表单Tab
activeTab.value = 'form'
isEditing.value = true
editingRecord.value = item
// 重新计算履职天数
if (formData.value.startDate && formData.value.endDate) {
await fetchPerformanceDays()
}
}
/**
* 预填表单数据
* 业务背景:修改时需要用列表行的数据回显到表单
* 设计决策:日期统一转为时间戳,避免与日期组件冲突
*/
const prefillFormData = (item) => {
const { startRaw, endRaw } = normalizeStartEnd(item)
formData.value.leaveTypeId = item.leaveTypeId || item.leaveType || ''
formData.value.leaveTypeName = item.leaveTypeName || item.typeText || ''
formData.value.reason = item.reason || item.description || ''
formData.value.startDate = startRaw ? dayjs(startRaw).valueOf() : null
formData.value.endDate = endRaw ? dayjs(endRaw).valueOf() : null
formData.value.duration = item.leaveDays ? String(item.leaveDays) : item.duration || '0'
formData.value.beforeDays = item.lzDaysBeforeLeave
? String(item.lzDaysBeforeLeave)
: item.beforeDays || '0'
formData.value.afterDays = item.lzDaysAfterLeave
? String(item.lzDaysAfterLeave)
: item.afterDays || '0'
formData.value.approverId = item.approverId || ''
formData.value.approverName = item.approverName || ''
formData.value.attachments = item.fileInfo ? JSON.parse(item.fileInfo || '[]') : []
fileList.value = formData.value.attachments.map((file) => ({
...file,
status: 'success',
}))
}
/**
* 重新获取履职天数
* 业务背景:修改日期后需重新计算请假时长及履职天数
* 设计决策:复用后端接口 getPersonPerformanceDaysApi保持字段与老代码一致
*/
const fetchPerformanceDays = async () => {
if (!formData.value.startDate || !formData.value.endDate) return
const startDate = dayjs(formData.value.startDate).format('YYYY-MM-DD')
const endDate = dayjs(formData.value.endDate).format('YYYY-MM-DD')
try {
const res = await getPersonPerformanceDaysApi({
startDate,
endDate,
})
if (res && res.data) {
formData.value.duration = String(res.data.leaveDays || 0)
formData.value.beforeDays = String(res.data.lzDaysBeforeLeave || 0)
formData.value.afterDays = String(res.data.lzDaysAfterLeave || 0)
}
} catch (error) {
console.error('获取履职天数失败:', error)
uni.showToast({
title: '获取请假天数失败',
icon: 'none',
})
}
}
/**
* 处理撤销
* 业务背景:未来的请假可撤销
*/
const handleRevoke = (item) => {
uni.showToast({
title: '撤销功能待接入接口',
icon: 'none',
})
console.log('撤销', item)
}
/**
* 处理结束
* 业务背景:结束前需校验是否存在未结束的请假,避免重复操作
* 设计决策:先调用检查接口,按返回信息二次确认,再调用结束接口,完成后刷新列表
*/
const handleFinish = async (item) => {
try {
const checkRes = await checkHasUnfinishedLeaveApi()
const content = checkRes?.msg || '确认结束当前请假吗?'
uni.showModal({
title: '提示',
content,
cancelText: '再想想',
confirmText: '确认结束',
confirmColor: '#00aa7c',
success: async (res) => {
if (!res.confirm) return
try {
const endRes = await endLeaveRequestApi()
uni.showToast({
title: endRes?.msg || '结束成功',
icon: 'none',
})
// 刷新列表
queryParams.value.pageNum = 1
recordList.value = []
await loadRecordList()
} catch (err) {
console.error('结束失败:', err)
uni.showToast({
title: err?.message || '结束失败,请重试',
icon: 'none',
})
}
},
})
} catch (error) {
console.error('检查未结束请假失败:', error)
uni.showToast({
title: error?.data?.msg || '检查失败,请稍后重试',
icon: 'none',
})
}
}
/**
* 处理审批同意
* 业务背景:审批人同意请假申请
*/
const handleApprove = async (item) => {
if (!item.id) {
uni.showToast({
title: '数据异常,无法审批',
icon: 'none',
})
return
}
uni.showModal({
title: '确认审批',
content: '确认同意该请假申请吗?',
cancelText: '再想想',
confirmText: '确认',
confirmColor: '#07c160',
success: async (res) => {
if (!res.confirm) return
try {
await approveLeaveRequestApi({
id: item.id,
approverComment: '同意',
approverStatus: 1,
})
uni.showToast({
title: '审批成功',
icon: 'success',
})
// 刷新列表
queryParams.value.pageNum = 1
recordList.value = []
await loadRecordList()
} catch (error) {
console.error('审批失败:', error)
uni.showToast({
title: error?.data?.msg || '审批失败,请重试',
icon: 'none',
})
}
},
})
}
/**
* 处理审批拒绝
* 业务背景:审批人拒绝请假申请
*/
const handleReject = async (item) => {
if (!item.id) {
uni.showToast({
title: '数据异常,无法审批',
icon: 'none',
})
return
}
uni.showModal({
title: '确认拒绝',
content: '确认拒绝该请假申请吗?',
cancelText: '再想想',
confirmText: '确认拒绝',
confirmColor: '#ff4d4f',
success: async (res) => {
if (!res.confirm) return
try {
await rejectLeaveRequestApi({
id: item.id,
approverComment: '不同意',
approverStatus: 2,
})
uni.showToast({
title: '已拒绝',
icon: 'success',
})
// 刷新列表
queryParams.value.pageNum = 1
recordList.value = []
await loadRecordList()
} catch (error) {
console.error('拒绝失败:', error)
uni.showToast({
title: error?.msg || '操作失败,请重试',
icon: 'none',
})
}
},
})
}
const handleBack = () => {
uni.navigateBack()
}
onMounted(() => {
// 初始化
})
</script>
<style lang="scss" scoped>
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
.content-wrapper {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 20rpx;
padding-top: 0;
}
.tabs-container {
display: flex;
margin-top: 10rpx;
background: #fff;
border-radius: 16rpx;
padding: 8rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 10rpx 0;
position: relative;
transition: all 0.3s ease;
border-radius: 12rpx;
}
.tab-item.active {
background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
box-shadow: 0 2rpx 8rpx rgba(7, 193, 96, 0.3);
}
.tab-text {
font-size: 30rpx;
color: #666;
font-weight: 500;
text-align: center;
transition: all 0.3s ease;
}
.tab-item.active .tab-text {
color: #fff;
font-weight: 600;
}
// 表单样式
.scroll-container {
// flex: 1;
overflow-y: auto;
// padding: 20rpx;
box-sizing: border-box;
}
.form-picker-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx;
background: #fff;
border-radius: 0;
border: 1rpx solid #dcdfe6;
box-sizing: border-box;
width: 100%;
min-height: 70rpx;
}
.picker-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.picker-text.placeholder {
color: #999;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.submit-section {
padding: 40rpx 0;
}
// 列表样式
.list-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.filter-section {
margin: 10rpx 0;
}
.filter-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
// 选择框样式
::v-deep .filter-item .picker-wrapper {
border: 1rpx solid #dcdfe6;
background: #fff;
border-radius: 0;
box-sizing: border-box;
}
.list-container {
flex: 1;
overflow-y: auto;
}
.record-item {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.record-item:active {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.record-user {
display: flex;
align-items: center;
gap: 10rpx;
}
.record-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
.record-date {
font-size: 26rpx;
color: #999;
}
.record-type {
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.record-body {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.record-range {
display: flex;
align-items: center;
gap: 8rpx;
color: #555;
font-size: 28rpx;
}
.record-range-text {
color: #555;
}
.record-days {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.record-status {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12rpx;
}
.status-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.status-tag {
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
border: 1rpx solid #dcdfe6;
color: #666;
background: #f5f7fa;
}
.status-success {
color: #07c160;
border-color: #07c160;
background: rgba(7, 193, 96, 0.08);
}
.status-success.status-outline {
font-weight: 600;
}
.status-pending {
color: #ffb100;
border-color: #ffb100;
background: rgba(255, 177, 0, 0.12);
}
.status-error {
color: #ff4d4f;
border-color: #ff4d4f;
background: rgba(255, 77, 79, 0.12);
}
.record-actions {
display: flex;
gap: 12rpx;
}
.loading-text {
text-align: center;
padding: 40rpx 0;
font-size: 28rpx;
color: #999;
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.back-btn:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.95);
}
// 表单样式优化
::v-deep .up-form-item {
padding-bottom: 24rpx;
margin-bottom: 24rpx;
border-bottom: 1rpx solid #e0e0e0;
}
::v-deep .up-form-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
::v-deep .up-form-item__body__left__content__label {
font-size: 32rpx;
color: #333;
font-weight: 600;
width: 200rpx !important;
}
::v-deep .up-form-item__body__right {
flex: 1;
}
::v-deep .up-input {
border: 1rpx solid #dcdfe6;
border-radius: 0;
background: #fff;
width: 100%;
min-height: 70rpx;
}
::v-deep .up-input__content {
border: 1rpx solid #dcdfe6;
border-radius: 0;
}
::v-deep .up-input__content__field-wrapper__field {
padding: 16rpx 24rpx;
}
::v-deep .up-textarea {
border: 1rpx solid #dcdfe6;
border-radius: 0;
background: #fff;
width: 100%;
}
::v-deep .up-textarea__field {
border: 1rpx solid #dcdfe6;
border-radius: 0;
padding: 16rpx 24rpx;
}
::v-deep .up-form-item__body__right__message {
margin-left: 0 !important;
}
// 禁用输入框样式
::v-deep .up-input--disabled {
background: #f5f7fa;
color: #666;
}
::v-deep .up-input--disabled .up-input__content {
background: #f5f7fa;
}
</style>