19 Commits
new ... master

Author SHA1 Message Date
9da52525ac 图片显示修改
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-16 09:14:44 +08:00
70d7b2081c 1
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-15 16:09:29 +08:00
86cfe5a464 1.登录页面修改
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-15 16:02:46 +08:00
2476011b07 推送
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-14 18:14:36 +08:00
e6772a53e7 1
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-14 17:45:05 +08:00
a6245b29cd 消息推送
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-14 17:43:16 +08:00
57fe929080 请假 巡检
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-12 09:24:59 +08:00
9ed827a227 屏蔽巡检
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-10 13:55:35 +08:00
3b097ae838 首页修改
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-10 13:42:07 +08:00
a0e9c96897 首页修改
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-10 13:34:37 +08:00
400e95c81b 首页活动,报事报修评价
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-09 17:21:35 +08:00
fb02715afb 去掉报事报修详情 操作按钮
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-09 09:38:34 +08:00
f541c0f191 首页
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-08 16:24:59 +08:00
2e604b1823 巡检
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-08 11:30:52 +08:00
60aa1f09fa 巡检
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-08 10:28:27 +08:00
3718586e12 1.报事报修 2025-09-06 16:37:22 +08:00
7c0a09d534 巡检
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-05 17:09:49 +08:00
c7ff9a5234 1.
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-05 16:54:53 +08:00
8e23e63b3a unipush
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
2025-09-04 11:41:25 +08:00
50 changed files with 5348 additions and 1514 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

156
App.vue
View File

@@ -1,29 +1,145 @@
<script>
/**
* Copyright (c) 2013-Now http://aidex.vip All rights reserved.
*/
export default {
onLaunch() {
// 国际化,设置当前语言
if (this.vuex_locale){
this.$i18n.locale = this.vuex_locale;
this.$u.api.lang({lang: this.vuex_locale});
import config from "./common/config";
import {
showNotification
} from "@/utils/notify.js"
import ws from "@/utils/websocket.js"; // WebSocket 封装类
import NotifyPermission from "@/utils/notification-permission.js"
/**
* Copyright (c) 2013-Now http://aidex.vip All rights reserved.
*/
export default {
onLaunch() {
// 国际化,设置当前语言
if (this.vuex_locale) {
this.$i18n.locale = this.vuex_locale;
this.$u.api.lang({
lang: this.vuex_locale
});
}
// 监听登录成功
uni.$on("loginSuccess", () => {
this.initWebSocket()
NotifyPermission.ensurePermission(() => {
})
// // 仅在 Android 13+ 才需要
// if (plus.os.name === "Android" && parseInt(plus.os.version) >= 13) {
// var permission = "android.permission.POST_NOTIFICATIONS";
// // 请求权限
// plus.android.requestPermissions(
// [permission],
// (resultObj) => {
// this.initWebSocket()
// },
// (error) => {
// }
// );
// } else {
// this.initWebSocket()
// }
})
//只有在基座运行的情况下才能打印看到
const clientInfo = plus.push.getClientInfo()
console.log('clientid:', clientInfo.clientid)
this.$store.commit('$uStore', {
name: 'vuex_push_clientId',
value: clientInfo.clientid
});
//监听系统通知栏点击事件
plus.push.addEventListener("click", function(msg) {
console.log("用户点击了推送消息:", msg);
if (msg.payload) {
if (msg.payload.type == 100) {
uni.navigateTo({
url: "/pages/sys/workbench/earlyWarning/warnDetail?item=" + msg.payload.data,
});
} else if (msg.payload.type == 200) {
uni.navigateTo({
url: "/pages/sys/workbench/order/orderDetail?item=" + msg.payload.data,
});
}
}
});
// 收到推送(前台透传)
// plus.push.addEventListener("receive", function(msg) {
// console.log("收到推送消息:", msg);
// });
// 设置底部导航栏角标
// uni.setTabBarBadge({
// index: 0,
// text: '3'
// });
// uni.removeTabBarBadge({
// index: 0
// });
},
methods: {
initWebSocket() {
let url = this.vuex_config.baseUrl +
'/resource/websocket?clientid=dab457a1ea14411787c240db05bb0832&Authorization=Bearer ' + this
.vuex_token;
console.log('t1', url)
ws.connect(url)
ws.setDispatchHandler(this.handleWsMessage)
// this.ws = uni.connectSocket({
// url: url, // 鉴权
// success: () => console.log("连接请求已发送")
// })
// this.ws.onOpen(() => {
// console.log("WebSocket已连接")
// })
// this.ws.onMessage((res) => {
// console.log('t1', 1111111111)
// this.handleWsMessage(res.data)
// })
// this.ws.onClose(() => {
// console.log("WebSocket已关闭")
// })
// this.ws.onError((err) => {
// console.error("WebSocket错误", err)
// })
},
handleWsMessage(data) {
if (data != 'ping') {
try {
showNotification(data.title, data.content, data)
// const msg = JSON.parse(data)
// console.log("收到消息:", msg)
// uni.$emit("wsMessage", msg) // 分发消息
} catch (e) {
console.error("消息解析失败:", data)
}
}
},
closeWebSocket() {
if (this.ws) {
this.ws.close()
}
}
}
// 设置底部导航栏角标
// uni.setTabBarBadge({
// index: 0,
// text: '3'
// });
// uni.removeTabBarBadge({
// index: 0
// });
}
}
</script>
<style>
@import url("~@/static/iconfont/iconfont.css");
</style>
<style lang="scss">
@import "uview-ui/index.scss";
@import "pages/common/aidex.scss";
@import "uview-ui/index.scss";
@import "pages/common/aidex.scss";
</style>

View File

@@ -30,5 +30,7 @@ const config = {
config.baseUrl = 'http://183.230.235.66:11010/api';
// config.baseUrl = 'http://378a061a.r28.cpolar.top'
// config.baseUrl = 'http://3efb1a71.r28.cpolar.top';
config.imageUrl = 'http://183.230.235.66:11010';
export default config;

View File

@@ -13,6 +13,11 @@ const install = (Vue, vm) => {
login: (params = {}) => vm.$u.post(config.adminPath+'/auth/login', params),
getUserInfo: (params = {}) => vm.$u.get(config.adminPath+'/system/user/profile', params),
//首页公告
getNotices:(params = {})=>vm.$u.get(config.adminPath+'/property/mobile/notices/todayList', params),
//首页活动
getActivityList:(params = {})=>vm.$u.get(config.adminPath+'/property/mobile/activity/list', params),
//工作台列表
getFunList:(params = {})=>vm.$u.get(config.adminPath+'/system/funList/list', params),
//我的访客列表
@@ -26,9 +31,16 @@ const install = (Vue, vm) => {
getOrderList2:(params = {})=>vm.$u.get(config.adminPath+'/property/mobile/workOrders/list',params),
//订单类型
getOrdersType:(params = {})=>vm.$u.get(config.adminPath+'/property/workOrdersType/list',params),
getOrdersType:(params = {})=>vm.$u.get(config.adminPath+'/property/workOrdersType/list',params),
//报事报修订单类型
getRepairTypes:(params = {})=>vm.$u.get(config.adminPath+'/property/workOrdersType/queryList',params),
//报事报修订单类型
getRepairTypes2:(params = {})=>vm.$u.get(config.adminPath+'/property/mobile/workOrders/typeTree',params),
//工单详情
getRepairDetail:(params = {}, id) => vm.$u.get(config.adminPath+`/property/workOrders/{id}`,params),
//新增订单
addOrder:(params = {})=>vm.$u.post(config.adminPath+'/property/workOrders',params),
//新增报事报修
@@ -64,7 +76,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

@@ -26,6 +26,7 @@ const install = (Vue, vm) => {
}
req.header["source"] = "uniapp";
req.header["clientId"] = "dab457a1ea14411787c240db05bb0832"
req.header["pushClientId"] = vm.vuex_push_clientId
// 默认指定返回 JSON 数据
if (!req.header[ajaxHeader]){
req.header[ajaxHeader] = 'json';
@@ -52,6 +53,15 @@ const install = (Vue, vm) => {
vm.$u.toast('未连接到服务器')
return false;
}
if(data.code == 401 && req.url.indexOf('/system/user/profile') === -1){
// 清除用户信息并返回登录页
vm.$u.vuex('vuex_user', {});
vm.$u.vuex('vuex_token', '');
uni.redirectTo({
url: '/pages/sys/login/login'
});
return false
}
if (typeof data === 'object' && !(data instanceof Array)){
if (data.token){

View File

@@ -0,0 +1,314 @@
<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: false } // 控制是否允许上下滑切换
},
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(newMode, oldMode) {
this.generateRenderDays()
// 通知父组件mode已改变
this.$emit('modeChange', newMode);
},
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-- }
// 切换后的基准日期设为该月1号
this.anchorDate = new Date(year, m - 1, 1)
this.selected = this.formatDate(this.anchorDate) // ✅ 选中当月1号
this.$emit('dateChange', this.selected)
} else {
// 以当前 anchorDate 为基准移动周
const d = this.safeDate(this.anchorDate)
d.setDate(d.getDate() + direction * 7)
// 更新基准周
this.anchorDate = this.startOfWeek(d)
// ✅ 选中该周周日startOfWeek返回的就是周日
this.selected = this.formatDate(this.anchorDate)
this.$emit('dateChange', this.selected)
}
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

@@ -0,0 +1,140 @@
<template>
<view>
<view v-if="visible" class="mask" @click="close"></view>
<!-- 底部弹窗 -->
<view class="popup" v-if="visible">
<!-- 弹窗标题 -->
<view class="dialog-title">选择时间</view>
<!-- 日期显示区域 -->
<view class="date-display">
{{ selectedDate }}
</view>
<!-- 日历组件 -->
<view class="calendar-wrapper">
<CommonCalendar :initial-date="selectedDate" @dateChange="handleDateSelected" />
</view>
<!-- 确认按钮 -->
<view class="confirm-btn" @click="confirmSelection">
确认
</view>
</view>
</view>
</template>
<script>
import CommonCalendar from '@/components/CommonCalendar.vue'
export default {
components: {
CommonCalendar
},
name: "SelectCalendarDialog",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
selectedDate: this.getCurrentDate(), // 默认选中的日期
showCalendar: true // 控制日历是否显示
};
},
methods: {
close() {
this.$emit('update:visible', false);
},
// 获取当前日期
getCurrentDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 处理日期选择
handleDateSelected(date) {
this.selectedDate = date;
// 不再需要手动关闭日历,因为日历会一直显示在弹窗中
},
// 确认选择
confirmSelection() {
this.$emit('confirm', this.selectedDate);
this.close()
}
}
};
</script>
<style scoped>
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 998;
}
.popup {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
max-height: 80%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 999;
animation: slideUp 0.3s ease;
}
.dialog-title {
font-size: 36rpx;
color: #333;
margin-bottom: 20rpx;
margin-top: 45rpx;
text-align: center;
}
.date-display {
width: 100%;
height: 73rpx;
line-height: 73rpx;
text-align: start;
background: #EBF5FF;
border-radius: 10rpx;
font-size: 28rpx;
color: #0B0B0B;
margin-bottom: 6rpx;
margin-left: 35rpx;
margin-right: 35rpx;
padding-left: 35rpx;
}
.calendar-wrapper {
width: 100%;
margin-bottom: 89rpx;
}
.confirm-btn {
height: 88rpx;
line-height: 88rpx;
text-align: center;
background-color: #0090FF;
color: white;
border-radius: 44rpx;
font-size: 36rpx;
margin-left: 65rpx;
margin-right: 65rpx;
margin-bottom: 40rpx;
}
</style>

View File

@@ -13,7 +13,8 @@
<view class="search-bar">
<image class="search-icon" src="/static/ic_search_gray.png" />
<input class="search-input" placeholder="姓名、工号" />
<input class="search-input" placeholder="姓名" v-model="keyword" @confirm="handleSearch" confirm-type="search" />
<view class="search-btn" @click="handleSearch">搜索</view>
</view>
@@ -58,13 +59,18 @@
data() {
return {
keyword: '',
selected: []
selected: [],
allList: [], // 存放所有数据
filteredList: []
}
},
computed: {
filteredList() {
if (!this.keyword) return this.list;
return this.list.filter(item => item.name.includes(this.keyword) || item.value.includes(this.keyword));
watch: {
list: {
handler(newVal) {
this.allList = [...newVal];
this.filteredList = [...newVal];
},
immediate: true
}
},
methods: {
@@ -89,7 +95,18 @@
this.$emit('confirm', this.selected);
this.close();
},
onSearch() {}
// 搜索方法
handleSearch() {
if (!this.keyword) {
this.filteredList = [...this.allList];
return;
}
this.filteredList = this.allList.filter(item =>
item.name.includes(this.keyword) ||
(item.department && item.department.includes(this.keyword))
);
}
}
}
</script>
@@ -168,6 +185,16 @@
font-size: 26rpx;
flex: 1;
color: #000;
background: transparent;
}
.search-btn {
font-size: 26rpx;
color: #0090FF;
padding: 0 20rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.user-list {

115
components/punchInfo.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<!-- 上班 -->
<view class="timeline-item">
<view class="timeline-dot"></view>
<view class="timeline-content">
<text class="timeline-label">应上班 09:00</text>
<view class="clock-card late">
<text class="clock-text">已打卡 08:48:32</text>
<text class="status orange">迟到</text>
<text class="buka" @click="goBuka">申请补卡</text>
<view class="location">
<text class="icon">📍</text>
<text class="loc-text">某综合服务中心1栋</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default{
props:{
info:{}
},
methods:{
goBuka(){
uni.navigateTo({
url: '/pages/sys/user/myRecord/cardReplacement'
});
}
}
}
</script>
<style>
.timeline-item {
position: relative;
padding-left: 20rpx;
margin-left: 60rpx;
margin-right: 90rpx;
margin-bottom: 40rpx;
border-left: 2rpx solid #e0e0e0; /* 竖线 */
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -10rpx;
top: -10rpx;
width: 16rpx;
height: 16rpx;
background-color: #666;
border-radius: 50%;
}
.timeline-content {
margin-left: 20rpx;
}
.timeline-label {
font-size: 28rpx;
color: #737373;
}
/* 打卡卡片 */
.clock-card {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-top: 23rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.clock-text {
font-size: 34rpx;
color: #000000;
font-weight: 500;
}
.status {
margin-left: 20rpx;
font-size: 28rpx;
}
.status.orange {
color: #ff6b35;
}
.buka {
margin-left: 20rpx;
font-size: 28rpx;
color: #3370FF;
}
/* 地点 */
.location {
display: flex;
align-items: center;
margin-top: 12rpx;
}
.icon {
margin-right: 6rpx;
}
.loc-text {
font-size: 28rpx;
color: #1C9BFF;
}
</style>

View File

@@ -3,7 +3,9 @@
*/
import Vue from 'vue';
import App from './App';
import ws from '@/utils/websocket.js'
Vue.prototype.$ws = ws
Vue.config.productionTip = false;
App.mpType = 'app';

View File

@@ -1,6 +1,6 @@
{
"name" : "数字南川",
"appid" : "__UNI__7AF1078",
"appid" : "__UNI__DA9B8DE",
"description" : "",
"versionName" : "1.8.4",
"versionCode" : "100",
@@ -28,7 +28,8 @@
"modules" : {
"Payment" : {},
"Camera" : {},
"Barcode" : {}
"Barcode" : {},
"Push" : {}
},
"distribute" : {
"android" : {
@@ -53,7 +54,8 @@
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NOTIFICATION_POLICY\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
},
@@ -72,7 +74,8 @@
"UniversalLinks" : ""
},
"appleiap" : {}
}
},
"push" : {}
},
"icons" : {
"android" : {

View File

@@ -352,6 +352,18 @@
"navigationBarTitleText": "新增报事报修"
}
},
{
"path": "pages/sys/user/myRepair/repairDetail",
"style": {
"navigationBarTitleText": "报事详情"
}
},
{
"path": "pages/sys/user/myRepair/addSuc",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/sys/user/myRepair/selectLocation",
"style": {
@@ -373,7 +385,13 @@
{
"path": "pages/sys/user/myRecord/myRecord",
"style": {
"navigationStyle": "custom"
"navigationBarTitleText": "我的考勤"
}
},
{
"path": "pages/sys/user/myRecord/cardReplacement",
"style": {
"navigationBarTitleText": "补卡申请"
}
},
{
@@ -479,6 +497,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

@@ -38,27 +38,32 @@
</view>
<!-- 滚动资讯条 -->
<view class="news-bar">
<text class="news-label">头条</text>
<scroll-view scroll-x class="news-scroll">
<text v-for="(item, idx) in newsList" :key="idx" class="news-item">{{ item }}</text>
</scroll-view>
<image class="news-label" src="/static/ic_home_tt.png"></image>
<view class="news-scroll-container">
<scroll-view scroll-y class="news-scroll" :scroll-top="scrollTop">
<view class="news-list">
<!-- 重复列表以实现无缝循环 -->
<text v-for="(item, idx) in [...newsList, ...newsList]" :key="idx" class="news-item" @click="goNoticeDetail(item)">{{ item.title }}</text>
</view>
</scroll-view>
</view>
<text class="news-arrow"></text>
</view>
<!-- 热门活动区 -->
<view class="hot-section">
<view class="hot-title-row">
<text class="hot-title">热门活动</text>
<text class="hot-more">全部热门活动 ></text>
<text v-if="false" class="hot-more">全部热门活动 ></text>
</view>
<view class="hot-list">
<view class="hot-card" v-for="(item, idx) in hotList" :key="idx">
<image :src="item.img" class="hot-img" mode="aspectFill" />
<view class="hot-card" v-for="(item, idx) in hotList" :key="idx" @click="goActivityDetail(item)">
<image :src="item.activityImgUrl" class="hot-img" mode="aspectFill" />
<view class="hot-info">
<text class="hot-tag">#热门活动</text>
<text class="hot-desc">{{ item.title }}</text>
<view class="hot-meta">
<text class="hot-date">{{ item.date }}</text>
<text class="hot-status">进行中</text>
<text class="hot-date">{{ item.startTime }}</text>
<text class="hot-status">{{ getStatusText(item.status) }}</text>
</view>
</view>
</view>
@@ -69,6 +74,8 @@
</template>
<script>
import toast from '../../../uview-ui/libs/function/toast';
export default {
name: 'Home',
data() {
@@ -104,64 +111,99 @@
text: '报事报修',
url:'/pages/sys/user/myRepair/myRepair'
},
// {
// icon: '/static/aaa_bsbx.png',
// text: '报事报修',
// url:'/pages/sys/user/myRepair/myRepair'
// },
// {
// icon: '/static/aaa_tcjf.png',
// text: '停车缴费',
// url:'/pages/sys/user/myPayment/myPayment'
// },
// {
// icon: '/static/aaa_shfw.png',
// text: '生活服务'
// },
// {
// icon: '/static/aaa_fwzx.png',
// text: '服务中心',
// url:'/pages/sys/user/serviceCenter/serviceCenter'
// },
// {
// icon: '/static/aaa_hyyy.png',
// text: '会议预约',
// url:'/pages/sys/workbench/meet/meet'
// },
// {
// icon: '/static/aaa_gdgl.png',
// text: '工单管理',
// url:'/pages/sys/workbench/order/order'
// },
// {
// icon: '/static/aaa_fkgl.png',
// text: '访客管理',
// url:'/pages/sys/user/myVisitor/myVisitor'
// },
// {
// icon: '/static/aaa_jqqd.png',
// text: '敬请期待'
// }
],
newsList: [
'数智南川|最新资讯1',
'数智南川|最新资讯2',
'数智南川|最新资讯3'
],
hotList: [{
img: '/static/aaa_hd1.png',
title: '世界骑行日 低碳出行 让城市更美好',
date: '2025-07-03'
},
{
img: '/static/aaa_hd2.png',
title: '仲夏之夜低碳出行·绿色生活让城市更美好',
date: '2025-07-03'
}
scrollTop: 0,
scrollInterval: null,
scrollHeight: 0,
hotList: [
]
}
},
onLoad() {
this.getNotices()
this.getActivityList()
},
methods: {
// 获取状态文字描述
getStatusText(status) {
const statusMap = {
'1': '待进行',
'2': '进行中',
'3': '已完成'
};
return statusMap[status] || '未知状态';
},
async getNotices() {
let res = await this.$u.api.getNotices();
if (res.code == '200') {
this.newsList = res.rows
}
},
async getActivityList() {
let res = await this.$u.api.getActivityList();
if (res.code == '200') {
this.hotList = res.rows;
// 处理图片链接
await this.getImageUrl();
}
},
async getImageUrl() {
// 收集所有需要获取链接的图片ID
const imgIds = [];
const imgIdToIndexMap = []; // 保存图片ID对应的hotList索引
for (let i = 0; i < this.hotList.length; i++) {
const item = this.hotList[i];
if (item.activityImgUrl) {
imgIds.push(item.activityImgUrl);
imgIdToIndexMap.push({
id: item.activityImgUrl,
index: i
});
}
}
// 如果有图片ID需要处理则调用API一次获取所有图片链接
if (imgIds.length > 0) {
try {
const imgRes = await this.$u.api.getImageUrl({}, imgIds.join(','));
if (imgRes.code == 200 && imgRes.data) {
// 将返回的图片URL映射回对应的hotList项
imgRes.data.forEach(imgItem => {
const mapping = imgIdToIndexMap.find(m => m.id === imgItem.ossId);
if (mapping) {
this.$set(this.hotList[mapping.index], 'activityImgUrl', this.vuex_config.imageUrl+imgItem.url);
}
});
}
} catch (error) {
console.error('获取图片链接失败:', error);
}
}
},
goNoticeDetail(item){
let params = {}
params.title = item.title
params.content = item.afficheContent
params.time = item.startTime
const itemStr = encodeURIComponent(JSON.stringify(params));
uni.navigateTo({ url: '/pages/sys/user/serviceCenter/questionDetail?item=' + itemStr });
},
goActivityDetail(item){
let params = {}
params.title = item.title
params.content = item.activityContent
params.time = item.startTime
params.img = item.activityImgUrl
const itemStr = encodeURIComponent(JSON.stringify(params));
uni.navigateTo({ url: '/pages/sys/user/serviceCenter/questionDetail?item=' + itemStr });
},
onBannerChange(e) {
this.current = e.detail.current;
},
@@ -194,6 +236,40 @@
uni.navigateTo({
url: url
});
},
// 资讯跑马灯控制
startScroll() {
// 清除之前的定时器
if (this.scrollInterval) {
clearInterval(this.scrollInterval);
}
// 设置新的定时器
this.scrollHeight = this.newsList.length * 60;
this.scrollTop = 0;
this.scrollInterval = setInterval(() => {
this.scrollTop++;
// 当滚动到一半时重置位置,实现无缝循环
if (this.scrollTop >= this.scrollHeight) {
this.scrollTop = 0;
}
}, 50);
},
onScroll(e) {
// 滚动事件处理
}
},
mounted() {
// 页面加载完成后启动跑马灯
this.$nextTick(() => {
this.startScroll();
});
},
beforeDestroy() {
// 页面销毁前清除定时器
if (this.scrollInterval) {
clearInterval(this.scrollInterval);
}
}
}
@@ -360,36 +436,47 @@
.news-bar {
display: flex;
align-items: center;
padding: 10rpx 0;
padding: 40rpx 0;
border-top: 1rpx solid #f5f5f5;
margin-top: 10rpx;
height: 60rpx;
}
.news-label {
color: #FF6A00;
font-size: 28rpx;
font-weight: bold;
margin-right: 16rpx;
border: 1rpx solid #FF6A00;
border-radius: 4rpx;
padding: 2rpx 6rpx;
width: 54rpx;
height: 27rpx;
margin-left: 15rpx;
margin-right: 15rpx;
}
.news-scroll-container {
flex: 1;
height: 60rpx;
overflow: hidden;
}
.news-scroll {
flex: 1;
height: 60rpx;
white-space: nowrap;
}
.news-list {
display: flex;
flex-direction: column;
}
.news-item {
display: inline-block;
margin-right: 30rpx;
height: 60rpx;
line-height: 60rpx;
color: #666;
font-size: 24rpx;
flex-shrink: 0;
}
.news-arrow {
color: #999;
font-size: 32rpx;
flex-shrink: 0;
}
.hot-section {

View File

@@ -10,12 +10,12 @@
<view class="login-form">
<view class="input-row">
<image class="iconfont" src="/static/ic_login_phone.png" />
<input class="login-input" type="text" placeholder="输入手机号" v-model="username" />
<input class="login-input" type="text" placeholder="输入号" v-model="username" />
</view>
<view class="input-row">
<image class="iconfont2" src="/static/ic_login_code.png" />
<input class="login-input" type="text" placeholder="请输入验证码" v-model="password"/>
<button class="code-btn">获取校验码</button>
<input class="login-input" type="text" placeholder="请输入码" v-model="password" />
<!-- <button class="code-btn">获取校验码</button> -->
</view>
<view class="protocol-row">
<label class="custom-checkbox-label">
@@ -39,9 +39,11 @@
export default {
data() {
return {
// username: 'admin',
// password: 'admin123',
phoneNo: '',
username: 'admin',
password: 'admin123',
username: '',
password: '',
loginType: 'currentPhone',
showPassword: false,
remember: true,
@@ -63,6 +65,10 @@
},
methods: {
async submit() {
if(!this.checked){
this.$u.toast('请先同意用户协议和隐私政策');
return;
}
if (this.username.length == 0) {
this.$u.toast('请输入账号');
return;
@@ -88,19 +94,22 @@
});
this.getUserInfo()
// setTimeout(() => {
// uni.reLaunch({
// url: '/pages/sys/home/home'
// });
// uni.reLaunch({
// url: '/pages/sys/home/home'
// });
// }, 500);
}
},
getUserInfo(){
this.$u.api.getUserInfo({loginCheck: true}).then(res => {
if (res.code == '200'){
getUserInfo() {
this.$u.api.getUserInfo({
loginCheck: true
}).then(res => {
if (res.code == '200') {
this.$store.commit('$uStore', {
name: 'vuex_user',
value: res.data.user
});
uni.$emit("loginSuccess");
uni.reLaunch({
url: '/pages/sys/home/home'
});
@@ -181,8 +190,8 @@
.iconfont {
font-size: 32rpx;
margin-right: 16rpx;
width: 27rpx;
height: 43rpx;
width: 35rpx;
height: 35rpx;
}
.iconfont2 {

View File

@@ -19,7 +19,7 @@
<!-- 白色圆角面板 -->
<view class="mine-panel">
<view class="mine-list">
<view class="mine-list-item" v-for="(item, idx) in list" :key="idx" @click="handleItemClick(idx)">
<view class="mine-list-item" v-for="(item, idx) in list" :key="idx" @click="handleItemClick(item.id)">
<image class="mine-list-icon" :src="item.icon" />
<text class="mine-list-text">{{ item.text }}</text>
<text v-if="item.extra" class="mine-list-extra">{{ item.extra }}</text>
@@ -32,6 +32,7 @@
</template>
<script>
import ws from "@/utils/websocket.js"; // WebSocket 封装类
export default {
name: 'Mine',
data() {
@@ -40,36 +41,45 @@
nickname: '',
phone:''
},
list: [{
icon: '/static/ic_mine_info.png',
text: '我的信息'
},
{
icon: '/static/ic_mine_pay.png',
text: '我的缴费'
},
{
icon: '/static/ic_mine_repair.png',
text: '我的报修'
},
{
icon: '/static/ic_mine_visitor.png',
text: '我的访客'
},
list: [
// {
// id:0,
// icon: '/static/ic_mine_info.png',
// text: '我的信息'
// },
// {
// id:1,
// icon: '/static/ic_mine_pay.png',
// text: '我的缴费'
// },
// {
// id:2,
// icon: '/static/ic_mine_repair.png',
// text: '我的报修'
// },
// {
// id:3,
// icon: '/static/ic_mine_visitor.png',
// text: '我的访客'
// },
{
id:4,
icon: '/static/ic_mine_check.png',
text: '我的考勤'
},
{
id:5,
icon: '/static/ic_mine_pwd.png',
text: '修改密码'
},
{
id:6,
icon: '/static/ic_mine_version.png',
text: '系统版本',
extra: 'v1.00.01'
},
{
id:7,
icon: '/static/ic_mine_setting2.png',
text: '设置'
}
@@ -110,6 +120,8 @@
uni.navigateTo({
url: '/pages/sys/user/myRecord/myRecord'
});
}else if(idx === 7){
// ws.send({"data":"{\"bigType\":10,\"createBy\":-1,\"createDept\":103,\"createTime\":\"2025-09-13 16:40:43\",\"description\":\"非法停车\",\"deviceGroupId\":1961274194171736066,\"deviceIp\":\"192.168.24.33\",\"deviceName\":\"2号岗亭监控IP33\",\"id\":1966784089537634305,\"level\":2,\"params\":{},\"reportTime\":\"2025-09-13 16:40:43\",\"servBeginTime\":\"2025-09-13 16:40:43\",\"servEndTime\":\"2025-09-13 18:40:43\",\"smallType\":1028,\"state\":10,\"tenantId\":\"000000\",\"updateTime\":\"2025-09-13 16:40:43\"}","type":"100","title":"预警消息","content":"预警内功"})
}
},
logout() {

View File

@@ -0,0 +1,420 @@
<template>
<view class="page-container">
<!-- 表单区域 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">补卡时间</text>
<picker mode="date" :value="form.date" @change="onDateChange">
<view class="picker">{{ form.date }} {{ form.time }}</view>
</picker>
</view>
<view class="tip-text">
<text>本月已补卡{{ usedTimes }}剩余{{ remainTimes }}</text>
</view>
<view class="form-item">
<text class="form-label">补卡说明</text>
<textarea v-model="form.remark" placeholder="请输入" class="textarea" />
</view>
<view class="form-item">
<text class="form-label">附件</text>
<!-- 如果没有图片显示上传按钮 -->
<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>
<!-- 审批记录 -->
<view class="record-section">
<text class="section-title">审批记录</text>
<view class="timeline">
<view v-for="(item, index) in records" :key="index" class="timeline-item">
<!-- -->
<view class="timeline-dot"></view>
<!-- 节点内容 -->
<view class="timeline-content">
<text class="end-text">{{ item.node }}</text>
<view class="record-row" v-if="item.status !== '结束'">
<image class="avatar" :src="item.avatar" mode="aspectFill" />
<view class="name-node-container">
<text class="name">{{ item.name }}</text>
<text class="status" :class="statusClass(item.status)">{{ item.status }}</text>
</view>
<text class="time2">{{ item.time }}</text>
</view>
<text v-else class="time">{{ item.time }}</text>
</view>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="footer">
<button class="submit-btn" type="primary" @click="submit">提交</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
usedTimes: 0,
remainTimes: 5,
selectedImage: '',
form: {
date: '2025-07-13',
time: '09:00',
remark: ''
},
records: [{
node: '提交',
name: '于永乐',
status: '已提交',
time: '07-12 18:28:22',
avatar: '/static/avatar1.png'
},
{
node: '审批',
name: '张桂花',
status: '已同意',
time: '07-12 18:30:12',
avatar: '/static/avatar2.png'
},
{
node: '审批',
name: '张桂花',
status: '已拒绝',
time: '07-12 19:12:45',
avatar: '/static/avatar2.png'
},
{
node: '结束',
name: '',
status: '结束',
time: '07-12 20:00:00',
avatar: ''
}
]
}
},
methods: {
onDateChange(e) {
this.form.date = e.detail.value
},
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 = ''
},
submit() {
uni.showToast({
title: '提交成功',
icon: 'success'
})
},
statusClass(status) {
if (status === '已提交') return 'blue'
if (status === '已同意') return 'green'
if (status === '已拒绝') return 'orange'
if (status === '结束') return 'gray'
return ''
}
}
}
</script>
<style scoped>
.page-container {
background: #f5f5f5;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.form-section {
background: #fff;
margin: 20rpx;
padding: 20rpx;
border-radius: 12rpx;
}
.form-item {
margin-bottom: 20rpx;
}
.form-label {
font-size: 24rpx;
color: #737373;
margin-bottom: 10rpx;
display: block;
}
.tip-text {
height: 73rpx;
line-height: 73rpx;
background: #F7F7F7;
color: #808080;
font-size: 24rpx;
border-radius: 10rpx;
padding-left: 16rpx;
}
.textarea {
width: 100%;
height: 160rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
padding: 12rpx;
font-size: 24rpx;
}
.upload-box {
border: 2rpx dashed #aaa;
border-radius: 12rpx;
padding: 40rpx;
text-align: center;
color: #888;
}
.upload-icon {
width: 60rpx;
height: 60rpx;
margin-bottom: 12rpx;
}
/* 审批记录 */
.record-section {
background: #fff;
border-radius: 25rpx;
margin-left: 32rpx;
margin-right: 32rpx;
padding: 35rpx 25rpx 35rpx 25rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
/* timeline 方法二:每个节点自己画线 */
.timeline {
position: relative;
margin-left: 12rpx;
margin-top: 45rpx;
}
.timeline-item {
position: relative;
padding-left: 40rpx;
padding-bottom: 50rpx;
}
.timeline-item::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: -2rpx;
width: 2rpx;
background: #ddd;
}
/* 第一个节点:去掉上半段 */
.timeline-item:first-child::before {
top: 20rpx;
}
/* 最后一个节点:去掉下半段 */
.timeline-item:last-child::before {
bottom: auto;
height: 20rpx;
}
/* 状态点 */
.timeline-dot {
position: absolute;
left: -12rpx;
top: 10rpx;
width: 20rpx;
height: 20rpx;
border: 1rpx solid #10AF7F;
border-radius: 50%;
background: #fff;
}
.timeline-content {
margin-left: -20rpx;
}
.record-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.avatar {
width: 54rpx;
height: 54rpx;
border-radius: 50%;
margin-right: 15rpx;
background: #688CFF;
}
.name-node-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.name {
font-size: 28rpx;
color: #000;
}
.status {
font-size: 22rpx;
font-weight: 500;
}
.status.blue {
color: #007aff;
}
.status.green {
color: #4caf50;
}
.status.orange {
color: #ff6b35;
}
.status.gray {
color: #999;
}
.end-text {
font-size: 24rpx;
color: #000;
font-weight: bold;
}
.time {
font-size: 24rpx;
color: #626262;
float: right;
margin-right: 40rpx;
}
.time2 {
font-size: 24rpx;
color: #626262;
position: absolute;
right: 40rpx;
bottom: 70rpx;
}
.footer {
padding: 20rpx;
}
.submit-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
border-radius: 50rpx;
background: #007aff;
color: #fff;
font-size: 32rpx;
}
.text {
color: #000000;
font-size: 24rpx;
margin-left: 10rpx;
font-weight: 400;
}
.image {
width: 55rpx;
height: 42rpx;
}
.custom-upload-btn {
width: auto;
margin-left: 0rpx;
margin-right: 0rpx;
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;
}
</style>

View File

@@ -1,23 +1,6 @@
<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">
<view class="month-nav">
<view class="month-arrow" @click="prevMonth">
<image class="arrow-icon" src="/static/ic_arrow_gray.webp" mode="aspectFit" style="transform: rotate(180deg)" />
</view>
<text class="month-title">{{ currentYear }}{{ currentMonth }}</text>
<view class="month-arrow" @click="nextMonth">
<image class="arrow-icon" src="/static/ic_arrow_gray.webp" mode="aspectFit" />
</view>
</view>
</view>
<text class="month-title">{{ selectedDate.substring(0,4) }}{{ selectedDate.substring(5,7) }}</text>
<!-- 考勤统计 -->
<view class="attendance-stats">
@@ -41,277 +24,83 @@
<!-- 日历 -->
<view class="calendar">
<view class="weekdays">
<text v-for="day in weekdays" :key="day" class="weekday">{{ day }}</text>
</view>
<view class="dates" v-if="calendarExpanded">
<view
v-for="(date, index) in allDates"
:key="index"
:class="getDateClass(date)"
@click="selectDate(date)"
>
<text v-if="date.value" class="date-text">{{ date.value }}</text>
<view v-if="date.hasRecord" class="record-dot"></view>
</view>
</view>
<view class="dates" v-else>
<view
v-for="(date, index) in currentWeekDates"
:key="index"
:class="getDateClass(date)"
@click="selectDate(date)"
>
<text v-if="date.value" class="date-text">{{ date.value }}</text>
<view v-if="date.hasRecord" class="record-dot"></view>
</view>
</view>
<view class="calendar-toggle" @click="toggleCalendar">
<text class="toggle-text">{{ calendarExpanded ? '收起' : '展开' }}</text>
<CommonCalendar ref="calendarComponent" :initial-date="selectedDate" :allowWeekSwitch='true' :initialMode='mode' @dateChange="handleDateSelected" @modeChange="handleModeChange" />
<!-- 展开/收缩按钮 -->
<view class="calendar-toggle" @click="toggleCalendarMode">
<image v-if="mode == 'month'" class='image_zk' src="/static/ic_exp.png"></image>
<image v-else class='image_sq' src="/static/ic_sq.png"></image>
</view>
</view>
<!-- 固定班次 -->
<view class="fixed-shifts">
<text class="shifts-title">固定班次</text>
<view class="shift-item">
<view class="shift-time">
<view class="time-dot"></view>
<text class="time-text">应上班 09:00</text>
</view>
<view class="record leave-record">
<text class="record-text">已请假 09:0012:00</text>
</view>
</view>
<view class="shift-item">
<view class="shift-time">
<view class="time-dot"></view>
<text class="time-text">应下班 18:00</text>
</view>
<view class="record clock-record">
<text class="record-text">已打卡 18:02:18</text>
<text class="location">@某综合服务中心1栋</text>
</view>
</view>
</view>
<!-- 固定班次 -->
<view class="fixed-shifts">
<text class="shifts-title">固定班次</text>
<PunchInfo/>
<PunchInfo/>
</view>
</view>
</template>
<script>
/**
* 我的考勤页面
* @author lyc
* @description 显示用户考勤记录,包含可展开收缩的日历
*/
import CommonCalendar from '@/components/CommonCalendar.vue'
import PunchInfo from '@/components/punchInfo.vue'
export default {
components: {
CommonCalendar,
PunchInfo
},
data() {
return {
// 星期标题
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
// 日历是否展开
calendarExpanded: false,
// 当前选中的日期
selectedDate: 8,
// 当前年份
currentYear: 2025,
// 当前月份
currentMonth: 7,
// 完整月份日期数据
allDates: []
selectedDate: this.getCurrentDate(), // 默认选中的日期
mode: 'month'
};
},
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();
},
// 获取当前日期
getCurrentDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
/**
* 切换日历展开/收缩状态
*/
toggleCalendar() {
this.calendarExpanded = !this.calendarExpanded;
},
// 处理日期选择
handleDateSelected(date) {
this.selectedDate = date;
},
/**
* 选择日期
* @param {Object} date 日期对象
*/
selectDate(date) {
if (!date.value) return;
// 处理日历模式变化
handleModeChange(mode) {
this.mode = mode;
},
// 清除之前选中的日期
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,
'selected': date.selected,
'has-record': date.hasRecord,
'empty': !date.value
};
},
/**
* 切换到上一个月
*/
prevMonth() {
if (this.currentMonth === 1) {
this.currentYear--;
this.currentMonth = 12;
} else {
this.currentMonth--;
}
this.generateCalendarDates();
},
/**
* 切换到下一个月
*/
nextMonth() {
if (this.currentMonth === 12) {
this.currentYear++;
this.currentMonth = 1;
} else {
this.currentMonth++;
}
this.generateCalendarDates();
},
/**
* 生成日历数据
*/
generateCalendarDates() {
const year = this.currentYear;
const month = this.currentMonth;
// 获取当月第一天是星期几
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 });
}
// 添加当月日期
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
});
}
// 计算需要添加的下月空白日期数量使总数为7的倍数
const remainingDays = 7 - (this.allDates.length % 7);
if (remainingDays < 7) {
for (let i = 0; i < remainingDays; i++) {
this.allDates.push({ value: null });
}
}
}
// 切换日历模式
toggleCalendarMode() {
// 调用日历组件的toggleMode方法
if (this.$refs.calendarComponent) {
this.$refs.calendarComponent.toggleMode();
}
}
}
};
</script>
<style scoped>
.my-record-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 顶部导航栏 */
.header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
position: relative;
}
.back-btn {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
/* 月份标题和导航 */
.month-header {
padding: 20rpx 30rpx;
background-color: #fff;
}
.month-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.month-arrow {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.arrow-icon {
width: 32rpx;
height: 32rpx;
}
.month-title {
font-size: 32rpx;
font-weight: bold;
font-size: 28rpx;
font-weight: 600;
margin-left: 35rpx;
color: #333;
}
@@ -320,10 +109,10 @@ export default {
display: flex;
justify-content: space-around;
padding: 40rpx 30rpx;
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
border: 2rpx dashed #e0e0e0;
margin: 28rpx 30rpx 15rpx 30rpx;
background-color: #f7f7f7;
border-radius: 10rpx;
}
.stat-item {
@@ -348,160 +137,29 @@ 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: 10rpx; margin-right: 10rpx}
.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;}
.image_sq { width: 37rpx; height: 6rpx; }
.image_zk { width: 52rpx; height: 19rpx; }
/* 固定班次 */
.fixed-shifts {
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
flex: 1;
background-color: #f5f5f5;
padding: 30rpx;
overflow: auto;
}
.shifts-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
font-size: 28rpx;
color: #737373;
margin-bottom: 30rpx;
}
.shift-item {
margin-bottom: 40rpx;
}
.shift-item:last-child {
margin-bottom: 0;
}
.shift-time {
margin-top: 7rpx;
display: flex;
align-items: center;
margin-bottom: 20rpx;
justify-content: center;
}
.time-dot {
width: 12rpx;
height: 12rpx;
background-color: #ff6b35;
border-radius: 50%;
margin-right: 16rpx;
}
.time-text {
font-size: 28rpx;
color: #333;
}
.record {
border: 2rpx dashed #e0e0e0;
border-radius: 12rpx;
padding: 24rpx;
margin-left: 28rpx;
}
.leave-record {
background-color: #fff7f0;
border-color: #ffb366;
}
.clock-record {
background-color: #f0f9ff;
border-color: #66b3ff;
}
.record-text {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.location {
font-size: 24rpx;
color: #007aff;
display: block;
}
</style>

View File

@@ -71,14 +71,33 @@
this.selectedType = item;
},
async getOrderType() {
this.repairTypes = []
let params = {
parentId: '1952989217332658178'
}
let res = await this.$u.api.getRepairTypes(params);
let res = await this.$u.api.getRepairTypes2(params);
if (res.code == '200') {
this.repairTypes = res.data
this.repairInfo.type = res.data[0].id;
this.selectType(res.data[0].orderTypeName)
// 只处理前两条数据
const types = [];
const data = res.data.slice(0, 2); // 只取前两条数据
// 遍历前两个父级分类
data.forEach(parent => {
// 如果没有children或者children为空则添加父级数据
if (!parent.children || parent.children.length === 0) {
types.push(parent);
} else {
// 如果有children则添加所有children数据
types.push(...parent.children);
}
});
this.repairTypes = types;
if (this.repairTypes.length > 0) {
this.repairInfo.type = this.repairTypes[0].id;
this.selectType(this.repairTypes[0].orderTypeName)
}
}
},
// 删除图片
@@ -108,7 +127,9 @@
this.realSubmit();
return;
}
uni.showLoading({
title: '加载中...'
});
const result = await uploadFiles({
files: images,
url: this.vuex_config.baseUrl + '/resource/oss/upload',
@@ -121,6 +142,7 @@
title: '上传失败',
icon: 'none'
});
uni.hideLoading();
return;
}
@@ -134,10 +156,20 @@
async realSubmit() {
let res = await this.$u.api.addOrder2(this.repairInfo);
if (res.code == '200') {
uni.hideLoading();
// 关闭页面前发送事件通知前页面刷新
uni.$emit('refreshData', '');
// 返回上一页
uni.navigateBack();
const itemStr = encodeURIComponent(JSON.stringify(this.repairInfo));
uni.navigateTo({
url: "/pages/sys/user/myRepair/addSuc?item=" + itemStr,
});
}else{
uni.showToast({
title: res.msg,
icon: 'none'
});
}
},

View File

@@ -0,0 +1,126 @@
<template>
<view class="success-container">
<!-- 成功图标 -->
<view class="repaired-navbar">
<image src="/static/ic_back.png" class="repaired-back" @click="goBack" />
<text v v-if="false" class="repaired-progress" @click="goProgress">查看进度</text>
</view>
<image src="/static/ic_sub_suc.png" class="img" />
<!-- 提交成功标题 -->
<view class="success-title">提交成功</view>
<!-- 提示信息 -->
<view class="success-desc">提交成功后我们将及时为您服务</view>
<!-- 返回首页按钮 -->
<button class="back-home-btn" @click="goBack">返回首页</button>
</view>
</template>
<script>
export default {
data() {
return {
detail: {}
};
},
onLoad(options) {
if (options.item) {
const item = JSON.parse(decodeURIComponent(options.item));
this.detail = item;
}
},
methods: {
goBack() {
uni.navigateBack();
},
goProgress() {
const itemStr = encodeURIComponent(JSON.stringify(this.detail));
uni.navigateTo({
url: "/pages/sys/user/myRepair/repairDetail?item=" + itemStr,
});
}
// async getDetial() {
// let handlers = await this.$u.api.getRepairDetail({}, this.detail.id);
// if (handlers.code === 200) this.users = [...this.users, ...handlers.data];
// },
}
}
</script>
<style scoped>
.success-container {
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
background-color: #fff;
}
.success-icon {
margin-bottom: 20rpx;
}
.success-icon image {
width: 80rpx;
height: 80rpx;
}
.success-title {
font-size: 38rpx;
color: #333;
margin-bottom: 40rpx;
}
.success-desc {
font-size: 28rpx;
color: #80333333;
margin-bottom: 246rpx;
}
.back-home-btn {
width: 524rpx;
height: 92rpx;
line-height: 92rpx;
text-align: center;
background: linear-gradient(to right, #00C7FF, #0096FF);
color: #fff;
border-radius: 22rpx;
font-size: 28rpx;
}
.repaired-navbar {
width: 100%;
height: 100rpx;
display: flex;
align-items: center;
justify-content: flex-start;
position: relative;
margin-top: 40rpx;
}
.repaired-back {
width: 18rpx;
height: 32rpx;
margin-left: 24rpx;
}
.repaired-progress {
position: absolute;
right: 36rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #333;
}
.img {
width: 150rpx;
height: 150rpx;
margin-top: 150rpx;
margin-bottom: 40rpx;
}
</style>

View File

@@ -32,9 +32,9 @@
</view>
<image class="repair-line-image" src="/static/ic_my_repair_03.png" />
<view class="repair-info">建立时间{{ item.createTime }}</view>
<view class="repair-info">报事内容{{ item.typeName }}</view>
<view class="repair-info">工单类型{{ item.typeName }}</view>
<view class="repair-info">报事位置{{ item.location }}</view>
<view v-if="getStatusLabel(item.status) === '已结束'" class="repair-eval-btn eval-btn-right">服务评价</view>
<view v-if="getStatusLabel(item.status) === '已结束'" class="repair-eval-btn eval-btn-right" @click.stop="goTEvaluate(item)">服务评价</view>
</view>
<!-- 加载提示 -->
@@ -106,10 +106,14 @@
this.onRefresh()
},
onShow() {
uni.$once('refreshData', () => {
uni.$on('refreshData', () => {
this.onRefresh()
});
},
// 页面卸载时移除事件监听器
onUnload() {
uni.$off('refreshData');
},
methods: {
goBack() {
uni.navigateBack();
@@ -130,11 +134,11 @@
this.loading = true;
let params = {
type: "1952989217332658178",
// type: "1952989217332658178",
pageNum: this.pageNum,
pageSize: this.pageSize
};
let res = await this.$u.api.getOrderList(params);
let res = await this.$u.api.getOrderList2(params);
if (res.code == '200') {
const rows = res.rows || [];
if (reset) {
@@ -175,7 +179,7 @@
goDetail(item) {
const itemStr = encodeURIComponent(JSON.stringify(item));
uni.navigateTo({
url: "/pages/sys/workbench/order/orderDetail?item=" + itemStr,
url: "/pages/sys/user/myRepair/repairDetail?item=" + itemStr,
});
},
showDetail(item) {
@@ -208,10 +212,10 @@
};
return statusMap[status] || '';
},
goTEvaluate() {
const detailItemStr = encodeURIComponent(JSON.stringify(this.detailItem));
goTEvaluate(item) {
const itemStr = encodeURIComponent(JSON.stringify(item));
uni.navigateTo({
url: `/pages/sys/user/myRepair/repairEvaluate?detailItem=${detailItemStr}`
url: "/pages/sys/user/myRepair/repairEvaluate?item=" + itemStr
});
},
handleScroll(e) {

View File

@@ -0,0 +1,262 @@
<template>
<view class="page-container">
<view class="top-line" />
<view class="item-bg"><text class="detail-key">工单编号</text>{{ detail.orderNo }}</view>
<view class="item-bg">
<view class="item-title">上报人信息</view>
<view class="detail-key">发起人{{ detail.initiatorPeople }}</view>
<view class="detail-key">联系电话{{ detail.initiatorPhone }}</view>
</view>
<view class="item-bg">
<view class="item-title">保修信息</view>
<view class="detail-key">工单名称{{ detail.orderName }}</view>
<view class="detail-key">工单类型{{ detail.typeName }}</view>
<view class="detail-key">处理地点{{ detail.location }}</view>
<view class="detail-key">备注{{ detail.remark }}</view>
<view class="detail-value"><text class="detail-key">工单图片</text></view>
<view class="image-list" v-if="orderImgUrls.length > 0">
<u-image
v-for="(imgUrl, index) in orderImgUrls"
:key="index"
:src="imgUrl"
width="200rpx"
height="200rpx"
border-radius="10rpx"
@click="previewImage(orderImgUrls, index)"
style="margin-right: 20rpx; margin-bottom: 20rpx;"
mode="aspectFill"
></u-image>
</view>
</view>
<view v-if="detail.status>1" class="item-bg">
<view class="item-title">负责人信息</view>
<view class="detail-key">负责人{{ detail.handlerText }}</view>
<view class="detail-key">联系电话{{ detail.handlerPhone }}</view>
</view>
<!-- 纵向进度条 -->
<view class="item-bg">
<view class="item-title">进度跟踪</view>
<view v-for="(status, index) in detail.recordVoList" :key="index" class="repair-detail-step">
<view class="step-left">
<text class="step-date">{{ status.createTime.substring(0,11)}}</text>
<text class="step-time">{{ status.createTime.substring(11,16) }}</text>
</view>
<view class="step-dot-container">
<view class="repair-detail-dot" ></view>
<!-- 固定高度连线 -->
<view
v-if="index < detail.recordVoList.length - 1" class="repair-detail-line"
></view>
</view>
<view class="step-right">
<text class="step-name">{{ getStatusLabel(status.status) }}</text>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<!-- <view
v-if="((!isManager && detailStep != 0) || (isManager && detailStep == 0)) && detailStep != 3&&!isNaomalUser"
class="btn-group">
<button class="btn ghost"
@click="transfer(1)">{{ isManager ? '指派' : (detailStep == 2 ? '完成' : '开始') }}</button>
<button v-if="detailStep == 1" class="btn primary" @click="transfer(2)">转派</button>
</view>
<view class="kg">
</view>
<SelectUser :visible.sync="showSelect" :list="users" :multiple="false" @confirm="onConfirm" /> -->
</view>
</template>
<script>
import SelectUser from '@/components/SelectUser.vue'
export default {
components: { SelectUser },
data() {
return {
detailStep: 0,
detailStatus: '',
currentStatus: 2,
detail: {},
isManager: false,
isNaomalUser:true,
showSelect: false,
users: [],
orderImgUrls: []
};
},
onLoad(options) {
this.isManager = this.vuex_user?.roles?.[0]?.roleId < 3
// this.isNaomalUser = this.vuex_user?.roles?.[0]?.roleId >10
this.isNaomalUser = false
if (options.item) {
const item = JSON.parse(decodeURIComponent(options.item));
this.detail = item;
this.getStepInfo()
this.getImageUrl()
}
if ((this.isManager && this.detailStep == 0) || (!this.isManager && this.detailStep == 1)) {
this.getHandler()
}
},
methods: {
goBack() { uni.navigateBack(); },
async getImageUrl() {
if (!this.detail.orderImgUrl) return;
const imgIds = this.detail.orderImgUrl.split(',');
const res = await this.$u.api.getImageUrl({}, imgIds.join(','));
if (res.code == 200 && res.data) this.orderImgUrls = res.data.map(item => this.vuex_config.imageUrl+item.url);
},
async getHandler() {
let handlers = await this.$u.api.getHandler3({}, this.detail.type);
if (handlers.code === 200) this.users = [...this.users, ...handlers.data];
},
getStepInfo() {
let currentIndex = 0;
if (this.detail.status == 0) currentIndex = 0;
else if (this.detail.status == 1) currentIndex = 1;
else if (this.detail.status == 3) currentIndex = 2;
else if (this.detail.status == 4) currentIndex = 3;
this.detailStep = currentIndex;
},
getStatusLabel(status) {
const statusMap = {
0: "创建工单",
1: "已接单",
2: "已接单",
3: "处理中",
4: "已完成",
};
return statusMap[status] || "";
},
previewImage(urls, index) {
const validUrls = urls.filter(url => url && url.trim() !== '');
const currentIndex = validUrls.indexOf(urls[index]);
uni.previewImage({
urls: validUrls,
current: currentIndex >= 0 ? currentIndex : 0,
indicator: 'number',
backgroundColor: '#000'
})
},
async onConfirm(selected) {
let params = this.detail
params.handler = selected[0].value
params.status = 1
let res = await this.$u.api.updateOrder2(params);
if (res.code == '200') {
uni.$emit('refreshData', '');
if(!this.isManager){
uni.navigateBack();
return
}
this.detail.handler = selected.value
this.detail.status = 1
const d = new Date();
const z = n => n.toString().padStart(2, '0');
let time = `${d.getFullYear()}-${z(d.getMonth()+1)}-${z(d.getDate())} ${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`;
let step = {}
step.id= this.detail.recordVoList[0].id,
step.orderId= this.detail.recordVoList[0].orderId,
step.status= '1',
step.handler='',
step.handlerName='',
step.createTime= time
this.detail.recordVoList.push(step)
this.getStepInfo()
}
},
async submit() {
let params = this.detail
if (this.detail.status == 1 || this.detail.status == 2) params.status = 3
else params.status = 4
let res = await this.$u.api.updateOrder2(params);
if (res.code == 200) {
uni.$emit('refreshData', '');
this.detail.status = params.status
if(params.status ==3){
this.detail.handlerText = this.vuex_user.nickName
this.detail.handlerPhone = this.vuex_user.phonenumber
}
const d = new Date();
const z = n => n.toString().padStart(2, '0');
let time = `${d.getFullYear()}-${z(d.getMonth()+1)}-${z(d.getDate())} ${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`;
let step = {}
step.id= this.detail.recordVoList[0].id,
step.orderId= this.detail.recordVoList[0].orderId,
step.status= params.status,
step.handler='',
step.handlerName='',
step.createTime= time
this.detail.recordVoList.push(step)
this.getStepInfo()
}
},
transfer(type) {
if (this.isManager || type == 2) this.showSelect = true
else this.submit()
}
}
}
</script>
<style scoped>
.page-container { background: #f7f7f7; height: 100vh; }
.top-line { width: 100vw; height: 3rpx; background: #ECECEC; }
.item-bg{ background: #fff; border-radius: 20rpx; margin-top: 10px; margin-left: 25rpx; margin-right: 25rpx; padding: 25rpx; }
.item-title{ color: #000; font-size: 28rpx; font-weight: 600; margin-bottom: 20rpx; }
.repair-detail-step-container { display: flex; flex-direction: column; }
.repair-detail-step {
display: flex;
flex-direction: row;
margin-bottom: 20rpx;
}
.step-left { flex: 1; display: flex; flex-direction: column; align-items: flex-end; padding-right: 20rpx; }
.step-date { font-size: 24rpx; color: #333; }
.step-time { font-size: 24rpx; color: #666; margin-top: 10rpx; }
.step-dot-container {
display: flex;
flex-direction: column;
align-items: center;
width: 40rpx;
}
.repair-detail-dot {
width: 22rpx;
height: 22rpx;
border-radius: 50%;
background: #BDBDBD;
border: 2rpx solid #BDBDBD;
z-index: 2;
background: #2186FF;
border-color: #2186FF;
}
/* 固定高度连线 */
.repair-detail-line {
width: 4rpx;
height: 80rpx; /* 每条线的高度 */
background: #BDBDBD;
margin-top: 2rpx;
background: #2186FF;
}
.step-right { flex: 1; display: flex; flex-direction: column; padding-left: 20rpx; }
.step-name { font-size: 28rpx; color: #333; font-weight: bold; margin-bottom: 6rpx; }
.step-desc { font-size: 24rpx; color: #666; }
.detail-key { color: #222; font-size: 28rpx; margin-bottom: 20rpx; }
.detail-value { margin-bottom: 30rpx; color: #2F2F2F; }
.image-list { display: flex; flex-wrap: wrap; margin-top: 20rpx; }
.btn-group { display: flex; justify-content: space-around; margin-top: 60rpx; }
.btn { width: 276rpx; height: 88rpx; padding: 20rpx; font-size: 30rpx; border-radius: 50rpx; display: flex; align-items: center; justify-content: center; }
.primary { background-color: #1890ff; color: #fff; }
.ghost { background-color: #fff; color: #1890ff; border: 2rpx solid #1890ff; }
.kg{
height: 60rpx;
}
</style>

View File

@@ -1,6 +1,5 @@
<template>
<view class="evaluate-container">
<!-- 评分项 -->
<view class="evaluate-list">
<view class="evaluate-row">
@@ -8,23 +7,34 @@
<view class="evaluate-stars">
<image v-for="i in 5" :key="i"
:src="detailItem.serviceEvalua >= i ? '/static/ic_evaluate_select.png' : '/static/ic_evaluate_disselect.png'"
class="evaluate-star" @click="detailItem.serviceEvalua = i" />
class="evaluate-star" :class="{'disabled': isViewMode}" @click="!isViewMode && setRating(i)" />
</view>
</view>
</view>
<!-- 输入框 -->
<textarea class="evaluate-textarea" placeholder="说点什么吧..." v-model="detailItem.serviceEvaluaText" />
<textarea class="evaluate-textarea" placeholder="说点什么吧..." v-model="detailItem.serviceEvaluaText"
:disabled="isViewMode" />
<!-- 上传照片 -->
<!-- 上传照片 -->
<view class="repair-evaluate-section">
<view class="repair-evaluate-section" v-if="!isViewMode">
<view class="add-repair-label">上传照片 <text class="add-repair-optional">(非必填最多三张)</text></view>
<u-upload :fileList="selectedImages" @delete="deletePic" name="upload" multiple maxCount="3" width="180"
height="180" :autoUpload="false"></u-upload>
<u-upload :fileList="selectedImages" @on-list-change="onListChange" @delete="deletePic" name="upload"
multiple maxCount="3" width="180" height="180" :autoUpload="false"></u-upload>
</view>
<!-- 查看图片 -->
<view class="repair-evaluate-section" v-else>
<view class="add-repair-label">评价图片</view>
<view class="image-preview-container" v-if="imageList.length > 0">
<image v-for="(img, index) in imageList" :key="index" :src="img" class="preview-image"
@click="previewImage(index)"></image>
</view>
<view class="no-image" v-else>暂无图片</view>
</view>
<!-- 提交按钮 -->
<button class="evaluate-btn" @click="submit">提交</button>
<button class="evaluate-btn" @click="submit" v-if="!isViewMode">提交</button>
</view>
</template>
@@ -41,31 +51,73 @@
return {
comment: '',
selectedImages: [], // 存储已选图片
detailItem: null // 接收传递的详情项
imageList: [], // 查看模式下的图片列表
detailItem: {
serviceEvalua: 0,
serviceEvaluaText: ''
}, // 接收传递的详情项
isViewMode: false // 是否为查看模式
}
},
onLoad(options) {
// 接收传递的detailItem参数
if (options.detailItem) {
// 接收传递的参数
if (options.item) {
try {
this.detailItem = JSON.parse(decodeURIComponent(options.detailItem));
const item = JSON.parse(decodeURIComponent(options.item));
this.detailItem = {
...this.detailItem,
...item
};
console.log(this.detailItem)
this.isViewMode = this.detailItem.serviceEvalua
if (this.isViewMode) {
this.getImageUrl()
}
} catch (e) {
console.error('解析detailItem参数失败', e);
console.error('解析item参数失败', e);
}
}
},
methods: {
async getImageUrl() {
if (!this.detailItem.imgUrl) return;
const imgIds = this.detailItem.imgUrl.split(',');
const res = await this.$u.api.getImageUrl({}, imgIds.join(','));
if (res.code == 200 && res.data) this.imageList = res.data.map(item => this.vuex_config.imageUrl+item.url);
},
// 设置评分
setRating(rating) {
this.$set(this.detailItem, 'serviceEvalua', rating);
},
// 删除图片
deletePic(event) {
this.selectedImages.splice(event.index, 1);
},
// 图片列表变化处理
onListChange(list) {
this.selectedImages = list;
},
// 预览图片
previewImage(index) {
uni.previewImage({
current: index,
urls: this.imageList
});
},
async submit() {
console.log(this.selectedImages)
if (this.selectedImages.length <= 0) {
this.realSubmit()
return
}
// 遍历selectedImages数组并处理图片路径
const images = this.selectedImages.map(item => item.path.replace('file://', ''));
const images = this.selectedImages
.map(item => item?.path?.replace('file://', '') || item?.url || null)
.filter(path => path !== null);
uni.showLoading({
title: '加载中...'
});
const result = await uploadFiles({
files: images,
url: this.vuex_config.baseUrl + '/resource/oss/upload',
@@ -73,16 +125,24 @@
vm: this // 关键:用于注入 token 等
});
if (result.code == '200') {
// 遍历result获取data.url加上,分割
const urls = result.map(item => item.data?.url || '').filter(url => url !== '');
this.detailItem.imgUrl = urls.join(',');
this.realSubmit()
const allSuccess = result.every(item => item.code == 200);
if (!allSuccess) {
uni.showToast({
title: '上传失败',
icon: 'none'
});
uni.hideLoading();
return;
}
// 遍历result获取data.url加上,分割
const urls = result.map(item => item.data?.ossId || '').filter(ossId => ossId !== '');
this.detailItem.imgUrl = urls.join(',');
this.realSubmit()
},
async realSubmit() {
let res = await this.$u.api.updateOrder(this.detailItem);
let res = await this.$u.api.updateOrder2(this.detailItem);
if (res.code == '200') {
// 关闭页面前发送事件通知前页面刷新
uni.$emit('refreshData', '');
@@ -133,6 +193,10 @@
margin-right: 36rpx;
}
.evaluate-star.disabled {
opacity: 1;
}
.evaluate-tag {
flex: 0 0 calc((100% - 60rpx) / 4);
@@ -164,6 +228,10 @@
display: block;
}
.evaluate-textarea[disabled] {
background-color: #f0f0f0;
}
.evaluate-btn {
width: 80vw;
height: 88rpx;
@@ -198,4 +266,23 @@
font-size: 24rpx;
font-weight: 400;
}
.image-preview-container {
display: flex;
flex-wrap: wrap;
}
.preview-image {
width: 180rpx;
height: 180rpx;
margin-right: 20rpx;
margin-bottom: 20rpx;
}
.no-image {
color: #888;
font-size: 28rpx;
text-align: center;
padding: 20rpx 0;
}
</style>

View File

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

View File

@@ -1,87 +1,117 @@
<template>
<view class="detail-container">
<!-- 可滚动内容区 -->
<view class="scroll-content">
<!-- 问题标题 -->
<view class="question-title">
包月停车临时停车的办理流程及收费标准
<view class="title-underline"></view>
</view>
<!-- 内容区 -->
<view class="question-content">
<view class="question-desc">
您好:本项目只有8组团为包月停车办理流程为:业主携带身份证至物业客户中心前台办理租户带租赁合同和身份证到前台办理即可;5,6,7,9,10,11组团无包月停车无需办理包月停车手续;临时停车无需办理5,6组团为私家车位无需收费8组团包月停车的收费标准为:500/8组团临时停车的收费标准为:小区内9,10,11组团临时停车的收费标准为:二轮车:每小时1元12小时内5元/24小时内10元/;小型车:每小时3元12小时内10元/24小时内20元/;大型车:每小时4元12小时内15元/24小时内25元/
</view>
<image src="/static/ic_q_d_01.png" class="question-img" mode="widthFix" />
</view>
</view>
</view>
<view class="detail-container">
<!-- 可滚动内容区 -->
<view class="scroll-content">
<!-- 问题标题 -->
<view class="question-title">
{{info.title}}
<view class="title-underline"></view>
</view>
<!-- 内容区 -->
<view class="question-content">
<view class="question-desc">
{{info.content}}
</view>
<image src="info.img" class="question-img" mode="widthFix" />
<view class="question-time">{{info.time}}</view>
</view>
</view>
</view>
</template>
<script>
export default {
methods: {
export default {
data() {
return {
info: {},
}
}
};
},
onLoad(options) {
if (options.item) {
const item = JSON.parse(decodeURIComponent(options.item));
this.info = item;
}
},
methods: {
},
}
</script>
<style scoped>
.detail-container {
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
}
.detail-container {
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
}
.detail-back {
position: absolute;
left: 37rpx;
width: 15rpx;
height: 33rpx;
}
.detail-title {
font-size: 36rpx;
color: #000;
}
.scroll-content {
flex: 1;
overflow-y: auto;
padding-bottom: 40rpx;
}
.question-title {
font-size: 36rpx;
font-weight: bold;
color: #000000;
margin: 40rpx 32rpx 0 64rpx;
position: relative;
line-height: 1.4;
}
.title-underline {
width: 120rpx;
height: 6rpx;
background: #3B8BFF;
border-radius: 3rpx;
margin-top: 8rpx;
}
.question-content {
margin: 34rpx 24rpx 0 24rpx;
background: #EDF6FF;
border-radius: 12rpx;
border: 2rpx dashed #bfc8d6;
padding: 32rpx 24rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.question-desc {
font-size: 28rpx;
color: #4A4A4A;
line-height: 1.8;
margin-bottom: 24rpx;
}
.question-img {
width: 100%;
border-radius: 8rpx;
}
.detail-back {
position: absolute;
left: 37rpx;
width: 15rpx;
height: 33rpx;
}
.detail-title {
font-size: 36rpx;
color: #000;
}
.scroll-content {
flex: 1;
overflow-y: auto;
padding-bottom: 40rpx;
}
.question-title {
font-size: 36rpx;
font-weight: bold;
color: #000000;
margin: 40rpx 32rpx 0 64rpx;
position: relative;
line-height: 1.4;
}
.title-underline {
width: 120rpx;
height: 6rpx;
background: #3B8BFF;
border-radius: 3rpx;
margin-top: 8rpx;
}
.question-content {
min-height: 600rpx;
margin: 34rpx 24rpx 0 24rpx;
background: #EDF6FF;
border-radius: 12rpx;
padding: 32rpx 24rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
position: relative;
}
.question-desc {
font-size: 28rpx;
color: #4A4A4A;
line-height: 1.8;
margin-bottom: 24rpx;
}
.question-time {
font-size: 28rpx;
color: #4A4A4A;
line-height: 1.8;
align-self: flex-end;
margin-top: auto;
}
.question-img {
width: 100%;
border-radius: 8rpx;
}
</style>

View File

@@ -17,7 +17,7 @@
<!-- 常见问题 -->
<view class="faq-title">常见问题</view>
<view class="faq-list">
<view class="faq-item" v-for="(item, idx) in faqList" :key="idx" @click="goDetail(idx)">
<view class="faq-item" v-for="(item, idx) in faqList" :key="idx" @click="goDetail(item)">
<text class="faq-text">{{ item }}</text>
</view>
</view>
@@ -65,8 +65,13 @@
phoneNumber: '023950888'
});
},
goDetail(idx) {
uni.navigateTo({ url: '/pages/sys/user/serviceCenter/questionDetail' });
goDetail(item) {
let params = {}
params.title = item
params.content = item
params.time = item
const itemStr = encodeURIComponent(JSON.stringify(params));
uni.navigateTo({ url: '/pages/sys/user/serviceCenter/questionDetail?item=' + itemStr });
}
}
}

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();
@@ -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

@@ -171,7 +171,7 @@ export default {
const result = await this.$u.api.getImageUrl({}, imgIds);
if (result.code == 200 && result.data) {
// 提取res.data数组中每个对象的url字段
this.realImages = result.data.map(item => item.url);
this.realImages = result.data.map(item => this.vuex_config.imageUrl+item.url);
}
}
this.loading = false;

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>
</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.compleTime || '' }}</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 = {}
this.loading[idx] = true;
let params = {
pageNum: this.pageNum[idx],
pageSize: this.pageSize
};
let data = [];
// 根据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}`
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 {
@@ -212,6 +291,10 @@
color: #8A8A8A;
}
.ins-status.overdue {
color: #FF4D4D;
}
.ins-info {
font-size: 24rpx;
color: #888;

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

@@ -10,9 +10,8 @@
<!-- 左侧 + 竖线 -->
<view class="rail">
<view class="dot" :class="{
'dot-circle': item.dotShape === 'circle',
'dot-square': item.dotShape === 'square',
'dot-active': item.dotColor === 'blue'
'dot-active': item.inspectionState == 1,
}"></view>
</view>
@@ -21,30 +20,57 @@
<!-- 标题块 + 操作区打包在一起方便宽度同步 -->
<view class="title-ops-wrapper">
<!-- 标题块 -->
<view v-if="item.headerStyle === 'solid'" class="title-solid">
{{ item.pointName }}{{ item.date }} {{ item.time }}
<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 }}{{ item.date }} {{ item.time }}
{{ item.pointName }}
<view>
{{ item.pointStartTime.substring(0,16) }} - {{ item.pointEndTime.substring(0,16) }}
</view>
<!-- 添加3个子项 -->
<view v-if="item.inspectionResults != 1" class="order-container">
<view class="status2" @click="goDetail(item)">
<view>完成巡检</view>
<view class="badge" :class="'badge-warn'">
{{'异常' }}
</view>
</view>
<view v-if="getOptTime(item.mserviceWorkOrdersVo.recordVoList,0)" class="sub-item">
<view class="sub-dot"></view>
<view class="sub-text">提报时间{{ getOptTime(item.mserviceWorkOrdersVo.recordVoList,0) }}</view>
</view>
<view v-if="getOptTime(item.mserviceWorkOrdersVo.recordVoList,1)" class="sub-item">
<view class="sub-dot"></view>
<view class="sub-text">处理时间{{ getOptTime(item.mserviceWorkOrdersVo.recordVoList,1) }}</view>
</view>
<view v-if="getOptTime(item.mserviceWorkOrdersVo.recordVoList,4)" class="sub-item">
<view class="sub-dot"></view>
<view class="sub-text">完成时间{{ getOptTime(item.mserviceWorkOrdersVo.recordVoList,4) }}</view>
</view>
<view class="order-id">工单号{{item.mserviceWorkOrdersVo.orderNo}}</view>
</view>
</view>
<!-- 操作区宽度跟随标题块内部居中 -->
<view class="ops" v-if="item.status === '待巡检'">
<view class="ops" v-if="item.inspectionState == 0">
<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 class="status" v-else-if="item.inspectionResults == 1" @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 class="footer-placeholder">巡检提醒</view>
</view>
</template>
@@ -52,64 +78,76 @@
export default {
data() {
return {
taskInfo: {},
taskList: []
}
},
onLoad() {
this.getTaskListMock()
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: {
// ---- 模拟接口数据(可替换成 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'
}
]
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) {
uni.showToast({
title: `开始巡检:${item.pointName}`,
icon: 'none'
})
}
const detailItemStr = encodeURIComponent(JSON.stringify(item));
uni.navigateTo({
url: `/pages/sys/workbench/inspection/inspectionOpt?item=${detailItemStr}`
});
},
goDetail(item) {
// const detailItemStr = encodeURIComponent(JSON.stringify(item));
// uni.navigateTo({
// url: `/pages/sys/workbench/inspection/inspectionDetail?item=${detailItemStr}`
// });
},
getOptTime(process, status) {
if (!process || !Array.isArray(process) || process.length === 0) {
return null;
}
const record = process.find(item => item.status == status);
return record ? record.createTime : null;
},
}
}
</script>
@@ -123,8 +161,10 @@
.section-title {
font-size: 28rpx;
color: #333;
margin: 24rpx 24rpx 8rpx
color: #0B0B0B;
font-weight: bold;
padding-top: 24rpx;
padding-left: 40rpx;
}
/* 时间轴容器 */
@@ -174,29 +214,51 @@
width: 16rpx;
height: 16rpx;
margin-top: 20rpx;
background: #cfd3dc
}
.dot-circle {
background: #cfd3dc;
border-radius: 50%
}
.dot-square {
border-radius: 4rpx
}
.dot-active {
background: #2f6aff
}
/* 右侧卡片 */
.card {
padding: 16rpx 20rpx;
/* 子项 */
.sub-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.sub-item:last-child {
margin-bottom: 0;
}
/* 子项圆点 */
.sub-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: #BFBFBF;
margin-right: 20rpx;
flex-shrink: 0;
}
/* 子项文字 */
.sub-text {
font-size: 26rpx;
color: #666;
}
/* 右侧卡片 */
.card {}
/* 标题 + 操作区包裹(宽度由标题决定) */
.title-ops-wrapper {
display: inline-block;
display: flex;
flex-direction: column;
align-items: center;
}
/* 标题两种样式 */
@@ -204,18 +266,26 @@
background: #2f6aff;
color: #fff;
border-radius: 12rpx;
padding: 12rpx 18rpx;
display: inline-block;
font-size: 26rpx
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 18rpx;
display: inline-block;
font-size: 26rpx;
color: #333
padding: 12rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
font-size: 28rpx;
color: #000;
width: 100%;
box-sizing: border-box;
}
/* 操作区:宽度继承标题块,内部居中 */
@@ -224,6 +294,36 @@
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;
}
.status2 {
width: 280rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #BFBFBF;
margin-top: 30rpx;
color: #BFBFBF;
padding: 12rpx 24rpx;
border-radius: 12rpx;
font-size: 26rpx;
margin-bottom: 20rpx;
}
.btn-outline {
@@ -245,22 +345,16 @@
/* 结果徽标 */
.badge {
padding: 8rpx 18rpx;
border-radius: 22rpx;
font-size: 24rpx;
margin-left: 16rpx;
margin-left: 20rpx;
}
.badge-success {
color: #16a34a;
border: 2rpx solid #16a34a;
background: #fff
}
.badge-warn {
color: #f59e0b;
border: 2rpx solid #f59e0b;
background: #fff
}
/* 底部占位文字 */
@@ -271,4 +365,18 @@
margin-top: 80rpx;
letter-spacing: 2rpx
}
/* 工单信息容器 */
.order-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.order-id{
color: #296AEF;
font-size: 24rpx;
margin-bottom: 20rpx;
}
</style>

View File

@@ -0,0 +1,584 @@
<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" @click="startDateSelect">
<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>
<SelectCalendarDialog :visible.sync="showCalendarDialog" @confirm="onConfirm" />
</view>
</template>
<script>
import SelectCalendarDialog from '@/components/SelectCalendarDialog.vue'
export default {
components: { SelectCalendarDialog },
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,
showCalendarDialog: false,
}
},
methods: {
changeTab(idx) {
this.activeTab = idx;
},
bindPickerChange(e) {
this.leaveTypeIndex = e.detail.value;
},
startDateSelect() {
this.showCalendarDialog = true
},
onConfirm(selectedDate){
this.startDate = selectedDate
},
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: grid;
grid-template-columns: repeat(3, 1fr);
/* 三列可改成2/4列 */
gap: 20rpx;
/* 行列间距 */
padding: 20rpx;
}
.popup-option {
text-align: center;
padding: 20rpx 0;
border-radius: 12rpx;
background-color: #f5f5f5;
font-size: 28rpx;
color: #333;
}
.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 {
@@ -449,5 +504,15 @@
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,10 +148,14 @@ 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) {
this.activeTab = idx;

View File

@@ -9,7 +9,7 @@
<view class="detail-key">联系电话{{ detail.initiatorPhone }}</view>
</view>
<view class="item-bg">
<view class="item-title">修信息</view>
<view class="item-title">修信息</view>
<view class="detail-key">工单名称{{ detail.orderName }}</view>
<view class="detail-key">工单类型{{ detail.typeName }}</view>
<view class="detail-key">处理地点{{ detail.location }}</view>
@@ -90,7 +90,8 @@ export default {
},
onLoad(options) {
this.isManager = this.vuex_user?.roles?.[0]?.roleId < 3
this.isNaomalUser = this.vuex_user?.roles?.[0]?.roleId >10
// this.isNaomalUser = this.vuex_user?.roles?.[0]?.roleId >10
this.isNaomalUser = false
if (options.item) {
const item = JSON.parse(decodeURIComponent(options.item));
this.detail = item;
@@ -107,7 +108,7 @@ export default {
if (!this.detail.orderImgUrl) return;
const imgIds = this.detail.orderImgUrl.split(',');
const res = await this.$u.api.getImageUrl({}, imgIds.join(','));
if (res.code == 200 && res.data) this.orderImgUrls = res.data.map(item => item.url);
if (res.code == 200 && res.data) this.orderImgUrls = res.data.map(item => this.vuex_config.imageUrl+item.url);
},
async getHandler() {
let handlers = await this.$u.api.getHandler3({}, this.detail.type);

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: '/static/aaaa_gzxj.png',
text: '工作巡检',
url:'/pages/sys/workbench/inspection/inspection'
},
// {
// icon: 'https://picsum.photos/80/80?random=3',
// text: '会议',
// url:'/pages/sys/workbench/meet/meet'
// 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: 'oa',
// url:'/pages/sys/workbench/oa/oa'
// },
// {
// icon: 'https://picsum.photos/80/80?random=3',

BIN
static/aaaa_gzxj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

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_home_tt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
static/ic_sq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

BIN
static/ic_sub_suc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -16,7 +16,7 @@ try{
}
// 需要永久存储且下次APP启动需要取出的在state中的变量名
let saveStateKeys = ['vuex_user', 'vuex_token', 'vuex_remember', 'vuex_locale','vuex_isAgent'];
let saveStateKeys = ['vuex_user', 'vuex_token', 'vuex_remember', 'vuex_locale','vuex_isAgent','vuex_push_clientId'];
// 保存变量到本地存储中
const saveLifeData = function(key, value){
@@ -41,6 +41,7 @@ const store = new Vuex.Store({
vuex_remember: lifeData.vuex_remember ? lifeData.vuex_remember : '',
vuex_locale: lifeData.vuex_locale ? lifeData.vuex_locale : '',
vuex_isAgent: lifeData.vuex_isAgent ? lifeData.vuex_isAgent : '',
vuex_push_clientId: lifeData.vuex_push_clientId ? lifeData.vuex_push_clientId : '',
// 如果vuex_version无需保存到本地永久存储无需lifeData.vuex_version方式
vuex_config: config,

BIN
unpackage/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,73 @@
export default {
// 检查通知权限
checkPermission(callback) {
if (plus.os.name !== "Android") {
callback(true) // iOS 这里直接返回 true
return
}
try {
let NotificationManagerCompat = plus.android.importClass("androidx.core.app.NotificationManagerCompat")
let context = plus.android.runtimeMainActivity()
let manager = NotificationManagerCompat.from(context)
callback(manager.areNotificationsEnabled())
} catch (e) {
console.error("检查通知权限出错:", e)
callback(false)
}
},
// 打开通知权限设置页面
openPermissionSetting() {
if (plus.os.name !== "Android") return
try {
const main = plus.android.runtimeMainActivity()
const Intent = plus.android.importClass('android.content.Intent')
const Settings = plus.android.importClass('android.provider.Settings')
const Uri = plus.android.importClass('android.net.Uri')
const Build = plus.android.importClass('android.os.Build')
let intent
if (Build.VERSION.SDK_INT >= 26) {
// Android 8.0 及以上
intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, main.getPackageName())
} else if (Build.VERSION.SDK_INT >= 21) {
// Android 5.0 - 7.1
intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
intent.putExtra("app_package", main.getPackageName())
intent.putExtra("app_uid", main.getApplicationInfo().uid)
} else {
// Android 4.4 及以下,跳转到应用详情页
intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.setData(Uri.fromParts("package", main.getPackageName(), null))
}
main.startActivity(intent)
} catch (e) {
console.error("跳转通知设置页失败:", e)
}
},
// 统一方法:检查并申请
ensurePermission(next) {
this.checkPermission((enabled) => {
if (enabled) {
next && next() // 已有权限,继续逻辑
} else {
uni.showModal({
title: "通知权限未开启",
content: "请开启通知权限以接收消息提醒",
confirmText: "去开启",
success: (res) => {
if (res.confirm) {
console.log('t1', '11111111')
this.openPermissionSetting()
}
}
})
}
})
}
}

15
utils/notify.js Normal file
View File

@@ -0,0 +1,15 @@
export function showNotification(title, content, extraData = {}) {
// App-Plus 平台
if (typeof plus !== 'undefined' && plus.push) {
plus.push.createMessage(content, extraData, function(result){
});
} else {
// H5 或其他平台,可以用 alert / toast 代替
uni.showToast({
title: content,
icon: 'none',
duration: 3000
})
}
}

115
utils/websocket.js Normal file
View File

@@ -0,0 +1,115 @@
class WebSocketService {
constructor() {
this.socketTask = null
this.isConnected = false
this.heartbeatTimer = null
this.reconnectTimer = null
this.url = ""
this.dispatchHandler = null // 统一分发处理器
}
connect(url) {
if (this.isConnected) return
this.url = url
this.socketTask = uni.connectSocket({
url: url, // 鉴权
success: () => console.log("连接请求已发送")
})
// this.socketTask = uni.connectSocket({ url })
console.log('WebSocketService', 'connect called, url:', url)
// 注意:跨平台统一使用 socketTask 的事件监听
if (this.socketTask) {
this.socketTask.onOpen(() => {
console.log("WebSocket 已连接")
this.isConnected = true
this.startHeartbeat()
})
this.socketTask.onMessage((res) => {
console.log("📩 收到消息:", res.data)
let msg
try {
msg = JSON.parse(res.data)
} catch (e) {
msg = res.data
}
// 统一分发
if (this.dispatchHandler) {
this.dispatchHandler(msg)
} else {
console.warn("未注册 dispatchHandler消息未处理", msg)
}
})
this.socketTask.onClose(() => {
console.log("❌ WebSocket 已关闭")
this.isConnected = false
this.reconnect()
})
this.socketTask.onError((err) => {
console.error("⚠️ WebSocket 错误:", err)
this.isConnected = false
this.reconnect()
})
} else {
console.error("WebSocketService: socketTask 获取失败,可能平台不支持")
}
}
send(data) {
if (this.isConnected && this.socketTask) {
this.socketTask.send({
data: typeof data === "string" ? data : JSON.stringify(data),
})
console.log('WebSocketService', 'send:', data)
}
}
startHeartbeat() {
console.log('WebSocketService', 'startHeartbeat')
this.clearHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.send("ping")
console.log('WebSocketService', 'heartbeat ping sent')
}
}, 30000)
}
clearHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
reconnect() {
if (this.reconnectTimer) return
console.log("⏳ 5秒后尝试重连...")
this.reconnectTimer = setTimeout(() => {
this.connect(this.url)
this.reconnectTimer = null
}, 5000)
}
close() {
this.clearHeartbeat()
if (this.socketTask) {
this.socketTask.close()
this.socketTask = null
}
this.isConnected = false
console.log('WebSocketService', 'close called')
}
setDispatchHandler(handler) {
this.dispatchHandler = handler
}
}
export default new WebSocketService()