代码提交-3-13

This commit is contained in:
2026-03-13 14:32:24 +08:00
parent 3ca2451b2f
commit ae4aede94d
27 changed files with 2285 additions and 517 deletions

136
App.vue
View File

@@ -1,39 +1,119 @@
<script>
import config from './config'
import store from '@/store'
import { getToken } from '@/utils/auth'
import config from './config'
import store from '@/store'
import { getToken } from '@/utils/auth'
export default {
onLaunch: function() {
this.initApp()
export default {
onLaunch: function () {
this.initApp()
},
methods: {
// 初始化应用
initApp() {
// 初始化应用配置
this.initConfig()
// 检查用户登录状态
//#ifdef H5
this.checkLogin()
//#endif
},
methods: {
// 初始化应用
initApp() {
// 初始化应用配置
this.initConfig()
// 检查用户登录状态
//#ifdef H5
this.checkLogin()
//#endif
},
initConfig() {
this.globalData.config = config
},
checkLogin() {
// if (!getToken()) {
// this.$tab.reLaunch('/pages/login')
// }
const whiteList = ['/pages/work/sidebar/safetyDeclaratio/index'] // 添加不需要登录的页面路径
const currentPath = this.$route.path
if (!whiteList.includes(currentPath) && !getToken()) {
this.$tab.reLaunch('/pages/login')
initConfig() {
this.globalData.config = config
// 运行路径自动开关 CAS:/app-cas 启用,/app 禁用
try {
const path = (window.location && window.location.pathname) || ''
if (/^\/app-cas\/?/.test(path)) {
this.globalData.config.casEnable = true
} else if (/^\/app\/?/.test(path)) {
this.globalData.config.casEnable = false
}
} catch (e) { /* noop */ }
},
checkLogin() {
const whiteList = ['/pages/work/sidebar/safetyDeclaratio/index', '/pages/cas/callback']
const currentPath = this.$route && this.$route.path
const hasToken = getToken()
const cfg = this.globalData && this.globalData.config
const casEnable = cfg && cfg.casEnable
const casServer = cfg && cfg.casServer
const casService = cfg && cfg.casService
// 检查当前路径
const path = (window.location && window.location.pathname) || ''
// 1) 如果当前是 CAS 回调地址,根据来源处理
const isCasCallbackPath = /\/cas\/cas-callback$/.test(path) || /\/app-cas\/cas-callback$/.test(path)
if (isCasCallbackPath) {
try {
const from = sessionStorage.getItem('pasd_sso_from')
// 如果是从移动端来的请求且当前在PC端回调路径,则重定向到移动端回调路径
if (from === 'app-cas' && /^\/cas\/cas-callback$/.test(path)) {
sessionStorage.removeItem('pasd_sso_from')
window.location.href = `${window.location.origin}/app-cas/cas-callback${window.location.search || ''}`
return
}
// 如果是从PC端来的请求且当前在移动端回调路径,则重定向到PC端回调路径
if (!from && /^\/app-cas\/cas-callback$/.test(path)) {
window.location.href = `${window.location.origin}/cas/cas-callback${window.location.search || ''}`
return
}
} catch (e) { /* noop */ }
try {
const u = new URL(window.location.href)
const ticket = u.searchParams.get('ticket')
const q = ticket ? `?ticket=${encodeURIComponent(ticket)}` : ''
this.$tab.reLaunch(`/pages/cas/callback${q}`)
return
} catch (e) {
this.$tab.reLaunch('/pages/cas/callback')
return
}
}
// 仅在开启 CAS 时走统一认证
if (casEnable && casServer && casService) {
// 1.1) 如果落在 /login 并携带 redirect=...cas-callback,强制跳到回调地址(避免看到PC登录页)
try {
const u2 = new URL(window.location.href)
const p2 = u2.pathname || ''
const red = u2.searchParams.get('redirect') || ''
if (/^\/login$/.test(p2) && /\/(cas|app-cas)\/cas-callback/.test(red)) {
window.location.href = `${window.location.origin}${red}`
return
}
} catch (e) { /* noop */ }
// 2) 在 /app-cas/ 路径下,强制跳转到统一认证登录页(无论是否已登录)
if (/^\/app-cas\//.test(path)) {
let effectiveService = casService
// 使用统一的CAS回调地址,但通过sessionStorage记录来源
try {
sessionStorage.setItem('pasd_sso_from', 'app-cas')
} catch (e) { /* noop */ }
window.location.href = `${casServer}?service=${encodeURIComponent(effectiveService)}`
return
}
// 3) 其它未登录入口,直接跳转到统一认证登录页
if (!hasToken) {
let effectiveService = casService
// 记录PC端访问来源
try {
sessionStorage.removeItem('pasd_sso_from')
} catch (e) { /* noop */ }
window.location.href = `${casServer}?service=${encodeURIComponent(effectiveService)}`
return
}
}
// 未启用 CAS 或非 /app-cas/ 路径且未登录的场景,仍回到系统账号登录页
if (!hasToken && !whiteList.includes(currentPath)) {
this.$tab.reLaunch('/pages/login')
}
}
}
}
</script>
<style lang="scss">
@import '@/static/scss/index.scss'
@import '@/static/scss/index.scss'
</style>

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询巡检异常列表
export function listAbnormal(query) {
return request({
url: '/inspection/abnormal/list',
method: 'get',
params: query
})
}
// 查询巡检异常详细
export function getAbnormal(id) {
return request({
url: '/inspection/abnormal/' + id,
method: 'get'
})
}
// 新增巡检异常
export function addAbnormal(data) {
return request({
url: '/inspection/abnormal',
method: 'post',
data: data
})
}
// 修改巡检异常
export function updateAbnormal(data) {
return request({
url: '/inspection/abnormal',
method: 'put',
data: data
})
}
// 删除巡检异常
export function delAbnormal(id) {
return request({
url: '/inspection/abnormal/' + id,
method: 'delete'
})
}

View File

@@ -65,4 +65,43 @@ export const getRouters = () => {
url: '/getRouters',
method: 'get'
})
}
// CAS 登录方法(通过请求参数传递 ticket 与 service)
export function casLogin(ticket, service) {
return request({
url: '/cas/login',
headers: {
isToken: false
},
method: 'post',
// 后端使用 @RequestParam 接收参数
params: { ticket, service }
})
}
// 移动端CAS登录方法
export function casAppLogin(ticket) {
return request({
url: '/cas/app/login',
headers: {
isToken: false
},
method: 'post',
// 后端使用 @RequestParam 接收参数
params: { ticket }
})
}
// 统一CAS登录方法
export function casUnifiedLogin(ticket) {
return request({
url: '/cas/unified/login',
headers: {
isToken: false
},
method: 'post',
// 后端使用 @RequestParam 接收参数
params: { ticket }
})
}

View File

@@ -1,9 +1,22 @@
// 应用全局配置
module.exports = {
// baseUrl: 'http://172.16.129.101:8080/dev-api/',
baseUrl: 'https://pasd.gxsdxy.cn/prod-api/',
// baseUrl: 'https://pasd.gxsdxy.cn/prod-api/',
// baseUrl: 'http://172.16.129.101:8080',//172.16.129.101
// baseUrl: 'https://192.168.211.59:8080/',
// baseUrl: 'http://localhost:8080',
// 生产环境:通过 Nginx /prod-api 代理到后端
baseUrl: 'https://pasd.gxsdxy.cn/prod-api',
// 是否启用 CAS 登录(与系统账号登录并存,H5 自动跳转)
// 发布 CAS 端:开启
casEnable: true,
// CAS 服务端登录地址(示例为统一认证平台登录地址)
casServer: 'https://rsso.gxsdxy.cn/login',
// PC 端
casService: 'https://pasd.gxsdxy.cn/cas',
// 移动端(关键) - 现在统一使用相同的回调地址,由后端处理设备检测
casServiceMobile: 'https://pasd.gxsdxy.cn/cas',
// 应用信息
appInfo: {
// 应用名称
@@ -16,13 +29,13 @@ module.exports = {
site_url: "http://pasd.gxsdxy.cn",
// 政策协议
agreements: [{
title: "隐私政策",
url: "http://pasd.gxsdxy.cn/protocol.html"
},
{
title: "用户服务协议",
url: "http://pasd.gxsdxy.cn/protocol.html"
}
title: "隐私政策",
url: "http://pasd.gxsdxy.cn/protocol.html"
},
{
title: "用户服务协议",
url: "http://pasd.gxsdxy.cn/protocol.html"
}
]
}
}
}

View File

@@ -88,6 +88,7 @@
"router" : {
"mode" : "hash",
"base" : "./"
}
},
"publicPath": "./"
}
}

View File

@@ -4,6 +4,12 @@
"style": {
"navigationBarTitleText": "登录"
}
}, {
"path": "pages/cas/callback",
"style": {
"navigationBarTitleText": "CAS回调",
"navigationStyle": "custom"
}
}, {
"path": "pages/register",
"style": {
@@ -79,7 +85,7 @@
"style": {
"navigationBarTitleText": "申报记录",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#bbbbbb",
"navigationBarBackgroundColor": "#1677ff",
"enablePullDownRefresh": true, //配置后,可以下拉刷新,上拉加载`
"onReachBottomDistance": 100
}
@@ -88,8 +94,16 @@
"path": "pages/work/sidebar/safetyDeclaratio/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#bbbbbb",
"navigationBarTitleText": "隐患申报"
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTitleText": "异常隐患巡检"
}
},
{
"path": "pages/work/sidebar/filingDetail/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTitleText": "申报详情"
}
},
{
@@ -113,7 +127,7 @@
},
"style": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#bbbbbb",
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTitleText": "扫码签到"
}
},
@@ -124,7 +138,7 @@
"path": "pages/work/inspection/inspectionEx/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#bbbbbb",
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTitleText": "异常巡检"
}
},
@@ -132,13 +146,60 @@
"app-plus": {
"titleNView": false
},
"path": "pages/work/inspection/checkInUserList/index",
"style": {
"navigationBarTitleText": "用户打卡列表",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1677ff"
}
},
{
"app-plus": { "titleNView": false },
"path": "pages/work/inspection/checkInRecord/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#bbbbbb",
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTitleText": "打卡记录"
}
},
{
"app-plus": {
"titleNView": false
},
"usingComponents": true
"path": "pages/work/inspection/abnormalList/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "巡检异常列表",
"navigationBarBackgroundColor": "#1677ff"
}
},
{
"app-plus": {
"titleNView": false
},
"path": "pages/work/inspection/abnormalDetail/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "异常详情",
"navigationBarBackgroundColor": "#1677ff"
}
},
{
"path": "pages/work/inspection/checkInDetail/index",
"style": {
"navigationBarTitleText": "打卡详情",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1677ff"
}
},
{
"app-plus": { "titleNView": false },
"path": "pages/work/inspection/recordList/index",
"style": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTitleText": "巡检记录"
}
}
],
"tabBar": {

42
pages/cas/callback.vue Normal file
View File

@@ -0,0 +1,42 @@
<template>
<view class="cas-callback">
<text>正在处理统一认证登录...</text>
</view>
</template>
<script>
import appConfig from '@/config'
import { casUnifiedLogin } from '@/api/login'
export default {
onLoad(options) {
// H5 环境下 options 可携带 ticket 参数
const ticket = options?.ticket || (this.$route && this.$route.query && this.$route.query.ticket)
if (!ticket) {
this.$modal.msgError('CAS 未返回票据')
this.$tab.reLaunch('/pages/login')
return
}
// 使用移动端CAS登录API,因为现在Nginx会根据User-Agent和请求参数自动路由
this.$store.dispatch('CasAppLogin', ticket).then(() => {
// 拉取用户信息并进入工作台
this.$store.dispatch('GetInfo').then(() => {
this.$tab.reLaunch('/pages/work/index')
}).catch(() => {
this.$tab.reLaunch('/pages/work/index')
})
}).catch(err => {
this.$modal.msgError('移动端CAS登录失败:' + (err && err.toString ? err.toString() : '未知错误'))
this.$tab.reLaunch('/pages/login')
})
}
}
</script>
<style>
.cas-callback {
padding: 40rpx;
color: #666;
}
</style>

View File

@@ -2,7 +2,7 @@
<view class="content">
<image class="logo" src="@/static/logo.png"></image>
<view class="text-area">
<text class="title">Hello RuoYi</text>
<text class="title">平安水电</text>
</view>
</view>
</template>

View File

@@ -17,7 +17,7 @@
<view class="input-item flex align-center" style="width: 60%;margin: 0px;" v-if="captchaEnabled">
<view class="iconfont icon-code icon"></view>
<input v-model="loginForm.code" type="number" class="input" placeholder="请输入验证码" maxlength="4" />
<view class="login-code">
<view class="login-code">
<image :src="codeUrl" @click="getCode" class="login-code-img"></image>
</view>
</view>
@@ -25,179 +25,198 @@
<button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
</view>
<view class="reg text-center" v-if="register">
<text class="text-grey1">没有账号</text>
<text class="text-grey1">没有账号?</text>
<text @click="handleUserRegister" class="text-blue">立即注册</text>
</view>
<view class="xieyi text-center">
<text class="text-grey1">登录即代表同意</text>
<text @click="handleUserAgrement" class="text-blue">用户协议</text>
<text @click="handlePrivacy" class="text-blue">隐私协议</text>
<text @click="handleUserAgrement" class="text-blue">
<用户协议>
</text>
<text @click="handlePrivacy" class="text-blue">
<隐私协议>
</text>
</view>
</view>
</view>
</template>
<script>
import { getCodeImg } from '@/api/login'
import { getCodeImg } from '@/api/login'
export default {
data() {
return {
codeUrl: "",
captchaEnabled: true,
// 用户注册开关
register: false,
globalConfig: getApp().globalData.config,
loginForm: {
username: "",
password: "",
code: "",
uuid: ''
}
}
},
created() {
this.getCode()
},
methods: {
// 用户注册
handleUserRegister() {
this.$tab.redirectTo(`/pages/register`)
},
// 隐私协议
handlePrivacy() {
let site = this.globalConfig.appInfo.agreements[0]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
// 用户协议
handleUserAgrement() {
let site = this.globalConfig.appInfo.agreements[1]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
// 获取图形验证码
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (this.captchaEnabled) {
this.codeUrl = 'data:image/gif;base64,' + res.img
this.loginForm.uuid = res.uuid
}
})
},
// 登录方法
async handleLogin() {
if (this.loginForm.username === "") {
this.$modal.msgError("请输入您的账号")
} else if (this.loginForm.password === "") {
this.$modal.msgError("请输入您的密码")
} else if (this.loginForm.code === "" && this.captchaEnabled) {
this.$modal.msgError("请输入验证码")
} else {
this.$modal.loading("登录中,请耐心等待...")
this.pwdLogin()
}
},
// 密码登录
async pwdLogin() {
this.$store.dispatch('Login', this.loginForm).then(() => {
this.$modal.closeLoading()
this.loginSuccess()
}).catch(() => {
if (this.captchaEnabled) {
this.getCode()
}
})
},
// 登录成功后,处理函数
loginSuccess(result) {
// 设置用户信息
this.$store.dispatch('GetInfo').then(res => {
this.$tab.reLaunch('/pages/work/index')
})
export default {
data() {
return {
codeUrl: "",
captchaEnabled: true,
// 用户注册开关
register: false,
globalConfig: getApp().globalData.config,
loginForm: {
username: "",
password: "",
code: "",
uuid: ''
}
}
},
created() {
this.getCode()
// H5 场景下:若启用 CAS 且未登录,直接跳统一认证
//#ifdef H5
try {
const cfg = getApp().globalData && getApp().globalData.config
const hasToken = this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.token
if (cfg && cfg.casEnable && !hasToken) {
let service = cfg.casService
const p = (window.location && window.location.pathname) || ''
if (/^\/app-cas\//.test(p)) {
service = cfg.casServiceMobile
}
const url = `${cfg.casServer}?service=${encodeURIComponent(service)}`
window.location.href = url
}
} catch (e) { /* noop */ }
//#endif
},
methods: {
// 用户注册
handleUserRegister() {
this.$tab.redirectTo(`/pages/register`)
},
// 隐私协议
handlePrivacy() {
let site = this.globalConfig.appInfo.agreements[0]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
// 用户协议
handleUserAgrement() {
let site = this.globalConfig.appInfo.agreements[1]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
// 获取图形验证码
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (this.captchaEnabled) {
this.codeUrl = 'data:image/gif;base64,' + res.img
this.loginForm.uuid = res.uuid
}
})
},
// 登录方法
async handleLogin() {
if (this.loginForm.username === "") {
this.$modal.msgError("请输入您的账号")
} else if (this.loginForm.password === "") {
this.$modal.msgError("请输入您的密码")
} else if (this.loginForm.code === "" && this.captchaEnabled) {
this.$modal.msgError("请输入验证码")
} else {
this.$modal.loading("登录中,请耐心等待...")
this.pwdLogin()
}
},
// 密码登录
async pwdLogin() {
this.$store.dispatch('Login', this.loginForm).then(() => {
this.$modal.closeLoading()
this.loginSuccess()
}).catch(() => {
if (this.captchaEnabled) {
this.getCode()
}
})
},
// 登录成功后,处理函数
loginSuccess(result) {
// 设置用户信息
this.$store.dispatch('GetInfo').then(res => {
this.$tab.reLaunch('/pages/work/index')
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
page {
background-color: #ffffff;
}
.normal-login-container {
.normal-login-container {
width: 100%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
image {
border-radius: 4px;
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
}
.reg {
margin-top: 15px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
.login-code {
height: 38px;
float: right;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
}
}
.title {
margin-left: 10px;
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
}
.reg {
margin-top: 15px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
.login-code {
height: 38px;
float: right;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
}
}
}
}
</style>

View File

@@ -17,13 +17,13 @@
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>官方邮箱</view>
<view class="text-right">ruoyi@xx.com</view>
<view class="text-right"></view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>服务热线</view>
<view class="text-right">400-999-9999</view>
<view class="text-right"></view>
</view>
</view>
<view class="list-cell list-cell-arrow">
@@ -38,7 +38,7 @@
</view>
<view class="copyright">
<view>Copyright &copy; 2022 ruoyi.vip All Rights Reserved.</view>
<view>Copyright &copy; 2022 平安水电 All Rights Reserved.</view>
</view>
</view>
</template>

View File

@@ -20,21 +20,7 @@
data() {
return {
list: [{
icon: 'iconfont icon-github',
title: '若依问题',
childList: [{
title: '若依开源吗?',
content: '开源'
}, {
title: '若依可以商用吗?',
content: '可以'
}, {
title: '若依官网地址多少?',
content: 'http://ruoyi.vip'
}, {
title: '若依文档地址多少?',
content: 'http://doc.ruoyi.vip'
}]
},
{
icon: 'iconfont icon-help',

View File

@@ -27,22 +27,6 @@
<view class="content-section">
<view class="mine-actions grid col-4 text-center">
<view class="action-item" @click="handleJiaoLiuQun">
<view class="iconfont icon-friendfill text-pink icon"></view>
<text class="text">交流群</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-service text-blue icon"></view>
<text class="text">在线客服</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-community text-mauve icon"></view>
<text class="text">反馈社区</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-dianzan text-green icon"></view>
<text class="text">点赞我们</text>
</view>
</view>
<view class="menu-list">
@@ -52,7 +36,7 @@
<view>编辑资料</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleHelp">
<!-- <view class="list-cell list-cell-arrow" @click="handleHelp">
<view class="menu-item-box">
<view class="iconfont icon-help menu-icon"></view>
<view>常见问题</view>
@@ -63,7 +47,7 @@
<view class="iconfont icon-aixin menu-icon"></view>
<view>关于我们</view>
</view>
</view>
</view> -->
<view class="list-cell list-cell-arrow" @click="handleToSetting">
<view class="menu-item-box">
<view class="iconfont icon-setting menu-icon"></view>

View File

@@ -52,7 +52,7 @@
uniGridList: [{
text: '打卡记录',
type: "calendar-filled",
ifData: "checkRole(['admin'])"
ifData: false
},
{
text: '异常巡检',
@@ -64,6 +64,11 @@
type: "navigate-filled",
ifData: "checkRole(['admin',common])"
},
{
text: '巡检记录',
type: "list",
ifData: "checkRole(['admin'])"
},
// {
// text: '申报记录',
// type: "list",
@@ -105,12 +110,14 @@
if (index == 1) {
this.$tab.navigateTo("/pages/work/inspection/checkInRecord/index");
} else if (index == 2) {
this.$tab.navigateTo("/pages/work/inspection/inspectionEx/index");
} else if (index == 3) {
this.$tab.navigateTo("/pages/work/sidebar/safetyDeclaratio/index");
} else if (index == 4) {
this.$tab.navigateTo("/pages/work/sidebar/filingLog/index");
}
this.$tab.navigateTo("/pages/work/inspection/abnormalList/index");
} else if (index == 3) {
this.$tab.navigateTo("/pages/work/sidebar/filingLog/index");
} else if (index == 4) {
this.$tab.navigateTo("/pages/work/inspection/recordList/index");
} else if (index == 5) {
this.$tab.navigateTo("/pages/work/sidebar/filingLog/index");
}
}
}

View File

@@ -0,0 +1,101 @@
<template>
<view class="container">
<uni-section title="异常详情" type="line" class="mb-10" />
<uni-card :title="detail.inspectionPoint" :is-shadow="true" @click="onCardClick">
<uni-row class="row" :width="730">
<uni-col :span="16">
<view>
<text class="uni-body">巡检类型{{ typeLabel(detail.inspectionType) }}</text>
</view>
<view>
<text class="uni-body">巡检人{{ detail.inspectorId }}</text>
</view>
<view>
<text class="uni-body">巡检时间{{ formatDate(detail.inspectionTime) }}</text>
</view>
<view>
<text class="uni-body">备注{{ detail.remark || '-' }}</text>
</view>
</uni-col>
<uni-col :span="8">
<view class="thumbs">
<image v-for="(img,idx) in imageArray(detail.inspectionImg)" :key="idx" :src="imageUrl(img)" mode="aspectFill" class="thumb" @click.stop="preview(img, detail.inspectionImg)" />
</view>
</uni-col>
</uni-row>
</uni-card>
</view>
</template>
<script>
import { getAbnormal } from '@/api/inspection/abnormal.js'
import { listData } from '@/api/system/dict/data.js'
import config from '@/config'
export default {
data() {
return {
id: null,
detail: {},
typeDict: [],
}
},
async onLoad(options) {
this.id = options?.id || null
await this.initDict()
if (this.id) {
await this.fetchDetail()
}
},
methods: {
async initDict() {
const typeData = await listData({ dictType: 'inspection_type' })
this.typeDict = typeData.rows || []
},
typeLabel(val) {
const item = this.typeDict.find(i => String(i.dictValue) === String(val))
return item ? item.dictLabel : (val ?? '-')
},
async fetchDetail() {
const res = await getAbnormal(this.id)
this.detail = res?.data || {}
},
imageArray(inspectionImg) {
if (!inspectionImg) return []
return inspectionImg.split(',').filter(Boolean)
},
imageUrl(path) {
if (!path) return ''
return config.baseUrl + path
},
preview(current, all) {
const urls = this.imageArray(all).map(p => this.imageUrl(p))
uni.previewImage({ current: this.imageUrl(current), urls })
},
formatDate(val) {
if (!val) return ''
try {
const d = new Date(val)
const y = d.getFullYear()
const m = String(d.getMonth()+1).padStart(2,'0')
const dd = String(d.getDate()).padStart(2,'0')
const hh = String(d.getHours()).padStart(2,'0')
const mm = String(d.getMinutes()).padStart(2,'0')
const ss = String(d.getSeconds()).padStart(2,'0')
return `${y}-${m}-${dd} ${hh}:${mm}:${ss}`
} catch(e) { return val }
},
onCardClick(type) {
// 可按需处理卡片各区域点击,目前不做特殊逻辑
}
}
}
</script>
<style lang="scss">
.container { padding: 10px; }
.thumbs { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
.thumb { width: 60px; height: 60px; border-radius: 6px; background: #f5f5f5; }
.mb-10 { margin-bottom: 10px; }
</style>

View File

@@ -0,0 +1,188 @@
<template>
<view class="container">
<uni-section title="巡检异常列表" type="line" class="mb-10">
<template v-slot:right>
<button size="mini" type="primary" @click.stop="goAdd">新增</button>
</template>
</uni-section>
<view class="filters">
<uni-data-select v-model="queryParams.inspectionType" :localdata="selectList" placeholder="巡检类型" />
<uni-datetime-picker type="daterange" v-model="dateRange" :clear-icon="true" start="1990-01-01" end="2099-12-31" />
<button class="ml-6" size="mini" type="primary" @click="handleQuery">搜索</button>
<button class="ml-6" size="mini" @click="resetQuery">重置</button>
</view>
<uni-card v-for="item in list" :key="item.id" :title="item.inspectionPoint" @click="onCardClick($event, item)">
<uni-row class="row" :width="730">
<uni-col :span="16">
<view>
<text class="uni-body">巡检人{{ item.inspectorId }}</text>
</view>
<view>
<text class="uni-body">巡检时间{{ formatDate(item.inspectionTime) }}</text>
</view>
<view>
<text class="uni-body">备注{{ item.remark || '-' }}</text>
</view>
</uni-col>
<uni-col :span="8">
<view class="thumbs">
<image v-for="(img,idx) in firstThreeImages(item.inspectionImg)" :key="idx" :src="imageUrl(img)" mode="aspectFill" class="thumb" />
</view>
</uni-col>
</uni-row>
</uni-card>
<uni-load-more :status="loadStatus" />
</view>
</template>
<script>
import { listAbnormal } from '@/api/inspection/abnormal.js';
import { listData } from '@/api/system/dict/data.js';
import config from '@/config';
export default {
data() {
return {
loadStatus: 'more',
loading: false,
total: 0,
list: [],
selectList: [],
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 10,
inspectionPoint: '',
inspectorId: '',
inspectionType: null,
params: {}
}
}
},
created() {
this.initDict()
this.getList()
},
// 触底加载更多(页面滚动到底部)
onReachBottom() {
if (this.loadStatus !== 'more' || this.loading) return
this.loadStatus = 'loading'
this.queryParams.pageNum += 1
this.fetchList({ append: true })
},
// 页面重新显示时自动刷新列表(新增返回后生效)
onShow() {
this.getList()
},
methods: {
async initDict() {
const typeData = await listData({ dictType: 'inspection_type' })
this.selectList = typeData.rows.map(i => ({ value: i.dictValue, text: i.dictLabel }))
},
imageUrl(path) {
if (!path) return ''
return config.baseUrl + path
},
firstThreeImages(inspectionImg) {
if (!inspectionImg) return []
return inspectionImg.split(',').slice(0,3)
},
formatDate(val) {
if (!val) return ''
try {
const d = new Date(val)
const y = d.getFullYear()
const m = String(d.getMonth()+1).padStart(2,'0')
const dd = String(d.getDate()).padStart(2,'0')
const hh = String(d.getHours()).padStart(2,'0')
const mm = String(d.getMinutes()).padStart(2,'0')
const ss = String(d.getSeconds()).padStart(2,'0')
return `${y}-${m}-${dd} ${hh}:${mm}:${ss}`
} catch(e) { return val }
},
buildParams() {
if (this.dateRange && this.dateRange.length === 2) {
this.queryParams.params = {
beginTime: this.dateRange[0],
endTime: this.dateRange[1]
}
} else {
this.queryParams.params = {}
}
},
async fetchList({ append = false } = {}) {
try {
this.loading = true
this.buildParams()
const res = await listAbnormal(this.queryParams)
const rows = res?.rows || []
// 若接口提供 total 字段则使用,否则根据 pageSize 判断是否还有更多
this.total = typeof res?.total === 'number' ? res.total : (append ? this.list.length + rows.length : rows.length)
if (append) {
this.list = this.list.concat(rows)
} else {
this.list = rows
}
// 根据是否还有更多数据设置加载状态
if (typeof res?.total === 'number') {
this.loadStatus = this.list.length < this.total ? 'more' : 'noMore'
} else {
// 当本次返回数量等于 pageSize默认还有下一页
const hasMore = rows.length === this.queryParams.pageSize
this.loadStatus = hasMore ? 'more' : 'noMore'
}
} catch (e) {
this.loadStatus = 'noMore'
} finally {
this.loading = false
}
},
async getList() {
// 初始化查询第一页
this.queryParams.pageNum = 1
this.loadStatus = 'loading'
await this.fetchList({ append: false })
},
onCardClick(type, item) {
// 仅在内容区域点击时跳转title/extra也可按需
if (!item) return
this.goDetail(item)
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
inspectionPoint: '',
inspectorId: '',
inspectionType: null,
params: {}
}
this.dateRange = []
this.getList()
},
goAdd() {
this.$tab.navigateTo('/pages/work/inspection/inspectionEx/index')
},
goDetail(item) {
const id = item?.id
if (!id) return
this.$tab.navigateTo(`/pages/work/inspection/abnormalDetail/index?id=${id}`)
}
}
}
</script>
<style lang="scss">
.container { padding: 10px; }
.filters { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
.thumbs { display: flex; gap: 6px; justify-content: flex-end; }
.thumb { width: 60px; height: 60px; border-radius: 6px; background: #f5f5f5; }
.mb-10 { margin-bottom: 10px; }
.ml-6 { margin-left: 6px; }
</style>

View File

@@ -0,0 +1,96 @@
<template>
<view class="detail-page">
<uni-section class="mb-10" title="打卡详情" type="line"></uni-section>
<uni-card isShadow>
<view class="detail-row">
<text class="label">巡检点</text>
<text class="value">{{ detail.inspectionPoint || '-' }}</text>
</view>
<view class="detail-row">
<text class="label">巡检人</text>
<text class="value">{{ detail.inspectorUser || detail.inspectorName || '-' }}</text>
</view>
<view class="detail-row">
<text class="label">打卡时间</text>
<text class="value">{{ formatDate(detail.inspectionTime) }}</text>
</view>
<view class="detail-row" v-if="detail.remark">
<text class="label">备注</text>
<text class="value">{{ detail.remark }}</text>
</view>
<view v-if="images.length" class="img-group">
<image v-for="(img, idx) in images" :key="idx" :src="imageUrl(img)" mode="aspectFill" class="img" @click="previewImage(idx)" />
</view>
</uni-card>
</view>
</template>
<script>
import { getRecord } from '@/api/inspection/record.js'
import config from '@/config'
export default {
data() {
return {
id: null,
detail: {},
images: []
}
},
onLoad(query) {
this.id = query.id
this.fetchDetail()
},
methods: {
async fetchDetail() {
if (!this.id) {
uni.showToast({ title: '缺少记录ID', icon: 'none' })
return
}
try {
const res = await getRecord(this.id)
this.detail = (res && res.data) ? res.data : {}
this.images = this.resolveImages(this.detail.inspectionImg || this.detail.clockInImg || this.detail.signImg)
} catch (e) {
console.error('fetch record detail error', e)
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
resolveImages(imgs) {
if (!imgs) return []
if (Array.isArray(imgs)) return imgs
if (typeof imgs === 'string') return imgs.split(',').filter(Boolean)
return []
},
imageUrl(path) {
if (!path) return ''
if (/^https?:\/\//.test(path)) return path
return `${config.baseUrl}${path.startsWith('/') ? path : '/' + path}`
},
previewImage(index = 0) {
if (!this.images.length) return
uni.previewImage({
current: index,
urls: this.images.map(this.imageUrl)
})
},
formatDate(dateStr) {
if (!dateStr) return '-'
const d = new Date(dateStr)
if (isNaN(d.getTime())) return dateStr
const pad = n => (n < 10 ? ('0' + n) : n)
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
}
}
</script>
<style scoped>
.detail-page { padding: 10px; }
.detail-row { display: flex; margin-bottom: 8px; }
.label { color: #666; width: 80px; }
.value { color: #333; }
.img-group { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.img { width: 100px; height: 100px; border-radius: 6px; background: #f5f5f5; }
</style>

View File

@@ -7,24 +7,84 @@
</template>
</uni-section>
<!-- 汇总图表 -->
<qiun-data-charts type="ring" :opts="opts" :chartData="chartData" :errorShow="false" :tapLegend="false" />
<!-- 统计数据卡片 -->
<uni-row :width="730" class="stats">
<uni-col :span="6"><view class="stat"><text class="stat-label">总打卡</text><text class="stat-value">{{ stats.total }}</text></view></uni-col>
<uni-col :span="6"><view class="stat"><text class="stat-label">活跃天</text><text class="stat-value">{{ stats.activeDays }}</text></view></uni-col>
<uni-col :span="6"><view class="stat"><text class="stat-label">平均/</text><text class="stat-value">{{ stats.avgPerDay }}</text></view></uni-col>
<uni-col :span="6"><view class="stat"><text class="stat-label">最长连打</text><text class="stat-value">{{ stats.maxStreak }}</text></view></uni-col>
</uni-row>
<!-- 统计概览 -->
<uni-section class="mb-10" title="统计概览" type="line" />
<view class="charts">
<view class="chart-box">
<qiun-data-charts type="column" :opts="optsBar" :chartData="chartWeek" :errorShow="false" />
</view>
<view class="chart-box">
<qiun-data-charts type="column" :opts="optsBar" :chartData="chartHour" :errorShow="false" />
</view>
</view>
<!-- 打卡数据卡片样式与异常巡检一致 -->
<uni-card v-if="summary.clockIn > 0 || latestRecord" :title="'本月总打卡:' + (summary.clockIn || 0)" :thumbnail="avatar" @click="onSummaryClick">
<uni-row class="demo-uni-row" :width="nvueWidth">
<!-- 左侧信息 -->
<uni-col :span="16">
<view>
<text class="uni-body">打卡状态{{ summary.clockState || '-' }}</text>
</view>
<view v-if="latestRecord">
<text class="uni-body">最近时间{{ formatDate(latestRecord.inspectionTime) }}</text>
</view>
<view v-if="latestRecord">
<text class="uni-body">巡检点{{ latestRecord.inspectionPoint || '-' }}</text>
</view>
<view v-if="latestRecord">
<text class="uni-body">巡检人{{ latestRecord.inspectorUser || '-' }}</text>
</view>
</uni-col>
<!-- 右侧缩略图 -->
<uni-col :span="8" v-if="latestRecord">
<view class="thumbs" v-if="firstThreeImages(latestImages).length">
<image v-for="(img,i) in firstThreeImages(latestImages)" :key="i" :src="imageUrl(img)" mode="aspectFill" class="thumb" @click.stop="previewImages(latestImages, i)" />
</view>
<view class="tag-view">
<uni-tag :inverted="true" :circle="true" text="已打卡" size="small" />
</view>
</uni-col>
</uni-row>
</uni-card>
<uni-section class="mb-10" title="打卡列表" List="info">
<template v-slot:right>
<uni-data-select style="width:70px;" :clear=false v-model="queryform.weekType" :localdata="weekList"
@change="weekTypeSelectChange"></uni-data-select>
</template>
</uni-section>
<uni-card v-for="item of cardList" :title="item.inspectionPoint" :thumbnail="avatar">
<!-- 列表卡片点击可查看详情 -->
<uni-card v-for="item of cardList" :key="item.id || item.recordId" :title="item.inspectionPoint" :thumbnail="avatar" @click="onCardClick($event, item)">
<uni-row class="demo-uni-row" :width="nvueWidth">
<uni-col :span="20">
<!-- 左侧信息 -->
<uni-col :span="16">
<view>
<text class="uni-body">
打卡时间{{item.inspectionTime}}
</text>
<text class="uni-body">打卡时间{{ formatDate(item.inspectionTime) }}</text>
</view>
<view>
<text class="uni-body">巡检人{{ item.inspectorUser || '-' }}</text>
</view>
<view v-if="item.remark">
<text class="uni-body">备注{{ item.remark }}</text>
</view>
</uni-col>
<uni-col :span="4">
<!-- 右侧缩略图 -->
<uni-col :span="8">
<view class="thumbs" v-if="firstThreeImages(item.inspectionImg || item.clockInImg || item.signImg).length">
<image v-for="(img,i) in firstThreeImages(item.inspectionImg || item.clockInImg || item.signImg)" :key="i" :src="imageUrl(img)" mode="aspectFill" class="thumb" @click.stop="previewImages(item.inspectionImg || item.clockInImg || item.signImg, i)" />
</view>
<view class="tag-view">
<uni-tag :inverted="true" :circle="true" text="已打卡" size="small" />
</view>
@@ -38,9 +98,8 @@
<script>
import {
listRecordView
} from '@/api/inspection/record.js'
import { listRecordView, getRecord } from '@/api/inspection/record.js'
import config from '@/config'
export default {
data() {
return {
@@ -50,6 +109,10 @@
// 数据可视化
inspectionRecordViewTable: [],
cardList: [],
// 汇总卡片数据
summary: { clockIn: 0, clockState: '' },
latestRecord: null,
latestImages: [],
weekList: [{
text: "全部",
value: 0
@@ -128,92 +191,97 @@
chartData: {},
//这里的 opts 是图表类型 type="ring" 的全部配置参数,您可以将此配置复制到 config-ucharts.js 文件中下标为 ['ring'] 的节点中来覆盖全局默认参数。实际应用过程中 opts 只需传入与全局默认参数中不一致的【某一个属性】即可实现同类型的图表显示不同的样式,达到页面简洁的需求。
opts: {
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
color: ["#1890FF", "#91CB74", "#FAC858", "#EE6666", "#73C0DE", "#3CA272", "#FC8452", "#9A60B4", "#ea7ccc"],
padding: [5, 5, 5, 5],
fontSize: 13,
fontColor: "#666666",
dataLabel: false,
dataPointShape: false,
dataPointShapeType: "hollow",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: true,
position: "bottom",
lineHeight: 25,
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
hiddenColor: "#CECECE",
itemGap: 10
},
title: {
name: "已打卡",
fontSize: 15,
color: "#666666",
offsetX: 0,
offsetY: 0
},
subtitle: {
name: "30",
fontSize: 25,
color: "#fbbd08",
offsetX: 0,
offsetY: 0
},
extra: {
ring: {
ringWidth: 30,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: false,
borderWidth: 3,
borderColor: "#FFFFFF",
centerColor: "#FFFFFF",
customRadius: 0,
linearType: "none"
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666"
}
}
}
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
color: ["#1890FF", "#91CB74", "#FAC858", "#EE6666", "#73C0DE", "#3CA272", "#FC8452", "#9A60B4", "#ea7ccc"],
padding: [5, 5, 5, 5],
fontSize: 13,
fontColor: "#666666",
dataLabel: false,
dataPointShape: false,
dataPointShapeType: "hollow",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: true,
position: "bottom",
lineHeight: 25,
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
hiddenColor: "#CECECE",
itemGap: 10
},
title: {
name: "已打卡",
fontSize: 15,
color: "#666666",
offsetX: 0,
offsetY: 0
},
subtitle: {
name: "30",
fontSize: 25,
color: "#fbbd08",
offsetX: 0,
offsetY: 0
},
extra: {
ring: {
ringWidth: 30,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: false,
borderWidth: 3,
borderColor: "#FFFFFF",
centerColor: "#FFFFFF",
customRadius: 0,
linearType: "none"
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666"
}
}
},
// 新增:统计与柱状图数据
stats: { total: 0, activeDays: 0, avgPerDay: 0, maxStreak: 0 },
optsBar: { legend: { show: false }, xAxis: { disableGrid: true }, yAxis: { disableGrid: false } },
chartWeek: { categories: [], series: [{ name: '按周几', data: [] }] },
chartHour: { categories: [], series: [{ name: '按时段', data: [] }] },
};
},
methods: {
@@ -223,6 +291,12 @@
this.queryform.month = currentMonth;
listRecordView(this.queryform).then(res => {
let data = res.data
// 汇总卡片数据
this.summary.clockIn = data.clockIn || 0
this.summary.clockState = data.clockState || ''
this.latestRecord = (data.inspectionRecordTables && data.inspectionRecordTables.length) ? data.inspectionRecordTables[0] : null
this.latestImages = this.latestRecord ? (this.latestRecord.inspectionImg || this.latestRecord.clockInImg || this.latestRecord.signImg || []) : []
// 图表数据
if (data.clockIn != 0) {
let obj = [{
"name": data.clockState,
@@ -232,7 +306,11 @@
} else {
this.inspectionRecordViewTable = []
}
this.cardList = data.inspectionRecordTables
this.cardList = data.inspectionRecordTables || []
// 新增:构建统计与柱状图
this.buildWeekChart(this.cardList)
this.buildHourChart(this.cardList)
this.buildStats(this.cardList)
})
this.getServerData()
},
@@ -278,9 +356,83 @@
this.chartData = JSON.parse(JSON.stringify(res));
}, 1000);
},
// 统一图片数组处理(支持字符串或数组)
firstThreeImages(imgs) {
if (!imgs) return []
if (Array.isArray(imgs)) return imgs.slice(0,3)
if (typeof imgs === 'string') return imgs.split(',').filter(Boolean).slice(0,3)
return []
},
imageUrl(path) {
if (!path) return ''
if (/^https?:\/\//.test(path)) return path
return `${config.baseUrl}${path.startsWith('/') ? path : '/' + path}`
},
previewImages(imgs, index=0) {
const list = Array.isArray(imgs) ? imgs : (typeof imgs === 'string' ? imgs.split(',').filter(Boolean) : [])
if (!list.length) return
uni.previewImage({
current: index,
urls: list.map(this.imageUrl)
})
},
formatDate(dateStr) {
if (!dateStr) return '-'
const d = new Date(dateStr)
if (isNaN(d.getTime())) return dateStr
const pad = n => (n<10?('0'+n):n)
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
},
onCardClick(e, item) {
this.goDetail(item)
},
onSummaryClick() {
// 若有最近一条记录点击汇总卡片也进入其详情
if (this.latestRecord) {
this.goDetail(this.latestRecord)
}
},
goDetail(item) {
const id = item.id || item.recordId
if (!id) {
uni.showToast({ title: '记录ID缺失', icon: 'none' })
return
}
uni.navigateTo({ url: `/pages/work/inspection/checkInDetail/index?id=${id}` })
},
// 新增统计构建方法
buildWeekChart(rows) {
const names = ['周一','周二','周三','周四','周五','周六','周日']
const counts = Array(7).fill(0)
(rows || []).forEach(r => { const d = new Date(r.inspectionTime); if (!isNaN(d)) { const idx = (d.getDay()+6)%7; counts[idx]++ } })
this.chartWeek = { categories: names, series: [{ name: '按周几', data: counts }] }
},
buildHourChart(rows) {
const buckets = Array(24).fill(0)
(rows || []).forEach(r => { const d = new Date(r.inspectionTime); if (!isNaN(d)) { buckets[d.getHours()]++ } })
const labels = buckets.map((_,h)=> (h<10?('0'+h):h)+':00')
this.chartHour = { categories: labels, series: [{ name: '按时段', data: buckets }] }
},
buildStats(rows) {
const list = rows || []
const total = list.length
const daySet = new Set(list.map(r => this.formatDate(r.inspectionTime).slice(0,10)))
const activeDays = daySet.size
const avgPerDay = activeDays ? (total/activeDays).toFixed(2) : 0
// 计算最长连续天数
const days = Array.from(daySet).sort()
let maxStreak = 0, cur = 0, prev = null
const toDate = s => new Date(s+'T00:00:00')
days.forEach(s => { if (!prev) { cur=1; } else { const diff=(toDate(s)-toDate(prev))/(24*3600*1000); cur = diff===1 ? cur+1 : 1 } maxStreak = Math.max(maxStreak, cur); prev = s })
this.stats = { total, activeDays, avgPerDay, maxStreak }
}
},
created() {
this.getList()
},
onShow() {
// 返回页面时自动刷新列表,保持数据最新
this.getList()
}
};
</script>
@@ -304,6 +456,19 @@
/* 请根据实际需求修改父元素尺寸,组件自动识别宽高 */
.charts-box {
width: 100%;
height: 300px;
height: auto;
}
.thumbs { display:flex; gap:6px; justify-content:flex-end; }
.thumb { width: 60px; height: 60px; border-radius: 4px; background:#f5f5f5; }
.tag-view { margin-top: 8px; display:flex; justify-content:flex-end; }
/* 新增:统计与图表布局样式 */
.stats { margin: 10px 0; }
.stat { background:#fff; border-radius:8px; padding:10px; display:flex; flex-direction:column; align-items:center }
.stat-label { color:#666; font-size:12px }
.stat-value { color:#1677ff; font-weight:bold; font-size:18px }
.charts { display: grid; grid-template-columns: 1fr; gap: 12px; }
@media (min-width: 380px) { .charts { grid-template-columns: 1fr 1fr; } }
.chart-box { background: #fff; border-radius: 8px; padding: 6px; }
</style>

View File

@@ -0,0 +1,171 @@
<template>
<view class="container">
<uni-section title="用户打卡列表" type="line" class="mb-10">
<template v-slot:right>
<button class="ml-6" size="mini" type="primary" @click="handleQuery">搜索</button>
<button class="ml-6" size="mini" @click="resetQuery">重置</button>
</template>
</uni-section>
<view class="filters">
<uni-datetime-picker type="daterange" v-model="dateRange" :clear-icon="true" start="1990-01-01" end="2099-12-31" />
</view>
<uni-card v-for="item in list" :key="item.id || item.recordId" :title="item.inspectionPoint" @click="onCardClick($event, item)">
<uni-row class="row" :width="730">
<uni-col :span="16">
<view>
<text class="uni-body">打卡时间{{ formatDate(item.inspectionTime) }}</text>
</view>
<view>
<text class="uni-body">巡检人{{ item.inspectorUser || item.inspectorId || '-' }}</text>
</view>
<view v-if="item.remark">
<text class="uni-body">备注{{ item.remark }}</text>
</view>
</uni-col>
<uni-col :span="8">
<view class="thumbs">
<image v-for="(img,idx) in firstThreeImages(item.inspectionImg || item.clockInImg || item.signImg)" :key="idx" :src="imageUrl(img)" mode="aspectFill" class="thumb" />
</view>
</uni-col>
</uni-row>
</uni-card>
<uni-load-more :status="loadStatus" />
</view>
</template>
<script>
import { listRecord } from '@/api/inspection/record.js';
import config from '@/config';
export default {
data() {
return {
loadStatus: 'more',
loading: false,
total: 0,
list: [],
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 10,
inspectionPoint: '',
inspectorUser: this.$store?.state?.user?.nickName || '',
params: {}
}
}
},
created() {
this.getList()
},
onReachBottom() {
if (this.loadStatus !== 'more' || this.loading) return
this.loadStatus = 'loading'
this.queryParams.pageNum += 1
this.fetchList({ append: true })
},
onShow() {
this.getList()
},
methods: {
buildParams() {
if (this.dateRange && this.dateRange.length === 2) {
this.queryParams.params = {
beginTime: this.dateRange[0],
endTime: this.dateRange[1]
}
} else {
this.queryParams.params = {}
}
},
imageUrl(path) {
if (!path) return ''
if (/^https?:\/\//.test(path)) return path
return config.baseUrl + (path.startsWith('/') ? path : ('/' + path))
},
firstThreeImages(imgs) {
if (!imgs) return []
if (Array.isArray(imgs)) return imgs.slice(0,3)
if (typeof imgs === 'string') return imgs.split(',').filter(Boolean).slice(0,3)
return []
},
formatDate(val) {
if (!val) return ''
try {
const d = new Date(val)
const y = d.getFullYear()
const m = String(d.getMonth()+1).padStart(2,'0')
const dd = String(d.getDate()).padStart(2,'0')
const hh = String(d.getHours()).padStart(2,'0')
const mm = String(d.getMinutes()).padStart(2,'0')
const ss = String(d.getSeconds()).padStart(2,'0')
return `${y}-${m}-${dd} ${hh}:${mm}:${ss}`
} catch(e) { return val }
},
async fetchList({ append = false } = {}) {
try {
this.loading = true
this.buildParams()
const res = await listRecord(this.queryParams)
const rows = res?.rows || []
this.total = typeof res?.total === 'number' ? res.total : (append ? this.list.length + rows.length : rows.length)
if (append) {
this.list = this.list.concat(rows)
} else {
this.list = rows
}
if (typeof res?.total === 'number') {
this.loadStatus = this.list.length < this.total ? 'more' : 'noMore'
} else {
const hasMore = rows.length === this.queryParams.pageSize
this.loadStatus = hasMore ? 'more' : 'noMore'
}
} catch (e) {
this.loadStatus = 'noMore'
} finally {
this.loading = false
}
},
async getList() {
this.queryParams.pageNum = 1
this.loadStatus = 'loading'
await this.fetchList({ append: false })
},
onCardClick(type, item) {
if (!item) return
this.goDetail(item)
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
inspectionPoint: '',
inspectorUser: this.$store?.state?.user?.nickName || '',
params: {}
}
this.dateRange = []
this.getList()
},
goDetail(item) {
const id = item?.id || item?.recordId
if (!id) return
this.$tab.navigateTo(`/pages/work/inspection/checkInDetail/index?id=${id}`)
}
}
}
</script>
<style lang="scss">
.container { padding: 10px; }
.filters { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
.thumbs { display: flex; gap: 6px; justify-content: flex-end; }
.thumb { width: 60px; height: 60px; border-radius: 6px; background: #f5f5f5; }
.mb-10 { margin-bottom: 10px; }
.ml-6 { margin-left: 6px; }
.row { }
</style>

View File

@@ -34,8 +34,8 @@
listData
} from '@/api/system/dict/data.js'
import {
addRecord
} from "@/api/inspection/record.js"
addAbnormal
} from "@/api/inspection/abnormal.js"
import { addWatermarkToImage } from "@/utils/watermark.js"
export default {
// vue
@@ -109,8 +109,9 @@
submit() {
this.$refs.dynamicForm.validate().then(res => {
this.form.inspectionImg = this.joinList()
this.form.inspectionTime = new Date() // 添加当前时间
console.log("this.form",this.form);
addRecord(this.form).then(res => {
addAbnormal(this.form).then(res => {
console.log(res);
if (res.code==200) {
uni.showToast({

View File

@@ -0,0 +1,295 @@
<template>
<view class="container">
<uni-section title="巡检记录" type="line" class="mb-10">
<template v-slot:right>
<button class="ml-6" size="mini" type="primary" @click="handleQuery">搜索</button>
<button class="ml-6" size="mini" @click="resetQuery">重置</button>
</template>
</uni-section>
<view class="filters">
<uni-datetime-picker type="daterange" v-model="dateRange" :clear-icon="true" start="1990-01-01" end="2099-12-31" />
<uni-data-select class="ml-6" v-model="pointValue" :localdata="pointOptions" placeholder="巡检点" @change="onPointChange" />
</view>
<!-- 数据卡片概览 -->
<uni-row :width="730" class="stats">
<uni-col :span="6"><view class="stat"><text class="stat-label">总打卡</text><text class="stat-value">{{ stats.total }}</text></view></uni-col>
<uni-col :span="6"><view class="stat"><text class="stat-label">活跃天</text><text class="stat-value">{{ stats.activeDays }}</text></view></uni-col>
<uni-col :span="6"><view class="stat"><text class="stat-label">平均/</text><text class="stat-value">{{ stats.avgPerDay }}</text></view></uni-col>
<uni-col :span="6"><view class="stat"><text class="stat-label">最长连打</text><text class="stat-value">{{ stats.maxStreak }}</text></view></uni-col>
</uni-row>
<!-- 统计概览 -->
<uni-section title="统计概览" type="line" class="mb-10" />
<view class="charts">
<view class="chart-box">
<qiun-data-charts type="line" :opts="optsLine" :chartData="chartDaily" :errorShow="false" />
</view>
<view class="chart-box">
<qiun-data-charts type="ring" :opts="optsRing" :chartData="chartPointTop" :errorShow="false" />
</view>
<view class="chart-box">
<qiun-data-charts type="column" :opts="optsBar" :chartData="chartWeek" :errorShow="false" />
</view>
<view class="chart-box">
<qiun-data-charts type="column" :opts="optsBar" :chartData="chartHour" :errorShow="false" />
</view>
</view>
<uni-card v-for="item in displayList" :key="item.id || item.recordId" :title="item.inspectionPoint" @click="onCardClick($event, item)">
<uni-row class="row" :width="730">
<uni-col :span="16">
<view>
<text class="uni-body">巡检人{{ item.inspectorUser || item.inspectorId || '-' }}</text>
</view>
<view>
<text class="uni-body">巡检时间{{ formatDate(item.inspectionTime) }}</text>
</view>
<view v-if="item.remark">
<text class="uni-body">备注{{ item.remark }}</text>
</view>
</uni-col>
<uni-col :span="8">
<view class="thumbs">
<image v-for="(img,idx) in firstThreeImages(item.inspectionImg || item.clockInImg || item.signImg)" :key="idx" :src="imageUrl(img)" mode="aspectFill" class="thumb" @click.stop="previewImages(item.inspectionImg || item.clockInImg || item.signImg, idx)" />
</view>
</uni-col>
</uni-row>
</uni-card>
<uni-load-more :status="loadStatus" />
</view>
</template>
<script>
import { listRecord } from '@/api/inspection/record.js'
import config from '@/config'
export default {
data() {
return {
list: [],
displayList: [],
pageNum: 1,
pageSize: 10,
total: 0,
loadStatus: 'more',
dateRange: [],
pointOptions: [{ text: '全部', value: '' }],
pointValue: '',
stats: { total: 0, activeDays: 0, avgPerDay: 0, maxStreak: 0 },
// 图表配置与数据
optsLine: { xAxis: { disableGrid: true }, yAxis: { disableGrid: false }, legend: { show: false }, extra: { line: { width: 2 } } },
chartDaily: { categories: [], series: [{ name: '打卡次数', data: [] }] },
optsRing: { legend: { position: 'bottom' } },
chartPointTop: { series: [{ name: '巡检点占比', data: [] }] },
optsBar: { legend: { show: false }, xAxis: { disableGrid: true }, yAxis: { disableGrid: false } },
chartWeek: { categories: [], series: [{ name: '按周几', data: [] }] },
chartHour: { categories: [], series: [{ name: '按时段', data: [] }] },
}
},
onShow() {
this.resetQuery()
},
methods: {
async fetchList(reset = false) {
if (reset) { this.pageNum = 1; this.list = []; this.loadStatus = 'loading' }
const params = this.buildParams()
try {
const res = await listRecord(params)
const rows = res?.rows || res?.data?.rows || []
this.total = res?.total || res?.data?.total || rows.length
this.list = reset ? rows : this.list.concat(rows)
this.loadStatus = this.list.length >= this.total ? 'noMore' : 'more'
this.refreshPointOptions()
this.applyDisplayList()
// 生成统计数据(基于过滤后列表)
const used = this.displayList
this.buildDailyChart(used)
this.buildPointTopChart(used)
this.buildWeekChart(used)
this.buildHourChart(used)
this.buildStats(used)
} catch (err) {
console.error('fetchList error', err)
this.loadStatus = 'noMore'
}
},
onPointChange() { this.applyDisplayList(); const used = this.displayList; this.buildDailyChart(used); this.buildPointTopChart(used); this.buildWeekChart(used); this.buildHourChart(used); this.buildStats(used) },
refreshPointOptions() {
const set = new Set(this.list.map(i => i.inspectionPoint).filter(Boolean))
const arr = Array.from(set).map(n => ({ text: n, value: n }))
this.pointOptions = [{ text: '全部', value: '' }, ...arr]
},
applyDisplayList() {
this.displayList = this.pointValue ? this.list.filter(i => i.inspectionPoint === this.pointValue) : this.list.slice()
},
// ===== 图表数据构建 =====
buildDailyChart(rows) {
if (!rows || !rows.length) { this.chartDaily = { categories: [], series: [{ name: '打卡次数', data: [] }] }; return }
const map = new Map()
rows.forEach(r => { const d = this.formatDate(r.inspectionTime).slice(0, 10); map.set(d, (map.get(d) || 0) + 1) })
const categories = Array.from(map.keys()).sort()
const data = categories.map(d => map.get(d))
this.chartDaily = { categories, series: [{ name: '打卡次数', data }] }
},
buildPointTopChart(rows) {
if (!rows || !rows.length) { this.chartPointTop = { series: [{ name: '巡检点占比', data: [] }] }; return }
const map = new Map()
rows.forEach(r => { const p = r.inspectionPoint || '未知点位'; map.set(p, (map.get(p) || 0) + 1) })
const sorted = Array.from(map.entries()).sort((a,b)=>b[1]-a[1]).slice(0,8)
const data = sorted.map(([name, value]) => ({ name, value }))
this.chartPointTop = { series: [{ name: '巡检点占比', data }] }
},
buildWeekChart(rows) {
const names = ['周一','周二','周三','周四','周五','周六','周日']
const counts = Array(7).fill(0)
rows.forEach(r => { const d = new Date(r.inspectionTime); const idx = (d.getDay()+6)%7; counts[idx]++ })
this.chartWeek = { categories: names, series: [{ name: '按周几', data: counts }] }
},
buildHourChart(rows) {
const buckets = Array(24).fill(0)
rows.forEach(r => { const d = new Date(r.inspectionTime); buckets[d.getHours()]++ })
const labels = buckets.map((_,h)=> (h<10?('0'+h):h)+':00')
this.chartHour = { categories: labels, series: [{ name: '按时段', data: buckets }] }
},
buildStats(rows) {
const total = rows.length
const daySet = new Set(rows.map(r => this.formatDate(r.inspectionTime).slice(0,10)))
const activeDays = daySet.size
const avgPerDay = activeDays ? (total/activeDays).toFixed(2) : 0
// 计算最长连续天数
const days = Array.from(daySet).sort()
let maxStreak = 0, cur = 0, prev = null
const toDate = s => new Date(s+'T00:00:00')
days.forEach(s => { if (!prev) { cur=1; } else { const diff=(toDate(s)-toDate(prev))/(24*3600*1000); cur = diff===1 ? cur+1 : 1 } maxStreak = Math.max(maxStreak, cur); prev = s })
this.stats = { total, activeDays, avgPerDay, maxStreak }
},
buildParams() {
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
}
if (this.dateRange && this.dateRange.length === 2) {
params.params = {
beginTime: this.dateRange[0],
endTime: this.dateRange[1]
}
}
return params
},
handleQuery() {
this.fetchList(true)
},
resetQuery() {
this.dateRange = []
this.fetchList(true)
},
onReachBottom() {
if (this.loadStatus !== 'more') return
this.pageNum += 1
this.fetchList(false)
},
onCardClick(e, item) {
const id = item.id || item.recordId
if (!id) return
this.$tab.navigateTo(`/pages/work/inspection/checkInDetail/index?id=${id}`)
},
// ===== 工具方法 =====
buildDailyChart(rows) {
if (!rows || !rows.length) {
this.chartDaily = { categories: [], series: [{ name: '打卡次数', data: [] }] }
return
}
const map = new Map()
rows.forEach(r => {
const d = this.formatDate(r.inspectionTime).slice(0, 10)
map.set(d, (map.get(d) || 0) + 1)
})
const categories = Array.from(map.keys()).sort()
const data = categories.map(d => map.get(d))
this.chartDaily = { categories, series: [{ name: '打卡次数', data }] }
},
buildPointTopChart(rows) {
if (!rows || !rows.length) {
this.chartPointTop = { series: [{ name: '巡检点占比', data: [] }] }
return
}
const map = new Map()
rows.forEach(r => {
const p = r.inspectionPoint || '未知点位'
map.set(p, (map.get(p) || 0) + 1)
})
const sorted = Array.from(map.entries()).sort((a,b)=>b[1]-a[1]).slice(0,8)
const data = sorted.map(([name, value]) => ({ name, value }))
this.chartPointTop = { series: [{ name: '巡检点占比', data }] }
},
// ===== 工具方法 =====
firstThreeImages(imgs) {
if (!imgs) return []
const arr = Array.isArray(imgs) ? imgs : String(imgs).split(',').filter(Boolean)
return arr.slice(0, 3)
},
imageUrl(name) {
if (!name) return ''
if (/^https?:\/\//.test(name)) return name
return `${config.baseUrl}/profile/upload/${name}`
},
formatDate(val) {
if (!val) return '-'
const d = new Date(val)
const pad = (n) => (n < 10 ? '0' + n : n)
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
},
previewImages(imgs, idx) {
const arr = this.firstThreeImages(imgs)
const urls = arr.map(this.imageUrl)
if (!urls.length) return
uni.previewImage({
current: urls[idx] || urls[0],
urls
})
}
}
}
</script>
<style scoped>
.container {
padding: 10px 12px;
}
.filters {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.charts {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 380px) {
.charts { grid-template-columns: 1fr 1fr; }
}
.chart-box { background: #fff; border-radius: 8px; padding: 6px; }
.row {
display: flex;
}
.thumbs {
display: flex;
gap: 6px;
justify-content: flex-end;
}
.thumb {
width: 64px;
height: 64px;
border-radius: 6px;
}
.stats { margin-bottom: 10px; }
.stat { background:#fff; border-radius:8px; padding:10px; display:flex; flex-direction:column; align-items:center }
.stat-label { color:#666; font-size:12px }
.stat-value { color:#1677ff; font-weight:bold; font-size:18px }
.mb-10 { margin-bottom: 10px; }
.ml-6 { margin-left: 6px; }
</style>

View File

@@ -41,6 +41,7 @@
import { addRecord } from "@/api/inspection/record.js"
import { addWatermarkToImage } from "@/utils/watermark.js"
import appConfig from "@/config"
import { getToken } from "@/utils/auth"
// ------- URL 安全拼接,避免出现 /undefined/common/upload -------
function joinURL(base, path) {
@@ -110,25 +111,26 @@
},
// 选择文件(支持多张)
async upload(e) {
if (!e?.tempFiles?.length) return
if (this.isUploading) {
this.showMessage("warning", "图片正在上传中,请稍等")
return
}
this.isUploading = true
try {
for (const tf of e.tempFiles) {
await this.handleImageUpload(tf)
}
} catch (err) {
console.error(err)
this.showMessage("error", `图片上传失败:${err.message || err}`)
} finally {
this.isUploading = false
}
},
async upload(e) {
if (!e?.tempFiles?.length) return
if (this.isUploading) {
this.showMessage("warning", "图片正在上传中,请稍等")
return
}
this.isUploading = true
try {
for (const tf of e.tempFiles) {
await this.handleImageUpload(tf)
}
} catch (err) {
console.error(err)
const msg = err?.errMsg || err?.message || (typeof err === "string" ? err : JSON.stringify(err))
this.showMessage("error", `图片上传失败:${msg}`)
} finally {
this.isUploading = false
}
},
// 单张:加水印 -> 压缩(H5) -> 预览 -> 上传
async handleImageUpload(tempFile) {
@@ -260,19 +262,45 @@
url: UPLOAD_URL,
name: "file",
file, // H5 传 File不要传 filePath
header: { Authorization: "Bearer " + getToken() },
success: (res) => {
try {
if (res.statusCode && res.statusCode !== 200) {
throw new Error(`HTTP ${res.statusCode}`)
}
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data
if (data.code === 200) {
if (data && data.code === 200) {
resolve(data.fileName || data.url)
} else {
reject(new Error(data.msg || "上传失败"))
reject(new Error((data && data.msg) || "上传失败"))
}
} catch (e) {
reject(new Error("上传响应解析失败"))
}
},
fail: (err) => reject(err)
fail: async (err) => {
const msg = err?.errMsg || "uploadFile fail"
if (msg.includes("file error") || msg.includes("fail")) {
try {
const form = new FormData()
form.append("file", file)
const resp = await fetch(UPLOAD_URL, {
method: "POST",
headers: { Authorization: "Bearer " + getToken() },
body: form
})
if (!resp.ok) return reject(new Error(`HTTP ${resp.status}`))
const data = await resp.json()
if (data && data.code === 200) {
return resolve(data.fileName || data.url)
}
return reject(new Error((data && data.msg) || "上传失败"))
} catch (e) {
return reject(new Error(e?.message || msg))
}
}
reject(new Error(msg))
}
})
})
},
@@ -298,7 +326,7 @@
dialogConfirm() {
if (this.msgType === "success") {
uni.reLaunch({ url: "/pages/work/index" })
uni.reLaunch({ url: "/pages/work/inspection/recordList/index" })
}
},

View File

@@ -0,0 +1,319 @@
<template>
<view class="container">
<uni-section title="隐患申报详情" type="line" class="mb-10" />
<view v-if="detail.id" class="detail-content">
<!-- 基本信息卡片 -->
<uni-card title="基本信息" :is-shadow="true" class="info-card">
<view class="info-grid">
<view class="info-row">
<text class="info-label">申报类型</text>
<text class="info-value">{{ detail.declarationLabel || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">申报人</text>
<text class="info-value">{{ detail.applyUser || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">申报时间</text>
<text class="info-value">{{ formatDate(detail.occurTime) }}</text>
</view>
</view>
</uni-card>
<!-- 申报内容卡片 -->
<uni-card title="申报内容" :is-shadow="true" class="content-card">
<view class="content-text">
{{ detail.requirementDescription || '暂无内容' }}
</view>
<view v-if="detail.remark" class="remark-section">
<text class="remark-label">备注</text>
<text class="remark-text">{{ detail.remark }}</text>
</view>
</uni-card>
<!-- 图片展示卡片 -->
<uni-card title="相关图片" :is-shadow="true" class="image-card" v-if="imageArray(detail.declarationImg).length > 0">
<view class="image-grid">
<view v-for="(img, idx) in imageArray(detail.declarationImg)" :key="idx" class="image-item">
<image :src="imageUrl(img)" mode="aspectFill" class="detail-image" @click="previewImage(img, detail.declarationImg)" />
</view>
</view>
</uni-card>
<!-- 无图片提示 -->
<uni-card title="相关图片" :is-shadow="true" class="image-card" v-else>
<view class="no-image-tip">
<uni-icons type="image" size="40" color="#ccc"></uni-icons>
<text class="no-image-text">暂无相关图片</text>
</view>
</uni-card>
<!-- 操作按钮 -->
<view class="action-buttons" v-if="detail.status === '0' || detail.status === 0">
<button class="edit-btn" size="default" type="primary" @click="editDeclaration">编辑申报</button>
</view>
</view>
<!-- 加载状态 -->
<view v-else-if="loading" class="loading-state">
<uni-load-more status="loading" />
</view>
<!-- 错误状态 -->
<view v-else class="error-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="error-text">数据加载失败或记录不存在</text>
<button size="mini" type="default" @click="goBack">返回列表</button>
</view>
</view>
</template>
<script>
import { getSafetyDeclaration } from '@/api/sidebar/sidebar.js'
import { listData } from '@/api/system/dict/data.js'
import config from '@/config'
export default {
name: "FilingDetail",
data() {
return {
id: null,
detail: {},
loading: false,
typeDict: []
}
},
async onLoad(options) {
this.id = options?.id || null
if (this.id) {
await this.initDict()
await this.fetchDetail()
}
},
methods: {
async initDict() {
try {
const typeData = await listData({ dictType: 'hs_declaration_type' })
this.typeDict = typeData.rows || []
} catch (e) {
console.error('获取字典数据失败:', e)
this.typeDict = []
}
},
async fetchDetail() {
if (!this.id) return
try {
this.loading = true
const res = await getSafetyDeclaration(this.id)
this.detail = res?.data || {}
// 设置申报类型标签
if (this.detail.declarationType) {
const typeItem = this.typeDict.find(i => String(i.dictValue) === String(this.detail.declarationType))
this.detail.declarationLabel = typeItem ? typeItem.dictLabel : this.detail.declarationType
}
} catch (e) {
console.error('获取详情失败:', e)
uni.showToast({
title: '获取详情失败',
icon: 'error'
})
} finally {
this.loading = false
}
},
imageArray(declarationImg) {
if (!declarationImg) return []
if (typeof declarationImg === 'string') {
return declarationImg.split(',').filter(Boolean)
}
return declarationImg.filter(Boolean)
},
imageUrl(path) {
if (!path) return ''
return config.baseUrl + path
},
previewImage(current, all) {
const urls = this.imageArray(all).map(p => this.imageUrl(p))
uni.previewImage({
current: this.imageUrl(current),
urls
})
},
formatDate(val) {
if (!val) return '-'
try {
const d = new Date(val)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${dd} ${hh}:${mm}`
} catch (e) {
return val
}
},
getStatusText(status) {
const statusMap = {
'0': '待处理',
'1': '处理中',
'2': '已完成',
'3': '已关闭'
}
return statusMap[status] || '未知'
},
getStatusType(status) {
const typeMap = {
'0': 'error', // 待处理 - 红色
'1': 'warning', // 处理中 - 橙色
'2': 'success', // 已完成 - 绿色
'3': 'info' // 已关闭 - 蓝色
}
return typeMap[status] || 'default'
},
editDeclaration() {
if (!this.detail.id) return
this.$tab.navigateTo(`/pages/work/sidebar/safetyDeclaratio/index?id=${this.detail.id}`)
},
goBack() {
this.$tab.navigateBack()
}
}
}
</script>
<style lang="scss">
.container {
padding: 15px;
background-color: #f8f9fa;
min-height: 100vh;
}
.mb-10 {
margin-bottom: 15px;
}
.detail-content {
.info-card,
.content-card,
.image-card {
margin-bottom: 15px;
border-radius: 12px;
overflow: hidden;
}
.info-grid {
.info-row {
display: flex;
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 14px;
color: #666;
min-width: 80px;
flex-shrink: 0;
}
.info-value {
font-size: 14px;
color: #333;
flex: 1;
}
}
}
.content-text {
font-size: 15px;
line-height: 1.6;
color: #333;
margin-bottom: 15px;
}
.remark-section {
padding-top: 15px;
border-top: 1px solid #eee;
.remark-label {
font-size: 14px;
color: #666;
margin-right: 8px;
}
.remark-text {
font-size: 14px;
color: #333;
line-height: 1.5;
}
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
.image-item {
.detail-image {
width: 100px;
height: 100px;
border-radius: 8px;
background: #f5f5f5;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
}
}
}
.no-image-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
.no-image-text {
margin-top: 10px;
font-size: 14px;
color: #999;
}
}
.action-buttons {
margin-top: 20px;
padding: 20px 0;
.edit-btn {
width: 100%;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
}
}
}
.loading-state,
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
.error-text {
margin: 15px 0;
font-size: 14px;
color: #999;
text-align: center;
}
}
</style>

View File

@@ -1,150 +1,239 @@
<template>
<view class="work-container">
<uni-card v-for="(item,index) of dataList" class="parent">
<template v-slot:title>
<h3 class="title">{{item.declarationLabel}}</h3>
</template>
<h3>{{item.requirementDescription}}</h3>
<h3>{{item.occurTime}}</h3>
<view class="card-actions-item" @click="actionsClick(item.id)">
<button size="mini">详情</button>
</view>
</uni-card>
<uni-load-more status="已经没有更多数据了"></uni-load-more>
</uni-section>
</view>
<view class="container">
<uni-section title="异常隐患申报记录" type="line" class="mb-10">
<template v-slot:right>
<button size="mini" type="primary" @click.stop="addNew">新增</button>
</template>
</uni-section>
<view class="filters">
<uni-data-select v-model="queryParams.declarationType" :localdata="selectList" placeholder="申报类型" />
<uni-datetime-picker type="daterange" v-model="dateRange" :clear-icon="true" start="1990-01-01" end="2099-12-31" />
<button class="ml-6" size="mini" type="primary" @click="handleQuery">搜索</button>
<button class="ml-6" size="mini" @click="resetQuery">重置</button>
</view>
<uni-card v-for="item in dataList" :key="item.id" :title="item.declarationLabel || '异常隐患申报'" @click="onCardClick($event, item)">
<uni-row class="row" :width="730">
<uni-col :span="16">
<view>
<text class="uni-body">申报人{{ item.applyUser || '-' }}</text>
</view>
<view>
<text class="uni-body">申报时间{{ formatDate(item.occurTime) }}</text>
</view>
<view>
<text class="uni-body">备注{{ item.remark || '-' }}</text>
</view>
</uni-col>
<uni-col :span="8">
<view class="thumbs">
<image v-for="(img,idx) in firstThreeImages(item.declarationImg)" :key="idx" :src="imageUrl(img)" mode="aspectFill" class="thumb" />
</view>
</uni-col>
</uni-row>
</uni-card>
<uni-load-more :status="loadStatus" />
</view>
</template>
<script>
import {
listData
} from '@/api/system/dict/data.js'
import {
listSafetyDeclaration
} from "@/api/sidebar/sidebar.js"
import formatTime from '@/utils/formatTime.js'
import { listSafetyDeclaration } from '@/api/sidebar/sidebar.js';
import { listData } from '@/api/system/dict/data.js';
import config from '@/config';
export default {
name: "FilingLog",
data() {
return {
dataList: [],
queryParams: {
pageNum: 1, // 初始页码设为1
pageSize: 10
},
isLoading: false, // 添加一个标志位,用于防止重复请求
typeDataList: [],
total: 0
}
},
methods: {
actionsClick(e) {
uni.navigateTo({
url: `/pages/work/sidebar/safetyDeclaratio/index?id=` + e
})
},
async getList() {
if (this.isLoading) return; // 如果已经在加载中,则不执行
this.isLoading = true;
const res = await listSafetyDeclaration(this.queryParams);
if (this.typeDataList.length == 0) {
this.typeDataList = await listData({
dictType: "hs_declaration_type"
});
}
this.total = res.total
let processedDataList = res.rows.map(item => {
const matchedType = this.typeDataList.rows.find(type => type.dictValue == item.declarationType);
const time = formatTime(item.occurTime);
return {
...item,
occurTime: time,
declarationLabel: matchedType ? matchedType.dictLabel : "未知类型"
};
});
// 合并新旧数据
this.dataList = [...this.dataList, ...processedDataList];
},
onLoad(options) {
this.getList();
},
},
onReachBottom() {
this.isLoading = false;
//上拉到底时触发onReachBottom函数跟生命周期同级
let allTotal = this.queryParams.pageNum * this.queryParams.pageSize
console.log("allTotal",allTotal);
console.log("this.total",this.total);
if (allTotal < this.total) {
this.queryParams.pageNum++;
// this.queryParams.page++//当前条数小于总条数 则增加请求页数
this.getList() //调用加载数据方法
setTimeout(() => {
//结束下拉刷新
uni.stopPullDownRefresh();
}, 1000);
} else {
console.log('已加载全部数据')
}
},
onPullDownRefresh() {
this.queryParams.pageNum=1;
this.isLoading = false;
//下拉刷新触发onPullDownRefresh函数跟生命周期同级
this.dataList = []
this.getList().then(res=>{
setTimeout(() => {
//结束下拉刷新
uni.stopPullDownRefresh();
}, 1000);
})
}
}
export default {
name: "FilingLog",
data() {
return {
loadStatus: 'more',
loading: false,
total: 0,
dataList: [],
selectList: [],
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 10,
declarationType: null,
params: {}
}
}
},
created() {
this.initDict()
this.getList()
},
// 触底加载更多(页面滚动到底部)
onReachBottom() {
if (this.loadStatus !== 'more' || this.loading) return
this.loadStatus = 'loading'
this.queryParams.pageNum += 1
this.fetchList({ append: true })
},
// 页面重新显示时自动刷新列表(新增返回后生效)
onShow() {
this.getList()
},
methods: {
async initDict() {
try {
const typeData = await listData({ dictType: 'hs_declaration_type' })
this.selectList = typeData.rows.map(i => ({ value: i.dictValue, text: i.dictLabel }))
} catch (e) {
console.error('获取字典数据失败:', e)
this.selectList = []
}
},
imageUrl(path) {
if (!path) return ''
return config.baseUrl + path
},
firstThreeImages(declarationImg) {
if (!declarationImg) return []
if (typeof declarationImg === 'string') {
return declarationImg.split(',').slice(0, 3)
}
return declarationImg.slice(0, 3)
},
formatDate(val) {
if (!val) return ''
try {
const d = new Date(val)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
const ss = String(d.getSeconds()).padStart(2, '0')
return `${y}-${m}-${dd} ${hh}:${mm}:${ss}`
} catch (e) {
return val
}
},
buildParams() {
if (this.dateRange && this.dateRange.length === 2) {
this.queryParams.params = {
beginTime: this.dateRange[0],
endTime: this.dateRange[1]
}
} else {
this.queryParams.params = {}
}
},
async fetchList({ append = false } = {}) {
try {
this.loading = true
this.buildParams()
const res = await listSafetyDeclaration(this.queryParams)
const rows = res?.rows || []
// 若接口提供 total 字段则使用,否则根据 pageSize 判断是否还有更多
this.total = typeof res?.total === 'number' ? res.total : (append ? this.dataList.length + rows.length : rows.length)
if (append) {
this.dataList = this.dataList.concat(rows)
} else {
this.dataList = rows
}
// 根据是否还有更多数据设置加载状态
if (typeof res?.total === 'number') {
this.loadStatus = this.dataList.length < this.total ? 'more' : 'noMore'
} else {
// 当本次返回数量等于 pageSize默认还有下一页
const hasMore = rows.length === this.queryParams.pageSize
this.loadStatus = hasMore ? 'more' : 'noMore'
}
} catch (e) {
console.error('获取列表失败:', e)
this.loadStatus = 'noMore'
} finally {
this.loading = false
}
},
async getList() {
// 初始化查询第一页
this.queryParams.pageNum = 1
this.loadStatus = 'loading'
await this.fetchList({ append: false })
},
onCardClick(type, item) {
// 仅在内容区域点击时跳转title/extra也可按需
if (!item) return
this.goDetail(item)
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
declarationType: null,
params: {}
}
this.dateRange = []
this.getList()
},
addNew() {
this.$tab.navigateTo('/pages/work/sidebar/safetyDeclaratio/index')
},
goDetail(item) {
const id = item?.id
if (!id) return
this.$tab.navigateTo(`/pages/work/sidebar/filingDetail/index?id=${id}`)
}
}
}
</script>
<style lang="scss">
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff;
min-height: 100%;
height: auto;
}
.container {
padding: 10px;
background-color: #f8f9fa;
min-height: 100vh;
}
.work-container {
margin-left: 4px
}
.filters {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
padding: 15px;
background: #fff;
border-radius: 8px;
}
.thumbs {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: flex-end;
.thumb {
width: 50px;
height: 50px;
border-radius: 8px;
background: #f5f5f5;
}
}
.parent {
position: relative;
.row {
display: flex;
align-items: flex-start;
}
h3 {
margin-bottom: 5px;
font-weight:600;
color: rgb(153, 153, 153);
}
.uni-body {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.title {
font-weight:bold;
padding: 3rpx;
color: black;
}
}
// 工具类
.mb-10 {
margin-bottom: 10px;
}
.card-actions-item {
display: inline-block;
flex-direction: row;
align-items: center;
position: absolute;
bottom: 5px;
right: 30px;
button {
border-radius: 10px;
}
}
.ml-6 {
margin-left: 6px;
}
</style>

View File

@@ -133,13 +133,15 @@
// console.log();
}
},
submit(ref) {
this.$refs[ref].validate().then(res => {
this.SafetyDeclarationTable.declarationImg = this.joinList()
console.log("提交成功");
console.log("this.SafetyDeclarationTable", this.SafetyDeclarationTable);
addSafetyDeclaration(this.SafetyDeclarationTable).then(res => {
uni.showToast({
submit(ref) {
this.$refs[ref].validate().then(res => {
// 绑定当前用户为申报人
this.SafetyDeclarationTable.applyUser = this.$store.getters.name
this.SafetyDeclarationTable.declarationImg = this.joinList()
console.log("提交成功");
console.log("this.SafetyDeclarationTable", this.SafetyDeclarationTable);
addSafetyDeclaration(this.SafetyDeclarationTable).then(res => {
uni.showToast({
title: '提交成功',
icon: 'success', // 成功图标
duration: 1000 // 持续时间为2000ms

View File

@@ -5,7 +5,7 @@ const loginPage = "/pages/login"
// 页面白名单
const whiteList = [
'/pages/login', '/pages/register', '/pages/common/webview/index'
'/pages/login', '/pages/register', '/pages/common/webview/index', '/pages/cas/callback'
]
// 检查地址白名单

View File

@@ -1,7 +1,7 @@
import config from '@/config'
import storage from '@/utils/storage'
import constant from '@/utils/constant'
import { login, logout, getInfo } from '@/api/login'
import { login, logout, getInfo, casLogin, casAppLogin, casUnifiedLogin } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const baseUrl = config.baseUrl
@@ -12,7 +12,7 @@ const user = {
name: storage.get(constant.name),
avatar: storage.get(constant.avatar),
roles: storage.get(constant.roles),
nickName:storage.get(constant.nickName),
nickName: storage.get(constant.nickName),
permissions: storage.get(constant.permissions)
},
@@ -36,10 +36,10 @@ const user = {
state.permissions = permissions
storage.set(constant.permissions, permissions)
},
SET_NICKNAME:(state,nickName)=>{
state.nickName = nickName
storage.set(constant.nickName, nickName)
}
SET_NICKNAME: (state, nickName) => {
state.nickName = nickName
storage.set(constant.nickName, nickName)
}
},
actions: {
@@ -59,6 +59,43 @@ const user = {
})
})
},
// CAS 票据登录
CasLogin({ commit }, payload) {
const { ticket, service } = payload || {}
return new Promise((resolve, reject) => {
casLogin(ticket, service).then(res => {
const token = res.token || res.data?.token
if (token) {
setToken(token)
commit('SET_TOKEN', token)
resolve(res)
} else {
reject('CAS 登录未返回令牌')
}
}).catch(error => {
reject(error)
})
})
},
// 移动端CAS票据登录
CasAppLogin({ commit }, ticket) {
return new Promise((resolve, reject) => {
// 使用统一CAS登录接口
casUnifiedLogin(ticket).then(res => {
const token = res.token || res.data?.token
if (token) {
setToken(token)
commit('SET_TOKEN', token)
resolve(res)
} else {
reject('移动端CAS登录未返回令牌')
}
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
@@ -67,7 +104,7 @@ const user = {
const user = res.user
const avatar = (user == null || user.avatar == "" || user.avatar == null) ? require("@/static/images/profile.jpg") : baseUrl + user.avatar
const username = (user == null || user.userName == "" || user.userName == null) ? "" : user.userName
const nickName = (user == null || user.nickName == "" || user.nickName == null) ? "" : user.nickName
const nickName = (user == null || user.nickName == "" || user.nickName == null) ? "" : user.nickName
if (res.roles && res.roles.length > 0) {
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
@@ -76,7 +113,7 @@ const user = {
}
commit('SET_NAME', username)
commit('SET_AVATAR', avatar)
commit('SET_NICKNAME',nickName)
commit('SET_NICKNAME', nickName)
resolve(res)
}).catch(error => {
reject(error)

View File

@@ -10,8 +10,8 @@ import {
tansParams
} from '@/utils/common'
let timeout = 10000
const baseUrl = 'http://172.20.10.2:8081'
let timeout = 30000
const baseUrl = appConfig.baseUrl
const request = config => {
// 是否需要设置 token
@@ -30,7 +30,7 @@ const request = config => {
uni.request({
method: config.method || 'get',
timeout: config.timeout || timeout,
url: appConfig.baseUrl + config.url || baseUrl + config.url,
url: baseUrl + config.url,
data: config.data,
header: config.header,
dataType: 'json'