增加个人办公模块
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 通讯录相关接口
|
||||
|
||||
// 获取通讯录列表
|
||||
export function getContactList(params) {
|
||||
return request({
|
||||
url: '/backend/erp/contact/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取通讯录详情
|
||||
export function getContactDetail(id) {
|
||||
return request({
|
||||
url: `/backend/erp/contact/detail/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 创建联系人
|
||||
export function createContact(data) {
|
||||
return request({
|
||||
url: '/backend/erp/contact/create',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新联系人
|
||||
export function updateContact(id, data) {
|
||||
return request({
|
||||
url: `/backend/erp/contact/update/${id}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除联系人
|
||||
export function deleteContact(id) {
|
||||
return request({
|
||||
url: `/backend/erp/contact/delete/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 收藏/取消收藏
|
||||
export function starContact(id, starred) {
|
||||
return request({
|
||||
url: `/backend/erp/contact/star/${id}`,
|
||||
method: 'post',
|
||||
data: { is_starred: starred }
|
||||
})
|
||||
}
|
||||
|
||||
// 全量同步员工到通讯录
|
||||
export function syncAllContacts(params) {
|
||||
return request({
|
||||
url: '/backend/erp/contact/syncAll',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取通讯录组织树
|
||||
export function getContactOrgTree(params) {
|
||||
return request({
|
||||
url: '/backend/erp/contact/orgTree',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取组织架构列表(用于选择器)
|
||||
export function getOrganizationList(params) {
|
||||
return request({
|
||||
url: '/backend/erp/getOrganization',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取职位列表(用于选择器,可按部门筛选)
|
||||
export function getPositionList(params) {
|
||||
return request({
|
||||
url: '/backend/erp/getPosition',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
@@ -76,6 +76,18 @@ const staticMainChildren = [
|
||||
component: () => import("@/views/apps/oa/organization/index.vue"),
|
||||
meta: { requiresAuth: true, title: "组织架构", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/apps/oa/employee",
|
||||
name: "Employee",
|
||||
component: () => import("@/views/apps/oa/employee/index.vue"),
|
||||
meta: { requiresAuth: true, title: "人员管理", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/apps/oa/position",
|
||||
name: "Position",
|
||||
component: () => import("@/views/apps/oa/position/index.vue"),
|
||||
meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/tools/passwordStore",
|
||||
name: "BackendPasswordStore",
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="联系人详情"
|
||||
width="560px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="contact" class="detail-content">
|
||||
<!-- 头部信息 -->
|
||||
<div class="detail-header-card">
|
||||
<el-avatar :size="56" :src="contact.avatar || ''" class="detail-avatar">
|
||||
{{ contact.contact_name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
<div class="detail-header-info">
|
||||
<div class="detail-name-row">
|
||||
<span class="detail-name">{{ contact.contact_name }}</span>
|
||||
<el-tag :type="contact.contact_type === 1 ? 'primary' : 'info'" size="small">
|
||||
{{ contact.contact_type === 1 ? '内部员工' : '外部联系人' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="contact.is_starred === 1"
|
||||
type="warning"
|
||||
size="small"
|
||||
>
|
||||
已收藏
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-sub" v-if="contact.position_title || contact.org_name || contact.dept_name">
|
||||
<span v-if="contact.position_title">{{ contact.position_title }}</span>
|
||||
<span v-if="contact.position_title && (contact.org_name || contact.dept_name)"> · </span>
|
||||
<span v-if="contact.org_name || contact.dept_name">{{ contact.org_name || contact.dept_name }}</span>
|
||||
</div>
|
||||
<div class="detail-sub" v-if="contact.company_name">
|
||||
{{ contact.company_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<div class="detail-section">
|
||||
<h4>联系方式</h4>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="手机号">
|
||||
<span v-if="contact.phone">
|
||||
<a :href="'tel:' + contact.phone" class="contact-link">{{ contact.phone }}</a>
|
||||
</span>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办公电话">
|
||||
<span v-if="contact.work_phone">
|
||||
<a :href="'tel:' + contact.work_phone" class="contact-link">{{ contact.work_phone }}</a>
|
||||
</span>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
<span v-if="contact.email">
|
||||
<a :href="'mailto:' + contact.email" class="contact-link">{{ contact.email }}</a>
|
||||
</span>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信号">
|
||||
<span>{{ contact.wechat || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<div class="detail-section">
|
||||
<h4>其他信息</h4>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="性别">
|
||||
{{ genderLabel(contact.gender) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">
|
||||
{{ contact.org_name || contact.dept_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="地址">
|
||||
{{ contact.address || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">
|
||||
{{ contact.remark || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="contact.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ contact.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ contact.create_time || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="handleEdit">编辑</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
contact: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'edit'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const genderLabel = (gender) => {
|
||||
const map = { 1: '男', 2: '女' }
|
||||
return map[gender] || '未知'
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
emit('edit', props.contact)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.detail-content {
|
||||
max-height: 65vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-header-card {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-avatar {
|
||||
flex-shrink: 0;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.detail-header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.detail-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-sub {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.contact-link {
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contact-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="620px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
label-position="right"
|
||||
>
|
||||
<el-form-item label="联系人类型" prop="contact_type">
|
||||
<el-radio-group v-model="form.contact_type">
|
||||
<el-radio :value="1">内部员工</el-radio>
|
||||
<el-radio :value="2">外部联系人</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="姓名" prop="contact_name">
|
||||
<el-input
|
||||
v-model="form.contact_name"
|
||||
placeholder="请输入联系人姓名"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-radio-group v-model="form.gender">
|
||||
<el-radio :value="1">男</el-radio>
|
||||
<el-radio :value="2">女</el-radio>
|
||||
<el-radio :value="0">未知</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.contact_type === 1" label="所属部门" prop="org_id">
|
||||
<el-tree-select
|
||||
v-model="form.org_id"
|
||||
:data="orgOptions"
|
||||
:props="orgTreeProps"
|
||||
placeholder="请选择组织架构/部门"
|
||||
clearable
|
||||
check-strictly
|
||||
style="width: 100%"
|
||||
@change="handleOrgChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.contact_type === 1" label="职位" prop="position_id">
|
||||
<el-select
|
||||
v-model="form.position_id"
|
||||
placeholder="请选择职位"
|
||||
clearable
|
||||
filterable
|
||||
:loading="positionLoading"
|
||||
style="width: 100%"
|
||||
@change="handlePositionChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in positionOptions"
|
||||
:key="item.id"
|
||||
:label="item.position_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="form.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="办公电话" prop="work_phone">
|
||||
<el-input
|
||||
v-model="form.work_phone"
|
||||
placeholder="请输入办公电话"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入邮箱"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="微信号" prop="wechat">
|
||||
<el-input
|
||||
v-model="form.wechat"
|
||||
placeholder="请输入微信号"
|
||||
maxlength="64"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.contact_type === 2" label="公司名称" prop="company_name">
|
||||
<el-input
|
||||
v-model="form.company_name"
|
||||
placeholder="请输入公司名称"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.contact_type === 2" label="部门" prop="dept_name">
|
||||
<el-input
|
||||
v-model="form.dept_name"
|
||||
placeholder="请输入部门名称"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.contact_type === 2" label="职位" prop="position_title">
|
||||
<el-input
|
||||
v-model="form.position_title"
|
||||
placeholder="请输入职位"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input
|
||||
v-model="form.address"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入地址"
|
||||
maxlength="512"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入备注"
|
||||
maxlength="512"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="99999" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">正常</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrganizationList, getPositionList } from '@/api/contactOA'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
formData: { type: Object, default: () => ({}) },
|
||||
isEdit: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const orgOptions = ref([])
|
||||
const orgTreeProps = { value: 'id', label: 'org_name', children: 'children' }
|
||||
|
||||
const emptyForm = () => ({
|
||||
contact_name: '',
|
||||
contact_type: 2,
|
||||
gender: 0,
|
||||
phone: '',
|
||||
work_phone: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
avatar: '',
|
||||
org_id: null,
|
||||
company_name: '',
|
||||
dept_name: '',
|
||||
position_id: null,
|
||||
position_title: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const rules = {
|
||||
contact_name: [
|
||||
{ required: true, message: '请输入联系人姓名', trigger: 'blur' },
|
||||
{ min: 1, max: 64, message: '长度在 1 到 64 个字符', trigger: 'blur' }
|
||||
],
|
||||
contact_type: [
|
||||
{ required: true, message: '请选择联系人类型', trigger: 'change' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const title = computed(() => props.isEdit ? '编辑联系人' : '新建联系人')
|
||||
|
||||
const buildOrgTree = (data) => {
|
||||
const tree = []
|
||||
const map = {}
|
||||
data.forEach(item => {
|
||||
map[item.id] = { ...item, children: [] }
|
||||
})
|
||||
data.forEach(item => {
|
||||
const node = map[item.id]
|
||||
if (item.parent_id === 0) {
|
||||
tree.push(node)
|
||||
} else {
|
||||
const parent = map[item.parent_id]
|
||||
if (parent) {
|
||||
parent.children.push(node)
|
||||
}
|
||||
}
|
||||
})
|
||||
return tree
|
||||
}
|
||||
|
||||
const loadOrgOptions = async () => {
|
||||
try {
|
||||
const res = await getOrganizationList()
|
||||
const data = res?.data || res || []
|
||||
orgOptions.value = buildOrgTree(data)
|
||||
} catch (error) {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 职位选项(内部员工,按所选部门联动)----
|
||||
const positionOptions = ref([])
|
||||
const positionLoading = ref(false)
|
||||
|
||||
// 收集部门自身及其所有上级组织ID(职位可挂在任意层级组织上,如集团级“总经理”)
|
||||
const collectOrgSelfAndAncestors = (deptId) => {
|
||||
const ids = new Set()
|
||||
if (!deptId) return ids
|
||||
const parentMap = {}
|
||||
const walk = (nodes) => {
|
||||
nodes.forEach((n) => {
|
||||
parentMap[n.id] = n.parent_id
|
||||
if (n.children?.length) walk(n.children)
|
||||
})
|
||||
}
|
||||
walk(orgOptions.value)
|
||||
let cur = Number(deptId)
|
||||
while (cur && !ids.has(cur)) {
|
||||
ids.add(cur)
|
||||
cur = parentMap[cur]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
const loadPositionOptions = async (deptId) => {
|
||||
positionLoading.value = true
|
||||
try {
|
||||
const res = await getPositionList()
|
||||
const data = res?.data || res || []
|
||||
const list = Array.isArray(data) ? data : []
|
||||
const enabled = list.filter((p) => p.status === 1)
|
||||
if (!deptId) {
|
||||
positionOptions.value = enabled
|
||||
} else {
|
||||
// 允许选择本部门及其上级组织的职位
|
||||
const allowed = collectOrgSelfAndAncestors(deptId)
|
||||
positionOptions.value = enabled.filter((p) => allowed.has(Number(p.department_id)))
|
||||
}
|
||||
} catch (error) {
|
||||
positionOptions.value = []
|
||||
} finally {
|
||||
positionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrgChange = (val) => {
|
||||
// 切换部门后重新加载职位并清空已选职位
|
||||
form.value.position_id = null
|
||||
form.value.position_title = ''
|
||||
loadPositionOptions(val)
|
||||
}
|
||||
|
||||
const handlePositionChange = (val) => {
|
||||
const pos = positionOptions.value.find(p => p.id === val)
|
||||
form.value.position_title = pos ? pos.position_name : ''
|
||||
}
|
||||
|
||||
const initForm = () => {
|
||||
if (props.isEdit && props.formData) {
|
||||
form.value = { ...props.formData }
|
||||
} else {
|
||||
form.value = emptyForm()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
const payload = { ...form.value }
|
||||
emit('save', payload)
|
||||
} catch {
|
||||
// validation failed
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
form.value = emptyForm()
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, async (val) => {
|
||||
if (val) {
|
||||
// 先加载下拉选项再回填表单,确保已有值能正确映射label
|
||||
await loadOrgOptions()
|
||||
initForm()
|
||||
if (form.value.contact_type === 1) {
|
||||
loadPositionOptions(form.value.org_id)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 类型切换时,内部员工需加载职位选项;外部联系人清空组织/职位关联
|
||||
watch(() => form.value.contact_type, (type) => {
|
||||
if (type === 1) {
|
||||
loadPositionOptions(form.value.org_id)
|
||||
} else {
|
||||
form.value.org_id = null
|
||||
form.value.position_id = null
|
||||
form.value.position_title = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,645 @@
|
||||
<template>
|
||||
<div class="contact-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>通讯录</h2>
|
||||
<p>企业内部通讯录,与组织架构自动联动</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="handleSyncAll" :loading="syncing">同步员工</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateContact">添加联系人</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-container">
|
||||
<!-- 左侧组织树 -->
|
||||
<div class="org-tree-panel">
|
||||
<div class="tree-header">
|
||||
<span>组织架构</span>
|
||||
<el-button :icon="Refresh" circle size="small" @click="loadOrgTree" />
|
||||
</div>
|
||||
<div class="tree-body">
|
||||
<el-tree
|
||||
ref="orgTreeRef"
|
||||
:data="orgTreeData"
|
||||
:props="orgTreeProps"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleOrgClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="org-tree-node">
|
||||
<span
|
||||
v-if="data.status !== undefined"
|
||||
class="node-status-dot"
|
||||
:class="data.status === 1 ? 'is-active' : 'is-inactive'"
|
||||
:title="data.status === 1 ? '启用' : '停用'"
|
||||
/>
|
||||
<span class="node-label">{{ data.org_name }}</span>
|
||||
<span class="node-count">{{ data.contact_count || 0 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="contact-main">
|
||||
<!-- 搜索和筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
clearable
|
||||
placeholder="搜索姓名、手机号、邮箱"
|
||||
:prefix-icon="Search"
|
||||
style="width: 260px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="filters.contact_type" clearable placeholder="全部" style="width: 120px">
|
||||
<el-option label="内部员工" :value="1" />
|
||||
<el-option label="外部联系人" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 联系人列表 -->
|
||||
<div class="contact-list" v-loading="loading">
|
||||
<el-table :data="contactList" stripe @row-click="openDetail" style="cursor: pointer">
|
||||
<el-table-column label="收藏" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon
|
||||
class="star-icon"
|
||||
:class="{ starred: row.is_starred === 1 }"
|
||||
@click.stop="toggleStar(row)"
|
||||
>
|
||||
<StarFilled v-if="row.is_starred === 1" />
|
||||
<Star v-else />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="姓名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="contact-name-cell">
|
||||
<el-avatar :size="32" :src="row.avatar || ''" class="contact-avatar">
|
||||
{{ row.contact_name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
<div class="contact-name-info">
|
||||
<span class="contact-name">{{ row.contact_name }}</span>
|
||||
<el-tag
|
||||
:type="row.contact_type === 1 ? 'primary' : 'info'"
|
||||
size="small"
|
||||
class="contact-type-tag"
|
||||
>
|
||||
{{ row.contact_type === 1 ? '内部' : '外部' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" width="140" />
|
||||
<el-table-column prop="org_name" label="部门" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.org_name || row.dept_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position_title" label="职位" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.position_title || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.email || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="wechat" label="微信" width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.wechat || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" title="编辑" @click.stop="openEditContact(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" title="详情" @click.stop="openDetail(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" title="删除" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadContactList"
|
||||
@current-change="loadContactList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑/新建对话框 -->
|
||||
<ContactEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:form-data="editFormData"
|
||||
:is-edit="isEditMode"
|
||||
@save="handleSaveContact"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ContactDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
:contact="detailContact"
|
||||
@edit="handleDetailEdit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Refresh,
|
||||
Search,
|
||||
Star,
|
||||
StarFilled,
|
||||
Edit,
|
||||
View,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import ContactEditDialog from './components/contactEditDialog.vue'
|
||||
import ContactDetailDialog from './components/contactDetailDialog.vue'
|
||||
import {
|
||||
getContactList,
|
||||
createContact,
|
||||
updateContact,
|
||||
deleteContact,
|
||||
starContact,
|
||||
syncAllContacts,
|
||||
getContactOrgTree
|
||||
} from '@/api/contactOA'
|
||||
|
||||
// ---- 组织树 ----
|
||||
const orgTreeRef = ref()
|
||||
const orgTreeData = ref([])
|
||||
const orgTreeProps = { children: 'children', label: 'org_name' }
|
||||
const selectedOrgId = ref(null)
|
||||
|
||||
const loadOrgTree = async () => {
|
||||
try {
|
||||
const res = await getContactOrgTree()
|
||||
const raw = res?.data ?? res
|
||||
orgTreeData.value = Array.isArray(raw) ? [...raw] : []
|
||||
// 添加"全部"节点
|
||||
orgTreeData.value.unshift({
|
||||
id: 0,
|
||||
org_name: '全部联系人',
|
||||
is_company: 0,
|
||||
contact_count: 0,
|
||||
children: []
|
||||
})
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载组织架构失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrgClick = (data) => {
|
||||
selectedOrgId.value = data.id || null
|
||||
pagination.page = 1
|
||||
loadContactList()
|
||||
}
|
||||
|
||||
// ---- 筛选 ----
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
contact_type: ''
|
||||
})
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.keyword = ''
|
||||
filters.contact_type = ''
|
||||
selectedOrgId.value = null
|
||||
orgTreeRef.value?.setCurrentKey(null)
|
||||
pagination.page = 1
|
||||
loadContactList()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadContactList()
|
||||
}
|
||||
|
||||
// ---- 列表 ----
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const contactList = ref([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const loadContactList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
if (filters.keyword) params.keyword = filters.keyword
|
||||
if (filters.contact_type) params.contact_type = filters.contact_type
|
||||
if (selectedOrgId.value) params.org_id = selectedOrgId.value
|
||||
|
||||
const res = await getContactList(params)
|
||||
const result = res?.data || res || {}
|
||||
contactList.value = result.list || []
|
||||
pagination.total = result.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载通讯录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 收藏 ----
|
||||
const toggleStar = async (row) => {
|
||||
const newStarred = row.is_starred === 1 ? 0 : 1
|
||||
try {
|
||||
await starContact(row.id, newStarred)
|
||||
row.is_starred = newStarred
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 同步 ----
|
||||
const handleSyncAll = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('将全量同步员工数据到通讯录,是否继续?', '同步确认', { type: 'info' })
|
||||
syncing.value = true
|
||||
const res = await syncAllContacts()
|
||||
const data = res?.data || res || {}
|
||||
ElMessage.success(`同步完成:新增 ${data.created || 0},更新 ${data.updated || 0}`)
|
||||
await loadContactList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error?.message || '同步失败')
|
||||
}
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 编辑/新建 ----
|
||||
const editDialogVisible = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const editFormData = reactive({
|
||||
id: null,
|
||||
contact_name: '',
|
||||
contact_type: 2,
|
||||
gender: 0,
|
||||
phone: '',
|
||||
work_phone: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
avatar: '',
|
||||
org_id: null,
|
||||
company_name: '',
|
||||
dept_name: '',
|
||||
position_id: null,
|
||||
position_title: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const openCreateContact = () => {
|
||||
isEditMode.value = false
|
||||
Object.assign(editFormData, {
|
||||
id: null,
|
||||
contact_name: '',
|
||||
contact_type: 2,
|
||||
gender: 0,
|
||||
phone: '',
|
||||
work_phone: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
avatar: '',
|
||||
org_id: selectedOrgId.value || null,
|
||||
company_name: '',
|
||||
dept_name: '',
|
||||
position_id: null,
|
||||
position_title: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditContact = (row) => {
|
||||
isEditMode.value = true
|
||||
Object.assign(editFormData, {
|
||||
id: row.id,
|
||||
contact_name: row.contact_name,
|
||||
contact_type: row.contact_type,
|
||||
gender: row.gender,
|
||||
phone: row.phone,
|
||||
work_phone: row.work_phone,
|
||||
email: row.email,
|
||||
wechat: row.wechat,
|
||||
avatar: row.avatar,
|
||||
org_id: row.org_id,
|
||||
company_name: row.company_name,
|
||||
dept_name: row.dept_name,
|
||||
position_id: row.position_id || null,
|
||||
position_title: row.position_title,
|
||||
address: row.address,
|
||||
remark: row.remark,
|
||||
sort: row.sort,
|
||||
status: row.status
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSaveContact = async (payload) => {
|
||||
try {
|
||||
if (isEditMode.value) {
|
||||
await updateContact(editFormData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createContact(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
editDialogVisible.value = false
|
||||
await loadContactList()
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 详情 ----
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailContact = ref(null)
|
||||
|
||||
const openDetail = (row) => {
|
||||
detailContact.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDetailEdit = (row) => {
|
||||
detailDialogVisible.value = false
|
||||
openEditContact(row)
|
||||
}
|
||||
|
||||
// ---- 删除 ----
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除联系人「${row.contact_name}」吗?`, '删除确认', { type: 'warning' })
|
||||
await deleteContact(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadContactList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOrgTree()
|
||||
loadContactList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.contact-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
/* 左侧组织树 */
|
||||
.org-tree-panel {
|
||||
width: 280px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.tree-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.org-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.node-status-dot {
|
||||
flex-shrink: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.is-active {
|
||||
background-color: #67c23a;
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.node-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
background: #f0f2f5;
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.contact-main {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.filter-bar .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 联系人名称单元格 */
|
||||
.contact-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
flex-shrink: 0;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.contact-name-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.contact-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.contact-type-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 收藏图标 */
|
||||
.star-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: #c0c4cc;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.star-icon:hover {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.star-icon.starred {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-wrap {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.contact-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.org-tree-panel {
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="employee-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>人员管理</h2>
|
||||
<p>维护企业员工信息,员工变动自动同步通讯录</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="loadEmployees">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateEmployee">新建员工</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="employee-toolbar">
|
||||
<el-tree-select
|
||||
v-model="filterDepartmentId"
|
||||
:data="orgOptions"
|
||||
:props="treeProps"
|
||||
placeholder="按部门筛选"
|
||||
clearable
|
||||
check-strictly
|
||||
style="width: 220px"
|
||||
/>
|
||||
<el-select
|
||||
v-model="filterStatus"
|
||||
placeholder="按状态筛选"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
<el-option label="离职" :value="2" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索姓名/账号/电话/邮箱"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<span class="toolbar-count">共 {{ filteredList.length }} 人</span>
|
||||
</div>
|
||||
|
||||
<div class="employee-table">
|
||||
<el-table :data="pagedList" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="姓名" width="110" />
|
||||
<el-table-column prop="account" label="账号" width="140" />
|
||||
<el-table-column label="性别" width="70">
|
||||
<template #default="{ row }">
|
||||
{{ genderText(row.gender) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="部门" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ orgNameMap[Number(row.department)] || row.department || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position" label="职位" width="130" />
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="160" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.account_status)" size="small">
|
||||
{{ statusText(row.account_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="editEmployee(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="deleteEmployee(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无员工数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination" v-if="filteredList.length > pageSize">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="filteredList.length"
|
||||
layout="prev, pager, next, total"
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 员工编辑对话框(复用组织架构页组件) -->
|
||||
<EmployeeEditDialog
|
||||
v-model="employeeEditVisible"
|
||||
:form-data="employeeFormData"
|
||||
:is-edit="employeeEditMode"
|
||||
@save="saveEmployee"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh, Search } from "@element-plus/icons-vue";
|
||||
import EmployeeEditDialog from "../organization/components/employeeEditDialog.vue";
|
||||
import {
|
||||
getOrganizationList,
|
||||
getEmployeeList,
|
||||
createEmployee,
|
||||
updateEmployee,
|
||||
deleteEmployee as apiDeleteEmployee
|
||||
} from "@/api/organization";
|
||||
|
||||
const loading = ref(false);
|
||||
const employeeList = ref([]);
|
||||
const orgOptions = ref([]);
|
||||
const orgNameMap = ref({});
|
||||
const filterDepartmentId = ref(null);
|
||||
const filterStatus = ref(null);
|
||||
const keyword = ref("");
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 20;
|
||||
const employeeEditVisible = ref(false);
|
||||
const employeeEditMode = ref(false);
|
||||
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'org_name',
|
||||
children: 'children'
|
||||
};
|
||||
|
||||
const emptyEmployeeForm = () => ({
|
||||
name: '',
|
||||
account: '',
|
||||
gender: 0,
|
||||
birthday: '',
|
||||
affiliate_unit: '',
|
||||
department: '',
|
||||
position: '',
|
||||
education: '',
|
||||
nation: '',
|
||||
phone: '',
|
||||
wechat: '',
|
||||
email: '',
|
||||
home_address: '',
|
||||
account_status: 1
|
||||
});
|
||||
|
||||
const employeeFormData = reactive(emptyEmployeeForm());
|
||||
|
||||
const filteredList = computed(() => {
|
||||
let list = employeeList.value;
|
||||
if (filterDepartmentId.value) {
|
||||
list = list.filter(item => Number(item.department) === filterDepartmentId.value);
|
||||
}
|
||||
if (filterStatus.value !== null && filterStatus.value !== undefined && filterStatus.value !== '') {
|
||||
list = list.filter(item => item.account_status === filterStatus.value);
|
||||
}
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
if (kw) {
|
||||
list = list.filter(item =>
|
||||
(item.name || '').toLowerCase().includes(kw) ||
|
||||
(item.account || '').toLowerCase().includes(kw) ||
|
||||
(item.phone || '').toLowerCase().includes(kw) ||
|
||||
(item.email || '').toLowerCase().includes(kw)
|
||||
);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
const pagedList = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize;
|
||||
return filteredList.value.slice(start, start + pageSize);
|
||||
});
|
||||
|
||||
const genderText = (gender) => {
|
||||
switch (gender) {
|
||||
case 1: return '男';
|
||||
case 2: return '女';
|
||||
default: return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const statusText = (status) => {
|
||||
switch (status) {
|
||||
case 1: return '正常';
|
||||
case 2: return '离职';
|
||||
default: return '禁用';
|
||||
}
|
||||
};
|
||||
|
||||
const statusTagType = (status) => {
|
||||
switch (status) {
|
||||
case 1: return 'success';
|
||||
case 2: return 'info';
|
||||
default: return 'danger';
|
||||
}
|
||||
};
|
||||
|
||||
const loadEmployees = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getEmployeeList();
|
||||
const data = res?.data || res || [];
|
||||
employeeList.value = Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载员工列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadOrgOptions = async () => {
|
||||
try {
|
||||
const res = await getOrganizationList();
|
||||
const data = res?.data || res || [];
|
||||
const map = {};
|
||||
const build = (items) => {
|
||||
items.forEach(item => {
|
||||
map[item.id] = item.org_name;
|
||||
if (item.children && item.children.length) build(item.children);
|
||||
});
|
||||
};
|
||||
build(data);
|
||||
orgNameMap.value = map;
|
||||
orgOptions.value = buildOrgTree(data);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载组织列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const buildOrgTree = (data) => {
|
||||
const tree = [];
|
||||
const map = {};
|
||||
|
||||
data.forEach(item => {
|
||||
map[item.id] = { ...item, children: [] };
|
||||
});
|
||||
|
||||
data.forEach(item => {
|
||||
const node = map[item.id];
|
||||
if (item.parent_id === 0) {
|
||||
tree.push(node);
|
||||
} else {
|
||||
const parent = map[item.parent_id];
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
const openCreateEmployee = () => {
|
||||
employeeEditMode.value = false;
|
||||
Object.assign(employeeFormData, emptyEmployeeForm());
|
||||
employeeEditVisible.value = true;
|
||||
};
|
||||
|
||||
const editEmployee = (data) => {
|
||||
employeeEditMode.value = true;
|
||||
Object.assign(employeeFormData, {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
account: data.account,
|
||||
gender: data.gender,
|
||||
birthday: data.birthday,
|
||||
affiliate_unit: data.affiliate_unit,
|
||||
department: data.department,
|
||||
position: data.position,
|
||||
education: data.education,
|
||||
nation: data.nation,
|
||||
phone: data.phone,
|
||||
wechat: data.wechat,
|
||||
email: data.email,
|
||||
home_address: data.home_address,
|
||||
account_status: data.account_status
|
||||
});
|
||||
employeeEditVisible.value = true;
|
||||
};
|
||||
|
||||
const deleteEmployee = async (data) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除员工「${data.name}」吗?删除后将无法恢复。`, '删除确认', {
|
||||
type: 'warning',
|
||||
});
|
||||
await apiDeleteEmployee(data.id);
|
||||
ElMessage.success('删除成功');
|
||||
await loadEmployees();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error?.message || '删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveEmployee = async (payload) => {
|
||||
try {
|
||||
if (employeeEditMode.value) {
|
||||
await updateEmployee(employeeFormData.id, payload);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await createEmployee(payload);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
employeeEditVisible.value = false;
|
||||
await loadEmployees();
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadEmployees();
|
||||
loadOrgOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.employee-page {
|
||||
padding: 20px;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.employee-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-count {
|
||||
margin-left: auto;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.employee-table {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -96,12 +96,26 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位" prop="position">
|
||||
<el-input
|
||||
<el-select
|
||||
v-model="form.position"
|
||||
placeholder="请输入职位"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
:loading="positionLoading"
|
||||
placeholder="请选择职位"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in positionOptions"
|
||||
:key="item.id"
|
||||
:label="item.position_name"
|
||||
:value="item.position_name"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">
|
||||
可下拉选择职位表中职位,也可直接输入新职位
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="学历" prop="education">
|
||||
@@ -187,7 +201,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrganizationList } from '@/api/organization'
|
||||
import { getOrganizationList, getPositionList } from '@/api/organization'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -221,6 +235,8 @@ const treeProps = {
|
||||
}
|
||||
|
||||
const orgOptions = ref([])
|
||||
const positionOptions = ref([])
|
||||
const positionLoading = ref(false)
|
||||
|
||||
const emptyForm = () => ({
|
||||
name: '',
|
||||
@@ -295,13 +311,68 @@ const title = computed(() => props.isEdit ? '编辑员工' : '新建员工')
|
||||
|
||||
const loadOrgOptions = async () => {
|
||||
try {
|
||||
const data = await getOrganizationList()
|
||||
const res = await getOrganizationList()
|
||||
const data = res?.data || res || []
|
||||
orgOptions.value = buildOrgTree(data)
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载组织列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 收集部门自身及其所有上级组织ID(职位可挂在任意层级组织上,如集团级“总经理”)
|
||||
const collectOrgSelfAndAncestors = (deptId) => {
|
||||
const ids = new Set()
|
||||
if (!deptId) return ids
|
||||
const parentMap = {}
|
||||
const walk = (nodes) => {
|
||||
nodes.forEach((n) => {
|
||||
parentMap[n.id] = n.parent_id
|
||||
if (n.children?.length) walk(n.children)
|
||||
})
|
||||
}
|
||||
walk(orgOptions.value)
|
||||
let cur = Number(deptId)
|
||||
while (cur && !ids.has(cur)) {
|
||||
ids.add(cur)
|
||||
cur = parentMap[cur]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// 根据所选部门加载职位选项
|
||||
const loadPositionOptions = async () => {
|
||||
const departmentId = parseInt(form.value.department, 10) || 0
|
||||
positionLoading.value = true
|
||||
try {
|
||||
const res = await getPositionList()
|
||||
const data = res?.data || res || []
|
||||
const list = Array.isArray(data) ? data : []
|
||||
const enabled = list.filter((p) => p.status === 1)
|
||||
if (!departmentId) {
|
||||
positionOptions.value = enabled
|
||||
} else {
|
||||
// 允许选择本部门及其上级组织的职位
|
||||
const allowed = collectOrgSelfAndAncestors(departmentId)
|
||||
positionOptions.value = enabled.filter((p) => allowed.has(Number(p.department_id)))
|
||||
}
|
||||
} catch (error) {
|
||||
positionOptions.value = []
|
||||
} finally {
|
||||
positionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 部门变化时联动刷新职位并清空已选职位
|
||||
watch(() => form.value.department, (newVal, oldVal) => {
|
||||
if (!props.modelValue) return
|
||||
if (newVal !== oldVal) {
|
||||
if (oldVal !== undefined && oldVal !== '') {
|
||||
form.value.position = ''
|
||||
}
|
||||
loadPositionOptions()
|
||||
}
|
||||
})
|
||||
|
||||
const buildOrgTree = (data) => {
|
||||
const tree = []
|
||||
const map = {}
|
||||
@@ -351,18 +422,23 @@ const handleClose = () => {
|
||||
|
||||
const initForm = () => {
|
||||
if (props.isEdit && props.formData) {
|
||||
// affiliate_unit/department 存组织ID(字符串),转数字以匹配树节点value;一次性赋值避免触发部门watch清空职位
|
||||
const toOrgId = (val) => (val === null || val === undefined || val === '' ? '' : Number(val))
|
||||
form.value = {
|
||||
...props.formData
|
||||
...props.formData,
|
||||
affiliate_unit: toOrgId(props.formData.affiliate_unit),
|
||||
department: toOrgId(props.formData.department)
|
||||
}
|
||||
} else {
|
||||
form.value = emptyForm()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
if (newVal) {
|
||||
// 先加载下拉选项再回填表单,确保已有值能正确映射label
|
||||
await Promise.all([loadOrgOptions(), loadPositionOptions()])
|
||||
initForm()
|
||||
loadOrgOptions()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
@@ -195,8 +195,9 @@ const title = computed(() => props.isEdit ? '编辑组织' : '新建组织')
|
||||
|
||||
const loadOrgOptions = async () => {
|
||||
try {
|
||||
const data = await getOrganizationList()
|
||||
orgOptions.value = buildOrgTree(data)
|
||||
const res = await getOrganizationList()
|
||||
const data = res?.data || res || []
|
||||
orgOptions.value = buildOrgTree(Array.isArray(data) ? data : [])
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载组织列表失败')
|
||||
}
|
||||
@@ -204,8 +205,9 @@ const loadOrgOptions = async () => {
|
||||
|
||||
const loadEmployeeOptions = async () => {
|
||||
try {
|
||||
const data = await getEmployeeList()
|
||||
employeeOptions.value = data.map(item => ({
|
||||
const res = await getEmployeeList()
|
||||
const data = res?.data || res || []
|
||||
employeeOptions.value = (Array.isArray(data) ? data : []).map(item => ({
|
||||
id: item.id,
|
||||
name: item.name
|
||||
}))
|
||||
@@ -272,15 +274,19 @@ const initForm = () => {
|
||||
leader_id: props.formData.leader_id || null
|
||||
}
|
||||
} else {
|
||||
form.value = emptyForm()
|
||||
// 新建:保留父组件预填值(如添加子组织时预设的上级组织)
|
||||
form.value = {
|
||||
...emptyForm(),
|
||||
...props.formData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
if (newVal) {
|
||||
// 先加载下拉选项再回填表单,确保已有值能正确映射label
|
||||
await Promise.all([loadOrgOptions(), loadEmployeeOptions()])
|
||||
initForm()
|
||||
loadOrgOptions()
|
||||
loadEmployeeOptions()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
@@ -37,38 +37,31 @@
|
||||
<template #default="{ data }">
|
||||
<div class="tree-node">
|
||||
<div class="node-content">
|
||||
<span class="node-icon">
|
||||
<el-icon v-if="data.is_company">
|
||||
<OfficeBuilding />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Folder />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span
|
||||
class="node-status-dot"
|
||||
:class="data.status === 1 ? 'is-active' : 'is-inactive'"
|
||||
:title="data.status === 1 ? '启用' : '停用'"
|
||||
/>
|
||||
<span class="node-label">{{ data.org_name }}</span>
|
||||
<span class="node-code">{{ data.org_code }}</span>
|
||||
<span class="node-status">
|
||||
<el-tag :type="data.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ data.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-actions">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
title="编辑"
|
||||
@click.stop="editOrg(data)"
|
||||
>
|
||||
编辑
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
title="添加子组织"
|
||||
@click.stop="addChildOrg(data)"
|
||||
>
|
||||
添加子组织
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
@@ -203,10 +196,9 @@ import {
|
||||
Plus,
|
||||
Refresh,
|
||||
Setting,
|
||||
OfficeBuilding,
|
||||
Folder,
|
||||
Expand,
|
||||
Fold
|
||||
Fold,
|
||||
Edit
|
||||
} from "@element-plus/icons-vue";
|
||||
import OrgEditDialog from "./components/orgEditDialog.vue";
|
||||
import EmployeeEditDialog from "./components/employeeEditDialog.vue";
|
||||
@@ -273,8 +265,9 @@ const employeeFormData = reactive(emptyEmployeeForm());
|
||||
|
||||
const loadOrgTree = async () => {
|
||||
try {
|
||||
const data = await getOrganizationList();
|
||||
orgTreeData.value = buildTreeData(data);
|
||||
const res = await getOrganizationList();
|
||||
const data = res?.data || res || [];
|
||||
orgTreeData.value = buildTreeData(Array.isArray(data) ? data : []);
|
||||
if (orgTreeData.value.length > 0 && !selectedOrg.value) {
|
||||
selectedOrg.value = orgTreeData.value[0];
|
||||
await loadEmployeeList(selectedOrg.value.id);
|
||||
@@ -332,8 +325,9 @@ const collapseAll = () => {
|
||||
|
||||
const loadEmployeeList = async (orgId) => {
|
||||
try {
|
||||
const data = await getEmployeeList({ org_id: orgId });
|
||||
employeeList.value = data || [];
|
||||
const res = await getEmployeeList({ org_id: orgId });
|
||||
const data = res?.data || res || [];
|
||||
employeeList.value = Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载员工列表失败');
|
||||
}
|
||||
@@ -571,22 +565,24 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.node-code {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.node-status-dot {
|
||||
flex-shrink: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
.node-status {
|
||||
margin-left: 8px;
|
||||
&.is-active {
|
||||
background-color: #67c23a;
|
||||
}
|
||||
|
||||
&.is-inactive {
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.node-actions {
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
label-position="right"
|
||||
>
|
||||
<el-form-item label="所属部门" prop="department_id">
|
||||
<el-tree-select
|
||||
v-model="form.department_id"
|
||||
:data="orgOptions"
|
||||
:props="treeProps"
|
||||
placeholder="请选择所属部门"
|
||||
clearable
|
||||
check-strictly
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位名称" prop="position_name">
|
||||
<el-input
|
||||
v-model="form.position_name"
|
||||
placeholder="请输入职位名称"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位编码" prop="position_code">
|
||||
<el-input
|
||||
v-model="form.position_code"
|
||||
placeholder="留空则自动生成"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位类型" prop="position_type">
|
||||
<el-radio-group v-model="form.position_type">
|
||||
<el-radio :label="0">普通职位</el-radio>
|
||||
<el-radio :label="1">管理职位</el-radio>
|
||||
<el-radio :label="2">技术职位</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="2">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrganizationList } from '@/api/organization'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
formData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'org_name',
|
||||
children: 'children'
|
||||
}
|
||||
|
||||
const orgOptions = ref([])
|
||||
|
||||
const emptyForm = () => ({
|
||||
department_id: null,
|
||||
position_name: '',
|
||||
position_code: '',
|
||||
position_type: 0,
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const rules = {
|
||||
position_name: [
|
||||
{ required: true, message: '请输入职位名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const title = computed(() => props.isEdit ? '编辑职位' : '新建职位')
|
||||
|
||||
const loadOrgOptions = async () => {
|
||||
try {
|
||||
const res = await getOrganizationList()
|
||||
const data = res?.data || res || []
|
||||
orgOptions.value = buildOrgTree(Array.isArray(data) ? data : [])
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载组织列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const buildOrgTree = (data) => {
|
||||
const tree = []
|
||||
const map = {}
|
||||
|
||||
data.forEach(item => {
|
||||
map[item.id] = { ...item, children: [] }
|
||||
})
|
||||
|
||||
data.forEach(item => {
|
||||
const node = map[item.id]
|
||||
if (item.parent_id === 0) {
|
||||
tree.push(node)
|
||||
} else {
|
||||
const parent = map[item.parent_id]
|
||||
if (parent) {
|
||||
parent.children.push(node)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
const formData = { ...form.value }
|
||||
emit('save', formData)
|
||||
} catch (error) {
|
||||
// 校验失败
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
form.value = emptyForm()
|
||||
}
|
||||
|
||||
const initForm = () => {
|
||||
if (props.isEdit && props.formData) {
|
||||
form.value = {
|
||||
...emptyForm(),
|
||||
...props.formData
|
||||
}
|
||||
} else {
|
||||
form.value = emptyForm()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
if (newVal) {
|
||||
// 先加载下拉选项再回填表单,确保已有值能正确映射label
|
||||
await loadOrgOptions()
|
||||
initForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="position-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>职位管理</h2>
|
||||
<p>维护各部门职位信息,供人员管理、通讯录选用</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="loadPositions">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreatePosition">新建职位</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="position-toolbar">
|
||||
<el-tree-select
|
||||
v-model="filterDepartmentId"
|
||||
:data="orgOptions"
|
||||
:props="treeProps"
|
||||
placeholder="按部门筛选"
|
||||
clearable
|
||||
check-strictly
|
||||
style="width: 240px"
|
||||
/>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索职位名称/编码"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="position-table">
|
||||
<el-table :data="filteredList" v-loading="loading" stripe>
|
||||
<el-table-column prop="position_name" label="职位名称" min-width="160" />
|
||||
<el-table-column prop="position_code" label="职位编码" width="160" />
|
||||
<el-table-column label="所属部门" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ orgNameMap[row.department_id] || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="职位类型" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ positionTypeText(row.position_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="editPosition(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="deletePosition(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无职位数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 职位编辑对话框 -->
|
||||
<PositionEditDialog
|
||||
v-model="positionEditVisible"
|
||||
:form-data="positionFormData"
|
||||
:is-edit="positionEditMode"
|
||||
@save="savePosition"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh, Search } from "@element-plus/icons-vue";
|
||||
import PositionEditDialog from "./components/positionEditDialog.vue";
|
||||
import {
|
||||
getOrganizationList,
|
||||
getPositionList,
|
||||
createPosition,
|
||||
updatePosition,
|
||||
deletePosition as apiDeletePosition
|
||||
} from "@/api/organization";
|
||||
|
||||
const loading = ref(false);
|
||||
const positionList = ref([]);
|
||||
const orgOptions = ref([]);
|
||||
const orgNameMap = ref({});
|
||||
const filterDepartmentId = ref(null);
|
||||
const keyword = ref("");
|
||||
const positionEditVisible = ref(false);
|
||||
const positionEditMode = ref(false);
|
||||
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'org_name',
|
||||
children: 'children'
|
||||
};
|
||||
|
||||
const emptyPositionForm = () => ({
|
||||
department_id: null,
|
||||
position_name: '',
|
||||
position_code: '',
|
||||
position_type: 0,
|
||||
sort: 0,
|
||||
status: 1
|
||||
});
|
||||
|
||||
const positionFormData = reactive(emptyPositionForm());
|
||||
|
||||
const filteredList = computed(() => {
|
||||
let list = positionList.value;
|
||||
if (filterDepartmentId.value) {
|
||||
list = list.filter(item => item.department_id === filterDepartmentId.value);
|
||||
}
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
if (kw) {
|
||||
list = list.filter(item =>
|
||||
(item.position_name || '').toLowerCase().includes(kw) ||
|
||||
(item.position_code || '').toLowerCase().includes(kw)
|
||||
);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
const positionTypeText = (type) => {
|
||||
switch (type) {
|
||||
case 1: return '管理职位';
|
||||
case 2: return '技术职位';
|
||||
default: return '普通职位';
|
||||
}
|
||||
};
|
||||
|
||||
const loadPositions = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getPositionList();
|
||||
const data = res?.data || res || [];
|
||||
positionList.value = Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载职位列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadOrgOptions = async () => {
|
||||
try {
|
||||
const res = await getOrganizationList();
|
||||
const data = res?.data || res || [];
|
||||
const map = {};
|
||||
const build = (items) => {
|
||||
items.forEach(item => {
|
||||
map[item.id] = item.org_name;
|
||||
if (item.children && item.children.length) build(item.children);
|
||||
});
|
||||
};
|
||||
build(data);
|
||||
orgNameMap.value = map;
|
||||
orgOptions.value = buildOrgTree(data);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载组织列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const buildOrgTree = (data) => {
|
||||
const tree = [];
|
||||
const map = {};
|
||||
|
||||
data.forEach(item => {
|
||||
map[item.id] = { ...item, children: [] };
|
||||
});
|
||||
|
||||
data.forEach(item => {
|
||||
const node = map[item.id];
|
||||
if (item.parent_id === 0) {
|
||||
tree.push(node);
|
||||
} else {
|
||||
const parent = map[item.parent_id];
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
const openCreatePosition = () => {
|
||||
positionEditMode.value = false;
|
||||
Object.assign(positionFormData, emptyPositionForm());
|
||||
if (filterDepartmentId.value) {
|
||||
positionFormData.department_id = filterDepartmentId.value;
|
||||
}
|
||||
positionEditVisible.value = true;
|
||||
};
|
||||
|
||||
const editPosition = (data) => {
|
||||
positionEditMode.value = true;
|
||||
Object.assign(positionFormData, {
|
||||
id: data.id,
|
||||
department_id: data.department_id,
|
||||
position_name: data.position_name,
|
||||
position_code: data.position_code,
|
||||
position_type: data.position_type,
|
||||
sort: data.sort,
|
||||
status: data.status
|
||||
});
|
||||
positionEditVisible.value = true;
|
||||
};
|
||||
|
||||
const deletePosition = async (data) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除职位「${data.position_name}」吗?删除后将无法恢复。`, '删除确认', {
|
||||
type: 'warning',
|
||||
});
|
||||
await apiDeletePosition(data.id);
|
||||
ElMessage.success('删除成功');
|
||||
await loadPositions();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error?.message || '删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const savePosition = async (payload) => {
|
||||
try {
|
||||
if (positionEditMode.value) {
|
||||
await updatePosition(positionFormData.id, payload);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await createPosition(payload);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
positionEditVisible.value = false;
|
||||
await loadPositions();
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadPositions();
|
||||
loadOrgOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.position-page {
|
||||
padding: 20px;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.position-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.position-table {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user