增加通讯录

This commit is contained in:
李志强 2025-11-07 15:58:53 +08:00
parent 8b5915ba0a
commit b159d71a77

View File

@ -0,0 +1,605 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>通讯录</h2>
<div class="header-actions">
<el-input
v-model="searchKeyword"
placeholder="搜索姓名、工号、部门、职位"
clearable
style="width: 300px; margin-right: 12px"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 筛选栏 -->
<div class="filter-bar">
<el-select
v-model="selectedDepartment"
placeholder="选择部门"
clearable
style="width: 200px; margin-right: 12px"
@change="handleFilter"
>
<el-option
v-for="dept in departmentList"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
<el-select
v-model="selectedStatus"
placeholder="选择状态"
clearable
style="width: 150px"
@change="handleFilter"
>
<el-option label="在职" :value="1" />
<el-option label="离职" :value="0" />
</el-select>
</div>
<!-- 员工列表 -->
<div class="contacts-container" v-loading="loading">
<div
v-if="filteredEmployees.length === 0 && !loading"
class="empty-state"
>
<el-empty description="暂无员工信息" />
</div>
<div v-else class="contacts-grid">
<div
v-for="employee in paginatedEmployees"
:key="employee.id"
class="contact-card"
@click="handleViewDetail(employee)"
>
<div class="contact-avatar">
<div style="display: flex; align-items: center; gap: 10px">
<el-avatar
:size="30"
:style="{ backgroundColor: getAvatarColor(employee.name) }"
>
{{ getAvatarText(employee.name) }}
</el-avatar>
<div class="contact-name">{{ employee.name }}</div>
<el-tag
:type="employee.status === 1 ? 'success' : 'danger'"
size="small"
>
{{ employee.status === 1 ? "在职" : "离职" }}
</el-tag>
</div>
</div>
<el-divider style="margin: 20px" />
<div class="contact-info">
<div class="contact-meta">
<div class="meta-item">
<el-icon><User /></el-icon>
<span>{{ employee.employeeNo || "-" }}</span>
</div>
<div class="meta-item">
<el-icon><OfficeBuilding /></el-icon>
<span>{{ employee.departmentName || "-" }}</span>
</div>
<div class="meta-item">
<el-icon><Briefcase /></el-icon>
<span>{{ employee.positionName || "-" }}</span>
</div>
<div class="meta-item">
<el-icon><Phone /></el-icon>
<span>{{ employee.phone || "-" }}</span>
</div>
<div class="meta-item">
<el-icon><Message /></el-icon>
<span>{{ employee.email || "-" }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-bar" v-if="filteredEmployees.length > 0">
<el-pagination
background
v-model:current-page="page"
v-model:page-size="pageSize"
:total="filteredEmployees.length"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 员工详情对话框 -->
<el-dialog
v-model="detailVisible"
title="员工详情"
width="600px"
:close-on-click-modal="false"
>
<div v-if="currentEmployee" class="employee-detail">
<div class="detail-header">
<el-avatar
:size="80"
:style="{ backgroundColor: getAvatarColor(currentEmployee.name) }"
>
{{ getAvatarText(currentEmployee.name) }}
</el-avatar>
<div class="detail-title">
<h3>{{ currentEmployee.name }}</h3>
<el-tag
:type="currentEmployee.status === 1 ? 'success' : 'danger'"
size="small"
>
{{ currentEmployee.status === 1 ? "在职" : "离职" }}
</el-tag>
</div>
</div>
<el-divider />
<div class="detail-content">
<div class="detail-row">
<span class="label">工号</span>
<span class="value">{{ currentEmployee.employeeNo || "-" }}</span>
</div>
<div class="detail-row">
<span class="label">部门</span>
<span class="value">{{
currentEmployee.departmentName || "-"
}}</span>
</div>
<div class="detail-row">
<span class="label">职位</span>
<span class="value">{{ currentEmployee.positionName || "-" }}</span>
</div>
<div class="detail-row">
<span class="label">角色</span>
<span class="value">{{ currentEmployee.roleName || "-" }}</span>
</div>
<div class="detail-row">
<span class="label">手机号</span>
<span class="value">{{ currentEmployee.phone || "-" }}</span>
</div>
<div class="detail-row">
<span class="label">邮箱</span>
<span class="value"
>{{ currentEmployee.email || "-" }}
<a
href="mailto:{{ currentEmployee.email }}"
style="color: var(--el-color-primary); margin-left: 10px"
v-if="currentEmployee?.email"
>
发送邮件
</a>
</span>
</div>
<div class="detail-row" v-if="currentEmployee.bankName">
<span class="label">开户行</span>
<span class="value">{{ currentEmployee.bankName }}</span>
</div>
<div class="detail-row" v-if="currentEmployee.bankAccount">
<span class="label">银行卡号</span>
<span class="value">{{ currentEmployee.bankAccount }}</span>
</div>
<div class="detail-row">
<span class="label">入职时间</span>
<span class="value">{{ currentEmployee.createTime || "-" }}</span>
</div>
</div>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { ElMessage } from "element-plus";
import {
Search,
Refresh,
User,
OfficeBuilding,
Briefcase,
Phone,
Message,
} from "@element-plus/icons-vue";
import { getTenantEmployees } from "@/api/employee";
import { useAuthStore } from "@/stores/auth";
import { useOAStore } from "@/stores/oa";
const authStore = useAuthStore();
const oaStore = useOAStore();
//
const searchKeyword = ref("");
const selectedDepartment = ref(null);
const selectedStatus = ref(null);
//
const page = ref(1);
const pageSize = ref(10);
//
const employees = ref([]);
const loading = ref(false);
const detailVisible = ref(false);
const currentEmployee = ref(null);
// store
const departmentList = computed(() => oaStore.departments);
// ID
const getCurrentTenantId = () => {
if (authStore.user && authStore.user.tenant_id) {
return authStore.user.tenant_id;
}
const userInfo = localStorage.getItem("userInfo");
if (userInfo) {
try {
const user = JSON.parse(userInfo);
return user.tenant_id || user.tenantId || 0;
} catch (e) {
console.error("解析用户信息失败:", e);
}
}
return 0;
};
//
const fetchEmployees = async () => {
loading.value = true;
const tenantId = getCurrentTenantId();
try {
const res = await getTenantEmployees(tenantId);
//
let employeeList = [];
if (Array.isArray(res)) {
employeeList = res;
} else if (res?.data && Array.isArray(res.data)) {
employeeList = res.data;
} else if (res?.data?.data && Array.isArray(res.data.data)) {
employeeList = res.data.data;
} else if (res?.data) {
employeeList = res.data;
}
//
employees.value = employeeList.map((item) => {
//
let departmentName = "";
const departmentId = item.department_id || null;
if (departmentId) {
const deptInfo = oaStore.getDepartmentById(departmentId);
departmentName = deptInfo ? deptInfo.name : "";
}
//
let positionName = "";
const positionId = item.position_id || null;
if (positionId) {
const posInfo = oaStore.getPositionById(positionId);
positionName = posInfo ? posInfo.name : "";
}
//
let roleName = "";
const roleId = item.role || null;
if (roleId) {
const roleInfo = oaStore.getRoleById(roleId);
roleName = roleInfo ? roleInfo.roleName || roleInfo.name : "";
}
//
const createTime = item.create_time || item.createTime || null;
return {
id: item.id,
employeeNo: item.employee_no || item.employeeNo || "",
name: item.name || "",
phone: item.phone || "",
email: item.email || "",
department_id: departmentId,
departmentName: departmentName,
position_id: positionId,
positionName: positionName,
role: roleId,
roleName: roleName,
bankName: item.bank_name || item.bankName || "",
bankAccount: item.bank_account || item.bankAccount || "",
status: item.status || 1,
createTime: createTime
? new Date(createTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "",
};
});
} catch (e) {
employees.value = [];
ElMessage.error("获取员工列表失败");
} finally {
loading.value = false;
}
};
//
const filteredEmployees = computed(() => {
let result = employees.value;
//
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
result = result.filter((emp) => {
return (
emp.name?.toLowerCase().includes(keyword) ||
emp.employeeNo?.toLowerCase().includes(keyword) ||
emp.departmentName?.toLowerCase().includes(keyword) ||
emp.positionName?.toLowerCase().includes(keyword)
);
});
}
//
if (selectedDepartment.value) {
result = result.filter(
(emp) => emp.department_id === selectedDepartment.value
);
}
//
if (selectedStatus.value !== null && selectedStatus.value !== undefined) {
result = result.filter((emp) => emp.status === selectedStatus.value);
}
return result;
});
//
const paginatedEmployees = computed(() => {
const start = (page.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredEmployees.value.slice(start, end);
});
//
const getAvatarText = (name) => {
if (!name) return "?";
return name.charAt(0).toUpperCase();
};
//
const getAvatarColor = (name) => {
if (!name) return "#409EFF";
const colors = [
"#409EFF",
"#67C23A",
"#E6A23C",
"#F56C6C",
"#909399",
"#9C27B0",
"#00BCD4",
"#FF9800",
];
const index = name.charCodeAt(0) % colors.length;
return colors[index];
};
//
const handleSearch = () => {
page.value = 1;
};
//
const handleFilter = () => {
page.value = 1;
};
//
const handlePageChange = (p) => {
page.value = p;
};
const handleSizeChange = (size) => {
pageSize.value = size;
page.value = 1;
};
//
const handleViewDetail = (employee) => {
currentEmployee.value = employee;
detailVisible.value = true;
};
//
const refresh = async () => {
loading.value = true;
try {
await oaStore.refreshAll();
await fetchEmployees();
ElMessage.success("刷新成功");
} catch (error) {
ElMessage.error("刷新失败");
} finally {
loading.value = false;
}
};
onMounted(async () => {
//
try {
await oaStore.fetchAllBaseData();
} catch (error) {
console.error("获取基础数据失败:", error);
}
//
fetchEmployees();
});
</script>
<style lang="less" scoped>
.container-box {
padding: 20px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
}
}
.filter-bar {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.contacts-container {
min-height: 400px;
}
.contacts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.contact-card {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
border-color: var(--el-color-primary-light-7);
}
}
.contact-info {
width: 100%;
}
.contact-name {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.contact-meta {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-direction: column;
align-items: flex-start;
}
.meta-item {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 13px;
color: var(--el-text-color-regular);
.el-icon {
font-size: 14px;
color: var(--el-text-color-placeholder);
}
}
.contact-status {
margin-top: 8px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.pagination-bar {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.employee-detail {
.detail-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.detail-title {
flex: 1;
h3 {
margin: 0 0 8px 0;
font-size: 20px;
}
}
.detail-content {
.detail-row {
display: flex;
margin-bottom: 16px;
line-height: 1.6;
.label {
width: 100px;
color: var(--el-text-color-regular);
font-weight: 500;
}
.value {
flex: 1;
color: var(--el-text-color-primary);
}
}
}
}
</style>