This commit is contained in:
houjunxiang
2025-10-09 18:19:55 +08:00
parent f2ffc65094
commit 386f1e7466
1553 changed files with 284685 additions and 32820 deletions

View File

@@ -0,0 +1,257 @@
<template>
<view>
<navbar-back title="选择天平"></navbar-back>
<u-grid :col="3" @click="click">
<u-grid-item v-for="(auncel, index) in auncelList" :index="index" :key="index">
<view
style="
height: 180px;
width: 180px;
background-image: url(../../static/images/auncel.png);
background-repeat: no-repeat;
background-size: 100%;
"
>
<view style="height: 100px">
<view style="padding-top: 20px; text-align: center; font-size: 20rpx"
><text>{{ auncel.code }}</text></view
>
<view style="padding-top: 5px; text-align: center"
><text>{{ auncel.controlRealName }}</text></view
>
</view>
<view class="weight">
<view
:class="
auncel.weightStable == 0
? 'weight-data-yellow'
: auncel.weightStable == 1
? 'weight-data'
: 'weight-data-warning'
"
>
{{ auncel.weightData }}
</view>
<view class="weight-unit">{{ auncel.weightUnit }}</view>
</view>
</view>
</u-grid-item>
</u-grid>
</view>
</template>
<script>
export default {
data() {
return {
auncelList: [],
currentAuncel: '',
currentTaskNo: '',
currentTaskType: ''
}
},
onLoad(param) {
this.currentTaskNo = param.currentTaskNo
this.currentTaskType = param.currentTaskType
//获取数据
this.getPageData()
console.log('onLoad')
},
onShow() {
console.log('onShow')
// #ifdef H5
//注册websocket
let regData = {
msgId: this.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: this.userInfo.user_id,
tenantId: this.userInfo.tenant_id,
userRealName: this.userInfo.real_name
}
}
this.$measure.setRegData(JSON.stringify(regData))
this.$measure.reOpen()
// #endif
//监听websocket返回来的数据
//设备数据
uni.$on('deviceData', res => {
switch (res.deviceType) {
case 'auncel':
this.auncelList.forEach((item, index) => {
if (item.id === res.deviceId) {
item.weightData = res.weightData
item.weightUnit = res.weightUnit
item.weightStable = res.weightStable
item.isConnected = 1
item.controlRealName = res.controlRealName
}
})
break
default:
break
}
})
//设备状态
uni.$on('deviceStatus', res => {
this.auncelList.forEach((item, index) => {
if (item.id === res.deviceId) {
item.isConnected = res.connected
if (res.connected == 0) {
item.weightData = '天平断开'
item.weightUnit = ''
item.weightStable = 0
}
}
})
})
//连接断开
uni.$on('connClose', res => {
//重置
this.auncelList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.weightStable = 0
i.controlRealName = ''
i.isConnected = 0
})
})
},
onHide() {
console.log('onHide')
//移除监听websocket返回来的数据
uni.$off('deviceData')
uni.$off('deviceStatus')
uni.$off('connClose')
},
methods: {
getPageData() {
//显示loading
uni.showLoading({
title: '加载中...'
})
//获取计量点数量
this.$u.api
.getDeviceLaboratoryListBy({
deviceType: 'auncel',
pageNo: 0,
pageSize: -1,
status: '1',
isEnable: '1'
})
.then(res => {
let dataList = res.result.records
dataList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.isConnected = 0
i.weightStable = 0
i.temperature = 0
i.humidity = 0
i.controlRealName = ''
})
this.auncelList = dataList
uni.hideLoading()
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
},
click(index) {
// this.$refs.uToast.show({
// title: `点击了第${index + 1}宫格`,
// type: 'warning'
// });
console.log(this.auncelList[index])
this.currentAuncel = this.auncelList[index]
if (this.currentAuncel.isConnected != 1) {
this.$helper.showToast({
title: '天平设备尚未连接!'
})
return
}
if (this.currentAuncel.controlRealName && this.currentAuncel.controlRealName != this.userInfo.real_name) {
this.$helper.showToast({
title: '当前天平正被“' + this.currentAuncel.controlRealName + '”使用,请选择其他天平!'
})
return
}
//发送控制请求
let controlDevice = {
msgId: this.currentAuncel.id,
cmd: 'controlDevice',
clientType: 'caaClient',
data: {
deviceId: this.currentAuncel.id,
isControl: true,
controlRealName: this.userInfo.real_name
}
}
//发送控制数据
this.$measure.send(JSON.stringify(controlDevice))
uni.redirectTo({
url:
'/pages/auncel/auncel-weigh?currentTaskNo=' +
this.currentTaskNo +
'&currentTaskType=' +
this.currentTaskType +
'&currentAuncelId=' +
this.currentAuncel.id +
'&currentAuncelName=' +
this.currentAuncel.name +
'&currentAuncelCode=' +
this.currentAuncel.code
})
}
}
}
</script>
<style>
.weight {
width: 180px;
height: 40px;
padding: 0 10px;
display: flex;
flex-direction: row;
box-sizing: border-box;
}
.weight-data {
flex-grow: 1;
height: 100%;
color: #4cd964;
text-align: right;
line-height: 40px;
letter-spacing: 2px;
font-size: 32px;
font-family: zzjc-lcd;
}
.weight-data-yellow {
flex-grow: 1;
height: 100%;
color: #ffff00;
text-align: right;
line-height: 40px;
letter-spacing: 2px;
font-size: 32px;
font-family: zzjc-lcd;
}
.weight-data-warning {
flex-grow: 1;
height: 100%;
color: #ff3333;
text-align: right;
line-height: 40px;
font-size: 32px;
font-family: zzjc-lcd;
}
.weight-unit {
color: #ffffff;
font-size: 24px;
line-height: 40px;
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<view>
<navbar-back title="天平状态"></navbar-back>
<up-grid border :col="3">
<up-grid-item v-for="(auncel, index) in auncelList" :index="index" :key="index">
<view
class="auncel-item"
:style="{
backgroundImage: `url(${require('@/static/images/auncel.png')})`
}"
>
<view class="auncel-header">
<view class="auncel-code">{{ auncel.code }}</view>
<view class="auncel-name">{{ auncel.controlRealName }}</view>
</view>
<view class="weight">
<view
:class="{
'weight-data-yellow': auncel.weightStable === 0,
'weight-data': auncel.weightStable === 1,
'weight-data-warning': auncel.weightStable === 2
}"
>
{{ auncel.weightData || '' }}
</view>
<view class="weight-unit">{{ auncel.weightUnit }}</view>
</view>
</view>
</up-grid-item>
</up-grid>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onUnload, onHide } from '@dcloudio/uni-app'
import { getTenantId } from '@/defaultBaseUrl'
import nx from '@/nx'
// refs
const auncelList = ref([])
// computed
const userInfo = computed(() => nx.$store('user').userInfo)
// 页面加载
onMounted(() => {
// getPageData()
// #ifdef H5
const regData = {
msgId: nx.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: userInfo.value.id,
tenantId: getTenantId(),
userRealName: userInfo.value.realname
}
}
nx.$measure.setRegData(JSON.stringify(regData))
nx.$measure.reOpen()
// #endif
// 监听 WebSocket 数据
uni.$on('deviceData', handleDeviceData)
uni.$on('deviceStatus', handleDeviceStatus)
uni.$on('connClose', handleConnClose)
})
onHide(() => {
// #ifdef APP-PLUS
cleanup()
// #endif
})
onUnmounted(() => {
// #ifdef H5
cleanup()
// #endif
})
function cleanup() {
uni.$off('deviceData', handleDeviceData)
uni.$off('deviceStatus', handleDeviceStatus)
uni.$off('connClose', handleConnClose)
}
// 获取数据
function getPageData() {
nx.$api.laboratory
.getDeviceLaboratoryListBy({
deviceType: 'auncel',
status: '1',
isEnable: '1',
pageNo: 0,
pageSize: -1
})
.then(res => {
const dataList = res.records || []
dataList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.isConnected = 0
i.weightStable = 0
i.temperature = 0
i.humidity = 0
i.controlRealName = ''
})
auncelList.value = dataList
})
.catch(err => {
console.error('获取天平列表失败', err)
})
}
// WebSocket 事件处理器
function handleDeviceData(res) {
if (res.deviceType !== 'auncel') return
auncelList.value = auncelList.value.map(item => {
if (item.id === res.deviceId) {
return {
...item,
weightData: res.weightData,
weightUnit: res.weightUnit,
weightStable: res.weightStable,
isConnected: 1,
controlRealName: res.controlRealName
}
}
return item
})
}
function handleDeviceStatus(res) {
auncelList.value = auncelList.value.map(item => {
if (item.id === res.deviceId) {
const updated = { ...item, isConnected: res.connected }
if (res.connected === 0) {
updated.weightData = '天平断开'
updated.weightUnit = ''
updated.weightStable = 0
}
return updated
}
return item
})
}
function handleConnClose() {
auncelList.value = auncelList.value.map(i => ({
...i,
weightData: '',
weightUnit: '',
weightStable: 0,
controlRealName: '',
isConnected: 0
}))
}
</script>
<style scoped>
.auncel-item {
height: 180px;
width: 180px;
background-repeat: no-repeat;
background-size: 100% 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
box-sizing: border-box;
}
.auncel-header {
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.auncel-code {
font-size: 20px;
font-weight: bold;
}
.auncel-name {
margin-top: 5px;
font-size: 14px;
color: #666;
}
.weight {
width: 100%;
height: 40px;
display: flex;
flex-direction: row;
box-sizing: border-box;
}
.weight-data,
.weight-data-yellow,
.weight-data-warning {
flex-grow: 1;
height: 100%;
text-align: right;
line-height: 40px;
letter-spacing: 2px;
font-size: 32px;
font-family: zzjc-lcd;
}
.weight-data {
color: #4cd964;
}
.weight-data-yellow {
color: #ffff00;
}
.weight-data-warning {
color: #ff3333;
}
.weight-unit {
color: #ffffff;
font-size: 24px;
line-height: 40px;
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,839 @@
<template>
<view>
<u-navbar
title-color="#fff"
title-width="360"
back-icon-color="#ffffff"
:back-text-style="{ color: '#fff' }"
back-text="返回"
title="样品称重"
:border-bottom="false"
:custom-back="customBack"
:background="{ 'background-color': '#0055A2' }"
></u-navbar>
<u-row class="content-title" gutter="16">
<u-col span="3">
<view class="content-title-name">
<text>待称重样品</text>
</view>
<u-gap height="10" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="6">
<view class="content-title-name">
<text>{{ currentAuncel.code === '' ? '电子天平' : currentAuncel.code }}</text>
</view>
<u-gap height="10" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="3">
<view class="content-title-name">
<text>已称重样品</text>
</view>
<u-gap height="10" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row gutter="16" class="content-main-height" align="top">
<u-col span="3">
<scroll-view scroll-y scroll-with-animation class="content-main-left" :scroll-top="scrollTop">
<view
v-for="(sample, index) in notWeighSampleList"
:key="index"
class="u-tab-item"
:class="[current == index ? 'u-tab-item-active' : '']"
:data-current="index"
@tap.stop="swichSample(index)"
>
<u-row gutter="20" style="width: 100%">
<u-col span="3" style="padding: 0rpx">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="9">
<view>
<text>{{ sample.sampleCode }}</text>
</view>
<view style="padding-top: 20rpx">
<text>{{ sample.dataSourceType | getDataSourceTypeShow }}{{ sample.goodsName }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
<view class="content-main-left-operation">
<u-row>
<u-col span="2"></u-col>
<u-col span="8">
<view>
<view style="padding: 20rpx; text-align: right">
<text style="font-size: 30rpx; height: 50rpx">顺序称重</text>
<u-switch
v-model="orderWeighChecked"
active-color="#19be6b"
inactive-color="#eee"
@change="orderWeighChange"
></u-switch>
</view>
</view>
</u-col>
<u-col span="2"></u-col>
</u-row>
</view>
</u-col>
<u-col span="6">
<view class="content-main-height">
<view class="auncel-container">
<view class="auncel">
<view class="auncel-title">
<view style="padding-top: 40rpx; height: 100rpx; width: 100%; text-align: center">
<text style="font-size: 44rpx; font-weight: 300"
>{{ currentSample.sort }} {{ currentSample.dataSourceType | getDataSourceTypeShow }}</text
>
</view>
<view style="padding-top: 30rpx; height: 100rpx; width: 100%; text-align: center">
<text style="font-size: 44rpx; font-weight: 300">{{ currentSample.sampleCode }}</text>
</view>
<view
style="
padding-top: 20rpx;
padding-left: 140rpx;
padding-right: 140rpx;
display: flex;
justify-content: center;
align-content: center;
"
>
<block v-if="currentSample.dataSourceType">
<u-row v-if="currentSample.dataSourceType == 2 || currentSample.dataSourceType == 3">
<u-col span="1"></u-col>
<u-col span="3">
<u-input
style="color: #3b4144; background-color: #ffffff"
v-model="currentCupNum"
type="number"
border="border"
:disabled="checkCupNumReadonly"
placeholder="杯号"
/>
</u-col>
<u-col span="2">
<text style="width: 100%; text-align: left">号杯</text>
</u-col>
<u-col span="4">
<u-input
style="color: #3b4144; background-color: #ffffff"
v-model="currentSample.remarks"
type="number"
border="border"
placeholder="样重比例"
/>
</u-col>
<u-col span="2">
<text style="width: 100%; text-align: left">%</text>
</u-col>
</u-row>
<u-row v-else>
<u-col span="2"></u-col>
<u-col span="7">
<u-input
style="color: #3b4144; background-color: #ffffff"
v-model="currentCupNum"
type="number"
border="border"
:disabled="checkCupNumReadonly"
placeholder="杯号"
/>
</u-col>
<u-col span="3">
<text style="width: 100%; text-align: left">号杯</text>
</u-col>
</u-row>
</block>
</view>
</view>
<view class="auncel-weight">
<view class="weight">
<view
:class="
currentAuncel.weightStable == 0
? 'weight-data-yellow'
: currentAuncel.weightStable == 1
? 'weight-data'
: 'weight-data-warning'
"
@click="selectAuncel"
>
{{ currentAuncel.weightData }}
</view>
<view class="weight-unit">{{ currentAuncel.weightUnit }}</view>
</view>
</view>
</view>
</view>
<view>
<view style="padding: 0rpx 60rpx 10rpx 60rpx">
<block v-if="currentAuncel.code !== ''">
<u-button
style="height: 140rpx; font-size: 80rpx"
type="success"
:disabled="confirmWeightDisabled"
shape="circle"
@click="confirm"
>确认采集</u-button
>
</block>
</view>
</view>
</view>
</u-col>
<u-col span="3">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right" :scroll-top="scrollTop">
<view
v-for="(sample, index) in finishWeighSampleList"
:key="index"
class="u-tab-item"
style="height: 240rpx"
>
<u-row gutter="20" style="width: 100%">
<u-col span="3" style="padding: 0rpx">
<view>
<text>{{ sample.sort }}</text>
</view>
<view style="padding-top: 20rpx">
<text>{{ sample.cupNum }}号杯</text>
</view>
<view v-if="sample.remarks" style="padding-top: 20rpx">
<text>{{ sample.remarks }}</text>
</view>
</u-col>
<u-col span="9">
<view>
<text>{{ sample.sampleCode }}</text>
</view>
<view style="padding-top: 20rpx">
<text>{{ sample.dataSourceType | getDataSourceTypeShow }}{{ sample.goodsName }}</text>
</view>
<view style="padding-top: 20rpx">
<text style="padding-left: 20rpx">{{ sample.sampleWeight }} g</text>
<u-button style="margin-left: 20rpx" size="mini" type="error" @click="weighRevoke(sample.id)"
>撤销</u-button
>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="2"></u-col>
<u-col span="8">
<u-button
:disabled="finishWeighSampleList.length == 0"
class="btn-operation"
type="success"
@click="submit"
>立即提交</u-button
>
</u-col>
<u-col span="2"></u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
</view>
</template>
<script>
export default {
data() {
return {
scrollTop: 0, //tab标题的滚动条位置
current: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
orderWeighChecked: true, //顺序称重
weightDataIsToZero: false, //重量数据是否归零
currentCupNum: '',
currentSample: {}, //当前样品
currentTaskNo: '', //当前任务样品
currentTaskType: '', //当前任务类型
currentAuncel: {
id: '',
name: '',
code: '',
weightStable: 0,
weightData: '请选天平',
weightUnit: ''
},
allWeighSampleCount: 0,
notWeighSampleList: [],
finishWeighSampleList: []
}
},
computed: {
confirmWeightDisabled() {
if (
this.currentSample.sampleCode &&
this.currentAuncel.weightStable == 1 &&
Number(this.currentAuncel.weightData) > 0
) {
return false
}
return true
},
checkCupNumReadonly() {
if (!this.currentAuncel || this.currentAuncel.id == '') return true
return false
}
},
onLoad(param) {
//console.log(param);
if (param.currentTaskNo) {
this.currentTaskNo = param.currentTaskNo
this.currentTaskType = param.currentTaskType
}
if (param.currentAuncelId) {
this.currentAuncel.id = param.currentAuncelId
this.currentAuncel.weightData = ''
}
if (param.currentAuncelName) {
this.currentAuncel.name = param.currentAuncelName
}
if (param.currentAuncelCode) {
this.currentAuncel.code = param.currentAuncelCode
}
this.getAssayTaskDetail(this.currentTaskNo)
},
onShow() {
console.log('onShow')
//注册websocket
let regData = {
msgId: this.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: this.userInfo.user_id,
tenantId: this.userInfo.tenant_id,
userRealName: this.userInfo.real_name
}
}
this.$measure.setRegData(JSON.stringify(regData))
this.$measure.reOpen()
//监听websocket返回来的数据
//设备数据
uni.$on('deviceData', res => {
switch (res.deviceType) {
case 'auncel':
//console.log(res);
if (this.currentAuncel.id === res.deviceId) {
this.currentAuncel.weightData = res.weightData
this.currentAuncel.weightUnit = res.weightUnit
this.currentAuncel.weightStable = res.weightStable
this.currentAuncel.isConnected = 1
if (Number(res.weightData) == 0) {
this.weightDataIsToZero = true
}
}
break
default:
break
}
})
//设备状态
uni.$on('deviceStatus', res => {
if (this.currentAuncel.id === res.deviceId) {
console.log(res)
if (res.connected == 0) {
this.currentAuncel.weightStable = 0
this.currentAuncel.weightData = '天平断开'
this.currentAuncel.weightUnit = ''
}
}
this.currentAuncel.isConnected = res.connected
})
//控制设备状态
uni.$on('controlDevice', res => {
if (this.currentAuncel.id === res.deviceId) {
console.log(res)
this.currentAuncel.id = ''
this.currentAuncel.name = ''
this.currentAuncel.code = ''
this.currentAuncel.weightStable = 0
this.currentAuncel.weightData = '请选天平'
this.currentAuncel.weightUnit = ''
}
})
//连接断开
uni.$on('connClose', res => {
//重置
this.currentAuncel.weightData = ''
this.currentAuncel.weightUnit = ''
this.currentAuncel.weightStable = 0
this.currentAuncel.controlUserName = ''
this.currentAuncel.isConnected = 0
})
},
onHide() {
console.log('onHide')
//移除监听websocket返回来的数据
uni.$off('deviceData')
uni.$off('deviceStatus')
uni.$off('controlDevice')
uni.$off('connClose')
},
onUnload() {
//取消控制
//发送控制请求
let controlDevice = {
msgId: this.currentAuncel.id,
cmd: 'controlDevice',
clientType: 'caaClient',
data: {
deviceId: this.currentAuncel.id,
isControl: false,
controlRealName: this.userInfo.username
}
}
//发送控制数据
this.$measure.send(JSON.stringify(controlDevice))
},
methods: {
//返回上一页
customBack() {
uni.redirectTo({
url: '/pages/sample/sample-weigh'
})
},
//切换样品
async swichSample(index) {
if (index == this.current) return
if (this.orderWeighChecked) return //顺序称重不允许选中
this.current = index
// 如果为0意味着尚未初始化
if (this.menuHeight == 0 || this.menuItemHeight == 0) {
await this.getElRect('menu-scroll-view', 'menuHeight')
await this.getElRect('u-tab-item', 'menuItemHeight')
}
// 将菜单菜单活动item垂直居中
this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2
this.currentSample = this.notWeighSampleList[index]
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this)
query
.select('.' + elClass)
.fields({ size: true }, res => {
// 如果节点尚未生成res值为null循环调用执行
if (!res) {
setTimeout(() => {
this.getElRect(elClass)
}, 10)
return
}
this[dataVal] = res.height
})
.exec()
})
},
confirm() {
//保存数据
if (!this.weightDataIsToZero) {
this.$helper.showToast({
title: '天平必须先回零!'
})
return
}
//获取当前重量
let weight = this.currentAuncel.weightData
//获取当前样品
let sample = this.notWeighSampleList[this.current]
//判断杯号
if (this.currentCupNum == null || typeof this.currentCupNum == 'undefined') this.currentCupNum = ''
if (this.currentCupNum === '') {
this.$helper.showToast({
title: '杯号不允许为空!'
})
return
}
if (this.current < this.notWeighSampleList.length - 1) {
this.currentSample = this.notWeighSampleList[this.current + 1]
}
//杯号
sample.cupNum = this.currentCupNum
//删除当前样品
this.notWeighSampleList.splice(this.current, 1)
//样品重量赋值
sample.sampleWeight = weight
//天平编号赋值
sample.auncelNo = this.currentAuncel.code
if (sample.remarks) {
sample.remarks = sample.remarks + '%'
}
this.$u.api
.auncelWeigh(sample)
.then(res => {
//this.finishWeighSampleList.push(sample);
//确认采集重量后归零设为false
this.weightDataIsToZero = false
this.getAssayTaskDetail(this.currentTaskNo)
if (this.orderWeighChecked) {
this.current = 0
} else {
this.current = -1
this.currentSample = {}
}
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
submit() {
if (this.notWeighSampleList.length != 0) {
this.$helper.showToast({
title: '当前任务“' + this.currentTaskNo + '”尚未完成称重!'
})
return
}
uni.showModal({
title: '提示',
content: '确定提交数据?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.cancel) {
console.log('用户点击取消')
return
}
//显示loading
uni.showLoading({
title: '正在提交...'
})
this.$u.api
.weighBatchSubmitByTaskNo(this.currentTaskNo)
.then(res => {
this.$helper.showToast({
title: '提交成功!'
})
uni.redirectTo({
url: '/pages/sample/sample-weigh'
})
uni.hideLoading()
})
.catch(err => {
uni.hideLoading()
this.$helper.showToast({
title: '提交失败!'
})
console.log(err)
})
}
})
/**
if(this.currentTaskType == 4) { //临时样
let ids = [];
this.finishWeighSampleList.forEach(item => {
ids.push(item.id);
});
this.$u.api
.weighBatchSubmitByIds(ids.join(","))
.then(res => {
this.$helper.showToast({
title: '提交成功!'
});
})
.catch(err => {
this.showLoading = false;
console.log(err);
});
} else {//其他批量秤完提交
//if(this.allWeighSampleCount == this.finishWeighSampleList.length) {
console.log(this.notWeighSampleList)
if(this.notWeighSampleList.length == 0) {
this.$u.api
.weighBatchSubmitByTaskNo(this.currentTaskNo)
.then(res => {
this.$helper.showToast({
title: '提交成功!'
});
uni.redirectTo({
url: '/pages/sample/sample-weigh'
});
})
.catch(err => {
this.showLoading = false;
console.log(err);
});
} else {
this.$helper.showToast({
title: '当前任务“'+this.currentTaskNo+'”尚未完成称重!'
});
return;
}
}
**/
},
weighRevoke(id) {
uni.showModal({
title: '提示',
content: '确定撤销当前数据?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (!res.confirm) {
console.log('用户点击取消')
return
}
this.$u.api
.auncelWeighRevoke(id)
.then(res => {
this.getAssayTaskDetail(this.currentTaskNo)
if (this.orderWeighChecked) {
this.current = 0
} else {
this.current = -1
this.currentSample = {}
}
})
.catch(err => {
this.showLoading = false
console.log(err)
})
}
})
},
//自动生成杯号(仅顺序称重):第一杯:手填,后续杯 = 上一杯 + 1
autoGenerateCupNum() {
if (!this.orderWeighChecked) return
//取上一杯
this.currentCupNum = ''
let preSample = null
if (this.finishWeighSampleList && this.finishWeighSampleList.length > 0) preSample = this.finishWeighSampleList[0]
if (preSample == null) {
return
}
const cupNum = preSample.cupNum
if (!isNaN(cupNum)) this.currentCupNum = Number(cupNum) + 1
},
getAssayTaskDetail(taskNo) {
this.notWeighSampleList = []
this.finishWeighSampleList = []
this.$u.api
.getAssayTaskDetailListByTaskNo({ taskNo: taskNo })
.then(res => {
let dataList = res.result
this.allWeighSampleCount = dataList.length
dataList.forEach((item, index) => {
if (item.weightSubmitStatus == 0) {
this.notWeighSampleList.push(item)
} else if (item.weightSubmitStatus == 1) {
this.finishWeighSampleList.unshift(item)
//this.finishWeighSampleList.push(item);
}
})
this.showLoading = false
//console.log(this.currentSample);
if (this.notWeighSampleList[0]) {
if (this.orderWeighChecked) {
this.current = 0
this.currentSample = this.notWeighSampleList[0]
} else {
this.current = -1
this.currentSample = {}
}
}
this.autoGenerateCupNum() //计算生成当前杯号
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
selectAuncel() {
if (this.currentAuncel.weightData === '请选天平') {
uni.navigateTo({
url:
'/pages/auncel/auncel-select?currentTaskNo=' +
this.currentTaskNo +
'&currentTaskType=' +
this.currentTaskType
})
}
},
orderWeighChange(e) {
if (e && this.notWeighSampleList.length > 0) {
//this.current = 0;
//this.currentSample = this.notWeighSampleList[0];
this.getAssayTaskDetail(this.currentTaskNo)
return
}
this.current = -1
this.currentSample = {}
}
},
filters: {
getDataSourceTypeShow(val) {
if (val == 2) return '【筛上】'
if (val == 3) return '【筛下】'
return ''
}
}
}
</script>
<style lang="scss" scoped>
.content-title {
height: 110rpx;
width: 100%;
font-size: 46rpx;
font-weight: 300;
}
.content-title-name {
padding: 20rpx;
text-align: center;
}
.content-main-height {
height: calc(100vh - 250rpx);
}
.content-main-left {
height: calc(100vh - 410rpx);
background-color: #f6f6f6;
}
.u-tab-item {
height: 160rpx;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #444;
font-weight: 400;
line-height: 1;
border-width: 4rpx;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-size: 36rpx;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 32rpx;
left: 0;
top: 39rpx;
}
.content-main-left-operation {
height: 160rpx;
padding-top: 30rpx;
}
.content-main-right {
height: calc(100vh - 410rpx);
}
.content-main-right-operation {
height: 160rpx;
padding-top: 30rpx;
}
.btn-operation {
height: 100rpx;
font-size: 36rpx;
}
.auncel-container {
display: flex;
justify-content: center;
align-items: center;
}
.auncel {
width: 800rpx;
height: 800rpx;
background-image: url(../../static/images/auncel.png);
background-repeat: no-repeat;
background-size: 100%;
}
.auncel-title {
width: 100%;
height: 420rpx;
}
.auncel-weight {
padding: 40rpx;
}
.weight {
width: 100%;
min-width: 200px;
height: 200rpx;
padding: 0 30rpx;
display: flex;
flex-direction: row;
//background-color: #2c405a;
}
.weight-data {
flex-grow: 1;
height: 100%;
color: #4cd964;
text-align: right;
line-height: 200rpx;
letter-spacing: 4rpx;
font-size: 150rpx;
font-family: zzjc-lcd;
}
.weight-data-yellow {
flex-grow: 1;
height: 100%;
color: #ffff00;
text-align: right;
line-height: 200rpx;
letter-spacing: 4rpx;
font-size: 150rpx;
font-family: zzjc-lcd;
}
.weight-data-warning {
flex-grow: 1;
height: 100%;
color: #ff3333;
text-align: right;
line-height: 200rpx;
font-size: 150rpx;
font-family: zzjc-lcd;
}
.weight-unit {
color: #ffffff;
font-size: 130rpx;
line-height: 200rpx;
padding-left: 20rpx;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<view>
<navbar-back title="检斤检化验系统" :autoBack="false" leftIcon="" :leftText="`您好!${userInfo.nickname}`">
<u-icon @click="popupShow = true" size="28" color="#FFF" name="account-fill" />
</navbar-back>
<up-grid :col="gridCol" :border="false">
<up-grid-item class="mb20 mt20" v-for="item in menuItemList" :key="item.url" @click="goTo(item.url)">
<u-icon :name="item.otherConf.icon" color="#0055A2" size="80" />
<view class="grid-text">{{ item.name }}</view>
</up-grid-item>
</up-grid>
<mePopup :show="popupShow" @update:show="val => (popupShow = val)" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import nx from '@/nx'
import { useGridCol } from '@/nx/hooks/useGridCol'
import mePopup from '@/pages/index/me-popup.vue'
// 响应式数据
const popupShow = ref(false)
const isAllowAgainPrint = ref(false)
const menuItemList = ref([
{ url: '/pages/analysis/sample/sample-receive', otherConf: { icon: 'arrow-downward' }, name: '收样' },
{ url: '/pages/analysis/sample/sample-work-list', otherConf: { icon: 'edit-pen-fill' }, name: '样品分析' },
{ url: '/pages/analysis/sample/sample-report', otherConf: { icon: 'arrow-upward' }, name: '数据上报' },
{ url: '/pages/analysis/sample/sample-report-search', otherConf: { icon: 'search' }, name: '待审数据' },
{ url: '/pages/analysis/sample/sample-print', otherConf: { icon: 'file-text-fill' }, name: '单据补打' },
{ url: '/pages/analysis/setting/setting', otherConf: { icon: 'setting-fill' }, name: '系统设置' }
])
// 计算属性
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const goTo = url => {
nx.$router.go(url)
}
const checkAllowAgainPrint = () => {
const param = {
userId: userInfo.value.id,
clientId: 'auncel'
}
nx.$api.user.getAppPermission(param).then(res => {
if (!res.success) return
const ret = res.result || []
if (ret.length === 0) return
for (let i = 0; i < ret.length; i++) {
if (ret[i].roleValue && ret[i].roleValue === 2) {
isAllowAgainPrint.value = true
}
}
})
}
const openLaboratoryWs = () => {
const regData = {
msgId: nx.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: userInfo.value.id,
tenantId: userInfo.value.loginTenantId,
userRealName: userInfo.value.realname
}
}
nx.$measure.setRegData(JSON.stringify(regData))
nx.$measure.open()
}
// 生命周期
onMounted(() => {
// checkAllowAgainPrint()
// openLaboratoryWs()
})
// 动态设置 grid 列数
const { gridCol } = useGridCol([400], [2, 3])
</script>
<style scoped lang="scss">
.title_content {
text-align: center;
padding-top: 75px;
letter-spacing: 10px;
width: 100%;
font-size: 36px;
color: #fff;
}
.banner {
overflow: hidden;
background-repeat: no-repeat;
background-size: 100%;
}
.grid-text {
font-size: 24px;
}
</style>

View File

@@ -0,0 +1,583 @@
<template>
<view class="page">
<u-navbar
title="化学分析天平"
:is-back="false"
:border-bottom="false"
title-color="#fff"
:background="{ 'background-image': 'linear-gradient(45deg, rgb(54, 138, 217), rgb(64, 160, 255))' }"
></u-navbar>
<view style="text-align: center"><u-loading mode="flower" :show="showLoading" size="48"></u-loading></view>
<view class="container">
<view class="auncel" v-for="(item, index) in deviceLaboratoryList" :key="item.id">
<view class="title">
<u-row style="width: 100%; padding-bottom: 18rpx" gutter="16">
<u-col span="8">
<text class="measurePointName"
>{{ item.name }} {{ item.controlUserName ? '[' + item.controlUserName + ']远程控制' : '' }}</text
>
</u-col>
<u-col span="4">
<!--
<view style="width: 100%; display: flex; flex-direction: row; justify-content: flex-end;">
<text class="measurePointName" >{{item.isAutoWork ? '自动计量' : '集控接管'}}</text>
<u-icon style="padding: 0 20rpx;" name="file-text" label-pos="bottom" color="#2979ff" size="40" @click="goto(item.measurePointId)"></u-icon>
<u-icon style="padding: 0 20rpx;" name="setting" label-pos="bottom" label="设置" color="#2979ff" size="40" @click="goto(item.measurePointId)"></u-icon>
</view>
-->
</u-col>
</u-row>
</view>
<view :class="item.isConnected == 1 ? 'title-line-green' : 'title-line-gray'"></view>
<view class="weight-container">
<!-- <view class="infrared-container">
<view :class="item.infraredLeft == 0 ? 'infrared-line-gray' : item.infraredLeft == 1 ? 'infrared-line-green':'infrared-line-red'"></view>
</view>
-->
<block v-if="item.deviceType == 'auncel'">
<view class="weight">
<view
:class="
item.weightStable == 0
? 'weight-data-yellow'
: item.weightStable == 1
? 'weight-data'
: 'weight-data-warning'
"
>
{{ item.weightData }}
</view>
<view class="weight-unit">{{ item.weightUnit }}</view>
</view>
</block>
<block v-if="item.deviceType == 'temp'">
<view class="temp">
<view class="charts-box"
><qiun-data-charts
type="gauge"
:opts="temperatureOpts"
:chartData="temperatureChartData"
background="none"
/></view>
<view class="charts-box"
><qiun-data-charts type="gauge" :opts="humidityOpts" :chartData="humidityChartData" background="none"
/></view>
</view>
</block>
<!--
<view class="infrared-container">
<view :class="item.infraredRight == 0 ? 'infrared-line-gray' : item.infraredRight == 1 ? 'infrared-line-green':'infrared-line-red'"></view>
</view>
-->
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
showLoading: false,
runLogMaxLength: 8,
deviceLaboratoryList: [],
temperatureOpts: {},
temperatureChartData: {
categories: [
{
value: 0.2,
color: '#1890ff'
},
{
value: 1,
color: '#2fc25b'
}
],
series: [
{
name: '温度',
data: 0.66
}
]
},
humidityOpts: {},
humidityChartData: {
categories: [
{
value: 1,
color: '#1890ff'
}
],
series: [
{
name: '湿度',
data: 0.66
}
]
}
}
},
onLoad() {
//获取数据
this.getPageData()
//监听websocket返回来的数据
//设备数据
uni.$on('deviceData', res => {
switch (res.deviceType) {
case 'weighbridge':
this.mearsuPointList.forEach((item, index) => {
if (item.measurePointId === res.measurePointId) {
//item.weightData = (index + Number(res.deviceValue)).toFixed(2) + " "+ res.deviceUnit;
item.weightData = res.deviceValue
item.weightUnit = res.deviceUnit
item.weightStable = res.deviceStable
item.measureIsConnected = 1
item.isAutoWork = res.isAutoWork
}
})
break
case 'auncel':
this.deviceLaboratoryList.forEach((item, index) => {
if (item.id === res.deviceId) {
//item.weightData = (index + Number(res.deviceValue)).toFixed(2) + " "+ res.deviceUnit;
item.weightData = res.weightData
item.weightUnit = res.weightUnit
item.weightStable = 1
item.isConnected = 1
}
})
break
default:
break
}
// this.weight = res.weighbridge;
// this.infraredLeft = res.infrared;
// this.infraredRight = res.infrared;
})
//设备状态
uni.$on('deviceStatus', res => {
this.mearsuPointList.forEach((item, index) => {
if (item.measurePointId === res.measurePointId) {
item.devices.forEach((d, i) => {
if (d.id === res.deviceId) {
d.isConnected = res.connected
}
})
}
})
})
//日志数据
uni.$on('logData', res => {
this.mearsuPointList.forEach((item, index) => {
if (item.measurePointId === res.measurePointId) {
if (item.runLogs.length >= this.runLogMaxLength) {
//删除并返回数组的第一个元素
//item.runLogs.shift();
//删除并返回数组的最后一个元素
item.runLogs.pop()
}
//向数组的末尾添加
//item.runLogs.push(res);
//向数组的开头添
item.runLogs.unshift(res)
//滚动
// this.$nextTick(function() {
// item.scrollTop = 20*item.runLogs.length;
// });
}
})
})
//监听控制
uni.$on('controlMeasurePoint', res => {
console.log(res)
let data = res.data
this.mearsuPointList.forEach(i => {
if (i.measurePointId === data.measurePointId) {
if (data.success && data.isControl) {
i.controlUserName = data.controlUserName
} else {
i.controlUserName = ''
}
console.log(i)
}
})
})
//计量点状态
uni.$on('measurePointStatus', res => {
//断开
this.mearsuPointList.forEach(i => {
if (i.measurePointId === res.measurePointId) {
if (res.measureIsConnected != 1) {
i.weightData = ''
i.weightUnit = ''
i.weightStable = 0
i.infraredLeft = 0
i.infraredRight = 0
i.measureIsConnected = 0
if (i.runLogs.length >= this.runLogMaxLength) {
i.runLogs.pop()
}
i.runLogs.unshift({
id: this.$helper.uuid(),
content: '计量点连接断开!',
measurePointId: i.measurePointId,
time: this.$helper.dateFormat(new Date(), 'yy-MM-dd hh:mm:ss')
})
i.devices.forEach(d => {
d.isConnected = 0
})
} else {
i.measureIsConnected = 1
/* if(i.runLogs.length >= 8) {
i.runLogs.shift();
}
i.runLogs.push({
id: this.$helper.uuid(),
content: "计量点连接成功!",
measurePointId: i.measurePointId,
time: this.$helper.dateFormat(new Date(), "yy-MM-dd hh:mm:ss")
}); */
}
}
})
})
//连接断开
uni.$on('connClose', res => {
//重置
this.mearsuPointList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.weightStable = 0
i.controlUserName = ''
i.measureIsConnected = 0
if (i.runLogs.length >= this.runLogMaxLength) {
i.runLogs.pop()
}
let lastLog = i.runLogs[i.runLogs.length - 1]
if (lastLog && lastLog.content !== '控制中心连接断开!') {
i.runLogs.unshift({
id: this.$helper.uuid(),
content: '控制中心连接断开!',
measurePointId: i.measurePointId,
time: this.$helper.dateFormat(new Date(), 'yy-MM-dd hh:mm:ss')
})
}
i.devices.forEach(d => {
d.isConnected = 0
})
})
})
},
onUnload() {
//移除监听websocket返回来的数据
uni.$off('deviceData')
uni.$off('deviceStatus')
uni.$off('logData')
uni.$off('controlMeasurePoint')
uni.$off('measurePointStatus')
uni.$off('connClose')
},
filters: {
numFilter(value) {
// 截取当前数据到小数点后两位
let realVal = value.toFixed(2)
return realVal
}
},
computed: {},
methods: {
getPageData() {
//显示loading
this.showLoading = true
//获取计量点数量
this.$u.api
.getDeviceLaboratoryListBy()
.then(res => {
let dataList = res.data
dataList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.isConnected = 0
i.weightStable = 0
i.temperature = 0
i.humidity = 0
i.controlUserName = ''
})
this.deviceLaboratoryList = dataList
this.showLoading = false
//打开websocket
this.openLaboratoryWs()
//发送检查计量点控制情况
let checkControl = {
msgId: this.$helper.uuid(),
cmd: 'checkControl',
clientType: 'consoleClient'
}
if (this.$measure.isOpen) {
this.$measure.send(JSON.stringify(checkControl))
}
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
openLaboratoryWs() {
//注册websocket
let regData = {
msgId: this.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: this.userInfo.user_id,
tenantId: this.userInfo.tenant_id,
userRealName: this.userInfo.real_name
}
}
this.$measure.setRegData(JSON.stringify(regData))
this.$measure.open()
},
scroll(e) {
// console.log(e);
// this.mearsuPointList.forEach((i) => {
// if(i.measurepointid === e.currentTarget.dataset.measurepointid) {
// i.oldScrollTop = e.detail.scrollTop;
// }
// });
},
goto(measurePointId) {
uni.reLaunch({
url: '/pages/measure/measure?measurePointId=' + measurePointId
})
}
}
}
</script>
<style>
.container {
width: 100%;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
/* #ifdef H5 */
.auncel {
width: 31.33%;
display: flex;
flex-direction: column;
padding: 20rpx;
margin: 1%;
border-radius: 24rpx;
box-shadow: 1px 1px 10px #909399;
}
@media only screen and (max-width: 1200px) {
.auncel {
width: 48%;
}
}
@media only screen and (max-width: 600px) {
.auncel {
width: 100%;
}
}
/* #endif */
/* #ifndef H5 */
.auncel {
width: 98%;
display: flex;
flex-direction: column;
padding: 20rpx;
margin: 1%;
border-radius: 24rpx;
box-shadow: 1px 1px 10px #909399;
}
/* #endif */
.title-line-gray {
width: 100%;
height: 6rpx;
top: 0;
left: 0;
background-color: rgb(128, 134, 149);
border-radius: 4rpx 4rpx 0 0;
}
.title-line-green {
width: 100%;
height: 6rpx;
top: 0;
left: 0;
background-color: rgb(27, 201, 142);
border-radius: 4rpx 4rpx 0 0;
}
.title {
display: flex;
flex-direction: row;
justify-content: center;
}
.temp {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
}
.charts-box {
width: 50%;
height: 200px;
}
.measurePointName {
font-size: 30rpx;
}
.weight-container {
padding: 10rpx;
display: flex;
flex-direction: row;
}
.infrared-container {
width: 30%;
display: flex;
justify-content: center;
}
.infrared-line-green {
width: 8rpx;
height: 90rpx;
background-color: #18b566;
}
.infrared-line-red {
width: 8rpx;
height: 90rpx;
background-color: #ff3333;
}
.infrared-line-gray {
width: 8rpx;
height: 90rpx;
background-color: #909399;
}
.infrared-left {
width: 8rpx;
height: 90rpx;
background-color: #18b566;
}
.infrared-left-warning {
width: 8rpx;
height: 90rpx;
background-color: #ff3333;
}
.weight {
width: 100%;
min-width: 200px;
height: 90rpx;
padding: 0 8rpx;
display: flex;
flex-direction: row;
background-color: #2c405a;
}
.weight-data {
flex-grow: 1;
height: 100%;
color: #4cd964;
text-align: right;
line-height: 90rpx;
letter-spacing: 4rpx;
font-size: 90rpx;
font-family: zzjc-lcd;
}
.weight-data-yellow {
flex-grow: 1;
height: 100%;
color: #ffff00;
text-align: right;
line-height: 90rpx;
letter-spacing: 4rpx;
font-size: 90rpx;
font-family: zzjc-lcd;
}
.weight-data-warning {
flex-grow: 1;
height: 100%;
color: #ff3333;
text-align: right;
line-height: 90rpx;
font-size: 90rpx;
font-family: zzjc-lcd;
}
.weight-unit {
color: #ffffff;
font-size: 60rpx;
line-height: 90rpx;
padding-left: 20rpx;
}
.infrared-right {
width: 8rpx;
height: 90rpx;
background-color: #18b566;
}
.infrared-right-warning {
width: 8rpx;
height: 90rpx;
background-color: #ff3333;
}
.device-container {
padding: 20rpx;
width: 100%;
height: 200rpx;
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
justify-content: flex-start;
}
.device {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
}
.device-icon {
width: 40rpx;
height: 40rpx;
background-color: #ff3333;
border-radius: 50%;
}
.device-icon-green {
width: 40rpx;
height: 40rpx;
background-color: #18b566;
border-radius: 50%;
}
.device-icon-gray {
width: 40rpx;
height: 40rpx;
background-color: rgb(128, 134, 149);
border-radius: 50%;
}
.device-icon-red {
width: 40rpx;
height: 40rpx;
background-color: #ff3333;
border-radius: 50%;
}
.run-log {
width: 100%;
padding: 10rpx 20rpx;
height: 400rpx;
background-color: #ecf5ff;
border-radius: 24rpx;
--box-shadow: 1px 1px 10px #909399;
}
.log-content {
font-size: 28rpx;
font-weight: 200;
margin-bottom: 10rpx;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<view>
<navbar-back title="任务单预览"></navbar-back>
<view></view>
<!-- #ifdef H5 -->
<web-view :src="pdfUrlH5"></web-view>
<!-- #endif -->
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onLoad, onHide, onUnload } from '@dcloudio/uni-app'
import { getBaseUrl } from '@/defaultBaseUrl'
import nx from '@/nx'
// 响应式数据
const taskId = ref('')
const localFilePath = ref('')
const reportKey = ref('')
const hideResultFlag = ref('')
const viewerUrl = '/hybrid/html/web/viewer.html?file='
const pdfUrlH5 = ref('')
let wv = null // 计划创建的 webview
// 方法:下载 PDF
const downloadPdf = (pdfUrl, retryCount = 0) => {
const MAX_RETRY = 3 // 最多重试 3 次
return new Promise(resolve => {
if (retryCount >= MAX_RETRY) {
console.error('PDF 下载失败:超过最大重试次数')
return resolve(null)
}
uni.downloadFile({
url: pdfUrl,
success: res => {
if (res.statusCode !== 200) {
console.warn(`下载失败,状态码: ${res.statusCode},重试第 ${retryCount + 1}`)
return resolve(downloadPdf(pdfUrl, retryCount + 1))
}
const tempFilePath = res.tempFilePath
uni.getFileInfo({
filePath: tempFilePath,
success: fileInfo => {
if (fileInfo.size > 0) {
resolve(tempFilePath)
} else {
console.warn('文件为空,重试中...')
resolve(downloadPdf(pdfUrl, retryCount + 1))
}
},
fail: () => {
console.warn('获取文件信息失败,重试中...')
resolve(downloadPdf(pdfUrl, retryCount + 1))
}
})
},
fail: () => {
console.warn('网络请求失败,重试中...')
resolve(downloadPdf(pdfUrl, retryCount + 1))
}
})
})
}
// 方法Android 加载 PDF
const loadPdfOnAndroid = async pdfUrl => {
const tempFilePath = await downloadPdf(pdfUrl)
if (!tempFilePath) return
const localPath = plus.io.convertLocalFileSystemURL(tempFilePath)
localFilePath.value = localPath
const allUrl = viewerUrl + encodeURIComponent(localPath)
// 创建并加载 webview
wv = plus.webview.create('', 'custom-webview', {
top: uni.getSystemInfoSync().statusBarHeight + 44,
bottom: 0
})
wv.loadURL(allUrl)
const currentWebview = getCurrentPages().pop().$getAppWebview()
currentWebview.append(wv)
}
// 获取 PDF 预览 URL
const getPdf = async () => {
const printBaseUrl = getBaseUrl()
const baseUrl = getBaseUrl()
let dataUrl = `${baseUrl}/qms/bus/qmsBusAssayTask/getAssayTaskDataWithDetailData?taskId=${taskId.value}`
dataUrl += `&hideResultFlag=${hideResultFlag.value}`
const token = nx.$store('user').token
let url = `${printBaseUrl}/report/gridpp/report/previewDocs`
url += `?token=${token}`
url += `&type=PDF`
url += `&reportKey=${reportKey.value}`
url += `&dataUrl=${encodeURIComponent(dataUrl)}`
// #ifdef H5
pdfUrlH5.value = viewerUrl + encodeURIComponent(url)
// #endif
// #ifndef H5
await loadPdfOnAndroid(url)
// #endif
}
// 删除临时文件
const deleteTmpFile = () => {
if (!localFilePath.value) return
const dir = localFilePath.value.substring(0, localFilePath.value.lastIndexOf('/'))
plus.io.resolveLocalFileSystemURL(
dir,
entry => {
entry.removeRecursively(
() => console.log('删除成功'),
e => console.log('删除失败:' + e.message)
)
},
e => console.log('目录不存在:' + e.message)
)
}
// 生命周期
onLoad(param => {
if (param.taskId) {
taskId.value = param.taskId
reportKey.value = param.reportKey
hideResultFlag.value = param.hideResultFlag
}
getPdf()
})
onHide(() => {
deleteTmpFile()
})
onUnload(() => {
deleteTmpFile()
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,237 @@
<template>
<view>
<navbar-back :autoBack="false" title="任 务 单" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current == index ? 'u-tab-item-active' : '']"
:data-current="index"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 42rpx">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 20rpx">
<text>{{ task.taskName }}</text>
</view>
<view class="x-f" style="margin-top: 20rpx">
<u-icon color="" name="clock"></u-icon>
<text style="margin-left: 10rpx">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-height">
<block v-for="(sample, index) in sampleList" :key="index">
<view
v-if="index == 0 || (index > 0 && sampleList[index].sort != sampleList[index - 1].sort)"
style="padding: 10rpx; font-size: 36rpx"
>
<u-row>
<u-col span="2" style="text-align: center">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="10">
<view>
<text style="padding-left: 20rpx">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 20rpx">{{ sample.sampleName }}</text>
</view>
</u-col>
</u-row>
<u-line style="padding: 10rpx" color="#bbb" />
</view>
</block>
</scroll-view>
</view>
</u-col>
</u-row>
</view>
</template>
<script>
export default {
data() {
return {
scrollTop: 0, //tab标题的滚动条位置
current: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
taskList: [],
sampleList: []
}
},
onLoad() {
//获取任务列表
this.getAssayTask()
},
methods: {
//返回首页
customBack() {
uni.reLaunch({
url: '/pages/analysis/index/index'
})
},
//切换任务
async swichTask(index) {
if (index == this.current) return
this.current = index
// 如果为0意味着尚未初始化
if (this.menuHeight == 0 || this.menuItemHeight == 0) {
await this.getElRect('menu-scroll-view', 'menuHeight')
await this.getElRect('u-tab-item', 'menuItemHeight')
}
// 将菜单菜单活动item垂直居中
this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2
//获取任务详情
this.getAssayTaskDetail(this.taskList[index].taskNo)
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this)
query
.select('.' + elClass)
.fields({ size: true }, res => {
// 如果节点尚未生成res值为null循环调用执行
if (!res) {
setTimeout(() => {
this.getElRect(elClass)
}, 10)
return
}
this[dataVal] = res.height
})
.exec()
})
},
getAssayTask() {
//显示loading
uni.showLoading({
title: '加载中...'
})
const param = {
assayOper: this.userInfo.realname,
operateType: 'task'
}
this.$u.api
.getAssayTaskList(param)
.then(res => {
this.taskList = res.result
if (this.taskList && this.taskList.length > 0) {
this.getAssayTaskDetail(this.taskList[0].taskNo)
} else {
uni.hideLoading()
}
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
},
getAssayTaskDetail(taskNo) {
this.$u.api
.getAssayTaskDetailListByTaskNo({ taskNo: taskNo })
.then(res => {
this.sampleList = res.result
uni.hideLoading()
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
}
}
}
</script>
<style lang="scss" scoped>
.content-title {
height: 110rpx;
width: 100%;
font-size: 46rpx;
font-weight: 300;
}
.content-title-name {
padding: 20rpx;
text-align: center;
}
.content-main-height {
height: calc(100vh - 250rpx);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 10rpx;
height: 200rpx;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #444;
font-weight: 400;
line-height: 1;
border-width: 4rpx;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-size: 36rpx;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 32rpx;
left: 0;
top: 39rpx;
}
</style>

View File

@@ -0,0 +1,318 @@
<template>
<view>
<navbar-back :autoBack="false" title="单据补打" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<!-- 左侧任务列表 -->
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
ref="menuScrollView"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="{ 'u-tab-item-active': current === index }"
@tap.stop="switchTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 18px">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 10px">
<text>{{ task.taskName }} {{ task.assayOper }}</text>
</view>
<view class="x-f" style="margin-top: 10px">
<u-icon name="clock"></u-icon>
<text style="margin-left: 5px">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<!-- 右侧样品列表 -->
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view style="padding: 5px; font-size: 16px">
<u-row>
<u-col span="2" style="text-align: center">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="5">
<view>
<text style="padding-left: 10px">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 10px">{{ sample.sampleName }}</text>
</view>
</u-col>
</u-row>
<u-line style="padding: 10rpx" color="#bbb" />
</view>
</block>
</scroll-view>
<!-- 操作按钮 -->
<view class="content-main-right-operation">
<u-row>
<u-col span="6"></u-col>
<u-col span="3">
<u-button class="btn-operation" type="primary" @click="previewPDF" v-if="currentTaskId">
预览任务单
</u-button>
</u-col>
<u-col span="3">
<u-button class="btn-operation" :disabled="taskList.length <= 0" type="success" @click="printTask">
打印任务单
</u-button>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import nx from '@/nx'
// refs
const scrollTop = ref(0)
const current = ref(0)
const menuHeight = ref(0)
const menuItemHeight = ref(0)
const currentTask = ref({})
const currentTaskId = ref('')
const currentTaskNo = ref('')
const conAssayTask = ref(null)
const taskList = ref([])
const sampleList = ref([])
const menuScrollView = ref(null)
// 生命周期
onMounted(() => {
getAssayTask()
})
checkPrintConfig()
// 方法
function customBack() {
uni.reLaunch({
url: '/pages/analysis/index/index'
})
}
function taskStyle(task) {
return task.weightTaskStatus === 3 ? 'green' : ''
}
// 切换任务
async function switchTask(index) {
if (index === current.value) return
current.value = index
// 初始化高度
if (menuHeight.value === 0 || menuItemHeight.value === 0) {
await getElRect('menu-scroll-view', 'menuHeight')
await getElRect('.u-tab-item', 'menuItemHeight')
}
// 滚动居中
scrollTop.value = index * menuItemHeight.value + menuItemHeight.value / 2 - menuHeight.value / 2
// 更新当前任务
const task = taskList.value[index]
currentTask.value = task
currentTaskNo.value = task.taskNo
currentTaskId.value = task.id
getAssayTaskDetail(currentTaskNo.value)
}
// 获取元素尺寸
function getElRect(selector, targetRef) {
return new Promise(resolve => {
const query = uni.createSelectorQuery().in(getCurrentInstance().proxy)
query
.select(selector)
.fields({ size: true }, res => {
if (!res) {
// 如果未获取到,稍后重试(可选)
setTimeout(() => resolve(getElRect(selector, targetRef)), 10)
return
}
if (targetRef === 'menuHeight') {
menuHeight.value = res.height
} else if (targetRef === 'menuItemHeight') {
menuItemHeight.value = res.height
}
resolve(res)
})
.exec()
})
}
// 获取任务列表
function getAssayTask() {
const param = {
operateType: 'print',
finishStatus: 'submited,finished'
}
nx.$api.auncel.getAssayTaskList(param).then(res => {
taskList.value = res || []
if (taskList.value.length > 0) {
const first = taskList.value[0]
currentTask.value = first
currentTaskNo.value = first.taskNo
currentTaskId.value = first.id
getAssayTaskDetail(currentTaskNo.value)
} else {
currentTask.value = {}
currentTaskNo.value = ''
currentTaskId.value = ''
}
})
}
// 获取任务详情
function getAssayTaskDetail(taskNo) {
if (!taskNo) return
nx.$api.assayTask.getAssayTaskDetailListByTaskNo({ taskNo }).then(res => {
sampleList.value = res.result || []
if (res.additionalProperties?.conAssayTask) {
conAssayTask.value = res.additionalProperties.conAssayTask
}
})
}
// 打印任务
function printTask() {
const task = currentTask.value
uni.showModal({
title: '提示',
content: `确定补打“${task.taskNo}的原始记录单”?`,
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.confirm) {
nx.$print.getPrintTemplateAndPrint(currentTask.value)
}
}
})
}
// 预览 PDF
function previewPDF() {
const url = `/pages/analysis/sample/pdf-preview?taskId=${currentTaskId.value}&reportKey=${conAssayTask.value?.assayTaskTemplateKey}&hideResultFlag=true`
uni.navigateTo({ url })
}
// 检查打印服务配置(模拟 onShow
function checkPrintConfig() {
const printList = uni.getStorageSync('KEY_PRINT_LIST')
if (!printList || printList.length <= 0) {
uni.showToast({
title: '打印服务未配置,请在系统设置中配置打印服务!',
icon: 'none'
})
}
}
</script>
<style lang="scss" scoped>
.content-title {
height: 50px;
width: 100%;
font-size: 20px;
font-weight: 300;
}
.content-title-name {
padding: 10px;
text-align: center;
}
.content-main-height {
height: calc(100vh - 125px);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 5px;
height: 100px;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #444;
line-height: 1;
border-width: 2px;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 16px;
left: 0;
top: 20px;
}
.content-main-right {
height: calc(100vh - 205px);
}
.content-main-right-operation {
height: 80px;
padding-top: 15px;
padding-right: 15px;
}
.btn-operation {
width: 95%;
}
</style>

View File

@@ -0,0 +1,339 @@
<template>
<view>
<navbar-back :autoBack="false" title="样品分析-收样" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<!-- 任务列表 -->
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current === index ? 'u-tab-item-active' : '']"
@tap.stop="switchTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view class="fs18">{{ task.taskNo }}</view>
<view style="margin-top: 10px">{{ task.taskName }}</view>
<view class="x-f" style="margin-top: 10px">
<u-icon name="clock"></u-icon>
<text style="margin-left: 5px">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<!-- 样品列表 -->
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view style="padding: 5px; font-size: 16px">
<u-row>
<u-col span="3" style="text-align: center">
<u-row>
<u-col span="6" style="text-align: center">
<u-checkbox
v-model="sample.checked"
v-if="sample.sampleProcessNo === currentNode"
@change="selectSample(sample)"
></u-checkbox>
</u-col>
<u-col span="6" style="text-align: center">
<text>{{ sample.sort }}</text>
</u-col>
</u-row>
</u-col>
<u-col span="9" class="sample_desc">
<view>
<view
><text style="padding-left: 10px">{{ sample.sampleCode }}</text></view
>
<view>
<text style="padding-left: 10px">
{{ getDataSourceTypeShow(sample.dataSourceType) }}{{ sample.sampleName }}
</text>
</view>
</view>
<view class="sample_desc_warn" v-if="sample.sampleProcessNo !== currentNode">
当前节点{{ getProcessNameShow(sample.sampleProcessNo) }}
</view>
</u-col>
</u-row>
<u-line style="padding: 5px" color="#bbb" />
</view>
</block>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="8"></u-col>
<u-col span="4">
<u-button class="btn-operation" :disabled="taskList.length <= 0" type="success" @click="confirmReceipt">
确认收样
</u-button>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import nx from '@/nx'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
// 响应式数据
const currentNode = ref('F30')
const scrollTop = ref(0)
const current = ref(0)
const taskList = ref([])
const sampleList = ref([])
const dicSampleProcessCodeList = ref([])
// 计算属性
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const customBack = () => {
uni.reLaunch({ url: '/pages/analysis/index/index' })
}
const selectSample = sample => {
sample.checked = !sample.checked
}
const taskStyle = task => {
if ((task.weightTaskStatus === 0 || task.weightTaskStatus === 1) && task.reviewCount > 0) {
return 'red'
}
if (task.weightTaskStatus === 2 && task.reviewCount > 0) {
return 'green'
}
return ''
}
const confirmReceipt = () => {
const errNodeList = sampleList.value.filter(item => item.sampleProcessNo !== currentNode.value)
if (errNodeList.length > 0) {
uni.showToast({
title: '存在异常节点,联系管理员处理!',
icon: 'none',
duration: 3000
})
return
}
const checkedSampleList = sampleList.value.filter(item => item.checked)
if (checkedSampleList.length !== sampleList.value.length) {
uni.showToast({ title: '样品未全部勾选,请检查!', icon: 'none' })
return
}
uni.showModal({
title: '提示',
content: '确认收样?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.cancel) return
const sampleIdList = checkedSampleList.map(item => item.busSubCsampleId)
const data = {
busAssayTaskId: taskList.value[current.value]?.id,
sampleSourceType: 2,
sampleProcessNo: currentNode.value,
isGenSampleHandover: false,
sampleIdList
}
nx.$api.assayTask
.execReceiveSample(data)
.then(() => {
getAssayTask()
})
.catch(err => {
console.error('收样失败:', err)
})
}
})
}
const switchTask = index => {
if (index === current.value) return
current.value = index
const task = taskList.value[index]
if (!task) return
getAssayTaskDetail(task.taskNo)
}
const getAssayTask = () => {
const param = {
finishStatus: 'waiting_receive',
assayOper: userInfo.value.nickname
}
nx.$api.auncel
.getAssayTaskList(param)
.then(res => {
taskList.value = res || []
if (taskList.value.length > 0) {
current.value = 0
getAssayTaskDetail(taskList.value[0].taskNo)
} else {
sampleList.value = []
}
})
.catch(err => {
console.error('获取任务列表失败:', err)
sampleList.value = []
})
}
const getAssayTaskDetail = taskNo => {
const param = {
taskNo,
waiting_receive: '1'
}
nx.$api.assayTask
.getAssayTaskDetailListByTaskNo(param)
.then(res => {
sampleList.value = (res.result || []).map(item => ({ ...item, checked: false }))
})
.catch(err => {
console.error('获取任务详情失败:', err)
sampleList.value = []
})
}
const getDicSampleProcessCodeList = () => {
nx.$api.assayTask.queryQmsDicSampleProcessCodeList().then(res => {
dicSampleProcessCodeList.value = res.records || []
})
}
const getProcessNameShow = val => {
const item = dicSampleProcessCodeList.value.find(i => i.processCode === val)
return item ? item.processName : val
}
const getDataSourceTypeShow = val => {
if (val === 2) return '【筛上】'
if (val === 3) return '【筛下】'
return ''
}
// 生命周期
onLoad(() => {
getDicSampleProcessCodeList()
getAssayTask()
})
onBackPress(() => {
customBack()
return true
})
</script>
<style lang="scss" scoped>
.content-title {
height: 50px;
width: 100%;
font-size: 20px;
font-weight: 300;
}
.content-title-name {
padding: 10px;
text-align: center;
}
.content-main-height {
height: calc(100vh - 125px);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 5px;
height: 100px;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #444;
line-height: 1;
border-bottom: 2px dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-weight: 600;
background: #fff;
}
.content-main-right {
height: calc(100vh - 205px);
}
.content-main-right-operation {
height: 80px;
padding-top: 15px;
padding-right: 15px;
}
.btn-operation {
height: 50px;
font-size: 18px;
}
.sample_desc {
width: 100%;
display: flex;
flex-wrap: nowrap;
justify-content: space-between !important;
padding-right: 15px !important;
}
.sample_desc_warn {
color: red;
}
</style>

View File

@@ -0,0 +1,299 @@
<template>
<view>
<navbar-back :autoBack="false" title="审核查询" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current.value === index ? 'u-tab-item-active' : '']"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 18px">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 10px">
<text>{{ task.taskName }} {{ task.assayOper }}</text>
</view>
<view class="x-f" style="margin-top: 10px">
<u-icon color="" name="clock"></u-icon>
<text style="margin-left: 5px">{{ task.reportTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view style="padding: 5px; font-size: 16px">
<u-row
@click="showSampleDetail(sample.id, index)"
:class="selectedIndex.value === index ? 'selected_Sample' : ''"
>
<u-col span="2" style="text-align: center" :style="sampleStyle(sample)">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="5">
<view>
<text style="padding-left: 10px">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 10px">
{{ getDataSourceTypeShow(sample.dataSourceType) }}{{ sample.sampleName }}
</text>
</view>
<view v-if="sample.sampleWeight">
<text style="padding-left: 10px">{{ sample.sampleWeight }} g</text>
</view>
</u-col>
<u-col span="5">
<view>
<text style="padding-left: 10px">{{ sample.remarks }}</text>
</view>
</u-col>
</u-row>
<u-line style="padding: 5px" color="#bbb" />
</view>
</block>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="3"></u-col>
<u-col span="6">
<u-button class="btn-operation" type="primary" @click="previewPDF" v-if="currentTaskId.value">
任务单预览
</u-button>
</u-col>
<u-col span="3"> </u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
<sample-detail-popup
ref="sampleDetailPopup"
:showPopup="showDetailPopup"
:detailPopupParam="detailPopupParam"
></sample-detail-popup>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import SampleDetailPopup from '@/components/sample/sample-detail-popup.vue'
import nx from '@/nx'
// 响应式数据
const scrollTop = ref(0)
const current = ref(0)
const currentTask = ref({})
const currentTaskId = ref('')
const currentTaskNo = ref('')
const conAssayTask = ref('')
const reviewNum = ref(0)
const selectedIndex = ref(-1)
const taskList = ref([])
const sampleList = ref([])
const showDetailPopup = ref(false)
const detailPopupParam = ref({ taskDetailId: '' })
// 计算属性
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const customBack = () => {
uni.reLaunch({ url: '/pages/analysis/index/index' })
}
const taskStyle = task => {
if ((task.weightTaskStatus === 0 || task.weightTaskStatus === 1) && task.reviewCount > 0) return 'red'
if (task.weightTaskStatus === 2 && task.reviewCount > 0) return 'green'
return ''
}
const sampleStyle = sample => {
if ((sample.weightSubmitStatus === 0 || sample.weightSubmitStatus === 1) && sample.reviewCount > 0) {
return 'color: red'
}
if (sample.weightSubmitStatus === 2 && sample.reviewCount > 0) {
return 'color: green'
}
return ''
}
const swichTask = async index => {
if (index === current.value) return
current.value = index
selectedIndex.value = -1
const task = taskList.value[index]
currentTask.value = task
currentTaskNo.value = task.taskNo
currentTaskId.value = task.id
getAssayTaskDetail(task.taskNo)
}
const showSampleDetail = (detailId, index) => {
selectedIndex.value = index
console.log('detailId', detailId)
detailPopupParam.value = { taskDetailId: detailId }
showDetailPopup.value = true
}
const getAssayTask = () => {
taskList.value = []
sampleList.value = []
const param = {
finishStatus: 'finished',
wfStatus: 'running',
assayOper: userInfo.value.realname
}
nx.$api.auncel.getAssayTaskList(param).then(res => {
taskList.value = res
if (taskList.value.length > 0) {
current.value = 0
currentTask.value = taskList.value[0]
currentTaskNo.value = taskList.value[0].taskNo
currentTaskId.value = taskList.value[0].id
getAssayTaskDetail(currentTaskNo.value)
} else {
current.value = 0
currentTask.value = {}
currentTaskNo.value = ''
currentTaskId.value = ''
}
})
}
const getAssayTaskDetail = taskNo => {
reviewNum.value = 0
nx.$api.assayTaskTemplateKey
.getAssayTaskDetailListByTaskNo({ taskNo })
.then(res => {
sampleList.value = res.result || []
if (res.additionalProperties?.conAssayTask) {
conAssayTask.value = res.additionalProperties.conAssayTask
}
})
.catch(err => {
console.error(err)
})
}
const previewPDF = () => {
const url = `/pages/analysis/sample/pdf-preview?taskId=${currentTaskId.value}&reportKey=${conAssayTask.value.assayTaskTemplateKey}`
uni.navigateTo({ url })
}
const getDataSourceTypeShow = val => {
if (val === 2) return '【筛上】'
if (val === 3) return '【筛下】'
return ''
}
// 生命周期
onMounted(() => {
getAssayTask()
uni.$on('sample-detail-popup_close', () => {
showDetailPopup.value = false
})
})
onBackPress(() => {
customBack()
return true
})
</script>
<style lang="scss" scoped>
/* 样式部分保持不变 */
.content-title {
height: 50px;
width: 100%;
font-size: 20px;
font-weight: 300;
}
.content-title-name {
padding: 10px;
text-align: center;
}
.content-main-height {
height: calc(100vh - 125px);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 5px;
height: 100px;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #444;
line-height: 1;
border-width: 2px;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 16px;
left: 0;
top: 20px;
}
.content-main-right {
height: calc(100vh - 205px);
}
.content-main-right-operation {
height: 80px;
padding-top: 15px;
padding-right: 15px;
}
.btn-operation {
width: 95%;
}
.selected_Sample {
background-color: #d7e9fa;
}
</style>

View File

@@ -0,0 +1,413 @@
<template>
<view>
<navbar-back :autoBack="false" title="数据上报" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="current === index ? 'u-tab-item-active' : ''"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 18px">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 10px">
<text>{{ task.taskName }} {{ task.assayOper }}</text>
</view>
<view class="x-f" style="margin-top: 10px">
<u-icon color="" name="clock"></u-icon>
<text style="margin-left: 5px">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view style="padding: 5px; font-size: 16px">
<u-row
@click="showSampleDetail(sample.id, index)"
:class="selectedIndex === index ? 'selected_Sample' : ''"
>
<u-col span="2" style="text-align: center" :style="sampleStyle(sample)">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="5">
<view>
<text style="padding-left: 10px">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 10px"
>{{ getDataSourceTypeShow(sample.dataSourceType) }}{{ sample.sampleName }}</text
>
</view>
</u-col>
<u-col span="5">
<view class="sample_desc_warn" v-if="sample.sampleProcessNo !== currentNode">
当前节点{{ getProcessNameShow(sample.sampleProcessNo) }}
</view>
</u-col>
</u-row>
<u-line style="padding: 5px" color="#bbb" />
</view>
</block>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="3"></u-col>
<u-col span="3">
<u-button class="btn-operation" type="primary" @click="previewPDF" v-if="currentTaskId">
任务单预览
</u-button>
</u-col>
<u-col span="3">
<u-button
class="btn-operation"
v-if="currentTaskId"
type="warning"
:disabled="taskReviewDisabled"
@click="taskReview"
>
撤回任务单
</u-button>
</u-col>
<u-col span="3">
<u-button class="btn-operation" :disabled="dataReportDisabled" type="success" @click="dataReport">
数据上报
</u-button>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
<!-- 样品详情 -->
<sample-detail-popup
ref="sampleDetailPopup"
:showPopup="showDetailPopup"
:detailPopupParam="detailPopupParam"
></sample-detail-popup>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import SampleDetailPopup from '@/components/sample/sample-detail-popup.vue'
import nx from '@/nx'
// 响应式数据
const scrollTop = ref(0)
const current = ref(0)
const currentTask = ref({})
const currentTaskId = ref('')
const currentTaskNo = ref('')
const conAssayTask = ref('')
const reviewNum = ref(0)
const selectedIndex = ref(-1)
const taskList = ref([])
const sampleList = ref([])
const showDetailPopup = ref(false)
const detailPopupParam = ref({ taskDetailId: '' })
const currentNode = ref('F31')
const dicSampleProcessCodeList = ref([])
// 计算属性
const taskReviewDisabled = computed(() => currentTask.value.finishStatus !== 'finished')
const dataReportDisabled = computed(() => currentTask.value.finishStatus !== 'finished')
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const customBack = () => {
uni.reLaunch({ url: '/pages/analysis/index/index' })
}
const taskStyle = task => {
if ((task.weightTaskStatus === 0 || task.weightTaskStatus === 1) && task.reviewCount > 0) return 'red'
if (task.weightTaskStatus === 2 && task.reviewCount > 0) return 'green'
return ''
}
const sampleStyle = sample => {
if ((sample.weightSubmitStatus === 0 || sample.weightSubmitStatus === 1) && sample.reviewCount > 0) {
return 'color: red'
}
if (sample.weightSubmitStatus === 2 && sample.reviewCount > 0) {
return 'color: green'
}
return ''
}
const swichTask = async index => {
if (index === current.value) return
current.value = index
const task = taskList.value[index]
currentTask.value = task
currentTaskNo.value = task.taskNo
currentTaskId.value = task.id
getAssayTaskDetail(task.taskNo)
}
const showSampleDetail = (detailId, index) => {
selectedIndex.value = index
detailPopupParam.value = { taskDetailId: detailId }
showDetailPopup.value = true
}
const getAssayTask = () => {
taskList.value = []
sampleList.value = []
const param = {
finishStatus: 'finished',
wfStatus: '0,revoke',
assayOper: userInfo.value.nickname
}
nx.$api.auncel
.getAssayTaskList(param)
.then(res => {
taskList.value = res
if (taskList.value.length > 0) {
current.value = 0
currentTask.value = taskList.value[0]
currentTaskNo.value = taskList.value[0].taskNo
currentTaskId.value = taskList.value[0].id
getAssayTaskDetail(currentTaskNo.value)
} else {
current.value = 0
currentTask.value = {}
currentTaskNo.value = ''
currentTaskId.value = ''
}
})
.catch(err => {
console.error(err)
})
}
const getAssayTaskDetail = taskNo => {
reviewNum.value = 0
$u.api
.getAssayTaskDetailListByTaskNo({ taskNo })
.then(res => {
sampleList.value = res.result || []
if (res.additionalProperties?.conAssayTask) {
conAssayTask.value = res.additionalProperties.conAssayTask
}
})
.catch(err => {
console.error(err)
})
}
const previewPDF = () => {
const url = `/pages/analysis/sample/pdf-preview?taskId=${currentTaskId.value}&reportKey=${conAssayTask.value.assayTaskTemplateKey}&hideResultFlag=true`
uni.navigateTo({ url })
}
const taskReview = () => {
const errNodeList = sampleList.value.filter(item => item.sampleProcessNo !== currentNode.value)
if (errNodeList.length > 0) {
uni.showToast({ title: '存在异常节点,联系管理员处理!', icon: 'none', duration: 3000 })
return
}
uni.showModal({
title: '提示',
content: '撤回当前任务指派单,是否继续?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.confirm) {
nx.$api.assayTask.rollbackAssayTask(currentTaskId.value).then(() => {
currentTaskId.value = ''
currentTask.value = {}
uni.showToast({ title: '撤回成功!' })
uni.navigateTo({ url: '/pages/analysis/sample/sample-work-list' })
})
}
}
})
}
const dataReport = () => {
const errNodeList = sampleList.value.filter(item => item.sampleProcessNo !== currentNode.value)
if (errNodeList.length > 0) {
uni.showToast({ title: '存在异常节点,联系管理员处理!', icon: 'none', duration: 3000 })
return
}
uni.showModal({
title: '提示',
content: '确定上报数据?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.confirm) {
nx.$api.assayTask.reportAssayTask(currentTaskId.value).then(res => {
currentTaskId.value = ''
currentTask.value = {}
if (res.additionalProperties?.conAssayTask?.isPrint === 1) {
printTask()
}
getAssayTask()
})
}
}
})
}
const getDicSampleProcessCodeList = () => {
nx.$api.assayTask.queryQmsDicSampleProcessCodeList().then(res => {
dicSampleProcessCodeList.value = res.records
})
}
const getProcessNameShow = val => {
const item = dicSampleProcessCodeList.value.find(i => i.processCode === val)
return item ? item.processName : val
}
const getDataSourceTypeShow = val => {
if (val === 2) return '【筛上】'
if (val === 3) return '【筛下】'
return ''
}
const printTask = () => {
uni.showModal({
title: '提示',
content: '数据上报成功!是否打印“原始记录单”?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.confirm) {
nx.$print.getPrintTemplateAndPrint(currentTask.value)
}
}
})
}
// 生命周期
onMounted(() => {
getDicSampleProcessCodeList()
getAssayTask()
// 监听 popup 关闭
uni.$on('sample-detail-popup_close', () => {
showDetailPopup.value = false
})
})
onBackPress(() => {
customBack()
return true
})
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.content-title {
height: 50px;
width: 100%;
font-size: 20px;
font-weight: 300;
}
.content-title-name {
padding: 10px;
text-align: center;
}
.content-main-height {
height: calc(100vh - 125px);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 5px;
height: 100px;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #444;
line-height: 1;
border-width: 2px;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 16px;
left: 0;
top: 20px;
}
.content-main-right {
height: calc(100vh - 205px);
}
.content-main-right-operation {
height: 80px;
padding-top: 15px;
padding-right: 15px;
}
.btn-operation {
width: 95%;
}
.selected_Sample {
background-color: #d7e9fa;
}
.sample_desc_warn {
color: red;
padding-right: 10px;
}
</style>

View File

@@ -0,0 +1,423 @@
<template>
<view>
<navbar-back :autoBack="false" title="样重复核" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current == index ? 'u-tab-item-active' : '']"
:data-current="index"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 42rpx">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 20rpx">
<text>{{ task.taskName }}</text>
</view>
<view class="x-f" style="margin-top: 20rpx">
<u-icon name="clock"></u-icon>
<text style="margin-left: 10rpx">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<u-checkbox-group @change="checkboxGroupChange" style="width: 100%">
<block v-for="(sample, index) in sampleList" :key="index">
<view style="width: 100%; padding: 10rpx; font-size: 36rpx">
<u-row>
<u-col span="2" style="text-align: center">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="4">
<view>
<text style="padding-left: 20rpx">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 20rpx"
>{{ sample.dataSourceType | getDataSourceTypeShow }}{{ sample.sampleName }}</text
>
</view>
<view>
<text v-if="sample.sampleWeight" style="padding-left: 20rpx">{{ sample.sampleWeight }} g</text>
</view>
</u-col>
<u-col span="4">
<view>
<text style="padding-left: 20rpx">{{ sample.cupNum }}号杯</text>
</view>
<view>
<text style="padding-left: 20rpx">{{ sample.remarks }}</text>
</view>
<view>
<text style="padding-left: 20rpx">{{ sample.measureTime | getWeightTimeShow }}</text>
</view>
</u-col>
<u-col span="2">
<view>
<u-checkbox
@change="checkboxChange"
v-model="sample.checked"
:label="sample.id"
:name="sample.id"
>
</u-checkbox>
<!-- <block v-if="sample.reviewCount > 0 && sample.weightSubmitStatus == 0">
<u-button
type="warning"
@click="recovery(sample.id)"
>恢复</u-button>
</block>
<block v-else>
<u-button
type="default"
@click="review(sample.id)"
>复核</u-button>
</block> -->
</view>
</u-col>
</u-row>
<u-line style="padding: 10rpx" color="#bbb" />
</view>
</block>
</u-checkbox-group>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="4"><!-- <u-button class="btn-operation" type="primary">原始记录单打印</u-button> --></u-col>
<u-col span="4"
><!-- <u-button class="btn-operation" :disabled="reviewNum > 0" type="success" >数据上报</u-button> --></u-col
>
<u-col span="4">
<!-- <u-button class="btn-operation" :disabled="reviewNum == 0" type="warning" @click="taskReview">任务单退回复核</u-button> -->
<u-button class="btn-operation" :disabled="reviewList.length <= 0" type="warning" @click="taskReviewAll"
>任务单退回复核</u-button
>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
</view>
</template>
<script>
export default {
data() {
return {
scrollTop: 0, //tab标题的滚动条位置
current: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
currentTaskId: '', //当前选择的任务单id
currentTaskNo: '', //当前选中的任务编号
reviewNum: 0, //复核数
reviewList: [],
taskList: [],
sampleList: []
}
},
onLoad(param) {
if (param.currentTaskNo) {
this.currentTaskNo = param.currentTaskNo
}
//获取任务列表
this.getAssayTask()
},
methods: {
//返回上一页
customBack() {
uni.redirectTo({
url: '/pages/analysis/sample/sample-report'
})
},
//开始秤样
startWeighSample() {
uni.navigateTo({
url: '/pages/auncel/auncel-weigh'
})
},
//切换任务
async swichTask(index) {
if (index == this.current) return
this.current = index
// 如果为0意味着尚未初始化
if (this.menuHeight == 0 || this.menuItemHeight == 0) {
await this.getElRect('menu-scroll-view', 'menuHeight')
await this.getElRect('u-tab-item', 'menuItemHeight')
}
// 将菜单菜单活动item垂直居中
this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2
//获取任务详情
this.currentTaskNo = this.taskList[index].taskNo
this.currentTaskId = this.taskList[index].id
this.getAssayTaskDetail(this.currentTaskNo)
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this)
query
.select('.' + elClass)
.fields({ size: true }, res => {
// 如果节点尚未生成res值为null循环调用执行
if (!res) {
setTimeout(() => {
this.getElRect(elClass)
}, 10)
return
}
this[dataVal] = res.height
})
.exec()
})
},
getAssayTask() {
//显示loading
uni.showLoading({
title: '加载中...'
})
// this.$u.api
// .getAssayTaskListBy({taskStatus:2})
const param = {
operateType: 'review',
taskNo: this.currentTaskNo
}
this.$u.api
.getAssayTaskList(param)
.then(res => {
this.taskList = res.result
this.currentTaskNo = this.taskList[0].taskNo
this.currentTaskId = this.taskList[0].id
this.getAssayTaskDetail(this.currentTaskNo)
uni.hideLoading()
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
},
getAssayTaskDetail(taskNo) {
this.reviewNum = 0
this.$u.api
.getAssayTaskDetailListByTaskNo({ taskNo: taskNo })
.then(res => {
this.sampleList = res.result
this.showLoading = false
//统计需要复核的数量
this.sampleList.forEach(item => {
if (item.reviewCount > 0 && item.weightSubmitStatus == 0) {
item.checked = true
this.reviewNum += 1
}
})
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
review(id) {
this.$u.api
.assayTaskDetailReview(id)
.then(res => {
this.getAssayTaskDetail(this.currentTaskNo)
this.showLoading = false
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
recovery(id) {
this.$u.api
.assayTaskDetailRecovery(id)
.then(res => {
this.getAssayTaskDetail(this.currentTaskNo)
this.showLoading = false
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
//任务单退回复核
taskReview() {
this.$u.api
.assayTaskReview(this.currentTaskId)
.then(res => {
this.showLoading = false
uni.redirectTo({
url: '/pages/sample/sample-report'
})
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
//任务单及样品退回复核
taskReviewAll() {
uni.showModal({
title: '提示',
content: '确定复核当前选中的样品?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.cancel) {
console.log('用户点击取消')
return
}
console.log('确定。。。')
//显示loading
uni.showLoading({
title: '退回中...'
})
this.$u.api
.reviewWeightTaskByTaskIdAndSampleIds(this.currentTaskId, this.reviewList.join(','))
.then(res => {
uni.hideLoading()
uni.redirectTo({
url: '/pages/sample/sample-report'
})
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
}
})
},
checkboxGroupChange(v) {
this.reviewList = v
this.reviewNum = v.length
//console.log('group change', v);
},
checkboxChange(e) {
/* if(e.value) {
this.review(e.name);
} else {
this.recovery(e.name);
} */
}
},
filters: {
getWeightTimeShow(time) {
if (time == null) return ''
return time.split(' ')[1]
},
getDataSourceTypeShow(val) {
if (val == 2) return '【筛上】'
if (val == 3) return '【筛下】'
return ''
}
}
}
</script>
<style lang="scss" scoped>
.content-title {
height: 110rpx;
width: 100%;
font-size: 46rpx;
font-weight: 300;
}
.content-title-name {
padding: 20rpx;
text-align: center;
}
.content-main-height {
height: calc(100vh - 250rpx);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 10rpx;
height: 200rpx;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #444;
font-weight: 400;
line-height: 1;
border-width: 4rpx;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-size: 36rpx;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 32rpx;
left: 0;
top: 39rpx;
}
.content-main-right {
height: calc(100vh - 410rpx);
}
.content-main-right-operation {
height: 160rpx;
padding-top: 30rpx;
}
.btn-operation {
height: 100rpx;
font-size: 36rpx;
}
</style>

View File

@@ -0,0 +1,425 @@
<!--送样-->
<template>
<view class="page">
<navbar-back :autoBack="false" title="样品分析-送样" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current == index ? 'u-tab-item-active' : '']"
:data-current="index"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 42rpx">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 20rpx">
<text>{{ task.taskName }}</text>
</view>
<view class="x-f" style="margin-top: 20rpx">
<u-icon color="" name="clock"></u-icon>
<text style="margin-left: 10rpx">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view style="padding: 10rpx; font-size: 36rpx">
<u-row>
<u-col span="3" style="text-align: center">
<view>
<text>{{ sample.sort }}</text>
</view>
<!-- <u-row>-->
<!-- <u-col span="6" style="text-align: center;">-->
<!-- <u-checkbox v-model="sample.checked" v-if="sample.sampleProcessNo == 'F39'" @change="selectSample(sample)"></u-checkbox>-->
<!-- </u-col>-->
<!-- <u-col span="6" style="text-align: center;">-->
<!-- -->
<!-- </u-col>-->
<!-- </u-row>-->
</u-col>
<u-col span="9" class="sample_desc">
<view>
<view>
<text style="padding-left: 20rpx">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 20rpx"
>{{ sample.dataSourceType | getDataSourceTypeShow }}{{ sample.sampleName }}</text
>
</view>
</view>
<view class="sample_desc_warn" v-if="sample.sampleProcessNo != 'F39'">
状态异常{{ sample.sampleProcessNo }}
</view>
</u-col>
</u-row>
<u-line style="padding: 10rpx" color="#bbb" />
</view>
</block>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="4"></u-col>
<u-col span="4"></u-col>
<u-col span="4">
<u-button
class="btn-operation"
:disabled="this.taskList.length <= 0"
type="success"
@click="confirmReceipt"
>确认送样</u-button
>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
</view>
</template>
<script>
export default {
data() {
return {
scrollTop: 0, //tab标题的滚动条位置
current: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
currentTask: '', //当前选中任务
currentTaskNo: '', //当前选中的任务编号
currentTaskType: '', //当前任务类型
taskList: [],
sampleList: []
}
},
onLoad() {
//获取任务列表
this.getAssayTask()
},
methods: {
//返回首页
customBack() {
uni.reLaunch({
url: '/pages/analysis/index/index'
})
},
selectSample(sample) {
if (sample.checked) sample.checked = false
else sample.checked = true
},
taskStyle(task) {
if (task.weightTaskStatus == 0 && task.reviewCount > 0) {
return 'red'
}
if (task.weightTaskStatus == 1 && task.reviewCount > 0) {
return 'red'
}
if (task.weightTaskStatus == 2 && task.reviewCount > 0) {
return 'green'
}
return ''
},
sampleStyle(sample) {
//console.log(sample);
if (sample.weightSubmitStatus == 0 && sample.reviewCount > 0) {
return 'color: red'
}
if (sample.weightSubmitStatus == 1 && sample.reviewCount > 0) {
//return 'color: #e0861a';
return 'color: green'
}
if (sample.weightSubmitStatus == 2 && sample.reviewCount > 0) {
return 'color: green'
}
return ''
},
//确认送样
confirmReceipt() {
//检查是否选中所有
// let checkedSampleList = this.sampleList.filter(item => item.checked);
// if (checkedSampleList.length != this.sampleList.length) {
// this.$helper.showToast({
// title: '样品未全部勾选,请检查!'
// });
// return;
// }
//检查是否所有样品都在F39
const checkList = this.sampleList.filter(item => item.sampleProcessNo != 'F39')
if (checkList.length > 0) {
this.$helper.showToast({
title: '部分样品状态异常,请联系技术支持人员处理!'
})
return
}
const checkedSampleList = this.sampleList
uni.showModal({
title: '提示',
content: '确认送样?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.cancel) {
return
}
const sampleIdList = checkedSampleList.map(item => item.busSubCsampleId)
const data = {
busAssayTaskId: this.currentTask.id,
sampleSourceType: 2,
sampleProcessNo: 'F39',
isGenSampleHandover: false,
sampleIdList: sampleIdList
}
uni.showLoading({
title: '正在提交...'
})
this.$u.api
.execSendSample(data)
.then(res => {
let msg = '发生错误,请稍后再试!'
if (!res.success) {
if (res.message) {
msg = res.message
}
this.$helper.showToast({
title: msg
})
return
}
this.$helper.showToast({
title: '操作成功!'
})
this.getAssayTask()
})
.catch(err => {
this.$helper.showToast({
title: '提交失败!'
})
console.log(err)
})
.finally(() => {
uni.hideLoading()
})
}
})
},
//切换任务
async swichTask(index) {
if (index == this.current) return
this.current = index
// 如果为0意味着尚未初始化
// if (this.menuHeight == 0 || this.menuItemHeight == 0) {
// await this.getElRect('menu-scroll-view', 'menuHeight');
// await this.getElRect('u-tab-item', 'menuItemHeight');
// }
// // 将菜单菜单活动item垂直居中
// this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2;
//获取任务详情
this.currentTask = this.taskList[index]
this.currentTaskNo = this.taskList[index].taskNo
this.currentTaskType = this.taskList[index].taskType
this.getAssayTaskDetail(this.currentTaskNo)
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this)
query
.select('.' + elClass)
.fields({ size: true }, res => {
// 如果节点尚未生成res值为null循环调用执行
if (!res) {
setTimeout(() => {
this.getElRect(elClass)
}, 10)
return
}
this[dataVal] = res.height
})
.exec()
})
},
getAssayTask() {
//显示loading
uni.showLoading({
title: '加载中...'
})
const param = {
operateType: 'send',
assayOper: this.userInfo.realname
}
this.$u.api
.getAssayTaskList(param)
.then(res => {
this.taskList = res.result
if (!this.taskList || this.taskList.length == 0) {
this.sampleList = []
return
}
if (this.taskList && this.taskList.length > 0) {
this.current = 0
this.currentTask = this.taskList[0]
this.currentTaskNo = this.taskList[0].taskNo
this.currentTaskType = this.taskList[0].taskType
this.getAssayTaskDetail(this.currentTaskNo)
}
})
.catch(err => {
console.log(err)
})
.finally(() => {
uni.hideLoading()
})
},
getAssayTaskDetail(taskNo) {
uni.showLoading({
title: '加载中...'
})
const param = {
taskNo: taskNo,
waiting_receive: '1' //标记是收样
}
this.$u.api
.getAssayTaskDetailListByTaskNo(param)
.then(res => {
const list = res.result
list.forEach(item => {
item.checked = false
})
this.sampleList = list
this.showLoading = false
})
.catch(err => {
this.showLoading = false
console.log(err)
})
.finally(() => {
uni.hideLoading()
})
}
},
filters: {
getDataSourceTypeShow(val) {
if (val == 2) return '【筛上】'
if (val == 3) return '【筛下】'
return ''
}
}
}
</script>
<style lang="scss" scoped>
.content-title {
height: 110rpx;
width: 100%;
font-size: 46rpx;
font-weight: 300;
}
.content-title-name {
padding: 20rpx;
text-align: center;
}
.content-main-height {
height: calc(100vh - 250rpx);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 10rpx;
height: 200rpx;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #444;
font-weight: 400;
line-height: 1;
border-width: 4rpx;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-size: 36rpx;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 32rpx;
left: 0;
top: 39rpx;
}
.content-main-right {
height: calc(100vh - 410rpx);
}
.content-main-right-operation {
height: 160rpx;
padding-top: 30rpx;
}
.btn-operation {
height: 100rpx;
font-size: 36rpx;
}
.sample_desc {
width: 100%;
display: flex;
flex-wrap: nowrap; /* 禁止换行 */
justify-content: space-between !important;
padding-right: 15px !important;
}
.sample_desc_warn {
color: red;
}
</style>

View File

@@ -0,0 +1,319 @@
<template>
<view class="page">
<navbar-back :autoBack="false" title="样品称重" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current == index ? 'u-tab-item-active' : '']"
:data-current="index"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view>
<text style="font-size: 42rpx">{{ task.taskNo }}</text>
</view>
<view style="margin-top: 20rpx">
<text>{{ task.taskName }}</text>
</view>
<view class="x-f" style="margin-top: 20rpx">
<u-icon name="clock"></u-icon>
<text style="margin-left: 10rpx">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view v-if="currentTask.reviewCount == sample.reviewCount" style="padding: 10rpx; font-size: 36rpx">
<u-row>
<u-col span="2" style="text-align: center">
<view>
<text>{{ sample.sort }}</text>
</view>
</u-col>
<u-col span="10">
<view>
<text style="padding-left: 20rpx">{{ sample.sampleCode }}</text>
</view>
<view>
<text style="padding-left: 20rpx"
>{{ sample.dataSourceType | getDataSourceTypeShow }}{{ sample.sampleName }}</text
>
</view>
<block v-if="sample.sampleWeight && sample.weightSubmitStatus != 0">
<view>
<text v-if="sample.sampleWeight" style="padding-left: 20rpx">{{ sample.sampleWeight }} g</text>
</view>
</block>
</u-col>
</u-row>
<u-line style="padding: 10rpx" color="#bbb" />
</view>
</block>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="4"></u-col>
<u-col span="4"></u-col>
<u-col span="4">
<u-button
class="btn-operation"
:disabled="this.taskList.length <= 0"
type="success"
@click="startWeighSample"
>开始秤样</u-button
>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
</view>
</template>
<script>
export default {
data() {
return {
scrollTop: 0, //tab标题的滚动条位置
current: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
currentTask: '', //当前选中任务
currentTaskNo: '', //当前选中的任务编号
currentTaskType: '', //当前任务类型
taskList: [],
sampleList: []
}
},
onLoad() {
//获取任务列表
this.getAssayTask()
},
methods: {
//返回首页
customBack() {
uni.reLaunch({
url: '/pages/analysis/index/index'
})
},
taskStyle(task) {
if (task.weightTaskStatus == 0 && task.reviewCount > 0) {
return 'red'
}
if (task.weightTaskStatus == 1 && task.reviewCount > 0) {
return 'red'
}
if (task.weightTaskStatus == 2 && task.reviewCount > 0) {
return 'green'
}
return ''
},
sampleStyle(sample) {
//console.log(sample);
if (sample.weightSubmitStatus == 0 && sample.reviewCount > 0) {
return 'color: red'
}
if (sample.weightSubmitStatus == 1 && sample.reviewCount > 0) {
//return 'color: #e0861a';
return 'color: green'
}
if (sample.weightSubmitStatus == 2 && sample.reviewCount > 0) {
return 'color: green'
}
return ''
},
//开始秤样
startWeighSample() {
uni.navigateTo({
url:
'/pages/auncel/auncel-weigh?currentTaskNo=' + this.currentTaskNo + '&currentTaskType=' + this.currentTaskType
})
},
//切换任务
async swichTask(index) {
if (index == this.current) return
this.current = index
// 如果为0意味着尚未初始化
if (this.menuHeight == 0 || this.menuItemHeight == 0) {
await this.getElRect('menu-scroll-view', 'menuHeight')
await this.getElRect('u-tab-item', 'menuItemHeight')
}
// 将菜单菜单活动item垂直居中
this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2
//获取任务详情
this.currentTask = this.taskList[index]
this.currentTaskNo = this.taskList[index].taskNo
this.currentTaskType = this.taskList[index].taskType
this.getAssayTaskDetail(this.currentTaskNo)
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this)
query
.select('.' + elClass)
.fields({ size: true }, res => {
// 如果节点尚未生成res值为null循环调用执行
if (!res) {
setTimeout(() => {
this.getElRect(elClass)
}, 10)
return
}
this[dataVal] = res.height
})
.exec()
})
},
getAssayTask() {
//显示loading
uni.showLoading({
title: '加载中...'
})
const param = {
operateType: 'weight',
assayOper: this.userInfo.realname
}
this.$u.api
.getAssayTaskList(param)
.then(res => {
this.taskList = res.result
if (this.taskList && this.taskList.length > 0) {
this.currentTask = this.taskList[0]
this.currentTaskNo = this.taskList[0].taskNo
this.currentTaskType = this.taskList[0].taskType
this.getAssayTaskDetail(this.currentTaskNo)
}
uni.hideLoading()
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
},
getAssayTaskDetail(taskNo) {
this.$u.api
.getAssayTaskDetailListByTaskNo({ taskNo: taskNo })
.then(res => {
this.sampleList = res.result
this.showLoading = false
})
.catch(err => {
this.showLoading = false
console.log(err)
})
}
},
filters: {
getDataSourceTypeShow(val) {
if (val == 2) return '【筛上】'
if (val == 3) return '【筛下】'
return ''
}
}
}
</script>
<style lang="scss" scoped>
.content-title {
height: 110rpx;
width: 100%;
font-size: 46rpx;
font-weight: 300;
}
.content-title-name {
padding: 20rpx;
text-align: center;
}
.content-main-height {
height: calc(100vh - 250rpx);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 10rpx;
height: 200rpx;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #444;
font-weight: 400;
line-height: 1;
border-width: 4rpx;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-size: 36rpx;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 32rpx;
left: 0;
top: 39rpx;
}
.content-main-right {
height: calc(100vh - 410rpx);
}
.content-main-right-operation {
height: 160rpx;
padding-top: 30rpx;
}
.btn-operation {
height: 100rpx;
font-size: 36rpx;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,746 @@
<template>
<view>
<navbar-back titleWidth="800" :title="title"></navbar-back>
<u-row>
<u-col span="1"></u-col>
<u-col span="10">
<scroll-view scroll-y scroll-with-animation :scroll-top="scrollTop">
<u-form :model="taskInstance" ref="uForm" label-width="210">
<view class="form-item-my" v-for="(field, index) in formFields" :key="'field_' + index">
<view
class="label-my"
:style="{ fontWeight: checkFeadToDetailField(field.headToDetailField) ? 'bold' : 'normal' }"
>{{ field.label }}</view
>
<view class="content-my">
<view v-if="field.type == 'title' && field.display" class="content-title">
{{ field.textValue }}
</view>
<!--普通输入框-->
<u-input
v-if="
field.type == 'input' &&
(field.decimalCount == null || field.decimalCount == '' || Number(field.decimalCount == null) < 0)
"
v-model="field.value"
clearable
:placeholder="field.placeholder"
:disabled="field.fillingWay != '1'"
/>
<!--数字,限制小数位数-->
<u-input
v-if="field.type == 'input' && field.decimalCount != null && Number(field.decimalCount == null) >= 0"
type="number"
clearable
@blur="event => checkDecimal(event, field)"
v-model="field.value"
:placeholder="field.placeholder"
:disabled="field.fillingWay != '1'"
/>
<!--select-->
<view v-if="field.type == 'select'" class="x-bc select-my" @click="handleFieldClick(field)">
<text v-if="field.valueText">{{ field.valueText }}</text>
<text v-else>请选择</text>
<u-icon name="arrow-down" size="20"></u-icon>
</view>
<u-picker
v-if="field.type == 'select'"
:show="field.showPicker"
:columns="[field.options]"
keyName="displayName"
@cancel="field.showPicker = false"
@confirm="event => pickerConfirm(event, field)"
/>
<!--日期-->
<view v-if="field.type == 'date'" class="x-bc select-my" @click="handleFieldClick(field)">
<text v-if="field.value">{{ field.value }}</text>
<text v-else>请选择</text>
<u-icon name="calendar-fill" size="20"></u-icon>
</view>
<u-datetime-picker
v-if="field.type == 'date'"
:show="field.showPicker"
v-model="curDate"
mode="date"
@cancel="field.showPicker = false"
@confirm="event => pickerConfirm(event, field)"
></u-datetime-picker>
</view>
</view>
</u-form>
</scroll-view>
<u-button type="primary" @click="saveHeadData">保存</u-button>
</u-col>
<u-col span="1"> </u-col>
</u-row>
</view>
</template>
<script>
import request from '@/nx/request'
import { cloneDeep } from 'lodash'
import { calcAnalysisValue } from '@/nx/helper/calcAnalysisValue'
import nx from '@/nx'
export default {
data() {
return {
title: '编辑指派单',
taskInstance: {},
scrollTop: 0, //tab标题的滚动条位置
currentTaskNo: '', //当前任务样品
showDicPicker: false,
curDate: Number(new Date()),
//字段值对象
formValue: {},
//后端返回的字段结构
sourceFormFields: [],
//处理后的字段结构
formFields: [],
sampleList: []
}
},
computed: {
dynamicFieldPlaceholder(field) {
return '请输入'
}
},
onLoad(param) {
if (param.currentTaskNo) {
this.currentTaskNo = param.currentTaskNo
}
this.title = '样品分析-任务指派单:' + this.currentTaskNo
this.loadHeadFieldsAndValueByTaskNo()
this.getSampleList(this.currentTaskNo)
},
methods: {
handleFieldClick(field) {
if (field.type == 'date') {
if (field.fillingWay == '1') {
field.showPicker = true
}
return
}
field.showPicker = true
},
checkFeadToDetailField(headToDetailField) {
if (headToDetailField && headToDetailField.trim() != '') {
return true
} else {
return false
}
},
getSampleList(taskNo) {
this.$u.api
.getAssayTaskDetailListByTaskNo({ taskNo: taskNo })
.then(res => {
this.sampleList = res.result
})
.catch(err => {
console.error(err)
})
},
//返回上一页
// customBack() {
// uni.redirectTo({
// url: "/pages/sample/sample-work-detail"
// });
// },
navRightClick() {},
//处理小数位数补0或去除多余位数
checkDecimal(e, field) {
if (e == '') return
const decimalCount = field.decimalCount
if (decimalCount == null || decimalCount == '' || isNaN(decimalCount)) return
const count = Number(field.decimalCount)
const value = field.value
const pos = value.indexOf('.') + 1
let length = value.length - pos
if (pos == 0) {
length = 0
}
while (length < count) {
if (field.value.indexOf('.') < 0) field.value += '.'
field.value = field.value + '0'
length++
}
if (count === 0) {
field.value = parseInt(field.value) + ''
} else if (length > count) {
field.value = field.value.substring(0, pos + count)
}
},
updateFieldValue(name, val) {
for (const field of this.formFields) {
if (field.prop == name) {
field.value = val
break
}
}
},
assembleFields() {
const me = this
const formFields = []
for (const field of this.sourceFormFields) {
//日期类型的默认值
if (field.type == 'date') {
const value = field.value
field.showPicker = false
if (value == 'curDate') {
field.value = me.$helper.dateFormat(new Date(), 'yyyy-MM-dd')
}
}
if (field.type == 'select') {
field.showPicker = false
}
formFields.push(field)
}
//先序列化再转json避免json里定义的方法丢失
this.formFields = JSON.parse(JSON.stringify(formFields, replacer), reviver)
// this.formFields = formFields;
},
//绑定数据
bindFormValue() {
const me = this
//formValue
for (const field of me.formFields) {
const prop = field.prop
if (prop) {
const value = me.formValue[prop]
if (value) {
field.value = value
if (field.type == 'select') {
field.valueText = value
}
}
}
}
},
//获取表单字段和值(老数据)
loadHeadFieldsAndValueByTaskNo() {
const me = this
const currentTaskNo = me.currentTaskNo
this.$u.api
.queryHeadValueByTaskNo({ taskNo: currentTaskNo })
.then(res => {
const result = res.result
const formConf = result.formConf
me.sourceFormFields = eval(formConf)
me.formValue = JSON.parse(result.formValue)
//加载和处理表单字段
me.assembleFields()
//绑定数据
me.bindFormValue()
//读取字段里的动态选项
me.loadFieldApiData()
console.log('formFields', this.formFields)
})
.catch(err => {
//如果没有查到数据,按配置读取新的表单字段
me.getHeadFields()
})
},
// 获取表单字段(新数据)
getHeadFields() {
const me = this
const currentTaskNo = this.currentTaskNo
uni.showLoading({
title: '加载中...'
})
this.$u.api
.queryHeadFieldsByTaskNo({ taskNo: currentTaskNo })
.then(res => {
me.sourceFormFields = me.analysisFormAndFields(res)
//加载和处理表单字段
me.assembleFields()
//读取字段里的动态选项
me.loadFieldApiData()
})
.finally(() => {
uni.hideLoading()
})
},
analysisFormAndFields(formRet) {
const fieldsArray = []
for (const form of formRet) {
let content = cloneDeep(form.content)
content = eval('(' + content + ')')
const column = content.column
for (const field of column) {
fieldsArray.push(field)
}
}
return fieldsArray
},
//将抬头字段值同步保存到明细字段
saveHeadValueToDetail(onComplete) {
//循环抬头字段,提取需哟保存到明细的字段
const conf = []
for (const field of this.formFields) {
const prop = field.prop
const headToDetailField = field.headToDetailField
if (prop && headToDetailField && headToDetailField.trim() != '') {
const value = field.value
const r = {
prop: field.prop,
value: value,
headToDetailField: headToDetailField
}
conf.push(r)
}
}
const data = {
taskNo: this.currentTaskNo,
conf: conf
}
this.$u.api.saveHeadValueToDetail(data).then(res => {
if (onComplete) onComplete()
})
},
handleSave() {
//显示loading
uni.showLoading({
title: '正在保存...'
})
//组装数据
const formValue = {}
for (const field of this.formFields) {
const prop = field.prop
if (prop) {
formValue[prop] = field.value
}
}
const value = {
taskNo: this.currentTaskNo,
formValue: JSON.stringify(formValue),
formConf: JSON.stringify(this.sourceFormFields, replacer)
}
this.$u.api
.saveHeadValue(value)
.then(async res => {
await this.saveHeadValueToDetail(async () => {
if (this.checkPropertyEquality()) {
await this.processIds(this.sampleList, 100)
} else {
uni.hideLoading()
// this.$helper.showToast({
// title: '保存成功!'
// });
uni.redirectTo({
url: '/pages/analysis/sample/sample-work-detail?currentTaskNo=' + this.currentTaskNo
})
}
})
})
.catch(err => {
uni.hideLoading()
console.log(err)
})
},
// 检查字段修改前和修改后字段值是否相等
checkPropertyEquality() {
for (const field of this.formFields) {
const prop = field.prop
if (prop && field['headToDetailField'] && field['headToDetailField'].trim() !== '') {
const flag = this.formValue[prop] !== field.value ? true : false
if (flag) return true
}
}
return false
},
//保存抬头字段
saveHeadData() {
if (this.checkPropertyEquality()) {
uni.showModal({
title: '提示',
content: '您修改的字段将影响样品分析结果,系统将重新计算',
showCancel: false,
success: res => {
if (res.confirm) {
this.handleSave()
}
}
})
} else {
this.handleSave()
}
},
// 通过样品id查询明细
async getSampleDataById(taskDetailId) {
let fieldGroup = []
const { result, additionalProperties } = await this.$u.api.queryFieldsByTaskDetail({
taskDetailId,
isSearchSRange: '0'
})
fieldGroup = result
const conAssayTaskId = additionalProperties.conAssayTaskId
const busSubCSampleId = additionalProperties.busSubCSampleId
const detail = additionalProperties.taskDetail
//处理硫值、硫量:未保存过的数据,读取接口返回的硫值、硫量
// this.loadSValue(detail);
//按公式计算值,并检查原数据与计算后的数据是否一致
try {
calcAnalysisValue(fieldGroup)
} catch (error) {
console.log(error)
}
let valueList = []
let cupNum = 0
for (const g of fieldGroup) {
for (const f of g.fields) {
if (f.dicKey == 'bh' || f.dicKey == 'bh_up') cupNum = f.value
valueList.push({
id: f.detailId,
type: f.pOrE,
value: f.value,
name: f.name,
dataType: f.dataType
})
}
}
let params = {
busSubCSampleId,
conAssayTaskId,
// measureTime : this.curSample.measureTime,
elementParamValueList: valueList,
busAssayTaskDetailId: taskDetailId
}
if (typeof cupNum != 'undefined' && cupNum != null && cupNum != '' && cupNum != 0 && cupNum != '0') {
//提交杯号,保存到后台
params.cupNum = cupNum
}
return params
},
// 提交样品
async submitData(data) {
try {
await this.$u.api.saveDetailValue(data)
} catch (error) {
throw error
}
},
async processIds(list, interval) {
let index = 0
const intervalId = setInterval(async () => {
if (index < list.length) {
const item = list[index]
index++
const params = await this.getSampleDataById(item['id'])
await this.submitData(params)
} else {
clearInterval(intervalId) // 所有任务完成后清除定时器
uni.redirectTo({
url: '/pages/analysis/sample/sample-work-detail?currentTaskNo=' + this.currentTaskNo
})
}
}, interval)
},
async apiRequest(url) {
return request({
url: url,
method: 'GET',
custom: {
isApiEncryption: true
}
})
},
//读取字段里的API选项
async loadFieldApiData() {
const formFields = this.formFields
let changeFlag = false
for (const field of formFields) {
const dicUrl = field.dicUrl
const type = field.type
if (dicUrl && dicUrl != '') {
//读取API选项
try {
const res = await this.apiRequest(dicUrl)
const data = res
const confLabel = field.props.label
const confValue = field.props.value
const emptyItem = { name: '', displayName: '' }
emptyItem[confLabel] = ''
emptyItem[confValue] = ''
//设置valueText、displayName
for (const item of data) {
if (item[confValue] == field.value) {
changeFlag = true
field.valueText = item[confLabel]
}
}
// data.unshift(emptyItem) //添加空数据
field.options = data
} catch (e) {}
}
}
if (changeFlag) {
const formFields = this.formFields
// this.formFields = JSON.parse(JSON.stringify(formFields));
this.formFields = JSON.parse(JSON.stringify(formFields, replacer), reviver)
}
},
/*
* 选择器组件确认事件
* event 事件默认参数
* field 当前字段
* 处理逻辑:
* field.value = 选中的值
* field.valueText = 选中的文本
* todo 关联字段在动态字段的change事件处理
* */
pickerConfirm(event, field) {
const me = this
if (field.type == 'date') {
field.value = nx.$dayjs(event.value).format(field.format)
field.showPicker = false
return
}
const confLabel = field.props.label
const confValue = field.props.value
const selected = event.value[0]
const value = selected[confValue]
const displayName = selected[confLabel]
field.value = value
field.valueText = displayName
if (typeof field.change == 'function') {
field.change({ value }, selected, me)
}
field.showPicker = false
},
checkLoadSValue() {
const vKey = 'sRange'
const vF = this.getFieldByKey(vKey)
if (vF == null) return false
const v = vF.value
if (v == null || v == '') return true
try {
if (v == 0 || number(v) == 0) return true
} catch (e) {}
return false
},
//读取硫值、硫量
loadSValue(detail) {
let flag = this.checkLoadSValue()
if (!flag) return
const vKey = 'sValue'
const rKey = 'sRange'
const vF = this.getFieldByKey(vKey)
if (vF != null) {
vF.value = detail.svalue
}
const rF = this.getFieldByKey(rKey)
if (rF != null) {
rF.value = detail.srange
}
},
getFieldByKey(key) {
const group = this.fieldGroup
let field = null
for (let g of group) {
for (let f of g.fields) {
const dicKey = f.dicKey
if (dicKey && dicKey == key) {
field = f
break
}
}
}
// console.log('field', JSON.stringify(field));
return field
}
}
}
// 自定义 replacer将函数转换为字符串。json序列化和反序列化时避免函数丢失
function replacer(key, value) {
if (typeof value === 'function') {
return value.toString()
}
return value
}
// 自定义 reviver将字符串转换回函数.json序列化和反序列化时避免函数丢失
const functionKeys = ['change', 'dicFormatter']
function reviver(key, value) {
if (functionKeys.includes(key)) {
// 将字符串转换为函数
return new Function('return ' + value)()
}
return value
}
</script>
<style lang="scss" scoped>
.navbar-right {
font-size: 16px;
color: #fff;
margin-right: 20px;
}
.content-title {
width: 100%;
font-size: 20px;
font-weight: 300;
}
.content-title-name {
padding: 10px;
text-align: center;
}
.content-main-height {
height: calc(100vh - 125px);
}
.content-main-left {
height: calc(100vh - 205px);
background-color: #f6f6f6;
}
.form-item-my {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.label-my {
width: 170px; /* 标签宽度 */
padding-right: 10px;
text-align: right;
}
.label-my sub {
font-size: 0.6em; /* 调整下标字体大小 */
vertical-align: sub; /* 调整下标垂直对齐 */
}
.content-my {
flex: 1;
padding-left: 7px;
.select-my {
color: #303133;
font-size: 15px;
padding: 6px 8px;
border: 1px solid #dcdcdc;
border-radius: 5px;
}
}
.u-tab-item {
height: 80px;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #444;
font-weight: 400;
line-height: 1;
border-width: 2px;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-size: 18px;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 16px;
left: 0;
top: 20px;
}
.content-main-left-operation {
height: 80px;
padding-top: 15px;
}
.content-main-right {
height: calc(100vh - 205px);
}
.content-main-right-operation {
height: 80px;
padding-top: 15px;
}
.auncel-container {
display: flex;
justify-content: center;
align-items: center;
}
.auncel {
width: 400px;
height: 400px;
background-image: url(/static/images/auncel.png);
background-repeat: no-repeat;
background-size: 100%;
}
.auncel-title {
width: 100%;
height: 210px;
}
.auncel-weight {
padding: 20px;
}
.weight {
width: 100%;
min-width: 200px;
height: 100px;
padding: 0 15px;
display: flex;
flex-direction: row;
//background-color: #2c405a;
}
.weight-data {
flex-grow: 1;
height: 100%;
color: #4cd964;
text-align: right;
line-height: 100px;
letter-spacing: 2px;
font-size: 75px;
font-family: zzjc-lcd;
}
.weight-data-yellow {
flex-grow: 1;
height: 100%;
color: #ffff00;
text-align: right;
line-height: 100px;
letter-spacing: 2px;
font-size: 75px;
font-family: zzjc-lcd;
}
.weight-data-warning {
flex-grow: 1;
height: 100%;
color: #ff3333;
text-align: right;
line-height: 100px;
font-size: 75px;
font-family: zzjc-lcd;
}
.weight-unit {
color: #ffffff;
font-size: 65px;
line-height: 100px;
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,379 @@
<template>
<view>
<navbar-back :autoBack="false" title="样品分析" @leftClick="customBack"></navbar-back>
<u-row class="content-title" gutter="16">
<u-col span="4">
<view class="content-title-name">
<text>任务列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="8">
<view class="content-title-name">
<text>样品列表</text>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row class="content-main-height" gutter="16" align="top">
<u-col span="4">
<scroll-view
scroll-y
scroll-with-animation
class="content-main-height content-main-left"
:scroll-top="scrollTop"
>
<view
v-for="(task, index) in taskList"
:key="index"
class="u-tab-item"
:class="[current === index ? 'u-tab-item-active' : '']"
@tap.stop="swichTask(index)"
>
<u-row style="width: 100%">
<u-col span="2" style="text-align: center">
<u-icon :color="taskStyle(task)" name="tags-fill" size="34"></u-icon>
</u-col>
<u-col span="10">
<view class="fs18">{{ task.taskNo }}</view>
<view style="margin-top: 10px">{{ task.taskName }}</view>
<view class="x-f" style="margin-top: 10px">
<u-icon color="" name="clock"></u-icon>
<text style="margin-left: 5px">{{ task.taskOperTime }}</text>
</view>
</u-col>
</u-row>
</view>
</scroll-view>
</u-col>
<u-col span="8">
<view class="content-main-height">
<scroll-view scroll-y scroll-with-animation class="content-main-right">
<block v-for="(sample, index) in sampleList" :key="index">
<view v-if="currentTask.reviewCount === sample.reviewCount" style="padding: 5px; font-size: 16px">
<u-row>
<u-col span="3" style="text-align: center">
<u-row>
<u-col span="6" style="text-align: center">
<u-checkbox
v-model="sample.checked"
v-if="
sample.sampleProcessNo === currentNode &&
sample.rollbackStatus !== 'running' &&
sample.rollbackStatus !== 'finished'
"
@change="() => selectSample(sample)"
></u-checkbox>
</u-col>
<u-col span="6" style="text-align: center">
<view
><text>【{{ sample.sort }}】</text></view
>
</u-col>
</u-row>
</u-col>
<u-col span="9">
<view class="sample_desc">
<view>
<view
><text style="padding-left: 10px">{{ sample.sampleCode }}</text></view
>
<view>
<text style="padding-left: 10px">
{{ getDataSourceTypeShow(sample.dataSourceType) }}{{ sample.sampleName }}
</text>
</view>
</view>
<view class="sample_desc_warn" v-if="sample.sampleProcessNo !== currentNode">
当前节点:{{ getProcessNameShow(sample.sampleProcessNo) }}
</view>
<view class="sample_desc_warn" v-if="sample.rollbackStatus === 'revoke'"> 样品退回被驳回 </view>
<view class="sample_desc_warn" v-if="sample.rollbackStatus === 'running'"> 样品退回审批中 </view>
<view class="sample_desc_warn" v-if="sample.rollbackStatus === 'finished'">
样品已退回,请联系管理员处理
</view>
</view>
</u-col>
</u-row>
<u-line style="padding: 5px" color="#bbb" />
</view>
</block>
</scroll-view>
<view class="content-main-right-operation">
<u-row>
<u-col span="3.5"></u-col>
<u-col span="4">
<u-button class="btn-operation" type="warning" @click="showRollbackModal">申请退回样品</u-button>
</u-col>
<u-col span="4">
<u-button class="btn-operation" :disabled="taskList.length === 0" type="success" @click="startWork">
开始分析
</u-button>
</u-col>
</u-row>
</view>
</view>
</u-col>
</u-row>
<!-- 退回样品弹窗 -->
<up-modal
:show="showRollbackModalFlag"
showCancelButton
@confirm="applyRollbackSample"
@cancel="showRollbackModalFlag = false"
title="退回说明"
width="500px"
>
<u--textarea v-model="rollbackContent" placeholder="请输入退回原因或说明"></u--textarea>
</up-modal>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import nx from '@/nx'
import { useScreenOrientation } from '@/nx/hooks/useScreenOrientation'
// 响应式数据
const currentNode = ref('F31')
const dicSampleProcessCodeList = ref([])
const showRollbackModalFlag = ref(false)
const rollbackContent = ref('')
const scrollTop = ref(0)
const current = ref(0)
const currentTask = ref({})
const currentTaskNo = ref('')
const currentTaskType = ref('')
const taskList = ref([])
const sampleList = ref([])
// 计算属性
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const customBack = () => {
uni.reLaunch({ url: '/pages/analysis/index/index' })
}
const selectSample = sample => {
sample.checked = !sample.checked
}
const taskStyle = task => {
if ((task.weightTaskStatus === 0 || task.weightTaskStatus === 1) && task.reviewCount > 0) return 'red'
if (task.weightTaskStatus === 2 && task.reviewCount > 0) return 'green'
return ''
}
const showRollbackModal = () => {
const checkedSampleList = sampleList.value.filter(item => item.checked)
if (checkedSampleList.length === 0) {
uni.showToast({ title: '请选择要退回的样品!', icon: 'none' })
return
}
showRollbackModalFlag.value = true
}
const applyRollbackSample = () => {
const checkedSampleList = sampleList.value.filter(item => item.checked)
if (!rollbackContent.value.trim()) {
uni.showToast({ title: '请输入退回说明!', icon: 'none' })
return
}
showRollbackModalFlag.value = false
uni.showModal({
title: '提示',
content: '发起申请,退回已勾选的样品,是否继续?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.cancel) return
const detailIdList = checkedSampleList.map(item => item.id)
const data = {
remark: rollbackContent.value,
taskId: currentTask.value.id,
detailIds: detailIdList.join(',')
}
uni.showLoading({ title: '正在发起申请...' })
nx.$api.assayTask
.createRollbackApply(data)
.then(() => {
getAssayTaskDetail(currentTaskNo.value)
})
.catch(console.error)
.finally(() => {
uni.hideLoading()
})
}
})
}
const checkWork = () => {
let checkedSampleList = sampleList.value.filter(
item => item.rollbackStatus === 'running' || item.rollbackStatus === 'finished'
)
if (checkedSampleList.length > 0) {
uni.showToast({ title: '存在未处理的退回申请,请处理后再分析!', icon: 'none' })
return false
}
checkedSampleList = sampleList.value.filter(item => item.sampleProcessNo !== currentNode.value)
if (checkedSampleList.length > 0) {
uni.showToast({ title: '部分样品状态异常,请联系管理员处理!', icon: 'none', duration: 2300 })
return false
}
return true
}
const startWork = () => {
if (!checkWork()) return
uni.navigateTo({
url: `/pages/analysis/sample/sample-work-detail?currentTaskNo=${currentTaskNo.value}`
})
}
const swichTask = async index => {
if (index === current.value) return
current.value = index
rollbackContent.value = ''
const task = taskList.value[index]
currentTask.value = task
currentTaskNo.value = task.taskNo
currentTaskType.value = task.taskType
getAssayTaskDetail(task.taskNo)
}
const getAssayTask = () => {
rollbackContent.value = ''
const param = {
finishStatus: 'waiting',
assayOper: userInfo.value.nickname
}
nx.$api.auncel.getAssayTaskList(param).then(res => {
if (res) {
taskList.value = res
if (taskList.value.length > 0) {
const first = taskList.value[0]
currentTask.value = first
currentTaskNo.value = first.taskNo
currentTaskType.value = first.taskType
getAssayTaskDetail(first.taskNo)
}
}
})
}
const getAssayTaskDetail = taskNo => {
sampleList.value = []
nx.$api.assayTask.getAssayTaskDetailListByTaskNo({ taskNo }).then(res => {
const list = res.result || []
list.forEach(item => (item.checked = false))
sampleList.value = list
})
}
const getDicSampleProcessCodeList = () => {
nx.$api.assayTask.queryQmsDicSampleProcessCodeList().then(res => {
dicSampleProcessCodeList.value = res.records || []
})
}
const getProcessNameShow = val => {
const item = dicSampleProcessCodeList.value.find(i => i.processCode === val)
return item ? item.processName : val
}
const getDataSourceTypeShow = val => {
if (val === 2) return '【筛上】'
if (val === 3) return '【筛下】'
return ''
}
// 生命周期
onLoad(() => {
const { lockOrientation } = useScreenOrientation()
lockOrientation('landscape')
getDicSampleProcessCodeList()
getAssayTask()
})
onBackPress(() => {
customBack()
return true
})
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.content-title {
height: 50px;
width: 100%;
font-size: 20px;
font-weight: 300;
}
.content-title-name {
padding: 10px;
text-align: center;
}
.content-main-height {
height: calc(100vh - 125px);
}
.content-main-left {
background-color: #f6f6f6;
}
.u-tab-item {
padding: 5px;
height: 100px;
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #444;
line-height: 1;
border-width: 2px;
border-bottom: dotted;
}
.u-tab-item-active {
position: relative;
color: #0055a2;
font-weight: 600;
background: #fff;
}
.u-tab-item-active::before {
content: '';
position: absolute;
height: 16px;
left: 0;
top: 20px;
}
.content-main-right {
height: calc(100vh - 205px);
}
.content-main-right-operation {
height: 80px;
padding-top: 15px;
padding-right: 15px;
}
.btn-operation {
height: 50px;
font-size: 18px;
width: 95%;
}
.sample_desc {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.sample_desc_warn {
color: red;
padding-right: 10px;
}
</style>

View File

@@ -0,0 +1,240 @@
<template>
<view>
<navbar-back title="打印服务">
<view class="navbar-right" slot="right">
<view class="message-box right-item" @click="refreshPrint">
<up-icon name="reload" color="#fff" size="20" />
</view>
<view class="dot-box right-item" @click="addPrintShow">
<up-icon name="plus" color="#fff" size="20" />
</view>
</view>
</navbar-back>
<view>
<u-swipe-action
v-for="(print, index) in printList"
:key="index"
:index="index"
:options="options"
@click="printSwipeClick"
>
<view class="item u-border-bottom">
<image :src="`/static/images/print${print.isOpen ? '' : '-close'}.png`" mode="aspectFill" />
<view style="width: 100%; padding-left: 30px; margin: 30px">
<u-row>
<u-col span="6">
<text>服务名称{{ print.printName }}</text>
</u-col>
<u-col span="6">
<text>服务IP地址{{ print.printIp }}</text>
</u-col>
</u-row>
<u-row>
<u-col span="6">
<text>服务端口{{ print.printPort }}</text>
</u-col>
<u-col span="6">
<text>连接状态{{ print.isOpen ? '连接正常!!!' : '连接关闭!!!' }}</text>
</u-col>
</u-row>
</view>
</view>
</u-swipe-action>
</view>
<u-modal
:show="printShow"
title="添加打印服务"
show-cancel-button
width="50vw"
confirm-text="添加"
@confirm="addPrint"
@cancel="printShow = false"
>
<view class="slot-content">
<u-form :model="form">
<u-form-item label="名称">
<u-input v-model="form.printName" />
</u-form-item>
<u-form-item label="IP">
<u-input v-model="form.printIp" />
</u-form-item>
<u-form-item label="端口">
<u-input v-model="form.printPort" type="number" />
</u-form-item>
</u-form>
</view>
</u-modal>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 响应式数据
const printShow = ref(false)
const printList = ref([])
const form = ref({
printName: '',
printIp: '',
printPort: 22333
})
const options = [
{
text: '删除',
style: {
fontSize: '28rpx',
backgroundColor: 'rgb(255,58,49)'
}
}
]
// 方法
const addPrintShow = () => {
if (printList.value.length > 0) {
uni.showToast({
title: '已存在打印服务!',
icon: 'none'
})
return
}
form.value = {
printName: '',
printIp: '',
printPort: 22333
}
printShow.value = true
}
const refreshPrint = () => {
const printListData = uni.getStorageSync('KEY_PRINT_LIST') || []
if (printListData.length > 0) {
printListData.forEach(print => {
uni.$print.open(print.printIp)
})
uni.showToast({
title: '刷新成功!',
icon: 'none'
})
} else {
uni.showModal({
title: '提示',
content: '未配置打印服务,请点击右边的“+”配置打印服务!',
showCancel: false
})
}
}
const addPrint = () => {
const { printName, printIp, printPort } = form.value
if (!printName.trim()) {
uni.showToast({ title: '打印服务名称不允许为空!', icon: 'none' })
return
}
if (!printIp.trim()) {
uni.showToast({ title: '打印服务IP不允许为空', icon: 'none' })
return
}
const ipReg =
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
if (!ipReg.test(printIp)) {
uni.showToast({ title: '打印服务IP不正确', icon: 'none' })
return
}
let storedList = uni.getStorageSync('KEY_PRINT_LIST') || []
const exists = storedList.some(p => p.printIp === printIp)
if (exists) {
uni.showToast({ title: '打印服务已存在!', icon: 'none' })
return
}
storedList.push({
printName,
printIp,
printPort,
isDefault: storedList.length === 0
})
uni.$print.open(printIp)
uni.setStorageSync('KEY_PRINT_LIST', storedList)
refreshList()
printShow.value = false
}
const printSwipeClick = (index, optionsIndex) => {
if (optionsIndex === 0) {
const delPrint = printList.value[index]
uni.$print.close(delPrint.printIp)
printList.value.splice(index, 1)
if (delPrint.isDefault && printList.value.length > 0) {
printList.value[0].isDefault = true
}
uni.setStorageSync('KEY_PRINT_LIST', printList.value)
uni.showToast({ title: '删除成功!', icon: 'none' })
}
}
const refreshList = () => {
const stored = uni.getStorageSync('KEY_PRINT_LIST') || []
printList.value = stored.map(print => {
const printer = uni.$print.printMap?.get(print.printIp)
return {
...print,
isOpen: printer ? printer.isOpen : false
}
})
}
// 生命周期
onMounted(() => {
refreshList()
uni.$on('printStatus', refreshList)
})
onUnmounted(() => {
uni.$off('printStatus', refreshList)
})
</script>
<style lang="scss" scoped>
.navbar-right {
margin-right: 12px;
display: flex;
}
.right-item {
margin: 0 6px;
position: relative;
color: #ffffff;
display: flex;
}
.slot-content {
font-size: 14px;
color: $u-content-color;
padding-left: 15px;
padding-right: 15px;
}
.item {
display: flex;
padding: 20rpx;
}
image {
width: 60px;
flex: 0 0 60px;
height: 60px;
margin-right: 10px;
border-radius: 6px;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view>
<navbar-back title="系统设置"></navbar-back>
<view>
<u-cell-group title="打印设置">
<u-cell icon="order" title="打印服务" @click="nx.$router.go('/pages/analysis/setting/print')"></u-cell>
</u-cell-group>
<u-cell-group title="天平状态">
<u-cell
icon="hourglass-half-fill"
title="查看天平"
@click="nx.$router.go('/pages/analysis/auncel/auncel-status')"
></u-cell>
</u-cell-group>
</view>
</view>
</template>
<script setup>
import nx from '@/nx'
</script>
<style></style>

View File

@@ -1,202 +0,0 @@
<template>
<view class="form-container">
<u-form :model="formData" ref="formRef" :rules="rules" label-width="180rpx">
<u-form-item label="合同编号" prop="code">
<u-input v-model="formData.code" placeholder="请输入合同编号" />
</u-form-item>
<u-form-item label="合同名称" prop="name">
<u-input v-model="formData.name" placeholder="请输入合同名称" />
</u-form-item>
<u-form-item label="合同状态" prop="status">
<u-picker
v-model="formData.status"
:range="statusOptions"
range-key="text"
@change="onStatusChange"
>
<u-input
v-model="statusText"
disabled
placeholder="请选择合同状态"
suffix-icon="arrow-down"
/>
</u-picker>
</u-form-item>
<u-form-item label="签订日期" prop="signDate">
<uni-datetime-picker v-model="formData.signDate" type="date" />
</u-form-item>
<u-form-item label="合同开始日期" prop="startDate">
<uni-datetime-picker v-model="formData.startDate" type="date" />
</u-form-item>
<u-form-item label="合同结束日期" prop="endDate">
<uni-datetime-picker v-model="formData.endDate" type="date" />
</u-form-item>
<u-form-item label="合同金额" prop="amount">
<u-input v-model="formData.amount" placeholder="请输入合同金额" type="number" />
</u-form-item>
<u-form-item label="备注" prop="remark">
<u-textarea v-model="formData.remark" placeholder="请输入备注" />
</u-form-item>
<u-form-item label="岗位ID" prop="postId">
<u-input v-model="formData.postId" placeholder="请输入岗位ID" />
</u-form-item>
<!-- TODO: 附件上传 -->
</u-form>
<u-button
type="primary"
@click="submit"
class="submit-btn"
:loading="submitting"
:disabled="submitting"
>提交</u-button>
</view>
</template>
<script setup>
import { ref, reactive, computed, toRaw } from 'vue'
import DemoContractApi from '@/sheep/api/infra/democontract'
import { onLoad } from '@dcloudio/uni-app'
const formType = ref('create')
const formData = reactive({
id: undefined,
code: '',
name: '',
status: 0,
signDate: '',
startDate: '',
endDate: '',
amount: undefined,
remark: '',
postId: ''
})
const statusOptions = [
{ value: 0, text: '草稿' },
{ value: 1, text: '审核中' },
{ value: 2, text: '已通过' },
{ value: 3, text: '已拒绝' }
]
const statusText = computed(() => {
const option = statusOptions.find(item => item.value === formData.status)
return option ? option.text : '请选择合同状态'
})
const rules = {
code: [{ required: true, message: '合同编号不能为空', trigger: 'blur' }],
name: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }],
amount: [{ required: true, message: '合同金额不能为空', trigger: 'blur' }],
postId: [{ required: true, message: '岗位ID不能为空', trigger: 'blur' }]
}
const formRef = ref(null)
const submitting = ref(false)
const formTitle = computed(() => {
return formType.value === 'create' ? '新增合同' : '修改合同'
})
onLoad((options) => {
if (options.type) {
formType.value = options.type
}
if (options.id) {
getDemo(options.id)
}
})
const getDemo = async (id) => {
const res = await DemoContractApi.getDemoContract(id)
if (!res || res.code !== 0) {
return
}
const data = res.data || {}
formData.id = data.id
formData.code = data.code || ''
formData.name = data.name || ''
formData.status = Number.isNaN(Number(data.status)) ? 0 : Number(data.status)
formData.signDate = data.signDate || ''
formData.startDate = data.startDate || ''
formData.endDate = data.endDate || ''
formData.amount = data.amount !== undefined && data.amount !== null ? String(data.amount) : ''
formData.remark = data.remark || ''
formData.postId = data.postId !== undefined && data.postId !== null ? String(data.postId) : ''
}
const onStatusChange = (e) => {
formData.status = statusOptions[e.detail.value].value
}
const validateForm = async () => {
if (!formRef.value || typeof formRef.value.validate !== 'function') {
throw new Error('表单未准备就绪')
}
try {
await formRef.value.validate()
return true
} catch (error) {
throw error
}
}
const buildPayload = () => {
const raw = toRaw(formData)
const payload = {
...raw,
status: Number(raw.status)
}
if (Number.isNaN(payload.status)) {
payload.status = 0
}
if (payload.amount !== undefined && payload.amount !== null && payload.amount !== '') {
const amountNumber = Number(payload.amount)
payload.amount = Number.isNaN(amountNumber) ? payload.amount : amountNumber
}
if (formType.value === 'create') {
delete payload.id
}
return payload
}
const submit = async () => {
if (submitting.value) {
return
}
try {
await validateForm()
} catch (error) {
return
}
const payload = buildPayload()
const apiFn = formType.value === 'create' ? DemoContractApi.createDemoContract : DemoContractApi.updateDemoContract
submitting.value = true
try {
const result = await apiFn(payload)
if (!result || result.code !== 0) {
return
}
uni.showToast({
title: formType.value === 'create' ? '新增成功' : '修改成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 600)
} finally {
submitting.value = false
}
}
</script>
<style lang="scss" scoped>
.form-container {
padding: 30rpx;
background: #fff;
}
.submit-btn {
margin-top: 30rpx;
}
</style>

View File

@@ -1,192 +0,0 @@
<template>
<view class="app-container">
<!-- 搜索工作栏 -->
<view class="search-section">
<u-search
placeholder="请输入合同名称"
v-model="queryParams.name"
:show-action="true"
action-text="搜索"
@custom="handleQuery"
@search="handleQuery"
></u-search>
<!-- 高级搜索 -->
<view class="advanced-search" v-if="showAdvanced">
<u-form :model="queryParams" label-width="120rpx">
<u-form-item label="合同编号">
<u-input v-model="queryParams.code" placeholder="请输入合同编号" />
</u-form-item>
<u-form-item label="合同金额">
<u-input v-model="queryParams.amount" placeholder="请输入合同金额" type="number" />
</u-form-item>
</u-form>
</view>
<view class="search-toggle">
<u-button type="info" size="small" @click="showAdvanced = !showAdvanced">
{{ showAdvanced ? '收起' : '高级搜索' }}
</u-button>
</view>
</view>
<!-- 操作工具栏 -->
<view class="action-bar">
<u-button type="primary" icon="plus" text="新增" @click="openForm('create')"></u-button>
</view>
<!-- 列表 -->
<u-list @scrolltolower="scrolltolower">
<u-list-item v-for="item in list" :key="item.id">
<u-cell :title="item.name" :label="item.code">
<template #value>
<view class="item-info">
<text class="info-text">状态: {{ getStatusText(item.status) }}</text>
<text class="info-text">金额: ¥{{ item.amount || 0 }}</text>
<text class="info-text" v-if="item.signDate">签订: {{ sheep.$helper.timeFormat(item.signDate, 'yyyy-mm-dd') }}</text>
</view>
</template>
<template #right-icon>
<view class="action-buttons">
<u-button type="primary" size="mini" @click="openForm('update', item.id)">编辑</u-button>
<u-button type="error" size="mini" @click="handleDelete(item.id)">删除</u-button>
</view>
</template>
</u-cell>
</u-list-item>
<u-loadmore :status="status" />
</u-list>
</view>
</template>
<script setup>
import sheep from '@/sheep'
import { ref, reactive, onMounted } from 'vue'
import DemoContractApi from '@/sheep/api/infra/democontract'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
const loading = ref(false)
const total = ref(0)
const list = ref([])
const showAdvanced = ref(false)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: '',
name: '',
status: '',
signDate: [],
startDate: [],
endDate: [],
amount: '',
remark: '',
postId: ''
})
const status = ref('loadmore')
const getList = async () => {
loading.value = true
status.value = 'loading'
try {
const { data } = await DemoContractApi.getDemoContractPage(queryParams)
if (queryParams.pageNo === 1) {
list.value = data.list
} else {
list.value = list.value.concat(data.list)
}
total.value = data.total
if (list.value.length >= total.value) {
status.value = 'nomore'
} else {
status.value = 'loadmore'
}
} finally {
loading.value = false
uni.stopPullDownRefresh()
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const openForm = (type, id) => {
let url = `/pages/app/democontract/form?type=${type}`
if (id) {
url += `&id=${id}`
}
uni.navigateTo({ url })
}
const handleDelete = async (id) => {
const [err, res] = await uni.showModal({
title: '提示',
content: '确定要删除吗?'
})
if (res && res.confirm) {
await DemoContractApi.deleteDemoContract(id)
uni.showToast({ title: '删除成功' })
handleQuery() // 重新加载列表
}
}
const getStatusText = (status) => {
// 根据实际状态值进行映射,这里需要根据字典或枚举值调整
const statusMap = {
0: '草稿',
1: '审核中',
2: '已通过',
3: '已拒绝'
}
return statusMap[status] || '未知'
}
const scrolltolower = () => {
if (status.value === 'loadmore') {
queryParams.pageNo++
getList()
}
}
onMounted(() => {
getList()
})
onPullDownRefresh(() => {
handleQuery()
})
onReachBottom(() => {
scrolltolower()
})
</script>
<style lang="scss" scoped>
.search-wrap {
padding: 20rpx;
background: #fff;
}
.action-bar {
padding: 20rpx;
}
.list-wrap {
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
}
.item-title {
font-size: 30rpx;
font-weight: bold;
}
.item-desc {
font-size: 26rpx;
color: #999;
margin-top: 10rpx;
}
}
</style>

View File

@@ -1,452 +0,0 @@
<!-- 签到界面 -->
<template>
<s-layout title="签到有礼">
<s-empty v-if="state.loading" icon="/static/data-empty.png" text="签到活动还未开始" />
<view v-if="state.loading" />
<view class="sign-wrap" v-else-if="!state.loading">
<!-- 签到日历 -->
<view class="content-box calendar">
<view class="sign-everyday ss-flex ss-col-center ss-row-between ss-p-x-30">
<text class="sign-everyday-title">签到日历</text>
<view class="sign-num-box">
已连续签到 <text class="sign-num">{{ state.signInfo.continuousDay }}</text>
</view>
</view>
<view
class="list acea-row row-between-wrapper"
style="
padding: 0 30rpx;
height: 240rpx;
display: flex;
justify-content: space-between;
align-items: center;
"
>
<view class="item" v-for="(item, index) in state.signConfigList" :key="index">
<view
:class="
(index === state.signConfigList.length ? 'reward' : '') +
' ' +
(state.signInfo.continuousDay >= item.day ? 'rewardTxt' : '')
"
>
{{ item.day }}
</view>
<view
class="venus"
:class="
(index + 1 === state.signConfigList.length ? 'reward' : '') +
' ' +
(state.signInfo.continuousDay >= item.day ? 'venusSelect' : '')
"
>
</view>
<view class="num" :class="state.signInfo.continuousDay >= item.day ? 'on' : ''">
+ {{ item.point }}
</view>
</view>
</view>
<!-- 签到按钮 -->
<view class="myDateTable">
<view class="ss-flex ss-col-center ss-row-center sign-box ss-m-y-40">
<button
class="ss-reset-button sign-btn"
v-if="!state.signInfo.todaySignIn"
@tap="onSign"
>
签到
</button>
<button class="ss-reset-button already-btn" v-else disabled> 已签到 </button>
</view>
</view>
</view>
<!-- 签到说明 -->
<view class="bg-white ss-m-t-16 ss-p-t-30 ss-p-b-60 ss-p-x-40">
<view class="activity-title ss-m-b-30">签到说明</view>
<view class="activity-des">1.已累计签到{{ state.signInfo.totalDay }}</view>
<view class="activity-des">
2.据说连续签到第 {{ state.maxDay }} 天可获得超额积分要坚持签到哦~~
</view>
<view class="activity-des"> 3.积分可以在购物时抵现金结算的哦 ~~</view>
</view>
</view>
<!-- 签到结果弹窗 -->
<su-popup :show="state.showModel" type="center" round="10" :isMaskClick="false">
<view class="model-box ss-flex-col">
<view class="ss-m-t-56 ss-flex-col ss-col-center">
<text class="cicon-check-round"></text>
<view class="score-title">
<text v-if="state.signResult.point">{{ state.signResult.point }} 积分 </text>
<text v-if="state.signResult.experience"> {{ state.signResult.experience }} 经验</text>
</view>
<view class="model-title ss-flex ss-col-center ss-m-t-22 ss-m-b-30">
已连续打卡 {{ state.signResult.day }}
</view>
</view>
<view class="model-bg ss-flex-col ss-col-center ss-row-right">
<view class="title ss-m-b-64">签到成功</view>
<view class="ss-m-b-40">
<button class="ss-reset-button confirm-btn" @tap="onConfirm">确认</button>
</view>
</view>
</view>
</su-popup>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onReady } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import SignInApi from '@/sheep/api/member/signin';
const headerBg = sheep.$url.css('/static/img/shop/app/sign.png');
const state = reactive({
loading: true,
signInfo: {}, // 签到信息
signConfigList: [], // 签到配置列表
maxDay: 0, // 最大的签到天数
showModel: false, // 签到弹框
signResult: {}, // 签到结果
});
// 发起签到
async function onSign() {
const { code, data } = await SignInApi.createSignInRecord();
if (code !== 0) {
return;
}
state.showModel = true;
state.signResult = data;
// 重新获得签到信息
await getSignInfo();
}
// 签到确认刷新页面
function onConfirm() {
state.showModel = false;
}
// 获得个人签到统计
async function getSignInfo() {
const { code, data } = await SignInApi.getSignInRecordSummary();
if (code !== 0) {
return;
}
state.signInfo = data;
state.loading = false;
}
// 获取签到配置
async function getSignConfigList() {
const { code, data } = await SignInApi.getSignInConfigList();
if (code !== 0) {
return;
}
state.signConfigList = data;
if (data.length > 0) {
state.maxDay = data[data.length - 1].day;
}
}
onReady(() => {
getSignInfo();
getSignConfigList();
});
</script>
<style lang="scss" scoped>
.header-box {
border-top: 2rpx solid rgba(#dfdfdf, 0.5);
}
// 日历
.calendar {
background: #fff;
.sign-everyday {
height: 100rpx;
background: rgba(255, 255, 255, 1);
border: 2rpx solid rgba(223, 223, 223, 0.4);
.sign-everyday-title {
font-size: 32rpx;
color: rgba(51, 51, 51, 1);
font-weight: 500;
}
.sign-num-box {
font-size: 26rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
.sign-num {
font-size: 30rpx;
font-weight: 600;
color: #ff6000;
padding: 0 10rpx;
font-family: OPPOSANS;
}
}
}
// 年月日
.bar {
height: 100rpx;
.date {
font-size: 30rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #333333;
line-height: normal;
}
}
.cicon-back {
margin-top: 6rpx;
font-size: 30rpx;
color: #c4c4c4;
line-height: normal;
}
.cicon-forward {
margin-top: 6rpx;
font-size: 30rpx;
color: #c4c4c4;
line-height: normal;
}
// 星期
.week {
.week-item {
font-size: 24rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
flex: 1;
}
}
// 日历表
.myDateTable {
display: flex;
flex-wrap: wrap;
.dateCell {
width: calc(750rpx / 7);
height: 80rpx;
font-size: 26rpx;
font-weight: 400;
color: rgba(51, 51, 51, 1);
}
}
}
.is-sign {
width: 48rpx;
height: 48rpx;
position: relative;
.is-sign-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
line-height: normal;
}
.is-sign-image {
position: absolute;
left: 0;
top: 0;
width: 48rpx;
height: 48rpx;
}
}
.cell-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #333333;
line-height: normal;
}
.cicon-title {
position: absolute;
right: -10rpx;
top: -6rpx;
font-size: 20rpx;
color: red;
}
// 签到按钮
.sign-box {
height: 140rpx;
width: 100%;
.sign-btn {
width: 710rpx;
height: 80rpx;
border-radius: 35rpx;
font-size: 30rpx;
font-weight: 500;
box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
background: linear-gradient(90deg, #ff6000, #fe832a);
color: #fff;
}
.already-btn {
width: 710rpx;
height: 80rpx;
border-radius: 35rpx;
font-size: 30rpx;
font-weight: 500;
}
}
.model-box {
width: 520rpx;
// height: 590rpx;
background: linear-gradient(177deg, #ff6000 0%, #fe832a 100%);
// background: linear-gradient(177deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 10rpx;
.cicon-check-round {
font-size: 70rpx;
color: #fff;
}
.score-title {
font-size: 34rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #fcff00;
}
.model-title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
}
.model-bg {
width: 520rpx;
height: 344rpx;
background-size: 100% 100%;
background-image: v-bind(headerBg);
background-repeat: no-repeat;
border-radius: 0 0 10rpx 10rpx;
.title {
font-size: 34rpx;
font-weight: bold;
color: var(--ui-BG-Main-TC);
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #999999;
}
.cancel-btn {
width: 220rpx;
height: 70rpx;
border: 2rpx solid #ff6000;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ff6000;
line-height: normal;
margin-right: 10rpx;
}
.confirm-btn {
width: 220rpx;
height: 70rpx;
background: linear-gradient(90deg, #ff6000, #fe832a);
box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
line-height: normal;
}
}
}
//签到说明
.activity-title {
font-size: 32rpx;
font-weight: 500;
color: #333333;
line-height: normal;
}
.activity-des {
font-size: 26rpx;
font-weight: 500;
color: #666666;
line-height: 40rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.rewardTxt {
width: 74rpx;
height: 32rpx;
background-color: #f4b409;
border-radius: 16rpx;
font-size: 20rpx;
color: var(--ui-BG-Main-TC);
line-height: 32rpx;
text-align: center;
padding: 2rpx;
}
.venus {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDQ3N0E3RkJEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDQ3N0E3RkFEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnZClGIAAAT7SURBVHja7JtLcxtFEMdnn1rJUhIrTiS7KLsK/EXwNck3gCMcuUFx4QvACW48buHGMZxJPkiowpjC2LElcKzXSqul/6OdoOB9zezsCCimqmvXtdqd+U33dPf0rq2Tn54zg81KjrGuB75x8FbuddsQWIvkS5IxySA5b5no2DUE94TkKPk7IHmf5JDkUQJdW7MNw623o+Ra698KmAdnDNLeIJwRSHvDcLVDVnYynU771fnLl9eFcLZts+VymQf5iJ6pzfFYJeKgT/IZyTskXdWOPM9jjYbPLMticRyz2Sxk8/lc9XFDksckH1IcDKtq8FOSD6rMIuCCoPHXrBIk/qYDC0MlyO1kTBOSj6uuwXerwPn+63DrrdFo8OsV2ns6nEy3Chwg8lpFyK4OwNrgNGpSvxfFoDzP5etJR8PzsiYETmk+X5BjmpkBrHPGU109TeKqv5X3rT3QQ3ObaPDGRjIZXWZpol+b/cebUUA4iuHw938UoNbk9+zsjP16eqoV4JfjH1uqgLjxe10DiaKIDYZDNqfU7OrqSifjkzxIuwDubV2juLi8ZK7j8oT74uJSJ+BRHqQtC6cS/7A9wtrDvb7v84A9Go2UMyQZSFsWrmz6td4GgyF38a7r8lgGefHiQjnjkYG064ZDmjWktecRnEOCDa9DpjqdTrnUDSk2vJXgsHGdTKa0t5vx/V0Yhvy4WMx5hdf3VqYJDQqTxW+QdmECPM8h8flvPPRFR/QXBEEuKEw9Yz/5AyoDtBkei5zriyw4pGVFmoOHPD7+mQ/ccR1mWzZpyuIacx2Hgzl0FJkIjjBTHBeLiO6LuDZH4zFbRkt63oLt7+8XAmJcIhFP0eTn2C9CgzBTrPjUp7XbW6VSJGjx5OSEBhsz2wGkxc0R967LuukuSWKaFEwMnXJQSL/fZ51Op/QSuL5OdVjY7beFBq2sAlHZ/A8aOTg4YKenv/EBr8Nl5ZUO6jPJRGCg0dJie/d3WbPZlMpPMwpZsN0Y2sOVb7PcOzou22CGe3u7fB3J3Cd+udvvScG9soT0Kt13tAZj4UVRwHmWtZClcj+azX6vxx0GN8ECUFyHjnsEp+KlM8b3TBTKBCDqkA/SIFV20jCbO3duk8OIcgHF7G9ttbinVYFLcTBgeAAP+vc4mAkJVywLibARpZvOjWWA36rApYSI1+DSMhltkJPpRGIyJrXAZeWiWiCnk9kNU4RGl8m5MN1VLFzwWKobLm83kQtZbnMb8lgYJ2aIYH//3g73lEHQ5ECAXYUSu7QWZeCKqmoCcqSyPkQgd8lHbne3uSMRbWenSwNtU476BxtT9oJQOZ3OKKloq6SmmXBlyoZKb3nG4wnXyt1ul9261UkN9ggjvd497mDOz885KGN3pfvKg1OuixY15JCHh2/ymFjUms2AMqD9BFB/qwUQg5ZtrVY9b7H/LxtmZSCbaCr9KgGmpEeG6qpzM2tQBHudb5eKNLfKiUMzgAIyL6uRreHklB9qX4MDlQfLpHUV4AY6AB+rzl4ZyIqa+0aHiX6UlDTwMcK2CqQoEGmEE5+RfFK4N636vWjKh0Cp5ceS38k8JXko8yHQ7W7PXKBPBvYwGegNF4/q12g05mV7HXClqgs1ffEr+/LmaTIx0nCb+uI3U5M64Tadi5aBrBXORLKdB1k7nKndhID8GqUaErze/coEXJ1OJm9CY2bw3wr+FGAAoa6PIUihovYAAAAASUVORK5CYII=');
background-repeat: no-repeat;
background-size: 100% 100%;
width: 56rpx;
height: 56rpx;
margin: 10rpx auto;
}
.venusSelect {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QzkwRkI4NEFEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QzkwRkI4NDlEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkX00M0AAAfVSURBVHjaxFpdcBNVFD67STbppn9poU0LtSBYykBlgKrjqKOtihbq8ICKILyJwxM+6fjCk2/6pC+O4hs+6fgAbWFw1OID+ICMAwwKlRmKLW3Tpvlrskk3m13Pvdm0NNn//HA73+ymd3Pv+e4595xzT5bJ3DgJNWyMelUqNaD36a8N+9kaEeMR3yAERES952sxsbtya2lIbgQxqH72IT5EbEW8pZKuHkGlugyLyT3aBtW+qpJkKb/qgEeMIAYNnhlUn+Edz2NuokqtNVdTTbqVyhO0Q67qJMt2MnXbTq/cp+9+ZkyOYYFxN4EiLaF5SbokccyKkWRS1z4we4ZDfIE4hmhxvJLNu8HTtg85ekGRRcjOXwIp9pfT4aKIs4iP+f4zYrl78HPEqbLMJNAPXHBI/SQjSTd+PoD3LpCi15wMGVBlSiM+NfSihJ8Jjlt4Rheu5n7wtL+B93IJPO37aH8Z45+ohBdtKUtz7a+jJLK+/dN+BTX5p5MpWh6HF6XNE9iLwr9mSG6VZP65bPR6NVI1BwQZF3BtA+Bq2oG3Pt3HFCVnfUHaX6XQHCeXgVz8Nojz4+SDzTgo2yfIBV9G89utzi5DtRvDcnQ+JSciyd+rH+hdjb22tFOp5mreCUrocg0CPet5LATJvHbldWSiGllIzZpdeR2ZqFPtZTNJSIQeQGv3DucEbcrLmkRSXvP/cs4RZm6Ow/T1McffpyiSJTZ+jDfOZFDlOuARo5p9aKJ2IYkpCN+9AmLsIcSm/3Y0BkWpPCPRX9/nDVI1BTTAI0YRA9r99gWbv3MF90MGvG4WQrfHnRMslWcQMRL55SivbaJk064FjxhFDGj0gad5u22zkrMZiExcBQ7JcW4GlhfuQ3L+gSMTzc9fItcgYiTy8xHeTIOGmnMHtoEn2G971cP3MAUTk+B2MeBBkh4kGbp92ZEGyfxEDj1NLl56j9fbg4U9N6C179zNPZhK7cV7yRZkSYTI3atIjgUXywCLINf03L8gRGZtj0dA5CDyaMg5SPZk+OLhFZLMwtg7hTLDKGJAM09s6QGuY6+upxLTKcgkFmE5FUdFIYQEiHgv4VURl4BjgZon0SA9EeKKi1kZliU8Nnn84OabwIPw+huB8zdR+OqbwdcYMAwB4ux1yEYmtLp+I5WBdft/EApx8Cs9cu6mblyxXXTl9FpOFGDyjzGQMwlwIQlUEDAMagqvhBTVHl4ZplDezpsq+UdOTkMuIcByfBYElCWHmsgpLHQ/NwTe+gaTBH0XWkgGpPgDrfLHl4gTTOj8IVLdTqkF2aIslwF+2zCeGDjzIJ5OwtSVc6Cko9QMCUmWyZMiZJn8cI8E7Lwm11wJOTypBPuHoCG4yVrgz2VBmFBj69pGTvv1hVSN0XSxHE8LRUbaW9G01wdPvHgQQtcugJwMU5LFpIrWDjXMUGIsMNRR5DwcBPcMQV1L0NKchYHIkU2WkiVrTvgT6XEjyN/TY08R5KyAaWc6n3tagMvjhuCzQ+jlOmylVPRZjw/an9kPdYF1lucjIPLJmERoyP9j+8GflIIXPYW4XOKVJAmWZ27Y8nAMaq5t9yB4WjeumJ4hOaIErx/W978JXH2TbY9K5ZNKMhzC5dSjyTapQ5IyFxozvLJGz9Hp/Am+Y7utH8kan+yDhYVp8lVgdDWX33f+ji3gruNpnLPTxNl/8vKtbeTAeKDj0DmhuGShT3Jxkp4guGCv5cnT0QX0hgq4dOnBSshIxxehUbZJbu4OSJEpbXJvnxf0zoP6JMP/keVGTfZYEiCDQlttmXjE1hlTnJ3A+Ketuc53RwSz86AuSXFxitYzueBWUyGWE9E1pqiozoRhXdQJMLAaEyUMMdJyGp2Ux4Lm7iG5h5rkNhweFUpLFtonZANNTmOA3WzmFiGLGY3XlSel0DpOCzRs6gWXj4fkgwnIhKZIcKDhgsTLTCICfMs683gb1tbchiNjgt0TfYFkym7JQkyl8nkoixkMesiGzT1Q19q20t+0dTvwnU/A0iQSDYeoNinBQLOTE/2BjUcvCE5rMoJ2XcSYYBr3FOPmoHHLU+APdtJMpvg7bp8XAr19mKJ1weK9u5CJxUDpMt+HxfJ2HbsoVLzwa1aT4fw8dD3/Au43lv7YYjQF19gAHXv6UYMJa7UepQZFJzMT9fp9lJidorCvgbfkSRXbBB2UDRX5MdREnZYNwZECRcxQXLUnl8s5KPw6MNFsdBFzzdaaE8xGwrUx0eXZORrw3c1NNdEk0ZwUi2OQn7fvZBz9fEZKDjNzFLqn7dYApnVtNtKvecx5oxVfHCsmSt4ts/0rrxiO5NO6jvUWyC0guZgT+SPllu4Jzjr9AT0bjqKWQ1qH0RWQfvKcwzm+s6BB01X6RC1pHIf82w02NRmnsng7WjX28iJqLu5Ec4XXSE5XYg+S91A+UlHS/H2riXfq1n3N8mM2HKOOgutsodkNqZKIMxGQokvFw40jhnFMoZZ70CzyrpLd2S0kb00Oa5KMJDC8LAHL4QEmK4HGKYaSq+/bJFTyZ/GyX+VK3pzUStA1SRJrkTNZrWHG1e8IGuMZt5eqrUH9U8iwUbVci1w1BKnW65RWSVaVnBomAKoI3E9IQEEipX3jap9Q1hxmBElBocp/AmIYcQaRQSQQ36r/E8odvepOxoa5khfRT1pf+8q0/wUYAFU/P0XyeZQPAAAAAElFTkSuQmCC');
}
.num {
font-size: 36rpx;
font-family: 'Guildford Pro';
}
.item {
align-items: center;
justify-content: space-between;
flex-direction: column;
height: 130rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.on {
color: #f4b409;
}
</style>

View File

@@ -1,81 +0,0 @@
<template>
<view class="content-container">
<span v-for="(part, index) in formattedContent" :key="index"
@click="handleClick(part)"
:class="{'highlight-number': part.isNumber, 'phone-number': part.isPhone}">
{{ part.text }}
</span>
</view>
</template>
<script>
export default {
name: 'HighlightNumber',
props: {
content: {
type: String,
required: true
}
},
computed: {
formattedContent() {
const phoneRegex = /(1[3-9]\d{9})/g;
const numberRegex = /(\d+)/g;
let text = this.content;
let result = [];
let match;
// Step 1: 提取手机号
while ((match = phoneRegex.exec(text)) !== null) {
if (match.index > 0) {
const before = text.slice(0, match.index);
result.push(...this.splitAndPush(before, false, false));
}
result.push({ text: match[0], isNumber: true, isPhone: true });
text = text.slice(match.index + match[0].length);
}
// Step 2: 提取普通数字
while ((match = numberRegex.exec(text)) !== null) {
if (match.index > 0) {
const before = text.slice(0, match.index);
result.push(...this.splitAndPush(before, false, false));
}
result.push({ text: match[0], isNumber: true });
text = text.slice(match.index + match[0].length);
}
// Step 3: 添加剩余文本
if (text.length > 0) {
result.push(...this.splitAndPush(text, false, false));
}
return result;
}
},
methods: {
splitAndPush(str, isNumber = false, isPhone = false) {
return str.split('').map(char => ({ text: char, isNumber, isPhone }));
},
handleClick(part) {
if (part.isPhone) {
this.$emit('phone-click', { phoneNumber: part.text });
} else if (part.isNumber) {
this.$emit('number-click', { number: part.text });
}
}
}
};
</script>
<style scoped>
.highlight-number {
color: #ff5722;
font-weight: bold;
}
.phone-number {
color: #007AFF;
text-decoration: underline;
}
</style>

View File

@@ -1,227 +0,0 @@
<!-- 商品分类列表 -->
<template>
<s-layout :bgStyle="{ color: '#fff' }" tabbar="/pages/index/category" title="分类">
<view class="s-category">
<view class="three-level-wrap ss-flex ss-col-top">
<!-- 商品分类 -->
<view class="side-menu-wrap" :style="[{ top: Number(statusBarHeight + 88) + 'rpx' }]">
<scroll-view scroll-y :style="[{ height: pageHeight + 'px' }]">
<view
class="menu-item ss-flex"
v-for="(item, index) in state.categoryList"
:key="item.id"
:class="[{ 'menu-item-active': index === state.activeMenu }]"
@tap="onMenu(index)"
>
<view class="menu-title ss-line-1">
{{ item.name }}
</view>
</view>
</scroll-view>
</view>
<!-- 商品分类 -->
<view class="goods-list-box" v-if="state.categoryList?.length">
<scroll-view scroll-y :style="[{ height: pageHeight + 'px' }]">
<image
v-if="state.categoryList[state.activeMenu].picUrl"
class="banner-img"
:src="sheep.$url.cdn(state.categoryList[state.activeMenu].picUrl)"
mode="widthFix"
/>
<first-one v-if="state.style === 'first_one'" :pagination="state.pagination" />
<first-two v-if="state.style === 'first_two'" :pagination="state.pagination" />
<second-one
v-if="state.style === 'second_one'"
:data="state.categoryList"
:activeMenu="state.activeMenu"
/>
<uni-load-more
v-if="
(state.style === 'first_one' || state.style === 'first_two') &&
state.pagination.total > 0
"
:status="state.loadStatus"
:content-text="{
contentdown: '点击查看更多',
}"
@tap="loadMore"
/>
</scroll-view>
</view>
</view>
</view>
</s-layout>
</template>
<script setup>
import secondOne from './components/second-one.vue';
import firstOne from './components/first-one.vue';
import firstTwo from './components/first-two.vue';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
import _ from 'lodash-es';
import { handleTree } from '@/sheep/helper/utils';
const state = reactive({
style: 'second_one', // first_one一级 - 样式一), first_two二级 - 样式二), second_one二级
categoryList: [], // 商品分类树
activeMenu: 0, // 选中的一级菜单,在 categoryList 的下标
pagination: {
// 商品分页
list: [], // 商品列表
total: [], // 商品总数
pageNo: 1,
pageSize: 6,
},
loadStatus: '',
});
const { safeArea } = sheep.$platform.device;
const pageHeight = computed(() => safeArea.height - 44 - 50);
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
// 加载商品分类
async function getList() {
// API已被移除返回空数据
state.categoryList = [];
}
// 选中菜单
const onMenu = (val) => {
state.activeMenu = val;
if (state.style === 'first_one' || state.style === 'first_two') {
state.pagination.pageNo = 1;
state.pagination.list = [];
state.pagination.total = 0;
getGoodsList();
}
};
// 加载商品列表
async function getGoodsList() {
// 加载列表
state.loadStatus = 'loading';
// API已被移除返回空数据
state.pagination.list = [];
state.pagination.total = 0;
state.loadStatus = 'noMore';
}
// 加载更多商品
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGoodsList();
}
onLoad(async (params) => {
await getList();
// 首页点击分类的处理:查找满足条件的分类
const foundCategory = state.categoryList.find((category) => category.id === Number(params.id));
// 如果找到则调用 onMenu 自动勾选相应分类,否则调用 onMenu(0) 勾选第一个分类
onMenu(foundCategory ? state.categoryList.indexOf(foundCategory) : 0);
});
function handleScrollToLower() {
loadMore();
}
</script>
<style lang="scss" scoped>
.s-category {
:deep() {
.side-menu-wrap {
width: 200rpx;
height: 100%;
padding-left: 12rpx;
background-color: #f6f6f6;
position: fixed;
left: 0;
.menu-item {
width: 100%;
height: 88rpx;
position: relative;
transition: all linear 0.2s;
.menu-title {
line-height: 32rpx;
font-size: 30rpx;
font-weight: 400;
color: #333;
margin-left: 28rpx;
position: relative;
z-index: 0;
&::before {
content: '';
width: 64rpx;
height: 12rpx;
background: linear-gradient(
90deg,
var(--ui-BG-Main-gradient),
var(--ui-BG-Main-light)
) !important;
position: absolute;
left: -64rpx;
bottom: 0;
z-index: -1;
transition: all linear 0.2s;
}
}
&.menu-item-active {
background-color: #fff;
border-radius: 20rpx 0 0 20rpx;
&::before {
content: '';
position: absolute;
right: 0;
bottom: -20rpx;
width: 20rpx;
height: 20rpx;
background: radial-gradient(circle at 0 100%, transparent 20rpx, #fff 0);
}
&::after {
content: '';
position: absolute;
top: -20rpx;
right: 0;
width: 20rpx;
height: 20rpx;
background: radial-gradient(circle at 0% 0%, transparent 20rpx, #fff 0);
}
.menu-title {
font-weight: 600;
&::before {
left: 0;
}
}
}
}
}
.goods-list-box {
background-color: #fff;
width: calc(100vw - 200rpx);
padding: 10px;
margin-left: 200rpx;
}
.banner-img {
width: calc(100vw - 130px);
border-radius: 5px;
margin-bottom: 20rpx;
}
}
}
</style>

View File

@@ -1,23 +0,0 @@
<!-- 分类展示first-one 风格 -->
<template>
<view class="ss-flex-col">
<!-- 商品组件已被移除 -->
<view class="empty-message" style="text-align: center; padding: 50px; color: #999;">
暂无商品数据
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: 100%;
}
</style>

View File

@@ -1,66 +0,0 @@
<!-- 分类展示first-two 风格 -->
<template>
<view>
<view class="ss-flex flex-wrap">
<view class="goods-box" v-for="item in pagination?.list" :key="item.id">
<view @click="sheep.$router.go('/pages/goods/index', { id: item.id })">
<view class="goods-img">
<image class="goods-img" :src="item.picUrl" mode="aspectFit" />
</view>
<view class="goods-content">
<view class="goods-title ss-line-1 ss-m-b-28">{{ item.name }}</view>
<view class="goods-price">{{ fen2yuan(item.price) }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: calc((100% - 20rpx) / 2);
margin-bottom: 20rpx;
.goods-img {
width: 100%;
height: 246rpx;
border-radius: 10rpx 10rpx 0px 0px;
}
.goods-content {
width: 100%;
background: #ffffff;
box-shadow: 0px 0px 20rpx 4rpx rgba(199, 199, 199, 0.22);
padding: 20rpx 0 32rpx 16rpx;
box-sizing: border-box;
border-radius: 0 0 10rpx 10rpx;
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
}
.goods-price {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #e1212b;
}
}
&:nth-child(2n + 1) {
margin-right: 20rpx;
}
}
</style>

View File

@@ -1,80 +0,0 @@
<!-- 分类展示second-one 风格 -->
<template>
<view>
<!-- 一级分类的名字 -->
<view class="title-box ss-flex ss-col-center ss-row-center ss-p-b-30">
<view class="title-line-left" />
<view class="title-text ss-p-x-20">{{ props.data[activeMenu].name }}</view>
<view class="title-line-right" />
</view>
<!-- 二级分类的名字 -->
<view class="goods-item-box ss-flex ss-flex-wrap ss-p-b-20">
<view
class="goods-item"
v-for="item in props.data[activeMenu].children"
:key="item.id"
@tap="
sheep.$router.go('/pages/goods/list', {
categoryId: item.id,
})
"
>
<image class="goods-img" :src="item.picUrl" mode="aspectFill" />
<view class="ss-p-10">
<view class="goods-title ss-line-1">{{ item.name }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
activeMenu: [Number, String],
});
</script>
<style lang="scss" scoped>
.title-box {
.title-line-left,
.title-line-right {
width: 15px;
height: 1px;
background: #d2d2d2;
}
}
.goods-item {
width: calc((100% - 20px) / 3);
margin-right: 10px;
margin-bottom: 10px;
&:nth-of-type(3n) {
margin-right: 0;
}
.goods-img {
width: calc((100vw - 140px) / 3);
height: calc((100vw - 140px) / 3);
}
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
line-height: 40rpx;
text-align: center;
}
.goods-price {
color: $red;
line-height: 40rpx;
}
}
</style>

View File

@@ -1,547 +1,32 @@
<!-- 首页 - 重新设计 -->
<template>
<view class="container">
<s-layout
title="首页"
navbar="custom"
tabbar="/pages/index/index"
onShareAppMessage
>
<!-- 用户信息区域 - 使用 uview-plus Card 组件 -->
<u-card
v-if="isLogin"
:show-head="false"
:show-foot="false"
margin="30rpx"
>
<template #body>
<view class="user-info-section">
<view class="user-avatar">
<s-avatar
:src="userInfo.avatar"
:nickname="userInfo.nickname || userInfo.username"
:size="60"
:bg-color="getAvatarBgColor(userInfo.nickname || userInfo.username)"
@click="handleUserClick"
/>
</view>
<view class="user-details">
<u-text
:text="userInfo.nickname || '用户'"
size="18"
color="#333"
bold
/>
<u-text
:text="userInfo.mobile || '欢迎使用系统'"
size="14"
color="#999"
margin="6rpx 0 0 0"
/>
</view>
<view class="user-actions">
<u-button
text="个人中心"
size="mini"
type="primary"
plain
@click="navigateTo('/pages/index/user')"
margin="0 0 10rpx 0"
/>
<u-button
text="退出登录"
size="mini"
type="error"
plain
@click="handleLogout"
/>
</view>
</view>
</template>
</u-card>
<!-- 未登录提示区域 - 使用 uview-plus Card Button -->
<u-card
v-else
:show-head="false"
:show-foot="false"
margin="30rpx"
>
<template #body>
<view class="login-prompt">
<view class="prompt-content">
<u-text text="欢迎使用" size="20" color="#333" bold />
<u-text
text="请先登录以使用完整功能"
size="14"
color="#999"
margin="10rpx 0 30rpx 0"
/>
<u-button
text="立即登录"
type="primary"
size="normal"
@click="goToLogin"
/>
</view>
</view>
</template>
</u-card>
<!-- 功能模块入口 - 使用 uview-plus Grid 组件 -->
<u-card
v-if="isLogin"
:show-head="false"
:show-foot="false"
margin="30rpx"
>
<template #body>
<view class="function-modules">
<view class="section-title">
<u-text text="功能模块" size="16" color="#333" bold />
</view>
<u-grid :col="3" :border="false">
<u-grid-item
v-for="module in functionModules"
:key="module.id"
@click="handleModuleClick(module)"
>
<view class="module-content">
<view class="module-icon" :style="{ backgroundColor: module.color + '20' }">
<u-icon :name="module.icon" size="24" :color="module.color" />
</view>
<u-text
:text="module.name"
size="12"
color="#666"
margin="10rpx 0 0 0"
/>
</view>
</u-grid-item>
</u-grid>
</view>
</template>
</u-card>
</s-layout>
<view class="page bg-w pt30">
<up-grid :border="false" :col="gridCol">
<up-grid-item class="mb25 mt25" v-for="(item, listIndex) in list" :key="listIndex" @click="goSystem(item.url)">
<image style="width: 80px; height: 80px" :src="`/static/images/menus/${item.icon}.png`"></image>
<text class="grid-text">{{ item.name }}</text>
</up-grid-item>
</up-grid>
</view>
</template>
<script setup>
import { onLoad, onPullDownRefresh, onShow } from '@dcloudio/uni-app';
import { computed, ref } from 'vue';
import sheep from '@/sheep';
import { getAvatarBgColor } from '@/sheep/utils/avatar.js';
// 隐藏原生tabBar
uni.hideTabBar({
fail: () => {},
});
import { reactive, ref, computed, onMounted } from 'vue'
import nx from '@/nx'
import { useGridCol } from '@/nx/hooks/useGridCol'
let list = reactive([
{ url: '/pages/lims/index/index', name: '设备管理系统', icon: 'dailyCheck' },
{ url: '/pages/analysis/index/index', name: '化学分析系统', icon: 'maintain' }
])
// 跳转到登录页
function goToLogin() {
uni.navigateTo({
url: '/pages/login/index'
});
}
// const sysMenus = computed(() => nx.$store('user').sysMenus)
function goSystem(url) {
uni.reLaunch({ url })
}
const isLogin = computed(() => sheep.$store('user').isLogin);
const userInfo = computed(() => sheep.$store('user').userInfo);
// 用于防止重复验证
let isValidating = false;
// 用户头像点击事件
function handleUserClick() {
if (!isLogin.value) {
goToLogin();
return;
}
uni.navigateTo({
url: '/pages/index/user'
});
}
// 格式化最后登录时间
const formatLastLoginTime = computed(() => {
const now = new Date();
return `${now.getMonth() + 1}${now.getDate()}${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`;
});
// 功能模块数据
const functionModules = ref([
{
id: 'profile',
name: '个人资料',
icon: 'account',
color: 'var(--ui-BG-Main)', // 主题色
path: '/pages/index/user'
},
{
id: 'settings',
name: '系统设置',
icon: 'setting',
color: '#666666', // $dark-6
action: 'showSettings'
},
{
id: 'security',
name: '安全中心',
icon: 'lock',
color: '#d10019', // $red
action: 'showSecurity'
},
{
id: 'feedback',
name: '意见反馈',
icon: 'chat',
color: '#8dc63f', // $green
action: 'showFeedback'
}
]);
// 快捷操作数据
const quickActions = ref([
{
id: 'logout',
title: '退出登录',
desc: '安全退出当前账户',
icon: 'logout',
action: 'logout'
}
]);
onLoad((options) => {
console.log('首页加载完成');
checkLoginStatus();
});
onShow(() => {
// 只有在页面从后台返回前台时才需要重新验证登录状态
// 避免首次加载时的重复验证
console.log('页面显示,检查是否需要验证登录状态');
// 延迟一点时间确保不与onLoad的验证冲突
setTimeout(() => {
checkLoginStatus();
}, 100);
});
onPullDownRefresh(async () => {
console.log('下拉刷新');
try {
// 刷新用户信息
if (isLogin.value) {
await sheep.$store('user').getInfo();
console.log('用户信息刷新成功');
}
} catch (error) {
console.log('刷新用户信息失败,可能登录已过期', error);
// 如果刷新失败,可能是登录过期,重新验证登录状态
await checkLoginStatus();
} finally {
setTimeout(() => {
uni.stopPullDownRefresh();
}, 1000);
}
});
// 检查登录状态
async function checkLoginStatus() {
// 防止重复验证
if (isValidating) {
console.log('正在验证中,跳过重复验证');
return;
}
console.log('检查登录状态...');
// 如果本地显示未登录,直接跳转到登录页
if (!isLogin.value) {
console.log('本地状态未登录,跳转到登录页');
setTimeout(() => {
goToLogin();
}, 500);
return;
}
// 如果本地显示已登录需要验证token是否还有效
console.log('本地状态已登录验证token有效性...');
isValidating = true;
try {
// 尝试获取用户信息来验证token是否有效
await sheep.$store('user').getInfo();
console.log('Token有效用户已登录');
} catch (error) {
console.log('Token已失效需要重新登录', error);
// 清除本地登录状态
sheep.$store('user').logout(false);
// 延迟显示登录弹窗,确保页面渲染完成
setTimeout(() => {
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
duration: 2000
});
setTimeout(() => {
goToLogin();
}, 2000);
}, 500);
} finally {
// 验证完成,重置标志
setTimeout(() => {
isValidating = false;
}, 1000);
}
}
// 导航到指定页面
function navigateTo(path) {
uni.navigateTo({
url: path
});
}
// 处理退出登录
function handleLogout() {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
showCancel: true,
confirmText: '确定',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
try {
// 显示加载提示
uni.showLoading({
title: '退出中...',
mask: true
});
// 调用退出登录API
await sheep.$store('user').logout(true);
// 隐藏加载提示
uni.hideLoading();
// 显示退出成功提示
uni.showToast({
title: '退出成功',
icon: 'success',
duration: 1500
});
// 延迟跳转到登录页
setTimeout(() => {
goToLogin();
}, 1500);
} catch (error) {
// 隐藏加载提示
uni.hideLoading();
console.error('退出登录失败:', error);
uni.showToast({
title: '退出失败,请重试',
icon: 'none',
duration: 2000
});
}
}
}
});
}
// 处理模块点击
function handleModuleClick(module) {
if (module.path) {
navigateTo(module.path);
} else if (module.action) {
handleAction(module.action);
}
}
// 处理快捷操作点击
function handleActionClick(action) {
handleAction(action.action);
}
// 处理各种操作
function handleAction(action) {
switch (action) {
case 'showSettings':
uni.showToast({
title: '系统设置功能开发中',
icon: 'none'
});
break;
case 'showSecurity':
uni.showToast({
title: '安全中心功能开发中',
icon: 'none'
});
break;
case 'showFeedback':
uni.showToast({
title: '意见反馈功能开发中',
icon: 'none'
});
break;
case 'showAbout':
uni.showModal({
title: '关于应用',
content: '应用版本v1.0.0\n开发者Yudao Team\n更新时间2024年',
showCancel: false
});
break;
case 'scanCode':
uni.scanCode({
success: (res) => {
uni.showModal({
title: '扫描结果',
content: res.result,
showCancel: false
});
},
fail: (err) => {
uni.showToast({
title: '扫描失败',
icon: 'none'
});
}
});
break;
case 'shareApp':
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 0,
href: '',
title: '推荐一个好用的应用',
summary: '快来试试这个应用吧!',
imageUrl: ''
});
break;
case 'clearCache':
uni.showModal({
title: '清理缓存',
content: '确定要清理应用缓存吗?',
success: (res) => {
if (res.confirm) {
// 这里可以添加清理缓存的逻辑
uni.clearStorageSync();
uni.showToast({
title: '缓存清理完成',
icon: 'success'
});
}
}
});
break;
case 'logout':
handleLogout();
break;
default:
uni.showToast({
title: '功能开发中',
icon: 'none'
});
}
}
const { gridCol } = useGridCol([400, 600], [2, 3, 4])
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
background: #f5f5f5;
}
/* 用户信息区域 */
.user-info-section {
display: flex;
align-items: center;
padding: 20rpx;
}
.user-avatar {
margin-right: 20rpx;
}
.user-details {
flex: 1;
}
.user-actions {
display: flex;
flex-direction: column;
gap: 10rpx;
}
/* 未登录提示区域 */
.login-prompt {
text-align: center;
padding: 40rpx 20rpx;
}
.prompt-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
/* 功能模块 */
.function-modules {
padding: 20rpx;
}
.section-title {
margin-bottom: 20rpx;
}
.module-content {
text-align: center;
padding: 20rpx 10rpx;
}
.module-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10rpx;
}
/* 快捷操作 */
.quick-actions {
padding: 20rpx;
}
.action-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15rpx;
background: #f0f0f0;
border-radius: 8rpx;
}
/* 系统信息 */
.system-info {
padding: 20rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
}
</style>
.grid-text {
font-size: 24px;
}
</style>

View File

@@ -1,39 +0,0 @@
<!-- 微信公众号的登录回调页 -->
<template>
<!-- 空登陆页 -->
<view />
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
onLoad(async (options) => {
// #ifdef H5
// 将 search 参数赋值到 options 中,方便下面解析
new URLSearchParams(location.search).forEach((value, key) => {
options[key] = value;
});
// 执行登录 or 绑定,注意需要 await 绑定
const event = options.event;
const code = options.code;
const state = options.state;
if (event === 'login') { // 场景一:登录
await sheep.$platform.useProvider().login(code, state);
} else if (event === 'bind') { // 场景二:绑定
await sheep.$platform.useProvider().bind(code, state);
}
// 检测 H5 登录回调
let returnUrl = uni.getStorageSync('returnUrl');
if (returnUrl) {
uni.removeStorage({key:'returnUrl'});
location.replace(returnUrl);
} else {
uni.switchTab({
url: '/',
});
}
// #endif
});
</script>

81
pages/index/me-popup.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<u-popup :show="show" mode="right" @close="close" @open="open" closeable>
<view style="width: 50vw" class="p20">
<scroll-view scroll-y="true">
<view class="x-f pb20 pt20">
<u-avatar src=""></u-avatar>
<view class="pl20">您好{{ userInfo.nickname }}</view>
</view>
<u-cell-group>
<u-cell icon="grid-fill" title="切换系统" :is-link="true" @click="handleTo('/pages/index/index')" />
<u-cell icon="info-circle-fill" title="关于我们" :is-link="true" @click="handleTo('/pages/me/aboutMe')" />
</u-cell-group>
<u-button class="mt40" type="warning" plain text="退出当前账号" @click="handleLoginOut('general')" />
</scroll-view>
</view>
<u-modal
width="250px"
:show="modalShow"
title="提示"
content="确定退出登录吗?"
show-cancel-button
async-close
@confirm="confirm"
@cancel="modalShow = false"
/>
</u-popup>
</template>
<script setup>
import { ref, computed } from 'vue'
import nx from '@/nx'
// Props & Emits
const props = defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:show', 'open', 'close'])
// 响应式数据
const modalShow = ref(false)
// 计算属性
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const handleTo = url => {
nx.$router.go(url)
}
const handleLoginOut = () => {
modalShow.value = true
}
const confirm = async () => {
try {
await nx.$store('user').logout()
} catch (error) {
console.error('退出登录失败:', error)
} finally {
modalShow.value = false
}
}
const open = () => {
emit('open')
}
const close = () => {
emit('close')
emit('update:show', false)
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,266 +0,0 @@
<!-- 菜单页面 -->
<template>
<s-layout
title="菜单"
tabbar="/pages/index/menu"
navbar="custom"
onShareAppMessage
>
<view class="menu-container">
<!-- 轮播图 -->
<view class="swiper-section">
<swiper
class="swiper-container"
:indicator-dots="true"
:autoplay="true"
:interval="3000"
:duration="500"
indicator-color="rgba(255, 255, 255, 0.3)"
indicator-active-color="#fff"
circular
>
<swiper-item v-for="(item, index) in swiperList" :key="index">
<view class="swiper-item">
<image :src="item.image" mode="aspectFill" class="swiper-image" />
</view>
</swiper-item>
</swiper>
</view>
<!-- 工作台 -->
<view class="section">
<view class="section-title">工作台</view>
<view class="grid-container">
<view
class="grid-item"
v-for="(item, index) in quickMenuList"
:key="index"
@click="handleMenuClick(item)"
>
<view class="item-icon" :style="{ background: item.bg }">
<u-icon
:name="item.icon"
size="24"
:color="item.color"
/>
</view>
<view class="item-title">{{ item.title }}</view>
</view>
</view>
</view>
</view>
</s-layout>
</template>
<script>
import sheep from '@/sheep';
export default {
onShow() {
const userStore = sheep.$store('user');
if (!userStore.isLogin) {
sheep.$router.redirect('/pages/login/index');
}
},
data() {
return {
// 轮播图数据
swiperList: [
{
image: '/static/images/swiper/bg1.png',
title: '轮播图1'
},
{
image: '/static/images/swiper/bg2.png',
title: '轮播图2'
},
{
image: '/static/images/swiper/bg3.png',
title: '轮播图3'
}
],
// 工作台功能数据
quickMenuList: [
{
title: 'Demo',
icon: 'file-text',
color: '#fff',
bg: '#3c9cff',
path: '/pages/app/democontract/index'
},
]
};
},
methods: {
// 处理菜单点击
handleMenuClick(item) {
sheep.$router.go(item.path);
}
}
};
</script>
<style lang="scss" scoped>
.menu-container {
min-height: 100vh;
background-color: #f8f9fa;
padding: 20rpx;
}
/* 轮播图样式 */
.swiper-section {
margin-bottom: 30rpx;
.swiper-container {
height: 300rpx;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.swiper-item {
width: 100%;
height: 100%;
.swiper-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
.section {
margin-bottom: 40rpx;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
margin-bottom: 30rpx;
padding-left: 10rpx;
}
}
/* 快捷功能网格布局 */
.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
.grid-item {
background: #fff;
border-radius: 16rpx;
padding: 30rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.item-icon {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.item-title {
font-size: 24rpx;
color: #606266;
text-align: center;
line-height: 1.2;
}
}
}
/* 管理功能列表布局 */
.list-container {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1px solid #f5f5f5;
transition: background-color 0.3s ease;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f8f9fa;
}
.item-left {
display: flex;
align-items: center;
flex: 1;
.item-icon-small {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.item-info {
flex: 1;
.item-title {
font-size: 28rpx;
color: #303133;
margin-bottom: 8rpx;
font-weight: 500;
}
.item-desc {
font-size: 22rpx;
color: #909399;
line-height: 1.3;
}
}
}
}
}
/* 响应式调整 */
@media screen and (max-width: 750rpx) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.grid-item {
padding: 24rpx 16rpx;
.item-icon {
width: 64rpx;
height: 64rpx;
}
.item-title {
font-size: 22rpx;
}
}
}
</style>

View File

@@ -1,51 +0,0 @@
<!-- 自定义页面支持装修 -->
<template>
<s-layout
:title="state.name"
navbar="custom"
:bgStyle="state.page"
:navbarStyle="state.navigationBar"
onShareAppMessage
showLeftButton
>
<s-block v-for="(item, index) in state.components" :key="index" :styles="item.property.style">
<s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
</s-block>
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import { onLoad, onPageScroll } from '@dcloudio/uni-app';
// Diy API removed with promotion module
const state = reactive({
name: '',
components: [],
navigationBar: {},
page: {},
});
onLoad(async (options) => {
let id = options.id
// #ifdef MP
// 小程序预览自定义页面
if (options.scene) {
const sceneParams = decodeURIComponent(options.scene).split('=');
id = sceneParams[1];
}
// #endif
const { code, data } = await DiyApi.getDiyPage(id);
if (code === 0) {
state.name = data.name;
state.components = data.property?.components;
state.navigationBar = data.property?.navigationBar;
state.page = data.property?.page;
}
});
onPageScroll(() => {});
</script>
<style></style>

View File

@@ -1,144 +0,0 @@
<!-- 搜索界面 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" class="set-wrap" title="搜索">
<view class="search-container">
<view class="search-input-wrapper">
<u-search
v-model="searchText"
placeholder="请输入关键字"
:show-action="false"
:focus="true"
shape="square"
@search="onSearch"
@confirm="onSearch"
/>
</view>
<!-- 搜索历史标题 -->
<view class="search-section">
<view class="section-header">
<u-text text="搜索历史" size="16" bold color="#333" />
<u-button
text="清除搜索历史"
size="mini"
type="error"
plain
@click="onDelete"
/>
</view>
<!-- 历史标签 -->
<view class="history-tags" v-if="state.historyList.length">
<u-tag
v-for="(item, index) in state.historyList"
:key="index"
:text="item"
type="info"
plain
size="medium"
closable
@click="onSearch(item)"
@close="removeHistoryItem(index)"
:custom-style="{ margin: '5rpx 10rpx 5rpx 0' }"
/>
</view>
<!-- 无历史提示 -->
<u-empty
v-else
mode="search"
text="暂无搜索历史"
textSize="14"
/>
</view>
</view>
</s-layout>
</template>
<script setup>
import { reactive, ref } from 'vue';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
const searchText = ref('');
const state = reactive({
historyList: [],
});
// 搜索
function onSearch(keyword = '') {
const searchKeyword = keyword || searchText.value;
if (!searchKeyword) {
return;
}
saveSearchHistory(searchKeyword);
// 前往商品列表(带搜索条件)
sheep.$router.go('/pages/goods/list', { keyword: searchKeyword });
}
// 移除单个历史记录
function removeHistoryItem(index) {
state.historyList.splice(index, 1);
uni.setStorageSync('searchHistory', state.historyList);
}
// 保存搜索历史
function saveSearchHistory(keyword) {
// 如果关键词在搜索历史中,则把此关键词先移除
if (state.historyList.includes(keyword)) {
state.historyList.splice(state.historyList.indexOf(keyword), 1);
}
// 置顶关键词
state.historyList.unshift(keyword);
// 最多保留 10 条记录
if (state.historyList.length >= 10) {
state.historyList.length = 10;
}
uni.setStorageSync('searchHistory', state.historyList);
}
function onDelete() {
uni.$u.modal({
title: '提示',
content: '确认清除搜索历史吗?',
showCancelButton: true,
confirmText: '确定',
cancelText: '取消'
}).then(res => {
if (res) {
state.historyList = [];
uni.removeStorageSync('searchHistory');
}
});
}
onLoad(() => {
state.historyList = uni.getStorageSync('searchHistory') || [];
});
</script>
<style lang="scss" scoped>
.search-container {
padding: 30rpx;
}
.search-input-wrapper {
margin-bottom: 40rpx;
}
.search-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.history-tags {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
}
</style>

View File

@@ -1,258 +0,0 @@
<!-- 个人中心 -->
<template>
<s-layout
title="我的"
tabbar="/pages/index/user"
navbar="custom"
onShareAppMessage
>
<view class="user-container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-info">
<view class="avatar-section" @click="handleAvatarClick">
<s-avatar
:src="userInfo.avatar"
:nickname="userInfo.nickname || userInfo.username"
:size="80"
:bg-color="getAvatarBgColor(userInfo.nickname || userInfo.username)"
/>
<view class="user-details">
<view class="user-name">{{ userInfo.nickname || userInfo.username || '未登录' }}</view>
<view class="user-desc">{{ userInfo.mobile || '点击登录获取更多功能' }}</view>
</view>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-group">
<view class="group-title">账户管理</view>
<view class="menu-list">
<view class="menu-item" @click="goToUserInfo">
<view class="item-left">
<view class="item-icon" style="background: var(--ui-BG-Main-opacity-1);">
<u-icon name="account" size="20" color="var(--ui-BG-Main)"/>
</view>
<view class="item-title">个人信息</view>
</view>
<u-icon name="arrow-right" size="16" color="#c0c4cc"/>
</view>
<view class="menu-item" @click="goToAbout">
<view class="item-left">
<view class="item-icon" style="background: var(--ui-BG-Main-opacity-1);">
<u-icon name="info-circle" size="20" color="var(--ui-BG-Main)"/>
</view>
<view class="item-title">关于我们</view>
</view>
<u-icon name="arrow-right" size="16" color="#c0c4cc"/>
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section" v-if="isLogin">
<view class="logout-btn" @click="handleLogout">
退出登录
</view>
</view>
</view>
</view>
</s-layout>
</template>
<script setup>
import { computed } from 'vue';
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { getAvatarBgColor } from '@/sheep/utils/avatar.js';
// 用户信息
const userInfo = computed(() => sheep.$store('user').userInfo);
const isLogin = computed(() => sheep.$store('user').isLogin);
// 页面事件
onShow(() => {
// 先检查登录状态,未登录则会自动跳转到登录页
if (!sheep.$store('user').isLogin) {
return;
}
// 已登录时更新用户数据
sheep.$store('user').updateUserData();
});
onPullDownRefresh(() => {
sheep.$store('user').updateUserData();
setTimeout(() => {
uni.stopPullDownRefresh();
}, 1000);
});
// 头像点击事件
function handleAvatarClick() {
// 跳转到个人信息页面编辑头像
uni.navigateTo({
url: '/pages/user/info'
});
}
// 方法
function goToUserInfo() {
uni.navigateTo({
url: '/pages/user/info'
});
}
function goToAbout() {
uni.navigateTo({
url: '/pages/public/about'
});
}
function handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
sheep.$store('user').logout();
}
}
});
}
</script>
<style lang="scss" scoped>
.user-container {
min-height: 100vh;
background-color: #f8f9fa;
padding: 20rpx;
}
/* 用户信息卡片 */
.user-card {
background: var(--gradient-diagonal-primary, linear-gradient(135deg, #0055A2, rgba(0, 85, 162, 0.6)));
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 85, 162, 0.3);
.user-info {
display: flex;
align-items: center;
justify-content: space-between;
.avatar-section {
display: flex;
align-items: center;
flex: 1;
.user-details {
margin-left: 20rpx;
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-bottom: 8rpx;
}
.user-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
}
}
.user-actions {
padding: 10rpx;
}
}
}
/* 菜单区域 */
.menu-section {
.menu-group {
margin-bottom: 30rpx;
.group-title {
font-size: 28rpx;
font-weight: 600;
color: #303133;
margin-bottom: 20rpx;
padding-left: 10rpx;
}
.menu-list {
background: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1px solid #f5f5f5;
transition: background-color 0.3s ease;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f8f9fa;
}
.item-left {
display: flex;
align-items: center;
flex: 1;
.item-icon {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.item-title {
font-size: 28rpx;
color: #303133;
font-weight: 500;
}
}
}
}
}
}
/* 退出登录按钮 */
.logout-section {
margin-top: 40rpx;
.logout-btn {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
text-align: center;
font-size: 28rpx;
color: #f56c6c;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
background-color: #fef0f0;
transform: translateY(2rpx);
}
}
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<uni-card style="height: 100%" spacing="0">
<view class="content">
<up-row class="flex-wrap">
<up-col :span="gridCol"
>使用部门
<text class="value">{{ acceptInfo.useDept }}</text>
</up-col>
<up-col :span="gridCol"
>到货日期
<text class="value">{{ acceptInfo.deviceCode }}</text>
</up-col>
<up-col :span="gridCol"
>安装调试
<text class="value">{{ acceptInfo.installStatus }}</text>
</up-col>
<up-col :span="gridCol"
>验收结果
<text class="value">{{ acceptInfo.acceptResult }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</view>
</uni-card>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeMount } from 'vue'
import { acceptDetailList } from '@/nx/api/deviceInfo'
import { useGridCol } from '@/nx/hooks/useGridCol'
import nx from '@/nx'
const { gridCol } = useGridCol([700], [6, 4])
const acceptSchema = [
{
label: '使用部门',
field: 'useDept'
},
{
label: '到货日期',
field: 'arrivalDate'
},
{
label: '验收日期',
field: 'acceptDate'
},
{
label: '安装调试',
field: 'installStatus'
},
{
label: '验收结果',
field: 'acceptResult'
},
{
label: '验收状态',
field: 'acceptStatus'
},
{
label: '备注',
field: 'remark'
}
]
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
getDetailInfo(id)
})
let acceptInfo = ref({})
let commentWf = ref([])
async function getDetailInfo(id) {
const res = await acceptDetailList({
deviceBusInfoId: id
})
if (res.length > 0) {
let info = res[0]
acceptInfo.value = info
if (info.commentJson) {
try {
commentWf.value = JSON.parse(info.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
color: #000;
.u-col {
padding: 10px;
}
.value {
color: #666;
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备借用单"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row class="flex-wrap">
<up-col :span="gridCol"
>借用人
<text class="value">{{ detailInfo.borrowOper }}</text>
</up-col>
<up-col :span="gridCol"
>借用部门
<text class="value">{{ detailInfo.borrowDept }}</text>
</up-col>
<up-col :span="gridCol"
>借用日期
<text class="value">{{ detailInfo.borrowDate }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="6"
>计划归还日期
<text class="value">{{ detailInfo.planGivebackDate }}</text>
</up-col>
<up-col span="6"
>借用原因
<text class="value">{{ detailInfo.borrowReason }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>备注
<text class="value">{{ detailInfo.remark }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
let commentWf = ref([])
function handleOpen() {
detailInfo.value = props.checkInfo
if (props.checkInfo.commentJson) {
try {
commentWf.value = JSON.parse(props.checkInfo.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 80vh;
width: 80vw;
padding: 20px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

109
pages/lims/borrow/list.vue Normal file
View File

@@ -0,0 +1,109 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<borrow-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import BorrowDetailPopup from './detail.vue'
import { borrowList } from '@/nx/api/deviceInfo'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '借用人',
name: 'borrowOper',
width: 120
},
{
label: '借用部门',
name: 'borrowDept',
width: 140
},
{
label: '借用日期',
name: 'borrowDate',
width: 120
},
{
label: '借用原因',
name: 'borrowReason',
width: 180
},
{
label: '计划归还日期',
name: 'planGivebackDate',
width: 140
},
{
label: '备注',
name: 'remark',
width: 140
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceBusInfoId: deviceId.value,
borrowStatus: '已借出'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: borrowList
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,9 @@
import request from '@/nx/request'
export function list(params) {
return request({
url: '/lims/bus/deviceBusCalibration/queryPageList',
method: 'GET',
params
})
}

View File

@@ -0,0 +1,144 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备检定/校准信息"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row class="flex-wrap">
<up-col :span="gridCol"
>仪器设备名称
<text class="value">{{ detailInfo.deviceName }}</text>
</up-col>
<up-col span="4"
>别名
<text class="value">{{ detailInfo.alias }}</text>
</up-col>
<up-col span="4"
>生产厂家
<text class="value">{{ detailInfo.manufacturer }}</text>
</up-col>
<up-col :span="gridCol"
>规格型号
<text class="value">{{ detailInfo.modelNo }}</text>
</up-col>
</up-row>
<up-row class="flex-wrap">
<up-col :span="gridCol"
>设备编号
<text class="value">{{ detailInfo.deviceCode }}</text>
</up-col>
<up-col :span="gridCol"
>统一编号
<text class="value">{{ detailInfo.deviceCode }}</text>
</up-col>
<up-col :span="gridCol"
>检定/校准单位
<text class="value">{{ detailInfo.agent }}</text>
</up-col>
</up-row>
<up-row class="flex-wrap">
<up-col :span="gridCol"
>检定/校准日期
<text class="value">{{ detailInfo.checkDate }}</text>
</up-col>
<up-col :span="gridCol"
>证书编号
<text class="value">{{ detailInfo.certificateCode }}</text>
</up-col>
<up-col :span="gridCol"
>确认结果
<text class="value">{{ detailInfo.checkResult }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>确认情况(理由)
<up-parse style="background: #f3f4f6" :content="processedContent"></up-parse>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>备注
<text class="value">{{ detailInfo.checkRemark }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { getImgBaseUrl } from '@/defaultBaseUrl'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
// / 处理富文本内容
const processedContent = computed(() => {
if (!detailInfo.value.checkContent) {
return ''
}
return detailInfo.value.checkContent.replace(
/<img([^>]+?)src="((?!http)[^"]*?)(file\/[^"]*)"/gi,
(match, attributes, prefix, filePath) => {
return `<img${attributes}src="${getImgBaseUrl()}/${filePath}"`
}
)
})
let commentWf = ref([])
function handleOpen() {
detailInfo.value = props.checkInfo
if (props.checkInfo.commentJson) {
try {
commentWf.value = JSON.parse(props.checkInfo.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 80vh;
width: 90vw;
padding: 10px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<calibration-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import CalibrationDetailPopup from './detail.vue'
import { list } from './calibration.api'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '检定/校准日期',
name: 'checkDate',
width: 140
},
{
label: '确认人',
name: 'checkPersonName',
width: 200
},
{
label: '检定/校准周期',
name: 'frequencyRemark',
width: 200
},
{
label: '检定/校准单位',
name: 'agent',
width: 200
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceId: deviceId.value,
effectiveFlag: '1',
wfStatus: 'finished'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: list
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,32 @@
import { ref, reactive } from 'vue'
export const detailSchema = [
{ label: '设备名称', value: 'deviceName' },
{ label: '别名', value: 'alias' },
{ label: '设备型号', value: 'modelNo' },
{ label: '设备编码', value: 'deviceCode' },
{ label: '生产厂商', value: 'manufacturer' },
{ label: '使用班组', value: 'deptName' }
]
export const column = reactive([
{
label: '点检人',
name: 'checkUserName',
width: 160
},
{
label: '点检日期',
name: 'checkDate',
width: 180
},
{
label: '备注',
name: 'content',
width: 200
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])

View File

@@ -0,0 +1,138 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="18px" type="line" title="设备点检信息"> </uni-section>
<scroll-view scroll-y="true" style="height: 85vh; width: 90vw">
<view class="content">
<up-row class="flex-wrap p10" style="background-color: #f5f7fa">
<up-col class="mb10" :span="gridCol" v-for="(item, index) in detailSchema">
<view style="color: #666"
><span style="color: #333">{{ item.label }}</span>{{ detailInfo[item.value] }}</view
>
</up-col>
</up-row>
<view class="p5">
<view class="pt5"
>备注
<text style="color: #666">
{{ detailInfo.content }}
</text>
</view>
<up-row>
<up-col span="6">
<view class="pt5"
>点检人
<text style="color: #666">
{{ detailInfo.checkUserName }}
</text>
</view>
<view class="pt5"
>点检日期
<text style="color: #666">
{{ detailInfo.checkDate }}
</text>
</view>
</up-col>
<up-col span="6">
<up-album multiple-size="70" single-size="70" :urls="attachment" row-count="4"> </up-album>
</up-col>
</up-row>
</view>
<view>
<up-row class="font-bold" style="background-color: #f5f5f5">
<up-col span="5">点检项目</up-col>
<up-col span="3">检查标准</up-col>
<up-col span="2"> 频次</up-col>
<up-col span="2">是否正常</up-col>
</up-row>
<view class="pt10">
<up-row class="pb10" v-for="(item, index) in detailInfo.maintainItemList" :key="index">
<up-col span="5">
<view class="x-f">
<text class="pl10">
{{ item.itemName }}
</text>
</view>
</up-col>
<up-col span="3">{{ item.standard }}</up-col>
<up-col span="2">{{ item.frequencyRemark }}</up-col>
<up-col span="2">
<u-tag v-if="item.checkResult == '正常'" text="正常" size="mini" type="success" plain plainFill></u-tag>
<u-tag v-else text="不正常" type="error" size="mini" plain plainFill></u-tag>
</up-col>
</up-row>
</view>
<view class="p10"></view>
</view>
</view>
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { detailSchema } from './dailyCheck.data'
import dailyCheckApi from '@/nx/api/dailyCheck'
import { getImgBaseUrl } from '@/defaultBaseUrl'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const attachment = ref([])
async function getDetailInfo(id) {
const res = await dailyCheckApi.queryById(id)
res.maintainItemList.forEach(item => {
if (item.checkResult == 'false' || !item.checkResult) {
item.checkResult = false
}
if (item.checkResult == 'true') {
item.checkResult = true
}
})
detailInfo.value = res
attachment.value = []
let files = res?.attachment?.split(',') || []
if (files.length > 0) {
attachment.value = files.map(item => getImgBaseUrl() + item)
}
}
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
function handleOpen() {
getDetailInfo(props.checkInfo.id)
}
</script>
<style lang="scss" scoped>
.content {
background-color: #fff;
height: 100%;
padding: 16px;
padding-top: 10px;
font-size: 16px;
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

View File

@@ -0,0 +1,293 @@
<template>
<view>
<up-sticky>
<navbar-back title="点检">
<up-button
v-if="detailInfo.id"
type="primary"
:plain="true"
icon="list"
size="small"
text="设备点检记录"
@click="handleCheckRecord"
></up-button>
</navbar-back>
</up-sticky>
<view class="container">
<n-scanTemp
v-if="!detailInfo.id"
title="请扫描设备条码进行点检"
icon="dailyCheck"
@deviceId="id => getDailyCheckRecord(id)"
/>
<view v-else class="content">
<view>
<uni-section titleFontSize="22px" type="line" title="设备点检信息">
<template v-slot:right>
<up-button
v-if="detailInfo.submitFlag == '1'"
type="success"
text="新建点检"
@click="handleCreateDailyCheck"
></up-button>
</template>
</uni-section>
<up-row class="flex-wrap p10" style="background-color: #f5f7fa">
<up-col class="mb10" :span="gridCol" v-for="(item, index) in detailSchema">
<view style="color: #666"
><span style="color: #333">{{ item.label }}</span>{{ detailInfo[item.value] }}</view
>
</up-col>
</up-row>
</view>
<view>
<uni-section titleFontSize="22px" type="line" title="检查项"> </uni-section>
<up-row class="p10 font-bold" style="background-color: #f5f5f5">
<up-col span="4">点检项目</up-col>
<up-col span="3">检查标准</up-col>
<up-col span="2"> 频次</up-col>
<up-col style="text-align: center" span="3">是否正常</up-col>
</up-row>
<view class="pt10">
<up-row class="pb12" v-for="(item, index) in detailInfo.maintainItemList" :key="index">
<up-col span="4">
<view class="x-f">
<u-badge type="warning " :value="index + 1"></u-badge>
<text class="pl10">
{{ item.itemName }}
</text>
</view>
</up-col>
<up-col span="3">{{ item.standard }}</up-col>
<up-col span="2">{{ item.frequencyRemark }}</up-col>
<up-col span="3">
<u-radio-group v-model="item.checkResult" placement="row">
<u-radio activeColor="green" label="正常" name="正常"></u-radio>
<u-radio activeColor="red" label="不正常" name="不正常"></u-radio>
</u-radio-group>
</up-col>
</up-row>
</view>
</view>
<view>
<uni-section titleFontSize="22px" type="line" title="备注"> </uni-section>
<up-textarea v-model="detailInfo.content" placeholder="请输入内容"></up-textarea>
<view class="p10">附件照片</view>
<n-upload v-model="detailInfo.attachment" />
<view class="p10">点检人</view>
<up-input v-model="detailInfo.checkUserName"></up-input>
<view v-if="detailInfo.submitFlag == '1'">
<view class="p10">点检日期</view>
<uni-datetime-picker type="datetime" v-model="detailInfo.checkDate" />
</view>
</view>
<view class="mt40 x-bc" v-if="detailInfo.submitFlag == '0'">
<up-button
style="width: 40%"
loadingText="保存中..."
type="warning"
text="暂存"
@click="handleSubmit('0')"
></up-button>
<up-button
style="width: 40%"
loadingText="提交中..."
type="primary"
text="提交"
@click="handleSubmit('1')"
></up-button>
</view>
</view>
</view>
<up-modal
:show="modalShow"
title="提示"
:content="modalText"
ref="uModal"
:asyncClose="true"
showCancelButton
@confirm="confirm"
@cancel="modalShow = false"
></up-modal>
<up-loading-page :loading="pageLoading"></up-loading-page>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, toRefs, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import dailyCheckApi from '@/nx/api/dailyCheck'
import { detailSchema } from './dailyCheck.data'
import { useScreenOrientation } from '@/nx/hooks/useScreenOrientation'
import { useGridCol } from '@/nx/hooks/useGridCol'
import nx from '@/nx'
const { gridCol } = useGridCol([700], [6, 4])
const { lockOrientation } = useScreenOrientation()
const pageLoading = ref(false)
let detailInfo = ref({})
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/lims/deviceBusDailyCheck/index') {
try {
const codeObj = JSON.parse(newVal)
if (!pageLoading.value) {
getDailyCheckRecord(codeObj.id)
}
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描设备码',
icon: 'none'
})
}
}
})
const modalText = computed(() => {
return `确定${modalType.value == '0' ? '暂存' : '提交'}吗?${modalType.value == '0' ? '' : '提交后不能修改'} `
})
onShow(() => {
scanQRInfo.value = ''
})
let goBack = ref(false)
onLoad(options => {
if (options.deviceId) {
goBack.value = true
getDailyCheckRecord(options.deviceId)
}
})
async function getDailyCheckRecord(id) {
pageLoading.value = true
const res = await dailyCheckApi
.getCheckRecord({
deviceId: id,
dataType: 'dailyCheck'
})
.finally(() => {
pageLoading.value = false
})
if (!res.checkUserName) {
res.checkUserName = nx.$store('user').userInfo.realname
res.checkUserId = nx.$store('user').userInfo.id
}
detailInfo.value = res
modalType.value = res.submitFlag
lockOrientation('landscape')
}
const modalShow = ref(false)
const modalType = ref('')
// 是否有异常项
const hasAbnormal = computed(() => {
if (modalType.value == '1') {
const items = detailInfo.value.maintainItemList || []
for (let i = 0; i < items.length; i++) {
const checkResult = items[i].checkResult
if (checkResult == '不正常') {
return true
}
}
}
return false
})
function handleSubmit(type) {
// 新增维护保养项验证
if (type == '1') {
const items = detailInfo.value.maintainItemList || []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const itemNumber = `${i + 1}`
if (!item.checkResult?.trim()) {
return uni.showToast({ title: `${itemNumber}设备检查状态未选择`, icon: 'none' })
}
}
}
if (!detailInfo.value.checkUserName) {
return uni.showToast({
title: '请输入点检人',
icon: 'none'
})
}
detailInfo.value.checkDate = nx.$dayjs().format('YYYY-MM-DD HH:mm:ss')
modalType.value = type
modalShow.value = true
console.log(detailInfo.value)
}
const submitLoading = ref(false)
async function confirm() {
if (submitLoading.value) return
submitLoading.value = true
await dailyCheckApi.submit({ ...detailInfo.value, submitFlag: modalType.value }).finally(() => {
submitLoading.value = false
modalShow.value = false
reset()
})
if (goBack.value && modalType.value == 1) {
uni.navigateBack()
}
}
function reset() {
detailInfo.value = {}
}
function handleCheckRecord() {
let deviceInfo = {
id: detailInfo.value.deviceId,
deviceName: detailInfo.value.deviceName
}
nx.$store('biz').deviceInfo = deviceInfo
nx.$router.go('/pages/lims/deviceBusDailyCheck/list')
}
// 新建点检
function handleCreateDailyCheck() {
uni.showModal({
title: '提示',
content: '确定新建点检吗?',
success: async function (res) {
if (res.confirm) {
const res = await dailyCheckApi.createDailyCheck({
deviceId: detailInfo.value.deviceId,
dataType: 'dailyCheck'
})
res.checkUserName = nx.$store('user').userInfo.realname
res.checkUserId = nx.$store('user').userInfo.id
res.checkDate = nx.$dayjs().format('YYYY-MM-DD HH:mm:ss')
detailInfo.value = res
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
.container {
.content {
background-color: #fff;
height: 100%;
padding: 16px;
padding-top: 10px;
font-size: 18px;
.title {
font-size: 22px;
font-weight: bold;
}
:deep(.u-checkbox) {
justify-content: center;
}
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<view>
<up-sticky v-if="!isComponent">
<navbar-back title="点检记录"> </navbar-back>
</up-sticky>
<uni-card spacing="0">
<uni-section v-if="!isComponent" titleFontSize="20px" type="line" :title="deviceText"> </uni-section>
<view :style="{ height: isComponent ? '70vh' : '72vh' }">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<daily-check-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import DailyCheckDetailPopup from './detail.vue'
import dailyCheckApi from '@/nx/api/dailyCheck'
import { useListData } from '@/nx/hooks/usePageListData'
import { column } from './dailyCheck.data'
import nx from '@/nx'
let props = defineProps({
isComponent: {
type: Boolean,
default: false
}
})
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
dataType: 'dailyCheck',
deviceId: deviceId.value
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: dailyCheckApi.list
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<view class="container">
<up-sticky>
<navbar-back :title="`${deviceText})设备信息`"></navbar-back>
</up-sticky>
<up-tabs lineWidth="30" :list="tabs" @click="tabClick"></up-tabs>
<scroll-view scroll-y="true" class="content">
<BaseInfoCard v-if="activeIndex == 0" @getDeviceInfo="getDeviceInfo" />
<AcceptDetail v-if="activeIndex == 1" />
<DailyCheckList v-if="activeIndex == 2" isComponent />
<MaintainList v-if="activeIndex == 3" isComponent />
<UseRecordList v-if="activeIndex == 4" isComponent />
<PeriodCheckList v-if="activeIndex == 5" />
<CalibrationList v-if="activeIndex == 6" />
<RepairList v-if="activeIndex == 7" />
<BorrowList v-if="activeIndex == 8" />
<GivebackList v-if="activeIndex == 9" />
<StopList v-if="activeIndex == 10" />
<DocumentList v-if="activeIndex == 11" />
<ScrapInfo v-if="activeIndex == 12 && scrapFlag == 1" />
</scroll-view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import nx from '@/nx'
import BaseInfoCard from '@/pages/lims/deviceBusInfo/baseInfoCard'
import AcceptDetail from '@/pages/lims/accept/detail'
import DailyCheckList from '@/pages/lims/deviceBusDailyCheck/list'
import MaintainList from '@/pages/lims/deviceBusMaintain/list'
import UseRecordList from '@/pages/lims/deviceBusUseRecord/list'
import PeriodCheckList from '@/pages/lims/periodCheckList/index'
import CalibrationList from '@/pages/lims/calibrationList/index'
import RepairList from '@/pages/lims/repair/list'
import BorrowList from '@/pages/lims/borrow/list'
import GivebackList from '@/pages/lims/giveback/list'
import StopList from '@/pages/lims/stop/list'
import DocumentList from '@/pages/lims/documentList/index'
import ScrapInfo from '@/pages/lims/scrap/detail'
import { tabList } from './deviceBusInfo.data'
let activeIndex = ref(0)
let deviceText = ref('')
const detailId = ref('')
const { id, deviceName, scrapFlag } = nx.$store('biz').deviceInfo
onMounted(() => {
detailId.value = id
})
const getDeviceInfo = info => {
deviceText.value = info.deviceName + (info.alias ? `[${info.alias}]` : '')
}
const tabs = computed(() => {
return tabList.filter((item, index) => {
if (scrapFlag != 1) {
return item.name != '报废信息'
} else {
return item
}
})
})
function tabClick(e) {
activeIndex.value = e.index
if (activeIndex.value == 0) {
getDeviceInfo()
}
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
.container {
background-color: #f0f2f5;
height: 100%;
.content {
height: 75vh;
}
}
:deep(.u-tabs__wrapper__nav__item__text) {
font-size: 18px;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<uni-card spacing="0">
<view class="detail-content">
<up-row align="top">
<template v-for="item in deviceSchema">
<up-col :span="item.span ? item.span : 4">
<view class="x-f detail-item" :style="item.itemStyle">
<view class="detail-label" :style="item.labelStyle">{{ item.label }}</view>
<template v-if="item.field == 'photo'">
<view style="height: 50px">
<up-album multipleSize="50" singleSize="50" maxCount="2" :urls="imgs"></up-album>
</view>
</template>
<text v-else class="detail-value" :style="item.valueStyle">{{ detailInfo[item.field] }}</text>
</view>
</up-col>
</template>
</up-row>
</view>
</uni-card>
<up-loading-page :loading="pageLoading"></up-loading-page>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import nx from '@/nx'
import { getDeviceBusInfoById } from '@/nx/api/deviceInfo'
import { getImgBaseUrl } from '@/defaultBaseUrl'
import { deviceSchema } from './deviceBusInfo.data'
const detailId = ref('')
const pageLoading = ref(false)
const emit = defineEmits(['getDeviceInfo'])
onMounted(() => {
const { id } = nx.$store('biz').deviceInfo
detailId.value = id
getDeviceInfo()
})
let detailInfo = ref({})
let imgs = ref([])
// 获取设备详情
async function getDeviceInfo() {
pageLoading.value = true
const res = await getDeviceBusInfoById(detailId.value).finally(() => {
pageLoading.value = false
})
detailInfo.value = (data => {
const statuses = [
data.repairFlag == 1 && '维修',
data.demoteFlag == 1 && '降级',
data.scrapFlag == 1 && '报废',
data.disableFlag == 1 && '停用',
data.lendFlag == 1 && '外借'
].filter(Boolean) // 去掉 false 值
if (data.acceptFlag != 'finished') {
data.deviceStatus = '--'
data.inUseFlag = '--'
} else {
data.deviceStatus = statuses.length > 0 ? statuses.join('、') : '正常'
if (data.inUseFlag == '1') {
data.inUseFlag = '是'
} else {
data.inUseFlag = '否'
}
}
switch (data.acceptFlag) {
case '0':
data.acceptFlag = '待验收'
break
case 'running':
data.acceptFlag = '验收审批中'
break
case 'finished':
data.acceptFlag = '已验收'
break
default:
data.acceptFlag = '--'
}
return data
})(res)
imgs.value = getFileList(res.photo)
emit('getDeviceInfo', detailInfo.value)
}
function getFileList(photo) {
if (photo) {
let fileIds = photo.split(',').map(item => getImgBaseUrl() + item)
return fileIds
} else {
return []
}
}
</script>
<style scoped>
.detail-content {
border: 0.5px solid #f0f0f0;
width: 100%;
.detail-item {
display: flex;
align-items: center;
padding: 10px 6px;
box-sizing: border-box;
font-size: 16px;
border: 0.5px solid #f0f0f0;
.detail-label {
color: #000;
}
.detail-value {
font-size: 14px;
width: 72%;
overflow: auto;
}
}
}
:deep(.u-row) {
flex-wrap: wrap;
width: 100%;
}
</style>

View File

@@ -0,0 +1,104 @@
import { ref } from 'vue'
export function getColumn(isFold) {
return [
{
label: '设备名称',
name: 'deviceName',
width: isFold ? 200 : 130,
fixed: true
},
{
label: '别名',
name: 'alias',
width: isFold ? 120 : 90,
fixed: true
},
{
label: '设备状态',
name: 'stateShow',
width: isFold ? 120 : 90
},
{
label: '使用状态',
name: 'inUseFlag',
width: isFold ? 120 : 90
},
{
label: '规格型号',
name: 'modelNo',
width: isFold ? 160 : 130
},
{
label: '使用班组',
name: 'deptName',
width: isFold ? 140 : 100
},
{
label: '负责人',
name: 'managerUserName',
width: isFold ? 100 : 90
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '设备信息', func: 'detail' }]
}
]
}
export const tabList = [
{ name: '基础信息' },
{ name: '验收信息' },
{ name: '点检' },
{ name: '维护保养' },
{ name: '使用记录' },
{ name: '期间核查' },
{ name: '检定/校准' },
{ name: '维修记录' },
{ name: '借用记录' },
{ name: '归还记录' },
{ name: '停用记录' },
{ name: '设备文档' },
{ name: '报废信息' }
]
export const deviceSchema = [
{ label: '设备名称', field: 'deviceName' },
{ label: '别名', field: 'alias' },
{ label: '设备用途', field: 'deviceUse' },
{ label: '存放位置', field: 'position' },
{ label: '等级分类', field: 'gradeClassify' },
{ label: '设备状态', field: 'stateShow' },
{ label: '验收状态', field: 'acceptFlag' },
{ label: '使用状态', field: 'inUseFlag' },
{ label: '数量', field: 'deviceNum' },
{ label: '管理编号', field: 'deviceCode' },
{ label: '资产编号', field: 'assetCode' },
{ label: '出厂编号', field: 'factoryCode' },
{ label: '规格型号', field: 'modelNo' },
{ label: '购入价格', field: 'purchasePrice' },
{ label: '采购时间', field: 'purchaseDate' },
{ label: '出厂日期', field: 'productiveDate' },
{ label: '安装日期', field: 'deployDate' },
{ label: '安装人员', field: 'deployEngineer' },
{ label: '验收人员', field: 'acceptUserName' },
{ label: '负责人', field: 'managerUserName' },
{ label: '所属班组', field: 'deptName' },
{
label: '安装位置',
field: 'deployLocation'
},
{
label: '技术指标',
field: 'deviceParameters',
valueStyle: { fontSize: 12 + 'px', height: 50 + 'px' }
},
{
label: '设备图片',
field: 'photo'
}
]

View File

@@ -0,0 +1,213 @@
<template>
<view class="container">
<up-sticky>
<navbar-back title="设备查询" />
</up-sticky>
<view class="content">
<up-transition :show="!isFold" mode="slide-left">
<view class="con-left" v-show="!isFold">
<uni-card style="height: 100%; overflow: auto" title="分类和产品" padding="0" margin="0" spacing="0">
<DaTree
ref="DaTreeRef"
:data="roomTreeData"
labelField="name"
valueField="id"
appendField="modelNo"
:indent="10"
@change="handleTreeChange"
></DaTree>
</uni-card>
</view>
</up-transition>
<view class="con-right">
<uni-card padding="0" margin="10">
<view class="p10 x-f" style="width: 60%">
<image
class="mr15"
style="width: 30px; height: 30px"
:src="`/static/images/${isFold ? 'zhedie2' : 'zhedie1'}.png`"
@click="isFold = !isFold"
></image>
<up-search
v-model="keyword"
shape="square"
placeholder="请输入设备名称或者扫描设备二维码码查询"
actionText="重置"
:clearabled="false"
:showAction="true"
@change="handleInputSearch"
@custom="handleReset"
></up-search>
</view>
<view style="height: 73vh; width: 100%">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, computed, toRefs } from 'vue'
import DaTree from '@/components/da-tree/index.vue'
import { onShow } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import { useListData } from '@/nx/hooks/usePageListData'
import { throttle } from '@/uview-plus'
import { getColumn } from './deviceBusInfo.data'
import { deviceList, treeData, getDeviceBusInfoById } from '@/nx/api/deviceInfo'
import { useScreenOrientation } from '@/nx/hooks/useScreenOrientation'
import nx from '@/nx'
const column = computed(() => getColumn(isFold.value))
const isFold = ref(false)
const roomTreeData = ref([])
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/lims/deviceBusInfo/index') {
try {
const codeObj = JSON.parse(newVal)
handleDetail({ id: codeObj.id })
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描设备码',
icon: 'none'
})
}
}
})
onShow(() => {
scanQRInfo.value = ''
})
async function getTreeData() {
const res = await treeData()
roomTreeData.value = res
}
getTreeData()
const { lockOrientation } = useScreenOrientation()
onMounted(async () => {
lockOrientation('landscape')
await getInitData()
console.log('listData', listData.value)
})
const searchParams = computed(() => {
const params = {
deviceName: keyword.value
}
if (selectedNode.value) {
params.productId = selectedNode.value.key
}
return params
})
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: deviceList,
processData: data => {
return data.map(item => {
const statuses = [
item.repairFlag == 1 && '维修',
item.demoteFlag == 1 && '降级',
item.scrapFlag == 1 && '报废',
item.disableFlag == 1 && '停用',
item.lendFlag == 1 && '外借'
].filter(Boolean) // 去掉 false 值
if (item.acceptFlag != 'finished') {
item.deviceStatus = '--'
} else {
item.deviceStatus = statuses.length > 0 ? statuses.join('、') : '正常'
}
if (item.inUseFlag == '1') {
item.inUseFlag = '是'
} else {
item.inUseFlag = '否'
}
return item
})
}
})
let zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
let selectedNode = ref(null)
let DaTreeRef = ref()
function handleTreeChange(allCheckedKeys, currentItem) {
console.log(allCheckedKeys, currentItem)
selectedNode.value = currentItem
getInitData()
}
let keyword = ref('')
function handleInputSearch() {
if (!keyword.value) return
throttle(getInitData, 500)
}
function handleReset() {
keyword.value = ''
if (selectedNode.value) {
DaTreeRef.value.setCheckedKeys(selectedNode.value.key, false)
}
selectedNode.value = ''
getInitData()
}
async function handleDetail(row, index) {
nx.$store('biz').deviceInfo = row
await getDeviceBusInfoById(row.id)
nx.$router.go('/pages/lims/deviceBusInfo/baseInfo')
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
.container {
background-color: #f0f2f5;
height: 100%;
.content {
display: flex;
.con-left {
height: 83vh;
overflow: scroll;
margin-top: 10px;
margin-left: 10px;
flex: 2;
}
.con-right {
flex: 8;
overflow: auto;
}
}
}
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
:deep(.u-search__action--active) {
padding: 5px;
border-radius: 3px;
background: #0055a2;
color: #fff;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="18px" type="line" title="设备维护保养信息"> </uni-section>
<scroll-view scroll-y="true" style="height: 85vh; width: 90vw">
<view class="content">
<up-row class="flex-wrap pt10 pl10" style="background-color: #f5f7fa">
<up-col class="mb8" :span="gridCol" v-for="(item, index) in detailSchema">
<view style="color: #666"
><span style="color: #333">{{ item.label }}</span>{{ detailInfo[item.value] }}</view
>
</up-col>
</up-row>
<view class="pt5">
<up-row>
<up-col span="6">
<view
>维护保养人
<text style="color: #666">
{{ detailInfo.checkUserName }}
</text>
</view>
<view class="pt5"
>维护保养日期
<text style="color: #666">
{{ detailInfo.checkDate }}
</text>
</view>
</up-col>
<up-col span="6">
<u-album multiple-size="70" single-size="70" :urls="attachment" row-count="4" />
</up-col>
</up-row>
</view>
<view>
<up-row class="p10 font-bold" style="background-color: #f5f5f5">
<up-col span="5">维护保养内容</up-col>
<up-col span="3"> 频次</up-col>
<up-col span="4">维护保养标准</up-col>
</up-row>
<view>
<view v-for="(item, index) in detailInfo.maintainItemList" :key="index">
<up-row class="p10">
<up-col span="5">
<view class="x-f">
<text>
{{ item.itemName }}
</text>
</view>
</up-col>
<up-col span="3">{{ item.frequencyRemark }}</up-col>
<up-col span="4">{{ item.standard }}</up-col>
</up-row>
<up-row class="fill-content">
<up-col span="12"
><view
>维护保养情况:
<text style="color: #666">
{{ item.checkRemark }}
</text>
</view>
</up-col>
</up-row>
</view>
<view class="p10"></view>
</view>
</view>
</view>
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { useGridCol } from '@/nx/hooks/useGridCol'
import dailyCheckApi from '@/nx/api/dailyCheck'
import { getImgBaseUrl } from '@/defaultBaseUrl'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const detailSchema = [
{ label: '设备名称', value: 'deviceName' },
{ label: '别名', value: 'alias' },
{ label: '设备型号', value: 'modelNo' },
{ label: '设备编码', value: 'deviceCode' },
{ label: '使用班组', value: 'deptName' }
]
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
const attachment = computed(() => {
let files = detailInfo.value?.attachment?.split(',') || []
if (files.length > 0) {
return files.map(item => getImgBaseUrl() + item)
} else {
return []
}
})
let detailInfo = ref({})
async function getDetailInfo(id) {
const res = await dailyCheckApi.queryById(id)
detailInfo.value = res
}
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
function handleOpen() {
getDetailInfo(props.checkInfo.id)
}
</script>
<style lang="scss" scoped>
.content {
background-color: #fff;
height: 100%;
padding: 10px;
font-size: 16px;
.fill-content {
font-size: 14px;
border-radius: 3px;
box-shadow: 2px 2px 8px 2px rgba(0, 0, 0, 0.2);
background-color: #fdf6ec;
padding: 10px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

View File

@@ -0,0 +1,262 @@
<template>
<view>
<up-sticky>
<navbar-back title="维护保养">
<up-button
v-if="detailInfo.id"
type="primary"
:plain="true"
icon="list"
size="small"
text="设备维护保养记录"
@click="handleCheckRecord"
></up-button>
</navbar-back>
</up-sticky>
<view class="container">
<n-scanTemp
v-if="!detailInfo.id"
title="请扫描设备条码进行维护保养"
icon="maintain"
@deviceId="id => getDetailInfo(id)"
/>
<view v-else class="content">
<view>
<uni-section titleFontSize="22px" type="line" title="设备维护保养信息"> </uni-section>
<up-row class="flex-wrap p10" style="background-color: #f5f7fa">
<up-col class="mb10" :span="gridCol" v-for="(item, index) in detailSchema">
<view style="color: #666"
><span style="color: #333">{{ item.label }}</span>{{ detailInfo[item.value] }}</view
>
</up-col>
</up-row>
</view>
<view class="check-header">
<uni-section titleFontSize="22px" type="line" title="维护保养项"> </uni-section>
<up-row class="p10 font-bold border-b" style="background-color: #f5f5f5">
<up-col span="4">维护保养内容</up-col>
<up-col span="4"> 频次</up-col>
<up-col span="4">维护保养标准</up-col>
</up-row>
<view class="pt10">
<view v-for="(item, index) in detailInfo.maintainItemList" :key="index">
<up-row class="p20">
<up-col span="4">
<view class="x-f">
<u-badge type="warning " :value="index + 1"></u-badge>
<text class="pl10">
{{ item.itemName }}
</text>
</view>
</up-col>
<up-col span="4">{{ item.frequencyRemark }}</up-col>
<up-col span="4">{{ item.standard }}</up-col>
</up-row>
<up-row class="fill-content">
<up-col span="12" class="x-f"
><view class="p10"><text class="required-star">*</text>维护保养情况:</view>
<up-input class="bg-w" v-model="item.checkRemark" placeholder="请输入维护保养情况"></up-input
></up-col>
</up-row>
</view>
</view>
</view>
<view>
<view class="p10">附件照片</view>
<n-upload v-model="detailInfo.attachment" />
<view class="p10">维护保养人</view>
<up-input v-model="detailInfo.checkUserName"></up-input>
<view v-if="detailInfo.submitFlag == '1'">
<view class="p10">维护保养日期</view>
<uni-datetime-picker type="date" v-model="detailInfo.checkDate" />
</view>
</view>
<view class="mt40 x-bc" v-if="detailInfo.submitFlag == '0'">
<up-button
style="width: 40%"
loadingText="保存中..."
type="warning"
text="暂存"
@click="handleSubmit('0')"
></up-button>
<up-button
style="width: 40%"
loadingText="提交中..."
type="primary"
text="提交"
@click="handleSubmit('1')"
></up-button>
</view>
</view>
</view>
<up-modal
:show="modalShow"
title="提示"
:content="modalText"
ref="uModal"
:asyncClose="true"
showCancelButton
@confirm="confirm"
@cancel="modalShow = false"
></up-modal>
<up-loading-page :loading="pageLoading"></up-loading-page>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch, toRefs } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import dailyCheckApi from '@/nx/api/dailyCheck'
import nx from '@/nx'
import { useScreenOrientation } from '@/nx/hooks/useScreenOrientation'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const { lockOrientation } = useScreenOrientation()
const pageLoading = ref(false)
const detailSchema = [
{ label: '设备名称', value: 'deviceName' },
{ label: '设备型号', value: 'modelNo' },
{ label: '设备编码', value: 'deviceCode' },
{ label: '使用班组', value: 'deptName' }
]
let detailInfo = ref({})
let checkList = ref([])
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/lims/deviceBusMaintain/index') {
try {
const codeObj = JSON.parse(newVal)
if (!pageLoading.value) {
getDetailInfo(codeObj.id)
}
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描设备码',
icon: 'none'
})
}
}
})
onShow(() => {
scanQRInfo.value = ''
})
const modalText = computed(() => {
return `确定${modalType.value == '0' ? '暂存' : '提交'}吗?${modalType.value == '0' ? '' : '提交后不能修改'} `
})
async function getDetailInfo(id) {
pageLoading.value = true
const res = await dailyCheckApi
.getCheckRecord({
deviceId: id,
dataType: 'maintain',
submitFlag: '0',
cancelFlag: '0'
})
.finally(() => {
pageLoading.value = false
})
if (!res.checkUserName) {
res.checkUserName = nx.$store('user').userInfo.realname
res.checkUserId = nx.$store('user').userInfo.id
}
if (!res.checkDate) {
res.checkDate = nx.$dayjs().format('YYYY-MM-DD 00:00:00')
}
detailInfo.value = res
modalType.value = res.submitFlag
lockOrientation('landscape')
}
const modalShow = ref(false)
const modalType = ref('')
function handleSubmit(type) {
if (!detailInfo.value.checkUserName) {
return uni.showToast({
title: '请输入维护保养人',
icon: 'none'
})
}
if (!detailInfo.value.checkDate) {
return uni.showToast({
title: '请选择维护保养日期',
icon: 'none'
})
}
if (type === '1') {
// 新增维护保养项验证
const items = detailInfo.value.maintainItemList || []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const itemNumber = `${i + 1}`
if (!item.checkRemark?.trim()) {
return uni.showToast({ title: `${itemNumber}维护保养情况未填写`, icon: 'none' })
}
}
}
modalType.value = type
modalShow.value = true
console.log(detailInfo.value)
}
const submitLoading = ref(false)
async function confirm() {
if (submitLoading.value) return
submitLoading.value = true
await dailyCheckApi.submit({ ...detailInfo.value, submitFlag: modalType.value }).finally(() => {
submitLoading.value = false
modalShow.value = false
reset()
})
}
function reset() {
detailInfo.value = {}
}
function handleCheckRecord() {
let deviceInfo = {
id: detailInfo.value.deviceId,
deviceName: detailInfo.value.deviceName
}
nx.$store('biz').deviceInfo = deviceInfo
nx.$router.go('/pages/lims/deviceBusMaintain/list')
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
.container {
.required-star {
color: red;
margin-right: 4px;
}
.content {
background-color: #fff;
height: 100%;
padding: 16px;
padding-top: 10px;
font-size: 18px;
.title {
font-size: 22px;
font-weight: bold;
}
.check-header {
.u-sticky {
top: 70px !important;
}
}
.fill-content {
border-radius: 3px;
box-shadow: 2px 2px 8px 2px rgba(0, 0, 0, 0.2);
background-color: #fdf6ec;
padding: 10px;
box-sizing: border-box;
}
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view>
<up-sticky v-if="!isComponent">
<navbar-back title="维护保养记录"> </navbar-back>
</up-sticky>
<uni-card spacing="0">
<uni-section v-if="!isComponent" titleFontSize="20px" type="line" :title="deviceText"> </uni-section>
<view :style="{ height: isComponent ? '70vh' : '72vh' }">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<maintain-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import MaintainDetailPopup from './detail.vue'
import dailyCheckApi from '@/nx/api/dailyCheck'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
let props = defineProps({
isComponent: {
type: Boolean,
default: false
}
})
const column = reactive([
{
label: '维护保养人',
name: 'checkUserName',
width: 160
},
{
label: '维护保养日期',
name: 'checkDate',
width: 300
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
dataType: 'maintain',
deviceId: deviceId.value
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: dailyCheckApi.list
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备使用记录"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row>
<up-col span="6"
>开始使用人
<text class="value">{{ detailInfo.userName }}</text>
</up-col>
<up-col span="6"
>开始时间
<text class="value">{{ detailInfo.useTimeStart }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="6"
>结束使用人
<text class="value">{{ detailInfo.userNameEnd }}</text>
</up-col>
<up-col span="6"
>结束时间
<text class="value">{{ detailInfo.useTimeEnd }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="4"
>使用前状态
<text class="value">{{ detailInfo.stateBefore }}</text>
</up-col>
<up-col span="4"
>使用后状态
<text class="value">{{ detailInfo.stateAfter }}</text>
</up-col>
<up-col span="4"
>温度()
<text class="value">{{ detailInfo.temperature }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="4"
>湿度(%RH)
<text class="value">{{ detailInfo.humidity }}</text>
</up-col>
<up-col span="6"
>样品类别/个数/任务
<text class="value">{{ detailInfo.useRemark }}</text>
</up-col>
</up-row>
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
function handleOpen() {
detailInfo.value = props.checkInfo
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 80vh;
width: 80vw;
padding: 10px 20px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

View File

@@ -0,0 +1,330 @@
<template>
<view>
<up-sticky>
<navbar-back title="使用">
<up-button
v-if="detailInfo.id"
type="primary"
:plain="true"
icon="list"
size="small"
text="设备使用记录"
@click="handleUseRecord"
></up-button>
</navbar-back>
</up-sticky>
<view class="container">
<n-scanTemp v-if="!detailInfo.id" title="请扫描设备条码进行设备使用" @deviceId="id => handleTestAction(id)" />
<view v-else class="content">
<view>
<uni-section titleFontSize="20px" type="line" title="设备信息"> </uni-section>
<up-row class="flex-wrap p10" style="background-color: #f5f7fa">
<up-col class="mb10" :span="gridCol" v-for="(item, index) in detailSchema">
<view style="color: #666"
><span style="color: #333">{{ item.label }}</span>{{ detailInfo[item.value] }}</view
>
</up-col>
</up-row>
</view>
<view>
<up-row justify="space-around">
<up-col span="3.5">
<view class="p5">使用前状态</view>
<uni-data-select :disabled="useIng" v-model="formData.stateBefore" :localdata="ditData"></uni-data-select>
</up-col>
<up-col span="3.5">
<view class="p5">使用中状态</view>
<uni-data-select :disabled="!useIng" v-model="formData.stateRun" :localdata="ditData"></uni-data-select>
</up-col>
<up-col span="3.5">
<view class="p5">使用后状态</view>
<uni-data-select :disabled="!useIng" v-model="formData.stateAfter" :localdata="ditData"></uni-data-select>
</up-col>
</up-row>
<up-row justify="space-around">
<up-col span="3.5">
<view class="p5">温度</view>
<up-input v-model="formData.temperature"></up-input>
</up-col>
<up-col span="3.5">
<view class="p5">湿度%HR</view>
<up-input v-model="formData.humidity"></up-input>
</up-col>
<up-col span="3.5">
<view class="p5">样品类别/个数/任务</view>
<up-input v-model="formData.useRemark"></up-input>
</up-col>
</up-row>
<up-row justify="space-around">
<up-col span="11.5">
<view class="p5">{{ useIng ? '结束使用人' : '开始使用人' }}</view>
<up-input v-if="useIng" v-model="formData.userNameEnd"></up-input>
<up-input v-else v-model="formData.userName"></up-input>
</up-col>
</up-row>
<up-row justify="space-around">
<up-col span="11.5">
<view class="p5">备注</view>
<up-textarea v-model="formData.remark" placeholder="请输入内容" autoHeight></up-textarea>
</up-col>
</up-row>
</view>
<up-button
style="width: 90%"
class="mt40"
loadingText="提交中..."
type="primary"
:text="useIng ? '结束使用' : '开始使用'"
@click="handleValidate"
></up-button>
</view>
</view>
<up-modal
:show="modalShow"
title="提示"
content="确定提交吗?"
ref="uModal"
:asyncClose="true"
showCancelButton
@confirm="submitConfirm"
@cancel="modalShow = false"
></up-modal>
<up-loading-page :loading="pageLoading"></up-loading-page>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, toRefs } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { getDeviceBusInfoById } from '@/nx/api/deviceInfo'
import { getUseRecordById, addUseRecord, editUseRecord } from './useRecord.api'
import dailyCheckApi from '@/nx/api/dailyCheck'
import { useScreenOrientation } from '@/nx/hooks/useScreenOrientation'
import { useGridCol } from '@/nx/hooks/useGridCol'
import nx from '@/nx'
const { gridCol } = useGridCol([700], [6, 4])
const { lockOrientation } = useScreenOrientation()
const ditData = ref([
{ value: '正常', text: '正常' },
{ value: '异常', text: '异常' }
])
const detailSchema = [
{ label: '设备名称', value: 'deviceName' },
{ label: '设备型号', value: 'modelNo' },
{ label: '设备编码', value: 'deviceCode' },
{ label: '使用班组', value: 'deptName' }
]
const pageLoading = ref(false)
const userId = nx.$store('user').userInfo['id']
const userName = nx.$store('user').userInfo['realname']
let detailInfo = ref({})
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/lims/deviceBusUseRecord/index') {
try {
const codeObj = JSON.parse(newVal)
if (!pageLoading.value) {
getLastDailyCheckOfToday(codeObj.id)
}
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描设备码',
icon: 'none'
})
}
}
})
onShow(() => {
scanQRInfo.value = ''
})
// 检查该设备在使用前是否已经点检过
function getLastDailyCheckOfToday(id) {
pageLoading.value = true
try {
dailyCheckApi.getLastDailyCheckOfToday({ deviceId: id }).then(async res => {
if (!res || res.submitFlag == '0') {
setTimeout(() => {
uni.showToast({
title: '设备使用前请先进行设备点检',
icon: 'none'
})
}, 100)
pageLoading.value = false
nx.$router.go('/pages/deviceBusDailyCheck/index', { deviceId: id })
} else {
getDeviceInfo(id)
await getUseIngRecord(id)
pageLoading.value = false
}
})
} catch (error) {
pageLoading.value = false
}
}
function handleTestAction(id) {
getLastDailyCheckOfToday(id)
}
// 获取设备详情
async function getDeviceInfo(id) {
const res = await getDeviceBusInfoById(id).finally(() => {})
detailInfo.value = res
lockOrientation('landscape')
}
const useIng = ref(false)
// 获取使用记录判断是否在使用中
async function getUseIngRecord(id) {
const res = await getUseRecordById(id)
if (res) {
useIng.value = true
formData.value = res
formData.value.userIdEnd = userId
formData.value.userNameEnd = userName
// checkUseStatusIsMySelf(res)
} else {
useIng.value = false
}
}
// 定义一个变量,用于标记当前是否有 modal 正在显示
let isModalShowing = false
// 如果设备在使用中,检查设备使用记录是否是所登录用户的
function checkUseStatusIsMySelf(useInfo) {
if (isModalShowing) {
return
}
if (useInfo.userId != userId) {
isModalShowing = true
uni.showModal({
title: '提示',
content: '该设备正在使用中,是否结束使用',
success: function (res) {
if (res.confirm) {
submitConfirm()
isModalShowing = false
} else if (res.cancel) {
console.log('用户点击取消')
isModalShowing = false
}
}
})
}
}
let formData = ref({
userName,
userId,
stateBefore: '正常'
})
const modalShow = ref(false)
function handleValidate() {
if (!useIng.value && !formData.value.stateBefore) {
return uni.showToast({
title: '请选择使用前状态',
icon: 'none'
})
}
if (useIng.value) {
if (!formData.value.stateRun) {
return uni.showToast({
title: '请选择使用中状态',
icon: 'none'
})
}
if (!formData.value.stateAfter) {
return uni.showToast({
title: '请选择使用后状态',
icon: 'none'
})
}
}
if (!formData.value.userName) {
return uni.showToast({
title: '请输入使用人',
icon: 'none'
})
}
if (!formData.value.useRemark) {
return uni.showToast({
title: '请输入样品类别/个数/任务',
icon: 'none'
})
}
modalShow.value = true
}
const submitLoading = ref(false)
async function submitConfirm() {
if (submitLoading.value) return
submitLoading.value = true
let submitParams = {
...formData.value,
deviceId: detailInfo.value.id,
temperature: formData.value.temperature || '/',
humidity: formData.value.humidity || '/'
}
// 如果在使用中加入使用结束参数
let currentTime = nx.$dayjs().format('YYYY-MM-DD HH:mm:ss')
let submitApi = useIng.value ? editUseRecord : addUseRecord
if (useIng.value) {
submitParams.useTimeEnd = currentTime
} else {
submitParams.useTimeStart = currentTime
}
await submitApi(submitParams)
.then(() => {
submitLoading.value = false
modalShow.value = false
reset()
})
.catch(() => {
modalShow.value = false
submitLoading.value = false
})
}
function reset() {
formData.value = {
userName: nx.$store('user').userInfo['realname']
}
detailInfo.value = {}
}
function handleUseRecord() {
nx.$store('biz').deviceInfo = detailInfo.value
nx.$router.go('/pages/lims/deviceBusUseRecord/list')
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
.container {
.content {
background-color: #fff;
height: 100%;
padding: 16px;
padding-top: 10px;
font-size: 16px;
.title {
font-size: 22px;
font-weight: bold;
}
.check-header {
.u-sticky {
top: 70px !important;
}
}
.fill-content {
border-radius: 3px;
box-shadow: 2px 2px 8px 2px rgba(0, 0, 0, 0.2);
background-color: #fdf6ec;
padding: 10px;
}
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<view>
<up-sticky v-if="!isComponent">
<navbar-back title="设备使用记录查看"> </navbar-back>
</up-sticky>
<uni-card spacing="0">
<uni-section v-if="!isComponent" titleFontSize="20px" type="line" :title="deviceText"> </uni-section>
<view class="p10" style="width: 50%">
<!-- <uni-datetime-picker v-model="startEndTime" type="daterange" @change="datetimeChange" /> -->
<up-search
v-model="keyword"
shape="square"
placeholder="请输入使用人"
actionText="重置"
:clearabled="false"
:showAction="true"
@change="handleInputSearch"
@custom="reset"
></up-search>
</view>
<view :style="{ height: isComponent ? '60vh' : '62vh' }">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<use-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import UseDetailPopup from './detail.vue'
import { getList } from './useRecord.api'
import { throttle } from '@/uview-plus'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
let props = defineProps({
isComponent: {
type: Boolean,
default: false
}
})
let startEndTime = ref([])
const column = reactive([
{
label: '开始使用人',
name: 'userName',
width: 100
},
{
label: '开始时间',
name: 'useTimeStart',
width: 170
},
{
label: '结束使用人',
name: 'userNameEnd',
width: 100
},
{
label: '结束时间',
name: 'useTimeEnd',
width: 170
},
{
label: '使用前状态',
name: 'stateBefore'
},
{
label: '使用后状态',
name: 'stateAfter'
},
{
label: '温度(℃)',
name: 'temperature'
},
{
label: '湿度(%RH)',
name: 'humidity'
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
let zbTableRef = ref(null)
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceId: deviceId.value,
userName: keyword.value
}))
const { listData, loadingData, getInitData, scrollToLower, loadStatus } = useListData({
searchParams,
api: getList
})
function pullUpLoadingAction(done) {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
let keyword = ref('')
function handleInputSearch() {
if (!keyword.value) return
throttle(getInitData, 500)
}
function reset() {
keyword.value = ''
getInitData()
}
function datetimeChange(e) {
startEndTime.value = [nx.$dayjs(e[0]).format('YYYY-MM-DD 00:00:00'), nx.$dayjs(e[1]).format('YYYY-MM-DD 23:59:59')]
getInitData()
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
background-color: #fff !important;
}
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
:deep(.u-search__action--active) {
padding: 5px;
border-radius: 3px;
background: #0055a2;
color: #fff;
}
</style>

View File

@@ -0,0 +1,36 @@
import request from '@/nx/request'
export function addUseRecord(data) {
return request({
url: '/lims/bus/device/use-record/create',
method: 'POST',
data,
custom: {
showSuccess: true
}
})
}
export function editUseRecord(data) {
return request({
url: '/lims/bus/device/use-record/edit',
method: 'POST',
data,
custom: {
showSuccess: true
}
})
}
export function getList(params) {
return request({
url: '/lims/bus/device/use-record/list',
method: 'GET',
params
})
}
export function getUseRecordById(id) {
return request({
url: '/lims/bus/device/use-record/queryLastUsingData',
method: 'GET',
params: { deviceId: id }
})
}

View File

@@ -0,0 +1,16 @@
import request from '@/nx/request'
export function getDeviceDocumentList(params) {
return request({
url: '/lims/document/device-relation/list',
method: 'GET',
params
})
}
export function getDocumentInfoById(id) {
return request({
url: '/lims/document/info/queryById',
method: 'GET',
params: { id }
})
}

View File

@@ -0,0 +1,117 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<up-modal :show="fileShow" :title="fileInfo.documentName" confirmText="关闭" @confirm="fileShow = false">
<scroll-view scroll-y="true" style="max-height: 60vh">
<uni-card>
<up-parse :content="fileInfo.documentContent"></up-parse>
</uni-card>
</scroll-view>
</up-modal>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import { getDeviceDocumentList, getDocumentInfoById } from './document.api'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '文档名称',
name: 'documentName',
width: 260
},
{
label: '文档描述',
name: 'documentAbstract',
width: 420
},
{
label: '文档类型',
name: 'documentType',
width: 120
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '查看', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceBusInfoId: deviceId.value,
searchRelationType: 'device'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: getDeviceDocumentList,
processData: data => {
return data.map(item => {
return {
...item,
documentType: item.documentType === 'file' ? '文件' : '文本'
}
})
}
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const fileShow = ref(false)
const fileInfo = ref({})
async function handleDetail(row) {
console.log(row)
fileInfo.value = row
if (row.documentType === '文本') {
const { documentContent } = await getDocumentInfoById(row.documentBusInfoId)
fileInfo.value.documentContent = documentContent
fileShow.value = true
} else {
nx.$router.go('/pages/lims/documentList/preview', { documentUrl: row.documentUrl })
}
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<view class="container">
<up-icon size="20" class="close-icon" @click="handleClose" name="close-circle"></up-icon>
<web-view :src="previewUrl"></web-view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import CryptoJS from 'crypto-js'
import { getImgBaseUrl } from '@/defaultBaseUrl'
import { onLoad } from '@dcloudio/uni-app'
let previewUrl = ref('')
onLoad(option => {
let url = encodeURIComponent(
CryptoJS.enc.Utf8.parse(getImgBaseUrl() + option.documentUrl).toString(CryptoJS.enc.Base64)
)
previewUrl.value = getImgBaseUrl() + '/preview/onlinePreview?url=' + url
})
</script>
<style lang="scss" scoped>
.container {
.close-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
z-index: 999999;
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备借用单"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row class="flex-wrap">
<up-col :span="gridCol"
>归还人
<text class="value">{{ detailInfo.givebackOper }}</text>
</up-col>
<up-col :span="gridCol"
>归还日期
<text class="value">{{ detailInfo.givebackDate }}</text>
</up-col>
<up-col :span="gridCol"
>接收人
<text class="value">{{ detailInfo.givebackReceiveOper }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>备注
<text class="value">{{ detailInfo.remark }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
let commentWf = ref([])
function handleOpen() {
detailInfo.value = props.checkInfo
if (props.checkInfo.commentJson) {
try {
commentWf.value = JSON.parse(props.checkInfo.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 80vh;
width: 75vw;
padding: 20px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<giveback-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import GivebackDetailPopup from './detail.vue'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import { givebackList } from '@/nx/api/deviceInfo'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '归还人',
name: 'givebackOper',
width: 140
},
{
label: '归还日期',
name: 'givebackDate',
width: 140
},
{
label: '接收人',
name: 'givebackReceiveOper',
width: 140
},
{
label: '备注',
name: 'remark',
width: 140
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceBusInfoId: deviceId.value,
givebackStatus: '已归还'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: givebackList
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<view>
<navbar-back title="设备管理系统" :autoBack="false" leftIcon="" :leftText="`您好!${userInfo.realname}`">
<u-icon @click="popupShow = true" size="28" color="#FFF" name="account-fill" />
</navbar-back>
<up-grid :border="false" :col="gridCol">
<up-grid-item
class="mb20 mt20"
v-for="(item, listIndex) in list"
:key="listIndex"
@click="nx.$router.go(item.url)"
>
<image style="width: 80px; height: 80px" :src="`/static/images/menus/${item.icon}.png`"></image>
<text class="grid-text">{{ item.name }}</text>
</up-grid-item>
</up-grid>
<mePopup v-model:show="popupShow" />
</view>
</template>
<script setup>
import { reactive, ref, computed } from 'vue'
import nx from '@/nx'
import { useGridCol } from '@/nx/hooks/useGridCol'
import mePopup from '@/pages/index/me-popup.vue'
const { gridCol } = useGridCol([400, 600], [2, 3, 4])
let popupShow = ref(false)
let list = reactive([
{ url: '/pages/lims/deviceBusDailyCheck/index', name: '点检', icon: 'dailyCheck' },
{ url: '/pages/lims/deviceBusMaintain/index', name: '维护保养', icon: 'maintain' },
{ url: '/pages/lims/deviceBusUseRecord/index', name: '使用', icon: 'useRecord' },
{ url: '/pages/lims/deviceBusInfo/index', name: '设备查询', icon: 'baseInfo' },
{ url: '/pages/lims/knowledge/index', name: '知识库查询', icon: 'knowledge' }
])
const roleMenus = computed(() => nx.$store('user').roleMenus)
const userInfo = computed(() => nx.$store('user').userInfo)
</script>
<style lang="scss" scoped>
.grid-text {
font-size: 24px;
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<view>
<up-sticky>
<navbar-back title="知识库查询"></navbar-back>
</up-sticky>
<uni-card spacing="0">
<view class="p10" style="width: 50%">
<!-- <uni-datetime-picker v-model="startEndTime" type="daterange" @change="datetimeChange" /> -->
<up-search
v-model="keyword"
shape="square"
placeholder="请输入文档名称"
actionText="重置"
:clearabled="false"
:showAction="true"
@change="handleInputSearch"
@custom="reset"
></up-search>
</view>
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<up-modal :show="fileShow" :title="fileInfo.documentName" confirmText="关闭" @confirm="fileShow = false">
<scroll-view scroll-y="true" style="max-height: 60vh">
<uni-card>
<up-parse :content="fileInfo.documentContent"></up-parse>
</uni-card>
</scroll-view>
</up-modal>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import { getDeviceDocumentList } from './knowledge.api'
import { getDocumentInfoById } from '../documentList/document.api'
import { useListData } from '@/nx/hooks/usePageListData'
import { throttle } from '@/uview-plus'
import nx from '@/nx'
const column = reactive([
{
label: '文档名称',
name: 'documentName',
width: 260
},
{
label: '文档描述',
name: 'documentAbstract',
width: 420
},
{
label: '文档类型',
name: 'documentType',
width: 120
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '查看', func: 'detail' }]
}
])
onMounted(() => {
getInitData()
})
const searchParams = computed(() => ({
documentName: keyword.value
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: getDeviceDocumentList,
processData: data => {
return data.map(item => {
return {
...item,
documentType: item.documentType === 'file' ? '文件' : '文本'
}
})
}
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
let keyword = ref('')
function handleInputSearch() {
if (!keyword.value) return
throttle(getInitData, 500)
}
function reset() {
keyword.value = ''
getInitData()
}
const fileShow = ref(false)
const fileInfo = ref({})
async function handleDetail(row) {
console.log(row)
fileInfo.value = row
if (row.documentType === '文本') {
const { documentContent } = await getDocumentInfoById(row.documentBusInfoId)
fileInfo.value.documentContent = documentContent
fileShow.value = true
} else {
nx.$router.go('/pages/lims/documentList/preview', { documentUrl: row.documentUrl })
}
}
</script>
<style lang="scss" scoped>
.u-sticky {
top: 0 !important;
}
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
:deep(.u-search__action--active) {
padding: 5px;
border-radius: 3px;
background: #0055a2;
color: #fff;
}
</style>

View File

@@ -0,0 +1,16 @@
import request from '@/nx/request'
export function getDeviceDocumentList(params) {
return request({
url: 'lims/knowledge/document-relation/list',
method: 'GET',
params
})
}
export function getDocumentInfoById(id) {
return request({
url: '/lims/knowledge/base/queryById',
method: 'GET',
params: { id }
})
}

View File

@@ -0,0 +1,118 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备期间核查信息"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row class="flex-wrap">
<up-col :span="gridCol"
>核查对象
<text class="value">{{ detailInfo.deviceName }}</text>
</up-col>
<up-col span="4"
>别名
<text class="value">{{ detailInfo.alias }}</text>
</up-col>
<up-col span="4"
>核查日期
<text class="value">{{ detailInfo.checkDate }}</text>
</up-col>
<up-col :span="gridCol"
>核查方法
<text class="value">{{ detailInfo.checkAccording }}</text>
</up-col>
</up-row>
<up-row class="flex-wrap">
<up-col span="12"
>核查方法描述
<text class="value">{{ detailInfo.checkAccordingRemark }}</text>
</up-col>
<up-col span="12"
>核查记录
<up-parse style="background: #f3f4f6" :content="processedContent"></up-parse>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>备注
<text class="value">{{ detailInfo.checkRemark }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { getImgBaseUrl } from '@/defaultBaseUrl'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
// / 处理富文本内容
const processedContent = computed(() => {
if (!detailInfo.value.checkContent) {
return ''
}
return detailInfo.value.checkContent.replace(
/<img([^>]+?)src="((?!http)[^"]*?)(file\/[^"]*)"/gi,
(match, attributes, prefix, filePath) => {
return `<img${attributes}src="${getImgBaseUrl()}/${filePath}"`
}
)
})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
let commentWf = ref([])
function handleOpen() {
detailInfo.value = props.checkInfo
if (props.checkInfo.commentJson) {
try {
commentWf.value = JSON.parse(props.checkInfo.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 85vh;
width: 80vw;
padding: 10px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<period-check-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import PeriodCheckDetailPopup from './detail.vue'
import { list } from './period.api'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '核查日期',
name: 'checkDate',
width: 120
},
{
label: '核查人',
name: 'checkPersonName',
width: 120
},
{
label: '核查方法',
name: 'checkAccording',
width: 120
},
{
label: '期间核查频次',
name: 'frequencyRemark',
width: 200
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceId: deviceId.value,
effectiveFlag: '1',
wfStatus: 'finished',
cancelFlag: '0'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: list
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,9 @@
import request from '@/nx/request'
export function list(params) {
return request({
url: '/lims/bus/deviceBusPeriodCheck/queryPageList',
method: 'GET',
params
})
}

View File

@@ -0,0 +1,130 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备维修单"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row class="flex-wrap">
<up-col :span="gridCol"
>设备名称
<text class="value">{{ detailInfo.deviceName }}</text>
</up-col>
<up-col span="4"
>别名
<text class="value">{{ detailInfo.alias }}</text>
</up-col>
<up-col span="4"
>设备编号
<text class="value">{{ detailInfo.deviceCode }}</text>
</up-col>
<up-col :span="gridCol"
>生产厂家
<text class="value">{{ detailInfo.manufacturer }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="6"
>使用部门
<text class="value">{{ detailInfo.deviceDeptName }}</text>
</up-col>
<up-col span="6"
>设备负责人
<text class="value">{{ detailInfo.deviceManagerUserName }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>故障分析及处理方案
<text class="value">{{ detailInfo.repairAnalysis }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>故障判定
<text class="value">{{ detailInfo.repairDetermine }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>维修事项及配件更换
<text class="value">{{ detailInfo.repairContent }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>维修后仪器设备状态
<text class="value">{{ detailInfo.repairResult }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>备注
<text class="value">{{ detailInfo.remark }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { useGridCol } from '@/nx/hooks/useGridCol'
const { gridCol } = useGridCol([700], [6, 4])
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
let commentWf = ref({})
function handleOpen() {
detailInfo.value = props.checkInfo
if (props.checkInfo.commentJson) {
try {
commentWf.value = JSON.parse(props.checkInfo.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 80vh;
width: 80vw;
padding: 20px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

104
pages/lims/repair/list.vue Normal file
View File

@@ -0,0 +1,104 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import DetailPopup from './detail.vue'
import { repairList } from '@/nx/api/deviceInfo'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '申请人',
name: 'applyUserName',
width: 140
},
{
label: '报修时间',
name: 'applyTime',
width: 120
},
{
label: '设备责任人',
name: 'deviceManagerUserName',
width: 120
},
{
label: '设备使用部门',
name: 'deviceDeptName',
width: 140
},
{
label: '故障判定',
name: 'repairDetermine',
width: 240
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceId: deviceId.value,
wfStatus: 'finished'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: repairList
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<uni-card style="height: 100%" spacing="0">
<scroll-view scroll-y="true" class="content">
<up-row>
<up-col span="6"
>报废类型
<text class="value">{{ scrapInfo.scrapType }}</text>
</up-col>
<up-col span="6"
>报废日期
<text class="value">{{ scrapInfo.scrapDate }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>报废原因
<text class="value">{{ scrapInfo.scrapReason }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
<view class="p20"></view>
</scroll-view>
</uni-card>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeMount } from 'vue'
import { scrapDetailList } from '@/nx/api/deviceInfo'
import nx from '@/nx'
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
getDetailInfo(id)
})
let scrapInfo = ref({})
let commentWf = ref([])
async function getDetailInfo(id) {
const records = await scrapDetailList({
deviceBusInfoId: id
})
if (records.length > 0) {
scrapInfo.value = records[0]
if (records[0].commentJson) {
try {
commentWf.value = JSON.parse(records[0].commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.content {
height: 80vh;
font-size: 18px;
color: #000;
padding: 10px;
.u-row {
border-bottom: 1px solid #eee;
}
.value {
color: #666;
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<up-popup :show="visible" mode="right" closeable @close="handleClose" @open="handleOpen">
<uni-section titleFontSize="20px" type="line" title="设备借用单"> </uni-section>
<scroll-view scroll-y="true" class="content">
<up-row>
<up-col span="12"
>停用日期
<text class="value">{{ detailInfo.stopDate }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>借用原因
<text class="value">{{ detailInfo.stopReason }}</text>
</up-col>
</up-row>
<up-row>
<up-col span="12"
>备注
<text class="value">{{ detailInfo.remark }}</text>
</up-col>
</up-row>
<wf-comment :commentWf="commentWf" />
</scroll-view>
</up-popup>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
checkInfo: {
type: Object
}
})
const visible = ref(props.show)
// 监听外部传入的show属性变化
watch(
() => props.show,
newVal => {
visible.value = newVal
}
)
let detailInfo = ref({})
const emit = defineEmits(['close', 'open'])
function handleClose() {
emit('close')
}
let commentWf = ref([])
function handleOpen() {
detailInfo.value = props.checkInfo
if (props.checkInfo.commentJson) {
try {
commentWf.value = JSON.parse(props.checkInfo.commentJson)
} catch (error) {
uni.showToast({
title: '解析数据错误',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
font-size: 18px;
height: 80vh;
width: 75vw;
padding: 20px;
.u-row {
border-bottom: 1px solid #eee;
padding: 10px 0;
}
.value {
color: #666;
font-size: 16px;
}
}
:deep(.uicon-close) {
font-size: 22px !important;
}
</style>

94
pages/lims/stop/list.vue Normal file
View File

@@ -0,0 +1,94 @@
<template>
<view>
<uni-card spacing="0">
<view style="height: 72vh">
<zb-table
ref="zbTableRef"
isShowLoadMore
stripe
:fit="false"
:columns="column"
:cellStyle="setCellStyle"
:cellHeaderStyle="setCellHeaderStyle"
:data="listData"
@detail="handleDetail"
@pullUpLoading="pullUpLoadingAction"
></zb-table>
</view>
</uni-card>
<stop-detail-popup :show="detailShow" :checkInfo="checkInfo" @close="detailShow = false" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { setCellHeaderStyle, setCellStyle } from '@/nx/config/zbTable'
import { stopList } from '@/nx/api/deviceInfo'
import StopDetailPopup from './detail.vue'
import { useListData } from '@/nx/hooks/usePageListData'
import nx from '@/nx'
const column = reactive([
{
label: '停用日期',
name: 'stopDate',
width: 140
},
{
label: '停用原因',
name: 'stopReason',
width: 280
},
{
label: '备注',
name: 'remark',
width: 140
},
{
name: 'operation',
type: 'operation',
label: '操作',
renders: [{ name: '详情', func: 'detail' }]
}
])
const deviceId = ref('')
const deviceText = ref('')
onMounted(() => {
const { id, deviceName } = nx.$store('biz').deviceInfo
deviceId.value = id
deviceText.value = deviceName
getInitData()
})
const searchParams = computed(() => ({
deviceBusInfoId: deviceId.value,
stopListStatus: '已停用'
}))
const { listData, loadingData, scrollToLower, loadStatus, getInitData } = useListData({
searchParams,
api: stopList
})
const zbTableRef = ref()
function pullUpLoadingAction() {
if (loadingData.value) return
if (loadStatus.value === 'nomore') {
zbTableRef.value.pullUpCompleteLoading('ok')
} else {
scrollToLower()
}
}
const detailShow = ref(false)
const checkInfo = ref({})
function handleDetail(row) {
checkInfo.value = row
detailShow.value = true
}
</script>
<style lang="scss" scoped>
:deep(.zb-table uni-button[type='primary']) {
background-color: $uni-color-primary !important;
}
</style>

View File

@@ -1,342 +0,0 @@
<template>
<s-layout class="login-container" title="登录/注册" :bgStyle="{
background: '#fff'
}">
<view class="login-wrap">
<!-- 1. 统一登录组件 (整合账号密码登录和短信登录) -->
<unified-login
v-if="authType === 'accountLogin' || authType === 'smsLogin'"
:agreeStatus="state.protocol"
@onConfirm="onConfirm"
/>
<!-- 3. 忘记密码 resetPassword-->
<!-- <reset-password v-if="authType === 'resetPassword'" /> -->
<!-- 4. 绑定手机号 changeMobile -->
<change-mobile v-if="authType === 'changeMobile'" />
<!-- 5. 修改密码 changePassword-->
<changePassword v-if="authType === 'changePassword'" />
<!-- 6. 微信小程序授权 -->
<mp-authorization v-if="authType === 'mpAuthorization'" />
<!-- 7. 第三方登录 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="auto-login-box ss-flex ss-flex-col ss-row-center ss-col-center"
>
<!-- 7.1 微信小程序的快捷登录 -->
<view v-if="sheep.$platform.name === 'WechatMiniProgram'" class="ss-flex register-box">
<view class="register-title">还没有账号?</view>
<button
class="ss-reset-button login-btn"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
style="color: var(--ui-BG-Main, #0055A2) !important"
>
快捷登录
</button>
<view class="circle" />
</view>
<!-- 7.2 微信的公众号App小程序的登录基于 openid + code -->
<button
v-if="
['WechatOfficialAccount', 'WechatMiniProgram', 'App'].includes(sheep.$platform.name) &&
sheep.$platform.isWechatInstalled
"
@tap="thirdLogin('wechat')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
/>
</button>
<!-- 7.3 iOS 登录 TODO 芋艿:等后面搞 App 再弄 -->
<button
v-if="sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
@tap="thirdLogin('apple')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/apple.png')"
/>
</button>
</view>
<!-- 用户协议的勾选 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="agreement-box ss-flex ss-flex-col ss-col-center"
:class="{ shake: currentProtocol }"
>
<view class="agreement-title ss-m-b-20">
请阅读并同意以下协议:
</view>
<view class="agreement-options-container">
<view class="agreement-option" @tap="onAgree">
<view class="radio-container ss-flex ss-col-center">
<view
class="custom-radio"
:class="{ 'custom-radio-checked': state.protocol === true }"
>
<view v-if="state.protocol === true" class="radio-dot"></view>
</view>
<view class="agreement-text ss-flex ss-col-center ss-m-l-8">
我已阅读并同意遵守
<view class="tcp-text" @tap.stop="onProtocol('用户协议')"> 《用户协议》 </view>
<view class="agreement-text">与</view>
<view class="tcp-text" @tap.stop="onProtocol('隐私协议')"> 《隐私协议》 </view>
</view>
</view>
</view>
</view>
</view>
<view class="safe-box" />
</view>
</s-layout>
</template>
<script setup>
import { computed, reactive, ref } from 'vue';
import sheep from '@/sheep';
import unifiedLogin from '@/sheep/components/s-auth-modal/components/unified-login.vue';
import changeMobile from '@/sheep/components/s-auth-modal/components/change-mobile.vue';
import changePassword from '@/sheep/components/s-auth-modal/components/change-password.vue';
import mpAuthorization from '@/sheep/components/s-auth-modal/components/mp-authorization.vue';
import { onLoad } from '@dcloudio/uni-app';
import { navigateAfterLogin } from '@/sheep/helper/login-redirect';
const authType = ref('accountLogin');
onLoad((options) => {
if (options.authType) {
authType.value = options.authType;
}
});
const state = reactive({
protocol: false, // false 表示未勾选true 表示已同意
});
const currentProtocol = ref(false);
// 同意协议
function onAgree() {
state.protocol = !state.protocol;
uni.showToast({
title: state.protocol ? '已勾选协议' : '已取消勾选',
icon: state.protocol ? 'success' : 'none',
duration: 1000
});
}
// 查看协议
function onProtocol(title) {
sheep.$router.go('/pages/public/richtext', {
title,
});
}
// 点击登录 / 注册事件
function onConfirm(e) {
currentProtocol.value = e;
setTimeout(() => {
currentProtocol.value = false;
}, 1000);
}
// 第三方授权登陆微信小程序、Apple
const thirdLogin = async (provider) => {
if (state.protocol !== true) {
currentProtocol.value = true;
setTimeout(() => {
currentProtocol.value = false;
}, 1000);
sheep.$helper.toast('请先勾选协议');
return;
}
const loginRes = await sheep.$platform.useProvider(provider).login();
if (loginRes) {
const userInfo = await sheep.$store('user').getInfo();
// 如果用户已经有头像和昵称,不需要再次授权
if (userInfo.avatar && userInfo.nickname) {
// 登录成功后跳转到首页
navigateAfterLogin();
return;
}
// 触发小程序授权信息弹框
// #ifdef MP-WEIXIN
authType.value = 'mpAuthorization';
// #endif
}
};
// 微信小程序的“手机号快速验证”https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
const getPhoneNumber = async (e) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
sheep.$helper.toast('快捷登录失败');
return;
}
let result = await sheep.$platform.useProvider().mobileLogin(e.detail);
if (result) {
// 登录成功后跳转到首页
navigateAfterLogin();
}
};
</script>
<style lang="scss" scoped>
@import '@/sheep/components/s-auth-modal/index.scss';
.login-container {
.login-wrap {
padding-top: 100rpx;
}
}
.shake {
animation: shake 0.05s linear 4 alternate;
}
@keyframes shake {
from {
transform: translateX(-10rpx);
}
to {
transform: translateX(10rpx);
}
}
.register-box {
position: relative;
justify-content: center;
.register-btn {
color: #999999;
font-size: 30rpx;
font-weight: 500;
}
.register-title {
color: #999999;
font-size: 30rpx;
font-weight: 400;
margin-right: 24rpx;
}
.or-title {
margin: 0 16rpx;
color: #999999;
font-size: 30rpx;
font-weight: 400;
}
.login-btn {
color: var(--ui-BG-Main, #0055A2) !important;
font-size: 30rpx;
font-weight: 500;
}
.circle {
position: absolute;
right: 0rpx;
top: 18rpx;
width: 8rpx;
height: 8rpx;
border-radius: 8rpx;
background: var(--ui-BG-Main, #0055A2) !important;
}
}
.safe-box {
height: calc(constant(safe-area-inset-bottom) / 5 * 3);
height: calc(env(safe-area-inset-bottom) / 5 * 3);
}
.tcp-text {
color: var(--ui-BG-Main, #0055A2) !important;
}
.agreement-text {
color: $dark-9;
}
.agreement-title {
font-size: 28rpx;
color: $dark-9;
text-align: left;
width: 100%;
padding-left: 60rpx;
position: relative;
}
.protocol-status {
font-size: 24rpx;
font-weight: bold;
margin-top: 8rpx;
padding: 4rpx 12rpx;
border-radius: 16rpx;
display: inline-block;
&.protocol-agreed {
color: #52c41a;
background-color: rgba(82, 196, 26, 0.1);
}
&.protocol-refused {
color: #ff4d4f;
background-color: rgba(255, 77, 79, 0.1);
}
}
.agreement-options-container {
width: 100%;
padding-left: 100rpx;
}
.agreement-option {
width: 100%;
display: flex;
justify-content: flex-start;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
opacity: 0.8;
}
}
.radio-container {
display: flex;
align-items: center;
width: 100%;
}
/* 自定义radio样式 - 同意 */
.custom-radio {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #d9d9d9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
&.custom-radio-checked {
border-color: var(--ui-BG-Main, #409eff);
background-color: var(--ui-BG-Main, #409eff);
}
}
.radio-dot {
width: 16rpx;
height: 16rpx;
background-color: #fff;
border-radius: 50%;
}
</style>

190
pages/login/login.vue Normal file
View File

@@ -0,0 +1,190 @@
<template>
<view class="content">
<view class="setting">
<view></view>
<view class="x-f" @click="handleUrlConfig"
><u-icon color="#0055A2" size="25" name="setting"></u-icon><text>请求地址设置</text></view
>
</view>
<view class="header">
<image src="/static/images/login/logo.png"></image>
</view>
<view class="list">
<view class="list-call">
<u-icon name="account" color="#0055a2" size="30"></u-icon>
<input class="sl-input" v-model="loginInfo.username" placeholder="请输入账号" />
</view>
<view class="list-call">
<u-icon name="lock" color="#0055a2" size="30"></u-icon>
<input
class="sl-input"
v-model="loginInfo.password"
type="text"
maxlength="32"
placeholder="请输入密码"
password="true"
/>
</view>
</view>
<view class="login-btn">
<u-button
style="width: 60%"
size="large"
color="linear-gradient(-90deg, rgb(0, 85, 162), rgb(31 127 247))"
shape="circle"
text="登录"
loadingText="登录中..."
:loading="loading"
@click="bindLogin"
></u-button>
</view>
<!-- 验证码弹窗 -->
<n-verify ref="verifyRef" @success="onCaptchaSuccess" @error="onCaptchaError" />
</view>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import nx from '@/nx'
import callCheckUpdate from '@/nx/utils/check-update'
const loading = ref(false)
const captchaEnable = true
let loginInfo = reactive({
username: '',
password: '',
captchaVerification: ''
})
onShow(() => {
//检查APP更新
// #ifdef APP-PLUS
if (process.env.NODE_ENV !== 'development') {
setTimeout(() => {
callCheckUpdate()
}, 1500)
}
// #endif
})
async function bindLogin() {
if (!loginInfo.username || !loginInfo.password) {
uni.showToast({
icon: 'none',
title: '请输入账号和密码'
})
return
}
loading.value = true
// 如果启用验证码,先显示验证码
if (captchaEnable) {
showCaptcha()
} else {
// 直接登录
performAccountLogin()
}
}
const verifyRef = ref(null)
// 显示验证码弹窗
function showCaptcha() {
if (verifyRef.value) {
verifyRef.value.show()
}
}
function handleUrlConfig() {
nx.$router.go('/pages/setting/UrlConfig')
}
// 验证码验证成功回调
function onCaptchaSuccess(data) {
loginInfo.captchaVerification = data.captchaVerification
performAccountLogin()
}
// 执行账号登录
async function performAccountLogin() {
await nx
.$store('user')
.login(loginInfo)
.finally(() => {
loading.value = false
loginInfo.captchaVerification = ''
})
}
// 验证码验证失败回调
function onCaptchaError(error) {
uni.showToast({
icon: 'none',
title: error?.message || '验证码验证失败'
})
}
</script>
<style lang="scss" scoped>
.content {
display: flex;
flex-direction: column;
justify-content: center;
.setting {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 16px;
padding: 20px;
color: $uni-color-primary;
}
}
.header {
margin-left: auto;
margin-right: auto;
}
.header image {
width: 120px;
height: 120px;
}
.list {
display: flex;
flex-direction: column;
padding-top: 20px;
padding-left: 20%;
padding-right: 20%;
}
.list-call {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 80px;
color: #333333;
border-bottom: 0.5px solid #e2e2e2;
}
.list-call .sl-input {
flex: 1;
text-align: left;
font-size: 16px;
margin-left: 16px;
}
.button-login {
color: #ffffff;
font-size: 34px;
width: 470px;
height: 100px;
background: linear-gradient(-90deg, rgb(0, 85, 162), rgb(21, 97, 192));
border-radius: 50px;
line-height: 100px;
text-align: center;
margin-left: auto;
margin-right: auto;
margin-top: 100px;
}
.login-btn {
margin-top: 20px;
}
</style>

210
pages/login/reg.vue Normal file
View File

@@ -0,0 +1,210 @@
<template>
<view class="content">
<view class="header">
<image src="/static/images/login/logo.jpg"></image>
</view>
<view class="list">
<view class="list-call">
<input class="sl-input" v-model="phone" type="number" maxlength="11" placeholder="手机号" />
</view>
<view class="list-call">
<input class="sl-input" v-model="password" type="text" maxlength="32" placeholder="登录密码"
:password="!showPassword" />
</view>
<view class="list-call">
<input class="sl-input" v-model="code" type="number" maxlength="4" placeholder="验证码" />
<view class="yzm" :class="{ yzms: second>0 }" @tap="getcode">{{yanzhengma}}</view>
</view>
</view>
<view class="button-login" hover-class="button-hover" @tap="bindLogin">
<text>注册</text>
</view>
</view>
</template>
<script setup>
import {
reactive,
onUnmounted,
computed
} from 'vue';
const state = reactive({
phone: '',
password: '',
code: '',
showPassword: false,
second: 0
});
function clear() {
clearInterval(js)
js = null
state.second = 0
}
function display() {
state.showPassword = !state.showPassword
}
onUnmounted(() => {
clear()
});
// 注册
function bindLogin() {
}
function getcode() {
if (state.phone.length != 11) {
uni.showToast({
icon: 'none',
title: '手机号不正确'
});
return;
}
if (state.second > 0) {
return;
}
state.second = 60;
//请求业务
js = setInterval(function() {
state.second--;
if (state.second == 0) {
state.clear()
}
}, 1000)
// uni.request({
// url: 'http://***/getcode.html', //仅为示例,并非真实接口地址。
// data: {
// phone: this.phone,
// type: 'reg'
// },
// method: 'POST',
// dataType: 'json',
// success: (res) => {
// if (res.data.code != 200) {
// uni.showToast({
// title: res.data.msg,
// icon: 'none'
// });
// } else {
// uni.showToast({
// title: res.data.msg
// });
// js = setInterval(function() {
// _this.second--;
// if (_this.second == 0) {
// _this.clear()
// }
// }, 1000)
// }
// },
// fail() {
// this.second == 0
// }
// });
}
const yanzhengma = computed(() => {
if (state.second == 0) {
return '获取验证码';
} else {
if (state.second < 10) {
return '重新获取0' + state.second;
} else {
return '重新获取' + state.second;
}
}
});
</script>
<style>
page {
background-color: #fff;
}
.content {
display: flex;
flex-direction: column;
justify-content: center;
}
.header {
width: 161rpx;
height: 161rpx;
border-radius: 50%;
margin-top: 30rpx;
margin-left: auto;
margin-right: auto;
}
.header image {
width: 161rpx;
height: 161rpx;
border-radius: 50%;
}
.list {
display: flex;
flex-direction: column;
padding-top: 50rpx;
padding-left: 70rpx;
padding-right: 70rpx;
}
.list-call {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 100rpx;
color: #333333;
border-bottom: 0.5px solid #e2e2e2;
}
.list-call .sl-input {
flex: 1;
text-align: left;
font-size: 32rpx;
margin-left: 16rpx;
}
.yzm {
color: #FF7D13;
font-size: 24rpx;
line-height: 64rpx;
padding-left: 10rpx;
padding-right: 10rpx;
width: auto;
height: 64rpx;
border: 1rpx solid #FFA800;
border-radius: 50rpx;
}
.yzms {
color: #999999 !important;
border: 1rpx solid #999999;
}
.button-login {
color: #FFFFFF;
font-size: 34rpx;
width: 470rpx;
height: 100rpx;
line-height: 100rpx;
background: linear-gradient(-90deg, rgba(193, 25, 32, 1), rgba(238, 38, 38, 1));
border-radius: 50rpx;
text-align: center;
margin-left: auto;
margin-right: auto;
margin-top: 100rpx;
}
.button-hover {
background: linear-gradient(-90deg, rgba(193, 25, 32, 0.8), rgba(238, 38, 38, 0.8));
}
</style>

89
pages/me/aboutMe.vue Normal file
View File

@@ -0,0 +1,89 @@
<template>
<view>
<view>
<view class="aboutme">
<view class="content">
<image class="logo" src="../../static/images/logo_mas.png"></image>
<view style="font-size: 40px; padding-top: 20px">{{ state.appName }}</view>
<view style="padding-top: 20px; font-size: 22px">当前APP版本{{ state.appVersion }}</view>
<rich-text :nodes="state.aboutMe"></rich-text>
</view>
<view class="foot">
<view class="foot-phone">技术支持云南志者竟成科技有限公司</view>
<!-- <view class="foot-email">电子邮箱support@will-way.cn</view>-->
<!-- <view class="copyright">Copyright © 2019-{{ copyrightYear }} 云南志者竟成科技有限公司</view>-->
</view>
</view>
</view>
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { reactive } from 'vue'
let state = reactive({
pageHeight: 0,
appVersion: '1.1.0',
appName: '凉山矿业设备管理系统',
copyrightYear: '2019',
aboutMe:
'<div style="text-align:left;font-size:20px;padding:18px"><p style="text-indent:2em;line-height:32px"></p></div>'
})
onLoad(() => {
// #ifdef APP-PLUS
let appId = plus.runtime.appid
plus.runtime.getProperty(appId, function (wgtinfo) {
state.appName = wgtinfo.name
state.appVersion = wgtinfo.version
console.log(wgtinfo)
})
// #endif
state.copyrightYear = new Date().getFullYear()
//获取屏幕高度减去4px是解决在pda上会使用屏幕高度会出现滚动条
state.pageHeight = uni.getSystemInfoSync().windowHeight - 4
})
</script>
<style lang="scss" scoped>
.aboutme {
height: calc(100vh - 70px);
display: flex;
flex-direction: column;
}
.content {
margin: 5% 1%;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.logo {
height: 130px;
width: 130px;
margin-top: 10px;
}
.foot {
margin: 5% 1%;
height: 150px;
display: flex;
color: #666666;
text-align: center;
flex-direction: column;
justify-content: flex-end;
}
.foot-phone {
font-size: 15px;
}
.foot-email {
font-size: 15px;
}
.copyright {
font-size: 15px;
}
</style>

48
pages/me/index.vue Normal file
View File

@@ -0,0 +1,48 @@
<template>
<view class="container">
<view class="x-f pb20 pt20">
<u-avatar src=""></u-avatar> <view class="user-name"><span>您好</span>{{ userInfo.realname }}</view>
</view>
<u-cell-group>
<u-cell icon="setting-fill" title="切换系统" :isLink="true" @click="handleTo('/pages/index/index')"></u-cell>
<u-cell icon="info-circle-fill" title="关于我们" :isLink="true" @click="handleTo('/pages/me/aboutMe')"></u-cell>
</u-cell-group>
<u-button class="mt40" type="warning" :plain="true" text="退出当前账号" @click="handleLoginOut"></u-button>
<my-tabBar :currentTab="1" />
</view>
</template>
<script setup>
import { computed, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import nx from '@/nx'
const userInfo = computed(() => nx.$store('user').userInfo)
function handleTo(url) {
nx.$router.go(url)
}
function handleLoginOut() {
uni.showModal({
title: '提示',
content: `确定退出登录吗?`,
success: res => {
if (res.confirm) {
nx.$store('user').logout()
}
}
})
}
</script>
<style lang="scss" scoped>
.container {
padding: 0 30px;
.user-name {
padding-left: 30px;
font-size: 20px;
}
}
::v-deep .u-button__text {
font-size: 16px !important;
}
</style>

View File

@@ -1,540 +0,0 @@
<template>
<s-layout :bgStyle="{ color: '#f5f7fa' }" class="about-page" title="关于我们">
<u-card :show-head="false" margin="20rpx" padding="40rpx" border-radius="24rpx">
<template #body>
<view class="hero-content">
<u-avatar
v-if="appInfo.logo"
:src="sheep.$url.cdn(appInfo.logo)"
size="120"
bg-color="transparent"
class="hero-logo"
/>
<u-text
:text="displayName"
type="primary"
size="40rpx"
bold
color="#fff"
class="hero-name"
/>
<u-text
:text="`${displayName} 致力于用开源的数字化能力,帮助企业快速构建电商、营销与业务协同平台。`"
size="28rpx"
color="rgba(255,255,255,0.9)"
class="hero-slogan"
/>
<view class="hero-tags">
<u-tag
v-for="tag in heroTags"
:key="tag"
:text="tag"
bg-color="rgba(255,255,255,0.16)"
color="#fff"
size="mini"
shape="circle"
plain
/>
</view>
</view>
</template>
</u-card>
<u-row gutter="16" class="metrics-row">
<u-col span="4" v-for="metric in metrics" :key="metric.title">
<u-card :show-head="false" margin="0" padding="32rpx 24rpx" border-radius="20rpx">
<template #body>
<view class="metric-content">
<u-text
:text="metric.value"
size="48rpx"
bold
type="primary"
class="metric-value"
/>
<u-text
:text="metric.title"
size="26rpx"
bold
color="#303133"
class="metric-title"
/>
<u-text
:text="metric.desc"
size="22rpx"
color="#909399"
class="metric-desc"
/>
</view>
</template>
</u-card>
</u-col>
</u-row>
<u-card :show-head="false" margin="20rpx" padding="32rpx 28rpx" border-radius="24rpx">
<template #body>
<u-text text="我们提供什么" size="32rpx" bold color="#303133" class="section-title" />
<u-text text="围绕企业全链路经营的核心能力" size="24rpx" color="#909399" class="section-subtitle" />
<view class="capability-grid">
<u-card
v-for="item in capabilities"
:key="item.title"
:show-head="false"
margin="0 0 24rpx 0"
padding="28rpx"
border-radius="20rpx"
bg-color="linear-gradient(135deg, rgba(245, 247, 250, 1), rgba(245, 247, 250, 0.6))"
>
<template #body>
<u-text :text="item.title" size="30rpx" bold color="#2c3e50" class="capability-title" />
<u-text :text="item.desc" size="24rpx" color="#606266" class="capability-desc" />
<view class="capability-tags">
<u-tag
v-for="tag in item.tags"
:key="tag"
:text="tag"
bg-color="rgba(0, 85, 162, 0.08)"
color="var(--ui-BG-Main)"
size="mini"
shape="circle"
/>
</view>
</template>
</u-card>
</view>
</template>
</u-card>
<u-card :show-head="false" margin="20rpx" padding="32rpx 28rpx" border-radius="24rpx">
<template #body>
<u-text text="我们的理念" size="32rpx" bold color="#303133" class="section-title" />
<view class="value-list">
<u-card
v-for="item in values"
:key="item.title"
:show-head="false"
margin="0 0 24rpx 0"
padding="24rpx 26rpx"
border-radius="20rpx"
bg-color="rgba(0, 85, 162, 0.05)"
>
<template #body>
<u-text :text="item.title" size="28rpx" bold color="#2c3e50" class="value-title" />
<u-text :text="item.desc" size="24rpx" color="#606266" class="value-desc" />
</template>
</u-card>
</view>
</template>
</u-card>
<u-card :show-head="false" margin="20rpx" padding="32rpx 28rpx" border-radius="24rpx">
<template #body>
<u-text text="发展里程碑" size="32rpx" bold color="#303133" class="section-title" />
<view class="timeline">
<view
class="timeline-item"
v-for="(item, index) in milestones"
:key="item.year"
>
<view class="timeline-dot"></view>
<view class="timeline-content">
<u-text :text="item.year" size="28rpx" bold color="#2c3e50" class="timeline-year" />
<u-text :text="item.title" size="26rpx" bold color="#303133" class="timeline-title" />
<u-text :text="item.desc" size="24rpx" color="#606266" class="timeline-desc" />
</view>
</view>
</view>
</template>
</u-card>
<u-card :show-head="false" margin="20rpx" padding="32rpx 28rpx" border-radius="24rpx">
<template #body>
<u-text text="联系与合作" size="32rpx" bold color="#303133" class="section-title" />
<u-text text="欢迎与我们探讨更多共赢的可能" size="24rpx" color="#909399" class="section-subtitle" />
<view class="contact-list">
<u-cell
v-for="item in contacts"
:key="item.label"
:title="item.label"
:label="item.value"
:is-link="true"
arrow-direction="right"
bg-color="rgba(245, 247, 250, 0.8)"
title-style="font-size: 26rpx; font-weight: 600; color: #2c3e50;"
label-style="font-size: 24rpx; color: #606266; word-break: break-all; line-height: 34rpx;"
@click="handleContact(item)"
class="contact-cell"
/>
</view>
</template>
</u-card>
<u-text
text="移动商城 始终坚持开源共享,与开发者和企业伙伴一起打造可持续的数字化生态。"
size="24rpx"
color="#909399"
align="center"
class="footer-hint"
/>
</s-layout>
</template>
<script setup>
import { computed } from 'vue';
import sheep from '@/sheep';
const appInfo = computed(() => sheep.$store('app').info || {});
const displayName = computed(() => appInfo.value.name || '移动商城');
const heroTags = [
'100% 开源可商用',
'支持多端统一交付',
'沉淀企业级最佳实践'
];
const metrics = [
{
title: '开源历程',
value: '7+',
desc: '年持续迭代与社区共建'
},
{
title: '行业方案',
value: '20+',
desc: '覆盖零售、制造、政企等场景'
},
{
title: '交付效率',
value: '30%',
desc: '平均缩短客户上线周期'
}
];
const capabilities = [
{
title: '产品矩阵',
desc: '以电商中台为核心,延展直播带货、会员营销、全渠道订单与履约管理。',
tags: ['商城交易', '营销裂变', '数据运营']
},
{
title: '技术架构',
desc: '基于 Spring Cloud Alibaba + Vue3 + UniApp支持多租户、微服务与多端同源。',
tags: ['微服务', '多端一致', '低代码装修']
},
{
title: '服务体系',
desc: '提供从咨询、定制、交付到长期运营的全生命周期陪伴式服务。',
tags: ['数字化顾问', '驻场交付', '持续运营']
}
];
const values = [
{
title: '使命',
desc: '让每一家企业都能以更低成本、更快速度完成业务数字化升级。'
},
{
title: '愿景',
desc: '打造开放、可信赖、可持续的企业级数字商业生态。'
},
{
title: '价值观',
desc: '客户成功、极致体验、持续创新、坦诚协作。'
}
];
const milestones = [
{
year: '2018',
title: '开源起航',
desc: '发布首个电商项目原型,与社区开发者共同启动开源计划。'
},
{
year: '2020',
title: '企业级升级',
desc: '引入多租户、权限与大促能力,满足成长型企业核心诉求。'
},
{
year: '2022',
title: '多端一体',
desc: '统一 H5、App 与小程序技术栈,实现一次开发,多端交付。'
},
{
year: '2024',
title: '智能加速',
desc: '引入智能客服、营销推荐等 AI 能力,提升运营效率。'
},
{
year: '2025',
title: '生态共建',
desc: '联合生态伙伴共建行业方案,形成开放合作的生态体系。'
}
];
const contacts = [
{
label: '商务合作',
value: 'business@iocoder.cn',
type: 'email'
},
{
label: '客服热线',
value: '400-860-888',
type: 'phone',
actionValue: '400860888'
},
{
label: '开源仓库',
value: 'https://github.com/YunaiV/ruoyi-vue-pro',
type: 'link'
},
{
label: '文档中心',
value: 'https://doc.iocoder.cn',
type: 'link'
},
{
label: '办公地址',
value: '浙江省杭州市滨江区江南大道 777 号数字产业园',
type: 'text'
}
];
function handleContact(item) {
if (item.type === 'phone') {
const phoneNumber = item.actionValue || item.value.replace(/[^0-9]/g, '');
if (!phoneNumber) {
return;
}
uni.makePhoneCall({
phoneNumber,
fail: () => {
uni.setClipboardData({
data: item.value,
success: () => {
uni.showToast({ title: '号码已复制', icon: 'none' });
}
});
}
});
return;
}
if (item.type === 'link') {
sheep.$router.go('/pages/public/webview', {
title: item.label,
url: encodeURIComponent(item.value)
});
uni.setClipboardData({
data: item.value,
success: () => {
uni.showToast({ title: '链接已复制', icon: 'none' });
}
});
return;
}
if (item.type === 'text') {
uni.setClipboardData({
data: item.value,
success: () => {
uni.showToast({ title: '地址已复制', icon: 'none' });
}
});
return;
}
uni.setClipboardData({
data: item.value,
success: () => {
uni.showToast({ title: '信息已复制', icon: 'none' });
}
});
}
</script>
<style lang="scss" scoped>
.about-page {
min-height: 100vh;
padding: 0;
background: #f5f7fa;
box-sizing: border-box;
}
// Hero card 渐变背景
:deep(.u-card) {
&:first-child {
background: linear-gradient(135deg, rgba(0, 85, 162, 0.95), rgba(0, 85, 162, 0.68));
box-shadow: 0 16rpx 36rpx rgba(0, 85, 162, 0.25);
color: #fff;
}
}
.hero-content {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
.hero-logo {
margin-bottom: 24rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.15);
padding: 16rpx;
}
.hero-name {
margin-bottom: 16rpx;
}
.hero-slogan {
margin-bottom: 28rpx;
line-height: 42rpx;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
}
.metrics-row {
margin: 20rpx;
}
.metric-content {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
.metric-value {
margin-bottom: 8rpx;
}
.metric-title {
margin-bottom: 6rpx;
}
.metric-desc {
line-height: 32rpx;
}
}
.section-title {
margin-bottom: 12rpx;
}
.section-subtitle {
margin-bottom: 24rpx;
}
.capability-grid {
display: flex;
flex-direction: column;
}
.capability-title {
margin-bottom: 12rpx;
}
.capability-desc {
margin-bottom: 16rpx;
line-height: 38rpx;
}
.capability-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
}
.value-list {
display: flex;
flex-direction: column;
}
.value-title {
margin-bottom: 12rpx;
}
.value-desc {
line-height: 36rpx;
}
.timeline {
position: relative;
margin-left: 20rpx;
padding-left: 20rpx;
border-left: 2rpx solid rgba(0, 85, 162, 0.2);
display: flex;
flex-direction: column;
gap: 32rpx;
.timeline-item {
position: relative;
.timeline-dot {
position: absolute;
left: -31rpx;
top: 6rpx;
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: var(--ui-BG-Main);
box-shadow: 0 0 0 6rpx rgba(0, 85, 162, 0.15);
}
.timeline-content {
padding-left: 12rpx;
.timeline-year {
margin-bottom: 6rpx;
}
.timeline-title {
margin-bottom: 6rpx;
}
.timeline-desc {
line-height: 36rpx;
}
}
}
}
.contact-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.contact-cell {
border-radius: 18rpx;
overflow: hidden;
}
.footer-hint {
padding: 24rpx 36rpx 80rpx;
line-height: 36rpx;
}
@media (min-width: 750px) {
.metrics-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.capability-grid,
.value-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24rpx;
}
}
</style>

View File

@@ -1,60 +0,0 @@
<!-- 错误界面 -->
<template>
<view class="error-page">
<s-empty
v-if="errCode === 'NetworkError'"
icon="/static/internet-empty.png"
text="网络连接失败"
showAction
actionText="重新连接"
@clickAction="onReconnect"
buttonColor="#ff3000"
/>
<s-empty
v-else-if="errCode === 'TemplateError'"
icon="/static/internet-empty.png"
text="未找到模板,请前往后台启用对应模板"
showAction
actionText="重新加载"
@clickAction="onReconnect"
buttonColor="#ff3000"
/>
<s-empty
v-else-if="errCode !== ''"
icon="/static/internet-empty.png"
:text="errMsg"
showAction
actionText="重新加载"
@clickAction="onReconnect"
buttonColor="#ff3000"
/>
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { ref } from 'vue';
import { ShoproInit } from '@/sheep';
const errCode = ref('');
const errMsg = ref('');
onLoad((options) => {
errCode.value = options.errCode;
errMsg.value = options.errMsg;
});
// 重新连接
async function onReconnect() {
uni.reLaunch({
url: '/pages/index/menu',
});
await ShoproInit();
}
</script>
<style lang="scss" scoped>
.error-page {
width: 100%;
}
</style>

View File

@@ -1,15 +0,0 @@
<!-- 页面已移除 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" class="set-wrap" title="页面已下线">
<s-empty text="该页面已下线" icon="/static/internet-empty.png" />
</s-layout>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.set-wrap {
min-height: 100vh;
}
</style>

View File

@@ -1,45 +0,0 @@
<!-- 文章展示 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" :title="state.title" class="set-wrap">
<view class="ss-p-30 richtext"><mp-html :content="state.content"></mp-html></view>
</s-layout>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
// Article API removed with promotion module
const state = reactive({
title: '',
content: '',
});
async function getRichTextContent(id, title) {
// Article API has been removed as it was part of promotion module
state.title = title || '内容';
state.content = '暂无内容';
}
onLoad((options) => {
if (options.title) {
state.title = options.title;
uni.setNavigationBarTitle({
title: state.title,
});
}
getRichTextContent(options.id, options.title);
});
</script>
<style lang="scss" scoped>
.set-title {
margin: 0 30rpx;
}
:deep() {
image {
display: block;
}
}
</style>

View File

@@ -1,236 +0,0 @@
<template>
<s-layout :bgStyle="{ color: '#fff' }" class="set-wrap" title="系统设置">
<view class="header-box ss-flex-col ss-row-center ss-col-center">
<image
class="logo-img ss-m-b-46"
:src="sheep.$url.cdn(appInfo.logo)"
mode="aspectFit"
></image>
<view class="name ss-m-b-24">{{ appInfo.name }}</view>
</view>
<view class="container-list">
<uni-list :border="false">
<uni-list-item
title="当前版本"
:rightText="appInfo.version"
showArrow
clickable
:border="false"
class="list-border"
@tap="onCheckUpdate"
/>
<uni-list-item
title="本地缓存"
:rightText="storageSize"
showArrow
:border="false"
class="list-border"
/>
<uni-list-item
title="关于我们"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '关于我们'
})
"
/>
<!-- 为了过审 只有 iOS-App 有注销账号功能 -->
<uni-list-item
v-if="isLogin && sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
title="注销账号"
rightText=""
showArrow
clickable
:border="false"
class="list-border"
@click="onLogoff"
/>
</uni-list>
</view>
<view class="set-footer ss-flex-col ss-row-center ss-col-center">
<view class="agreement-box ss-flex ss-col-center ss-m-b-40">
<view class="ss-flex ss-col-center ss-m-b-10">
<view
class="tcp-text"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '用户协议'
})
"
>
用户协议
</view>
<view class="agreement-text"></view>
<view
class="tcp-text"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '隐私协议'
})
"
>
隐私协议
</view>
</view>
</view>
<view class="copyright-text ss-m-b-10">{{ appInfo.copyright }}</view>
<view class="copyright-text">{{ appInfo.copytime }}</view>
</view>
<su-fixed bottom placeholder>
<view class="ss-p-x-20 ss-p-b-40">
<button
class="loginout-btn ss-reset-button ui-BG-Main ui-Shadow-Main"
@tap="onLogout"
v-if="isLogin"
>
退出登录
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive } from 'vue';
import AuthUtil from '@/sheep/api/system/auth';
const appInfo = computed(() => sheep.$store('app').info);
const isLogin = computed(() => sheep.$store('user').isLogin);
const storageSize = uni.getStorageInfoSync().currentSize + 'Kb';
const state = reactive({
showModal: false,
});
function onCheckUpdate() {
sheep.$platform.checkUpdate();
// 小程序初始化时已检查更新
// H5实时更新无需检查
// App 1.跳转应用市场更新 2.手动热更新 3.整包更新
}
// 注销账号
function onLogoff() {
uni.showModal({
title: '提示',
content: '确认注销账号?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await AuthUtil.logout();
if (code !== 0) {
return;
}
sheep.$store('user').logout();
sheep.$router.go('/pages/index/user');
},
});
}
// 退出账号
function onLogout() {
uni.showModal({
title: '提示',
content: '确认退出账号?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await AuthUtil.logout();
if (code !== 0) {
return;
}
sheep.$store('user').logout();
sheep.$router.go('/pages/index/user');
},
});
}
</script>
<style lang="scss" scoped>
.container-list {
width: 100%;
}
.set-title {
margin: 0 30rpx;
}
.header-box {
padding: 100rpx 0;
.logo-img {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
.name {
font-size: 42rpx;
font-weight: 400;
color: $dark-3;
}
.version {
font-size: 32rpx;
font-weight: 500;
line-height: 32rpx;
color: $gray-b;
}
}
.set-footer {
margin: 100rpx 0 0 0;
.copyright-text {
font-size: 22rpx;
font-weight: 500;
color: $gray-c;
line-height: 30rpx;
}
.agreement-box {
font-size: 26rpx;
font-weight: 500;
.tcp-text {
color: var(--ui-BG-Main);
}
.agreement-text {
color: $dark-9;
}
}
}
.loginout-btn {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
}
.list-border {
font-size: 28rpx;
font-weight: 400;
color: #333333;
border-bottom: 2rpx solid #eeeeee;
}
:deep(.uni-list-item__content-title) {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
:deep(.uni-list-item__extra-text) {
color: #bbbbbb;
font-size: 28rpx;
}
</style>

View File

@@ -1,18 +0,0 @@
<!-- 网页加载 -->
<template>
<view>
<web-view :src="url" />
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { ref } from 'vue';
const url = ref('');
onLoad((options) => {
url.value = decodeURIComponent(options.url);
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,122 @@
<template>
<view class="container">
<uni-section type="line" title="基础信息选择"> </uni-section>
<uni-forms label-position="top">
<uni-forms-item label="区域" name="areaCode">
<uni-data-select
:clear="false"
v-model="areaCode"
:localdata="areaCodeList"
@change="handleAreaChange"
></uni-data-select>
</uni-forms-item>
<uni-forms-item v-if="areaCode" label="岗位" name="postCode">
<uni-data-select
:clear="false"
v-model="postCode"
:localdata="postCodeList"
@change="handlePostChange"
></uni-data-select>
</uni-forms-item>
<uni-forms-item v-if="postCode && postCode === weighingPostCode" label="计量点" name="scaleNo">
<uni-data-select :clear="false" v-model="scaleNo" :localdata="scaleNoList"></uni-data-select>
</uni-forms-item>
</uni-forms>
<u-button type="primary" @click="submitForm">保存</u-button>
<u-loading-page :loading="loadingPage"></u-loading-page>
</view>
</template>
<script>
// import { getAreaCodeConfig, getScaleNoConfig, getPostList } from '@/nxTemp/apis/sys.api'
export default {
data() {
return {
weighingPostCode: '监磅',
areaCode: '',
scaleNo: '',
postCode: '',
areaCodeList: [],
scaleNoList: [],
postCodeList: [],
loadingPage: false
}
},
async onLoad() {
this.loadingPage = true
try {
this.areaCodeList = (await getAreaCodeConfig()).map(item => ({ text: item.areaName, value: item.areaCode }))
this.postCodeList = (await getPostList()).map(item => ({ text: item.name, value: item.name }))
if (this.areaCode) {
this.getScaleNoList()
}
this.loadingPage = false
} catch (error) {
this.loadingPage = false
}
},
mounted() {
this.areaCode = this.$store.state.areaCode
this.scaleNo = this.$store.state.scaleNo
this.postCode = this.$store.state.postCode
},
methods: {
async getScaleNoList() {
if (this.postCode && this.postCode === this.weighingPostCode) {
// this.scaleNo = ''
this.scaleNoList = (await getScaleNoConfig({ areaCode: this.areaCode })).map(item => ({
text: item.pointName,
value: item.pointCode
}))
}
},
async handleAreaChange() {
this.postCode = ''
this.scaleNo = ''
},
async handlePostChange() {
this.scaleNo = ''
this.getScaleNoList()
},
async goIndex() {
await this.$store.dispatch('getRoleMenus', this.$store.state.postCode)
uni.reLaunch({
url: 'pages/lims/index/index'
})
},
submitForm() {
if (this.areaCode && this.postCode && this.postCode !== this.weighingPostCode) {
this.$store.commit('SET_AREA_CODE', this.areaCode)
this.$store.commit('SET_POST_CODE', this.postCode)
this.goIndex()
} else if (this.postCode && this.postCode === this.weighingPostCode) {
if (this.scaleNo) {
this.$store.commit('SET_AREA_CODE', this.areaCode)
this.$store.commit('SET_POST_CODE', this.postCode)
this.$store.commit('SET_SCALE_NO', this.scaleNo)
this.goIndex()
} else {
uni.showToast({
title: '请选择',
icon: 'none'
})
}
} else {
uni.showToast({
title: '请选择',
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 30rpx;
}
::v-deep .uni-section-header__decoration {
background-color: $uni-color-primary !important;
}
</style>

109
pages/setting/UrlConfig.vue Normal file
View File

@@ -0,0 +1,109 @@
<template>
<view class="container">
<uni-section type="line" title="请求地址和租户ID配置" @click="handleClick"> </uni-section>
<uni-forms label-position="top" label-width="100">
<uni-forms-item label="系统请求地址">
<uni-easyinput v-model="baseUrl" placeholder="请输入系统请求地址"></uni-easyinput>
</uni-forms-item>
<uni-forms-item v-if="showContent" label="租户ID">
<uni-easyinput v-model="tenantId" placeholder="请输入租户ID"></uni-easyinput>
</uni-forms-item>
<uni-forms-item v-if="showContent" label="app更新地址">
<uni-easyinput v-model="upgradeBaseUrl" placeholder="请输入"></uni-easyinput>
</uni-forms-item>
</uni-forms>
<u-button type="primary" @click="submitForm">保存</u-button>
<u-button v-if="showContent" type="primary" @click="clearCache">清除缓存</u-button>
</view>
</template>
<script>
export default {
data() {
return {
baseUrl: '',
tenantId: '',
upgradeBaseUrl: '',
clickCount: 0, // 初始化点击次数为0
threshold: 5, //设置点击次数的阈值
thresholdTime: 2, // 设置连续点击的时间阈值(秒)
timer: null
}
},
computed: {
showContent() {
return this.clickCount >= this.threshold
}
},
mounted() {
this.baseUrl = uni.getStorageSync('base_url')
this.tenantId = uni.getStorageSync('tenant_id')
this.upgradeBaseUrl = uni.getStorageSync('upgradeBaseUrl')
},
methods: {
clearCache() {
uni.showModal({
title: '提示',
content: '确定要清空缓存?',
success: function (res) {
if (res.confirm) {
try {
uni.clearStorageSync()
plus.runtime.restart()
} catch (e) {}
}
}
})
},
submitForm() {
if (!this.baseUrl) {
uni.showToast({
title: '请输入地址和租户',
icon: 'none'
})
return
}
uni.setStorageSync('base_url', this.baseUrl)
uni.setStorageSync('tenant_id', this.tenantId)
uni.setStorageSync('upgradeBaseUrl', this.upgradeBaseUrl)
uni.showToast({
title: '保存成功',
icon: 'none'
})
uni.navigateBack()
},
handleClick() {
// 如果达到阈值,显示内容并重置计数
if (this.showContent) {
clearTimeout(this.timer)
return
}
// 增加点击次数
this.clickCount++
uni.showToast({
title: `再点击${this.threshold - this.clickCount}次可显示内容`,
icon: 'none',
duration: 1000
})
clearTimeout(this.timer)
// 清除之前的定时器(如果有),设置新的定时器
this.timer = setTimeout(() => {
this.resetClickCount()
}, this.thresholdTime * 1000)
},
resetClickCount() {
if (!this.showContent) {
this.clickCount = 0
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 30px;
}
</style>

View File

@@ -0,0 +1,507 @@
<template>
<view class="mask flex-center">
<view class="content botton-radius">
<view class="content-top">
<text class="content-top-text">{{ title }}</text>
<image
class="content-top"
style="top: 0"
width="100%"
height="100%"
src="../../static/images/app_update_bg_top.png"
>
</image>
</view>
<view class="content-header"></view>
<view class="content-body">
<view class="title">
<text>{{ subTitle }}</text>
<!-- <text style="padding-left:20rpx;font-size: 0.5em;color: #666;">v.{{version}}</text> -->
</view>
<view class="body">
<scroll-view class="box-des-scroll" scroll-y="true">
<text class="box-des">
{{ contents }}
</text>
</scroll-view>
</view>
<view class="footer flex-center">
<template v-if="isiOS">
<button class="content-button" style="border: none; color: #fff" plain @click="jumpToAppStore">
{{ downLoadBtnTextiOS }}
</button>
</template>
<template v-else>
<template v-if="!downloadSuccess">
<view class="progress-box flex-column" v-if="downloading">
<progress
class="progress"
border-radius="35"
:percent="downLoadPercent"
activeColor="#3DA7FF"
show-info
stroke-width="10"
/>
<view style="width: 100%; font-size: 14px; display: flex; justify-content: space-around">
<text>{{ downLoadingText }}</text>
<text>({{ downloadedSize }}/{{ packageFileSize }}M)</text>
</view>
</view>
<button v-else class="content-button" style="border: none; color: #fff" plain @click="downloadPackage">
{{ downLoadBtnText }}
</button>
</template>
<button
v-else-if="downloadSuccess"
class="content-button"
style="border: none; color: #fff"
plain
:loading="installing"
:disabled="installing"
@click="installPackage"
>
{{ installing ? '正在安装……' : '下载完成,立即安装' }}
</button>
<button
v-if="installed && isWGT"
class="content-button"
style="border: none; color: #fff"
plain
@click="restart"
>
安装完毕点击重启
</button>
</template>
</view>
</view>
<image
v-if="!is_mandatory"
class="close-img"
src="../../static/images/app_update_close.png"
@click.stop="closeUpdate"
></image>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
const localFilePathKey = '__localFilePath__'
const platform_iOS = 'iOS'
let downloadTask = null
// 工具函数保持原样
function compare(v1 = '0', v2 = '0') {
v1 = String(v1).split('.')
v2 = String(v2).split('.')
const minVersionLens = Math.min(v1.length, v2.length)
let result = 0
for (let i = 0; i < minVersionLens; i++) {
const curV1 = Number(v1[i])
const curV2 = Number(v2[i])
if (curV1 > curV2) {
result = 1
break
} else if (curV1 < curV2) {
result = -1
break
}
}
if (result === 0 && v1.length !== v2.length) {
const v1BiggerThenv2 = v1.length > v2.length
const maxLensVersion = v1BiggerThenv2 ? v1 : v2
for (let i = minVersionLens; i < maxLensVersion.length; i++) {
const curVersion = Number(maxLensVersion[i])
if (curVersion > 0) {
v1BiggerThenv2 ? (result = 1) : (result = -1)
break
}
}
}
return result
}
// 响应式状态
const installForBeforeFilePath = ref('')
const installed = ref(false)
const installing = ref(false)
const downloadSuccess = ref(false)
const downloading = ref(false)
const downLoadPercent = ref(0)
const downloadedSize = ref(0)
const packageFileSize = ref(0)
const tempFilePath = ref('')
const title = ref('更新日志')
const contents = ref('')
const is_mandatory = ref(false)
const subTitle = ref('发现新版本')
const downLoadBtnTextiOS = ref('立即跳转更新')
const downLoadBtnText = ref('立即下载更新')
const downLoadingText = ref('安装包下载中,请稍后')
const version = ref('')
const url = ref('')
const type = ref('')
const platform = ref('')
// 计算属性
const isWGT = computed(() => type.value === 'wgt')
const isiOS = computed(() => !isWGT.value && platform.value.includes(platform_iOS))
// 方法
function checkLocalStoragePackage() {
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
if (localFilePathRecord) {
const { version: recordVersion, savedFilePath, installed: recordInstalled } = localFilePathRecord
if (!recordInstalled && compare(recordVersion, version.value) === 0) {
downloadSuccess.value = true
installForBeforeFilePath.value = savedFilePath
tempFilePath.value = savedFilePath
} else {
deleteSavedFile(savedFilePath)
}
}
}
async function closeUpdate() {
if (downloading.value) {
if (is_mandatory.value) {
uni.showToast({ title: '下载中,请稍后……', icon: 'none', duration: 500 })
return
}
uni.showModal({
title: '是否取消下载?',
cancelText: '否',
confirmText: '是',
success: res => {
if (res.confirm) {
downloadTask?.abort()
uni.navigateBack()
}
}
})
return
}
if (downloadSuccess.value && tempFilePath.value) {
await saveFile(tempFilePath.value, version.value)
uni.navigateBack()
return
}
uni.navigateBack()
}
function downloadPackage() {
downloading.value = true
downloadTask = uni.downloadFile({
url: url.value,
success: res => {
if (res.statusCode === 200) {
downloadSuccess.value = true
tempFilePath.value = res.tempFilePath
// 强制更新
if (is_mandatory.value) {
installPackage()
}
}
},
complete: () => {
downloading.value = false
downLoadPercent.value = 0
downloadedSize.value = 0
packageFileSize.value = 0
downloadTask = null
}
})
downloadTask.onProgressUpdate(res => {
downLoadPercent.value = res.progress
downloadedSize.value = (res.totalBytesWritten / 1024 ** 2).toFixed(2)
packageFileSize.value = (res.totalBytesExpectedToWrite / 1024 ** 2).toFixed(2)
})
}
function installPackage() {
// #ifdef APP-PLUS
if (isWGT.value) {
installing.value = true
}
plus.runtime.install(
tempFilePath.value,
{
force: false
},
async () => {
installing.value = false
installed.value = true
if (isWGT.value) {
if (is_mandatory.value) {
uni.showLoading({ icon: 'none', title: '安装成功,正在重启……' })
setTimeout(() => {
uni.hideLoading()
restart()
}, 1000)
}
} else {
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
uni.setStorageSync(localFilePathKey, {
...localFilePathRecord,
installed: true
})
}
},
async err => {
if (installForBeforeFilePath.value) {
await deleteSavedFile(installForBeforeFilePath.value)
installForBeforeFilePath.value = ''
}
installing.value = false
installed.value = false
uni.showModal({
title: `更新失败${isWGT.value ? '' : 'APK文件不存在'},请重新下载`,
content: err.message,
showCancel: false
})
}
)
if (!isWGT.value) {
uni.navigateBack()
}
// #endif
}
function restart() {
installed.value = false
// #ifdef APP-PLUS
plus.runtime.restart()
// #endif
}
async function saveFile(tempFilePath, version) {
try {
const res = await uni.saveFile({ tempFilePath })
uni.setStorageSync(localFilePathKey, {
version,
savedFilePath: res.savedFilePath
})
} catch (err) {
console.error('保存文件失败:', err)
}
}
async function deleteSavedFile(filePath) {
try {
await uni.removeSavedFile({ filePath })
uni.removeStorageSync(localFilePathKey)
} catch (err) {
console.error('删除文件失败:', err)
}
}
function jumpToAppStore() {
plus.runtime.openURL(url.value)
}
// 生命周期
onLoad(params => {
const { local_storage_key } = params
if (!local_storage_key) {
console.error('local_storage_key为空')
// uni.navigateBack()
return
}
const localPackageInfo = uni.getStorageSync(local_storage_key)
if (!localPackageInfo) {
console.error('安装包信息为空')
uni.navigateBack()
return
}
const requiredKeys = ['version', 'url', 'type']
for (const key of requiredKeys) {
if (!localPackageInfo[key]) {
console.error(`参数 ${key} 必填`)
uni.navigateBack()
return
}
}
// 更新响应式状态
version.value = localPackageInfo.version
url.value = localPackageInfo.url
type.value = localPackageInfo.type
platform.value = localPackageInfo.platform || ''
title.value = localPackageInfo.title || '更新日志'
contents.value = localPackageInfo.contents || ''
is_mandatory.value = localPackageInfo.is_mandatory || false
subTitle.value = localPackageInfo.subTitle || '发现新版本'
downLoadBtnTextiOS.value = localPackageInfo.downLoadBtnTextiOS || '立即跳转更新'
downLoadBtnText.value = localPackageInfo.downLoadBtnText || '立即下载更新'
downLoadingText.value = localPackageInfo.downLoadingText || '安装包下载中,请稍后'
checkLocalStoragePackage()
})
onBackPress(() => {
if (is_mandatory.value) return true
if (downloadTask) downloadTask.abort()
return false
})
</script>
<style>
page {
background: transparent;
}
.flex-center {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
}
.mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
}
.botton-radius {
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
}
.content {
position: relative;
top: 0;
width: 400px;
background-color: #fff;
box-sizing: border-box;
padding: 0 30px;
font-family: Source Han Sans CN;
max-height: 80vh;
overflow: hidden;
border-radius: 20px;
}
.content-top {
position: absolute;
top: -55px;
left: 0;
width: 400px;
height: 180px;
}
.content-top-text {
font-size: 24px;
font-weight: bold;
color: #f8f8fa;
position: absolute;
top: 80px;
left: 30px;
z-index: 1;
}
.content-header {
padding-top: 80px;
height: 40px;
}
.content-body {
padding-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
color: #3da7ff;
line-height: 1.5;
margin-bottom: 15px;
}
.footer {
height: 90px;
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 15px;
}
.box-des-scroll {
box-sizing: border-box;
padding: 0 20px;
height: 50px;
text-align: left;
margin-bottom: 15px;
}
.box-des {
font-size: 16px;
color: #000000;
line-height: 1.5;
}
.progress-box {
width: 100%;
}
.progress {
width: 90%;
height: 20px;
border-radius: 10px;
margin-bottom: 10px;
}
.close-img {
width: 40px;
height: 40px;
z-index: 1000;
position: absolute;
bottom: -70px;
left: calc(50% - 20px);
}
.content-button {
text-align: center;
flex: 1;
font-size: 18px;
font-weight: 400;
color: #ffffff;
border-radius: 20px;
margin: 0 10px;
height: 50px;
line-height: 50px;
background: linear-gradient(to right, #1785ff, #3da7ff);
min-width: 160px;
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -1,410 +0,0 @@
<!-- 用户信息 -->
<template>
<s-layout title="用户信息" class="set-userinfo-wrap">
<uni-forms
:model="state.model"
:rules="state.rules"
labelPosition="left"
border
class="form-box"
>
<!-- 头像 -->
<view class="ss-flex ss-row-center ss-col-center ss-p-t-60 ss-p-b-0 bg-white">
<view class="header-box-content">
<s-avatar
:src="state.model?.avatar"
:nickname="state.model?.nickname || state.model?.username"
:size="160"
:editable="true"
:bg-color="getAvatarBgColor(state.model?.nickname || state.model?.username)"
@click="onChangeAvatar"
/>
</view>
</view>
<view class="bg-white ss-p-x-30">
<!-- 昵称 + 性别 -->
<uni-forms-item name="nickname" label="昵称">
<uni-easyinput
v-model="state.model.nickname"
type="nickname"
placeholder="设置昵称"
:inputBorder="false"
:placeholderStyle="placeholderStyle"
/>
</uni-forms-item>
<uni-forms-item name="sex" label="性别">
<view class="ss-flex ss-col-center ss-h-100">
<radio-group @change="onChangeGender" class="ss-flex ss-col-center">
<label class="radio" v-for="item in sexRadioMap" :key="item.value">
<view class="ss-flex ss-col-center ss-m-r-32">
<radio
:value="item.value"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
:checked="parseInt(item.value) === state.model?.sex"
/>
<view class="gender-name">{{ item.name }}</view>
</view>
</label>
</radio-group>
</view>
</uni-forms-item>
<uni-forms-item name="mobile" label="手机号" @tap="onChangeMobile">
<uni-easyinput
v-model="userInfo.mobile"
placeholder="请绑定手机号"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
>
<template v-slot:right>
<view class="ss-flex ss-col-center">
<su-radio v-if="userInfo.verification?.mobile" :modelValue="true" />
<button v-else class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx"></text>
</button>
</view>
</template>
</uni-easyinput>
</uni-forms-item>
</view>
</uni-forms>
<!-- 当前社交平台的绑定关系只处理 wechat 微信场景 -->
<view v-if="sheep.$platform.name !== 'H5'">
<view class="title-box ss-p-l-30">第三方账号绑定</view>
<view class="account-list ss-flex ss-row-between">
<view v-if="'WechatOfficialAccount' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/WechatOfficialAccount.png')"
/>
<text class="list-name">微信公众号</text>
</view>
<view v-if="'WechatMiniProgram' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/WechatMiniProgram.png')"
/>
<text class="list-name">微信小程序</text>
</view>
<view v-if="'App' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
/>
<text class="list-name">微信开放平台</text>
</view>
<view class="ss-flex ss-col-center">
<view class="info ss-flex ss-col-center" v-if="state.thirdInfo">
<image class="avatar ss-m-r-20" :src="sheep.$url.cdn(state.thirdInfo.avatar)" />
<text class="name">{{ state.thirdInfo.nickname }}</text>
</view>
<view class="bind-box ss-m-l-20">
<button
v-if="state.thirdInfo.openid"
class="ss-reset-button relieve-btn"
@tap="unBindThirdOauth"
>
解绑
</button>
<button v-else class="ss-reset-button bind-btn" @tap="bindThirdOauth">绑定</button>
</view>
</view>
</view>
</view>
<su-fixed bottom placeholder bg="none">
<view class="footer-box ss-p-20">
<button class="ss-rest-button logout-btn ui-Shadow-Main" @tap="onSubmit">保存</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import { computed, reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
import { clone } from 'lodash-es';
import UserApi from '@/sheep/api/system/user';
import { getAvatarBgColor } from '@/sheep/utils/avatar.js';
import {
chooseAndUploadFile,
uploadFilesFromPath,
} from '@/sheep/components/s-uploader/choose-and-upload-file';
const state = reactive({
model: {}, // 个人信息
rules: {},
thirdInfo: {}, // 社交用户的信息
});
const placeholderStyle = 'color:#BBBBBB;font-size:28rpx;line-height:normal';
const sexRadioMap = [
{
name: '男',
value: '1',
},
{
name: '女',
value: '2',
},
];
const userInfo = computed(() => sheep.$store('user').userInfo);
// 选择性别
function onChangeGender(e) {
state.model.sex = e.detail.value;
}
// 修改手机号
const onChangeMobile = () => {
uni.navigateTo({
url: '/pages/login/index?authType=changeMobile'
});
};
// 选择微信的头像,进行上传
async function onChooseAvatar(e) {
debugger;
const tempUrl = e.detail.avatarUrl || '';
if (!tempUrl) return;
const files = await uploadFilesFromPath(tempUrl);
if (files.length > 0) {
state.model.avatar = files[0].url;
}
}
// 手动选择头像,进行上传
async function onChangeAvatar() {
const files = await chooseAndUploadFile({ type: 'image' });
if (files.length > 0) {
state.model.avatar = files[0].url;
}
}
// 绑定第三方账号
async function bindThirdOauth() {
let result = await sheep.$platform.useProvider('wechat').bind();
if (result) {
await getUserInfo();
}
}
// 解绑第三方账号
function unBindThirdOauth() {
uni.showModal({
title: '解绑提醒',
content: '解绑后您将无法通过微信登录此账号',
cancelText: '再想想',
confirmText: '确定',
success: async function (res) {
if (!res.confirm) {
return;
}
const result = await sheep.$platform.useProvider('wechat').unbind(state.thirdInfo.openid);
if (result) {
await getUserInfo();
}
},
});
}
// 保存信息
async function onSubmit() {
const { code } = await UserApi.updateUser({
avatar: state.model.avatar,
nickname: state.model.nickname,
sex: state.model.sex,
});
if (code === 0) {
await getUserInfo();
}
}
// 获得用户信息
const getUserInfo = async () => {
// 个人信息
const userInfo = await sheep.$store('user').getInfo();
state.model = clone(userInfo);
// 获得社交用户的信息
if (sheep.$platform.name !== 'H5') {
const result = await sheep.$platform.useProvider('wechat').getInfo();
state.thirdInfo = result || {};
}
};
onBeforeMount(() => {
getUserInfo();
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-file-picker {
border-radius: 50%;
}
.uni-file-picker__container {
margin: -14rpx -12rpx;
}
.file-picker__progress {
height: 0 !important;
}
.uni-list-item__content-title {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.uni-icons {
font-size: 40rpx !important;
}
.is-disabled {
color: #333333;
}
}
:deep(.disabled) {
opacity: 1;
}
.gender-name {
font-size: 28rpx;
font-weight: 500;
line-height: normal;
color: #333333;
}
.title-box {
font-size: 28rpx;
font-weight: 500;
color: #666666;
line-height: 100rpx;
}
.logout-btn {
width: 710rpx;
height: 80rpx;
background: var(--gradient-horizontal-primary, linear-gradient(90deg, #0055A2, rgba(0, 85, 162, 0.6)));
border-radius: 40rpx;
font-size: 30rpx;
font-weight: 500;
color: $white;
transition: all 0.3s ease;
&:active {
background: linear-gradient(90deg, #003F73, rgba(0, 63, 115, 0.6));
transform: scale(0.98);
}
}
.radio-dark {
filter: grayscale(100%);
filter: gray;
opacity: 0.4;
}
.content-img {
border-radius: 50%;
}
.header-box-content {
position: relative;
width: 160rpx;
height: 160rpx;
overflow: hidden;
border-radius: 50%;
}
.avatar-action {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
z-index: 1;
width: 160rpx;
height: 46rpx;
background: rgba(#000000, 0.3);
.avatar-action-btn {
width: 160rpx;
height: 46rpx;
font-weight: 500;
font-size: 24rpx;
color: #ffffff;
}
}
// 绑定项
.account-list {
background-color: $white;
height: 100rpx;
padding: 0 20rpx;
.list-img {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.list-name {
font-size: 28rpx;
color: #333333;
}
.info {
.avatar {
width: 38rpx;
height: 38rpx;
border-radius: 50%;
overflow: hidden;
}
.name {
font-size: 28rpx;
font-weight: 400;
color: $dark-9;
}
}
.bind-box {
width: 100rpx;
height: 50rpx;
line-height: normal;
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
.bind-btn {
width: 100%;
height: 100%;
border-radius: 25rpx;
background: #f4f4f4;
color: #999999;
}
.relieve-btn {
width: 100%;
height: 100%;
border-radius: 25rpx;
background: var(--ui-BG-Main-opacity-1);
color: var(--ui-BG-Main);
}
}
}
image {
width: 100%;
height: 100%;
}
</style>