Files
opc-web/src/views/enterprise/TaskCreateView.vue

294 lines
11 KiB
Vue
Raw Normal View History

<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>