2025-11-06 16:56:35 +08:00
|
|
|
|
<template>
|
2025-11-07 17:18:33 +08:00
|
|
|
|
<view class="container">
|
|
|
|
|
|
<!-- 第一级:tabNode横向滚动Tab -->
|
|
|
|
|
|
<view class="tab-container">
|
|
|
|
|
|
<scroll-view
|
|
|
|
|
|
class="tab-scroll"
|
|
|
|
|
|
scroll-x
|
|
|
|
|
|
show-scrollbar="false"
|
|
|
|
|
|
:scroll-with-animation="true"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="tab-wrapper">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="(tab, index) in tabOption"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ 'tab-active': defaultTabNode === tab.value }"
|
|
|
|
|
|
@click="handleTabClick(tab.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>{{ tab.label }}</text>
|
|
|
|
|
|
<!-- 选中状态显示清除图标 -->
|
|
|
|
|
|
<text
|
|
|
|
|
|
v-if="defaultTabNode === tab.value"
|
|
|
|
|
|
class="clear-icon"
|
|
|
|
|
|
@click.stop="clearTab"
|
|
|
|
|
|
>×</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 第二级:firstLevelNode横向滚动Tab(联动) -->
|
|
|
|
|
|
<view class="tab-container second-tab" v-if="filteredFirstLevelOptions.length > 0">
|
|
|
|
|
|
<scroll-view
|
|
|
|
|
|
class="tab-scroll"
|
|
|
|
|
|
scroll-x
|
|
|
|
|
|
show-scrollbar="false"
|
|
|
|
|
|
:scroll-with-animation="true"
|
|
|
|
|
|
:disabled="!defaultTabNode"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="tab-wrapper">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="(item, index) in filteredFirstLevelOptions"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'tab-active': defaultFirstLevelNode === item.value,
|
|
|
|
|
|
'tab-disabled': !defaultTabNode
|
|
|
|
|
|
}"
|
|
|
|
|
|
@click="handleFirstLevelClick(item.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>{{ item.label }}</text>
|
|
|
|
|
|
<!-- 选中状态显示清除图标 -->
|
|
|
|
|
|
<text
|
|
|
|
|
|
v-if="defaultFirstLevelNode === item.value"
|
|
|
|
|
|
class="clear-icon"
|
|
|
|
|
|
@click.stop="clearFirstLevel"
|
|
|
|
|
|
>×</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 筛选后的列表 -->
|
|
|
|
|
|
<view class="list-container">
|
|
|
|
|
|
<view class="summary">{{ scheduleSummary }}</view>
|
|
|
|
|
|
<view v-if="loading" class="loading">加载中...</view>
|
|
|
|
|
|
<view v-else-if="filterList.length === 0" class="empty">暂无数据</view>
|
|
|
|
|
|
<view v-else class="list-item" v-for="(item, index) in filterList" :key="item.id || index">
|
|
|
|
|
|
<view class="item-name">
|
|
|
|
|
|
{{ item.stepName }}
|
|
|
|
|
|
<oa-remind-time :expire-date="item.planEnd" :threshold-days="5" :finished="item.status == 2"/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="item-info">
|
|
|
|
|
|
状态:{{ item.status === 2 ? '已完成' : item.status === 1 ? '待验收' : '未开始' }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="item-info">
|
|
|
|
|
|
负责人:{{ item.nodeHeader || '未指定' }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-11-06 16:56:35 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-11-07 17:18:33 +08:00
|
|
|
|
import { listPage } from "@/api/oa/projectScheduleStep";
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
projectScheduleStepList: [],
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
defaultTabNode: "", // 默认不选中
|
|
|
|
|
|
defaultFirstLevelNode: "", // 默认不选中
|
|
|
|
|
|
filterParams: {
|
|
|
|
|
|
onlyMy: false,
|
|
|
|
|
|
onlyUnfinished: false
|
|
|
|
|
|
},
|
|
|
|
|
|
queryParams: {
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 999,
|
|
|
|
|
|
scheduleId: undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
// 第一级Tab选项
|
|
|
|
|
|
tabOption() {
|
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
const tabNodes = [];
|
|
|
|
|
|
for (const item of this.projectScheduleStepList) {
|
|
|
|
|
|
const currentTabNode = item.tabNode;
|
|
|
|
|
|
if (currentTabNode && !seen.has(currentTabNode)) {
|
|
|
|
|
|
seen.add(currentTabNode);
|
|
|
|
|
|
tabNodes.push(currentTabNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return tabNodes.map(item => ({ label: item, value: item }));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 第二级原始选项(带关联tabNode)
|
|
|
|
|
|
firstLevelOption() {
|
|
|
|
|
|
const uniqueMap = {};
|
|
|
|
|
|
this.projectScheduleStepList.forEach(item => {
|
|
|
|
|
|
if (item.firstLevelNode && item.tabNode) {
|
|
|
|
|
|
const uniqueKey = `${item.firstLevelNode}-${item.tabNode}`;
|
|
|
|
|
|
uniqueMap[uniqueKey] = item;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return Object.values(uniqueMap).map(item => ({
|
|
|
|
|
|
label: item.firstLevelNode,
|
|
|
|
|
|
value: item.firstLevelNode,
|
|
|
|
|
|
tabNode: item.tabNode
|
|
|
|
|
|
}));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 联动过滤后的第二级选项
|
|
|
|
|
|
filteredFirstLevelOptions() {
|
|
|
|
|
|
if (!this.defaultTabNode) return [];
|
|
|
|
|
|
return this.firstLevelOption.filter(option =>
|
|
|
|
|
|
option.tabNode === this.defaultTabNode
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 统计信息
|
|
|
|
|
|
scheduleSummary() {
|
|
|
|
|
|
const totalCount = this.projectScheduleStepList.length;
|
|
|
|
|
|
const completedCount = this.projectScheduleStepList.filter(item => item.status === 2).length;
|
|
|
|
|
|
const pendingCount = this.projectScheduleStepList.filter(item => item.status === 1).length;
|
|
|
|
|
|
return `已完成(${completedCount})+ 待验收(${pendingCount}) / 总节点数(${totalCount})`;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选后的列表
|
|
|
|
|
|
filterList() {
|
|
|
|
|
|
// const { onlyMy, onlyUnfinished } = this.filterParams;
|
|
|
|
|
|
|
|
|
|
|
|
let filtered = this.projectScheduleStepList.filter(item => {
|
|
|
|
|
|
// 只有选中了才过滤,未选中则不过滤该条件
|
|
|
|
|
|
if (this.defaultTabNode && item.tabNode !== this.defaultTabNode) return false;
|
|
|
|
|
|
if (this.defaultFirstLevelNode && item.firstLevelNode !== this.defaultFirstLevelNode) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// if (onlyMy) {
|
|
|
|
|
|
// filtered = filtered.filter(item => item.nodeHeader === this.$store.getters.nickName);
|
|
|
|
|
|
// }
|
|
|
|
|
|
// if (onlyUnfinished) {
|
|
|
|
|
|
// filtered = filtered.filter(item => item.status !== 2);
|
|
|
|
|
|
// }
|
|
|
|
|
|
console.log('筛选后的列表', filtered)
|
|
|
|
|
|
|
|
|
|
|
|
return filtered;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
getList() {
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
listPage(this.queryParams).then(response => {
|
|
|
|
|
|
this.projectScheduleStepList = response.rows || [];
|
|
|
|
|
|
this.total = response.total || 0;
|
|
|
|
|
|
// 移除自动选中逻辑,保持默认不选中
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
this.projectScheduleStepList = [];
|
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 第一级Tab点击
|
|
|
|
|
|
handleTabClick(value) {
|
|
|
|
|
|
// 如果点击的是已选中的Tab,不做操作(清除通过图标实现)
|
|
|
|
|
|
if (this.defaultTabNode === value) return;
|
|
|
|
|
|
this.defaultTabNode = value;
|
|
|
|
|
|
this.defaultFirstLevelNode = ""; // 切换Tab时重置第二级
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 清除第一级选中状态
|
|
|
|
|
|
clearTab() {
|
|
|
|
|
|
this.defaultTabNode = "";
|
|
|
|
|
|
this.defaultFirstLevelNode = ""; // 同时清空第二级
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 第二级Tab点击
|
|
|
|
|
|
handleFirstLevelClick(value) {
|
|
|
|
|
|
if (!this.defaultTabNode) return;
|
|
|
|
|
|
// 如果点击的是已选中的Tab,不做操作(清除通过图标实现)
|
|
|
|
|
|
if (this.defaultFirstLevelNode === value) return;
|
|
|
|
|
|
this.defaultFirstLevelNode = value;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 清除第二级选中状态
|
|
|
|
|
|
clearFirstLevel() {
|
|
|
|
|
|
this.defaultFirstLevelNode = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad(options) {
|
|
|
|
|
|
this.queryParams.scheduleId = options.id;
|
|
|
|
|
|
this.getList();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-11-06 16:56:35 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-11-07 17:18:33 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.container {
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 横向滚动Tab样式 */
|
|
|
|
|
|
.tab-container {
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
padding: 8rpx 0;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.second-tab {
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-scroll {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
padding: 8rpx 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-wrapper {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item {
|
|
|
|
|
|
padding: 12rpx 24rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
border-radius: 30rpx;
|
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8rpx; /* 文字和清除图标间距 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item.tab-active {
|
|
|
|
|
|
background-color: #007aff;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item.tab-disabled {
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 清除图标样式 */
|
|
|
|
|
|
.clear-icon {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
width: 24rpx;
|
|
|
|
|
|
height: 24rpx;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background-color: rgba(255,255,255,0.3);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 列表样式 */
|
|
|
|
|
|
.list-container {
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.summary {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
padding: 16rpx 0;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading, .empty {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 60rpx 0;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.list-item {
|
|
|
|
|
|
padding: 20rpx 0;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.list-item:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-name {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
2025-11-06 16:56:35 +08:00
|
|
|
|
|
2025-11-07 17:18:33 +08:00
|
|
|
|
.item-info {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|