OfficeSystem/pages/project/list/index.vue
2025-11-22 14:24:17 +08:00

1431 lines
35 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="header">
<view @click="goToProjectSearch">
<view style="height: 5px;"></view>
<img src="https://api.ccttiot.com/image-1763782244238.png" alt="" style="width: 20px !important; height: 20px !important;">
</view>
<view class="header-tabs">
<view
class="tab-item"
v-for="tab in mainStatusTabs"
:key="tab.value"
:class="{ 'active': activeStatusTab === tab.value }"
@click="handleStatusTabClick(tab.value)"
>
{{ tab.label }}
</view>
</view>
<view class="filter-btn" @click="showFilter = !showFilter">
<text class="filter-text">筛选</text>
</view>
</view>
<!-- 筛选面板 -->
<view class="filter-panel" v-if="showFilter">
<view class="filter-item">
<text class="filter-label">状态:</text>
<view class="filter-options">
<text
class="filter-option"
v-for="tab in filterStatusTabs"
:key="tab.value"
:class="{ 'active': filterParams.status === tab.value }"
@click="handleFilterStatusChange(tab.value)"
>{{ tab.label }}</text>
</view>
</view>
<view class="filter-item">
<text class="filter-label">成员:</text>
<picker
mode="selector"
:range="memberOptions"
range-key="nickName"
@change="handleMemberChange"
>
<view class="filter-picker">
<text class="picker-text">{{ selectedMemberName || '请选择用户' }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="filter-item">
<text class="filter-label">开发超期:</text>
<view class="filter-options">
<text
class="filter-option"
:class="{ 'active': filterParams.overdue === '' }"
@click="filterParams.overdue = ''"
>全部</text>
<text
class="filter-option"
:class="{ 'active': filterParams.overdue === true }"
@click="filterParams.overdue = true"
>是</text>
<text
class="filter-option"
:class="{ 'active': filterParams.overdue === false }"
@click="filterParams.overdue = false"
>否</text>
</view>
</view>
<view class="filter-actions">
<uv-button size="small" @click="handleReset">重置</uv-button>
<uv-button type="primary" size="small" @click="handleSearch">确定</uv-button>
</view>
</view>
<!-- 项目列表 -->
<view
class="project-list"
:class="{ 'with-filter': showFilter }"
>
<view class="project-container">
<view
class="project-card"
v-for="project in projects"
:key="project.id"
@click="goToProjectDetail(project)"
>
<!-- 卡片头部:状态标签、发布日期、操作菜单 -->
<view class="card-header">
<view class="header-left">
<view class="status-tag">{{ getStatusText(project.status) }}</view>
<view class="urgent-tag" v-if="getUrgentStatus(project)">
{{ getUrgentStatus(project) }}
</view>
</view>
<view class="header-right">
<text class="release-date">发布: {{ formatDate(project.createTime || project.releaseTime) }}</text>
<view class="action-menu" @click.stop="handleCardMenu(project)">
<text class="menu-icon">⋯</text>
</view>
</view>
</view>
<!-- 项目标题 -->
<view class="card-title">{{ project.name }}</view>
<!-- 项目标签和描述 -->
<!-- <view class="card-tags-row">-->
<!-- <view class="tag-circle" :style="{ backgroundColor: getTagColor(project) }">-->
<!-- <text class="tag-text">{{ getTagText(project) }}</text>-->
<!-- </view>-->
<!-- <text class="member-text">{{ project.createName }}</text>-->
<!-- </view>-->
<view class="card-tags-row" v-if="project.remark">
<text class="card-description" v-if="project.remark">{{ project.remark }}</text>
</view>
<!-- 操作标签 -->
<view class="action-tags" v-if="project.tags && project.tags.length > 0">
<view
class="action-tag"
v-for="(tag, index) in project.tags"
:key="index"
>
{{ tag }}
</view>
</view>
<!-- 日期和剩余时间 -->
<view class="card-footer">
<view class="date-info">
<text class="date-text">{{ formatDate(project.expireTime) }}</text>
<text class="time-icon">🕐</text>
<text class="time-text" :class="{ overdue: isOverdue(project) }">
{{ getTimeStatus(project) }}
</text>
</view>
<!-- 团队成员 -->
<view class="member-info" v-if="project.memberList && project.memberList.length > 0">
<view class="member-avatars">
<view
class="member-avatar"
v-for="(member, index) in getDisplayMembers(project.memberList)"
:key="index"
:style="{ backgroundColor: getAvatarColor(member, index) }"
>
<text class="avatar-text">{{ getAvatarText(member) }}</text>
</view>
</view>
<text class="member-text">{{ formatMemberNames(project.memberList) }}</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="empty-state" v-if="loading" style="grid-column: 1 / -1;">
<text class="empty-text">加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="isEmpty" style="grid-column: 1 / -1;">
<text class="empty-text">暂无项目数据</text>
</view>
<!-- 加载更多提示 -->
<view class="load-more-tip" v-if="!isEmpty && !loading && !noMore" style="grid-column: 1 / -1;">
<text class="load-more-text">上拉加载更多</text>
</view>
<view class="load-more-tip" v-if="!isEmpty && noMore" style="grid-column: 1 / -1;">
<text class="load-more-text">没有更多数据了</text>
</view>
</view>
</view>
<!-- 开始开发弹窗 -->
<view v-if="showStartModal" class="start-modal">
<view class="modal-mask" @click="closeStartModal"></view>
<view class="modal-panel">
<view class="modal-header">
<text class="modal-title">开始开发</text>
<text class="modal-close" @click="closeStartModal">✕</text>
</view>
<view class="modal-body">
<text class="modal-subtitle" v-if="currentStartProjectName">项目:{{ currentStartProjectName }}</text>
<view class="field">
<text class="field-label">预计完成日期</text>
<view class="date-input" @click="openStartDatePicker">
<text class="placeholder" v-if="!startProjectForm.expectedCompleteDate">请选择预计完成日期</text>
<text class="value" v-else>{{ startProjectForm.expectedCompleteDate }}</text>
<text class="date-icon">📅</text>
</view>
</view>
</view>
<view class="modal-actions">
<uv-button size="small" @click="closeStartModal" :disabled="startingProject">取消</uv-button>
<uv-button
type="primary"
size="small"
@click="submitStartDevelopment"
:loading="startingProject"
:disabled="startingProject"
>
确定
</uv-button>
</view>
</view>
</view>
<uv-datetime-picker
ref="startDatePickerRef"
v-model="startDatePickerValue"
mode="date"
@confirm="onStartDateConfirm"
></uv-datetime-picker>
<!-- 悬浮添加按钮 -->
<FabPlus @click="goToCreateProject" />
</view>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick, onUnmounted } from 'vue';
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
import { getProjectList, getUserList, deleteProject, startProjectDevelopment } from '@/api';
import { usePagination } from '@/composables';
import { useDictStore } from '@/store/dict';
import { useUserStore } from '@/store/user';
import { getDictLabel } from '@/utils/dict';
import FabPlus from '@/components/FabPlus.vue';
const dictStore = useDictStore();
const userStore = useUserStore();
// 筛选参数
const filterParams = ref({
joinUserId: '',
overdue: '', // ''表示全部, true表示是, false表示否
status: '', // 筛选面板中的状态筛选
});
// 默认状态字典(用于字典数据尚未加载时的兜底)
const fallbackProjectStatusDict = [
{ dictLabel: '前期费用', dictValue: 'DEVELOPMENT_COST', listClass: 'primary', dictSort: 0 },
{ dictLabel: '中期费用', dictValue: 'MIDDLE_COST', listClass: 'success', dictSort: 0 },
{ dictLabel: '后期费用', dictValue: 'AFTER_COST', listClass: 'warning', dictSort: 0 },
{ dictLabel: '待开始', dictValue: 'WAIT_START', listClass: 'info', dictSort: 1 },
{ dictLabel: '开发中', dictValue: 'IN_PROGRESS', listClass: 'warning', dictSort: 100 },
{ dictLabel: '开发完成', dictValue: 'COMPLETED', listClass: 'primary', dictSort: 200 },
{ dictLabel: '已验收', dictValue: 'ACCEPTED', listClass: 'success', dictSort: 300 },
{ dictLabel: '维护中', dictValue: 'MAINTENANCE', listClass: 'warning', dictSort: 400 },
{ dictLabel: '维护到期', dictValue: 'MAINTENANCE_OVERDUE', listClass: 'warning', dictSort: 500 },
{ dictLabel: '开发超期', dictValue: 'DEVELOPMENT_OVERDUE', listClass: 'danger', dictSort: 600 }
];
// 状态标签(使用字典键值)
const statusTabs = computed(() => {
const dictItems = typeof dictStore.getDictByType === 'function'
? dictStore.getDictByType('project_status')
: [];
const source = Array.isArray(dictItems) && dictItems.length > 0
? dictItems
: fallbackProjectStatusDict;
return source
.slice()
.sort((a, b) => {
const sortA = parseInt(a.dictSort) || 0;
const sortB = parseInt(b.dictSort) || 0;
return sortA - sortB;
})
.map(item => ({
label: item.dictLabel || item.label,
value: item.dictValue || item.value,
listClass: item.listClass || item.type || 'primary'
}));
});
const activeStatusTab = ref('IN_PROGRESS'); // 默认选中"开发中"
const showFilter = ref(false);
// 主要状态标签(开发中、维护中、全部)
const mainStatusTabs = computed(() => {
const allTabs = statusTabs.value;
const mainTabs = [
allTabs.find(tab => tab.value === 'IN_PROGRESS'),
allTabs.find(tab => tab.value === 'MAINTENANCE'),
{ label: '全部', value: 'ALL' }
].filter(Boolean);
return mainTabs;
});
// 筛选面板中的其他状态(除了开发中、维护中之外的所有状态)
const filterStatusTabs = computed(() => {
const allTabs = statusTabs.value;
return allTabs.filter(tab =>
tab.value !== 'IN_PROGRESS' &&
tab.value !== 'MAINTENANCE'
);
});
// 当字典加载完成后确保当前选中状态仍然存在
watch(statusTabs, (tabs) => {
if (!Array.isArray(tabs) || tabs.length === 0) {
return;
}
const exists = tabs.some(tab => tab.value === activeStatusTab.value);
if (!exists) {
activeStatusTab.value = tabs[0].value;
}
}, { immediate: true });
// 成员选项
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 && activeStatusTab.value !== 'ALL') {
// 如果导航栏选择的是"开发中"或"维护中",只使用导航栏的状态,忽略筛选面板中的状态
requestParams.statusList = [activeStatusTab.value];
} else if (activeStatusTab.value === 'ALL') {
// 如果导航栏选择的是"全部",则使用筛选面板中的状态(如果有选择的话)
if (filterParams.value.status) {
requestParams.statusList = [filterParams.value.status];
}
// 如果筛选面板中也没有选择状态则不设置statusList显示所有状态
}
// 添加其他筛选条件
if (filterParams.value.joinUserId) {
requestParams.joinUserId = filterParams.value.joinUserId;
}
// 如果筛选面板中选择了开发超期(是或否),则传递参数
if (filterParams.value.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;
}
console.log('查询参数',requestParams);
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 '未知';
// 优先从字典获取字典键值作为dictValue
const dictLabel = getDictLabel('project_status', status);
if (dictLabel && dictLabel !== String(status)) {
return dictLabel;
}
// 默认映射(兼容字典键值)
const statusMap = {
'DEVELOPMENT_COST': '前期费用',
'MIDDLE_COST': '中期费用',
'AFTER_COST': '后期费用',
'WAIT_START': '待开始',
'IN_PROGRESS': '开发中',
'COMPLETED': '开发完成',
'ACCEPTED': '已验收',
'MAINTENANCE': '维护中',
'MAINTENANCE_OVERDUE': '维护到期',
'DEVELOPMENT_OVERDUE': '开发超期',
// 兼容旧数据(数字状态值)
'1': '待开始',
'2': '开发中',
'4': '开发完成',
'5': '已验收',
'6': '维护中',
'7': '维护到期',
'8': '开发超期'
};
return statusMap[String(status)] || `状态${status}`;
};
// 获取状态类型(用于标签颜色)
const getStatusType = (status) => {
if (!status && status !== 0) return 'primary';
const typeMap = {
// 字典键值映射
'DEVELOPMENT_COST': 'primary', // 前期费用
'MIDDLE_COST': 'success', // 中期费用
'AFTER_COST': 'warning', // 后期费用
'WAIT_START': 'primary', // 待开始
'IN_PROGRESS': 'warning', // 开发中
'COMPLETED': 'success', // 开发完成
'ACCEPTED': 'info', // 已验收
'MAINTENANCE': 'primary', // 维护中
'MAINTENANCE_OVERDUE': 'warning', // 维护到期
'DEVELOPMENT_OVERDUE': 'error', // 开发超期
// 兼容旧数据(数字状态值)
'1': 'primary', // 待开始
'2': 'warning', // 开发中
'4': 'success', // 开发完成
'5': 'info', // 已验收
'6': 'primary', // 维护中
'7': 'warning', // 维护到期
'8': 'error' // 开发超期
};
return typeMap[String(status)] || 'primary';
};
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
if (typeof dateStr === 'number') {
return formatDateValue(dateStr);
}
return dateStr.split(' ')[0];
};
const formatDateValue = (value) => {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const extractDateOnly = (value) => {
if (!value) return '';
if (typeof value === 'string') {
const trimmed = value.trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
return trimmed;
}
if (trimmed.includes(' ')) {
return trimmed.split(' ')[0];
}
}
return formatDateValue(value);
};
// 获取负责人名称
const getOwnerNames = (memberList) => {
if (!Array.isArray(memberList) || memberList.length === 0) return '未分配';
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、');
};
// 获取紧急状态标签
const getUrgentStatus = (project) => {
if (!project.expireTime) return '';
const expireDate = new Date(project.expireTime);
const now = new Date();
const diffTime = expireDate - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
// 已逾期
return '';
} else if (diffDays <= 3) {
// 即将到期3天内
return '即将到期';
}
return '';
};
// 判断是否逾期
const isOverdue = (project) => {
if (!project.expireTime) return false;
const expireDate = new Date(project.expireTime);
const now = new Date();
return expireDate < now;
};
// 获取时间状态文本(剩余天数或逾期天数)
const getTimeStatus = (project) => {
if (!project.expireTime) return '';
const expireDate = new Date(project.expireTime);
const now = new Date();
const diffTime = expireDate - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return `逾期${Math.abs(diffDays)}`;
} else if (diffDays === 0) {
return '今天到期';
} else {
return `剩余${diffDays}`;
}
};
// 获取标签颜色(根据项目创建人或负责人)
const getTagColor = (project) => {
const colors = ['#87CEEB', '#FFB6C1', '#FFA500', '#98D8C8', '#DDA0DD'];
const name = project.createName || project.ownerName || '';
if (!name) return colors[0];
const index = name.charCodeAt(0) % colors.length;
return colors[index];
};
// 获取标签文本(取创建人名字的第一个字)
const getTagText = (project) => {
const name = project.createName || project.ownerName || '';
if (!name) return '项';
return name.charAt(0);
};
// 获取显示成员最多3个
const getDisplayMembers = (memberList) => {
if (!Array.isArray(memberList)) return [];
return memberList.slice(0, 3);
};
// 格式化成员名称
const formatMemberNames = (memberList) => {
if (!Array.isArray(memberList) || memberList.length === 0) return '';
const names = memberList.map(member => member.userName || member.name || '').filter(name => name);
if (names.length <= 3) {
return names.join('');
} else {
const firstThree = names.slice(0, 3).join('');
return `${firstThree}${names.length}`;
}
};
// 获取头像颜色
const getAvatarColor = (member, index) => {
const colors = ['#FFB6C1', '#87CEEB', '#DDA0DD', '#98D8C8', '#FFA500'];
return colors[index % colors.length];
};
// 获取头像文本(取名字的第一个字)
const getAvatarText = (member) => {
const name = member.userName || member.name || '';
if (!name) return '?';
return name.charAt(0);
};
const deletingProjectId = ref('');
const showStartModal = ref(false);
const startingProject = ref(false);
const startProjectForm = reactive({
id: '',
expectedCompleteDate: ''
});
const currentStartProjectName = ref('');
const startDatePickerRef = ref(null);
const startDatePickerValue = ref(Date.now());
// 处理卡片菜单
const handleCardMenu = (project) => {
uni.showActionSheet({
itemList: ['修改', '删除', '新增任务', '开始开发'],
success: (res) => {
if (res.tapIndex === 0) {
goToEditProject(project);
} else if (res.tapIndex === 1) {
handleDeleteProject(project);
} else if (res.tapIndex === 2) {
goToAddTask(project);
} else if (res.tapIndex === 3) {
handleStartDevelopment(project);
}
}
});
};
const handleStartDevelopment = (project) => {
if (!project?.id) {
uni.showToast({
title: '缺少项目ID',
icon: 'none'
});
return;
}
startProjectForm.id = project.id;
startProjectForm.expectedCompleteDate = extractDateOnly(project.expectedCompleteDate || project.expireTime || '');
currentStartProjectName.value = project.name || project.projectName || '';
showStartModal.value = true;
};
const closeStartModal = () => {
resetStartForm();
showStartModal.value = false;
};
const resetStartForm = () => {
startProjectForm.id = '';
startProjectForm.expectedCompleteDate = '';
currentStartProjectName.value = '';
};
const submitStartDevelopment = async () => {
if (!startProjectForm.id) {
uni.showToast({
title: '缺少项目ID',
icon: 'none'
});
return;
}
if (!startProjectForm.expectedCompleteDate) {
uni.showToast({
title: '请选择预计完成日期',
icon: 'none'
});
return;
}
try {
startingProject.value = true;
await startProjectDevelopment({
id: startProjectForm.id,
expectedCompleteDate: startProjectForm.expectedCompleteDate
});
uni.showToast({ title: '已开始开发', icon: 'success' });
closeStartModal();
await refresh();
} catch (err) {
console.error('开始开发失败:', err);
uni.showToast({
title: err?.message || '操作失败,请稍后重试',
icon: 'none'
});
} finally {
startingProject.value = false;
}
};
const openStartDatePicker = () => {
startDatePickerValue.value = startProjectForm.expectedCompleteDate
? new Date(startProjectForm.expectedCompleteDate.replace(/-/g, '/')).getTime()
: Date.now();
if (startDatePickerRef.value?.open) {
startDatePickerRef.value.open();
}
};
const onStartDateConfirm = (event) => {
if (!event?.value) return;
startProjectForm.expectedCompleteDate = formatDateValue(event.value);
};
const handleDeleteProject = (project) => {
if (!project?.id) {
uni.showToast({
title: '缺少项目ID',
icon: 'none'
});
return;
}
if (deletingProjectId.value) {
return;
}
const projectName = project.name || project.projectName || '';
uni.showModal({
title: '删除项目',
content: projectName ? `确认删除项目「${projectName}」?` : '确认删除该项目?',
confirmColor: '#f56c6c',
success: async ({ confirm }) => {
if (!confirm) {
return;
}
try {
deletingProjectId.value = project.id;
uni.showLoading({ title: '删除中...', mask: true });
await deleteProject(project.id);
uni.showToast({ title: '删除成功', icon: 'success' });
await refresh();
} catch (err) {
console.error('删除项目失败:', err);
uni.showToast({
title: err?.message || '删除失败,请稍后重试',
icon: 'none'
});
} finally {
deletingProjectId.value = '';
uni.hideLoading();
}
}
});
};
const goToCreateProject = () => {
uni.navigateTo({
url: '/pages/project/form/index?mode=add'
});
};
const goToEditProject = (project) => {
if (!project?.id) {
uni.showToast({
title: '缺少项目ID',
icon: 'none'
});
return;
}
uni.navigateTo({
url: `/pages/project/form/index?mode=edit&id=${project.id}`
});
};
const goToAddTask = (project) => {
if (!project?.id) {
uni.showToast({
title: '缺少项目ID',
icon: 'none'
});
return;
}
const projectName = project.name || project.projectName || '';
const encodedName = encodeURIComponent(projectName);
uni.navigateTo({
url: `/pages/task/add/index?projectId=${project.id}&projectName=${encodedName}`
});
};
// 处理状态标签点击
const handleStatusTabClick = (value) => {
activeStatusTab.value = value;
// 点击导航栏按钮时,清空筛选面板中的状态选择,确保导航栏状态和筛选面板状态独立
filterParams.value.status = '';
handleSearch();
};
// 跳转到项目搜索页面
const goToProjectSearch = () => {
uni.navigateTo({
url: '/pages/project/search/index'
});
};
// 处理成员选择
const handleMemberChange = (e) => {
const index = e.detail.value;
const member = memberOptions.value[index];
if (member) {
filterParams.value.joinUserId = member.userId;
selectedMemberName.value = member.nickName;
} else {
filterParams.value.joinUserId = '';
selectedMemberName.value = '';
}
};
// 处理筛选面板中的状态选择
const handleFilterStatusChange = (status) => {
filterParams.value.status = status;
// 当在筛选面板中选择具体状态(非"全部")时,自动将导航栏切换到"全部",使筛选面板中的状态选择生效
// 这样导航栏的状态和筛选面板中的状态是独立的
if (status) {
activeStatusTab.value = 'ALL';
}
};
// 搜索
const handleSearch = () => {
// 使用 updateParams 更新查询参数,会自动重置页码并重新加载数据
updateParams({});
};
// 重置
const handleReset = () => {
filterParams.value = {
joinUserId: '',
overdue: '',
status: ''
};
selectedMemberName.value = '';
handleSearch();
};
// 上拉加载更多 - 使用 uniapp 的 onReachBottom
onReachBottom(() => {
if (!noMore.value && !loading.value) {
loadMore();
}
});
// 下拉刷新 - 使用 uniapp 的 onPullDownRefresh
onPullDownRefresh(async () => {
try {
// 重置并刷新数据
await refresh();
} finally {
// 停止下拉刷新动画
uni.stopPullDownRefresh();
}
});
// 跳转到项目详情
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();
uni.$on('projectListRefresh', handleSearch);
});
onUnmounted(() => {
uni.$off('projectListRefresh', handleSearch);
});
onLoad(() => {
nextTick(() => {
isInitialized.value = true;
// 初始化时直接调用 getList使用 reset=true 重置列表
getList(true);
});
});
</script>
<style lang="scss" scoped>
.project-list-page {
margin-top: var(--status-bar-height, 0);
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: #f5f7fa;
position: relative;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
gap: 16px;
padding: 5px 24px;
background-color: #fff;
border-bottom: 1px solid #e4e7ed;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.header-tabs {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
flex: 1;
}
.tab-item {
font-size: 14px;
color: #909399;
padding: 8px 0;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
&.active {
color: #303133;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #409eff;
}
}
&:active {
opacity: 0.7;
}
}
.filter-btn {
display: flex;
align-items: center;
padding: 6px 0;
cursor: pointer;
flex-shrink: 0;
&:active {
opacity: 0.7;
}
}
.filter-text {
font-size: 14px;
color: #303133;
font-weight: 500;
}
/* 筛选面板 */
.filter-panel {
background-color: #fff;
padding: 16px;
border-bottom: 1px solid #e4e7ed;
position: fixed;
top: 41px;
right: 0;
left: 0;
z-index: 99;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filter-item {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.filter-label {
font-size: 14px;
color: #606266;
flex-shrink: 0;
font-weight: 500;
min-width: 80px;
}
.filter-input {
flex: 1;
height: 36px;
padding: 0 12px;
background: #f5f6f7;
border-radius: 6px;
font-size: 14px;
}
.filter-picker {
flex: 1;
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;
}
.filter-options {
display: flex;
gap: 10px;
flex-wrap: wrap;
flex: 1;
}
.filter-option {
padding: 6px 16px;
font-size: 14px;
color: #606266;
background-color: #f5f7fa;
border-radius: 20px;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid transparent;
&:active {
transform: scale(0.95);
}
&.active {
color: #2885ff;
background-color: #e6f2ff;
border-color: #2885ff;
font-weight: 500;
}
}
.filter-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e4e7ed;
}
.project-list {
flex: 1;
padding-top: 6px;
padding-bottom: 100px;
background-color: #f5f7fa;
transition: padding-top 0.3s ease;
&.with-filter {
padding-top: 320px; /* header(41px) + filter panel(约280px) */
}
}
.project-container {
padding: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.project-card {
background: #fff;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: relative;
transition: all 0.3s ease;
cursor: pointer;
}
.project-card:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 4px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.status-tag {
display: inline-block;
padding: 4px 10px;
background: #E5E5E5;
border-radius: 4px;
font-size: 12px;
color: #333;
width: fit-content;
}
.urgent-tag {
display: inline-block;
padding: 4px 10px;
background: #FF4444;
border-radius: 4px;
font-size: 12px;
color: #fff;
width: fit-content;
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
.release-date {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.action-menu {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.action-menu:active {
background-color: #f0f0f0;
}
.menu-icon {
font-size: 18px;
color: #666;
line-height: 1;
transform: rotate(90deg);
}
/* 项目标题 */
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-top: 4px;
}
/* 标签和描述行 */
.card-tags-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.tag-circle {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tag-text {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.card-description {
font-size: 14px;
color: #666;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 操作标签 */
.action-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
}
.action-tag {
padding: 4px 10px;
background: #E3F2FD;
border-radius: 4px;
font-size: 12px;
color: #1976D2;
}
/* 卡片底部 */
.card-footer {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.date-info {
display: flex;
align-items: center;
gap: 6px;
}
.date-text {
font-size: 14px;
color: #333;
}
.time-icon {
font-size: 14px;
}
.time-text {
font-size: 14px;
color: #666;
&.overdue {
color: #FF4444;
}
}
.member-info {
display: flex;
align-items: center;
gap: 8px;
}
.member-avatars {
display: flex;
align-items: center;
}
.member-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #fff;
margin-left: -8px;
&:first-child {
margin-left: 0;
}
}
.avatar-text {
font-size: 10px;
color: #fff;
font-weight: 500;
}
.member-text {
font-size: 12px;
color: #666;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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;
}
.start-modal {
position: fixed;
inset: 0;
z-index: 999;
}
.modal-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
}
.modal-panel {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 86%;
max-width: 360px;
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.modal-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.modal-close {
font-size: 18px;
color: #999;
padding: 4px;
}
.modal-body {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.modal-subtitle {
font-size: 14px;
color: #666;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-label {
font-size: 14px;
color: #333;
}
.date-input {
height: 40px;
border-radius: 8px;
background: #f5f6f7;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
color: #333;
}
.date-input .placeholder {
color: #999;
}
.date-icon {
font-size: 16px;
margin-left: 8px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>