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

294 lines
11 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 { ArrowLeft, Rocket, Plus } from 'lucide-vue-next';
import { useRouter, useRoute } from 'vue-router';
import { createTask, updateTask, getTaskDetail } from '@/api/tasks';
import { uploadFileToMinIO } from '@/api/index';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '@/stores/auth';
import api from '@/api';
const authStore = useAuthStore();
const router = useRouter();
const route = useRoute();
const isLoading = ref(false);
// Detect if we are in admin or enterprise context for navigation
const routePrefix = computed(() => route.path.startsWith('/admin') ? '/admin' : '/enterprise');
const isEdit = computed(() => !!route.params.id);
const teamMembers = ref<any[]>([]);
const form = ref({
title: '',
description: '',
skill_tags: [] as string[],
budget_min: null as number | null,
budget_max: null as number | null,
deadline: '',
task_type: 'OUTSOURCE',
attachments: [] as string[],
status: 'OPEN',
contact_user: null as string | null,
contact_name: '',
contact_phone: '',
contact_email: '',
contact_wechat: ''
});
const fileList = ref<any[]>([]);
onMounted(async () => {
fetchSkills();
try {
const res: any = await api.get('/enterprise-members/');
teamMembers.value = res.results || res || [];
} catch (e) {
console.error('Failed to load team members', e);
}
if (isEdit.value) {
try {
const res: any = await getTaskDetail(route.params.id as string);
form.value = {
title: res.title,
description: res.description,
skill_tags: res.skill_tags || [],
budget_min: res.budget_min,
budget_max: res.budget_max,
deadline: res.deadline,
task_type: res.task_type || 'OUTSOURCE',
attachments: res.attachments || [],
status: res.status,
contact_user: res.contact_user,
contact_name: res.contact_name || '',
contact_phone: res.contact_phone || '',
contact_email: res.contact_email || '',
contact_wechat: res.contact_wechat || ''
};
if (res.attachments && Array.isArray(res.attachments)) {
fileList.value = res.attachments.map((url: string, index: number) => ({
name: `附件_${index + 1}`,
url,
uid: index
}));
}
} catch (e: any) {
ElMessage.error('无法加载任务数据');
}
} else {
// Default to current user
if (authStore.user) {
form.value.contact_name = authStore.user.nickname || authStore.user.username || '';
form.value.contact_email = authStore.user.email || '';
}
}
});
const handleContactSelect = (userId: string) => {
const member = teamMembers.value.find(m => m.user === userId);
if (member) {
form.value.contact_name = member.user_details?.nickname || member.user_details?.username || '';
form.value.contact_email = member.user_details?.email || '';
form.value.contact_phone = member.user_details?.phone || '';
}
};
const handleSave = async (statusOverride?: string) => {
if (statusOverride) form.value.status = statusOverride;
if (!form.value.title) {
ElMessage.warning('请输入任务名称');
return;
}
isLoading.value = true;
try {
const uploadedUrls = [];
for (const file of fileList.value) {
if (file.raw) {
const url = await uploadFileToMinIO(file.raw, 'tasks');
uploadedUrls.push(url);
} else {
uploadedUrls.push(file.url);
}
}
form.value.attachments = uploadedUrls;
if (isEdit.value) {
await updateTask(route.params.id as string, form.value);
} else {
await createTask(form.value);
}
ElMessage.success(isEdit.value ? '任务已更新' : '任务已发布');
router.push('/enterprise/tasks');
} catch (e: any) {
ElMessage.error(e.response?.data?.detail || '保存失败');
} finally {
isLoading.value = false;
}
};
const skillOptions = ref<string[]>([]);
const fetchSkills = async () => {
try {
const res: any = await api.get('/skills/');
const items = res.results || res || [];
skillOptions.value = items.map((s: any) => s.name);
} catch (e) {
console.error('Failed to fetch skills', e);
}
};
const taskTypes = [
{ id: 'OUTSOURCE', name: '项目外包' },
{ id: 'CONSULT', name: '专家咨询' },
{ id: 'CONTENT', name: '内容创作' },
{ id: 'DATA_LABEL', name: '数据标注' },
{ id: 'OTHER', name: '其他' },
];
const tagInput = ref('');
const addCustomTag = () => {
if (tagInput.value && !form.value.skill_tags.includes(tagInput.value)) {
form.value.skill_tags.push(tagInput.value);
tagInput.value = '';
}
};
const removeSkill = (skill: string) => {
form.value.skill_tags = form.value.skill_tags.filter(s => s !== skill);
};
</script>
<template>
<div class="max-w-5xl mx-auto space-y-5">
<!-- Header -->
<div class="flex items-center gap-3">
<el-button @click="router.back()" :icon="ArrowLeft" circle />
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ isEdit ? '编辑任务' : '发布新任务' }}</h1>
<p class="text-sm text-gray-400 mt-1"> OPC 专家创建新的合作机会</p>
</div>
</div>
<el-row :gutter="16">
<!-- Left: Form -->
<el-col :span="16">
<div class="space-y-4">
<!-- Basic Info -->
<el-card shadow="never">
<template #header><span class="font-semibold">基本信息</span></template>
<el-form label-position="top">
<el-form-item label="任务名称" required>
<el-input v-model="form.title" placeholder="例如:大语言模型 RLHF 数据标注项目" size="large" />
</el-form-item>
<el-form-item label="任务描述" required>
<el-input v-model="form.description" type="textarea" :rows="10" placeholder="请详细说明任务背景、质量要求、交付标准等..." />
</el-form-item>
</el-form>
</el-card>
<!-- Skills -->
<el-card shadow="never">
<template #header><span class="font-semibold">技能要求</span></template>
<div class="flex flex-wrap gap-2 mb-3">
<el-check-tag
v-for="skill in skillOptions"
:key="skill"
:checked="form.skill_tags.includes(skill)"
@change="(checked: boolean) => checked ? form.skill_tags.push(skill) : removeSkill(skill)"
>{{ skill }}</el-check-tag>
</div>
<div class="flex gap-2">
<el-input v-model="tagInput" placeholder="自定义标签" size="small" style="width: 200px" @keyup.enter="addCustomTag" />
<el-button size="small" @click="addCustomTag" :icon="Plus">添加</el-button>
</div>
<div v-if="form.skill_tags.length > 0" class="mt-3 flex flex-wrap gap-1">
<el-tag v-for="tag in form.skill_tags" :key="tag" closable @close="removeSkill(tag)" type="primary" effect="plain">{{ tag }}</el-tag>
</div>
</el-card>
<!-- Contact Person -->
<el-card shadow="never">
<template #header><span class="font-semibold">项目联系人</span></template>
<el-form label-position="top">
<div class="grid grid-cols-2 gap-4">
<el-form-item label="选择项目经理">
<el-select v-model="form.contact_user" placeholder="默认当前用户" clearable @change="handleContactSelect">
<el-option
v-for="member in teamMembers"
:key="member.user"
:label="member.user_details?.nickname || member.user_details?.username"
:value="member.user"
/>
</el-select>
</el-form-item>
<el-form-item label="联系人姓名">
<el-input v-model="form.contact_name" placeholder="将展示给专家" />
</el-form-item>
<el-form-item label="手机号码">
<el-input v-model="form.contact_phone" placeholder="选填" />
</el-form-item>
<el-form-item label="微信号">
<el-input v-model="form.contact_wechat" placeholder="选填,方便沟通" />
</el-form-item>
<el-form-item label="联系邮箱" class="col-span-2">
<el-input v-model="form.contact_email" placeholder="选填" />
</el-form-item>
</div>
</el-form>
</el-card>
<!-- Attachments -->
<el-card shadow="never">
<template #header><span class="font-semibold">相关附件可选</span></template>
<el-upload v-model:file-list="fileList" drag action="" :auto-upload="false" multiple>
<div class="py-4">
<div class="text-4xl text-gray-300 mb-2">📥</div>
<div class="text-sm text-gray-500">拖拽文件到此处<em class="text-blue-500">点击上传</em></div>
<div class="text-xs text-gray-400 mt-1">支持 PDF / ZIP / 视频 (最大 50MB)</div>
</div>
</el-upload>
</el-card>
</div>
</el-col>
<!-- Right: Sidebar -->
<el-col :span="8">
<el-card shadow="never" class="sticky top-20">
<template #header><span class="font-semibold">发布设置</span></template>
<el-form label-position="top">
<el-form-item label="任务类型">
<el-radio-group v-model="form.task_type" class="!flex flex-col gap-2">
<el-radio v-for="type in taskTypes" :key="type.id" :value="type.id" border class="!mr-0 !w-full">{{ type.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="预算区间 (CNY)">
<div class="flex items-center gap-2 w-full">
<el-input-number v-model="form.budget_min" :min="0" placeholder="最低" controls-position="right" class="flex-1" />
<span class="text-gray-300"></span>
<el-input-number v-model="form.budget_max" :min="0" placeholder="最高" controls-position="right" class="flex-1" />
</div>
</el-form-item>
<el-form-item label="截止日期">
<el-date-picker v-model="form.deadline" type="date" placeholder="选择日期" class="!w-full" value-format="YYYY-MM-DD" />
</el-form-item>
</el-form>
<el-divider />
<div class="space-y-2">
<el-button type="primary" class="w-full" size="large" :loading="isLoading" @click="handleSave('OPEN')">
<Rocket class="w-4 h-4 mr-2" v-if="!isLoading" />
{{ isEdit ? '保存并更新' : '立即发布招募' }}
</el-button>
<el-button v-if="!isEdit" class="w-full" :loading="isLoading" @click="handleSave('DRAFT')">保存至草稿箱</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>