Files
opc-web/src/views/user/tasks/MyApplicationsView.vue
xujl 8e18e77747 开发了多角色登录功能:实现了普通用户、企业用户和管理员可以分别通过不同入口登录系统,并且支持用账号、邮箱或手机号登录。
开发了权限分配功能:实现了一个可以在后台勾选页面的功能,通过给角色勾选菜单,就能直接控制不同身份的人登录后能看到哪些页面。
开发了实名认证功能:实现了企业可以提交营业执照认证,个人可以提交身份证件和技能认证的功能,管理员在后台可以进行审核。
开发了任务大厅功能:实现了企业可以发布需要做的任务,个人用户能在任务大厅里看到这些任务,并且可以点击申请接单,大家都能看到任务是“进行中”还是“已完成”状态。
开发了专家库与邀约功能:实现了企业可以去专家库里搜索合适的人才,并且可以直接给他们发送工作邀约。
开发了平台数据大屏展示功能:实现了在首页和各自的工作台页面,展示任务数量、收益金额等核心数据的概览面板。
2026-04-28 16:22:50 +08:00

263 lines
8.2 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.

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { Send, Clock, CheckCircle, XCircle, ArrowRight, Search } 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: 'PENDING', label: '待审核' },
{ key: 'APPROVED', label: '已通过' },
{ key: 'REJECTED', label: '被拒绝' },
{ key: 'WITHDRAWN', 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);
import { watch } from 'vue';
watch(() => route.query.tab, (newTab) => {
if (typeof newTab === 'string' && validKeys.includes(newTab)) {
activeTab.value = newTab;
}
});
const filteredApps = computed(() => {
return allApps.value.filter(app => {
// DELIVERED/COMPLETED belong to project management, not applications
if (['DELIVERED', 'COMPLETED'].includes(app.status)) return false;
const task = app.task_detail || {};
const matchSearch = !searchQuery.value ||
task.title?.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
task.description?.toLowerCase().includes(searchQuery.value.toLowerCase());
if (!matchSearch) return false;
if (activeTab.value === 'ALL') return true;
return app.status === activeTab.value;
});
});
const tabCounts = computed(() => {
const appOnly = allApps.value.filter(a => !['DELIVERED', 'COMPLETED'].includes(a.status));
const counts: Record<string, number> = { ALL: appOnly.length };
for (const app of appOnly) {
counts[app.status] = (counts[app.status] || 0) + 1;
}
return counts;
});
const getStatusType = (status: string) => {
switch (status) {
case 'APPROVED': return 'success';
case 'DELIVERED': return 'primary';
case 'REJECTED': return 'danger';
case 'PENDING': return 'warning';
case 'COMPLETED': return 'info';
case 'WITHDRAWN': return 'info';
default: return 'info';
}
};
const translateStatus = (status: string) => {
const map: Record<string, string> = {
'PENDING': '待审核',
'APPROVED': '已通过',
'DELIVERED': '已交付',
'REJECTED': '被拒绝',
'WITHDRAWN': '已撤回',
'COMPLETED': '已完成',
};
return map[status] || status;
};
const getTimeAgo = (dateStr: string) => {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 60) return `${mins}分钟前`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}天前`;
return new Date(dateStr).toLocaleDateString();
};
</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-blue-50 text-blue-500 flex items-center justify-center">
<Send 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>
<el-button type="primary" @click="router.push('/user/tasks/market')">
去接单大厅
<ArrowRight class="w-4 h-4 ml-1" />
</el-button>
</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" size="default">
<template #prefix><Search class="w-4 h-4 text-gray-400" /></template>
</el-input>
</div>
<!-- List -->
<div v-loading="isLoading">
<div v-if="filteredApps.length > 0" class="space-y-3">
<div
v-for="app in filteredApps"
:key="app.id"
class="app-card"
@click="router.push(`/user/tasks/${app.task}`)"
>
<!-- Status indicator bar -->
<div class="status-bar"
:class="{
'bg-yellow-400': app.status === 'PENDING',
'bg-green-400': app.status === 'APPROVED',
'bg-red-400': app.status === 'REJECTED',
'bg-blue-400': app.status === 'DELIVERED',
'bg-gray-300': ['WITHDRAWN', 'COMPLETED'].includes(app.status),
}"
></div>
<div class="flex-1 min-w-0 p-4">
<!-- Top row -->
<div class="flex items-center gap-2 mb-2">
<el-tag :type="(getStatusType(app.status) as any)" size="small" effect="light" round>
{{ translateStatus(app.status) }}
</el-tag>
<span class="text-xs text-gray-400 font-mono">#{{ app.id?.toString().substring(0, 8) }}</span>
<span class="text-xs text-gray-400 ml-auto">{{ getTimeAgo(app.created_at) }}</span>
</div>
<!-- Task title -->
<h3 class="font-bold text-gray-800 truncate group-hover:text-blue-600 transition-colors">
{{ app.task_detail?.title || '任务加载中...' }}
</h3>
<!-- Bottom row -->
<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?.enterprise_name" class="text-gray-400 text-xs">
{{ app.task_detail.enterprise_name }}
</span>
</div>
<!-- Rejection reason -->
<div v-if="app.status === 'REJECTED' && app.reject_reason" class="mt-2 text-xs text-red-500 bg-red-50 rounded-lg px-3 py-2">
拒绝原因{{ app.reject_reason }}
</div>
</div>
<ArrowRight class="w-4 h-4 text-gray-300 mr-4 flex-shrink-0 self-center" />
</div>
</div>
<el-empty v-else-if="!isLoading" :description="activeTab === 'ALL' ? '暂无申请记录' : `暂无${tabs.find(t => t.key === activeTab)?.label}的申请`" 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: #3b82f6;
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: #eff6ff;
color: #3b82f6;
}
.app-card {
display: flex;
align-items: stretch;
background: #fff;
border-radius: 12px;
border: 1px solid #f0f0f0;
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
}
.app-card:hover {
border-color: #bfdbfe;
box-shadow: 0 4px 16px rgba(0,0,0,0.04);
transform: translateY(-1px);
}
.status-bar {
width: 4px;
flex-shrink: 0;
}
</style>