OfficeSystem/pages/project/list/index.vue

673 lines
16 KiB
Vue
Raw Normal View History

2025-11-17 11:58:49 +08:00
<template>
<view class="project-list-page">
<!-- 筛选区域 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<text class="filter-label">客户</text>
<input
class="filter-input"
v-model="filterParams.customerName"
placeholder="请输入客户名称"
@confirm="handleSearch"
/>
</view>
<view class="filter-item">
<text class="filter-label">项目编号</text>
<input
class="filter-input"
v-model="filterParams.projectId"
placeholder="请输入项目编号"
@confirm="handleSearch"
/>
</view>
</view>
<view class="filter-row">
<view class="filter-item">
<text class="filter-label">项目名称</text>
<input
class="filter-input"
v-model="filterParams.projectName"
placeholder="请输入项目名称"
@confirm="handleSearch"
/>
</view>
<view class="filter-item">
<text class="filter-label">成员</text>
<picker
mode="selector"
:range="memberOptions"
range-key="userName"
@change="handleMemberChange"
>
<view class="filter-picker">
<text class="picker-text">{{ selectedMemberName || '请选择用户' }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
</view>
<!-- 开发超期筛选 -->
<view class="filter-row">
<text class="filter-label">开发超期</text>
<view class="radio-group">
<view
class="radio-item"
:class="{ active: filterParams.overdue === '' }"
@click="filterParams.overdue = ''; handleSearch()"
>
<text>全部</text>
</view>
<view
class="radio-item"
:class="{ active: filterParams.overdue === true }"
@click="filterParams.overdue = true; handleSearch()"
>
<text></text>
</view>
<view
class="radio-item"
:class="{ active: filterParams.overdue === false }"
@click="filterParams.overdue = false; handleSearch()"
>
<text></text>
</view>
</view>
</view>
<!-- 状态标签 -->
<view class="status-tabs">
<view
class="status-tab"
v-for="tab in statusTabs"
:key="tab.value"
:class="{ active: activeStatusTab === tab.value }"
@click="handleStatusTabClick(tab.value)"
>
<text>{{ tab.label }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<uv-button type="primary" size="small" @click="handleSearch">
<text class="btn-icon">🔍</text>
<text>搜索</text>
</uv-button>
<uv-button size="small" @click="handleReset">
<text>重置</text>
</uv-button>
</view>
</view>
<!-- 项目列表 -->
<scroll-view
class="project-scroll"
scroll-y
@scrolltolower="handleScrollToLower"
>
<view class="project-container">
<view
class="project-card"
v-for="project in projects"
:key="project.id"
@click="goToProjectDetail(project)"
>
<!-- 状态标签和过期时间 -->
<view class="project-header">
<view class="status-badge">
<uv-tags
:text="getStatusText(project.status)"
:type="getStatusType(project.status)"
size="mini"
:plain="false"
></uv-tags>
</view>
<view class="expire-time">
<text class="expire-label">过期时间:</text>
<text class="expire-value">{{ formatDate(project.expireTime) }}</text>
</view>
</view>
<!-- 项目信息 -->
<view class="project-content">
<text class="project-name">{{ project.projectName }}</text>
<text class="project-description">{{ project.description }}</text>
<view class="project-meta">
<text class="meta-item">创建人: {{ project.createName }}</text>
<text class="meta-item">负责人: {{ getOwnerNames(project.memberList) }}</text>
<view class="meta-row">
<text class="meta-item">提交: {{ project.submitCount || 0 }}</text>
<text class="meta-item">接收: {{ project.receivedCount || 0 }}</text>
</view>
</view>
<!-- 逾期提示 -->
<view class="overdue-tip" v-if="project.overdue">
<text class="overdue-text"> 已逾期</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="empty-state" v-if="loading">
<text class="empty-text">加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="isEmpty">
<text class="empty-text">暂无项目数据</text>
</view>
<!-- 加载更多提示 -->
<view class="load-more-tip" v-if="!isEmpty && !loading && !noMore">
<text class="load-more-text">上拉加载更多</text>
</view>
<view class="load-more-tip" v-if="!isEmpty && noMore">
<text class="load-more-text">没有更多数据了</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getProjectList, getUserList } from '@/api';
import { usePagination } from '@/composables';
import { useDictStore } from '@/store/dict';
import { useUserStore } from '@/store/user';
import { getDictLabel } from '@/utils/dict';
const dictStore = useDictStore();
const userStore = useUserStore();
// 筛选参数
const filterParams = ref({
customerName: '',
projectId: '',
projectName: '',
memberId: '',
overdue: '', // ''表示全部, true表示是, false表示否
});
2025-11-17 13:54:18 +08:00
// 状态标签(使用字典键值)
2025-11-17 11:58:49 +08:00
const statusTabs = ref([
2025-11-17 13:54:18 +08:00
{ label: '待开始', value: 'WAIT_START' },
{ label: '开发中', value: 'IN_PROGRESS' },
{ label: '开发完成', value: 'COMPLETED' },
{ label: '已验收', value: 'ACCEPTED' },
{ label: '维护中', value: 'MAINTENANCE' },
{ label: '维护到期', value: 'MAINTENANCE_OVERDUE' },
{ label: '开发超期', value: 'DEVELOPMENT_OVERDUE' }
2025-11-17 11:58:49 +08:00
]);
2025-11-17 13:54:18 +08:00
const activeStatusTab = ref('IN_PROGRESS'); // 默认选中"开发中"
2025-11-17 11:58:49 +08:00
// 成员选项
const memberOptions = ref([]);
const selectedMemberName = ref('');
// 使用分页组合式函数
const {
list,
noMore,
isEmpty,
loading,
getList,
loadMore,
updateParams,
refresh,
queryParams,
reset
} = usePagination({
fetchData: async (params) => {
// 构建请求参数
const requestParams = {
...params,
orderByColumn: 'expireTime',
isAsc: 'ascending'
};
// 添加状态筛选
if (activeStatusTab.value) {
2025-11-17 13:54:18 +08:00
// 根据选中的状态标签设置statusList使用字典键值
// if (activeStatusTab.value === 'DEVELOPMENT_OVERDUE') {
// // 开发超期需要特殊处理状态是IN_PROGRESS但需要overdue=true
// requestParams.statusList = ['IN_PROGRESS'];
// requestParams.overdue = true;
// } else
{
// 其他状态直接使用字典键值
requestParams.statusList = [activeStatusTab.value];
2025-11-17 11:58:49 +08:00
}
}
// 添加其他筛选条件
if (filterParams.value.customerName) {
requestParams.customerName = filterParams.value.customerName;
}
if (filterParams.value.projectId) {
requestParams.projectId = filterParams.value.projectId;
}
if (filterParams.value.projectName) {
requestParams.projectName = filterParams.value.projectName;
}
if (filterParams.value.memberId) {
requestParams.memberId = filterParams.value.memberId;
}
2025-11-17 13:54:18 +08:00
if (filterParams.value.overdue !== '' && activeStatusTab.value !== 'DEVELOPMENT_OVERDUE') {
2025-11-17 11:58:49 +08:00
// 如果不是"开发超期"标签才使用overdue筛选
requestParams.overdue = filterParams.value.overdue;
}
// 添加创建人和负责人筛选(根据用户私有视角)
const userId = userStore.getUserInfo?.user?.userId || userStore.getUserInfo?.userId;
const privateView = userStore.privateView;
if (userId && privateView) {
requestParams.ownerId = userId;
requestParams.createId = userId;
}
const res = await getProjectList(requestParams);
return res;
},
mode: 'loadMore',
pageSize: 20,
defaultParams: {}
});
// 项目列表
const projects = computed(() => list.value);
// 获取状态文本
const getStatusText = (status) => {
if (!status && status !== 0) return '未知';
2025-11-17 13:54:18 +08:00
// 优先从字典获取字典键值作为dictValue
2025-11-17 11:58:49 +08:00
const dictLabel = getDictLabel('project_status', status);
if (dictLabel && dictLabel !== String(status)) {
return dictLabel;
}
2025-11-17 13:54:18 +08:00
// 默认映射(兼容字典键值)
2025-11-17 11:58:49 +08:00
const statusMap = {
2025-11-17 13:54:18 +08:00
'WAIT_START': '待开始',
'IN_PROGRESS': '开发中',
'COMPLETED': '开发完成',
'ACCEPTED': '已验收',
'MAINTENANCE': '维护中',
'MAINTENANCE_OVERDUE': '维护到期',
'DEVELOPMENT_OVERDUE': '开发超期',
// 兼容旧数据(数字状态值)
2025-11-17 11:58:49 +08:00
'1': '待开始',
'2': '开发中',
'4': '开发完成',
'5': '已验收',
'6': '维护中',
2025-11-17 13:54:18 +08:00
'7': '维护到期',
'8': '开发超期'
2025-11-17 11:58:49 +08:00
};
return statusMap[String(status)] || `状态${status}`;
};
// 获取状态类型(用于标签颜色)
const getStatusType = (status) => {
if (!status && status !== 0) return 'primary';
const typeMap = {
2025-11-17 13:54:18 +08:00
// 字典键值映射
'WAIT_START': 'primary', // 待开始
'IN_PROGRESS': 'warning', // 开发中
'COMPLETED': 'success', // 开发完成
'ACCEPTED': 'info', // 已验收
'MAINTENANCE': 'primary', // 维护中
'MAINTENANCE_OVERDUE': 'warning', // 维护到期
'DEVELOPMENT_OVERDUE': 'error', // 开发超期
// 兼容旧数据(数字状态值)
2025-11-17 11:58:49 +08:00
'1': 'primary', // 待开始
'2': 'warning', // 开发中
'4': 'success', // 开发完成
'5': 'info', // 已验收
'6': 'primary', // 维护中
2025-11-17 13:54:18 +08:00
'7': 'warning', // 维护到期
'8': 'error' // 开发超期
2025-11-17 11:58:49 +08:00
};
return typeMap[String(status)] || 'primary';
};
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
return dateStr.split(' ')[0];
};
// 获取负责人名称
const getOwnerNames = (memberList) => {
if (!Array.isArray(memberList) || memberList.length === 0) return '未分配';
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、');
};
// 处理状态标签点击
const handleStatusTabClick = (value) => {
activeStatusTab.value = value;
handleSearch();
};
// 处理成员选择
const handleMemberChange = (e) => {
const index = e.detail.value;
const member = memberOptions.value[index];
if (member) {
filterParams.value.memberId = member.userId;
selectedMemberName.value = member.userName;
} else {
filterParams.value.memberId = '';
selectedMemberName.value = '';
}
};
// 搜索
const handleSearch = () => {
reset();
getList();
};
// 重置
const handleReset = () => {
filterParams.value = {
customerName: '',
projectId: '',
projectName: '',
memberId: '',
overdue: ''
};
selectedMemberName.value = '';
activeStatusTab.value = '2';
handleSearch();
};
// 处理滚动到底部
const handleScrollToLower = () => {
if (!noMore.value && !loading.value) {
loadMore();
}
};
// 跳转到项目详情
const goToProjectDetail = (project) => {
// TODO: 跳转到项目详情页面
uni.showToast({
title: '项目详情功能开发中',
icon: 'none'
});
};
// 加载成员列表
const loadMemberList = async () => {
try {
const res = await getUserList({ pageSize: 200 });
if (res && res.rows) {
memberOptions.value = res.rows || [];
}
} catch (err) {
console.error('加载成员列表失败:', err);
}
};
// 页面初始化标志
const isInitialized = ref(false);
// 监听用户私有视角变化
watch(() => userStore.privateView, () => {
if (isInitialized.value) {
handleSearch();
}
});
onMounted(() => {
// 加载字典数据
if (!dictStore.isLoaded) {
dictStore.loadDictData();
}
// 加载成员列表
loadMemberList();
});
onLoad(() => {
nextTick(() => {
isInitialized.value = true;
handleSearch();
});
});
</script>
<style lang="scss" scoped>
.project-list-page {
width: 100%;
height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.filter-section {
background: #fff;
padding: 16px;
margin-bottom: 8px;
}
.filter-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.filter-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-label {
font-size: 12px;
color: #666;
}
.filter-input {
height: 36px;
padding: 0 12px;
background: #f5f6f7;
border-radius: 6px;
font-size: 14px;
}
.filter-picker {
height: 36px;
padding: 0 12px;
background: #f5f6f7;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-text {
font-size: 14px;
color: #333;
}
.picker-arrow {
font-size: 12px;
color: #999;
}
.radio-group {
display: flex;
gap: 12px;
margin-top: 6px;
}
.radio-item {
padding: 6px 16px;
background: #f5f6f7;
border-radius: 16px;
font-size: 14px;
color: #666;
&.active {
background: #2885ff;
color: #fff;
}
}
.status-tabs {
display: flex;
gap: 8px;
margin: 12px 0;
flex-wrap: wrap;
}
.status-tab {
padding: 6px 16px;
background: #f5f6f7;
border-radius: 16px;
font-size: 14px;
color: #666;
&.active {
background: #2885ff;
color: #fff;
}
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 12px;
}
.btn-icon {
margin-right: 4px;
}
.project-scroll {
flex: 1;
width: 100%;
}
.project-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.project-card {
background: #fff;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.project-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.status-badge {
display: flex;
align-items: center;
}
.expire-time {
display: flex;
align-items: center;
gap: 6px;
}
.expire-label {
font-size: 12px;
color: #999;
}
.expire-value {
font-size: 14px;
color: #333;
}
.project-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.project-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
.project-description {
font-size: 14px;
color: #666;
line-height: 1.5;
}
.project-meta {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 8px;
}
.meta-item {
font-size: 12px;
color: #999;
}
.meta-row {
display: flex;
gap: 16px;
}
.overdue-tip {
margin-top: 8px;
}
.overdue-text {
font-size: 12px;
color: #ff4444;
}
.empty-state {
padding: 40px 0;
text-align: center;
}
.empty-text {
font-size: 14px;
color: #999;
}
.load-more-tip {
padding: 20px 0;
text-align: center;
}
.load-more-text {
font-size: 12px;
color: #999;
}
</style>