点口管理

This commit is contained in:
2026-06-10 17:26:36 +08:00
parent 787ad93dee
commit 810d2bfc40
12 changed files with 571 additions and 810 deletions
-2
View File
@@ -3,8 +3,6 @@ import {
post
} from '@/utils/request.js'
// BaseInfo/getProjectWorkArea?projectId=0792b983-4de2-44d5-adba-361b7fc99a97&
/**
* 请求单位工程
*/
+6 -1
View File
@@ -6,4 +6,9 @@ import {
/**
* 获取焊接首页数据
*/
export const reqHJIndexData = (projectId)=>get(`HJGLIndex/GetJGLIndexItem?projectId=${projectId}`)
export const reqHJIndexData = (projectId)=>get(`HJGLIndex/GetJGLIndexItem?projectId=${projectId}`)
/**
* 获取点口管理列表
*/
export const reqGetNotEndPointBatch = (unitWorkId,projectId)=>get(`NDETrust/getNotEndPointBatch?unitWorkId=${unitWorkId}&projectId=${projectId}`)
+125 -1
View File
@@ -44,4 +44,128 @@
}
}
}
}
}
.nbd-select {
display: flex;
flex-direction: column;
height: 100%;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
&-header {
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #e8e8e8;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background: #fafbfc;
.u-size-mini{
padding: 0 30rpx !important;
margin: 0;
}
.nbd-select-title-wrap {
display: flex;
flex-direction: column;
.nbd-select-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.nbd-select-tip {
font-size: 24rpx;
color: #999;
}
}
}
&-filter {
padding: 20rpx 32rpx;
border-bottom: 1rpx solid #e8e8e8;
background: #fafbfc;
.search-box {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 36rpx;
padding: 0 24rpx;
height: 64rpx;
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
height: 100%;
}
.search-clear {
display: flex;
align-items: center;
padding: 4rpx;
}
}
}
&-inner {
flex: 1;
overflow-y: auto;
background: #ffffff;
&-wrap {
padding: 12rpx 0 30rpx;
}
}
&-empty {
padding: 80rpx 0;
}
&-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f5f6f8;
transition: all 0.2s;
background: #ffffff;
&:nth-of-type(2n) {
background: #fafbfc;
}
&:last-child {
border-bottom: none;
}
&.active {
background: rgba(41, 121, 255, 0.08);
border-left: 4rpx solid $u-type-primary;
padding-left: 26rpx;
.nbd-select-item-text {
color: $u-type-primary;
font-weight: 500;
}
}
.nbd-select-item-text {
flex: 1;
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nbd-select-checkbox {
margin-left: 16rpx;
}
}
}
+147
View File
@@ -195,6 +195,14 @@
grid-template-columns: repeat(4, 1fr);
gap: 24rpx 16rpx;
overflow-x: hidden;
.empty-tip {
grid-column: 1 / -1;
text-align: center;
padding: 40rpx 0;
font-size: 26rpx;
color: #999;
}
}
.grid-item {
@@ -351,4 +359,143 @@
}
}
}
}
// 列表页通用样式
.page-list {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f6f8;
// 头部过滤区域
.filter-header {
background: #ffffff;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.filter-row {
display: flex;
align-items: center;
gap: 16rpx;
}
// 选择按钮
.project-select-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 24rpx;
background: #f5f6f8;
border-radius: 12rpx;
max-height: 70rpx;
.select-text {
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 12rpx;
}
}
.refresh-btn {
padding: 8rpx;
display: flex;
align-items: center;
justify-content: center;
.rotating {
animation: rotate 1s linear infinite;
}
}
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
// 列表区域
.list-scroll {
flex: 1;
overflow: hidden;
}
// 加载中
.loading-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 80rpx 0;
.loading-text {
margin-top: 20rpx;
font-size: 26rpx;
color: #999;
}
}
.list-wrap {
padding: 24rpx;
}
// 列表项:序号 + 标题 + 箭头
.list-item {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
background-color: #ffffff;
margin-bottom: 20rpx;
border-radius: 12rpx;
&:active {
background: #f8f9fa;
}
.item-index {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f0f3ff;
color: $u-type-primary;
font-size: 24rpx;
font-weight: 600;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.item-title {
flex: 1;
font-size: 30rpx;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.loading-more {
display: flex;
flex-direction: column;
align-items: center;
padding: 32rpx 0;
color: #999;
font-size: 24rpx;
}
.no-more {
text-align: center;
padding: 32rpx 0;
color: #ccc;
font-size: 24rpx;
}
}
-163
View File
@@ -1,163 +0,0 @@
<template>
<u-popup v-model="visible" mode="bottom" height="80%">
<view class="project-select">
<view class="project-select-btns">
<u-button size="mini" type="info" @click="handleCancel">取消</u-button>
<text></text>
<u-button size="mini" type="primary" @click="handleConfirm">确定</u-button>
</view>
<view class="project-select-filter">
<u-search placeholder="请输入项目名称" height="60" v-model="searchKey" :show-action="false"
@change="handleSearchKey"></u-search>
</view>
<scroll-view scroll-y="true" class="project-select-inner">
<view class="project-select-inner-wrap">
<view class="li" v-for="(item,idx) in filteredList" :key="idx"
:class="selectObj && selectObj[props.valueKey] == item[props.valueKey] ? 'active' : ''"
@click="handerChangList(item)"
>
{{ item[props.labelKey] }}
</view>
<view v-if="filteredList.length === 0" class="empty-tip">暂无匹配项目</view>
</view>
</scroll-view>
</view>
</u-popup>
</template>
<script setup>
import {
ref,
watch,
computed
} from 'vue';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
searchKey: {
type: String,
default: ''
},
list: {
type: Array,
default: () => []
},
labelKey: {
type: String,
default: 'label'
},
valueKey: {
type: String,
default: 'value'
},
selectId: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'cancel', 'confirm', 'searchChange'])
const visible = ref(false)
const searchKey = ref("")
const selectObj = ref(null)
// 过滤后的列表
const filteredList = computed(() => {
if (!props.searchKey) return props.list
return props.list.filter(item =>
item[props.labelKey]?.includes(props.searchKey)
)
})
watch(() => props.modelValue, (val) => {
visible.value = val
if(val){
const selectedItem = props.list.find(item => item[props.valueKey] === props.selectId)
if (selectedItem) {
selectObj.value = selectedItem
} else {
selectObj.value = null
}
}
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
const handerChangList = (item)=>{
selectObj.value = item
}
// 点击取消
const handleCancel = () => {
emit('update:modelValue', false)
}
// 点击确认
const handleConfirm = () => {
emit("confirm", selectObj.value)
}
// 搜索
const handleSearchKey = (val) => {
emit('searchChange', val)
}
</script>
<style lang="scss" scoped>
.project-select {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
.project-select-btns {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.project-select-filter {
padding: 16rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.project-select-inner {
flex: 1;
overflow-y: auto;
.project-select-inner-wrap {
padding: 0 32rpx 32rpx;
.li {
padding: 28rpx 0;
border-bottom: 1rpx solid #f5f6f8;
font-size: 28rpx;
color: #333;
transition: all 0.2s;
&:last-child {
border-bottom: none;
}
&.active {
color: $u-type-primary;
font-weight: 500;
}
}
.empty-tip {
text-align: center;
padding: 80rpx 0;
color: #999;
font-size: 26rpx;
}
}
}
}
</style>
+179 -283
View File
@@ -1,22 +1,24 @@
<template>
<u-popup v-model="localVisible" mode="bottom" :height="height">
<view class="nbd-select">
<!-- 标题栏 -->
<view class="nbd-select-header" v-if="title">
<text class="nbd-select-title">{{ title }}</text>
<text class="nbd-select-tip" v-if="tip">{{ tip }}</text>
<!-- 顶部按钮 -->
<view class="nbd-select-header">
<u-button size="mini" @click="handleCancel">
{{ cancelText }}
</u-button>
<view v-if="title || tip" class="nbd-select-title-wrap">
<text class="nbd-select-title">{{ title }}</text>
<text class="nbd-select-tip" v-if="tip">{{ tip }}</text>
</view>
<u-button size="mini" type="primary" @click="handleConfirm">
{{ confirmText }}
</u-button>
</view>
<!-- 搜索栏 -->
<view class="nbd-select-filter" v-if="showSearch">
<u-search
:placeholder="searchPlaceholder"
:height="72"
v-model="searchKey"
:show-action="false"
@change="handleSearch"
@clear="handleSearch"
></u-search>
<u-search :placeholder="searchPlaceholder" :height="60" v-model="searchKey" :show-action="false"
@change="handleSearch" @clear="handleSearch"></u-search>
</view>
<!-- 列表内容 -->
@@ -28,298 +30,192 @@
</view>
<!-- 列表项 -->
<view
v-for="(item, idx) in filteredList"
:key="idx"
class="nbd-select-item"
:class="{ active: isSelected(item) }"
@click="handleItemClick(item)"
>
<view v-for="(item, idx) in filteredList" :key="idx" class="nbd-select-item"
:class="{ active: isSelected(item) }" @click="handleItemClick(item)">
<text class="nbd-select-item-text">{{ item[labelKey] }}</text>
<view v-if="multiple" class="nbd-select-checkbox">
<u-icon
:name="isSelected(item) ? 'checkbox-mark' : 'square'"
:size="36"
:color="isSelected(item) ? $u.type.primary : '#dcdee0'"
></u-icon>
<u-icon :name="isSelected(item) ? 'checkbox-mark' : 'square'" :size="36"
:color="isSelected(item) ? $u.type.primary : '#dcdee0'"></u-icon>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="nbd-select-footer">
<u-button class="nbd-select-btn-cancel" @click="handleCancel">
{{ cancelText }}
</u-button>
<u-button class="nbd-select-btn-confirm" type="primary" @click="handleConfirm">
{{ confirmText }}
</u-button>
</view>
</view>
</u-popup>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import {
ref,
computed,
watch
} from 'vue'
const props = defineProps({
// 弹窗显示状态
modelValue: {
type: Boolean,
default: false
},
// 弹窗高度
height: {
type: String,
default: '70%'
},
// 标题
title: {
type: String,
default: ''
},
// 标题提示
tip: {
type: String,
default: ''
},
// 数据列表
list: {
type: Array,
default: () => []
},
// 选中项(支持数组,多选时使用)
model: {
type: [Object, Array, String, Number],
default: null
},
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 显示搜索框
showSearch: {
type: Boolean,
default: false
},
// 搜索占位符
searchPlaceholder: {
type: String,
default: '请输入关键词搜索'
},
// 字段名配置
labelKey: {
type: String,
default: 'label'
},
valueKey: {
type: String,
default: 'value'
},
// 自定义搜索字段(默认搜索 labelKey)
searchKeyField: {
type: String,
default: ''
},
// 按钮文字
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
}
})
const emit = defineEmits([
'update:modelValue',
'update:model',
'cancel',
'confirm',
'searchChange',
'change'
])
const localVisible = ref(false)
const searchKey = ref('')
// 当前选中的项(多选时为数组)
const selectedItems = ref(props.multiple ? (props.model || []) : (props.model || null))
// 监听外部 model 变化
watch(() => props.model, (newVal) => {
selectedItems.value = props.multiple ? (newVal || []) : newVal
}, { deep: true })
// 弹窗状态同步
watch(() => props.modelValue, (val) => {
localVisible.value = val
if (val) {
// 打开时重置搜索
searchKey.value = ''
}
})
watch(localVisible, (val) => {
emit('update:modelValue', val)
})
// 过滤后的列表
const filteredList = computed(() => {
if (!searchKey.value) return props.list
const keyword = searchKey.value.toLowerCase()
const searchField = props.searchKeyField || props.labelKey
return props.list.filter(item => {
const value = item[searchField]
if (value === null || value === undefined) return false
return String(value).toLowerCase().includes(keyword)
const props = defineProps({
// 弹窗显示状态
modelValue: {
type: Boolean,
default: false
},
// 弹窗高度
height: {
type: String,
default: '70%'
},
// 标题
title: {
type: String,
default: ''
},
// 标题提示
tip: {
type: String,
default: ''
},
// 数据列表
list: {
type: Array,
default: () => []
},
// 选中项(支持数组,多选时使用)
model: {
type: [Object, Array, String, Number],
default: null
},
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 显示搜索框
showSearch: {
type: Boolean,
default: false
},
// 搜索占位符
searchPlaceholder: {
type: String,
default: '请输入关键词搜索'
},
// 字段名配置
labelKey: {
type: String,
default: 'label'
},
valueKey: {
type: String,
default: 'value'
},
// 自定义搜索字段(默认搜索 labelKey)
searchKeyField: {
type: String,
default: ''
},
// 按钮文字
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
}
})
})
// 判断是否选中
const isSelected = (item) => {
if (props.multiple) {
return selectedItems.value.some(
selected => selected[props.valueKey] === item[props.valueKey]
)
}
if (selectedItems.value && typeof selectedItems.value === 'object') {
return selectedItems.value[props.valueKey] === item[props.valueKey]
}
return selectedItems.value === item[props.valueKey]
}
const emit = defineEmits([
'update:modelValue',
'update:model',
'cancel',
'confirm',
'searchChange',
'change'
])
// 点击列表项
const handleItemClick = (item) => {
if (props.multiple) {
const index = selectedItems.value.findIndex(
selected => selected[props.valueKey] === item[props.valueKey]
)
if (index > -1) {
selectedItems.value.splice(index, 1)
const localVisible = ref(false)
const searchKey = ref('')
// 当前选中的项(多选时为数组)
const selectedItems = ref(props.multiple ? (props.model || []) : (props.model || null))
// 监听外部 model 变化
watch(() => props.model, (newVal) => {
selectedItems.value = props.multiple ? (newVal || []) : newVal
}, {
deep: true
})
// 弹窗状态同步
watch(() => props.modelValue, (val) => {
localVisible.value = val
if (val) {
// 打开时重置搜索
searchKey.value = ''
}
})
watch(localVisible, (val) => {
emit('update:modelValue', val)
})
// 过滤后的列表
const filteredList = computed(() => {
if (!searchKey.value) return props.list
const keyword = searchKey.value.toLowerCase()
const searchField = props.searchKeyField || props.labelKey
return props.list.filter(item => {
const value = item[searchField]
if (value === null || value === undefined) return false
return String(value).toLowerCase().includes(keyword)
})
})
// 判断是否选中
const isSelected = (item) => {
if (props.multiple) {
return selectedItems.value.some(
selected => selected[props.valueKey] === item[props.valueKey]
)
}
if (selectedItems.value && typeof selectedItems.value === 'object') {
return selectedItems.value[props.valueKey] === item[props.valueKey]
}
return selectedItems.value === item[props.valueKey]
}
// 点击列表项
const handleItemClick = (item) => {
if (props.multiple) {
const index = selectedItems.value.findIndex(
selected => selected[props.valueKey] === item[props.valueKey]
)
if (index > -1) {
selectedItems.value.splice(index, 1)
} else {
selectedItems.value.push(item)
}
} else {
selectedItems.value.push(item)
}
} else {
selectedItems.value = item
}
emit('change', selectedItems.value)
}
// 搜索
const handleSearch = (val) => {
emit('searchChange', val)
}
// 取消
const handleCancel = () => {
emit('cancel')
localVisible.value = false
}
// 确认
const handleConfirm = () => {
emit('confirm', selectedItems.value)
emit('update:model', selectedItems.value)
localVisible.value = false
}
</script>
<style lang="scss" scoped>
.nbd-select {
display: flex;
flex-direction: column;
height: 100%;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
&-header {
display: flex;
flex-direction: column;
padding: 24rpx 32rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
.nbd-select-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.nbd-select-tip {
font-size: 24rpx;
color: #999;
selectedItems.value = item
}
emit('change', selectedItems.value)
}
&-filter {
padding: 16rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
// 搜索
const handleSearch = (val) => {
emit('searchChange', val)
}
&-inner {
flex: 1;
overflow-y: auto;
padding: 0 32rpx;
&-wrap {
padding: 16rpx 0;
}
// 取消
const handleCancel = () => {
emit('cancel')
localVisible.value = false
}
&-empty {
padding: 80rpx 0;
// 确认
const handleConfirm = () => {
emit('confirm', selectedItems.value)
emit('update:model', selectedItems.value)
localVisible.value = false
}
&-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 0;
border-bottom: 1rpx solid #f5f6f8;
transition: background 0.2s;
&:last-child {
border-bottom: none;
}
&.active .nbd-select-item-text {
color: $u-type-primary;
font-weight: 500;
}
.nbd-select-item-text {
flex: 1;
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nbd-select-checkbox {
margin-left: 16rpx;
}
}
&-footer {
display: flex;
gap: 24rpx;
padding: 20rpx 32rpx 40rpx;
.nbd-select-btn-cancel,
.nbd-select-btn-confirm {
flex: 1;
height: 80rpx;
font-size: 28rpx;
border-radius: 40rpx;
}
}
}
</style>
</script>
+23 -14
View File
@@ -22,37 +22,37 @@
<view class="hj-row">
<view class="hj-cell">
<text class="hj-label">工厂焊接一次合格率</text>
<text class="hj-value">{{hjIndexData?.GCFirstPassRate}}</text>
<text class="hj-value">{{ hjIndexData?.GCFirstPassRate|| '--'}}</text>
</view>
<view class="hj-cell">
<text class="hj-label">现场焊接一次合格率</text>
<text class="hj-value">{{hjIndexData?.XCFirstPassRate}}</text>
<text class="hj-value">{{ hjIndexData?.XCFirstPassRate || '--' }}</text>
</view>
</view>
<view class="hj-row">
<view class="hj-cell">
<text class="hj-label">工厂预制进度</text>
<text class="hj-value hj-sub">{{hjIndexData?.GCProgress}}</text>
<text class="hj-value hj-sub">{{ hjIndexData?.GCProgress|| '--' }}</text>
</view>
<view class="hj-cell">
<text class="hj-label">现场焊接进度</text>
<text class="hj-value hj-sub">{{hjIndexData?.XCProgress}}</text>
<text class="hj-value hj-sub">{{ hjIndexData?.XCProgress || '--'}}</text>
</view>
</view>
<view class="hj-row">
<view class="hj-cell">
<text class="hj-label">工厂焊工功效</text>
<text class="hj-value">{{hjIndexData?.GCWelderEfficacy}}</text>
<text class="hj-value">{{ hjIndexData?.GCWelderEfficacy || '--'}}</text>
</view>
<view class="hj-cell">
<text class="hj-label">现场焊接功效</text>
<text class="hj-value">{{hjIndexData?.XCWelderEfficacy}}</text>
<text class="hj-value">{{ hjIndexData?.XCWelderEfficacy || '--'}}</text>
</view>
</view>
</view>
<view class="hj-footer">
<text class="hj-footer-label">项目管道实时预制率 (包含工厂和现场的预制口)</text>
<text class="hj-footer-value">{{hjIndexData?.PipePrefabricationRate}}</text>
<text class="hj-footer-value">{{ hjIndexData?.PipePrefabricationRate || '--'}}</text>
</view>
</view>
@@ -73,13 +73,14 @@
</view>
<text class="item-label">{{ item.name }}</text>
</view>
<view v-if="quickApps.length === 0" class="empty-tip">请选择应用</view>
</view>
</view>
<!-- 业务查询 -->
<view class="section-card home-query" v-if="queryApps&&queryApps.length>0">
<view class="section-card home-query" v-if="queryApps && queryApps.length > 0">
<view class="section-title">
<view class="title-left">
<text class="title-line"></text>
@@ -89,7 +90,7 @@
<view class="section-grid">
<view class="grid-item" v-for="(item, idx) in queryApps" :key="idx" @click="handleSkip(item)">
<view class="icon-box">
<u-icon :name="item.icon" :custom-prefix='item.prefix' :size="56" :color="item.color" />
<u-icon :name="item.icon" :custom-prefix='item.prefix' :size="56" :color="pastelColors[idx % 6]" />
</view>
<text class="item-label">{{ item.name }}</text>
</view>
@@ -121,9 +122,9 @@
</view>
</view>
</u-popup>
<nbd-select-project v-model="showProject" :select-id="currentProject?.ProjectId" :list="projectList"
label-key="ProjectName" value-key="ProjectId" :search-key="projectNameKeyword"
@search-change="handleSearchChange" @confirm="handleConfirmProject" />
<nbd-select v-model="showProject" title="请选择项目" :list="projectList" label-key="ProjectName" :show-search="true"
value-key="ProjectId" :default-value="currentProject?.ProjectId" @search-change="handleSearchChange"
@confirm="handleConfirmProject" />
</view>
</template>
@@ -178,7 +179,15 @@
const currentProjectName = computed(() => currentProject.value?.ProjectName || '')
const handleSkip= (item) => {
const handleSkip = (item) => {
if(!currentProject.value.ProjectId){
handleShowProjectSelect()
uni.showToast({
title: '请选择一个项目',
icon: 'none'
})
return
}
uni.navigateTo({
url: item.path
})
@@ -201,7 +210,7 @@
showEditQuick.value = false
uni.showToast({
title: '已更新快捷应用',
icon: 'success'
icon: 'none'
})
}
+84 -334
View File
@@ -1,14 +1,13 @@
<template>
<view class="page">
<view class="page-list">
<!-- 头部过滤区域 -->
<view class="filter-header">
<view class="filter-row">
<!-- 下拉过滤 1 -->
<u-dropdown v-model="filter1Value" :options="filter1Options" @change="handleFilter1Change"></u-dropdown>
<!-- 下拉过滤 2 -->
<u-dropdown v-model="filter2Value" :options="filter2Options" @change="handleFilter2Change"></u-dropdown>
<!-- 单位工程下拉选择 -->
<view class="project-select-btn">
<u-input style="color: #333;" v-model="unitWorkName" :clearable="false" type="select"
placeholder="请选择单位工程" @click="handleShowUnitWorkSelect" />
</view>
<!-- 刷新按钮 -->
<view class="refresh-btn" @click="handleRefresh">
<u-icon name="reload" :size="36" :class="{ rotating: loading }"></u-icon>
@@ -17,355 +16,106 @@
</view>
<!-- 列表内容 -->
<scroll-view
class="list-scroll"
scroll-y
:scroll-into-view="scrollIntoView"
@scrolltolower="loadMore"
refresher-enabled
:refresher-triggered="refreshing"
@refresh="handleRefresh"
>
<!-- 列表项 -->
<view class="list-wrap">
<view
v-for="(item, index) in listData"
:key="item.id || index"
class="list-item"
@click="handleItemClick(item)"
>
<view class="item-header">
<text class="item-title">{{ item.name || '未命名' }}</text>
<text class="item-status" :class="item.status">{{ item.statusText || '' }}</text>
</view>
<view class="item-content">
<text class="item-desc">{{ item.description || '' }}</text>
</view>
<view class="item-footer">
<text class="item-time">{{ item.createTime || '' }}</text>
<text class="item-author">{{ item.creator || '' }}</text>
</view>
<view class="list-scroll">
<!-- 加载中 -->
<view v-if="loading" class="loading-box">
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">加载中...</text>
</view>
<!-- 数据列表 -->
<view v-else class="list-wrap">
<view v-for="(item, index) in listData" :key="item.id || index" class="list-item"
@click="handleItemClick(item)">
<text class="item-index">{{ index + 1 }}</text>
<text class="item-title">{{ item.PointBatchCode || '--' }}</text>
<u-icon name="arrow-right" :size="28" color="#ccc"></u-icon>
</view>
<!-- 空状态 -->
<u-empty
v-if="!loading && listData.length === 0"
mode="list"
text="暂无数据"
></u-empty>
<!-- 加载提示 -->
<view v-if="loading && listData.length > 0" class="loading-more">
<u-loading mode="circle" size="32"></u-loading>
<text>加载中...</text>
</view>
<!-- 没有更多 -->
<view v-if="!loading && !hasMore && listData.length > 0" class="no-more">
<text> 没有更多了 </text>
</view>
<u-empty v-if="listData.length === 0" mode="list" text="暂无数据"></u-empty>
</view>
</scroll-view>
</view>
<!-- 单位工程选择弹窗 -->
<nbd-select v-model="showUnitWorkSelect" :show-search="true" title="请选择单位工程" :list="unitWorkList"
label-key="BaseInfoName" value-key="BaseInfoId" @confirm="handleUnitWorkConfirm" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import {
ref,
onMounted,
computed
} from 'vue'
import {
reqProjectWorkArea
} from '@/api/base.js'
import {
reqGetNotEndPointBatch
} from '@/api/hj.js'
import {
useUserStore
} from '@/store'
import {
storeToRefs
} from 'pinia'
// ===== 过滤条件 =====
const filter1Value = ref('')
const filter2Value = ref('')
const userStore = useUserStore()
const {
currentProject
} = storeToRefs(userStore)
// 过滤选项 1
const filter1Options = ref([
{ label: '全部类型', value: '' },
{ label: '类型 A', value: 'type_a' },
{ label: '类型 B', value: 'type_b' },
{ label: '类型 C', value: 'type_c' }
])
// ===== 单位工程选择 =====
const showUnitWorkSelect = ref(false)
const selectedUnitWork = ref(null)
const unitWorkName = computed(() => selectedUnitWork.value?.BaseInfoName || '')
const unitWorkList = ref([])
// 过滤选项 2
const filter2Options = ref([
{ label: '全部状态', value: '' },
{ label: '进行中', value: 'pending' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
])
// ===== 列表数据 =====
const listData = ref([])
const loading = ref(false)
const refreshing = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = 20
const scrollIntoView = ref('')
// ===== 筛选条件变更 =====
const filterParams = reactive({
filter1: '',
filter2: ''
})
// 过滤 1 变化
const handleFilter1Change = (value) => {
filterParams.filter1 = value
resetAndLoad()
}
// 过滤 2 变化
const handleFilter2Change = (value) => {
filterParams.filter2 = value
resetAndLoad()
}
// ===== 数据加载 =====
// 加载数据
const fetchData = async (isRefresh = false) => {
if (loading.value || (!isRefresh && !hasMore.value)) return
loading.value = true
if (isRefresh) {
refreshing.value = true
}
try {
// TODO: 替换为实际 API 调用
// const res = await api.getList({
// page: currentPage.value,
// pageSize,
// ...filterParams
// })
// 模拟数据
const mockData = await mockApiFetch({
page: currentPage.value,
pageSize,
...filterParams
const handleShowUnitWorkSelect = () => {
reqProjectWorkArea(currentProject.value.ProjectId).then(res => {
unitWorkList.value = res.data || []
showUnitWorkSelect.value = true
})
if (isRefresh) {
listData.value = mockData.list
} else {
listData.value = [...listData.value, ...mockData.list]
}
hasMore.value = mockData.hasMore
currentPage.value++
} catch (error) {
console.error('加载数据失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
loading.value = false
refreshing.value = false
}
}
// 重置并重新加载
const resetAndLoad = () => {
currentPage.value = 1
hasMore.value = true
listData.value = []
fetchData(true)
}
// 下拉刷新
const handleRefresh = () => {
resetAndLoad()
}
// 上拉加载更多
const loadMore = () => {
if (!loading.value && hasMore.value) {
fetchData(false)
}
}
// 列表项点击
const handleItemClick = (item) => {
console.log('点击列表项:', item)
// TODO: 跳转详情
// uni.navigateTo({ url: `/pages/xxx/detail?id=${item.id}` })
}
// ===== 模拟 API(实际使用时删除)=====
const mockApiFetch = async (params) => {
return new Promise((resolve) => {
setTimeout(() => {
const start = (params.page - 1) * params.pageSize
const end = start + params.pageSize
const hasMore = end < 100 // 模拟总共 100 条数据
const list = Array.from({ length: hasMore ? params.pageSize : (100 - start) }, (_, i) => ({
id: start + i + 1,
name: `项目 ${start + i + 1}`,
description: '这是一个测试项目的描述信息,可能包含更多内容',
status: ['pending', 'completed', 'cancelled'][Math.floor(Math.random() * 3)],
statusText: ['进行中', '已完成', '已取消'][Math.floor(Math.random() * 3)],
createTime: '2024-01-01 12:00:00',
creator: `用户${Math.floor(Math.random() * 10) + 1}`
}))
resolve({
list,
hasMore
})
}, 500)
})
}
// ===== 生命周期 =====
onMounted(() => {
fetchData(true)
})
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f6f8;
}
// 头部过滤区域
.filter-header {
background: #ffffff;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.filter-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.refresh-btn {
margin-left: auto;
padding: 8rpx;
display: flex;
align-items: center;
justify-content: center;
.rotating {
animation: rotate 1s linear infinite;
}
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 列表滚动区域
.list-scroll {
flex: 1;
overflow: hidden;
}
.list-wrap {
padding: 24rpx;
}
// 列表项
.list-item {
background: #ffffff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
&:last-child {
margin-bottom: 0;
const handleUnitWorkConfirm = (item) => {
selectedUnitWork.value = item
showUnitWorkSelect.value = false
handleRefresh()
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
// ===== 列表数据 =====
const listData = ref([])
const loading = ref(false)
.item-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
// 加载数据
const fetchData = async () => {
if (loading.value) return
loading.value = true
.item-status {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
background: #f0f0f0;
color: #666;
&.pending {
background: rgba(255, 167, 38, 0.1);
color: #ffa726;
}
&.completed {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
}
&.cancelled {
background: rgba(244, 67, 54, 0.1);
color: #f44336;
}
try {
const res = await reqGetNotEndPointBatch(
selectedUnitWork.value?.BaseInfoId || '',
currentProject.value?.ProjectId
)
listData.value = res.data || []
} catch (error) {
console.error('加载数据失败:', error)
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
} finally {
loading.value = false
}
}
.item-content {
margin-bottom: 16rpx;
.item-desc {
font-size: 26rpx;
color: #666;
line-height: 1.6;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
const handleRefresh = () => {
listData.value = []
fetchData()
}
.item-footer {
display: flex;
justify-content: space-between;
.item-time,
.item-author {
font-size: 22rpx;
color: #999;
}
const handleItemClick = (item) => {
console.log('点击列表项:', item)
// TODO: 跳转详情
}
}
// 加载中
.loading-more {
display: flex;
flex-direction: column;
align-items: center;
padding: 32rpx 0;
color: #999;
font-size: 24rpx;
}
// 没有更多
.no-more {
text-align: center;
padding: 32rpx 0;
color: #ccc;
font-size: 24rpx;
}
</style>
</script>
+1 -1
View File
@@ -96,7 +96,7 @@ export const useMenuStore = defineStore('menu', () => {
enabled: true,
strategies: [{
key: 'menu',
paths: [],
paths: ['appsList'],
}]
},
})
-5
View File
@@ -42,11 +42,6 @@ export const useUserStore = defineStore('user', () => {
const logout = () => {
token.value = ""
userInfo.value = null
// loginForm.value = {
// account: '',
// password: '',
// Telephone: '',
// }
currentProject.value = null
}
+5 -5
View File
@@ -16,34 +16,34 @@ export const defaultApps = [{
menuId: '3ACE25CE-C5CE-4CEC-AD27-0D5CF1DF2F01',
path: "/pipe/diankou/list",
prefix: 'nbd-icon',
selected: true
selected: false
},
{
name: '试压管理',
icon: 'shiyabao',
menuId: '55976B16-2C33-406E-B514-2FE42D031071',
prefix: 'nbd-icon',
selected: true
selected: false
},
{
name: '包装管理',
icon: 'baozhuangguanli',
menuId: "25DED954-10C9-47CC-99F2-C44FDE9E0A81",
prefix: 'nbd-icon',
selected: true
selected: false
},
{
name: '发货管理',
icon: 'a-facheguanli',
menuId: "EEC0D060-C15E-4D25-B015-C2B91F735DAC",
prefix: 'nbd-icon',
selected: true
selected: false
},
{
name: '预制组件管理',
icon: 'yuzhijian',
menuId: "8255554C-0A92-4C7B-BF19-779AF0220A8C",
prefix: 'nbd-icon',
selected: true
selected: false
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
import { useUserStore } from '@/store'
// TODO: 替换为实际的 API 地址
export const baseUrl = ' https://lygcgs.com.cn:8078/shjapi/api/'
export const baseUrl = 'https://sggl.sedin.com.cn/sgglapi/api/'
// ===== 防止重复请求 =====
const pendingRequests = new Map()