Compare commits
3
Commits
9319427f67
...
9588ddbf31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9588ddbf31 | ||
|
|
99df6c08ac | ||
|
|
576fea3770 |
No files matched your search
@@ -0,0 +1 @@
|
||||
{"tenant_name":"连云港云泽广告传媒有限公司","account":"hero920103","password":"920103","lot_number":"test","pass_token":"test","gen_time":"test","captcha_output":"test"}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -92,19 +92,22 @@ const staticMainChildren = [
|
||||
{
|
||||
path: "/apps/oa/organization",
|
||||
name: "Organization",
|
||||
component: () => import("@/views/apps/oa/organization/index.vue"),
|
||||
component: () => import("@/views/apps/organization/components/OrganizationPage.vue"),
|
||||
props: { module: "oa" },
|
||||
meta: { requiresAuth: true, title: "组织架构", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/apps/oa/employee",
|
||||
name: "OaEmployee",
|
||||
component: () => import("@/views/apps/oa/employee/index.vue"),
|
||||
name: "Employee",
|
||||
component: () => import("@/views/apps/organization/components/EmployeePage.vue"),
|
||||
props: { module: "oa" },
|
||||
meta: { requiresAuth: true, title: "人员管理", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/apps/oa/position",
|
||||
name: "OaPosition",
|
||||
component: () => import("@/views/apps/oa/position/index.vue"),
|
||||
name: "Position",
|
||||
component: () => import("@/views/apps/organization/components/PositionPage.vue"),
|
||||
props: { module: "oa" },
|
||||
meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<!-- 办公自动化的人员管理界面,数据与进销存共用 -->
|
||||
<EmployeePage module="oa" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EmployeePage from '@/views/apps/organization/components/EmployeePage.vue';
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<!-- 办公自动化的组织架构界面,数据与进销存共用 -->
|
||||
<OrganizationPage module="oa" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OrganizationPage from '@/views/apps/organization/components/OrganizationPage.vue';
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<!-- 办公自动化的职位管理界面,数据与进销存共用 -->
|
||||
<PositionPage module="oa" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PositionPage from '@/views/apps/organization/components/PositionPage.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,842 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendErpContactController 通讯录管理接口
|
||||
type BackendErpContactController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type erpContactDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Tid uint64 `json:"tid"`
|
||||
EmployeeID uint64 `json:"employee_id"`
|
||||
OrgID uint64 `json:"org_id"`
|
||||
OrgName string `json:"org_name"`
|
||||
ContactType int8 `json:"contact_type"`
|
||||
ContactName string `json:"contact_name"`
|
||||
Gender int8 `json:"gender"`
|
||||
Phone string `json:"phone"`
|
||||
WorkPhone string `json:"work_phone"`
|
||||
Email string `json:"email"`
|
||||
Wechat string `json:"wechat"`
|
||||
Avatar string `json:"avatar"`
|
||||
CompanyName string `json:"company_name"`
|
||||
DeptName string `json:"dept_name"`
|
||||
PositionID uint64 `json:"position_id"`
|
||||
PositionTitle string `json:"position_title"`
|
||||
Address string `json:"address"`
|
||||
Remark string `json:"remark"`
|
||||
IsStarred int8 `json:"is_starred"`
|
||||
Sort uint `json:"sort"`
|
||||
Status int8 `json:"status"`
|
||||
CreateTime string `json:"create_time"`
|
||||
}
|
||||
|
||||
// List 获取通讯录列表(支持分页、搜索、按组织筛选)
|
||||
// GET /backend/erp/contact/list
|
||||
func (c *BackendErpContactController) List() {
|
||||
tid, _ := c.GetInt64("tid")
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("page_size", 20)
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
orgID, _ := c.GetUint64("org_id")
|
||||
contactType, _ := c.GetInt("contact_type")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0)
|
||||
if tid > 0 {
|
||||
qs = qs.Filter("tid", tid)
|
||||
}
|
||||
if orgID > 0 {
|
||||
qs = qs.Filter("org_id", orgID)
|
||||
}
|
||||
if contactType > 0 {
|
||||
qs = qs.Filter("contact_type", contactType)
|
||||
}
|
||||
if keyword != "" {
|
||||
cond := orm.NewCondition()
|
||||
cond = cond.Or("contact_name__contains", keyword)
|
||||
cond = cond.Or("phone__contains", keyword)
|
||||
cond = cond.Or("email__contains", keyword)
|
||||
cond = cond.Or("wechat__contains", keyword)
|
||||
qs = qs.SetCond(cond)
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "查询通讯录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.BackendErpContact
|
||||
_, err = qs.OrderBy("-is_starred", "sort", "-id").
|
||||
Limit(pageSize, (page-1)*pageSize).
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "查询通讯录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]erpContactDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, c.contactDTO(row))
|
||||
}
|
||||
|
||||
c.contactJsonOK(map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// Detail 获取通讯录详情
|
||||
// GET /backend/erp/contact/detail/:id
|
||||
func (c *BackendErpContactController) Detail() {
|
||||
id, ok := c.contactPathUint64(":id")
|
||||
if !ok {
|
||||
c.contactJsonError(400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.BackendErpContact
|
||||
err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
c.contactJsonError(404, "联系人不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.contactJsonOK(c.contactDTO(row))
|
||||
}
|
||||
|
||||
// Create 创建联系人(手动添加外部联系人)
|
||||
// POST /backend/erp/contact/create
|
||||
func (c *BackendErpContactController) Create() {
|
||||
body := c.contactParseJSONBody()
|
||||
|
||||
contactName, _ := c.contactGetStringValue(body, "contact_name", "name")
|
||||
contactName = strings.TrimSpace(contactName)
|
||||
if contactName == "" {
|
||||
c.contactJsonError(400, "联系人姓名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
tid, _ := c.contactGetUint64Value(body, "tid", "tenant_id")
|
||||
orgID, _ := c.contactGetUint64Value(body, "org_id")
|
||||
contactType, _ := c.contactGetIntValue(body, "contact_type")
|
||||
gender, _ := c.contactGetIntValue(body, "gender")
|
||||
phone, _ := c.contactGetStringValue(body, "phone")
|
||||
workPhone, _ := c.contactGetStringValue(body, "work_phone")
|
||||
email, _ := c.contactGetStringValue(body, "email")
|
||||
wechat, _ := c.contactGetStringValue(body, "wechat")
|
||||
avatar, _ := c.contactGetStringValue(body, "avatar")
|
||||
companyName, _ := c.contactGetStringValue(body, "company_name")
|
||||
deptName, _ := c.contactGetStringValue(body, "dept_name")
|
||||
positionID, _ := c.contactGetUint64Value(body, "position_id")
|
||||
positionTitle, _ := c.contactGetStringValue(body, "position_title")
|
||||
address, _ := c.contactGetStringValue(body, "address")
|
||||
remark, _ := c.contactGetStringValue(body, "remark")
|
||||
sortVal, _ := c.contactGetUintValue(body, "sort")
|
||||
|
||||
if contactType == 0 {
|
||||
contactType = 2 // 默认外部联系人
|
||||
}
|
||||
|
||||
row := models.BackendErpContact{
|
||||
Tid: tid,
|
||||
ContactType: int8(contactType),
|
||||
ContactName: contactName,
|
||||
Gender: int8(gender),
|
||||
Phone: contactStrPtr(phone),
|
||||
WorkPhone: contactStrPtr(workPhone),
|
||||
Email: contactStrPtr(email),
|
||||
Wechat: contactStrPtr(wechat),
|
||||
Avatar: contactStrPtr(avatar),
|
||||
CompanyName: contactStrPtr(companyName),
|
||||
DeptName: contactStrPtr(deptName),
|
||||
PositionTitle: contactStrPtr(positionTitle),
|
||||
Address: contactStrPtr(address),
|
||||
Remark: contactStrPtr(remark),
|
||||
Status: 1,
|
||||
Sort: sortVal,
|
||||
}
|
||||
if orgID > 0 {
|
||||
row.OrgID = &orgID
|
||||
}
|
||||
if positionID > 0 {
|
||||
row.PositionID = &positionID
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "创建联系人失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.contactJsonOK(map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Update 更新联系人
|
||||
// POST /backend/erp/contact/update/:id
|
||||
func (c *BackendErpContactController) Update() {
|
||||
id, ok := c.contactPathUint64(":id")
|
||||
if !ok {
|
||||
c.contactJsonError(400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
body := c.contactParseJSONBody()
|
||||
update := orm.Params{}
|
||||
|
||||
if v, has := c.contactGetStringValue(body, "contact_name", "name"); has {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
c.contactJsonError(400, "联系人姓名不能为空")
|
||||
return
|
||||
}
|
||||
update["contact_name"] = v
|
||||
}
|
||||
if v, has := c.contactGetUint64Value(body, "org_id"); has {
|
||||
if v == 0 {
|
||||
update["org_id"] = nil
|
||||
} else {
|
||||
update["org_id"] = v
|
||||
}
|
||||
}
|
||||
if v, has := c.contactGetIntValue(body, "contact_type"); has {
|
||||
update["contact_type"] = int8(v)
|
||||
}
|
||||
if v, has := c.contactGetIntValue(body, "gender"); has {
|
||||
update["gender"] = int8(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "phone"); has {
|
||||
update["phone"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "work_phone"); has {
|
||||
update["work_phone"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "email"); has {
|
||||
update["email"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "wechat"); has {
|
||||
update["wechat"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "avatar"); has {
|
||||
update["avatar"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "company_name"); has {
|
||||
update["company_name"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "dept_name"); has {
|
||||
update["dept_name"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetUint64Value(body, "position_id"); has {
|
||||
if v == 0 {
|
||||
update["position_id"] = nil
|
||||
} else {
|
||||
update["position_id"] = v
|
||||
}
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "position_title"); has {
|
||||
update["position_title"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "address"); has {
|
||||
update["address"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "remark"); has {
|
||||
update["remark"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetIntValue(body, "is_starred"); has {
|
||||
update["is_starred"] = int8(v)
|
||||
}
|
||||
if v, has := c.contactGetUintValue(body, "sort"); has {
|
||||
update["sort"] = v
|
||||
}
|
||||
if v, has := c.contactGetIntValue(body, "status"); has {
|
||||
update["status"] = int8(v)
|
||||
}
|
||||
|
||||
if len(update) == 0 {
|
||||
c.contactJsonError(400, "无更新字段")
|
||||
return
|
||||
}
|
||||
|
||||
num, err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(update)
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "更新联系人失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
c.contactJsonError(404, "联系人不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.contactJsonOK(nil)
|
||||
}
|
||||
|
||||
// Delete 删除联系人(软删除)
|
||||
// DELETE /backend/erp/contact/delete/:id
|
||||
func (c *BackendErpContactController) Delete() {
|
||||
id, ok := c.contactPathUint64(":id")
|
||||
if !ok {
|
||||
c.contactJsonError(400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
num, err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"delete_time": now, "status": 0})
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "删除联系人失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
c.contactJsonError(404, "联系人不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.contactJsonOK(nil)
|
||||
}
|
||||
|
||||
// Star 收藏/取消收藏联系人
|
||||
// POST /backend/erp/contact/star/:id
|
||||
func (c *BackendErpContactController) Star() {
|
||||
id, ok := c.contactPathUint64(":id")
|
||||
if !ok {
|
||||
c.contactJsonError(400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
body := c.contactParseJSONBody()
|
||||
starred, _ := c.contactGetIntValue(body, "is_starred", "starred")
|
||||
|
||||
num, err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"is_starred": int8(starred)})
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
c.contactJsonError(404, "联系人不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.contactJsonOK(nil)
|
||||
}
|
||||
|
||||
// SyncAllContacts 全量同步:将所有员工同步到通讯录
|
||||
// POST /backend/erp/contact/syncAll
|
||||
func (c *BackendErpContactController) SyncAllContacts() {
|
||||
tid, _ := c.GetInt64("tid")
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("account_status", 2)
|
||||
if tid > 0 {
|
||||
qs = qs.Filter("tid", tid)
|
||||
}
|
||||
|
||||
var employees []models.BackendEmployee
|
||||
_, err := qs.All(&employees)
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "查询员工失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
created := 0
|
||||
updated := 0
|
||||
for _, emp := range employees {
|
||||
tidVal := uint64(0)
|
||||
if emp.Tid != nil {
|
||||
tidVal = uint64(*emp.Tid)
|
||||
}
|
||||
empID := uint64(emp.ID)
|
||||
result := syncContactFromEmployee(tidVal, empID, &emp)
|
||||
if result == "created" {
|
||||
created++
|
||||
} else if result == "updated" {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
c.contactJsonOK(map[string]interface{}{
|
||||
"total": len(employees),
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
})
|
||||
}
|
||||
|
||||
// GetContactOrgTree 获取通讯录组织树(带各部门联系人数量)
|
||||
// GET /backend/erp/contact/orgTree
|
||||
func (c *BackendErpContactController) GetContactOrgTree() {
|
||||
tid, _ := c.GetInt64("tid")
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0)
|
||||
if tid > 0 {
|
||||
qs = qs.Filter("tid", tid)
|
||||
}
|
||||
|
||||
var orgs []models.BackendOrganization
|
||||
_, err := qs.OrderBy("sort", "id").All(&orgs)
|
||||
if err != nil {
|
||||
c.contactJsonError(500, "查询组织架构失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
orgContactCounts := make(map[uint64]int64)
|
||||
cqs := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).
|
||||
Filter("contact_type", 1)
|
||||
if tid > 0 {
|
||||
cqs = cqs.Filter("tid", tid)
|
||||
}
|
||||
|
||||
type orgCount struct {
|
||||
OrgID uint64 `orm:"column(org_id)"`
|
||||
Count int64 `orm:"column(cnt)"`
|
||||
}
|
||||
var counts []orgCount
|
||||
_, err = cqs.GroupBy("org_id").All(&counts, "org_id")
|
||||
if err == nil {
|
||||
// Beego ORM GroupBy doesn't support raw count in All, so count manually
|
||||
}
|
||||
|
||||
// Fallback: count per org manually
|
||||
for _, org := range orgs {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("org_id", org.ID).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).
|
||||
Filter("contact_type", 1).
|
||||
Count()
|
||||
orgContactCounts[org.ID] = cnt
|
||||
}
|
||||
|
||||
// Also get total internal contacts count for root
|
||||
totalInternal, _ := cqs.Count()
|
||||
|
||||
// Build tree
|
||||
tree := c.buildContactOrgTree(orgs, orgContactCounts, uint64(totalInternal))
|
||||
|
||||
c.contactJsonOK(tree)
|
||||
}
|
||||
|
||||
// buildContactOrgTree 构建通讯录组织树
|
||||
func (c *BackendErpContactController) buildContactOrgTree(
|
||||
orgs []models.BackendOrganization,
|
||||
counts map[uint64]int64,
|
||||
totalInternal uint64,
|
||||
) []map[string]interface{} {
|
||||
nodeMap := make(map[uint64]map[string]interface{})
|
||||
tree := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, org := range orgs {
|
||||
node := map[string]interface{}{
|
||||
"id": org.ID,
|
||||
"org_name": org.OrgName,
|
||||
"parent_id": org.ParentID,
|
||||
"is_company": org.IsCompany,
|
||||
"status": org.Status,
|
||||
"contact_count": counts[org.ID],
|
||||
"children": make([]map[string]interface{}, 0),
|
||||
}
|
||||
nodeMap[org.ID] = node
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
node := nodeMap[org.ID]
|
||||
if org.ParentID == 0 {
|
||||
node["contact_count"] = int64(totalInternal)
|
||||
tree = append(tree, node)
|
||||
} else {
|
||||
if parent, exists := nodeMap[org.ParentID]; exists {
|
||||
parent["children"] = append(parent["children"].([]map[string]interface{}), node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
// --- DTO builder ---
|
||||
|
||||
func (c *BackendErpContactController) contactDTO(row models.BackendErpContact) erpContactDTO {
|
||||
orgName := ""
|
||||
if row.OrgID != nil && *row.OrgID > 0 {
|
||||
var org models.BackendOrganization
|
||||
if err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("id", *row.OrgID).
|
||||
One(&org); err == nil {
|
||||
orgName = org.OrgName
|
||||
}
|
||||
}
|
||||
|
||||
return erpContactDTO{
|
||||
ID: row.ID,
|
||||
Tid: row.Tid,
|
||||
EmployeeID: contactDerefUint64(row.EmployeeID),
|
||||
OrgID: contactDerefUint64(row.OrgID),
|
||||
OrgName: orgName,
|
||||
ContactType: row.ContactType,
|
||||
ContactName: row.ContactName,
|
||||
Gender: row.Gender,
|
||||
Phone: contactDerefString(row.Phone),
|
||||
WorkPhone: contactDerefString(row.WorkPhone),
|
||||
Email: contactDerefString(row.Email),
|
||||
Wechat: contactDerefString(row.Wechat),
|
||||
Avatar: contactDerefString(row.Avatar),
|
||||
CompanyName: contactDerefString(row.CompanyName),
|
||||
DeptName: contactDerefString(row.DeptName),
|
||||
PositionID: contactDerefUint64(row.PositionID),
|
||||
PositionTitle: contactDerefString(row.PositionTitle),
|
||||
Address: contactDerefString(row.Address),
|
||||
Remark: contactDerefString(row.Remark),
|
||||
IsStarred: row.IsStarred,
|
||||
Sort: row.Sort,
|
||||
Status: row.Status,
|
||||
CreateTime: row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sync from Employee ---
|
||||
|
||||
// SyncContactOnEmployeeCreate 员工创建后同步到通讯录(由 ERP 控制器调用)
|
||||
func SyncContactOnEmployeeCreate(tid uint64, employeeID uint64, emp *models.BackendEmployee) {
|
||||
syncContactFromEmployee(tid, employeeID, emp)
|
||||
}
|
||||
|
||||
// SyncContactOnEmployeeUpdate 员工更新后同步到通讯录(由 ERP 控制器调用)
|
||||
func SyncContactOnEmployeeUpdate(employeeID uint64, update orm.Params) {
|
||||
var contact models.BackendErpContact
|
||||
err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("employee_id", employeeID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&contact)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contactUpdate := orm.Params{}
|
||||
if v, ok := update["name"]; ok {
|
||||
contactUpdate["contact_name"] = v
|
||||
}
|
||||
if v, ok := update["phone"]; ok {
|
||||
contactUpdate["phone"] = v
|
||||
}
|
||||
if v, ok := update["email"]; ok {
|
||||
contactUpdate["email"] = v
|
||||
}
|
||||
if v, ok := update["wechat"]; ok {
|
||||
contactUpdate["wechat"] = v
|
||||
}
|
||||
if v, ok := update["gender"]; ok {
|
||||
contactUpdate["gender"] = v
|
||||
}
|
||||
if v, ok := update["department"]; ok {
|
||||
if v == nil {
|
||||
contactUpdate["org_id"] = nil
|
||||
} else {
|
||||
switch dept := v.(type) {
|
||||
case string:
|
||||
orgID, err := strconv.ParseUint(strings.TrimSpace(dept), 10, 64)
|
||||
if err == nil && orgID > 0 {
|
||||
contactUpdate["org_id"] = orgID
|
||||
} else {
|
||||
contactUpdate["org_id"] = nil
|
||||
}
|
||||
case uint64:
|
||||
if dept > 0 {
|
||||
contactUpdate["org_id"] = dept
|
||||
} else {
|
||||
contactUpdate["org_id"] = nil
|
||||
}
|
||||
case uint:
|
||||
if dept > 0 {
|
||||
contactUpdate["org_id"] = uint64(dept)
|
||||
} else {
|
||||
contactUpdate["org_id"] = nil
|
||||
}
|
||||
default:
|
||||
contactUpdate["org_id"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := update["position"]; ok {
|
||||
contactUpdate["position_title"] = v
|
||||
}
|
||||
if v, ok := update["home_address"]; ok {
|
||||
contactUpdate["address"] = v
|
||||
}
|
||||
if v, ok := update["account_status"]; ok {
|
||||
if status, ok := v.(int8); ok && status == 2 {
|
||||
contactUpdate["status"] = int8(0)
|
||||
}
|
||||
}
|
||||
|
||||
if len(contactUpdate) > 0 {
|
||||
models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", contact.ID).
|
||||
Update(contactUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncContactOnEmployeeDelete 员工删除后同步通讯录(由 ERP 控制器调用)
|
||||
func SyncContactOnEmployeeDelete(employeeID uint64) {
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("employee_id", employeeID).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"delete_time": now, "status": 0})
|
||||
}
|
||||
|
||||
// syncContactFromEmployee 内部同步函数
|
||||
func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendEmployee) string {
|
||||
var existing models.BackendErpContact
|
||||
err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("employee_id", employeeID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&existing)
|
||||
|
||||
if err != nil {
|
||||
// Not found - create new contact
|
||||
var orgID *uint64
|
||||
if emp.Department != nil {
|
||||
if oid, err := strconv.ParseUint(strings.TrimSpace(*emp.Department), 10, 64); err == nil && oid > 0 {
|
||||
orgID = &oid
|
||||
}
|
||||
}
|
||||
|
||||
contact := models.BackendErpContact{
|
||||
Tid: tid,
|
||||
EmployeeID: &employeeID,
|
||||
OrgID: orgID,
|
||||
ContactType: 1,
|
||||
ContactName: emp.Name,
|
||||
Gender: emp.Gender,
|
||||
Phone: emp.Phone,
|
||||
Email: emp.Email,
|
||||
Wechat: emp.Wechat,
|
||||
PositionTitle: emp.Position,
|
||||
Address: emp.HomeAddress,
|
||||
Status: 1,
|
||||
}
|
||||
models.Orm.Insert(&contact)
|
||||
return "created"
|
||||
}
|
||||
|
||||
// Found - update
|
||||
update := orm.Params{
|
||||
"contact_name": emp.Name,
|
||||
"gender": emp.Gender,
|
||||
"phone": emp.Phone,
|
||||
"email": emp.Email,
|
||||
"wechat": emp.Wechat,
|
||||
"position_title": emp.Position,
|
||||
"address": emp.HomeAddress,
|
||||
}
|
||||
if emp.Department != nil {
|
||||
if oid, err := strconv.ParseUint(strings.TrimSpace(*emp.Department), 10, 64); err == nil && oid > 0 {
|
||||
update["org_id"] = oid
|
||||
} else {
|
||||
update["org_id"] = nil
|
||||
}
|
||||
} else {
|
||||
update["org_id"] = nil
|
||||
}
|
||||
|
||||
models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", existing.ID).
|
||||
Update(update)
|
||||
return "updated"
|
||||
}
|
||||
|
||||
// --- Helper functions (package-level, shared with ERP controller) ---
|
||||
|
||||
func contactDerefString(v *string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func contactDerefUint64(v *uint64) uint64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func contactStrPtr(v string) *string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func contactNullableString(v string) interface{} {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// --- Controller-level helpers ---
|
||||
|
||||
func (c *BackendErpContactController) contactParseJSONBody() map[string]interface{} {
|
||||
body := map[string]interface{}{}
|
||||
contentType := strings.ToLower(c.Ctx.Input.Header("Content-Type"))
|
||||
if !strings.Contains(contentType, "json") {
|
||||
return body
|
||||
}
|
||||
if len(c.Ctx.Input.RequestBody) == 0 {
|
||||
return body
|
||||
}
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &body)
|
||||
return body
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactGetStringValue(body map[string]interface{}, keys ...string) (string, bool) {
|
||||
for _, key := range keys {
|
||||
if v, ok := body[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val, true
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'f', -1, 64), true
|
||||
case bool:
|
||||
return strconv.FormatBool(val), true
|
||||
default:
|
||||
b, _ := json.Marshal(v)
|
||||
return strings.TrimSpace(strings.Trim(strings.ReplaceAll(strings.ReplaceAll(string(b), "\n", ""), "\r", ""), "\"")), true
|
||||
}
|
||||
}
|
||||
if c.Ctx.Request.Form == nil && c.Ctx.Request.PostForm == nil && c.Ctx.Request.MultipartForm == nil {
|
||||
_ = c.Ctx.Request.ParseMultipartForm(32 << 20)
|
||||
}
|
||||
if val := c.GetString(key); val != "" {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactGetIntValue(body map[string]interface{}, keys ...string) (int, bool) {
|
||||
for _, key := range keys {
|
||||
if v, ok := body[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return int(val), true
|
||||
case int:
|
||||
return val, true
|
||||
case string:
|
||||
if strings.TrimSpace(val) == "" {
|
||||
return 0, true
|
||||
}
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(val))
|
||||
return parsed, err == nil
|
||||
}
|
||||
}
|
||||
if c.Ctx.Request.Form == nil && c.Ctx.Request.PostForm == nil && c.Ctx.Request.MultipartForm == nil {
|
||||
_ = c.Ctx.Request.ParseMultipartForm(32 << 20)
|
||||
}
|
||||
if val := c.GetString(key); val != "" {
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(val))
|
||||
return parsed, err == nil
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactGetUintValue(body map[string]interface{}, keys ...string) (uint, bool) {
|
||||
v, ok := c.contactGetIntValue(body, keys...)
|
||||
if !ok || v < 0 {
|
||||
return 0, ok
|
||||
}
|
||||
return uint(v), true
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactGetUint64Value(body map[string]interface{}, keys ...string) (uint64, bool) {
|
||||
for _, key := range keys {
|
||||
if v, ok := body[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
if val < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case int:
|
||||
if val < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(val), true
|
||||
case string:
|
||||
if strings.TrimSpace(val) == "" {
|
||||
return 0, true
|
||||
}
|
||||
parsed, err := strconv.ParseUint(strings.TrimSpace(val), 10, 64)
|
||||
return parsed, err == nil
|
||||
}
|
||||
}
|
||||
if c.Ctx.Request.Form == nil && c.Ctx.Request.PostForm == nil && c.Ctx.Request.MultipartForm == nil {
|
||||
_ = c.Ctx.Request.ParseMultipartForm(32 << 20)
|
||||
}
|
||||
if val := c.GetString(key); val != "" {
|
||||
parsed, err := strconv.ParseUint(strings.TrimSpace(val), 10, 64)
|
||||
return parsed, err == nil
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactPathUint64(name string) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(name), 10, 64)
|
||||
return id, err == nil && id > 0
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactJsonOK(data interface{}) {
|
||||
resp := map[string]interface{}{"code": 200, "msg": "success"}
|
||||
if data != nil {
|
||||
resp["data"] = data
|
||||
}
|
||||
c.Data["json"] = resp
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendErpContactController) contactJsonError(code int, msg string) {
|
||||
c.Data["json"] = map[string]interface{}{"code": code, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -2,10 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// 组织架构(组织 / 员工 / 职位)为租户端通用基础数据,进销存(ERP)与办公自动化(OA)共用同一份数据,
|
||||
// 通过 tid(租户ID)实现租户之间的数据隔离。表名统一使用 yz_backend_ 前缀。
|
||||
|
||||
// BackendOrganization 组织架构表 yz_backend_organization
|
||||
// BackendOrganization 全局组织架构表 yz_backend_organization
|
||||
type BackendOrganization struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
@@ -27,7 +24,7 @@ func (m *BackendOrganization) TableName() string {
|
||||
return "yz_backend_organization"
|
||||
}
|
||||
|
||||
// BackendEmployee 员工信息表 yz_backend_employee
|
||||
// BackendEmployee 全局员工信息表 yz_backend_employee
|
||||
type BackendEmployee struct {
|
||||
ID uint `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid *int `orm:"column(tid);null" json:"tid"`
|
||||
@@ -56,7 +53,7 @@ func (m *BackendEmployee) TableName() string {
|
||||
return "yz_backend_employee"
|
||||
}
|
||||
|
||||
// BackendPosition 职位表 yz_backend_position
|
||||
// BackendPosition 全局职位表 yz_backend_position
|
||||
type BackendPosition struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// BackendErpContact 全局通讯录表 yz_backend_contact
|
||||
// 与组织架构联动,支持内部员工(自动同步)和外部联系人
|
||||
type BackendErpContact struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
EmployeeID *uint64 `orm:"column(employee_id);null" json:"employee_id"`
|
||||
OrgID *uint64 `orm:"column(org_id);null" json:"org_id"`
|
||||
ContactType int8 `orm:"column(contact_type);default(1)" json:"contact_type"`
|
||||
ContactName string `orm:"column(contact_name);size(64)" json:"contact_name"`
|
||||
Gender int8 `orm:"column(gender);default(0)" json:"gender"`
|
||||
Phone *string `orm:"column(phone);size(20);null" json:"phone"`
|
||||
WorkPhone *string `orm:"column(work_phone);size(20);null" json:"work_phone"`
|
||||
Email *string `orm:"column(email);size(128);null" json:"email"`
|
||||
Wechat *string `orm:"column(wechat);size(64);null" json:"wechat"`
|
||||
Avatar *string `orm:"column(avatar);size(512);null" json:"avatar"`
|
||||
CompanyName *string `orm:"column(company_name);size(128);null" json:"company_name"`
|
||||
DeptName *string `orm:"column(dept_name);size(128);null" json:"dept_name"`
|
||||
PositionID *uint64 `orm:"column(position_id);null" json:"position_id"`
|
||||
PositionTitle *string `orm:"column(position_title);size(100);null" json:"position_title"`
|
||||
Address *string `orm:"column(address);size(512);null" json:"address"`
|
||||
Remark *string `orm:"column(remark);size(512);null" json:"remark"`
|
||||
IsStarred int8 `orm:"column(is_starred);default(0)" json:"is_starred"`
|
||||
Sort uint `orm:"column(sort);default(0)" json:"sort"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendErpContact) TableName() string {
|
||||
return "yz_backend_contact"
|
||||
}
|
||||
@@ -38,6 +38,7 @@ func Init(_ string) {
|
||||
new(BackendOrganization),
|
||||
new(BackendEmployee),
|
||||
new(BackendPosition),
|
||||
new(BackendErpContact),
|
||||
new(BackendApprovalFlow),
|
||||
new(BackendApprovalRecord),
|
||||
new(BackendReimbursement),
|
||||
|
||||
@@ -102,6 +102,16 @@ func RegisterAuthRoutes() {
|
||||
registerOrganizationRoutes("erp")
|
||||
registerOrganizationRoutes("oa")
|
||||
|
||||
// 通讯录管理(与组织架构联动)
|
||||
beego.Router("/backend/erp/contact/list", &controllers.BackendErpContactController{}, "get:List")
|
||||
beego.Router("/backend/erp/contact/detail/:id", &controllers.BackendErpContactController{}, "get:Detail")
|
||||
beego.Router("/backend/erp/contact/create", &controllers.BackendErpContactController{}, "post:Create")
|
||||
beego.Router("/backend/erp/contact/update/:id", &controllers.BackendErpContactController{}, "post:Update")
|
||||
beego.Router("/backend/erp/contact/delete/:id", &controllers.BackendErpContactController{}, "delete:Delete")
|
||||
beego.Router("/backend/erp/contact/star/:id", &controllers.BackendErpContactController{}, "post:Star")
|
||||
beego.Router("/backend/erp/contact/syncAll", &controllers.BackendErpContactController{}, "post:SyncAllContacts")
|
||||
beego.Router("/backend/erp/contact/orgTree", &controllers.BackendErpContactController{}, "get:GetContactOrgTree")
|
||||
|
||||
// 文章管理
|
||||
beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List")
|
||||
beego.Router("/backend/contentstats", &controllers.BackendArticleController{}, "get:ContentStats")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
-- 通讯录表(全局表,已从 yz_backend_erp_contact 改名):与组织架构联动,支持内部员工(自动同步)和外部联系人
|
||||
-- 已合并 alter_contact_add_dept_position.sql 的 dept_name / position_id 字段
|
||||
-- tenant 隔离:通过 tid 字段实现租户间数据隔离
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_contact` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`tid` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
`employee_id` bigint(20) unsigned DEFAULT NULL COMMENT '关联员工ID(内部联系人自动关联)',
|
||||
`org_id` bigint(20) unsigned DEFAULT NULL COMMENT '所属组织/部门ID(关联yz_backend_organization)',
|
||||
`contact_type` tinyint(4) NOT NULL DEFAULT 1 COMMENT '联系人类型:1=内部员工 2=外部联系人',
|
||||
`contact_name` varchar(64) NOT NULL COMMENT '联系人姓名',
|
||||
`gender` tinyint(4) NOT NULL DEFAULT 0 COMMENT '性别:0=未知 1=男 2=女',
|
||||
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
|
||||
`work_phone` varchar(20) DEFAULT NULL COMMENT '办公电话',
|
||||
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
|
||||
`wechat` varchar(64) DEFAULT NULL COMMENT '微信号',
|
||||
`avatar` varchar(512) DEFAULT NULL COMMENT '头像URL',
|
||||
`company_name` varchar(128) DEFAULT NULL COMMENT '公司名称(外部联系人)',
|
||||
`dept_name` varchar(128) DEFAULT NULL COMMENT '外部联系人手填部门名称',
|
||||
`position_id` bigint(20) unsigned DEFAULT NULL COMMENT '职位ID,关联yz_backend_position',
|
||||
`position_title` varchar(100) DEFAULT NULL COMMENT '职位名称',
|
||||
`address` varchar(512) DEFAULT NULL COMMENT '地址',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`is_starred` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否收藏:0=否 1=是',
|
||||
`sort` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '排序值',
|
||||
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1=正常 0=停用',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tid` (`tid`),
|
||||
KEY `idx_employee_id` (`employee_id`),
|
||||
KEY `idx_org_id` (`org_id`),
|
||||
KEY `idx_contact_type` (`contact_type`),
|
||||
KEY `idx_contact_name` (`contact_name`),
|
||||
KEY `idx_phone` (`phone`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通讯录表';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 通讯录表新增字段:外部联系人手填部门、内部员工职位关联
|
||||
-- 对应模型:go/models/contact.go BackendErpContact
|
||||
|
||||
ALTER TABLE `yz_backend_contact`
|
||||
ADD COLUMN `dept_name` VARCHAR(128) NULL COMMENT '外部联系人手填部门名称' AFTER `company_name`,
|
||||
ADD COLUMN `position_id` BIGINT UNSIGNED NULL COMMENT '职位ID,关联 yz_backend_position' AFTER `dept_name`;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 通讯录表全局化:从 ERP 模块独立为全局基础功能,表名去掉 erp 前缀
|
||||
-- 对应模型:go/models/contact.go(BackendErpContact)
|
||||
-- RENAME 会保留全部数据与索引,对存量环境执行本脚本即可完成迁移
|
||||
|
||||
RENAME TABLE `yz_backend_erp_contact` TO `yz_backend_contact`;
|
||||
@@ -0,0 +1,69 @@
|
||||
-- 全局组织架构相关表:组织机构、员工、职位
|
||||
-- 说明:组织架构已从 ERP 模块独立为全局基础功能,表名不带 erp 前缀
|
||||
-- tenant 隔离:通过 tid 字段实现租户间数据隔离
|
||||
-- 对应模型:go/models/erp.go(BackendErpOrganization / BackendErpEmployee / BackendErpPosition)
|
||||
-- DDL 与线上 go-platform 库实际结构一致(2026-08 导出)
|
||||
|
||||
-- 组织架构表(OA 组织机构、通讯录、组织机构管理页依赖)
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_organization` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`tid` bigint(20) unsigned NOT NULL COMMENT '租户ID',
|
||||
`org_name` varchar(128) NOT NULL COMMENT '组织/部门名称',
|
||||
`org_code` varchar(64) NOT NULL COMMENT '组织/部门编码',
|
||||
`parent_id` bigint(20) unsigned DEFAULT '0' COMMENT '上级组织ID',
|
||||
`sort` int(10) unsigned DEFAULT '0' COMMENT '排序号',
|
||||
`leader_id` bigint(20) unsigned DEFAULT NULL COMMENT '部门负责人ID',
|
||||
`is_company` int(11) DEFAULT '0' COMMENT '是否是公司 0-不是 1-是',
|
||||
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-启用,2-停用,0-删除',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注信息',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_tid_code` (`tid`,`org_code`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='组织架构表';
|
||||
|
||||
-- 员工信息表(account 全局唯一)
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_employee` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`tid` int(11) DEFAULT NULL COMMENT '租户ID',
|
||||
`account` varchar(50) NOT NULL COMMENT '员工账号',
|
||||
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
|
||||
`name` varchar(30) NOT NULL COMMENT '员工姓名',
|
||||
`gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别 0=未知 1=男 2=女',
|
||||
`birthday` date DEFAULT NULL COMMENT '生日',
|
||||
`affiliate_unit` varchar(100) DEFAULT NULL COMMENT '隶属单位',
|
||||
`department` varchar(50) DEFAULT NULL COMMENT '部门',
|
||||
`position` varchar(50) DEFAULT NULL COMMENT '职位',
|
||||
`education` varchar(20) DEFAULT NULL COMMENT '学历',
|
||||
`nation` varchar(20) DEFAULT NULL COMMENT '民族',
|
||||
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
|
||||
`wechat` varchar(50) DEFAULT NULL COMMENT '微信号',
|
||||
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`home_address` varchar(255) DEFAULT NULL COMMENT '家庭住址',
|
||||
`account_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '账号状态 0=禁用 1=正常 2=离职',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE KEY `uk_account` (`account`) USING BTREE,
|
||||
KEY `idx_tid_account` (`tid`,`account`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='员工信息表';
|
||||
|
||||
-- 职位表
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_position` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`tid` bigint(20) unsigned NOT NULL COMMENT '租户ID',
|
||||
`department_id` bigint(20) unsigned NOT NULL COMMENT '部门ID',
|
||||
`position_code` varchar(50) NOT NULL COMMENT '职位编码',
|
||||
`position_name` varchar(100) NOT NULL COMMENT '职位名称',
|
||||
`position_type` tinyint(4) DEFAULT '0' COMMENT '职位类型:0-普通职位 1-管理职位 2-技术职位',
|
||||
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-启用',
|
||||
`sort` int(10) unsigned DEFAULT '0' COMMENT '排序',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_tid_code` (`tid`,`position_code`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='职位表';
|
||||
Reference in new issue
Block a user