1.
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled

This commit is contained in:
2025-09-05 16:54:53 +08:00
parent 8e23e63b3a
commit c7ff9a5234
22 changed files with 2849 additions and 591 deletions

View File

@@ -27,8 +27,8 @@ const config = {
// config.baseUrl = 'http://tc.cqsznc.com:7080/api';
//正式
config.baseUrl = 'http://183.230.235.66:11010/api';
// config.baseUrl = 'http://183.230.235.66:11010/api';
// config.baseUrl = 'http://378a061a.r28.cpolar.top'
config.baseUrl = 'http://799004da.r28.cpolar.top';
export default config;

View File

@@ -64,7 +64,17 @@ const install = (Vue, vm) => {
getImageUrl:(params = {}, ossIds) => vm.$u.get(config.adminPath+`/resource/oss/listByIds/${ossIds}`,params),
//巡检任务列表
getInspection:(params = {})=>vm.$u.get(config.adminPath+'/property/item/list',params),
getInspection:(params = {})=>vm.$u.get(config.adminPath+'/property/mobile/inspectionTask/list',params),
// getTaskList:(params = {},taskId)=>vm.$u.get(config.adminPath+`/property/mobile/taskDetail/list/${taskId}`,params),
//巡检任务
getTaskList:(params = {})=>vm.$u.get(config.adminPath+'/property/mobile/taskDetail/list',params),
//巡检签到
taskSignIn:(params = {})=>vm.$u.post(config.adminPath+'/property/mobile/taskDetail/signIn',params),
//巡检提交
taskSubmit:(params = {})=>vm.$u.post(config.adminPath+'/property/mobile/taskDetail/submit',params),
//巡检工单提报
taskOrderSubmit:(params = {})=>vm.$u.post(config.adminPath+'/property/mobile/taskDetail/reportedOrder',params),
// 基础服务:登录登出、身份信息、菜单授权、切换系统、字典数据等
lang: (params = {}) => vm.$u.get('/lang/'+params.lang),

View File

@@ -0,0 +1,300 @@
<template>
<view class="calendar-container">
<!-- 展开/收起按钮可以外部隐藏 -->
<view class="calendar-header">
<button size="mini" @click="toggleMode">
{{ mode === 'month' ? '收起为周' : '展开为月' }}
</button>
<text>{{ displayTitle }}</text>
</view>
<view
class="calendar-content"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
:style="{ height: mode === 'month' ? '450rpx' : '120rpx', transition: 'height 0.3s' }"
>
<!-- 星期栏 -->
<view class="calendar-week">
<text v-for="(w, i) in weeks" :key="i">{{ w }}</text>
</view>
<!-- 日期 -->
<view class="calendar-days">
<view
v-for="(item, i) in renderDays"
:key="i"
:class="{
'calendar-day': true,
'today': item.isToday,
'selected': isSelected(item),
'other-month': item.otherMonth
}"
@click="selectDate(item)"
>
{{ item.day || '' }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'CustomCalendar',
props: {
initialMode: { type: String, default: 'month' }, // 'month' or 'week'
initialDate: { type: String, default: '' },
allowWeekSwitch: { type: Boolean, default: true } // 控制是否允许上下滑切换
},
data() {
const today = new Date()
return {
mode: this.initialMode,
selected: this.initialDate || this.formatDate(today),
weeks: ['日', '一', '二', '三', '四', '五', '六'],
renderDays: [],
touchStartX: 0,
touchStartY: 0,
touchMoveX: 0,
touchMoveY: 0,
anchorDate: new Date() // 当前视图基准日期
}
},
computed: {
displayTitle() {
return `${this.anchorDate.getFullYear()}${this.anchorDate.getMonth() + 1}`
}
},
watch: {
mode() {
this.generateRenderDays()
},
selected() {
this.generateRenderDays()
}
},
mounted() {
this.anchorDate = this.safeDate(this.selected)
this.generateRenderDays()
},
methods: {
// =================== 日期工具 ===================
formatDate(date) {
const y = date.getFullYear()
const m = date.getMonth() + 1
const d = date.getDate()
return `${y}-${m < 10 ? '0' + m : m}-${d < 10 ? '0' + d : d}`
},
safeDate(date) {
return new Date(date)
},
startOfMonth(date) {
const d = this.safeDate(date)
d.setDate(1)
return d
},
startOfWeek(date) {
const d = this.safeDate(date)
const day = d.getDay()
d.setDate(d.getDate() - day)
return d
},
pad(num) {
return num < 10 ? '0' + num : '' + num
},
// =================== 渲染日历 ===================
generateRenderDays() {
const days = []
if (this.mode === 'month') {
const year = this.anchorDate.getFullYear()
const month = this.anchorDate.getMonth() + 1
const firstDayWeek = new Date(year, month - 1, 1).getDay()
const monthDays = new Date(year, month, 0).getDate()
// 上月补位
const lastMonth = month === 1 ? 12 : month - 1
const lastMonthYear = month === 1 ? year - 1 : year
const lastMonthDays = new Date(lastMonthYear, lastMonth, 0).getDate()
for (let i = 0; i < firstDayWeek; i++) {
days.push({
day: lastMonthDays - firstDayWeek + 1 + i,
date: this.formatDate(new Date(lastMonthYear, lastMonth - 1, lastMonthDays - firstDayWeek + 1 + i)),
otherMonth: true,
isToday: false
})
}
// 本月
const todayStr = this.formatDate(new Date())
for (let i = 1; i <= monthDays; i++) {
const dateStr = `${year}-${this.pad(month)}-${this.pad(i)}`
days.push({
day: i,
date: dateStr,
otherMonth: false,
isToday: dateStr === todayStr
})
}
// 下月补位
while (days.length % 7 !== 0) {
const nextDay = days.length - (firstDayWeek + monthDays) + 1
days.push({
day: nextDay,
date: this.formatDate(new Date(month === 12 ? year + 1 : year, month % 12, nextDay)),
otherMonth: true,
isToday: false
})
}
} else {
// 周视图
const base = this.safeDate(this.selected)
const weekDay = base.getDay()
for (let i = 0; i < 7; i++) {
const d = new Date(base)
d.setDate(base.getDate() - weekDay + i)
days.push({
day: d.getDate(),
date: this.formatDate(d),
otherMonth: d.getMonth() !== base.getMonth(),
isToday: this.formatDate(d) === this.formatDate(new Date())
})
}
}
this.renderDays = days
},
// =================== 事件 ===================
toggleMode() {
if (!this.allowWeekSwitch) return
this.mode = this.mode === 'month' ? 'week' : 'month'
this.anchorDate = this.mode === 'month' ? this.startOfMonth(this.safeDate(this.selected)) : this.startOfWeek(this.safeDate(this.selected))
this.generateRenderDays()
},
selectDate(item) {
if (!item.day) return
this.selected = item.date
this.$emit('dateChange', item.date)
},
isSelected(item) {
return item.date === this.selected && !item.otherMonth
},
// =================== 滑动 ===================
onTouchStart(e) {
const t = e.changedTouches[0]
this.touchStartX = t.clientX
this.touchStartY = t.clientY
},
onTouchMove(e) {
const t = e.changedTouches[0]
this.touchMoveX = t.clientX
this.touchMoveY = t.clientY
},
onTouchEnd(e) {
const t = e.changedTouches[0]
const endX = t.clientX
const endY = t.clientY
const deltaX = endX - this.touchStartX
const deltaY = endY - this.touchStartY
const threshold = 40
// 垂直滑动切换周/月
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > threshold) {
if (!this.allowWeekSwitch) return
if (deltaY < 0 && this.mode !== 'week') {
this.mode = 'week'
this.anchorDate = this.startOfWeek(this.safeDate(this.selected))
this.generateRenderDays()
} else if (deltaY > 0 && this.mode !== 'month') {
this.mode = 'month'
this.anchorDate = this.startOfMonth(this.safeDate(this.selected))
this.generateRenderDays()
}
return
}
// 水平滑动翻页
if (Math.abs(deltaX) > threshold && Math.abs(deltaX) >= Math.abs(deltaY)) {
if (deltaX < 0) this.slide(1)
else this.slide(-1)
}
},
slide(direction) {
if (this.mode === 'month') {
const y = this.anchorDate.getFullYear()
let m = this.anchorDate.getMonth() + 1 + direction
let year = y
if (m > 12) { m = 1; year++ }
if (m < 1) { m = 12; year-- }
this.anchorDate = new Date(year, m - 1, 1)
} else {
const d = this.safeDate(this.selected)
d.setDate(d.getDate() + direction * 7)
this.selected = this.formatDate(d)
this.anchorDate = this.startOfWeek(d)
}
this.generateRenderDays()
}
}
}
</script>
<style scoped>
.calendar-container {
width: 100%;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 16rpx #eee;
padding: 12rpx;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
}
.calendar-content {
overflow: hidden;
}
.calendar-week {
display: grid;
grid-template-columns: repeat(7, 1fr);
color: #999;
font-size: 24rpx;
padding: 8rpx 0;
text-align: center;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 80rpx; /* 每个格子固定高度 */
}
.calendar-day {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx; /* 固定宽度 */
height: 60rpx; /* 固定高度,和宽度一致 */
margin: auto; /* 居中对齐,避免被挤压 */
font-size: 30rpx;
border-radius: 50%;
transition: background 0.2s;
}
.today {
color: #007aff;
font-weight: bold;
border: 1rpx solid #007aff;
}
.selected {
background: #007aff;
color: #fff;
}
.other-month {
color: #ccc;
}
</style>

View File

@@ -373,7 +373,7 @@
{
"path": "pages/sys/user/myRecord/myRecord",
"style": {
"navigationStyle": "custom"
"navigationBarTitleText": "我的考勤"
}
},
{
@@ -479,6 +479,34 @@
{
"navigationBarTitleText" : "巡检任务"
}
},
{
"path" : "pages/sys/workbench/inspection/inspectionOpt",
"style" :
{
"navigationBarTitleText" : "巡检详情"
}
},
{
"path" : "pages/sys/workbench/inspection/inspectionDetail",
"style" :
{
"navigationBarTitleText" : "巡检详情"
}
},
{
"path" : "pages/sys/workbench/book/book",
"style" :
{
"navigationBarTitleText" : " 通讯录"
}
},
{
"path" : "pages/sys/workbench/leave/leave",
"style" :
{
"navigationBarTitleText" : "请假"
}
}
],
"tabBar": {

View File

@@ -1,10 +1,5 @@
<template>
<view class="my-record-container">
<!-- 顶部导航栏 -->
<view class="header">
<image class="back-btn" src="/static/ic_back.png" @click="goBack" />
<text class="page-title">我的考勤</text>
</view>
<!-- 月份标题和切换 -->
<view class="month-header">
@@ -66,8 +61,11 @@
<view v-if="date.hasRecord" class="record-dot"></view>
</view>
</view>
<!-- 展开/收缩按钮 -->
<view class="calendar-toggle" @click="toggleCalendar">
<text class="toggle-text">{{ calendarExpanded ? '收起' : '展开' }}</text>
<image v-if="calendarExpanded" class='image_zk' src="/static/ic_exp.png"></image>
<image v-else class='image_sq' src="/static/ic_sq.png"></image>
</view>
</view>
@@ -100,77 +98,43 @@
</template>
<script>
/**
* 我的考勤页面
* @author lyc
* @description 显示用户考勤记录,包含可展开收缩的日历
*/
export default {
data() {
return {
// 星期标题
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
// 日历是否展开
calendarExpanded: false,
// 当前选中的日期
calendarExpanded: true,
selectedDate: 8,
// 当前年份
currentYear: 2025,
// 当前月份
currentMonth: 7,
// 完整月份日期数据
allDates: []
allDates: [],
prevMonthDates: [],
nextMonthDates: [],
swiperCurrent: 1,
touchStartX: 0,
touchStartY: 0
};
},
created() {
this.generateCalendarDates();
},
computed: {
// 当前周的日期(收缩状态显示)
currentWeekDates() {
// 找到选中日期所在的周
const selectedIndex = this.allDates.findIndex(date => date.selected);
if (selectedIndex === -1) return this.allDates.slice(0, 7);
const startOfWeek = Math.floor(selectedIndex / 7) * 7;
return this.allDates.slice(startOfWeek, startOfWeek + 7);
}
},
created() {
// 初始化日历数据
this.generateCalendarDates();
},
methods: {
/**
* 返回上一页
*/
goBack() {
uni.navigateBack();
},
/**
* 切换日历展开/收缩状态
*/
toggleCalendar() {
this.calendarExpanded = !this.calendarExpanded;
},
/**
* 选择日期
* @param {Object} date 日期对象
*/
selectDate(date) {
if (!date.value) return;
// 清除之前选中的日期
this.allDates.forEach(d => d.selected = false);
// 设置新选中的日期
date.selected = true;
this.selectedDate = date.value;
},
/**
* 获取日期样式类名
* @param {Object} date 日期对象
* @returns {Object} 样式类名对象
*/
getDateClass(date) {
return {
'date-item': true,
@@ -179,10 +143,6 @@ export default {
'empty': !date.value
};
},
/**
* 切换到上一个月
*/
prevMonth() {
if (this.currentMonth === 1) {
this.currentYear--;
@@ -192,10 +152,6 @@ export default {
}
this.generateCalendarDates();
},
/**
* 切换到下一个月
*/
nextMonth() {
if (this.currentMonth === 12) {
this.currentYear++;
@@ -205,49 +161,57 @@ export default {
}
this.generateCalendarDates();
},
/**
* 生成日历数据
*/
generateCalendarDates() {
const year = this.currentYear;
const month = this.currentMonth;
// 获取当月第一天是星期几
this.allDates = this.buildMonthDates(this.currentYear, this.currentMonth);
let prevY = this.currentYear, prevM = this.currentMonth - 1;
if (prevM === 0) { prevM = 12; prevY--; }
this.prevMonthDates = this.buildMonthDates(prevY, prevM);
let nextY = this.currentYear, nextM = this.currentMonth + 1;
if (nextM === 13) { nextM = 1; nextY++; }
this.nextMonthDates = this.buildMonthDates(nextY, nextM);
},
buildMonthDates(year, month) {
const firstDay = new Date(year, month - 1, 1).getDay();
// 获取当月总天数
const daysInMonth = new Date(year, month, 0).getDate();
// 清空日历数据
this.allDates = [];
// 添加上月的空白日期
for (let i = 0; i < firstDay; i++) {
this.allDates.push({ value: null });
}
// 添加当月日期
let dates = [];
for (let i = 0; i < firstDay; i++) dates.push({ value: null });
for (let i = 1; i <= daysInMonth; i++) {
// 模拟考勤记录数据这里假设前20天有考勤记录
const hasRecord = i <= 20;
// 默认选中当月8号
const selected = i === 8 && month === 7 && year === 2025;
this.allDates.push({
value: i,
hasRecord,
selected
});
dates.push({ value: i, hasRecord: i <= 20, selected: i === this.selectedDate });
}
// 计算需要添加的下月空白日期数量使总数为7的倍数
const remainingDays = 7 - (this.allDates.length % 7);
if (remainingDays < 7) {
for (let i = 0; i < remainingDays; i++) {
this.allDates.push({ value: null });
}
const remaining = 7 - (dates.length % 7);
if (remaining < 7) for (let i = 0; i < remaining; i++) dates.push({ value: null });
return dates;
},
isInCurrentWeek(date) {
if (!date.value) return false;
const index = this.allDates.findIndex(d => d.value === this.selectedDate);
if (index === -1) return false;
const start = Math.floor(index / 7) * 7;
const week = this.allDates.slice(start, start + 7);
return week.includes(date);
},
onTouchStart(e) {
this.touchStartX = e.changedTouches[0].clientX;
this.touchStartY = e.changedTouches[0].clientY;
},
onTouchEnd(e) {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const deltaX = endX - this.touchStartX;
const deltaY = endY - this.touchStartY;
// 上下滑切换月/周视图
if (Math.abs(deltaY) > Math.abs(deltaX)) {
if (deltaY < -50 && this.calendarExpanded) this.calendarExpanded = false;
else if (deltaY > 50 && !this.calendarExpanded) this.calendarExpanded = true;
}
},
onSwiperChange(e) {
const current = e.detail.current;
if (current === 0) { this.prevMonth(); this.swiperCurrent = 1; }
else if (current === 2) { this.nextMonth(); this.swiperCurrent = 1; }
}
}
};
@@ -256,7 +220,7 @@ export default {
<style scoped>
.my-record-container {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #fff;
}
/* 顶部导航栏 */
@@ -286,6 +250,8 @@ export default {
/* 月份标题和导航 */
.month-header {
padding: 20rpx 30rpx;
margin-left: 180rpx;
margin-right: 180rpx;
background-color: #fff;
}
@@ -296,8 +262,8 @@ export default {
}
.month-arrow {
width: 60rpx;
height: 60rpx;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
@@ -321,9 +287,9 @@ export default {
justify-content: space-around;
padding: 40rpx 30rpx;
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
border: 2rpx dashed #e0e0e0;
background-color: #f7f7f7;
border-radius: 10rpx;
}
.stat-item {
@@ -348,96 +314,27 @@ export default {
color: #666;
}
/* 日历 */
.calendar {
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
border: 2rpx dashed #e0e0e0;
overflow: hidden;
}
/* 日历样式 */
.calendar { overflow: hidden; margin-left: 20rpx; margin-right: 20rpx}
.weekdays { display: flex; padding: 20rpx 0; }
.weekday { flex: 1; text-align: center; font-size: 28rpx; color: #666; }
.dates { display: flex; flex-wrap: wrap; padding: 20rpx 0; }
.date-item { width: 14.28%; height: 70rpx; display: flex; align-items: center; justify-content: center; position: relative; margin-bottom: 20rpx; }
.date-item.empty { color: #ccc; }
.date-item.selected { background-color: #007aff; border-radius: 50%; width: 60rpx; height: 60rpx; margin: 10rpx auto; }
.date-item.selected .date-text { color: #fff; }
.date-text { font-size: 28rpx; color: #333; }
.record-dot { position: absolute; bottom: 8rpx; left: 50%; transform: translateX(-50%); width: 8rpx; height: 8rpx; background-color: #007aff; border-radius: 50%; }
.date-item.selected .record-dot { background-color: #fff; }
.weekdays {
display: flex;
background-color: #f8f8f8;
padding: 20rpx 0;
}
.weekday {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
}
.dates {
display: flex;
flex-wrap: wrap;
padding: 20rpx 0;
}
.date-item {
width: 14.28%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20rpx;
}
.date-item.empty {
color: #ccc;
}
.date-item.selected {
background-color: #007aff;
border-radius: 50%;
width: 60rpx;
height: 60rpx;
margin: 10rpx auto;
}
.date-item.selected .date-text {
color: #fff;
}
.date-text {
font-size: 28rpx;
color: #333;
}
.record-dot {
position: absolute;
bottom: 8rpx;
left: 50%;
transform: translateX(-50%);
width: 8rpx;
height: 8rpx;
background-color: #007aff;
border-radius: 50%;
}
.date-item.selected .record-dot {
background-color: #fff;
}
.calendar-toggle {
padding: 20rpx;
text-align: center;
border-top: 1rpx solid #e0e0e0;
background-color: #f8f8f8;
}
.toggle-text {
font-size: 28rpx;
color: #666;
}
.calendar-toggle { text-align: center; background-color: #fff; margin-top: -30rpx;}
.image_sq { width: 37rpx; height: 6rpx; }
.image_zk { width: 52rpx; height: 19rpx; }
/* 固定班次 */
.fixed-shifts {
margin: 20rpx 30rpx;
background-color: #fff;
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 30rpx;
}

View File

@@ -108,7 +108,9 @@
this.realSubmit();
return;
}
uni.showLoading({
title: '加载中...'
});
const result = await uploadFiles({
files: images,
url: this.vuex_config.baseUrl + '/resource/oss/upload',
@@ -120,7 +122,8 @@
uni.showToast({
title: '上传失败',
icon: 'none'
});
});
uni.hideLoading();
return;
}
@@ -133,11 +136,17 @@
async realSubmit() {
let res = await this.$u.api.addOrder2(this.repairInfo);
if (res.code == '200') {
if (res.code == '200') {
uni.hideLoading();
// 关闭页面前发送事件通知前页面刷新
uni.$emit('refreshData', '');
// 返回上一页
uni.navigateBack();
}else{
uni.showToast({
title: res.msg,
icon: 'none'
});
}
},

View File

@@ -106,10 +106,14 @@
this.onRefresh()
},
onShow() {
uni.$once('refreshData', () => {
uni.$on('refreshData', () => {
this.onRefresh()
});
},
// 页面卸载时移除事件监听器
onUnload() {
uni.$off('refreshData');
},
methods: {
goBack() {
uni.navigateBack();

View File

@@ -91,9 +91,13 @@
}
},
onShow() {
uni.$once('selectPlate', plate => {
uni.$on('selectPlate', plate => {
this.form.licensePlate = plate;
});
},
// 页面卸载时移除事件监听器
onUnload() {
uni.$off('selectPlate');
},
methods: {
// 新增:处理图片上传

View File

@@ -0,0 +1,212 @@
<template>
<view class="page">
<!-- 搜索框 -->
<view class="search-box">
<input type="text" class="search-input" placeholder="部门、岗位、姓名" v-model="keyword"/>
</view>
<!-- 联系人列表 -->
<scroll-view
scroll-y
class="contact-list"
:scroll-into-view="currentView"
scroll-with-animation
>
<block v-for="(group, gIndex) in contacts" :key="gIndex">
<view class="group-title" :id="'group-' + group.letter">{{ group.letter }}</view>
<view class="contact-item" v-for="(item, index) in group.list" :key="index">
<image class="avatar" :src="item.avatar"></image>
<view class="contact-info">
<text class="name" :class="{'highlight': item.isHighlight}">{{ item.name }}</text>
<text class="desc">{{ item.job }} {{ item.phone }}</text>
</view>
</view>
</block>
</scroll-view>
<!-- 字母索引栏 -->
<view
class="index-bar"
@touchstart="onTouch"
@touchmove.stop.prevent="onTouch"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<view
v-for="(letter, i) in indexList"
:key="i"
class="index-item"
:style="{height: indexItemHeight + 'rpx', lineHeight: indexItemHeight + 'rpx'}"
>
{{ letter }}
</view>
</view>
<!-- 居中大字母提示 -->
<view v-if="showLetter" class="letter-toast">
{{ showLetter }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
keyword: "",
currentView: "",
showLetter: "",
indexList: "ABCDEFGHIJKLMNOPQRSTUVWXYZ#".split(""),
indexItemHeight: 30, // 索引单个高度 (rpx),可调整
contacts: [
{ letter: "A", list: [{ name: "张晓明", phone: "17895698899", job: "保洁", avatar: "https://img.yzcdn.cn/vant/cat.jpeg", isHighlight: true }] },
{ letter: "B", list: [
{ name: "阿俊", phone: "17895698899", job: "保洁", avatar: "https://img.yzcdn.cn/vant/dog.jpeg" },
{ name: "阿俊", phone: "17895698899", job: "保洁", avatar: "https://img.yzcdn.cn/vant/cat.jpeg" },
{ name: "阿俊", phone: "17895698899", job: "保洁", avatar: "https://img.yzcdn.cn/vant/elephant.jpeg" }
]},
{ letter: "C", list: [
{ name: "阿俊", phone: "17895698899", job: "保洁", avatar: "https://img.yzcdn.cn/vant/horse.jpeg" },
{ name: "阿俊", phone: "17895698899", job: "保洁", avatar: "https://img.yzcdn.cn/vant/lion.jpeg" }
]}
]
}
},
methods: {
// 开始/移动
onTouch(e) {
const touchY = e.touches[0].clientY
this.calcIndexByY(touchY)
},
// 松手/取消时隐藏
onTouchEnd() {
setTimeout(() => {
this.showLetter = ""
}, 300) // 0.3 秒后消失
},
// 计算位置 → 滚动 & 显示大字母
calcIndexByY(y) {
const query = uni.createSelectorQuery().in(this)
query.select('.index-bar').boundingClientRect(rect => {
if (!rect) return
const top = rect.top
const index = Math.floor((y - top) / (rect.height / this.indexList.length))
if (index >= 0 && index < this.indexList.length) {
const letter = this.indexList[index]
this.scrollTo(letter)
}
}).exec()
},
scrollTo(letter) {
const exists = this.contacts.find(item => item.letter === letter)
if (exists) {
this.currentView = "group-" + letter
}
this.showLetter = letter
}
}
}
</script>
<style>
.page {
display: flex;
flex-direction: column;
height: 100%;
background-color: #fff;
}
.header {
display: flex;
align-items: center;
height: 90rpx;
padding: 0 20rpx;
}
.back {
font-size: 40rpx;
}
.title {
flex: 1;
text-align: center;
font-size: 34rpx;
font-weight: bold;
}
.search-box {
padding: 20rpx;
}
.search-input {
width: auto;
height: 63rpx;
background: #F2F3F5;
border-radius: 25rpx;
padding-left: 20rpx;
font-size: 28rpx;
margin-left: 18rpx;
margin-right: 18rpx;
}
.contact-list {
flex: 1;
}
.group-title {
padding: 10rpx 20rpx;
background: #f5f5f5;
font-size: 26rpx;
color: #666;
}
.contact-item {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1px solid #f5f5f5;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.contact-info {
display: flex;
flex-direction: column;
}
.name {
font-size: 30rpx;
}
.name.highlight {
color: orange;
}
.desc {
font-size: 24rpx;
color: #888;
margin-top: 6rpx;
}
.index-bar {
position: fixed;
right: 10rpx;
top: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
background-color: transparent;
user-select: none;
}
.index-item {
font-size: 22rpx;
color: #3a6ea5;
}
.letter-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 80rpx;
font-weight: bold;
padding: 40rpx 60rpx;
border-radius: 20rpx;
}
</style>

View File

@@ -86,10 +86,15 @@
this.loadAllTabsData()
},
onShow() {
uni.$once('refreshData', () => {
// 使用$on替代$once确保每次都能监听到事件
uni.$on('refreshData', () => {
this.loadAllTabsData()
});
},
// 页面卸载时移除事件监听器
onUnload() {
uni.$off('refreshData');
},
methods: {
goBack() {
uni.navigateBack();
@@ -141,16 +146,16 @@
this.tabLoaded[idx] = true;
this.loading = false;
},
goDetail2(item) {
const itemStr = encodeURIComponent(JSON.stringify(item));
if([20, 30, 31, 32].includes(item.state)){
uni.navigateTo({
url: "/pages/sys/workbench/earlyWarning/warnDetail?item=" + itemStr,
});
}else{
uni.navigateTo({
url: "/pages/sys/workbench/earlyWarning/warnDetail?item=" + itemStr + "&pageType=detail",
});
goDetail2(item) {
const itemStr = encodeURIComponent(JSON.stringify(item));
if([20, 30, 31, 32].includes(item.state)){
uni.navigateTo({
url: "/pages/sys/workbench/earlyWarning/warnDetail?item=" + itemStr,
});
}else{
uni.navigateTo({
url: "/pages/sys/workbench/earlyWarning/warnDetail?item=" + itemStr + "&pageType=detail",
});
}
},
goStatistics() {
@@ -161,6 +166,11 @@
// 添加预加载所有标签页数据的方法
async loadAllTabsData() {
// 重置状态
this.pageNum = [1, 1];
this.noMore = [false, false];
this.tabData = [[], []];
// 并行加载所有标签页数据,提高加载速度
const loadPromises = [0, 1].map((index) => {
return this.loadTabData(index);

View File

@@ -8,24 +8,45 @@
<view v-if="idx === activeTab" class="tab-underline"></view>
</view>
</view>
<!-- 列表区 -->
<view class="ins-list">
<view v-for="(item, idx) in list" :key="idx" class="ins-card" @click="goProcess(item)">
<view class="ins-row">
<view class="ins-no">保洁部日常巡检 {{ item.createTime.substring(0,11) }}</view>
<view class="ins-status" :class="getStatusColor(item.status)">
{{ getStatusLabel(item.status) }}
</view>
</view>
<image class="ins-line-image" src="/static/ic_my_repair_03.png" />
<view class="ins-info">巡检人{{ item.createTime }}</view>
<view class="ins-info">计划完成时间{{ item.typeName }}</view>
<view class="ins-info">实际完成时间{{ item.location }}</view>
<view class="ins-info">巡检进度{{ item.location }}</view>
</view>
</view>
<!-- 为每个标签页创建独立的scroll-view -->
<view class="ins-list-container">
<scroll-view
v-for="(tab, idx) in tabs"
:key="idx"
v-show="idx === activeTab"
class="ins-list"
scroll-y
refresher-enabled
:refresher-triggered="refresherTriggered[idx]"
refresher-background="#f7f7f7"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
:scroll-with-animation="true"
>
<view v-for="(item, index) in tabData[idx].list" :key="index" class="ins-card" @click="goProcess(item)">
<view class="ins-row">
<view class="ins-no">{{item.planName || ''}} {{ item.createTime ? item.createTime.substring(0,11) : '' }}</view>
<view class="ins-status" :class="getStatusColor(item.status)">
{{ getStatusLabel(item.status) }}
</view>
</view>
<image class="ins-line-image" src="/static/ic_my_repair_03.png" />
<view class="ins-info">巡检人{{ item.actUserName || '' }}</view>
<view class="ins-info">计划完成时间{{ item.planInsTime || '' }}</view>
<view class="ins-info">实际完成时间{{ item.location || '' }}</view>
<view class="ins-info">巡检进度{{ item.inspectionProgress || '' }}</view>
</view>
<!-- 加载更多提示 -->
<view v-if="loading[idx]" style="text-align:center;color:#999;font-size:26rpx;padding:20rpx;">
加载中...
</view>
<view v-if="noMore[idx] && tabData[idx].list.length > 0" style="text-align:center;color:#999;font-size:26rpx;padding:20rpx;">
没有更多数据了
</view>
</scroll-view>
</view>
</view>
</template>
@@ -33,65 +54,122 @@
export default {
data() {
return {
tabs: ['待进行', '处理中', '已完成'],
tabs: ['待进行', '处理中', '已完成', '已超时'],
activeTab: 0,
tabData: [
[],
[],
[]
], // 每个tab的数据
tabLoaded: [false, false, false], // 每个tab是否已加载
loading: false
{ list: [], pageNum: 1, pageSize: 10 },
{ list: [], pageNum: 1, pageSize: 10 },
{ list: [], pageNum: 1, pageSize: 10 },
{ list: [], pageNum: 1, pageSize: 10 }
],
pageNum: [1, 1, 1, 1], // 每个 tab 当前页码
pageSize: 10,
noMore: [false, false, false, false], // 每个 tab 是否没有更多数据
tabLoaded: [false, false, false, false], // tab 是否加载过
loading: [false, false, false, false], // 每个 tab 的加载状态
refresherTriggered: [false, false, false, false] // 每个 tab 的下拉刷新状态
}
},
computed: {
list() {
return this.tabData[this.activeTab];
return this.tabData[this.activeTab].list;
},
hasMore() {
return !this.noMore[this.activeTab];
}
},
created() {
this.loadTabData(this.activeTab); // 初始化加载当前tab数据
this.loadAllTabsData();
},
methods: {
goBack() {
uni.navigateBack();
},
// 加载所有tab数据
async loadAllTabsData() {
for (let idx = 0; idx < this.tabs.length; idx++) {
if (!this.tabLoaded[idx]) {
await this.loadTabData(idx);
}
}
},
// 切换 tab
async changeTab(idx) {
this.activeTab = idx;
if (!this.tabLoaded[idx]) {
await this.loadTabData(idx);
await this.onRefresh();
}
},
// 下拉刷新
async onRefresh() {
this.refresherTriggered[this.activeTab] = true;
this.pageNum[this.activeTab] = 1;
this.noMore[this.activeTab] = false;
this.tabData[this.activeTab].list = [];
await this.loadTabData(this.activeTab);
this.refresherTriggered[this.activeTab] = false;
},
// 滚动加载更多
async loadMore() {
if (this.loading[this.activeTab] || this.noMore[this.activeTab]) return;
this.pageNum[this.activeTab]++;
this.loading[this.activeTab] = true;
await this.loadTabData(this.activeTab);
this.loading[this.activeTab] = false;
},
async loadTabData(idx) {
this.loading = true;
// 模拟接口请求不同tab返回不同mock数据
let params = {}
let data = [];
this.loading[idx] = true;
let params = {
pageNum: this.pageNum[idx],
pageSize: this.pageSize
};
// 根据tab索引设置不同的状态参数
switch(idx) {
case 0: // 待进行
params.status = '0';
break;
case 1: // 处理中
params.status = '1';
break;
case 2: // 已完成
params.status = '2';
break;
case 3: // 已超时
params.status = '3';
break;
}
let res = await this.$u.api.getInspection(params);
if (res.code == '200') {
data = res.rows
let rows = res.rows || [];
if (rows.length < this.pageSize) {
this.noMore[idx] = true;
}
if (this.pageNum[idx] === 1) {
// 刷新时重置数据
this.tabData[idx].list = rows;
} else {
// 加载更多时追加数据
this.tabData[idx].list = [...this.tabData[idx].list, ...rows];
}
}
this.$set(this.tabData, idx, data);
this.$set(this.tabLoaded, idx, true);
this.loading = false;
this.loading[idx] = false;
},
goProcess(item) {
const detailItemStr = encodeURIComponent(JSON.stringify(item));
uni.navigateTo({
url: `/pages/sys/workbench/inspection/inspectionProcess?detailItem=${item}`
uni.navigateTo({
url: `/pages/sys/workbench/inspection/inspectionProcess?item=${detailItemStr}`
});
},
getStatusLabel(status) {
const statusMap = {
0: '待确认',
1: '已确认',
2: '已取消',
3: '已完成'
0: '待进行',
1: '处理中',
2: '已完成',
3: '已超时'
};
return statusMap[status] || '';
},
@@ -100,7 +178,8 @@
0: '待确认',
1: 'orange',
2: '已取消',
3: '已完成'
3: 'done',
4: 'done'
};
return statusMap[status] || '';
}
@@ -153,15 +232,15 @@
margin-top: 8rpx;
}
.ins-list {
margin: 25rpx 0 0 0;
padding: 0 35rpx;
.ins-list-container {
flex: 1;
/* 占据所有剩余空间 */
overflow-y: auto;
/* 内容超出时,开启垂直滚动 */
padding-bottom: 200rpx;
/* 为底部按钮留出空间 */
overflow: hidden;
}
.ins-list {
height: 100%;
padding: 25rpx 35rpx 0;
box-sizing: border-box;
}
.ins-card {
@@ -180,43 +259,47 @@
margin-top: 25rpx;
margin-left: 19rpx;
margin-right: 50rpx;
}
.ins-no {
font-size: 24rpx;
color: #0B0B0B;
font-weight: 500;
}
.ins-status {
font-size: 24rpx;
font-weight: 500;
}
.ins-line-image {
margin: left 29rpx;
margin-right: 39rpx;
height: 2rpx;
margin-bottom: 29rpx;
}
.ins-status.orange {
color: #F3AB44;
}
.ins-status.doing {
color: #00C9AA;
}
.ins-status.done {
color: #8A8A8A;
}
.ins-info {
font-size: 24rpx;
color: #888;
margin-bottom: 30rpx;
margin-left: 47rpx;
}
.ins-no {
font-size: 24rpx;
color: #0B0B0B;
font-weight: 500;
}
.ins-status {
font-size: 24rpx;
font-weight: 500;
}
.ins-line-image {
margin: left 29rpx;
margin-right: 39rpx;
height: 2rpx;
margin-bottom: 29rpx;
}
.ins-status.orange {
color: #F3AB44;
}
.ins-status.doing {
color: #00C9AA;
}
.ins-status.done {
color: #8A8A8A;
}
.ins-status.overdue {
color: #FF4D4D;
}
.ins-info {
font-size: 24rpx;
color: #888;
margin-bottom: 30rpx;
margin-left: 47rpx;
}
</style>

View File

@@ -0,0 +1,452 @@
<template>
<view class="inspection-opt-container">
<view class="inspection-opt-lable"> 巡检人
<view class="inspection-opt-round" />
<text class="text">{{info.planInspectionPerson}}</text>
</view>
<view class="inspection-opt-lable"> 巡检位置
<text class="required">*</text>
</view>
<view class="inspection-opt-row">
<text class="text2">{{info.inspectionLocation}}</text>
</view>
<view class="inspection-opt-lable"> 巡检结果
<text class="required">*</text>
<view :class="info.inspectionResults == 1?'inspection-opt-round4':'inspection-opt-round2'"/>
<text class="text">正常</text>
<view :class="info.inspectionResults != 1?'inspection-opt-round3':'inspection-opt-round2'" />
<text class="text"> 异常</text>
</view>
<view class="inspection-opt-lable"> 巡检描述
<text class="required">*</text>
</view>
<text class="input" >{{ info.remark }}</text>
<!-- 上传照片 -->
<view class="page">
<!-- 已选图片预览 -->
<view class="preview-item">
<image :src="selectedImage" mode="aspectFill" @click="previewImage"></image>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
info: {
inspectionResults: 1 // 默认选中"正常"
},
selectedImage: '',
isSign: false,
showDialog: false // 控制弹窗显示
}
},
onLoad(options) {
if (options.item) {
try {
// 首先尝试解析原始字符串(可能未编码)
const item = JSON.parse(options.item);
this.info = item;
} catch (e) {
// 如果直接解析失败,再尝试解码后解析
try {
const item = JSON.parse(decodeURIComponent(options.item));
this.info = item;
} catch (e2) {
this.info = options.item;
}
}
this.isSign = this.info.actualSignState == 1
}
},
methods: {
// 预览图片
previewImage() {
uni.previewImage({
current: this.selectedImage,
urls: [this.selectedImage]
})
},
// 切换巡检结果状态
toggleInspectionResult(result) {
// 使用$set确保响应式更新
this.$set(this.info, 'inspectionResults', result);
},
beforeSubmit() {
if (this.info.inspectionResults == 1) {
// 直接提交
this.submit();
} else {
// 弹窗显示,生成工单提交
this.showDialog = true;
}
},
closeDialog() {
this.showDialog = false;
},
confirmDialog() {
this.showDialog = false;
// 生成工单提交
this.submit();
},
async taskSignIn() {
uni.scanCode({
success: async (scanRes) => {
// 使用扫码结果调用接口
let params = {
taskId: this.info.id,
qrCode: scanRes.result // 将扫码结果作为参数传递
};
if (scanRes.result == this.info.pointId) {
uni.showLoading({
title: '加载中...'
});
this.info.signType = 3
let res = await this.$u.api.taskSignIn(this.info);
// 隐藏loading
uni.hideLoading();
if (res.code == 200) {
this.isSign = true
uni.showToast({
title: '签到成功',
icon: 'none'
});
} else {
uni.showToast({
title: res.msg || '签到失败',
icon: 'none'
});
}
}else{
uni.showToast({
title: '当前签到巡检点不是目标点',
icon: 'none'
});
}
},
fail: (error) => {
// 扫码失败处理
uni.showToast({
title: '扫码失败,请重试',
icon: 'none'
});
}
});
},
async submit() {
// 显示loading
uni.showLoading({
title: '提交中...'
});
if (!this.selectedImage) {
this.realSubmit()
return
}
const images = [];
images.push(this.selectedImage?.path?.replace('file://', '') || this.selectedImage);
if (images.length === 0) {
this.realSubmit();
return;
}
const result = await uploadFiles({
files: images,
url: this.vuex_config.baseUrl + '/resource/oss/upload',
name: 'file',
vm: this // 关键:用于注入 token 等
});
const allSuccess = result.every(item => item.code == 200);
if (!allSuccess) {
uni.showToast({
title: '上传失败',
icon: 'none'
});
// 隐藏loading
uni.hideLoading();
return;
}
// 遍历result获取data.url加上,分割
const urls = result.map(item => item.data?.url || '').filter(url => url !== '');
this.info.inspectionImage = urls.join(',');
this.realSubmit()
},
async realSubmit() {
let res = await this.$u.api.taskSubmit(this.info);
if (res.code == '200') {
// 关闭页面前发送事件通知前页面刷新
uni.$emit('refreshData', '');
// 返回上一页
uni.navigateBack();
}
// 隐藏loading
uni.hideLoading();
},
}
}
</script>
<style scoped>
.inspection-opt-container {
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
}
.inspection-opt-lable {
font-size: 32rpx;
color: #000000;
font-weight: 600;
display: flex;
flex-direction: row;
align-items: center;
margin-left: 40rpx;
margin-top: 45rpx;
}
.inspection-opt-row {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 34rpx;
}
.required {
color: #DC9100;
margin-left: 10rpx;
}
.text {
color: #000000;
font-size: 24rpx;
margin-left: 10rpx;
font-weight: 400;
}
.text2 {
height: 73rpx;
flex: 1;
background: #F7F7F7;
border-radius: 10rpx;
display: flex;
align-items: center;
padding-left: 20rpx;
padding-right: 20rpx;
}
.text3 {
width: 88rpx;
height: 73rpx;
background: #296AEF;
border-radius: 10rpx;
color: #F7F7F7;
text-align: center;
margin-left: 18rpx;
line-height: 73rpx;
display: flex;
align-items: center;
justify-content: center;
}
.input {
height: 200rpx;
width: auto;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 40rpx;
background: #F7F7F7;
border-radius: 10rpx;
font-size: 24rpx;
color: #000000;
padding: 15rpx;
}
.image {
width: 55rpx;
height: 42rpx;
}
.inspection-opt-round {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #3370FF;
margin-left: 35rpx;
}
.inspection-opt-round2 {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #fff;
margin-left: 24rpx;
border: 1rpx solid #3370FF;
}
.inspection-opt-round3 {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #F27A0F;
margin-left: 24rpx;
}
.inspection-opt-round4 {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #3370FF;
margin-left: 24rpx;
}
.custom-upload-btn {
width: auto;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 24rpx;
height: 244rpx;
background: #f6f6f6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 16rpx;
}
.preview-item {
position: relative;
width: auto;
/* 宽度自动 */
margin-left: 40rpx;
margin-right: 40rpx;
height: 244rpx;
margin-top: 24rpx;
/* 固定高度 */
display: flex;
justify-content: center;
/* 水平居中 */
align-items: center;
/* 垂直居中 */
overflow: hidden;
/* 裁剪超出部分 */
border-radius: 10rpx;
/* 可选:圆角 */
}
.delete-btn {
position: absolute;
top: 0;
right: 0;
width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 24rpx;
}
/* 隐藏自带按钮 */
::v-deep .u-upload__wrap__add {
display: none !important;
}
.inspection-opt-submit-btn {
width: 60vw;
height: 73rpx;
background: linear-gradient(90deg, #005DE9 0%, #4B9BFF 100%);
color: #fff;
font-size: 32rpx;
border: none;
border-radius: 40rpx;
margin: 100rpx auto 0 auto;
display: block;
font-weight: bold;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.18);
}
/* 弹窗遮罩 */
.custom-dialog {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
/* 弹窗背景图 */
.dialog-bg {
position: absolute;
width: 656rpx;
height: 560rpx;
border-radius: 16rpx;
}
/* 弹窗内容覆盖在图片上 */
.dialog-content {
position: relative;
width: 80vw;
margin-top: 220rpx;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
z-index: 10;
}
.dialog-title {
font-size: 42rpx;
font-weight: bold;
color: #000;
margin-bottom: 40rpx;
}
.dialog-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 100rpx;
}
.dialog-btns {
display: flex;
width: 100%;
justify-content: space-around;
}
.btn-cancel,
.btn-confirm {
font-size: 36rpx;
}
.btn-cancel {
color: #333;
}
.btn-confirm {
color: #FF6B00;
}
</style>

View File

@@ -0,0 +1,573 @@
<template>
<view class="inspection-opt-container">
<view class="inspection-opt-lable"> 巡检人
<view class="inspection-opt-round" />
<text class="text">{{info.planInspectionPerson}}</text>
</view>
<view class="inspection-opt-lable"> 巡检位置
<text class="required">*</text>
</view>
<view class="inspection-opt-row">
<text class="text2">{{info.inspectionLocation}}</text>
<text v-if="!isSign" class="text3" @click="taskSignIn"> 签到</text>
</view>
<view class="inspection-opt-lable"> 巡检结果
<text class="required">*</text>
<view :class="info.inspectionResults == 1?'inspection-opt-round4':'inspection-opt-round2'"
@click="toggleInspectionResult(1)" />
<text class="text" @click="toggleInspectionResult(1)">正常</text>
<view :class="info.inspectionResults != 1?'inspection-opt-round3':'inspection-opt-round2'"
@click="toggleInspectionResult(0)" />
<text class="text" @click="toggleInspectionResult(0)"> 异常</text>
</view>
<view class="inspection-opt-lable"> 巡检描述
<text class="required">*</text>
</view>
<textarea type="text" v-model="info.remark" placeholder="请输入" class="input" />
<!-- 上传照片 -->
<view class="page">
<!-- 如果没有图片显示上传按钮 -->
<view class="custom-upload-btn" @click="chooseImage" v-if="!selectedImage">
<image class="image" src="/static/ic_camera.png"></image>
<text class="text">上传图片</text>
</view>
<!-- 已选图片预览 -->
<view class="preview-item" v-else>
<image class="preview-img" :src="selectedImage" mode="aspectFill" @click="previewImage"></image>
<view class="delete-btn" @click.stop="deletePic"></view>
</view>
</view>
<view v-if="info.inspectionResults != 1" class="inspection-opt-lable">报事报修</view>
<view v-if="info.inspectionResults != 1" class="repair-type" @click="chooseType">
<text class="text-type">{{ selectedType.orderTypeName }}</text>
<image class="right-arrow" src="/static/ic_right_arrow_g.png" />
</view>
<!-- 提交按钮 -->
<button class="inspection-opt-submit-btn"
@click="beforeSubmit">{{ info.inspectionResults == 1 ? '提交' : '生成工单提交' }}</button>
<!-- 弹窗 -->
<view class="custom-dialog" v-if="showDialog">
<image class="dialog-bg" src="/static/ic_dialog_01.png"></image>
<view class="dialog-content">
<text class="dialog-title">是否确定</text>
<text class="dialog-desc">转送确定生成工单编号</text>
<view class="dialog-btns">
<view class="btn-cancel" @click="closeDialog">下次再说</view>
<view class="btn-confirm" @click="confirmDialog">确认生成</view>
</view>
</view>
</view>
</view>
</template>
<script>
// 导入MediaSelector和MediaType
import MediaSelector, {
MediaType
} from '@/utils/mediaSelector';
import {
uploadFiles
} from '@/common/upload.js';
import toast from '../../../../uview-ui/libs/function/toast';
export default {
data() {
return {
info: {
inspectionResults: 1 // 默认选中"正常"
},
repairTypes: [],
selectedType: {},
selectedImage: '',
isSign: false,
showDialog: false // 控制弹窗显示
}
},
onLoad(options) {
if (options.item) {
try {
// 首先尝试解析原始字符串(可能未编码)
const item = JSON.parse(options.item);
this.info = item;
} catch (e) {
// 如果直接解析失败,再尝试解码后解析
try {
const item = JSON.parse(decodeURIComponent(options.item));
this.info = item;
} catch (e2) {
this.info = options.item;
}
}
this.isSign = this.info.actualSignState == 1
}
this.loadFilterData()
},
methods: {
async loadFilterData() {
let resType = await this.$u.api.getOrdersType();
if (resType.code === 200) {
this.repairTypes = [...this.repairTypes, ...resType.rows];
this.selectedType = this.repairTypes[0]
}
},
chooseImage() {
uni.chooseImage({
count: 1,
success: (res) => {
this.selectedImage = res.tempFilePaths[0]
}
})
},
// 预览图片
previewImage() {
uni.previewImage({
current: this.selectedImage,
urls: [this.selectedImage]
})
},
// 删除图片
deletePic(event) {
this.selectedImage = ''
},
// 切换巡检结果状态
toggleInspectionResult(result) {
// 使用$set确保响应式更新
this.$set(this.info, 'inspectionResults', result);
},
// 添加chooseType方法实现
chooseType() {
uni.showActionSheet({
itemList: this.repairTypes.map(item => item.orderTypeName),
success: (res) => {
this.selectedType = this.repairTypes[res.tapIndex];
},
fail: (err) => {
console.log('用户取消选择或出错', err);
}
});
},
beforeSubmit() {
// if(!this.isSign){
// uni.showToast({
// title: '请先签到',
// icon: 'none'
// });
// return
// }
if (this.info.inspectionResults == 1) {
// 直接提交
this.submit();
} else {
// 弹窗显示,生成工单提交
this.showDialog = true;
}
},
closeDialog() {
this.showDialog = false;
},
confirmDialog() {
this.showDialog = false;
// 生成工单提交
this.orderSubmig();
},
async taskSignIn() {
uni.scanCode({
success: async (scanRes) => {
// 使用扫码结果调用接口
let params = {
taskId: this.info.id,
qrCode: scanRes.result // 将扫码结果作为参数传递
};
if (scanRes.result == this.info.pointId) {
uni.showLoading({
title: '加载中...'
});
this.info.signType = 3
let res = await this.$u.api.taskSignIn(this.info);
// 隐藏loading
uni.hideLoading();
if (res.code == 200) {
this.isSign = true
uni.showToast({
title: '签到成功',
icon: 'none'
});
} else {
uni.showToast({
title: res.msg || '签到失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '当前签到巡检点不是目标点',
icon: 'none'
});
}
},
fail: (error) => {
// 扫码失败处理
// uni.showToast({
// title: '扫码失败,请重试',
// icon: 'none'
// });
}
});
},
async orderSubmig(){
this.info.orderTypeId = this.selectedType.id
uni.showLoading({
title: '工单提交中...'
});
let res = await this.$u.api.taskOrderSubmit(this.info);
uni.hideLoading();
if (res.code == 200) {
this.submit()
} else {
uni.showToast({
title: res.msg || '工单提交失败',
icon: 'none'
});
this.info.orderTypeId = ''
}
},
async submit() {
// 显示loading
uni.showLoading({
title: '提交中...'
});
if (!this.selectedImage) {
this.realSubmit()
return
}
const images = [];
images.push(this.selectedImage?.path?.replace('file://', '') || this.selectedImage);
if (images.length === 0) {
this.realSubmit();
return;
}
const result = await uploadFiles({
files: images,
url: this.vuex_config.baseUrl + '/resource/oss/upload',
name: 'file',
vm: this // 关键:用于注入 token 等
});
const allSuccess = result.every(item => item.code == 200);
if (!allSuccess) {
uni.showToast({
title: '上传失败',
icon: 'none'
});
// 隐藏loading
uni.hideLoading();
return;
}
// 遍历result获取data.url加上,分割
const urls = result.map(item => item.data?.url || '').filter(url => url !== '');
this.info.inspectionImage = urls.join(',');
this.realSubmit()
},
async realSubmit() {
let res = await this.$u.api.taskSubmit(this.info);
if (res.code == '200') {
// 关闭页面前发送事件通知前页面刷新
uni.$emit('refreshData', '');
// 返回上一页
uni.navigateBack();
}
// 隐藏loading
uni.hideLoading();
},
}
}
</script>
<style scoped>
.inspection-opt-container {
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
}
.inspection-opt-lable {
font-size: 32rpx;
color: #000000;
font-weight: 600;
display: flex;
flex-direction: row;
align-items: center;
margin-left: 40rpx;
margin-top: 45rpx;
}
.inspection-opt-row {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 34rpx;
}
.required {
color: #DC9100;
margin-left: 10rpx;
}
.text {
color: #000000;
font-size: 24rpx;
margin-left: 10rpx;
font-weight: 400;
}
.text2 {
height: 73rpx;
flex: 1;
background: #F7F7F7;
border-radius: 10rpx;
display: flex;
align-items: center;
padding-left: 20rpx;
padding-right: 20rpx;
}
.text3 {
width: 88rpx;
height: 73rpx;
background: #296AEF;
border-radius: 10rpx;
color: #F7F7F7;
text-align: center;
margin-left: 18rpx;
line-height: 73rpx;
display: flex;
align-items: center;
justify-content: center;
}
.input {
height: 200rpx;
width: auto;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 40rpx;
background: #F7F7F7;
border-radius: 10rpx;
font-size: 24rpx;
color: #000000;
padding: 15rpx;
}
.image {
width: 55rpx;
height: 42rpx;
}
.inspection-opt-round {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #3370FF;
margin-left: 35rpx;
}
.inspection-opt-round2 {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #fff;
margin-left: 24rpx;
border: 1rpx solid #3370FF;
}
.inspection-opt-round3 {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #F27A0F;
margin-left: 24rpx;
}
.inspection-opt-round4 {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #3370FF;
margin-left: 24rpx;
}
.custom-upload-btn {
width: auto;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 24rpx;
height: 244rpx;
background: #f6f6f6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 16rpx;
}
.preview-item {
position: relative;
width: auto;
/* 宽度自动 */
margin-left: 40rpx;
margin-right: 40rpx;
height: 244rpx;
margin-top: 24rpx;
/* 固定高度 */
display: flex;
justify-content: center;
/* 水平居中 */
align-items: center;
/* 垂直居中 */
overflow: hidden;
/* 裁剪超出部分 */
border-radius: 10rpx;
/* 可选:圆角 */
}
.delete-btn {
position: absolute;
top: 0;
right: 0;
width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 24rpx;
}
/* 隐藏自带按钮 */
::v-deep .u-upload__wrap__add {
display: none !important;
}
.inspection-opt-submit-btn {
width: 60vw;
height: 73rpx;
background: linear-gradient(90deg, #005DE9 0%, #4B9BFF 100%);
color: #fff;
font-size: 32rpx;
border: none;
border-radius: 40rpx;
margin: 100rpx auto 0 auto;
display: block;
font-weight: bold;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.18);
}
/* 弹窗遮罩 */
.custom-dialog {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
/* 弹窗背景图 */
.dialog-bg {
position: absolute;
width: 656rpx;
height: 560rpx;
border-radius: 16rpx;
}
/* 弹窗内容覆盖在图片上 */
.dialog-content {
position: relative;
width: 80vw;
margin-top: 220rpx;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
z-index: 10;
}
.dialog-title {
font-size: 42rpx;
font-weight: bold;
color: #000;
margin-bottom: 40rpx;
}
.dialog-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 100rpx;
}
.dialog-btns {
display: flex;
width: 100%;
justify-content: space-around;
}
.btn-cancel,
.btn-confirm {
font-size: 36rpx;
}
.btn-cancel {
color: #333;
}
.btn-confirm {
color: #FF6B00;
}
.repair-type {
height: 98rpx;
background: #F7F8FA;
border-radius: 10rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
box-sizing: border-box;
margin-left: 40rpx;
margin-right: 40rpx;
margin-top: 20rpx;
}
.text-type {
font-size: 24rpx;
color: #808080;
}
.right-arrow {
width: 11rpx;
height: 21rpx;
}
</style>

View File

@@ -1,274 +1,293 @@
<template>
<view class="page">
<view class="section-title">巡检点</view>
<!-- 时间轴列表 -->
<view class="timeline">
<view v-for="(item, idx) in taskList" :key="idx" class="node"
:class="[{ 'is-last': idx === taskList.length - 1 }]">
<!-- 左侧 + 竖线 -->
<view class="rail">
<view class="dot" :class="{
'dot-circle': item.dotShape === 'circle',
'dot-square': item.dotShape === 'square',
'dot-active': item.dotColor === 'blue'
}"></view>
</view>
<!-- 右侧内容 -->
<view class="card">
<!-- 标题块 + 操作区打包在一起方便宽度同步 -->
<view class="title-ops-wrapper">
<!-- 标题块 -->
<view v-if="item.headerStyle === 'solid'" class="title-solid">
{{ item.pointName }}{{ item.date }} {{ item.time }}
</view>
<view v-else class="title-dashed">
{{ item.pointName }}{{ item.date }} {{ item.time }}
</view>
<!-- 操作区宽度跟随标题块内部居中 -->
<view class="ops" v-if="item.status === '待巡检'">
<view class="btn-outline" @click="startTask(item)">立即巡检</view>
</view>
<view class="ops" v-else>
<view class="btn-disabled">完成巡检</view>
<view class="badge" :class="item.result === '正常' ? 'badge-success' : 'badge-warn'">
{{ item.result }}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="footer-placeholder">巡检提醒</view>
</view>
</template>
<script>
export default {
data() {
return {
taskList: []
}
},
onLoad() {
this.getTaskListMock()
},
methods: {
// ---- 模拟接口数据(可替换成 uni.request----
getTaskListMock() {
this.taskList = [
{
pointName: 'A区花园',
date: '2025-07-16',
time: '09:00—10:00',
status: '待巡检',
headerStyle: 'solid',
result: '',
dotShape: 'circle',
dotColor: 'gray'
},
{
pointName: 'A区花园',
date: '2025-07-16',
time: '09:00—10:00',
status: '已完成',
headerStyle: 'dashed',
result: '正常',
dotShape: 'circle',
dotColor: 'blue'
},
{
pointName: 'A区花园',
date: '2025-07-16',
time: '09:00—10:00',
status: '待巡检',
headerStyle: 'solid',
result: '',
dotShape: 'square',
dotColor: 'gray'
},
{
pointName: 'A区花园',
date: '2025-07-16',
time: '09:00—10:00',
status: '已完成',
headerStyle: 'dashed',
result: '异常',
dotShape: 'square',
dotColor: 'blue'
}
]
<template>
<view class="page">
<view class="section-title">巡检点</view>
<!-- 时间轴列表 -->
<view class="timeline">
<view v-for="(item, idx) in taskList" :key="idx" class="node"
:class="[{ 'is-last': idx === taskList.length - 1 }]">
<!-- 左侧 + 竖线 -->
<view class="rail">
<view class="dot" :class="{
'dot-active': item.inspectionState == 1,
}"></view>
</view>
<!-- 右侧内容 -->
<view class="card">
<!-- 标题块 + 操作区打包在一起方便宽度同步 -->
<view class="title-ops-wrapper">
<!-- 标题块 -->
<view v-if="item.inspectionState ==0 " class="title-solid">
{{ item.pointName }}
<view>
{{ item.pointStartTime.substring(0,16) }} - {{ item.pointEndTime.substring(0,16) }}
</view>
</view>
<view v-else class="title-dashed">
{{ item.pointName }}
<view>
{{ item.pointStartTime.substring(0,16) }} - {{ item.pointEndTime.substring(0,16) }}
</view>
</view>
<!-- 操作区宽度跟随标题块内部居中 -->
<view class="ops" v-if="item.inspectionState ==0">
<view class="btn-outline" @click="startTask(item)">立即巡检</view>
</view>
<view class="status" v-else @click="goDetail(item)">
<view>完成巡检</view>
<view class="badge" :class="item.inspectionResults ==1 ? 'badge-success' : 'badge-warn'">
{{ item.inspectionResults == 1 ? '正常' : '异常' }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
taskInfo: {},
taskList: []
}
},
onLoad(options) {
if (options.item) {
console.log('options.item:', options.item);
try {
// 首先尝试解析原始字符串(可能未编码)
const item = JSON.parse(options.item);
console.log('parsed item:', item);
this.taskInfo = item;
} catch (e) {
// 如果直接解析失败,再尝试解码后解析
try {
const item = JSON.parse(decodeURIComponent(options.item));
console.log('parsed decoded item:', item);
this.taskInfo = item;
} catch (e2) {
// 如果两种方式都失败,记录错误并使用原始数据
console.error('解析item失败:', e2);
this.taskInfo = options.item;
}
}
}
this.getTaskListMock();
},
onShow() {
uni.$on('refreshData', () => {
this.getTaskListMock();
});
},
// 页面卸载时移除事件监听器
onUnload() {
uni.$off('refreshData');
},
methods: {
async getTaskListMock() {
let params = {
taskId: this.taskInfo.id,
};
let res = await this.$u.api.getTaskList(params);
if (res.code == 200 && res.rows) {
// 提取res.data数组中每个对象的url字段
this.taskList = res.rows
}
},
startTask(item) {
const detailItemStr = encodeURIComponent(JSON.stringify(item));
uni.navigateTo({
url: `/pages/sys/workbench/inspection/inspectionOpt?item=${detailItemStr}`
});
},
startTask(item) {
uni.showToast({
title: `开始巡检:${item.pointName}`,
icon: 'none'
})
}
}
}
</script>
<style scoped>
/* 页面基础 */
.page {
background: #f7f8fa;
min-height: 100vh
}
.section-title {
font-size: 28rpx;
color: #333;
margin: 24rpx 24rpx 8rpx
}
/* 时间轴容器 */
.timeline {
position: relative;
padding: 16rpx 24rpx 40rpx 24rpx
}
/* 每个节点 */
.node {
position: relative;
padding-left: 72rpx;
margin-bottom: 32rpx
}
.node:last-child {
margin-bottom: 0
}
/* 左侧导轨与连线 */
.node::after {
content: "";
position: absolute;
left: 37rpx;
top: 35rpx;
bottom: -32rpx;
width: 2rpx;
background: #e8e9ee
}
.node.is-last::after {
display: none
}
/* 左栏(点的容器) */
.rail {
position: absolute;
left: 0;
top: 0;
width: 72rpx;
height: 100%;
display: flex;
justify-content: center
}
.dot {
width: 16rpx;
height: 16rpx;
margin-top: 20rpx;
background: #cfd3dc
}
.dot-circle {
border-radius: 50%
}
.dot-square {
border-radius: 4rpx
}
.dot-active {
background: #2f6aff
}
/* 右侧卡片 */
.card {
padding: 16rpx 20rpx;
}
/* 标题 + 操作区包裹(宽度由标题决定) */
.title-ops-wrapper {
display: inline-block;
}
/* 标题两种样式 */
.title-solid {
background: #2f6aff;
color: #fff;
border-radius: 12rpx;
padding: 12rpx 18rpx;
display: inline-block;
font-size: 26rpx
}
.title-dashed {
border-radius: 12rpx;
background: #fff;
padding: 12rpx 18rpx;
display: inline-block;
font-size: 26rpx;
color: #333
}
/* 操作区:宽度继承标题块,内部居中 */
.ops {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.btn-outline {
border: 2rpx solid #2f6aff;
color: #2f6aff;
background: #fff;
padding: 12rpx 28rpx;
border-radius: 12rpx;
font-size: 26rpx
}
.btn-disabled {
background: #f2f3f5;
color: #a0a0a0;
padding: 12rpx 24rpx;
border-radius: 12rpx;
font-size: 26rpx
}
/* 结果徽标 */
.badge {
padding: 8rpx 18rpx;
border-radius: 22rpx;
font-size: 24rpx;
margin-left: 16rpx;
}
.badge-success {
color: #16a34a;
border: 2rpx solid #16a34a;
background: #fff
}
.badge-warn {
color: #f59e0b;
border: 2rpx solid #f59e0b;
background: #fff
}
/* 底部占位文字 */
.footer-placeholder {
text-align: center;
color: #e5e6eb;
font-size: 28rpx;
margin-top: 80rpx;
letter-spacing: 2rpx
}
</style>
goDetail(item) {
// const detailItemStr = encodeURIComponent(JSON.stringify(item));
// uni.navigateTo({
// url: `/pages/sys/workbench/inspection/inspectionDetail?item=${detailItemStr}`
// });
}
}
}
</script>
<style scoped>
/* 页面基础 */
.page {
background: #f7f8fa;
min-height: 100vh
}
.section-title {
font-size: 28rpx;
color: #0B0B0B;
font-weight: bold;
padding-top: 24rpx;
padding-left: 40rpx;
}
/* 时间轴容器 */
.timeline {
position: relative;
padding: 16rpx 24rpx 40rpx 24rpx
}
/* 每个节点 */
.node {
position: relative;
padding-left: 72rpx;
margin-bottom: 32rpx
}
.node:last-child {
margin-bottom: 0
}
/* 左侧导轨与连线 */
.node::after {
content: "";
position: absolute;
left: 37rpx;
top: 35rpx;
bottom: -32rpx;
width: 2rpx;
background: #e8e9ee
}
.node.is-last::after {
display: none
}
/* 左栏(点的容器) */
.rail {
position: absolute;
left: 0;
top: 0;
width: 72rpx;
height: 100%;
display: flex;
justify-content: center
}
.dot {
width: 16rpx;
height: 16rpx;
margin-top: 20rpx;
background: #cfd3dc;
border-radius: 50%
}
.dot-active {
background: #2f6aff
}
/* 右侧卡片 */
.card {}
/* 标题 + 操作区包裹(宽度由标题决定) */
.title-ops-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
/* 标题两种样式 */
.title-solid {
background: #2f6aff;
color: #fff;
border-radius: 12rpx;
padding: 12rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
font-size: 28rpx;
width: 100%;
box-sizing: border-box;
}
.title-dashed {
border-radius: 12rpx;
background: #fff;
padding: 12rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
font-size: 28rpx;
color: #000;
width: 100%;
box-sizing: border-box;
}
/* 操作区:宽度继承标题块,内部居中 */
.ops {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.status {
width: 280rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #BFBFBF;
margin-top: 20rpx;
color: #BFBFBF;
padding: 12rpx 24rpx;
border-radius: 12rpx;
font-size: 26rpx;
margin-bottom: 40rpx;
}
.btn-outline {
border: 2rpx solid #2f6aff;
color: #2f6aff;
background: #fff;
padding: 12rpx 28rpx;
border-radius: 12rpx;
font-size: 26rpx
}
.btn-disabled {
background: #f2f3f5;
color: #a0a0a0;
padding: 12rpx 24rpx;
border-radius: 12rpx;
font-size: 26rpx
}
/* 结果徽标 */
.badge {
font-size: 24rpx;
margin-left: 20rpx;
}
.badge-success {
color: #16a34a;
}
.badge-warn {
color: #f59e0b;
}
/* 底部占位文字 */
.footer-placeholder {
text-align: center;
color: #e5e6eb;
font-size: 28rpx;
margin-top: 80rpx;
letter-spacing: 2rpx
}
</style>

View File

@@ -0,0 +1,583 @@
<template>
<view class="leave-container">
<!-- tab栏 -->
<view class="leave-tabs">
<view v-for="(tab, idx) in tabs" :key="idx" :class="['leave-tab', {active: idx === activeTab}]"
@click="changeTab(idx)">
{{ tab }}
<view v-if="idx === activeTab" class="tab-underline"></view>
</view>
</view>
<!-- 发起申请 -->
<scroll-view v-show="activeTab === 0" class="leave-form" scroll-y>
<!-- 提交人 -->
<view class="form-row2">
<text class="label">提交人</text>
<view class="leave-round"></view>
<text class="name">马花花</text>
</view>
<!-- 请假类型 -->
<view class="form-row">
<text class="label required">请假类型</text>
<view class="picker" @click="openLeaveTypePopup">
<text
:class="leaveTypeIndex === 0 ? 'placeholder' : ''">{{ leaveTypes[leaveTypeIndex] || '请选择' }}</text>
<image class="right-arrow" src="/static/ic_right_arrow_g.png" />
</view>
</view>
<!-- 开始时间 -->
<view class="form-row">
<text class="label required">开始时间</text>
<picker mode="date" @change="startDateChange">
<view class="picker">
<text :class="!startDate ? 'placeholder' : ''">{{ startDate || '请选择' }}</text>
<image class="right-arrow" src="/static/ic_right_arrow_g.png" />
</view>
</picker>
</view>
<!-- 结束时间 -->
<view class="form-row">
<text class="label required">结束时间</text>
<picker mode="date" @change="endDateChange">
<view class="picker">
<text :class="!endDate ? 'placeholder' : ''">{{ endDate || '请选择' }}</text>
<image class="right-arrow" src="/static/ic_right_arrow_g.png" />
</view>
</picker>
</view>
<!-- 时间小时 -->
<view class="form-row">
<text class="label required">时间小时</text>
<input type="number" v-model="hours" placeholder="请输入时间(小时)" />
</view>
<!-- 请假事由 -->
<view class="form-row">
<text class="label required">请假事由</text>
<textarea v-model="reason" placeholder="请输入请假事由..." />
</view>
<!-- 上传图片 -->
<u-upload class="upload-style" :fileList="selectedImages" @on-list-change="onListChange" @delete="deletePic" name="upload"
multiple maxCount="3" width="180" height="180" :autoUpload="false"></u-upload>
<!-- 提交按钮 -->
<button class="submit-btn" @click="submitLeave">提交</button>
</scroll-view>
<!-- 请假记录 -->
<scroll-view v-show="activeTab === 1" class="leave-list-scroll" scroll-y :refresher-enabled="true"
:refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<view v-for="(item, index) in leaveRecords" :key="index" class="oa-card" @click="goToDetail(item)">
<view class="card-row">
<view class="card-type">{{ item.type }}</view>
<view class="card-status-tag" :class="item.statusClass">{{ item.status }}</view>
</view>
<view class="card-info">
<view class="card-leave-type">请假类型<text class="card-leave-type">{{ item.leaveType }}</text></view>
<view class="card-leave-type">
<text>开始时间{{ item.startTime }}</text>
</view>
<view class="card-leave-type">
<text>结束时间{{ item.endTime }}</text>
</view>
</view>
<view class="card-footer">
<view class="card-user">
<view class="user-avatar" :style="{backgroundColor: item.avatarColor}">{{ item.avatarText }}
</view>
<text class="user-name">{{ item.userName }}</text>
</view>
<text class="card-datetime">{{ item.dateTime }}</text>
</view>
</view>
<!-- 加载更多提示 -->
<view class="loading-more">
<view v-if="loading && !refreshing" class="loading-text">加载中...</view>
<view v-else-if="!hasMore && leaveRecords.length > 0" class="loading-text">没有更多数据了</view>
</view>
</scroll-view>
<!-- 已完成 -->
<view v-show="activeTab === 2" class="completed-tab">
<!-- 暂时留空 -->
</view>
<!-- 弹出层 -->
<u-popup v-model="showLeaveTypePopup" mode="bottom" border-radius="16">
<view class="popup-content">
<view class="popup-title">请假类型</view>
<view class="popup-options">
<view
v-for="(type, index) in leaveTypes"
:key="index"
class="popup-option"
:class="{ 'selected': leaveTypeIndex === index }"
@click="selectLeaveType(index)"
>
{{ type }}
</view>
</view>
<button class="popup-submit-btn" @click="confirmLeaveType">提交</button>
</view>
</u-popup>
</view>
</template>
<script>
export default {
data() {
return {
tabs: ['发起申请', '请假记录', '已完成'],
activeTab: 0,
leaveTypes: ['事假', '调休', '病假', '年假', '产假', '陪产假', '婚假', '例假', '丧假', '哺乳假', '其他'],
leaveTypeIndex: -1, // 初始化为-1表示未选择
startDate: '',
endDate: '',
hours: '',
reason: '',
selectedImages: [],
leaveRecords: [],
loading: false,
hasMore: true,
refreshing: false,
pageNum: 1,
pageSize: 10,
showLeaveTypePopup: false,
}
},
methods: {
changeTab(idx) {
this.activeTab = idx;
},
bindPickerChange(e) {
this.leaveTypeIndex = e.detail.value;
},
startDateChange(e) {
this.startDate = e.detail.value;
},
endDateChange(e) {
this.endDate = e.detail.value;
},
submitLeave() {
// 提交请假逻辑
},
deletePic(event) {
this.selectedImages.splice(event.index, 1);
},
onListChange(list) {
this.selectedImages = list;
},
goToDetail(item) {
// 跳转到详情页逻辑
},
async onRefresh() {
this.refreshing = true;
this.pageNum = 1;
await this.loadLeaveRecords();
this.refreshing = false;
},
async onLoadMore() {
if (!this.hasMore || this.loading) return;
this.pageNum++;
await this.loadLeaveRecords();
},
async loadLeaveRecords() {
this.loading = true;
let data = [
{
type: '请假',
leaveType: '病假',
status: '已通过',
statusClass: 'green',
startTime: '2025-07-13',
endTime: '2025-07-15',
userName: '余永乐',
avatarText: '余',
avatarColor: '#4B7BF5',
dateTime: '07-12 18:28:22'
},
{
type: '请假',
leaveType: '病假',
status: '待审核',
statusClass: 'orange',
startTime: '2025-07-13',
endTime: '2025-07-15',
userName: '余永乐',
avatarText: '余',
avatarColor: '#4B7BF5',
dateTime: '07-12 18:28:22'
}
];
const start = (this.pageNum - 1) * this.pageSize;
const end = start + this.pageSize;
const pageData = data.slice(start, end);
await new Promise(res => setTimeout(res, 300));
if (this.pageNum === 1) {
this.leaveRecords = pageData;
} else {
this.leaveRecords = [...this.leaveRecords, ...pageData];
}
this.hasMore = end < data.length;
this.loading = false;
},
openLeaveTypePopup() {
this.showLeaveTypePopup = true;
},
selectLeaveType(index) {
this.leaveTypeIndex = index;
},
confirmLeaveType() {
if (this.leaveTypeIndex !== -1) {
this.showLeaveTypePopup = false;
// 更新表单中的请假类型
this.leaveType = this.leaveTypes[this.leaveTypeIndex];
} else {
uni.showToast({
title: '请选择请假类型',
icon: 'none'
});
}
}
}
}
</script>
<style scoped>
.leave-container {
height: 100vh;
background: #f7f7f7;
display: flex;
flex-direction: column;
overflow: hidden;
}
.leave-tabs {
display: flex;
align-items: center;
justify-content: space-around;
background: #fff;
height: 80rpx;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
}
.leave-tab {
flex: 1;
text-align: center;
font-size: 32rpx;
color: #737373;
position: relative;
padding: 0 0 10rpx 0;
cursor: pointer;
}
.leave-tab.active {
color: #007CFF;
font-weight: bold;
}
.tab-underline {
width: 60rpx;
height: 6rpx;
background: #2186FF;
border-radius: 3rpx;
margin: 0 auto;
margin-top: 8rpx;
}
/* 让 leave-form 可滚动 */
.leave-form {
flex: 1;
padding: 20rpx;
background: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
overflow-y: auto;
}
.form-row {
margin-bottom: 34rpx;
display: flex;
flex-direction: column;
margin-left: 20rpx;
margin-right: 20rpx;
}
.form-row2 {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 34rpx;
margin-left: 20rpx;
margin-right: 20rpx;
margin-top: 20rpx;
}
.label {
font-size: 32rpx;
color: #000000;
font-weight: 600;
}
.required::after {
content: "*";
color: #DC9100;
margin-left: 10rpx;
}
.picker {
height: 73rpx;
width: auto;
background: #F7F7F7;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
margin-top: 20rpx;
margin-right: 40rpx;
}
.placeholder {
color: #808080;
}
.right-arrow {
width: 11rpx;
height: 21rpx;
}
input {
height: 73rpx;
width: auto;
background: #F7F7F7;
border-radius: 10rpx;
display: flex;
align-items: center;
padding-left: 20rpx;
margin-top: 20rpx;
}
textarea {
height: 200rpx;
width: auto;
background: #F7F7F7;
border-radius: 10rpx;
font-size: 24rpx;
color: #000000;
padding: 15rpx;
margin-top: 20rpx;
}
.leave-round {
width: 34rpx;
height: 34rpx;
border-radius: 17rpx;
background: #3370FF;
margin-left: 18rpx;
}
.name {
font-size: 24rpx;
color: #000000;
margin-left: 10rpx;
font-weight: 400;
}
.submit-btn {
width: 80vw;
height: 88rpx;
background: linear-gradient(90deg, #005DE9 0%, #4B9BFF 100%);
border-radius: 44rpx;
font-size: 32rpx;
color: #fff;
margin-top: 20rpx;
margin-bottom: 90rpx;
}
.leave-list-scroll {
flex: 1;
padding: 0 35rpx;
overflow-y: auto;
box-sizing: border-box;
margin-top: 20rpx;
}
.oa-card {
background: #fff;
border-radius: 10rpx;
margin-bottom: 26rpx;
padding: 20rpx 20rpx 20rpx 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.card-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 46rpx;
}
.card-type {
width: 126rpx;
height: 46rpx;
font-weight: 600;
font-size: 32rpx;
padding-left: 10rpx;
padding-top: 7rpx;
border-radius: 15rpx;
color: #fff;
background: linear-gradient(90deg, #007CFF 0%, #FFFFFF 100%);
}
.card-status-tag {
font-size: 28rpx;
}
.card-status-tag.orange {
color: #F27A0F;
}
.card-status-tag.green {
color: #0AC88F;
}
.card-status-tag.gray {
color: #8F8F8F;
background-color: rgba(143, 143, 143, 0.1);
border: 1px solid #8F8F8F;
}
.card-info {
font-size: 26rpx;
color: #333;
margin-bottom: 42rpx;
}
.card-leave-type {
color: #626262;
font-size: 28rpx;
margin-bottom: 24rpx;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-user {
display: flex;
align-items: center;
}
.user-avatar {
width: 54rpx;
height: 54rpx;
border-radius: 27rpx;
background-color: #688CFF;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
margin-right: 18rpx;
}
.user-name {
font-size: 24rpx;
color: #626262;
}
.card-datetime {
font-size: 24rpx;
color: #626262;
}
.loading-more {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
.upload-style{
margin-left: 15rpx;
margin-right: 15rpx;
}
.custom-leave-type-btn {
width: 100%;
height: 73rpx;
background: #F7F7F7;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
margin-top: 20rpx;
font-size: 32rpx;
color: #000;
}
.popup-content {
width: 100%;
background-color: #fff;
padding: 40rpx;
border-radius: 16rpx 16rpx 0 0;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.popup-title {
font-size: 36rpx;
color: #000;
text-align: center;
margin-bottom: 40rpx;
}
.popup-options {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.popup-option {
width: 30%;
height: 80rpx;
background-color: #f7f7f7;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.popup-option.selected {
background-color: #3370FF;
color: #fff;
}
.popup-submit-btn {
width: 80vw;
height: 88rpx;
background: linear-gradient(90deg, #005DE9 0%, #4B9BFF 100%);
border-radius: 44rpx;
font-size: 32rpx;
color: #fff;
margin-top: 40rpx;
}
</style>

View File

@@ -23,7 +23,12 @@
v-show="idx === activeTab"
class="oa-list-scroll"
scroll-y
:refresher-enabled="false">
:scroll-top="scrollTop[idx]"
:refresher-enabled="true"
:refresher-triggered="refreshing[idx]"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
@scroll="onScroll($event, idx)">
<view v-for="(item, index) in tabData[idx]" :key="index" class="oa-card" @click="goToDetail(item)">
<view class="card-row">
<view class="card-type">{{ item.type }}</view>
@@ -46,6 +51,11 @@
<text class="card-datetime">{{ item.dateTime }}</text>
</view>
</view>
<!-- 加载更多提示 -->
<view class="loading-more">
<view v-if="loading && !refreshing[idx]" class="loading-text">加载中...</view>
<view v-else-if="!hasMore[idx] && tabData[idx].length > 0" class="loading-text">没有更多数据了</view>
</view>
</scroll-view>
</view>
</view>
@@ -63,7 +73,12 @@
[] // 已发起
],
tabLoaded: [false, false, false], // 每个tab是否已加载
loading: false
loading: false,
pageNum: [1, 1, 1], // 每个tab的页码
pageSize: 10, // 每页条数
hasMore: [true, true, true], // 每个tab是否还有更多数据
refreshing: [false, false, false], // 每个tab是否正在刷新
scrollTop: [0, 0, 0] // 每个tab的滚动位置
}
},
computed: {
@@ -93,6 +108,32 @@
}
*/
},
// 监听滚动事件记录每个tab的滚动位置
onScroll(e, idx) {
this.scrollTop[idx] = e.detail.scrollTop;
},
// 下拉刷新
async onRefresh() {
// 设置当前tab为刷新状态
this.$set(this.refreshing, this.activeTab, true);
// 重置页码
this.$set(this.pageNum, this.activeTab, 1);
// 重新加载数据
await this.loadTabData(this.activeTab);
// 取消刷新状态
this.$set(this.refreshing, this.activeTab, false);
},
// 上拉加载更多
async onLoadMore() {
// 如果没有更多数据或正在加载,则不处理
if (!this.hasMore[this.activeTab] || this.loading) {
return;
}
// 页码加1
this.$set(this.pageNum, this.activeTab, this.pageNum[this.activeTab] + 1);
// 加载更多数据
await this.loadTabData(this.activeTab);
},
async loadTabData(idx) {
this.loading = true;
// 模拟接口请求不同tab返回不同mock数据
@@ -227,9 +268,25 @@
}
];
}
// 模拟分页效果
const start = (this.pageNum[idx] - 1) * this.pageSize;
const end = start + this.pageSize;
const pageData = data.slice(start, end);
// 模拟网络延迟
await new Promise(res => setTimeout(res, 300));
this.$set(this.tabData, idx, data);
if (this.pageNum[idx] === 1) {
// 刷新操作,替换数据
this.$set(this.tabData, idx, pageData);
} else {
// 加载更多,追加数据
this.tabData[idx] = [...this.tabData[idx], ...pageData];
}
// 判断是否还有更多数据
this.$set(this.hasMore, idx, end < data.length);
this.$set(this.tabLoaded, idx, true);
this.loading = false;
},
@@ -334,6 +391,7 @@
.oa-list-container {
flex: 1;
position: relative;
margin-top: 20rpx;
}
.oa-list-scroll {
@@ -344,8 +402,6 @@
bottom: 0;
padding: 0 35rpx;
overflow-y: auto;
padding-bottom: 50rpx;
height: calc(100vh - 80rpx - 32rpx - 150rpx); /* 减去顶部区域高度 */
box-sizing: border-box;
}
@@ -354,7 +410,6 @@
padding: 0 35rpx;
flex: 1;
overflow-y: auto;
padding-bottom: 50rpx;
}
.oa-card {
@@ -448,6 +503,16 @@
font-size: 24rpx;
color: #626262;
}
.loading-more {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
</style>

View File

@@ -148,9 +148,13 @@ export default {
this.isManager = this.vuex_user.roles[0].roleId == 1
},
onShow() {
uni.$once('refreshData', () => {
uni.$on('refreshData', () => {
this.loadAllTabsData();
});
},
// 页面卸载时移除事件监听器
onUnload() {
uni.$off('refreshData');
},
methods: {
async changeTab(idx) {

View File

@@ -48,17 +48,7 @@
data() {
return {
commonApps: [
// {
// icon: 'https://picsum.photos/80/80?random=3',
// text: '工作巡检',
// url:'/pages/sys/workbench/inspection/inspection'
// },
// {
// icon: 'https://picsum.photos/80/80?random=3',
// text: '假勤',
// url:'/pages/sys/workbench/camera'
// },
{
icon: '/static/aaaa_gd.png',
text: '工单',
@@ -83,11 +73,26 @@
icon: '/static/aaaa_bsbx.png',
text: '报事报修',
url:'/pages/sys/user/myRepair/myRepair'
}
},
{
icon: 'https://picsum.photos/80/80?random=3',
text: '工作巡检',
url:'/pages/sys/workbench/inspection/inspection'
},
// {
// icon: 'https://picsum.photos/80/80?random=3',
// text: '通讯录',
// url:'/pages/sys/workbench/book/book'
// },
// {
// icon: 'https://picsum.photos/80/80?random=3',
// text: '请假',
// url:'/pages/sys/workbench/leave/leave'
// },
// {
// icon: 'https://picsum.photos/80/80?random=3',
// text: '会议',
// url:'/pages/sys/workbench/meet/meet'
// text: 'oa',
// url:'/pages/sys/workbench/oa/oa'
// },
// {
// icon: 'https://picsum.photos/80/80?random=3',

BIN
static/ic_camera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
static/ic_dialog_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

BIN
static/ic_exp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/ic_sq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B