OfficeSystem/pages/project/list/index.vue
2025-11-17 11:58:49 +08:00

662 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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表示否
});
// 状态标签
const statusTabs = ref([
{ label: '待开始', value: '1' },
{ label: '开发中', value: '2' },
{ label: '开发完成', value: '4' },
{ label: '已验收', value: '5' },
{ label: '维护中', value: '6' },
{ label: '维护到期', value: '7' },
{ label: '开发超期', value: '8' }
]);
const activeStatusTab = ref('2'); // 默认选中"开发中"
// 成员选项
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) {
// 根据选中的状态标签设置statusList
const statusMap = {
'1': [1], // 待开始
'2': [2], // 开发中
'4': [4], // 开发完成
'5': [5], // 已验收
'6': [6], // 维护中
'7': [7], // 维护到期
'8': [2] // 开发超期也是状态2但需要overdue=true
};
if (activeStatusTab.value === '8') {
// 开发超期需要特殊处理
requestParams.statusList = [2];
requestParams.overdue = true;
} else {
requestParams.statusList = statusMap[activeStatusTab.value] || [];
}
}
// 添加其他筛选条件
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;
}
if (filterParams.value.overdue !== '' && activeStatusTab.value !== '8') {
// 如果不是"开发超期"标签才使用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 '未知';
// 优先从字典获取,如果没有则使用默认映射
const dictLabel = getDictLabel('project_status', status);
if (dictLabel && dictLabel !== String(status)) {
return dictLabel;
}
// 默认映射
const statusMap = {
'1': '待开始',
'2': '开发中',
'4': '开发完成',
'5': '已验收',
'6': '维护中',
'7': '维护到期'
};
return statusMap[String(status)] || `状态${status}`;
};
// 获取状态类型(用于标签颜色)
const getStatusType = (status) => {
if (!status && status !== 0) return 'primary';
const typeMap = {
'1': 'primary', // 待开始
'2': 'warning', // 开发中
'4': 'success', // 开发完成
'5': 'info', // 已验收
'6': 'primary', // 维护中
'7': 'warning' // 维护到期
};
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>