OfficeSystem/pages/project/list/index.vue

994 lines
24 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"
2025-11-17 15:44:45 +08:00
range-key="nickName"
2025-11-17 11:58:49 +08:00
@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>
2025-11-17 16:58:11 +08:00
<uv-button type="success" size="small" @click="goToCreateProject">
<text>新增项目</text>
</uv-button>
2025-11-17 11:58:49 +08:00
</view>
</view>
<!-- 项目列表 -->
2025-11-17 14:06:09 +08:00
<view class="project-scroll">
2025-11-17 11:58:49 +08:00
<view class="project-container">
<view
class="project-card"
v-for="project in projects"
:key="project.id"
@click="goToProjectDetail(project)"
>
2025-11-17 13:58:47 +08:00
<!-- 卡片头部状态标签发布日期操作菜单 -->
<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>
2025-11-17 11:58:49 +08:00
</view>
2025-11-17 13:58:47 +08:00
<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>
2025-11-17 11:58:49 +08:00
</view>
</view>
2025-11-17 13:58:47 +08:00
<!-- 项目标题 -->
2025-11-17 14:51:22 +08:00
<view class="card-title">{{ project.name }}</view>
2025-11-17 13:58:47 +08:00
<!-- 项目标签和描述 -->
2025-11-18 16:02:59 +08:00
<!-- <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">
2025-11-17 14:51:22 +08:00
<text class="card-description" v-if="project.remark">{{ project.remark }}</text>
2025-11-17 13:58:47 +08:00
</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>
2025-11-17 11:58:49 +08:00
</view>
2025-11-17 13:58:47 +08:00
<!-- 团队成员 -->
<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>
2025-11-17 11:58:49 +08:00
</view>
</view>
</view>
<!-- 加载状态 -->
2025-11-17 13:58:47 +08:00
<view class="empty-state" v-if="loading" style="grid-column: 1 / -1;">
2025-11-17 11:58:49 +08:00
<text class="empty-text">加载中...</text>
</view>
<!-- 空状态 -->
2025-11-17 13:58:47 +08:00
<view class="empty-state" v-else-if="isEmpty" style="grid-column: 1 / -1;">
2025-11-17 11:58:49 +08:00
<text class="empty-text">暂无项目数据</text>
</view>
<!-- 加载更多提示 -->
2025-11-17 13:58:47 +08:00
<view class="load-more-tip" v-if="!isEmpty && !loading && !noMore" style="grid-column: 1 / -1;">
2025-11-17 11:58:49 +08:00
<text class="load-more-text">上拉加载更多</text>
</view>
2025-11-17 13:58:47 +08:00
<view class="load-more-tip" v-if="!isEmpty && noMore" style="grid-column: 1 / -1;">
2025-11-17 11:58:49 +08:00
<text class="load-more-text">没有更多数据了</text>
</view>
</view>
2025-11-17 14:06:09 +08:00
</view>
2025-11-17 11:58:49 +08:00
</view>
</template>
<script setup>
2025-11-17 16:58:11 +08:00
import { ref, computed, watch, onMounted, nextTick, onUnmounted } from 'vue';
2025-11-17 14:06:09 +08:00
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
2025-11-17 11:58:49 +08:00
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: '',
2025-11-17 15:44:45 +08:00
joinUserId: '',
2025-11-17 11:58:49 +08:00
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;
}
2025-11-17 15:44:45 +08:00
if (filterParams.value.joinUserId) {
requestParams.joinUserId = filterParams.value.joinUserId;
2025-11-17 11:58:49 +08:00
}
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('、');
};
2025-11-17 13:58:47 +08:00
// 获取紧急状态标签
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 handleCardMenu = (project) => {
uni.showActionSheet({
itemList: ['修改', '删除', '+ 新增任务', '√ 开始开发'],
success: (res) => {
if (res.tapIndex === 0) {
2025-11-17 16:58:11 +08:00
goToEditProject(project);
2025-11-17 13:58:47 +08:00
} else if (res.tapIndex === 1) {
// 删除
uni.showToast({ title: '删除功能开发中', icon: 'none' });
} else if (res.tapIndex === 2) {
// 新增任务
uni.showToast({ title: '新增任务功能开发中', icon: 'none' });
} else if (res.tapIndex === 3) {
// 开始开发
uni.showToast({ title: '开始开发功能开发中', icon: 'none' });
}
}
});
};
2025-11-17 16:58:11 +08:00
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}`
});
};
2025-11-17 11:58:49 +08:00
// 处理状态标签点击
const handleStatusTabClick = (value) => {
activeStatusTab.value = value;
handleSearch();
};
// 处理成员选择
const handleMemberChange = (e) => {
const index = e.detail.value;
const member = memberOptions.value[index];
if (member) {
2025-11-17 15:44:45 +08:00
filterParams.value.joinUserId = member.userId;
selectedMemberName.value = member.nickName;
2025-11-17 11:58:49 +08:00
} else {
2025-11-17 15:44:45 +08:00
filterParams.value.joinUserId = '';
2025-11-17 11:58:49 +08:00
selectedMemberName.value = '';
}
};
// 搜索
const handleSearch = () => {
2025-11-17 14:06:09 +08:00
// 使用 updateParams 更新查询参数,会自动重置页码并重新加载数据
updateParams({});
2025-11-17 11:58:49 +08:00
};
// 重置
const handleReset = () => {
filterParams.value = {
customerName: '',
projectId: '',
projectName: '',
2025-11-17 15:44:45 +08:00
joinUserId: '',
2025-11-17 11:58:49 +08:00
overdue: ''
};
selectedMemberName.value = '';
2025-11-17 13:58:47 +08:00
activeStatusTab.value = 'IN_PROGRESS';
2025-11-17 14:06:09 +08:00
// 使用 updateParams 更新查询参数,会自动重置页码并重新加载数据
updateParams({});
2025-11-17 11:58:49 +08:00
};
2025-11-17 14:06:09 +08:00
// 上拉加载更多 - 使用 uniapp 的 onReachBottom
onReachBottom(() => {
2025-11-17 11:58:49 +08:00
if (!noMore.value && !loading.value) {
loadMore();
}
2025-11-17 14:06:09 +08:00
});
// 下拉刷新 - 使用 uniapp 的 onPullDownRefresh
onPullDownRefresh(async () => {
try {
// 重置并刷新数据
await refresh();
} finally {
// 停止下拉刷新动画
uni.stopPullDownRefresh();
}
});
2025-11-17 11:58:49 +08:00
// 跳转到项目详情
const goToProjectDetail = (project) => {
// TODO: 跳转到项目详情页面
uni.showToast({
title: '项目详情功能开发中',
icon: 'none'
});
};
// 加载成员列表
const loadMemberList = async () => {
try {
const res = await getUserList({ pageSize: 200 });
if (res && res.rows) {
2025-11-17 15:44:45 +08:00
2025-11-17 11:58:49 +08:00
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();
2025-11-17 16:58:11 +08:00
uni.$on('projectListRefresh', handleSearch);
});
onUnmounted(() => {
uni.$off('projectListRefresh', handleSearch);
2025-11-17 11:58:49 +08:00
});
onLoad(() => {
nextTick(() => {
isInitialized.value = true;
2025-11-17 14:06:09 +08:00
// 初始化时直接调用 getList使用 reset=true 重置列表
getList(true);
2025-11-17 11:58:49 +08:00
});
});
</script>
<style lang="scss" scoped>
.project-list-page {
width: 100%;
2025-11-17 14:51:22 +08:00
min-height: 100vh;
2025-11-17 11:58:49 +08:00
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;
2025-11-17 13:58:47 +08:00
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
2025-11-17 11:58:49 +08:00
.project-card {
background: #fff;
2025-11-17 13:58:47 +08:00
border-radius: 8px;
2025-11-17 11:58:49 +08:00
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
2025-11-17 13:58:47 +08:00
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: relative;
transition: all 0.3s ease;
cursor: pointer;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.project-card:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
/* 卡片头部 */
.card-header {
2025-11-17 11:58:49 +08:00
display: flex;
2025-11-17 13:58:47 +08:00
align-items: flex-start;
justify-content: space-between;
margin-bottom: 4px;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.header-left {
2025-11-17 11:58:49 +08:00
display: flex;
2025-11-17 13:58:47 +08:00
flex-direction: column;
2025-11-17 11:58:49 +08:00
gap: 6px;
2025-11-17 13:58:47 +08:00
flex: 1;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.status-tag {
display: inline-block;
padding: 4px 10px;
background: #E5E5E5;
border-radius: 4px;
2025-11-17 11:58:49 +08:00
font-size: 12px;
2025-11-17 13:58:47 +08:00
color: #333;
width: fit-content;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.urgent-tag {
display: inline-block;
padding: 4px 10px;
background: #FF4444;
border-radius: 4px;
font-size: 12px;
color: #fff;
width: fit-content;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.header-right {
2025-11-17 11:58:49 +08:00
display: flex;
2025-11-17 13:58:47 +08:00
align-items: center;
2025-11-17 11:58:49 +08:00
gap: 8px;
}
2025-11-17 13:58:47 +08:00
.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 {
2025-11-17 11:58:49 +08:00
font-size: 16px;
font-weight: 600;
color: #333;
2025-11-17 13:58:47 +08:00
margin-top: 4px;
}
/* 标签和描述行 */
.card-tags-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.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 {
2025-11-17 11:58:49 +08:00
font-size: 14px;
color: #666;
2025-11-17 13:58:47 +08:00
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
/* 操作标签 */
.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 {
2025-11-17 11:58:49 +08:00
display: flex;
flex-direction: column;
2025-11-17 13:58:47 +08:00
gap: 8px;
2025-11-17 11:58:49 +08:00
margin-top: 8px;
}
2025-11-17 13:58:47 +08:00
.date-info {
display: flex;
align-items: center;
gap: 6px;
}
.date-text {
font-size: 14px;
color: #333;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.time-icon {
font-size: 14px;
}
.time-text {
font-size: 14px;
color: #666;
&.overdue {
color: #FF4444;
}
}
.member-info {
2025-11-17 11:58:49 +08:00
display: flex;
2025-11-17 13:58:47 +08:00
align-items: center;
gap: 8px;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.member-avatars {
display: flex;
align-items: center;
2025-11-17 11:58:49 +08:00
}
2025-11-17 13:58:47 +08:00
.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 {
2025-11-17 11:58:49 +08:00
font-size: 12px;
2025-11-17 13:58:47 +08:00
color: #666;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2025-11-17 11:58:49 +08:00
}
.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>