接口调试

This commit is contained in:
BianLzhaoMin 2025-12-08 18:02:40 +08:00
parent aec7da7272
commit 9f6c55c54b
6 changed files with 1059 additions and 170 deletions

View File

@ -10,6 +10,7 @@
ref="pickerRef"
:show="showPicker"
:columns="columns"
:defaultIndex="defaultIndex"
closeOnClickOverlay
@cancel="handleCancel"
@confirm="handleConfirm"
@ -94,7 +95,7 @@ const displayValue = computed(() => {
}
//
if (typeof props.options[0] === 'string') {
if (props.options.length > 0 && typeof props.options[0] === 'string') {
return props.options.find((item) => item === props.modelValue) || ''
}
@ -103,6 +104,29 @@ const displayValue = computed(() => {
return option ? option[props.labelKey] : ''
})
/**
* 计算默认选中索引
* 业务背景当选择器打开时需要根据当前 modelValue 找到对应的选项索引以便选择器默认选中该项
* 设计决策通过遍历 options 数组找到与 modelValue 匹配的选项索引
*/
const defaultIndex = computed(() => {
if (!props.modelValue || props.options.length === 0) {
return [0]
}
//
if (typeof props.options[0] === 'string') {
const index = props.options.findIndex((item) => item === props.modelValue)
return [index >= 0 ? index : 0]
}
//
const index = props.options.findIndex(
(item) => String(item[props.valueKey]) === String(props.modelValue),
)
return [index >= 0 ? index : 0]
})
/**
* 打开选择器
*/
@ -119,12 +143,32 @@ const handleCancel = () => {
/**
* 确认选择
* @param {Object} e - 事件对象
* 业务背景处理用户确认选择的操作获取选中的值并触发更新
* 设计决策
* 1. up-picker confirm 事件返回的 e.value[0] 是一个对象包含 text value 属性
* 2. update:modelValue 传递值符合 v-model 标准
* 3. change 事件传递整个项对象包含 label value方便父组件使用
* @param {Object} e - 事件对象e.value[0] 包含 { text, value }
*/
const handleConfirm = (e) => {
const selectedValue = e.value[0]
// up-picker e.value[0] text value
const selectedItem = e.value[0]
const selectedValue = selectedItem?.value ?? selectedItem
// options 便 label value
let changeItem = null
if (props.options.length > 0) {
if (typeof props.options[0] === 'string') {
changeItem = { label: selectedItem?.text, value: selectedValue }
} else {
changeItem = props.options.find(
(item) => String(item[props.valueKey]) === String(selectedValue),
) || { label: selectedItem?.text, value: selectedValue }
}
}
emit('update:modelValue', selectedValue)
emit('change', selectedValue)
emit('change', changeItem || { label: selectedItem?.text, value: selectedValue })
showPicker.value = false
}
</script>

View File

@ -11,8 +11,8 @@
ref="datePickerRef"
:mode="pickerMode"
:show="showPicker"
:minDate="minDate"
:maxDate="maxDate"
:minDate="computedMinDate"
:maxDate="computedMaxDate"
v-model="pickerValue"
@cancel="handleCancel"
@confirm="handleConfirm"
@ -20,7 +20,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed } from 'vue'
import dayjs from 'dayjs'
/**
@ -102,6 +102,28 @@ const pickerMode = computed(() => {
return 'date'
})
/**
* 计算最小日期
* 业务背景 minDate 0 会被解析为 1970-01-01导致选择器只能选择 1970
* 设计决策如果 minDate 0则不传递该属性让选择器使用默认的最小日期范围
*/
const computedMinDate = computed(() => {
// minDate 0 undefined
// 使
return props.minDate === 0 ? undefined : props.minDate
})
/**
* 计算最大日期
* 业务背景 maxDate 0 会被解析为 1970-01-01导致选择器只能选择 1970
* 设计决策如果 maxDate 0则不传递该属性让选择器使用默认的最大日期范围
*/
const computedMaxDate = computed(() => {
// maxDate 0 undefined
// 使
return props.maxDate === 0 ? undefined : props.maxDate
})
//
const displayValue = computed(() => {
if (!props.modelValue) {
@ -123,6 +145,7 @@ const displayValue = computed(() => {
const pickerValue = computed({
get: () => {
if (props.modelValue) {
console.log(props.modelValue, 'props.modelValue')
return typeof props.modelValue === 'number'
? props.modelValue
: new Date(props.modelValue).getTime()

View File

@ -38,10 +38,13 @@
<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="leaveType" required :borderBottom="true">
<up-form-item label="请假类型" prop="leaveTypeId" required :borderBottom="true">
<view class="form-picker-wrapper" @tap="handleOpenLeaveTypePicker">
<text class="picker-text" :class="{ placeholder: !formData.leaveType }">
{{ leaveTypeText || '请选择请假类型' }}
<text
class="picker-text"
:class="{ placeholder: !formData.leaveTypeId }"
>
{{ formData.leaveTypeName || '请选择请假类型' }}
</text>
<up-icon name="arrow-right" size="16" color="#999" />
</view>
@ -50,8 +53,8 @@
<!-- 请假日期 -->
<up-form-item label="请假日期" prop="leaveDate" required :borderBottom="true">
<view class="form-picker-wrapper" @tap="handleOpenDatePicker">
<text class="picker-text" :class="{ placeholder: !formData.leaveDate }">
{{ leaveDateText || '请选择请假日期' }}
<text class="picker-text" :class="{ placeholder: !leaveDateText }">
{{ leaveDateText || '请选择请假日期范围' }}
</text>
<up-icon name="arrow-right" size="16" color="#999" />
</view>
@ -94,9 +97,12 @@
</up-form-item>
<!-- 审批人 -->
<up-form-item label="审批人" prop="approver" :borderBottom="true">
<up-form-item label="审批人" prop="approverId" :borderBottom="true">
<view class="form-picker-wrapper" @tap="handleOpenApproverPicker">
<text class="picker-text" :class="{ placeholder: !formData.approver }">
<text
class="picker-text"
:class="{ placeholder: !formData.approverId }"
>
{{ approverText || '请选择审批人' }}
</text>
<up-icon name="arrow-right" size="16" color="#999" />
@ -106,6 +112,7 @@
<!-- 上传附件 -->
<up-form-item label="上传附件" prop="attachments" :borderBottom="true">
<up-upload
accept="file"
:fileList="fileList"
@afterRead="handleAfterRead"
@delete="handleDelete"
@ -119,7 +126,7 @@
<!-- 提交按钮 -->
<view class="submit-section">
<up-button
text="提交请假"
:text="submitButtonText"
type="primary"
:loading="submitting"
@tap="handleSubmit"
@ -164,10 +171,72 @@
class="record-item"
@tap="handleRecordClick(item)"
>
<view class="record-content">
<text class="record-title">{{ item.title || '请假记录' }}</text>
<text class="record-subtitle">{{ item.subtitle || '暂无描述' }}</text>
<text class="record-time">{{ item.createTime || '2025-01-01' }}</text>
<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.leaveCnt">
{{ 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">
<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)"
/>
</view>
</view>
</view>
@ -193,13 +262,23 @@
@close="showLeaveTypePicker = false"
/>
<!-- 日期选择器 -->
<!-- 开始日期选择器 -->
<up-datetime-picker
:show="showDatePicker"
:show="showStartDatePicker"
mode="date"
v-model="datePickerValue"
@cancel="showDatePicker = false"
@confirm="handleDateConfirm"
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"
/>
<!-- 审批人选择器 -->
@ -229,6 +308,10 @@ import {
submitLeaveRequestApi,
getMyLeaveRecordListApi,
getLeaveApprovalListApi,
getLeaveDetailApi,
getPersonPerformanceDaysApi,
checkHasUnfinishedLeaveApi,
endLeaveRequestApi,
} from '@/services/leader/leave-request'
/**
@ -250,24 +333,30 @@ const contentStyle = computed(() => {
// Tab
const activeTab = ref('form') // 'form' | 'record' | 'approval'
const isEditing = ref(false)
const editingRecord = ref(null)
//
const formRef = ref(null)
const formData = ref({
leaveType: '',
leaveDate: null,
leaveTypeId: '',
leaveTypeName: '',
startDate: null,
endDate: null,
reason: '',
duration: '0',
beforeDays: '0',
afterDays: '0',
approver: '',
approverId: '',
approverName: '',
attachments: [],
})
//
const rules = ref({
leaveType: [
leaveTypeId: [
{
type: 'number',
required: true,
message: '请选择请假类型',
trigger: 'change',
@ -275,8 +364,13 @@ const rules = ref({
],
leaveDate: [
{
required: true,
message: '请选择请假日期',
validator: (rule, value, callback) => {
if (!formData.value.startDate || !formData.value.endDate) {
callback(new Error('请选择请假日期范围'))
} else {
callback()
}
},
trigger: 'change',
},
],
@ -294,25 +388,37 @@ const showLeaveTypePicker = ref(false)
const leaveTypeColumns = computed(() => {
return [
leaveTypeList.value.map((item) => ({
text: item.dictLabel || item.label,
value: item.dictValue || item.value,
text: item.dictLabel,
value: item.dictCode || item.dictValue,
})),
]
})
const leaveTypeText = computed(() => {
if (!formData.value.leaveType) return ''
const item = leaveTypeList.value.find(
(item) => (item.dictValue || item.value) === formData.value.leaveType,
)
return item ? item.dictLabel || item.label : ''
})
//
const showDatePicker = ref(false)
const datePickerValue = ref(Date.now())
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.leaveDate) return ''
return dayjs(formData.value.leaveDate).format('YYYY-MM-DD')
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}`
})
//
@ -327,11 +433,8 @@ const approverColumns = computed(() => {
]
})
const approverText = computed(() => {
if (!formData.value.approver) return ''
const item = approverList.value.find(
(item) => (item.userId || item.id) === formData.value.approver,
)
return item ? item.nickName || item.name : ''
if (!formData.value.approverId) return ''
return formData.value.approverName || ''
})
//
@ -342,11 +445,33 @@ const loading = ref(false)
//
const queryParams = ref({
pageNum: 1,
pageSize: 20,
year: new Date().getFullYear().toString(),
month: '',
})
/**
* 计算请假记录查询的时间范围
* 业务背景后端列表接口要求按起止日期筛选不接受 pageSize
* 设计决策保留年/月选择体验将其转换为 startTimeendTime
*/
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()
@ -377,6 +502,14 @@ const monthOptions = ref([
{ label: '12月', value: '12' },
])
/**
* 计算提交按钮文案
* 业务背景编辑模式需要提示保存修改新增则为提交请假
*/
const submitButtonText = computed(() => {
return isEditing.value ? '保存修改' : '提交请假'
})
/**
* 计算是否还有更多数据
*/
@ -411,28 +544,67 @@ const handleOpenLeaveTypePicker = () => {
/**
* 处理请假类型确认
* 业务背景保存请假类型ID和名称
*/
const handleLeaveTypeConfirm = (e) => {
formData.value.leaveType = e.value[0]
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.leaveDate) {
datePickerValue.value = formData.value.leaveDate
if (formData.value.startDate) {
startDatePickerValue.value = formData.value.startDate
} else {
startDatePickerValue.value = Date.now()
}
showDatePicker.value = true
showStartDatePicker.value = true
}
/**
* 处理日期确认
* 处理开始日期确认
* 业务背景选择开始日期后自动打开结束日期选择器
*/
const handleDateConfirm = (e) => {
formData.value.leaveDate = e.value
showDatePicker.value = false
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()
}
/**
@ -447,9 +619,15 @@ const handleOpenApproverPicker = () => {
/**
* 处理审批人确认
* 业务背景保存审批人ID和名称
*/
const handleApproverConfirm = (e) => {
formData.value.approver = e.value[0]
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
}
@ -460,18 +638,12 @@ const handleApproverConfirm = (e) => {
* @returns {Promise<String>} 返回文件URL
*/
const uploadFilePromise = (filePath) => {
console.log(filePath, 'filePath')
return new Promise((resolve, reject) => {
const memberStore = useMemberStore()
const baseURL = import.meta.env.VITE_API_BASE_URL
const uploadUrl = baseURL + '/common/upload'
uni.uploadFile({
url: uploadUrl,
url: '/common/upload',
filePath: filePath,
name: 'file',
header: {
Authorization: memberStore.token || '',
},
formData: {
user: 'test',
},
@ -501,17 +673,31 @@ const uploadFilePromise = (filePath) => {
/**
* 处理文件上传
* 业务背景上传文件到服务器保存完整的文件信息对象
* 设计决策保存文件信息对象包含 url, size, name, type, status, message 等字段
*/
const handleAfterRead = async (event) => {
const { file } = event
console.log(file, 'file')
try {
const fileName = await uploadFilePromise(file.url)
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(fileName)
formData.value.attachments.push(fileInfo)
} catch (error) {
console.error('文件上传失败:', error)
uni.showToast({
@ -532,16 +718,36 @@ const handleDelete = (event) => {
/**
* 处理提交
* 业务背景按照后台接口要求的参数格式组装提交数据
* 设计决策
* 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 = {
...formData.value,
leaveDate: dayjs(formData.value.leaveDate).format('YYYY-MM-DD'),
attachments: JSON.stringify(formData.value.attachments),
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)
@ -551,13 +757,16 @@ const handleSubmit = async () => {
})
//
formData.value = {
leaveType: '',
leaveDate: null,
leaveTypeId: '',
leaveTypeName: '',
startDate: null,
endDate: null,
reason: '',
duration: '0',
beforeDays: '0',
afterDays: '0',
approver: '',
approverId: '',
approverName: '',
attachments: [],
}
fileList.value = []
@ -591,16 +800,58 @@ const loadLeaveTypeList = async () => {
*/
const loadApproverList = async () => {
try {
const res = await getApproverListApi({
pageNum: 1,
pageSize: 99999,
})
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',
})
}
}
/**
* 处理年份变更
*/
@ -621,24 +872,99 @@ const handleMonthChange = (item) => {
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 api = activeTab.value === 'record' ? getMyLeaveRecordListApi : getLeaveApprovalListApi
const res = await api(queryParams.value)
const res = await api({
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) => ({
...item,
title: item.title || item.leaveTypeName || item.name || '请假记录',
subtitle: item.subtitle || item.reason || item.description || '暂无描述',
createTime: item.createTime || item.createTimeStr || item.date || '',
}))
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) {
@ -684,6 +1010,139 @@ const handleRecordClick = (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?.message || '检查失败,请稍后重试',
icon: 'none',
})
}
}
const handleBack = () => {
uni.navigateBack()
}
@ -835,10 +1294,17 @@ onMounted(() => {
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
.record-content {
.record-header {
display: flex;
flex-direction: column;
gap: 8rpx;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.record-user {
display: flex;
align-items: center;
gap: 10rpx;
}
.record-title {
@ -847,16 +1313,89 @@ onMounted(() => {
font-weight: 600;
}
.record-subtitle {
font-size: 28rpx;
color: #666;
line-height: 1.5;
.record-date {
font-size: 26rpx;
color: #999;
}
.record-time {
.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;
color: #999;
margin-top: 4rpx;
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 {

View File

@ -58,10 +58,10 @@
<view class="filter-row-second">
<view class="filter-item">
<CommonPicker
:options="rectifyOptions"
:modelValue="queryParams.isRectified"
placeholder="是否整改"
@change="handleRectifyChange"
:options="zgTypeOptions"
:modelValue="queryParams.zgType"
placeholder="整改状态"
@change="handleZgTypeChange"
/>
</view>
<view class="search-item">
@ -93,10 +93,82 @@
class="record-item"
@tap="handleRecordClick(item)"
>
<!-- 序号标识 -->
<view class="record-badge">{{ index + 1 }}</view>
<view class="record-content">
<text class="record-title">{{ item.title || '问题记录' }}</text>
<text class="record-subtitle">{{ item.subtitle || '暂无描述' }}</text>
<text class="record-time">{{ item.createTime || '2025-01-01' }}</text>
<!-- 主标题 -->
<view class="record-header">
<text class="record-title">{{ getRecordTitle(item) }}</text>
<text class="record-type-tag">
{{ item.type == 0 ? '现场履职' : '班组履职' }}
</text>
</view>
<!-- 日期时间 -->
<text class="record-time">
{{ formatDateTime(item.createTime || item.createTimeStr) }}
</text>
<!-- 手册信息 -->
<view v-if="item.handbookName" class="record-info-row">
<up-icon name="file-text" size="16" color="#07c160" />
<text class="record-info-text">{{ item.handbookName }}</text>
<view>
<up-button
text="复制"
type="success"
size="mini"
shape="circle"
@tap.stop="handleCopyManual(item)"
/>
</view>
</view>
<!-- 问题信息列表 -->
<template v-if="item.problemList && item.problemList.length > 0">
<view
v-for="(problem, pIndex) in item.problemList"
:key="problem.id || pIndex"
class="record-info-row"
>
<up-icon name="edit-pen" size="16" color="#2979ff" />
<text class="record-problem-text">{{ problem.remark }}</text>
</view>
</template>
<!-- 负责人信息列表 -->
<template v-if="item.problemList && item.problemList.length > 0">
<view
v-for="(problem, pIndex) in item.problemList"
:key="`responsible-${problem.id || pIndex}`"
class="record-info-row"
>
<up-icon name="account" size="16" color="#ff9800" />
<text class="record-info-text">{{
problem.responsiblePerson
}}</text>
</view>
</template>
<!-- 同行人信息 -->
<view class="record-info-row">
<up-icon name="account-fill" size="16" color="#07c160" />
<text class="record-info-text">{{ item.otherUserName }}</text>
</view>
<!-- 整改状态标签 -->
<view class="record-info-row" v-if="item.zgType">
<text
class="rectify-status-tag"
:class="{
'rectify-status-tag--done': item.zgType == 1,
'rectify-status-tag--pending': item.zgType == 2,
}"
>
{{ item.zgType == 1 ? '已整改' : '未整改' }}
</text>
</view>
</view>
</view>
@ -156,11 +228,11 @@ const loading = ref(false)
//
const queryParams = ref({
pageNum: 1,
pageSize: 20,
pageSize: 10,
year: new Date().getFullYear().toString(),
month: '',
type: '',
isRectified: '',
zgType: '',
keyword: '',
})
@ -201,11 +273,11 @@ const typeOptions = ref([
{ label: '班组履职', value: '1' },
])
//
const rectifyOptions = ref([
//
const zgTypeOptions = ref([
{ label: '全部', value: '' },
{ label: '已整改', value: '1' },
{ label: '未整改', value: '0' },
{ label: '未整改', value: '2' },
])
/**
@ -263,11 +335,11 @@ const handleTypeChange = (item) => {
}
/**
* 处理是否整改变更
* 处理整改状态变更
* @param {Object} item - 选中的整改状态项
*/
const handleRectifyChange = (item) => {
queryParams.value.isRectified = item.value
const handleZgTypeChange = (item) => {
queryParams.value.zgType = item.value
queryParams.value.pageNum = 1
recordList.value = []
loadRecordList()
@ -283,6 +355,43 @@ const handleSearch = debounce(() => {
loadRecordList()
}, 500)
/**
* 构建查询参数
* 业务背景根据年份和月份生成 startTime endTime按照新的参数格式组装
* 设计决策如果选择了月份则精确到该月的第一天和最后一天如果只选择年份则使用整年范围
*/
const buildQueryParams = () => {
const { year, month, type, zgType, keyword, pageNum, pageSize } = queryParams.value
let startTime = ''
let endTime = ''
if (year) {
if (month) {
//
const yearNum = parseInt(year)
const monthNum = parseInt(month)
const lastDay = new Date(yearNum, monthNum, 0).getDate()
startTime = `${year}-${String(monthNum).padStart(2, '0')}-01`
endTime = `${year}-${String(monthNum).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`
} else {
// 使
startTime = `${year}-01-01`
endTime = `${year}-12-31`
}
}
return {
pageNum,
pageSize,
startTime,
endTime,
handbookType: type || '',
zgType: zgType || '',
keyword: keyword || '',
}
}
/**
* 加载记录列表
* 业务背景根据当前tab调用不同的接口获取记录列表
@ -293,38 +402,18 @@ const loadRecordList = async () => {
const api =
activeTab.value === 'my' ? getMyProblemRecordListApi : getPeerProblemRecordListApi
const res = await api(queryParams.value)
const params = buildQueryParams()
const res = await api(params)
total.value = res?.total || 0
if (res?.rows && res.rows.length > 0) {
//
const processedData = res.rows.map((item) => ({
...item,
title: item.title || item.problemTitle || item.name || '问题记录',
subtitle: item.subtitle || item.problemDesc || item.description || '暂无描述',
createTime: item.createTime || item.createTimeStr || item.date || '',
}))
recordList.value = [...recordList.value, ...processedData]
recordList.value = [...recordList.value, ...res.rows]
}
} 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
}
uni.showToast({
title: '加载失败,请重试',
icon: 'none',
})
} finally {
loading.value = false
}
@ -340,14 +429,88 @@ const onHandleScrollToLower = debounce(() => {
}
}, 1000)
/**
* 获取记录标题
* 业务背景根据项目名称和标题组合显示记录标题
* @param {Object} item - 记录项数据
* @returns {String} 记录标题
*/
const getRecordTitle = (item) => {
if (item.projectName && item.dutyTypeName) {
return `${item.projectName} ${item.dutyTypeName}`
}
return item.projectName || item.title || item.name || '问题记录'
}
/**
* 格式化日期时间
* 业务背景将日期时间格式化为 YYYY-MM-DD HH:mm 格式
* @param {String} dateTime - 日期时间字符串
* @returns {String} 格式化后的日期时间
*/
const formatDateTime = (dateTime) => {
if (!dateTime) return ''
//
const date = new Date(dateTime)
if (isNaN(date.getTime())) {
//
return dateTime
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
/**
* 处理复制手册
* 业务背景复制手册名称到剪贴板
* @param {Object} item - 记录项数据
*/
const handleCopyManual = (item) => {
if (!item.handbookName) return
uni.setClipboardData({
data: item.handbookName,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'success',
})
},
fail: () => {
uni.showToast({
title: '复制失败',
icon: 'none',
})
},
})
}
/**
* 处理记录点击
* @param {Object} item - 记录项数据
* 业务背景点击记录项后查看详情后续实现
* 业务背景点击记录项后查看详情
*/
const handleRecordClick = (item) => {
console.log('点击记录:', item)
// TODO:
if (!item.id) {
uni.showToast({
title: '记录ID不存在',
icon: 'none',
})
return
}
uni.navigateTo({
url: `/pages/leader/problem-record-details/index?recordId=${item.id}`,
fail: (err) => {
console.error('导航失败:', err)
uni.showToast({
title: '页面跳转失败',
icon: 'none',
})
},
})
}
const handleBack = () => {
@ -431,12 +594,8 @@ onMounted(() => {
gap: 16rpx;
}
.filter-item {
// display: flex;
}
.search-item {
display: flex;
.search-row {
width: 100%;
}
//
@ -464,10 +623,12 @@ onMounted(() => {
.record-item {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
padding: 32rpx 24rpx 24rpx;
padding-right: 60rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
position: relative;
}
.record-item:active {
@ -475,28 +636,99 @@ onMounted(() => {
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
.record-badge {
position: absolute;
top: 16rpx;
right: 16rpx;
width: 40rpx;
height: 40rpx;
background: #ff4444;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 600;
z-index: 1;
}
.record-content {
display: flex;
flex-direction: column;
gap: 8rpx;
gap: 12rpx;
}
.record-header {
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
padding-right: 0;
}
.record-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
flex: 1;
min-width: 0;
word-break: break-all;
}
.record-subtitle {
font-size: 28rpx;
color: #666;
line-height: 1.5;
.record-type-tag {
font-size: 24rpx;
color: #ff9800;
background: rgba(255, 152, 0, 0.1);
padding: 4rpx 12rpx;
border-radius: 4rpx;
white-space: nowrap;
}
.record-time {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
.record-info-row {
display: flex;
align-items: center;
gap: 8rpx;
flex-wrap: wrap;
}
.record-info-text {
font-size: 26rpx;
color: #666;
flex: 1;
min-width: 0;
word-break: break-all;
}
.record-problem-text {
font-size: 26rpx;
color: #ff4444;
flex: 1;
min-width: 0;
word-break: break-all;
}
.rectify-status-tag {
font-size: 24rpx;
padding: 6rpx 16rpx;
border-radius: 4rpx;
font-weight: 500;
white-space: nowrap;
}
.rectify-status-tag--done {
color: #07c160;
background: rgba(7, 193, 96, 0.1);
}
.rectify-status-tag--pending {
color: #ff4444;
background: rgba(255, 68, 68, 0.1);
}
.loading-text {

View File

@ -13,13 +13,8 @@ import { http } from '@/utils/http'
*/
export const getLeaveTypeListApi = () => {
return http({
url: '/system/dict/data/list',
url: '/perform_set/leave_type/applist',
method: 'GET',
data: {
pageNum: 1,
pageSize: 99999,
dictType: 'leave_type',
},
})
}
@ -29,11 +24,10 @@ export const getLeaveTypeListApi = () => {
* @param {Object} data - 查询参数 { pageNum, pageSize, keyword }
* @returns {Promise} 返回审批人列表数据
*/
export const getApproverListApi = (data) => {
export const getApproverListApi = () => {
return http({
url: '/system/user_leader/peerlist',
url: '/system/user/approver_list',
method: 'GET',
data,
})
}
@ -45,12 +39,54 @@ export const getApproverListApi = (data) => {
*/
export const submitLeaveRequestApi = (data) => {
return http({
url: '/leave-request/submit',
url: '/report/leave_log/list',
method: 'POST',
data,
})
}
// 修改请假申请
export const updateLeaveRequestApi = (data) => {
return http({
url: '/leave_log/edit_leave',
method: 'POST',
data,
})
}
// 撤销请假
export const cancelLeaveRequestApi = (id) => {
return http({
url: '/leave_log/cancel_leave/' + id,
method: 'PUT',
})
}
// 结束请假
export const endLeaveRequestApi = () => {
return http({
url: '/leave_log/end_leave',
method: 'PUT',
})
}
// 获取人员履职天数
export const getPersonPerformanceDaysApi = (data) => {
return http({
url: '/perform_set/user/detail',
method: 'GET',
data,
})
}
// 查看是否有未结束的请假
export const checkHasUnfinishedLeaveApi = () => {
return http({
url: '/leave_log/end_leave_info',
method: 'GET',
})
}
/**
* 获取我的请假记录列表
* 业务背景获取当前用户的请假记录列表
@ -59,7 +95,7 @@ export const submitLeaveRequestApi = (data) => {
*/
export const getMyLeaveRecordListApi = (data) => {
return http({
url: '/leave-request/my-list',
url: '/report/leave_log/list',
method: 'GET',
data,
})
@ -78,3 +114,18 @@ export const getLeaveApprovalListApi = (data) => {
data,
})
}
/**
* 获取请假详情数据
* 业务背景根据开始日期和结束日期获取请假相关的详情数据包括请假时长履职天数等
* 设计决策使用 GET 请求获取详情数据日期参数通过查询参数传递
* @param {Object} data - 查询参数 { startDate, endDate }日期格式为 YYYY-MM-DD
* @returns {Promise} 返回请假详情数据
*/
export const getLeaveDetailApi = (data) => {
return http({
url: '/perform_set/user/detail',
method: 'GET',
data,
})
}

View File

@ -112,8 +112,8 @@ export const getMyPerformanceRecordListApi = (data) => {
*/
export const getPeerPerformanceRecordListApi = (data) => {
return http({
url: '/performance-record/peer-list',
method: 'GET',
url: '/app/recordInfo/otherlist',
method: 'POST',
data,
})
}
@ -127,8 +127,8 @@ export const getPeerPerformanceRecordListApi = (data) => {
*/
export const getMyProblemRecordListApi = (data) => {
return http({
url: '/problem-record/my-list',
method: 'GET',
url: '/app/recordInfo/userProblemList',
method: 'POST',
data,
})
}
@ -142,8 +142,8 @@ export const getMyProblemRecordListApi = (data) => {
*/
export const getPeerProblemRecordListApi = (data) => {
return http({
url: '/problem-record/peer-list',
method: 'GET',
url: '/app/recordInfo/otherProblemList',
method: 'POST',
data,
})
}