开发了多角色登录功能:实现了普通用户、企业用户和管理员可以分别通过不同入口登录系统,并且支持用账号、邮箱或手机号登录。

开发了权限分配功能:实现了一个可以在后台勾选页面的功能,通过给角色勾选菜单,就能直接控制不同身份的人登录后能看到哪些页面。
开发了实名认证功能:实现了企业可以提交营业执照认证,个人可以提交身份证件和技能认证的功能,管理员在后台可以进行审核。
开发了任务大厅功能:实现了企业可以发布需要做的任务,个人用户能在任务大厅里看到这些任务,并且可以点击申请接单,大家都能看到任务是“进行中”还是“已完成”状态。
开发了专家库与邀约功能:实现了企业可以去专家库里搜索合适的人才,并且可以直接给他们发送工作邀约。
开发了平台数据大屏展示功能:实现了在首页和各自的工作台页面,展示任务数量、收益金额等核心数据的概览面板。
This commit is contained in:
2026-04-28 16:02:20 +08:00
commit 8e18e77747
83 changed files with 21363 additions and 0 deletions

View File

@@ -0,0 +1,721 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { Search, Building2, MoreHorizontal, CheckCircle, XCircle, Trash2, ShieldCheck, Upload, UserPlus, RotateCcw } from 'lucide-vue-next';
import { ElMessage, ElMessageBox } from 'element-plus';
import api, { uploadFileToMinIO } from '@/api';
import dayjs from 'dayjs';
import SlideVerify from '@/components/common/SlideVerify.vue';
const enterprises = ref<any[]>([]);
const loading = ref(true);
const selectedEnterprises = ref<any[]>([]);
const fetchEnterprises = async () => {
loading.value = true;
try {
const res: any = await api.get('/enterprises/');
enterprises.value = res.results || res;
} catch (error) {
ElMessage.error('无法获取企业列表');
} finally {
loading.value = false;
}
};
const handleEntSelectionChange = (val: any[]) => {
selectedEnterprises.value = val;
};
const handleStatusChange = async (row: any, status: string) => {
try {
await api.post(`/enterprises/${row.id}/verify_status/`, { status });
row.status = status;
ElMessage.success('审核状态已更新');
} catch (error) {
ElMessage.error('更新审核状态失败');
}
};
const enterpriseDialogVisible = ref(false);
const isEnterpriseEditing = ref(false);
const enterpriseSaveLoading = ref(false);
const enterpriseForm = ref({
id: '', email: '', password: '', company_name: '', credit_code: '',
contact_name: '', contact_phone: '', landline: '', contact_email: '',
address: '', description: '', logo_url: '', business_license: ''
});
const enterpriseDrawerVisible = ref(false);
const selectedEnterprise = ref<any>(null);
const enterpriseMembers = ref<any[]>([]);
const enterpriseTasks = ref<any[]>([]);
const membersLoading = ref(false);
const memberDialogVisible = ref(false);
const memberSaveLoading = ref(false);
const memberForm = ref({ username: '', password: '', nickname: '', role: 'MEMBER' });
const addEnterpriseMember = async () => {
memberSaveLoading.value = true;
try {
await api.post('/enterprise-members/add_member/', {
...memberForm.value,
enterprise_id: selectedEnterprise.value.id
});
ElMessage.success('成员添加成功');
memberDialogVisible.value = false;
memberForm.value = { username: '', password: '', nickname: '', role: 'MEMBER' };
openEnterpriseDrawer(selectedEnterprise.value); // refresh list
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || error.response?.data?.username?.[0] || '添加失败,请检查输入');
} finally {
memberSaveLoading.value = false;
}
};
const handleLogoUpload = async (options: any) => {
try {
const url = await uploadFileToMinIO(options.file, 'avatars');
enterpriseForm.value.logo_url = url;
ElMessage.success('企业 Logo 上传成功');
} catch (error) {
ElMessage.error('企业 Logo 上传失败');
}
};
const handleLicenseUpload = async (options: any) => {
try {
const url = await uploadFileToMinIO(options.file, 'certifications');
enterpriseForm.value.business_license = url;
ElMessage.success('营业执照上传成功');
} catch (error) {
ElMessage.error('营业执照上传失败');
}
};
const openEnterpriseDrawer = async (row: any) => {
selectedEnterprise.value = row;
enterpriseDrawerVisible.value = true;
membersLoading.value = true;
try {
const [membersRes, tasksRes]: any[] = await Promise.all([
api.get('/enterprise-members/', { params: { enterprise: row.id } }),
api.get('/tasks/', { params: { enterprise: row.id } }),
]);
enterpriseMembers.value = membersRes.results || membersRes;
enterpriseTasks.value = tasksRes.results || tasksRes || [];
} catch (error) {
ElMessage.error('获取企业详情失败');
} finally {
membersLoading.value = false;
}
};
const openCreateEnterpriseDialog = () => {
isEnterpriseEditing.value = false;
enterpriseForm.value = {
id: '', email: '', password: '', company_name: '', credit_code: '',
contact_name: '', contact_phone: '', landline: '', contact_email: '',
address: '', description: '', logo_url: '', business_license: ''
};
enterpriseDialogVisible.value = true;
};
const openEditEnterpriseDialog = (row: any) => {
isEnterpriseEditing.value = true;
enterpriseForm.value = {
id: row.id, email: '', password: '', company_name: row.company_name,
credit_code: row.credit_code, contact_name: row.contact_name,
contact_phone: row.contact_phone, landline: row.landline || '', contact_email: row.contact_email || '',
address: row.address || '', description: row.description || '',
logo_url: row.logo_url || '', business_license: row.business_license || ''
};
enterpriseDialogVisible.value = true;
};
const saveEnterprise = async () => {
enterpriseSaveLoading.value = true;
try {
if (isEnterpriseEditing.value) {
await api.put(`/enterprises/${enterpriseForm.value.id}/`, enterpriseForm.value);
ElMessage.success('企业信息已更新');
} else {
await api.post('/enterprises/', enterpriseForm.value);
ElMessage.success('企业创建成功');
}
enterpriseDialogVisible.value = false;
fetchEnterprises();
} catch (error: any) {
ElMessage.error('操作失败,请检查填写信息');
} finally {
enterpriseSaveLoading.value = false;
}
};
const deleteConfirmVisible = ref(false);
const batchDeleteVisible = ref(false);
const batchEntRemoveVisible = ref(false);
const enterpriseToDelete = ref<any>(null);
const selectedMembers = ref<any[]>([]);
const isDeleteVerified = ref(false);
const isBatchDeleteVerified = ref(false);
const isBatchEntRemoveVerified = ref(false);
const deleteLoading = ref(false);
const slideVerifyRef = ref<any>(null);
const batchSlideVerifyRef = ref<any>(null);
const batchEntSlideVerifyRef = ref<any>(null);
const handleMemberSelectionChange = (val: any[]) => {
selectedMembers.value = val;
};
const deleteEnterprise = async (row: any) => {
enterpriseToDelete.value = row;
isDeleteVerified.value = false;
if (slideVerifyRef.value) slideVerifyRef.value.reset();
try {
const res: any = await api.get('/enterprise-members/', { params: { enterprise: row.id } });
const members = res.results || res;
if (members.length > 0) {
await ElMessageBox.confirm(
`企业 <strong>${row.company_name}</strong> 当前仍有 <strong>${members.length}</strong> 名员工。强制清退将自动移除所有成员,并取消进行中的任务。<br><br>是否继续?`,
'强制清退企业',
{ dangerouslyUseHTMLString: true, type: 'warning', confirmButtonText: '强制清退', cancelButtonText: '取消', confirmButtonClass: 'el-button--danger' }
);
}
deleteConfirmVisible.value = true;
} catch (error) {
// User cancelled
}
};
const confirmDeleteEnterprise = async () => {
if (!enterpriseToDelete.value) return;
deleteLoading.value = true;
try {
await api.delete(`/enterprises/${enterpriseToDelete.value.id}/`);
ElMessage.success('企业已成功清退');
deleteConfirmVisible.value = false;
fetchEnterprises();
} catch (error) {
ElMessage.error('清退失败');
} finally {
deleteLoading.value = false;
}
};
const batchRemoveEnterprises = async () => {
if (selectedEnterprises.value.length === 0) return;
deleteLoading.value = true;
try {
for (const ent of selectedEnterprises.value) {
await api.delete(`/enterprises/${ent.id}/`);
}
ElMessage.success(`已批量清退 ${selectedEnterprises.value.length} 家企业`);
batchEntRemoveVisible.value = false;
selectedEnterprises.value = [];
fetchEnterprises();
} catch (error) {
ElMessage.error('批量清退失败');
} finally {
deleteLoading.value = false;
}
};
const confirmBatchDeleteMembers = async () => {
if (selectedMembers.value.length === 0) return;
deleteLoading.value = true;
try {
const memberIds = selectedMembers.value.map(m => m.id);
await api.post('/enterprise-members/batch_delete/', { member_ids: memberIds });
ElMessage.success(`已成功批量移除 ${selectedMembers.value.length} 名员工`);
batchDeleteVisible.value = false;
const res: any = await api.get('/enterprise-members/', { params: { enterprise: selectedEnterprise.value.id } });
enterpriseMembers.value = res.results || res;
selectedMembers.value = [];
} catch (error) {
ElMessage.error('批量移除失败,请检查网络或重试');
} finally {
deleteLoading.value = false;
}
};
const removeMember = (member: any) => {
ElMessageBox.confirm(`确定要移除员工 ${member.user_details?.nickname || member.user_details?.username} 吗?`, '移除员工', {
confirmButtonText: '确定移除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
}).then(async () => {
try {
await api.delete(`/enterprise-members/${member.id}/`);
ElMessage.success('已成功移除');
const res: any = await api.get('/enterprise-members/', { params: { enterprise: selectedEnterprise.value.id } });
enterpriseMembers.value = res.results || res;
} catch (e) {
ElMessage.error('移除失败');
}
}).catch(() => {});
};
const taskStatusMap: Record<string, { type: string, text: string }> = {
'DRAFT': { type: 'info', text: '草稿' },
'OPEN': { type: 'primary', text: '招募中' },
'IN_PROGRESS': { type: 'warning', text: '进行中' },
'IN_REVIEW': { type: '', text: '待验收' },
'COMPLETED': { type: 'success', text: '已完成' },
'CANCELLED': { type: 'danger', text: '已取消' },
};
onMounted(() => {
fetchEnterprises();
});
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD');
const statusConfig: Record<string, { type: string, text: string }> = {
'PENDING': { type: 'warning', text: '待审核' },
'VERIFIED': { type: 'success', text: '已认证' },
'REJECTED': { type: 'danger', text: '已拒绝' }
};
// ─── Enterprise Recycle Bin ───
const activeTab = ref('active');
const deletedEnterprises = ref<any[]>([]);
const deletedLoading = ref(false);
const fetchDeletedEnterprises = async () => {
deletedLoading.value = true;
try {
const res: any = await api.get('/enterprises/deleted_enterprises/');
deletedEnterprises.value = res.results || res || [];
} catch (error) {
ElMessage.error('无法获取已删除企业列表');
} finally {
deletedLoading.value = false;
}
};
const restoreEnterprise = async (ent: any) => {
try {
await ElMessageBox.confirm(
`确定要恢复企业 <strong>${ent.company_name}</strong> 吗?`,
'恢复企业',
{ dangerouslyUseHTMLString: true, type: 'info', confirmButtonText: '确认恢复' }
);
await api.post(`/enterprises/${ent.id}/restore/`);
ElMessage.success('企业已恢复');
fetchDeletedEnterprises();
fetchEnterprises();
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error?.response?.data?.detail || '恢复失败');
}
}
};
watch(activeTab, (val) => {
if (val === 'deleted') fetchDeletedEnterprises();
});
</script>
<template>
<div class="space-y-4">
<el-tabs v-model="activeTab" class="!-mb-2">
<el-tab-pane name="active">
<template #label><span class="flex items-center gap-1.5"><Building2 class="w-4 h-4" />企业管理</span></template>
</el-tab-pane>
<el-tab-pane name="deleted">
<template #label><span class="flex items-center gap-1.5"><Trash2 class="w-4 h-4" />回收站</span></template>
</el-tab-pane>
</el-tabs>
<el-card v-show="activeTab === 'active'" shadow="never" class="!border-none">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-800">企业管理</h2>
<div class="flex gap-2">
<el-button v-if="selectedEnterprises.length > 0" type="danger" plain @click="() => { batchEntRemoveVisible = true; isBatchEntRemoveVerified = false; }">
<Trash2 class="w-4 h-4 mr-1" /> 批量清退 ({{ selectedEnterprises.length }})
</el-button>
<el-button type="primary" @click="openCreateEnterpriseDialog">新增企业</el-button>
</div>
</div>
<!-- Grid Layout -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" v-loading="loading">
<el-card v-for="row in enterprises" :key="row.id" shadow="hover"
class="border-transparent hover:border-blue-200 hover:shadow-xl transition-all duration-300 rounded-xl relative"
:class="{ '!border-blue-400 ring-2 ring-blue-200': selectedEnterprises.some(e => e.id === row.id) }"
:body-style="{ padding: '0px', display: 'flex', flexDirection: 'column', height: '100%' }">
<!-- Selection checkbox -->
<el-checkbox
class="!absolute top-3 left-3 z-10"
:model-value="selectedEnterprises.some(e => e.id === row.id)"
@change="(v: any) => { if (v) selectedEnterprises.push(row); else selectedEnterprises = selectedEnterprises.filter(e => e.id !== row.id); }"
/>
<!-- Header -->
<div class="flex items-start justify-between p-6 pb-4 cursor-pointer pl-10" @click="openEnterpriseDrawer(row)">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-50 to-indigo-50 flex items-center justify-center flex-shrink-0 shadow-sm border border-blue-100/50 overflow-hidden">
<img v-if="row.logo_url" :src="row.logo_url" class="w-full h-full object-cover" />
<Building2 v-else class="w-6 h-6 text-blue-600" />
</div>
<el-dropdown trigger="click">
<div class="cursor-pointer p-1 text-gray-400 hover:text-blue-500 transition-colors" @click.stop>
<MoreHorizontal class="w-5 h-5" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openEditEnterpriseDialog(row)">编辑信息</el-dropdown-item>
<el-dropdown-item v-if="row.status !== 'VERIFIED'" @click="handleStatusChange(row, 'VERIFIED')"><CheckCircle class="w-4 h-4 mr-2 text-green-500" />通过认证</el-dropdown-item>
<el-dropdown-item v-if="row.status === 'PENDING'" @click="handleStatusChange(row, 'REJECTED')"><XCircle class="w-4 h-4 mr-2 text-red-500" />驳回申请</el-dropdown-item>
<el-dropdown-item divided class="text-red-500" @click="deleteEnterprise(row)"><Trash2 class="w-4 h-4 mr-2" />强制清退</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<!-- Content -->
<div class="px-6 pb-6 flex-1 cursor-pointer" @click="openEnterpriseDrawer(row)">
<div class="font-bold text-lg text-gray-800 line-clamp-1 mb-1">{{ row.company_name }}</div>
<div class="text-xs text-gray-400 mb-5 font-mono bg-gray-50 inline-block px-2 py-1 rounded">{{ row.credit_code || '无统一信用代码' }}</div>
<div class="space-y-3">
<div class="flex items-center text-sm">
<span class="text-gray-400 w-16 text-xs tracking-widest">负责人</span>
<span class="font-medium text-gray-700 flex items-center gap-1.5">
<el-avatar :size="18" :src="row.user_details?.avatar_url" class="bg-gray-100">{{ row.user_details?.nickname?.[0] || '管' }}</el-avatar>
{{ row.user_details?.nickname || row.user_details?.username || '未绑定' }}
</span>
</div>
<div class="flex items-center text-sm">
<span class="text-gray-400 w-16 text-xs tracking-widest">联系人</span>
<span class="font-medium text-gray-700">{{ row.contact_name || '未提供' }}</span>
</div>
<div class="flex items-center text-sm">
<span class="text-gray-400 w-16 text-xs tracking-widest">电话</span>
<span class="font-medium text-gray-700">{{ row.contact_phone || '未提供' }}</span>
</div>
<div class="flex items-center text-sm">
<span class="text-gray-400 w-16 text-xs tracking-widest">入驻日</span>
<span class="font-medium text-gray-700">{{ formatDate(row.created_at) }}</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="bg-gray-50 px-6 py-3 border-t border-gray-100 flex justify-between items-center mt-auto">
<span class="text-xs text-gray-500 font-medium">认证状态</span>
<el-tag :type="statusConfig[row.status]?.type || 'info'" size="small" effect="dark" class="!border-none shadow-sm rounded-md tracking-wider px-3">
{{ statusConfig[row.status]?.text || row.status }}
</el-tag>
</div>
</el-card>
<div v-if="enterprises.length === 0 && !loading" class="col-span-full py-20 text-center text-gray-400">
<el-empty description="暂无企业数据" />
</div>
</div>
<el-dialog v-model="enterpriseDialogVisible" :title="isEnterpriseEditing ? '企业详情/编辑' : '代注册新企业'" width="650px">
<el-form :model="enterpriseForm" label-width="100px" class="pr-6">
<template v-if="!isEnterpriseEditing">
<div class="mb-4 text-sm text-gray-500 bg-blue-50 p-3 rounded-lg flex items-center gap-2">
<ShieldCheck class="w-4 h-4 text-blue-500" /> 作为管理员代注册,将自动为其生成系统账号并免审直接生效。
</div>
<el-divider content-position="left">账号凭证</el-divider>
<el-form-item label="主账号邮箱" required>
<el-input v-model="enterpriseForm.email" placeholder="将作为企业主账号登录凭证" />
</el-form-item>
<el-form-item label="初始密码" required>
<el-input v-model="enterpriseForm.password" type="password" show-password placeholder="不少于6位" />
</el-form-item>
<el-divider content-position="left">资质资料</el-divider>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业全称" required>
<el-input v-model="enterpriseForm.company_name" placeholder="营业执照全称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="信用代码" required>
<el-input v-model="enterpriseForm.credit_code" placeholder="18位社会信用代码" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业Logo">
<el-upload
class="border border-gray-200 border-dashed rounded-lg cursor-pointer hover:border-blue-500 w-24 h-24 flex items-center justify-center overflow-hidden bg-gray-50 group"
action=""
:http-request="handleLogoUpload"
:show-file-list="false"
>
<img v-if="enterpriseForm.logo_url" :src="enterpriseForm.logo_url" class="w-full h-full object-cover" />
<div v-else class="text-gray-400 group-hover:text-blue-500 flex flex-col items-center">
<Upload class="w-5 h-5 mb-1" />
<span class="text-xs">点击上传</span>
</div>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="营业执照">
<el-upload
class="border border-gray-200 border-dashed rounded-lg cursor-pointer hover:border-blue-500 w-32 h-24 flex items-center justify-center overflow-hidden bg-gray-50 group"
action=""
:http-request="handleLicenseUpload"
:show-file-list="false"
>
<img v-if="enterpriseForm.business_license" :src="enterpriseForm.business_license" class="w-full h-full object-cover" />
<div v-else class="text-gray-400 group-hover:text-blue-500 flex flex-col items-center">
<Upload class="w-5 h-5 mb-1" />
<span class="text-xs">上传资质</span>
</div>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="业务联系人" required>
<el-input v-model="enterpriseForm.contact_name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号可选">
<el-input v-model="enterpriseForm.contact_phone" placeholder="手机号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="座机号码可选">
<el-input v-model="enterpriseForm.landline" placeholder=" 0592-5555555" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系邮箱">
<el-input v-model="enterpriseForm.contact_email" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="联系邮箱">
<el-input v-model="enterpriseForm.contact_email" />
</el-form-item>
<el-form-item label="企业地址">
<el-input v-model="enterpriseForm.address" />
</el-form-item>
<el-form-item label="企业介绍">
<el-input type="textarea" v-model="enterpriseForm.description" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="enterpriseDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEnterprise" :loading="enterpriseSaveLoading">{{ isEnterpriseEditing ? '保存修改' : '确认并创建账号' }}</el-button>
</template>
</el-dialog>
<!-- Enterprise Details Drawer -->
<el-drawer v-model="enterpriseDrawerVisible" size="600px" title="企业详情与团队">
<div v-if="selectedEnterprise" class="space-y-8 pb-10">
<!-- Header Info -->
<div class="flex items-center gap-6">
<el-avatar :size="80" shape="square" class="rounded-2xl shadow-sm border border-gray-100" :src="selectedEnterprise.logo_url">
<Building2 v-if="!selectedEnterprise.logo_url" class="w-10 h-10 text-gray-400" />
</el-avatar>
<div>
<h2 class="text-2xl font-bold text-gray-800 mb-2">{{ selectedEnterprise.company_name }}</h2>
<el-tag :type="statusConfig[selectedEnterprise.status]?.type || 'info'" size="small" effect="dark" class="!border-none shadow-sm rounded-md tracking-wider">
{{ statusConfig[selectedEnterprise.status]?.text || selectedEnterprise.status }}
</el-tag>
</div>
</div>
<el-descriptions title="基础档案" :column="1" border class="mt-6">
<el-descriptions-item label="统一信用代码">{{ selectedEnterprise.credit_code || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ selectedEnterprise.contact_name || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ selectedEnterprise.contact_phone || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="座机号码">{{ selectedEnterprise.landline || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="联系邮箱">{{ selectedEnterprise.contact_email || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="企业地址">{{ selectedEnterprise.address || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="入驻时间">{{ formatDate(selectedEnterprise.created_at) }}</el-descriptions-item>
</el-descriptions>
<div>
<h3 class="text-lg font-bold text-gray-800 mb-4 flex justify-between items-center">
<span>团队成员 ({{ enterpriseMembers.length }})</span>
<div class="space-x-2 flex">
<el-button v-if="selectedMembers.length > 0" type="danger" size="small" @click="() => { batchDeleteVisible = true; if (batchSlideVerifyRef) batchSlideVerifyRef.reset(); isBatchDeleteVerified = false; }">
<Trash2 class="w-4 h-4 mr-1" /> 批量移除 ({{ selectedMembers.length }})
</el-button>
<el-button type="primary" size="small" plain @click="memberDialogVisible = true">
<UserPlus class="w-4 h-4 mr-1" /> 添加成员
</el-button>
</div>
</h3>
<el-table :data="enterpriseMembers" v-loading="membersLoading" border @selection-change="handleMemberSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="成员" min-width="150">
<template #default="{ row }">
<div class="flex items-center gap-2">
<el-avatar :size="24" :src="row.user_details?.avatar_url">{{ row.user_details?.nickname?.[0] || 'U' }}</el-avatar>
<span class="font-medium">{{ row.user_details?.nickname || row.user_details?.username }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="role" label="角色" width="100">
<template #default="{ row }">
<el-tag :type="row.role === 'ADMIN' ? 'danger' : 'info'" size="small">{{ row.role === 'ADMIN' ? '管理员' : '普通成员' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="加入时间" width="120">
<template #default="{ row }">
{{ formatDate(row.joined_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text type="danger" size="small" @click="removeMember(row)">移除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-drawer>
<!-- Add Member Dialog -->
<el-dialog v-model="memberDialogVisible" title="添加团队成员" width="400px" append-to-body>
<el-form :model="memberForm" label-width="80px">
<el-form-item label="登录账号" required>
<el-input v-model="memberForm.username" placeholder="英文或数字" />
</el-form-item>
<el-form-item label="初始密码" required>
<el-input v-model="memberForm.password" type="password" show-password />
</el-form-item>
<el-form-item label="成员姓名">
<el-input v-model="memberForm.nickname" placeholder="真实姓名或昵称" />
</el-form-item>
<el-form-item label="系统角色">
<el-select v-model="memberForm.role" class="w-full">
<el-option label="普通成员" value="MEMBER" />
<el-option label="管理员" value="ADMIN" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="memberDialogVisible = false">取消</el-button>
<el-button type="primary" @click="addEnterpriseMember" :loading="memberSaveLoading">确认添加</el-button>
</template>
</el-dialog>
<!-- Enterprise Deletion Dialogs -->
<el-dialog v-model="deleteConfirmVisible" title="确认删除企业" width="400px" append-to-body>
<div class="mb-4 text-gray-600 text-sm">
您即将删除企业 <strong>{{ enterpriseToDelete?.company_name }}</strong>。该操作不可撤销。
</div>
<SlideVerify ref="slideVerifyRef" @success="isDeleteVerified = true" />
<template #footer>
<el-button @click="deleteConfirmVisible = false">取消</el-button>
<el-button type="danger" :disabled="!isDeleteVerified" @click="confirmDeleteEnterprise" :loading="deleteLoading">确认删除</el-button>
</template>
</el-dialog>
<el-dialog v-model="batchDeleteVisible" title="危险操作批量移除员工" width="450px" append-to-body>
<div class="mb-4">
<el-alert type="warning" :closable="false" show-icon>
<template #title>
您即将移除选中的 <strong>{{ selectedMembers.length }}</strong> 名员工。
</template>
</el-alert>
</div>
<div class="mb-6 text-sm text-gray-600 leading-relaxed">
批量移除员工后,这些用户将立刻失去企业的访问权限。为了防止误操作,请滑动下方滑块以验证您的操作。
</div>
<SlideVerify ref="batchSlideVerifyRef" @success="isBatchDeleteVerified = true" />
<template #footer>
<el-button @click="batchDeleteVisible = false">取消</el-button>
<el-button type="danger" :disabled="!isBatchDeleteVerified" @click="confirmBatchDeleteMembers" :loading="deleteLoading">确认批量移除</el-button>
</template>
</el-dialog>
<!-- Batch Enterprise Removal Dialog -->
<el-dialog v-model="batchEntRemoveVisible" title="批量清退企业" width="450px" append-to-body>
<div class="mb-4">
<el-alert type="error" :closable="false" show-icon>
<template #title>
即将强制清退 <strong>{{ selectedEnterprises.length }}</strong> 家企业,关联任务将被自动取消。
</template>
</el-alert>
</div>
<div class="space-y-2 mb-4 max-h-40 overflow-y-auto">
<div v-for="ent in selectedEnterprises" :key="ent.id" class="flex items-center gap-2 text-sm text-gray-600">
<el-avatar :size="20" :src="ent.logo_url" shape="square" class="bg-gray-100">{{ ent.company_name?.[0] }}</el-avatar>
{{ ent.company_name }}
</div>
</div>
<SlideVerify ref="batchEntSlideVerifyRef" @success="isBatchEntRemoveVerified = true" />
<template #footer>
<el-button @click="batchEntRemoveVisible = false">取消</el-button>
<el-button type="danger" :disabled="!isBatchEntRemoveVerified" @click="batchRemoveEnterprises" :loading="deleteLoading">确认批量清退</el-button>
</template>
</el-dialog>
</el-card>
<!-- Deleted Enterprises (Recycle Bin) -->
<el-card v-show="activeTab === 'deleted'" shadow="never" class="!border-none" v-loading="deletedLoading">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2 text-gray-500">
<Trash2 class="w-5 h-5" />
<span class="text-sm">已清退的企业保留在此,支持恢复操作</span>
</div>
</div>
<el-table :data="deletedEnterprises" stripe :header-cell-style="{ background:'#f8fafc', color:'#475569', fontWeight: '600' }">
<el-table-column label="企业" min-width="220">
<template #default="{ row }">
<div class="flex items-center gap-3">
<el-avatar :size="36" :src="row.logo_url" shape="square" class="bg-gray-100 text-gray-600 rounded-lg">
{{ row.company_name?.[0] || 'E' }}
</el-avatar>
<div>
<div class="font-medium text-gray-800">{{ row.company_name }}</div>
<div class="text-xs text-gray-400">{{ row.credit_code?.split('__deleted_')[0] || '-' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="联系人" width="120">
<template #default="{ row }">
<span class="text-sm text-gray-600">{{ row.contact_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="联系电话" width="150">
<template #default="{ row }">
<span class="text-sm text-gray-600 font-mono">{{ row.contact_phone || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="清退时间" width="150">
<template #default="{ row }">
<span class="text-sm text-gray-500">{{ formatDate(row.updated_at) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="restoreEnterprise(row)">
<RotateCcw class="w-3.5 h-3.5 mr-1" /> 恢复
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!deletedLoading && deletedEnterprises.length === 0" description="回收站为空" :image-size="60" />
</el-card>
</div>
</template>