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

722 lines
32 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, 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>