开发了多角色登录功能:实现了普通用户、企业用户和管理员可以分别通过不同入口登录系统,并且支持用账号、邮箱或手机号登录。
开发了权限分配功能:实现了一个可以在后台勾选页面的功能,通过给角色勾选菜单,就能直接控制不同身份的人登录后能看到哪些页面。 开发了实名认证功能:实现了企业可以提交营业执照认证,个人可以提交身份证件和技能认证的功能,管理员在后台可以进行审核。 开发了任务大厅功能:实现了企业可以发布需要做的任务,个人用户能在任务大厅里看到这些任务,并且可以点击申请接单,大家都能看到任务是“进行中”还是“已完成”状态。 开发了专家库与邀约功能:实现了企业可以去专家库里搜索合适的人才,并且可以直接给他们发送工作邀约。 开发了平台数据大屏展示功能:实现了在首页和各自的工作台页面,展示任务数量、收益金额等核心数据的概览面板。
This commit is contained in:
262
src/views/user/tasks/MyTasksView.vue
Normal file
262
src/views/user/tasks/MyTasksView.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import { Briefcase, Clock, ArrowRight, CheckCircle, Truck, Search, Ban } from 'lucide-vue-next';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getApplications } from '@/api/tasks';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const allApps = ref<any[]>([]);
|
||||
const isLoading = ref(true);
|
||||
const searchQuery = ref('');
|
||||
|
||||
const tabs = [
|
||||
{ key: 'ALL', label: '全部项目' },
|
||||
{ key: 'APPROVED', label: '执行中' },
|
||||
{ key: 'DELIVERED', label: '待验收' },
|
||||
{ key: 'COMPLETED', label: '已完成' },
|
||||
{ key: 'CANCELLED', label: '已取消' },
|
||||
];
|
||||
|
||||
const validKeys = tabs.map(t => t.key);
|
||||
const initTab = typeof route.query.tab === 'string' && validKeys.includes(route.query.tab)
|
||||
? route.query.tab : 'ALL';
|
||||
const activeTab = ref(initTab);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const res: any = await getApplications();
|
||||
allApps.value = res.results || [];
|
||||
} catch (e) { console.error(e); }
|
||||
finally { isLoading.value = false; }
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
watch(() => route.query.tab, (newTab) => {
|
||||
if (typeof newTab === 'string' && validKeys.includes(newTab)) {
|
||||
activeTab.value = newTab;
|
||||
}
|
||||
});
|
||||
|
||||
const getAppStatus = (app: any) => {
|
||||
if (app.task_detail?.status === 'CANCELLED') return 'CANCELLED';
|
||||
if (app.task_detail?.status === 'COMPLETED' || app.status === 'COMPLETED') return 'COMPLETED';
|
||||
return app.status;
|
||||
};
|
||||
|
||||
const validApps = computed(() => {
|
||||
return allApps.value.filter(app => {
|
||||
const s = getAppStatus(app);
|
||||
return ['APPROVED', 'DELIVERED', 'COMPLETED', 'CANCELLED'].includes(s);
|
||||
});
|
||||
});
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
return validApps.value.filter(app => {
|
||||
const s = getAppStatus(app);
|
||||
const task = app.task_detail || {};
|
||||
const matchSearch = !searchQuery.value ||
|
||||
task.title?.toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
|
||||
if (!matchSearch) return false;
|
||||
if (activeTab.value === 'ALL') return true;
|
||||
return s === activeTab.value;
|
||||
});
|
||||
});
|
||||
|
||||
const tabCounts = computed(() => {
|
||||
const counts: Record<string, number> = { ALL: validApps.value.length, APPROVED: 0, DELIVERED: 0, COMPLETED: 0, CANCELLED: 0 };
|
||||
validApps.value.forEach(app => {
|
||||
const s = getAppStatus(app);
|
||||
if (counts[s] !== undefined) counts[s]++;
|
||||
});
|
||||
return counts;
|
||||
});
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'APPROVED': return 'success';
|
||||
case 'DELIVERED': return 'primary';
|
||||
case 'COMPLETED': return 'success';
|
||||
case 'CANCELLED': return 'danger';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const translateStatus = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'APPROVED': '执行中',
|
||||
'DELIVERED': '待验收',
|
||||
'COMPLETED': '已完成',
|
||||
'CANCELLED': '已取消',
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-5">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-green-50 text-green-500 flex items-center justify-center">
|
||||
<Briefcase class="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-800">我的项目</h1>
|
||||
<p class="text-xs text-gray-400">正在执行和交付中的任务</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button type="primary" @click="router.push('/user/tasks/market')">
|
||||
接更多任务
|
||||
<ArrowRight class="w-4 h-4 ml-1" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs + Search -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-1 bg-gray-50 rounded-lg p-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
@click="activeTab = tab.key"
|
||||
class="tab-btn"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<span class="tab-count">{{ tabCounts[tab.key] || 0 }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<el-input v-model="searchQuery" placeholder="搜索项目名称..." clearable style="width: 240px">
|
||||
<template #prefix><Search class="w-4 h-4 text-gray-400" /></template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- Project Cards -->
|
||||
<div v-loading="isLoading">
|
||||
<div v-if="filteredProjects.length > 0" class="grid grid-cols-1 gap-4">
|
||||
<div
|
||||
v-for="app in filteredProjects"
|
||||
:key="app.id"
|
||||
class="project-card group"
|
||||
@click="router.push(`/user/tasks/${app.task}`)"
|
||||
>
|
||||
<div class="flex items-start gap-4 p-5">
|
||||
<!-- Status icon -->
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||
:class="getAppStatus(app) === 'APPROVED' ? 'bg-green-50 text-green-500' :
|
||||
getAppStatus(app) === 'DELIVERED' ? 'bg-blue-50 text-blue-500' :
|
||||
getAppStatus(app) === 'CANCELLED' ? 'bg-red-50 text-red-500' :
|
||||
'bg-gray-100 text-gray-500'">
|
||||
<CheckCircle v-if="getAppStatus(app) === 'APPROVED'" class="w-5 h-5" />
|
||||
<Truck v-else-if="getAppStatus(app) === 'DELIVERED'" class="w-5 h-5" />
|
||||
<Ban v-else-if="getAppStatus(app) === 'CANCELLED'" class="w-5 h-5" />
|
||||
<CheckCircle v-else class="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<el-tag :type="(getStatusType(getAppStatus(app)) as any)" size="small" effect="light" round>
|
||||
{{ translateStatus(getAppStatus(app)) }}
|
||||
</el-tag>
|
||||
<span class="text-xs text-gray-400 font-mono">#{{ app.id?.toString().substring(0, 8) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="font-bold text-lg text-gray-800 group-hover:text-blue-600 transition-colors truncate">
|
||||
{{ app.task_detail?.title || '任务加载中...' }}
|
||||
</h3>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="flex items-center gap-4 mt-2 text-sm">
|
||||
<span class="text-orange-500 font-bold">¥{{ app.expected_price?.toLocaleString() || 0 }}</span>
|
||||
<span class="text-gray-400 flex items-center gap-1">
|
||||
<Clock class="w-3.5 h-3.5" />{{ app.expected_days || 0 }} 天
|
||||
</span>
|
||||
<span v-if="app.task_detail?.deadline" class="text-gray-400 text-xs">
|
||||
截止 {{ app.task_detail.deadline }}
|
||||
</span>
|
||||
<span v-if="app.task_detail?.enterprise_name" class="text-gray-400 text-xs ml-auto">
|
||||
{{ app.task_detail.enterprise_name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArrowRight class="w-4 h-4 text-gray-300 flex-shrink-0 self-center opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
|
||||
<!-- Progress bar (visual indicator) -->
|
||||
<div class="h-1 bg-gray-50" v-if="['APPROVED', 'DELIVERED'].includes(getAppStatus(app))">
|
||||
<div class="h-full transition-all duration-500"
|
||||
:class="getAppStatus(app) === 'DELIVERED' ? 'bg-blue-400' : 'bg-green-400'"
|
||||
:style="{ width: getAppStatus(app) === 'DELIVERED' ? '80%' : '40%' }">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!isLoading" description="暂无项目" class="bg-white rounded-xl py-16">
|
||||
<el-button type="primary" @click="router.push('/user/tasks/market')">去接单大厅</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tab-btn {
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tab-btn:hover { color: #374151; }
|
||||
.tab-btn.active {
|
||||
background: #fff;
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
}
|
||||
.tab-count {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
background: #f3f4f6;
|
||||
color: #9ca3af;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tab-btn.active .tab-count {
|
||||
background: #ecfdf5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #f0f0f0;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.project-card:hover {
|
||||
border-color: #86efac;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.04);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user