722 lines
32 KiB
Vue
722 lines
32 KiB
Vue
|
|
<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>
|