优化backend端相关功能
This commit is contained in:
Vendored
+1
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
|
||||
+4
-1
@@ -1,8 +1,11 @@
|
||||
<script setup>
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -89,3 +89,11 @@ export function getPositionList(params) {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取职位详情(编辑联系人时,已选职位若不在当前部门列表内用于回显)
|
||||
export function getPositionDetail(id) {
|
||||
return request({
|
||||
url: `/backend/erp/getPositionDetail/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,6 +80,14 @@ export function getOpenVerify() {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前登录用户信息(含租户名称)
|
||||
export function getCurrentUser() {
|
||||
return request({
|
||||
url: '/backend/getCurrentUser',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
export function register(data) {
|
||||
return request({
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<span v-if="companyName" class="company-name" title="当前登录的企业">
|
||||
{{ companyName }}
|
||||
</span>
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<span class="el-dropdown-link" style="cursor: pointer;">
|
||||
<img :src="getImageUrl('user')" class="user" />
|
||||
@@ -289,6 +292,11 @@ const displayName = computed(() => {
|
||||
return user.account || '';
|
||||
});
|
||||
|
||||
// 当前登录的企业(租户)名称
|
||||
const companyName = computed(() => {
|
||||
return authStore.user?.tenant_name || '';
|
||||
});
|
||||
|
||||
const handleCollapse = () => {
|
||||
store.state.isCollapse = !store.state.isCollapse;
|
||||
};
|
||||
@@ -544,6 +552,20 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.company-name {
|
||||
max-width: 200px;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { getCurrentUser } from '@/api/login'
|
||||
|
||||
// 用户信息类型
|
||||
const defaultUser = {
|
||||
@@ -8,6 +9,7 @@ const defaultUser = {
|
||||
name: '',
|
||||
group_id: '',
|
||||
type: 'backend',
|
||||
tenant_name: '',
|
||||
avatar: ''
|
||||
}
|
||||
|
||||
@@ -29,8 +31,27 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 从后端拉取最新用户信息(含租户名称),用于已有会话补充 tenant_name
|
||||
async function fetchCurrentUser() {
|
||||
if (!token.value) return
|
||||
try {
|
||||
const res = await getCurrentUser()
|
||||
if (res && res.code === 200 && res.data) {
|
||||
Object.assign(user, res.data)
|
||||
localStorage.setItem('userInfo', JSON.stringify({ ...user }))
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败,不影响登录态
|
||||
console.error('Failed to fetch current user info:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载用户信息
|
||||
loadUserFromCache()
|
||||
// 若已登录但缺少租户名称,异步从后端补齐
|
||||
if (token.value) {
|
||||
fetchCurrentUser()
|
||||
}
|
||||
|
||||
// 保存登录信息(token 和用户信息)
|
||||
function setLoginInfo(loginData) {
|
||||
@@ -42,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
name: userInfo.name || '',
|
||||
group_id: userInfo.group_id || '',
|
||||
type: 'backend',
|
||||
tenant_name: userInfo.tenant_name || '',
|
||||
tid: userInfo.tid || '',
|
||||
avatar: userInfo.avatar || ''
|
||||
}
|
||||
@@ -101,7 +123,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
setToken,
|
||||
clearToken,
|
||||
checkAuth,
|
||||
updateUserInfo
|
||||
updateUserInfo,
|
||||
fetchCurrentUser
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -23,22 +23,22 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stat-card blue">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">核算员工</div>
|
||||
<div class="stat-value">{{ dashboard.employee_count || 0 }}</div>
|
||||
<div class="stat-sub">{{ dashboard.payroll_month || dashboardMonth }} 薪资单</div>
|
||||
</div>
|
||||
<div class="stat-card purple">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">应发工资</div>
|
||||
<div class="stat-value">¥{{ money(dashboard.total_gross_salary) }}</div>
|
||||
<div class="stat-sub">税前及各类补贴合计</div>
|
||||
</div>
|
||||
<div class="stat-card green">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">实发工资</div>
|
||||
<div class="stat-value">¥{{ money(dashboard.total_net_salary) }}</div>
|
||||
<div class="stat-sub">扣除五险一金及个税后</div>
|
||||
</div>
|
||||
<div class="stat-card orange">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">待处理</div>
|
||||
<div class="stat-value">{{ (dashboard.draft_count || 0) + (dashboard.confirmed_count || 0) }}</div>
|
||||
<div class="stat-sub">草稿 {{ dashboard.draft_count || 0 }} / 待发放 {{ dashboard.confirmed_count || 0 }}</div>
|
||||
@@ -248,7 +248,7 @@
|
||||
</el-table>
|
||||
<div class="scheme-form" v-if="schemeFormVisible">
|
||||
<div class="section-title">{{ schemeEditingID ? "编辑薪酬方案" : "新增薪酬方案" }}</div>
|
||||
<el-form ref="schemeFormRef" :model="schemeForm" :rules="schemeRules" label-width="100px">
|
||||
<el-form ref="schemeFormRef" :model="schemeForm" :rules="schemeRules" label-width="120px">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="方案名称" prop="scheme_name"><el-input v-model="schemeForm.scheme_name" /></el-form-item>
|
||||
<el-form-item label="适用员工" prop="employee_id"><el-select v-model="schemeForm.employee_id" filterable style="width: 100%"><el-option v-for="employee in employees" :key="employee.id" :label="employeeLabel(employee)" :value="employee.id" /></el-select></el-form-item>
|
||||
@@ -340,7 +340,7 @@ function money(value) { return Number(value || 0).toLocaleString("zh-CN", { mini
|
||||
function dateTime(value) { return value ? String(value).replace("T", " ").slice(0, 19) : "-" }
|
||||
function statusText(status) { return ["草稿", "已确认", "已发放", "已作废"][status] || "未知" }
|
||||
function statusType(status) { return ["info", "warning", "success", "danger"][status] || "info" }
|
||||
function employeeLabel(employee) { return `${employee.name}${employee.department ? `(${employee.department})` : ""}` }
|
||||
function employeeLabel(employee) { return `${employee.name}${employee.department_name ? `(${employee.department_name})` : (employee.department ? `(${employee.department})` : "")}` }
|
||||
function employeeName(id) { return employees.value.find(item => item.id === id)?.name || "-" }
|
||||
function fixedSchemeAmount(item) { return sum(item, ["base_salary", "post_allowance", "performance_salary", "transport_allowance", "meal_allowance", "communication_allowance"]) }
|
||||
|
||||
@@ -443,7 +443,7 @@ onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadD
|
||||
.header-actions, .filter-bar { gap: 12px; }
|
||||
.dashboard-toolbar { justify-content: flex-end; gap: 10px; margin: -2px 0 10px; color: #606266; font-size: 13px; }
|
||||
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }
|
||||
.stat-card { padding: 15px 18px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; border-top: 3px solid #409eff; }
|
||||
.stat-card { padding: 15px 18px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; }
|
||||
.stat-card.purple { border-top-color: #8b5cf6; }.stat-card.green { border-top-color: #67c23a; }.stat-card.orange { border-top-color: #e6a23c; }
|
||||
.stat-label, .stat-sub { color: #909399; font-size: 13px; }.stat-value { margin: 7px 0 4px; font-size: 25px; font-weight: 600; color: #303133; }.stat-sub { font-size: 12px; }
|
||||
.table-card { padding: 16px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; }.filter-bar { flex-wrap: wrap; margin-bottom: 14px; }.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
<el-descriptions-item label="微信号">
|
||||
<span>{{ contact.wechat || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="QQ">
|
||||
<span>{{ contact.qq || '-' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="620px"
|
||||
width="720px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
@@ -13,150 +13,200 @@
|
||||
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-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<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-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="姓名" prop="contact_name">
|
||||
<el-input
|
||||
v-model="form.contact_name"
|
||||
placeholder="请输入联系人姓名"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="contact_name">
|
||||
<el-input
|
||||
v-model="form.contact_name"
|
||||
placeholder="请输入联系人姓名"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-col>
|
||||
</el-row>
|
||||
|
||||
<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-row :gutter="20" v-if="form.contact_type === 1">
|
||||
<el-col :span="12">
|
||||
<el-form-item 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-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item 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-col>
|
||||
</el-row>
|
||||
|
||||
<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-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="form.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="办公电话" prop="work_phone">
|
||||
<el-input
|
||||
v-model="form.work_phone"
|
||||
placeholder="请输入办公电话"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<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-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入邮箱"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="微信号" prop="wechat">
|
||||
<el-input
|
||||
v-model="form.wechat"
|
||||
placeholder="请输入微信号"
|
||||
maxlength="64"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="form.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="QQ" prop="qq">
|
||||
<el-input
|
||||
v-model="form.qq"
|
||||
placeholder="请输入QQ号"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="办公电话" prop="work_phone">
|
||||
<el-input
|
||||
v-model="form.work_phone"
|
||||
placeholder="请输入办公电话"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20" v-if="form.contact_type === 2">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公司名称" prop="company_name">
|
||||
<el-input
|
||||
v-model="form.company_name"
|
||||
placeholder="请输入公司名称"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门" prop="dept_name">
|
||||
<el-input
|
||||
v-model="form.dept_name"
|
||||
placeholder="请输入部门名称"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入邮箱"
|
||||
maxlength="128"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20" v-if="form.contact_type === 2">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="职位" prop="position_title">
|
||||
<el-input
|
||||
v-model="form.position_title"
|
||||
placeholder="请输入职位"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="微信号" prop="wechat">
|
||||
<el-input
|
||||
v-model="form.wechat"
|
||||
placeholder="请输入微信号"
|
||||
maxlength="64"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<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-col>
|
||||
</el-row>
|
||||
|
||||
<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-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<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-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -173,7 +223,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrganizationList, getPositionList } from '@/api/contactOA'
|
||||
import { getOrganizationList, getPositionList, getPositionDetail } from '@/api/contactOA'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
@@ -201,6 +251,7 @@ const emptyForm = () => ({
|
||||
work_phone: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
qq: '',
|
||||
avatar: '',
|
||||
org_id: null,
|
||||
company_name: '',
|
||||
@@ -209,7 +260,6 @@ const emptyForm = () => ({
|
||||
position_title: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
@@ -270,45 +320,39 @@ const loadOrgOptions = async () => {
|
||||
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
|
||||
}
|
||||
|
||||
// 职位归属由 department_id 决定:只取挂在所选部门下的职位,
|
||||
// 不再向上追溯到上级组织(否则选了"运维部"会把"总经办"的职位也带出来)。
|
||||
const loadPositionOptions = async (deptId) => {
|
||||
positionLoading.value = true
|
||||
try {
|
||||
const res = await getPositionList()
|
||||
const params = deptId ? { department_id: deptId } : {}
|
||||
const res = await getPositionList(params)
|
||||
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)))
|
||||
}
|
||||
positionOptions.value = list.filter((p) => p.status === 1)
|
||||
} catch (error) {
|
||||
positionOptions.value = []
|
||||
} finally {
|
||||
positionLoading.value = false
|
||||
}
|
||||
await ensureCurrentPosition()
|
||||
}
|
||||
|
||||
// 编辑历史数据时,已选职位可能挂在其它组织上(如集团级的“总经理”),
|
||||
// 这时把它补进选项,保证回显出职位名称而不是一个裸 ID。
|
||||
const ensureCurrentPosition = async () => {
|
||||
const positionId = form.value?.position_id
|
||||
if (!positionId) return
|
||||
if (positionOptions.value.some((p) => Number(p.id) === Number(positionId))) return
|
||||
try {
|
||||
const res = await getPositionDetail(positionId)
|
||||
const detail = res?.data || res
|
||||
if (detail?.id) {
|
||||
positionOptions.value = [detail, ...positionOptions.value]
|
||||
}
|
||||
} catch (error) {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrgChange = (val) => {
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
{{ row.contact_name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
<div class="contact-name-info">
|
||||
<span class="contact-name">{{ row.contact_name }}</span>
|
||||
<span class="contact-name" @click.stop="openDetail(row)">{{ row.contact_name }}</span>
|
||||
<el-tag
|
||||
:type="row.contact_type === 1 ? 'primary' : 'info'"
|
||||
size="small"
|
||||
@@ -106,38 +106,37 @@
|
||||
</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">
|
||||
<el-table-column prop="position_title" label="职位" width="140">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.position_title || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="org_name" label="部门" width="140">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.org_name || row.dept_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" width="140" />
|
||||
<!-- <el-table-column prop="wechat" label="微信" width="180">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.wechat || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="qq" label="QQ" width="140">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.qq || '-' }}</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">
|
||||
<el-table-column label="操作" fixed="right" width="140" align="center">
|
||||
<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>
|
||||
@@ -323,6 +322,7 @@ const editFormData = reactive({
|
||||
work_phone: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
qq: '',
|
||||
avatar: '',
|
||||
org_id: null,
|
||||
company_name: '',
|
||||
@@ -331,7 +331,6 @@ const editFormData = reactive({
|
||||
position_title: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
@@ -354,7 +353,6 @@ const openCreateContact = () => {
|
||||
position_title: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
@@ -371,6 +369,7 @@ const openEditContact = (row) => {
|
||||
work_phone: row.work_phone,
|
||||
email: row.email,
|
||||
wechat: row.wechat,
|
||||
qq: row.qq,
|
||||
avatar: row.avatar,
|
||||
org_id: row.org_id,
|
||||
company_name: row.company_name,
|
||||
@@ -379,7 +378,6 @@ const openEditContact = (row) => {
|
||||
position_title: row.position_title,
|
||||
address: row.address,
|
||||
remark: row.remark,
|
||||
sort: row.sort,
|
||||
status: row.status
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
@@ -415,20 +413,6 @@ const handleDetailEdit = (row) => {
|
||||
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()
|
||||
@@ -585,7 +569,12 @@ onMounted(() => {
|
||||
|
||||
.contact-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.contact-type-tag {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="16" class="chart-row">
|
||||
<el-col :span="14" :xs="24">
|
||||
<div class="chart-card">
|
||||
<div class="chart-card trend-card">
|
||||
<div class="chart-title">近6个月报销趋势</div>
|
||||
<div ref="trendChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
@@ -42,9 +42,21 @@
|
||||
>查看全部</el-button
|
||||
>
|
||||
</div>
|
||||
<el-tabs v-model="scheduleTab" class="schedule-tabs">
|
||||
<el-tab-pane name="pending">
|
||||
<template #label>
|
||||
待完成 ({{ schedulePendingCount }})
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="done">
|
||||
<template #label>
|
||||
已完成 ({{ scheduleDoneCount }})
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div v-loading="scheduleLoading" class="schedule-list">
|
||||
<div
|
||||
v-for="item in scheduleReminders"
|
||||
v-for="item in filteredScheduleReminders"
|
||||
:key="item.id"
|
||||
class="schedule-item"
|
||||
:class="{ done: item.status === 1 }"
|
||||
@@ -52,9 +64,9 @@
|
||||
>
|
||||
<el-checkbox
|
||||
:model-value="item.status === 1"
|
||||
@change="toggleScheduleFinish(item)"
|
||||
@change="openScheduleAction(item)"
|
||||
/>
|
||||
<div class="schedule-item-main" @click="toggleScheduleFinish(item)">
|
||||
<div class="schedule-item-main" @click="openScheduleAction(item)">
|
||||
<div
|
||||
class="schedule-item-title"
|
||||
:class="{ done: item.status === 1 }"
|
||||
@@ -81,11 +93,65 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!scheduleReminders.length && !scheduleLoading"
|
||||
description="近期暂无日程安排"
|
||||
v-if="!filteredScheduleReminders.length && !scheduleLoading"
|
||||
:description="scheduleTab === 'done' ? '暂无已完成日程' : '近期暂无待完成日程'"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 日程操作确认弹窗 -->
|
||||
<el-dialog
|
||||
v-model="scheduleActionVisible"
|
||||
title="日程操作"
|
||||
width="420px"
|
||||
align-center
|
||||
>
|
||||
<div v-if="activeSchedule" class="schedule-action-content">
|
||||
<div class="schedule-action-title">{{ activeSchedule.title }}</div>
|
||||
<div class="schedule-action-meta">
|
||||
<span :class="{ overdue: isOverdueSchedule(activeSchedule) }">{{
|
||||
scheduleDateTag(activeSchedule)
|
||||
}}</span>
|
||||
<span>{{ scheduleTimeText(activeSchedule) }}</span>
|
||||
<el-tag
|
||||
v-if="activeSchedule.priority > 0"
|
||||
:type="activeSchedule.priority === 2 ? 'danger' : 'warning'"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ activeSchedule.priority === 2 ? "紧急" : "重要" }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="activeSchedule.status === 1 ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ activeSchedule.status === 1 ? "已完成" : "待完成" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="schedule-action-tip">
|
||||
请选择要执行的操作:
|
||||
</div>
|
||||
<div class="schedule-action-btns">
|
||||
<el-button @click="editSchedule(activeSchedule)">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeSchedule.status === 0"
|
||||
type="success"
|
||||
@click="confirmScheduleFinish(activeSchedule)"
|
||||
>
|
||||
标记完成
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeSchedule.status === 1"
|
||||
type="warning"
|
||||
@click="confirmScheduleFinish(activeSchedule)"
|
||||
>
|
||||
反审核
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -248,6 +314,18 @@ const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
|
||||
const router = useRouter();
|
||||
const scheduleReminders = ref([]);
|
||||
const scheduleLoading = ref(false);
|
||||
const scheduleTab = ref("pending");
|
||||
|
||||
const schedulePendingCount = computed(
|
||||
() => scheduleReminders.value.filter((r) => r.status === 0).length,
|
||||
);
|
||||
const scheduleDoneCount = computed(
|
||||
() => scheduleReminders.value.filter((r) => r.status === 1).length,
|
||||
);
|
||||
const filteredScheduleReminders = computed(() => {
|
||||
const status = scheduleTab.value === "done" ? 1 : 0;
|
||||
return scheduleReminders.value.filter((r) => r.status === status);
|
||||
});
|
||||
|
||||
const pad2 = (n) => String(n).padStart(2, "0");
|
||||
const fmtDate = (d) =>
|
||||
@@ -335,18 +413,46 @@ const scheduleTimeText = (item) =>
|
||||
const isOverdueSchedule = (item) =>
|
||||
item.status === 0 && item.schedule_date < fmtDate(new Date());
|
||||
|
||||
const toggleScheduleFinish = async (item) => {
|
||||
const res = await finishSchedule(item.id);
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
|
||||
loadScheduleReminders();
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "操作失败");
|
||||
// 点击日程项:打开操作弹窗,不直接改状态
|
||||
const scheduleActionVisible = ref(false);
|
||||
const activeSchedule = ref(null);
|
||||
const scheduleActioning = ref(false);
|
||||
|
||||
const openScheduleAction = (item) => {
|
||||
activeSchedule.value = item;
|
||||
scheduleActionVisible.value = true;
|
||||
};
|
||||
|
||||
// 在弹窗中确认执行「已完成 / 反审核」操作
|
||||
const confirmScheduleFinish = async (item) => {
|
||||
if (scheduleActioning.value) return;
|
||||
scheduleActioning.value = true;
|
||||
try {
|
||||
const res = await finishSchedule(item.id);
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
|
||||
scheduleActionVisible.value = false;
|
||||
loadScheduleReminders();
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "操作失败");
|
||||
} finally {
|
||||
scheduleActioning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goSchedule = () => router.push("/apps/oa/schedule");
|
||||
|
||||
// 跳转到日程管理页并打开该日程的编辑抽屉
|
||||
const editSchedule = (item) => {
|
||||
// 通过 sessionStorage 把待编辑日程传递给日程管理页,由其打开编辑抽屉
|
||||
sessionStorage.setItem("schedule_edit_item", JSON.stringify(item));
|
||||
scheduleActionVisible.value = false;
|
||||
router.push("/apps/oa/schedule");
|
||||
};
|
||||
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const data = responseData(await getReimbursementDashboard());
|
||||
@@ -484,12 +590,24 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.trend-card {
|
||||
height: 398px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.type-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
height: 100%;
|
||||
height: 398px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -499,8 +617,27 @@ onBeforeUnmount(() => {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.schedule-tabs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +701,42 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-action-content {
|
||||
.schedule-action-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.schedule-action-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.overdue {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-action-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.schedule-action-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-dist,
|
||||
.type-dist {
|
||||
display: flex;
|
||||
|
||||
@@ -50,9 +50,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
<el-button @click="emit('toggle', schedule)">{{
|
||||
schedule?.status === 1 ? "取消完成" : "完成"
|
||||
schedule?.status === 1 ? "反审核" : "完成"
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="emit('edit', schedule)"
|
||||
>编辑</el-button
|
||||
|
||||
@@ -208,7 +208,7 @@ const performLoginRequest = async () => {
|
||||
localStorage.setItem("loginRememberMe", "false");
|
||||
}
|
||||
|
||||
authStore.setLoginInfo(res.data);
|
||||
authStore.setLoginInfo({ ...res.data, tenant_name: tenant_name.value });
|
||||
|
||||
// 重置 Tabs 状态
|
||||
const { useTabsStore } = await import("@/stores");
|
||||
@@ -339,7 +339,7 @@ const startGeetest4 = async () => {
|
||||
localStorage.setItem("loginRememberMe", "false");
|
||||
}
|
||||
|
||||
authStore.setLoginInfo(loginRes.data);
|
||||
authStore.setLoginInfo({ ...loginRes.data, tenant_name: tenant_name.value });
|
||||
const { useTabsStore } = await import("@/stores");
|
||||
const tabsStore = useTabsStore();
|
||||
tabsStore.resetTabs();
|
||||
|
||||
@@ -519,6 +519,12 @@ func (c *BackendAdminUserController) EditUser() {
|
||||
|
||||
// DeleteUser 删除后台租户用户
|
||||
// DELETE /backend/deleteUser/:id
|
||||
//
|
||||
// 用户管理里的账号属于租户层面的登录凭证(yz_system_tenant_user),与员工
|
||||
//(yz_backend_employee)、人事档案(yz_backend_employee_file)、通讯录
|
||||
//(yz_backend_erp_contact)是三份不同用途的数据:账号只用于登录系统,员工与档案属于企业数据。
|
||||
// 因此这里只解除登录绑定,不做任何级联操作——删除账号不会影响员工档案和通讯录记录。
|
||||
// 员工侧的离职/禁用走 deleteEmployee,档案侧的离职走 employeefile/delete。
|
||||
func (c *BackendAdminUserController) DeleteUser() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||||
|
||||
@@ -114,13 +114,14 @@ func (c *BackendAuthController) LoginBackend() {
|
||||
"data": map[string]interface{}{
|
||||
"token": token,
|
||||
"user": map[string]interface{}{
|
||||
"id": loginUser.ID,
|
||||
"account": loginUser.Account,
|
||||
"name": loginUser.Name,
|
||||
"tid": loginUser.Tid,
|
||||
"rid": loginUser.Rid,
|
||||
"avatar": loginUser.Avatar,
|
||||
"role_name": loginUser.RoleName,
|
||||
"id": loginUser.ID,
|
||||
"account": loginUser.Account,
|
||||
"name": loginUser.Name,
|
||||
"tid": loginUser.Tid,
|
||||
"tenant_name": loginUser.TenantName,
|
||||
"rid": loginUser.Rid,
|
||||
"avatar": loginUser.Avatar,
|
||||
"role_name": loginUser.RoleName,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -171,17 +172,25 @@ func (c *BackendAuthController) GetCurrentUser() {
|
||||
name = strings.TrimSpace(*tenantUser.Name)
|
||||
}
|
||||
|
||||
// 查询当前租户名称
|
||||
tenantName := ""
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", claims.TenantId).One(&tenant); err == nil {
|
||||
tenantName = strings.TrimSpace(tenant.TenantName)
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"id": tenantUser.Uid,
|
||||
"account": account,
|
||||
"name": name,
|
||||
"tid": tenantUser.Tid,
|
||||
"rid": 0,
|
||||
"avatar": "",
|
||||
"role_name": "",
|
||||
"id": tenantUser.Uid,
|
||||
"account": account,
|
||||
"name": name,
|
||||
"tid": tenantUser.Tid,
|
||||
"tenant_name": tenantName,
|
||||
"rid": 0,
|
||||
"avatar": "",
|
||||
"role_name": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ type erpContactDTO struct {
|
||||
WorkPhone string `json:"work_phone"`
|
||||
Email string `json:"email"`
|
||||
Wechat string `json:"wechat"`
|
||||
QQ string `json:"qq"`
|
||||
Avatar string `json:"avatar"`
|
||||
CompanyName string `json:"company_name"`
|
||||
DeptName string `json:"dept_name"`
|
||||
@@ -116,6 +117,7 @@ func (c *BackendErpContactController) List() {
|
||||
cond = cond.Or("phone__contains", keyword)
|
||||
cond = cond.Or("email__contains", keyword)
|
||||
cond = cond.Or("wechat__contains", keyword)
|
||||
cond = cond.Or("qq__contains", keyword)
|
||||
qs = qs.SetCond(cond)
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ func (c *BackendErpContactController) List() {
|
||||
}
|
||||
|
||||
var rows []models.BackendErpContact
|
||||
_, err = qs.OrderBy("-is_starred", "sort", "-id").
|
||||
_, err = qs.OrderBy("-is_starred", "contact_name", "-id").
|
||||
Limit(pageSize, (page-1)*pageSize).
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
@@ -189,6 +191,7 @@ func (c *BackendErpContactController) Create() {
|
||||
workPhone, _ := c.contactGetStringValue(body, "work_phone")
|
||||
email, _ := c.contactGetStringValue(body, "email")
|
||||
wechat, _ := c.contactGetStringValue(body, "wechat")
|
||||
qq, _ := c.contactGetStringValue(body, "qq")
|
||||
avatar, _ := c.contactGetStringValue(body, "avatar")
|
||||
companyName, _ := c.contactGetStringValue(body, "company_name")
|
||||
deptName, _ := c.contactGetStringValue(body, "dept_name")
|
||||
@@ -211,6 +214,7 @@ func (c *BackendErpContactController) Create() {
|
||||
WorkPhone: contactStrPtr(workPhone),
|
||||
Email: contactStrPtr(email),
|
||||
Wechat: contactStrPtr(wechat),
|
||||
QQ: contactStrPtr(qq),
|
||||
Avatar: contactStrPtr(avatar),
|
||||
CompanyName: contactStrPtr(companyName),
|
||||
DeptName: contactStrPtr(deptName),
|
||||
@@ -281,6 +285,9 @@ func (c *BackendErpContactController) Update() {
|
||||
if v, has := c.contactGetStringValue(body, "wechat"); has {
|
||||
update["wechat"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "qq"); has {
|
||||
update["qq"] = contactNullableString(v)
|
||||
}
|
||||
if v, has := c.contactGetStringValue(body, "avatar"); has {
|
||||
update["avatar"] = contactNullableString(v)
|
||||
}
|
||||
@@ -555,6 +562,7 @@ func (c *BackendErpContactController) contactDTO(row models.BackendErpContact) e
|
||||
WorkPhone: contactDerefString(row.WorkPhone),
|
||||
Email: contactDerefString(row.Email),
|
||||
Wechat: contactDerefString(row.Wechat),
|
||||
QQ: contactDerefString(row.QQ),
|
||||
Avatar: contactDerefString(row.Avatar),
|
||||
CompanyName: contactDerefString(row.CompanyName),
|
||||
DeptName: contactDerefString(row.DeptName),
|
||||
@@ -571,11 +579,31 @@ func (c *BackendErpContactController) contactDTO(row models.BackendErpContact) e
|
||||
|
||||
// --- Sync from Employee ---
|
||||
|
||||
// SyncContactOnEmployeeCreate 员工创建后同步到通讯录(由 ERP 控制器调用)
|
||||
// SyncContactOnEmployeeCreate 员工创建后同步到通讯录(由组织架构控制器调用)
|
||||
func SyncContactOnEmployeeCreate(tid uint64, employeeID uint64, emp *models.BackendEmployee) {
|
||||
syncContactFromEmployee(tid, employeeID, emp)
|
||||
}
|
||||
|
||||
// SyncContactOnEmployeeRefresh 员工信息变更后重新全量同步通讯录(由组织架构控制器调用)。
|
||||
// 走的是与建档同步同一套逻辑:先取员工基础字段,再用人事档案字段覆盖(工作邮箱、家庭地址优先)。
|
||||
func SyncContactOnEmployeeRefresh(tid uint64, employeeID uint64) {
|
||||
var emp models.BackendEmployee
|
||||
if err := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("id", employeeID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&emp); err != nil {
|
||||
return
|
||||
}
|
||||
syncContactFromEmployee(tid, employeeID, &emp)
|
||||
}
|
||||
|
||||
// SyncContactOnEmployeeFileChange 人事档案建档/变更后同步通讯录(由 OA 档案控制器调用)。
|
||||
// 通讯录以 employee_id 关联员工,档案里的字段(工作邮箱、家庭地址)优先级高于员工自身字段。
|
||||
// 只做字段同步:不会新增/删除员工,也不会清理通讯录记录。
|
||||
func SyncContactOnEmployeeFileChange(tid uint64, employeeID uint64) {
|
||||
SyncContactOnEmployeeRefresh(tid, employeeID)
|
||||
}
|
||||
|
||||
// SyncContactOnEmployeeUpdate 员工更新后同步到通讯录(由 ERP 控制器调用)
|
||||
func SyncContactOnEmployeeUpdate(employeeID uint64, update orm.Params) {
|
||||
var contact models.BackendErpContact
|
||||
@@ -638,11 +666,8 @@ func SyncContactOnEmployeeUpdate(employeeID uint64, update orm.Params) {
|
||||
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)).
|
||||
@@ -651,7 +676,9 @@ func SyncContactOnEmployeeUpdate(employeeID uint64, update orm.Params) {
|
||||
}
|
||||
}
|
||||
|
||||
// SyncContactOnEmployeeDelete 员工删除后同步通讯录(由 ERP 控制器调用)
|
||||
// SyncContactOnEmployeeDelete 清理员工关联的通讯录记录。
|
||||
// 注意:员工离职/禁用(deleteEmployee)与后台账号删除(deleteUser)都不得调用本函数——
|
||||
// 通讯录是企业数据,账号或状态变化不等于联系人消失。仅在员工数据被真正物理清理时使用。
|
||||
func SyncContactOnEmployeeDelete(employeeID uint64) {
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
@@ -661,6 +688,7 @@ func SyncContactOnEmployeeDelete(employeeID uint64) {
|
||||
}
|
||||
|
||||
// syncContactFromEmployee 内部同步函数
|
||||
// 从员工信息和人事档案同步数据到通讯录
|
||||
func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendEmployee) string {
|
||||
var existing models.BackendErpContact
|
||||
err := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
@@ -668,6 +696,13 @@ func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendE
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&existing)
|
||||
|
||||
// 获取人事档案数据
|
||||
fileData := getEmployeeFileForContact(employeeID)
|
||||
|
||||
// 通讯录字段优先级:人事档案 > 员工(档案字段为空时回退员工自身字段)
|
||||
email := contactFirstNonEmptyPtr(&fileData.WorkEmail, emp.Email)
|
||||
address := contactFirstNonEmptyPtr(&fileData.HouseholdAddress, &fileData.CurrentAddress, emp.HomeAddress)
|
||||
|
||||
if err != nil {
|
||||
// Not found - create new contact
|
||||
var orgID *uint64
|
||||
@@ -685,10 +720,10 @@ func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendE
|
||||
ContactName: emp.Name,
|
||||
Gender: emp.Gender,
|
||||
Phone: emp.Phone,
|
||||
Email: emp.Email,
|
||||
Email: email,
|
||||
Wechat: emp.Wechat,
|
||||
PositionTitle: emp.Position,
|
||||
Address: emp.HomeAddress,
|
||||
Address: address,
|
||||
Status: 1,
|
||||
}
|
||||
models.Orm.Insert(&contact)
|
||||
@@ -700,10 +735,10 @@ func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendE
|
||||
"contact_name": emp.Name,
|
||||
"gender": emp.Gender,
|
||||
"phone": emp.Phone,
|
||||
"email": emp.Email,
|
||||
"email": email,
|
||||
"wechat": emp.Wechat,
|
||||
"position_title": emp.Position,
|
||||
"address": emp.HomeAddress,
|
||||
"address": address,
|
||||
}
|
||||
if emp.Department != nil {
|
||||
if oid, err := strconv.ParseUint(strings.TrimSpace(*emp.Department), 10, 64); err == nil && oid > 0 {
|
||||
@@ -721,6 +756,55 @@ func syncContactFromEmployee(tid uint64, employeeID uint64, emp *models.BackendE
|
||||
return "updated"
|
||||
}
|
||||
|
||||
// getEmployeeFileForContact 获取员工人事档案数据(用于通讯录同步)
|
||||
// 返回档案中与通讯录相关的字段
|
||||
func getEmployeeFileForContact(employeeID uint64) struct {
|
||||
WorkEmail string
|
||||
HouseholdAddress string
|
||||
CurrentAddress string
|
||||
EmergencyContact string
|
||||
EmergencyPhone string
|
||||
EmergencyRelation string
|
||||
EmploymentStatus int8
|
||||
} {
|
||||
var file models.BackendEmployeeFile
|
||||
err := models.Orm.QueryTable(new(models.BackendEmployeeFile)).
|
||||
Filter("employee_id", employeeID).
|
||||
Filter("is_deleted", 0).
|
||||
One(&file)
|
||||
|
||||
if err != nil {
|
||||
// 档案不存在,返回空值
|
||||
return struct {
|
||||
WorkEmail string
|
||||
HouseholdAddress string
|
||||
CurrentAddress string
|
||||
EmergencyContact string
|
||||
EmergencyPhone string
|
||||
EmergencyRelation string
|
||||
EmploymentStatus int8
|
||||
}{}
|
||||
}
|
||||
|
||||
return struct {
|
||||
WorkEmail string
|
||||
HouseholdAddress string
|
||||
CurrentAddress string
|
||||
EmergencyContact string
|
||||
EmergencyPhone string
|
||||
EmergencyRelation string
|
||||
EmploymentStatus int8
|
||||
}{
|
||||
WorkEmail: file.WorkEmail,
|
||||
HouseholdAddress: file.HouseholdAddress,
|
||||
CurrentAddress: file.CurrentAddress,
|
||||
EmergencyContact: file.EmergencyContact,
|
||||
EmergencyPhone: file.EmergencyPhone,
|
||||
EmergencyRelation: file.EmergencyRelation,
|
||||
EmploymentStatus: file.EmploymentStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper functions (package-level, shared with ERP controller) ---
|
||||
|
||||
func contactDerefString(v *string) string {
|
||||
@@ -879,3 +963,17 @@ func (c *BackendErpContactController) contactJsonError(code int, msg string) {
|
||||
c.Data["json"] = map[string]interface{}{"code": code, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// contactFirstNonEmptyPtr 按优先级取第一个非空字符串(去空格后判断),全部为空时返回 nil。
|
||||
// 用于通讯录"档案字段 > 员工字段"的取值优先级,避免用空字符串覆盖已有数据。
|
||||
func contactFirstNonEmptyPtr(values ...*string) *string {
|
||||
for _, v := range values {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if s := strings.TrimSpace(*v); s != "" {
|
||||
return &s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -594,6 +594,9 @@ func (c *BackendEmployeeFileController) Create() {
|
||||
return
|
||||
}
|
||||
|
||||
// 建档后同步通讯录(工作邮箱、家庭地址以档案为准)
|
||||
SyncContactOnEmployeeFileChange(uint64(tid), item.EmployeeID)
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
@@ -654,6 +657,9 @@ func (c *BackendEmployeeFileController) Update() {
|
||||
return
|
||||
}
|
||||
|
||||
// 档案变更后同步通讯录(工作邮箱、家庭地址以档案为准)
|
||||
SyncContactOnEmployeeFileChange(uint64(tid), item.EmployeeID)
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
@@ -718,7 +724,12 @@ func (c *BackendEmployeeFileController) UpdateCertificates() {
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/oa/employeefile/delete/:id 软删除档案。
|
||||
// Delete DELETE /backend/oa/employeefile/delete/:id 档案离职/禁用(档案不做删除)。
|
||||
//
|
||||
// 与员工同理:人事档案属于企业数据,通讯录(yz_backend_erp_contact)也以 employee_id 关联,
|
||||
// 软删档案会让通讯录里对应的人失去数据来源。因此这里只把在职状态置为"离职"(3),
|
||||
// 不写 is_deleted / delete_time,档案与通讯录记录始终保留可查。
|
||||
// 在职状态调整(试用/正式/离职)走 update 接口即可,这里只是给"删除"动作一个不毁数据的语义。
|
||||
func (c *BackendEmployeeFileController) Delete() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
@@ -733,15 +744,24 @@ func (c *BackendEmployeeFileController) Delete() {
|
||||
return
|
||||
}
|
||||
|
||||
var item models.BackendEmployeeFile
|
||||
if err := c.efQuery(tid).Filter("id", id).One(&item); err != nil {
|
||||
c.efErr(404, 404, "档案不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := c.efQuery(tid).Filter("id", id).
|
||||
Update(map[string]interface{}{
|
||||
"IsDeleted": int8(1),
|
||||
"DeleteTime": now,
|
||||
"UpdateTime": now,
|
||||
})
|
||||
if err != nil || n == 0 {
|
||||
c.efErr(404, 404, "档案不存在或已删除")
|
||||
// 离职日期留空时补当天,保证档案资料(离职需填离职日期)完整
|
||||
if item.LeaveDate == nil {
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
item.LeaveDate = &today
|
||||
}
|
||||
item.EmploymentStatus = 3
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item, "EmploymentStatus", "LeaveDate", "UpdateTime"); err != nil {
|
||||
log.Printf("员工档案离职失败: tid=%d file_id=%d err=%v", tid, id, err)
|
||||
c.efErr(500, 500, "离职操作失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -251,16 +251,35 @@ func (c *BackendOaCompensationController) Employees() {
|
||||
c.compensationError(500, "员工查询失败")
|
||||
return
|
||||
}
|
||||
orgNames := compensationOrgNameMap(claims.TenantId)
|
||||
list := make([]map[string]interface{}, 0, len(employees))
|
||||
for _, employee := range employees {
|
||||
department := compensationString(employee.Department)
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": employee.ID, "name": employee.Name, "account": employee.Account,
|
||||
"department": compensationString(employee.Department), "position": compensationString(employee.Position),
|
||||
"department": department, "department_name": efOrgNameByID(orgNames, department),
|
||||
"position": compensationString(employee.Position),
|
||||
})
|
||||
}
|
||||
c.compensationOK(list)
|
||||
}
|
||||
|
||||
// compensationOrgNameMap 组织ID -> 名称映射,用于把员工 department 字段(存的是组织ID)解析为部门名称展示。
|
||||
func compensationOrgNameMap(tid int) map[uint64]string {
|
||||
var rows []models.BackendOrganization
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&rows, "ID", "OrgName"); err != nil {
|
||||
return map[uint64]string{}
|
||||
}
|
||||
names := make(map[uint64]string, len(rows))
|
||||
for _, row := range rows {
|
||||
names[row.ID] = row.OrgName
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// SchemeList GET /backend/oa/compensation/schemes
|
||||
func (c *BackendOaCompensationController) SchemeList() {
|
||||
claims, err := c.compensationClaims()
|
||||
|
||||
@@ -73,7 +73,6 @@ type organizationDTO struct {
|
||||
Sort uint `json:"sort"`
|
||||
Status int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
EmployeeCount int64 `json:"employee_count"`
|
||||
CreateTime string `json:"create_time"`
|
||||
UpdateTime string `json:"update_time"`
|
||||
}
|
||||
@@ -958,6 +957,9 @@ func (c *BackendOrganizationController) CreateEmployee() {
|
||||
return
|
||||
}
|
||||
|
||||
// 同步到通讯录(员工为基础数据,已建档时档案字段优先)
|
||||
SyncContactOnEmployeeCreate(tid, uint64(id), &row)
|
||||
|
||||
c.jsonOK(map[string]interface{}{"id": id, "account": account})
|
||||
}
|
||||
|
||||
@@ -1063,10 +1065,19 @@ func (c *BackendOrganizationController) EditEmployee() {
|
||||
return
|
||||
}
|
||||
|
||||
// 同步到通讯录(字段优先级:人事档案 > 员工)
|
||||
SyncContactOnEmployeeRefresh(tid, uint64(id))
|
||||
|
||||
c.jsonOK(nil)
|
||||
}
|
||||
|
||||
// DeleteEmployee 删除员工(软删除)。
|
||||
// DeleteEmployee 员工离职/禁用:员工不做删除,只改账号状态。
|
||||
//
|
||||
// 说明:员工属于企业数据,人事档案(yz_backend_employee_file)与通讯录
|
||||
//(yz_backend_erp_contact)都以 employee_id 关联员工。一旦把员工软删除(写 delete_time),
|
||||
// 档案页与通讯录就查不到对应的人,等于连带删掉了企业数据。
|
||||
// 因此这里只把账号状态置为"离职"(2),不写 delete_time,也不触碰档案与通讯录记录。
|
||||
// 启用/禁用请走 editEmployee 改 account_status(1 启用 / 0 禁用 / 2 离职)。
|
||||
// DELETE /backend/{erp|oa}/deleteEmployee/:id
|
||||
func (c *BackendOrganizationController) DeleteEmployee() {
|
||||
tid, ok := c.tenantID()
|
||||
@@ -1079,14 +1090,15 @@ func (c *BackendOrganizationController) DeleteEmployee() {
|
||||
return
|
||||
}
|
||||
|
||||
num, err := c.employeeQuery(tid).Filter("id", id).
|
||||
Update(orm.Params{"delete_time": c.nowString(), "account_status": int8(2)})
|
||||
if err != nil {
|
||||
c.jsonError(500, "删除员工失败: "+err.Error())
|
||||
if !c.employeeQuery(tid).Filter("id", id).Exist() {
|
||||
c.jsonError(404, "员工不存在")
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
c.jsonError(404, "员工不存在")
|
||||
|
||||
// 员工不允许删除,仅置为离职状态;档案与通讯录原样保留
|
||||
if _, err := c.employeeQuery(tid).Filter("id", id).
|
||||
Update(orm.Params{"account_status": int8(2)}); err != nil {
|
||||
c.jsonError(500, "离职操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -29,24 +29,22 @@ func (c *BackendOrganizationController) organizationDTOList(tid uint64, rows []m
|
||||
|
||||
nameByID := c.orgNameMap(tid)
|
||||
leaderNames := c.employeeNameMap(tid)
|
||||
counts := c.employeeCountByDepartment(tid)
|
||||
|
||||
for _, row := range rows {
|
||||
list = append(list, c.assembleOrganizationDTO(row, nameByID, leaderNames, counts))
|
||||
list = append(list, c.assembleOrganizationDTO(row, nameByID, leaderNames))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *BackendOrganizationController) organizationDTO(tid uint64, row models.BackendOrganization) organizationDTO {
|
||||
return c.assembleOrganizationDTO(row,
|
||||
c.orgNameMap(tid), c.employeeNameMap(tid), c.employeeCountByDepartment(tid))
|
||||
c.orgNameMap(tid), c.employeeNameMap(tid))
|
||||
}
|
||||
|
||||
func (c *BackendOrganizationController) assembleOrganizationDTO(
|
||||
row models.BackendOrganization,
|
||||
nameByID map[uint64]string,
|
||||
leaderNames map[uint64]string,
|
||||
counts map[string]int64,
|
||||
) organizationDTO {
|
||||
leaderID := uint64(0)
|
||||
if row.LeaderID != nil {
|
||||
@@ -67,7 +65,6 @@ func (c *BackendOrganizationController) assembleOrganizationDTO(
|
||||
Sort: row.Sort,
|
||||
Status: row.Status,
|
||||
Remark: derefString(row.Remark),
|
||||
EmployeeCount: counts[strconv.FormatUint(row.ID, 10)],
|
||||
CreateTime: formatDateTime(&row.CreateTime),
|
||||
UpdateTime: formatDateTime(&row.UpdateTime),
|
||||
}
|
||||
@@ -179,23 +176,6 @@ func (c *BackendOrganizationController) employeeNameMap(tid uint64) map[uint64]s
|
||||
return result
|
||||
}
|
||||
|
||||
// employeeCountByDepartment 统计各部门(department 存的是组织ID字符串)的员工数。
|
||||
func (c *BackendOrganizationController) employeeCountByDepartment(tid uint64) map[string]int64 {
|
||||
result := map[string]int64{}
|
||||
var rows []models.BackendEmployee
|
||||
if _, err := c.employeeQuery(tid).All(&rows, "Department"); err != nil {
|
||||
return result
|
||||
}
|
||||
for _, row := range rows {
|
||||
key := strings.TrimSpace(derefString(row.Department))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
result[key]++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *BackendOrganizationController) orgExists(tid, id uint64) bool {
|
||||
if id == 0 {
|
||||
return false
|
||||
|
||||
@@ -16,6 +16,7 @@ type BackendErpContact struct {
|
||||
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"`
|
||||
QQ *string `orm:"column(qq);size(20);null" json:"qq"`
|
||||
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"`
|
||||
|
||||
@@ -29,6 +29,9 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
|
||||
beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
|
||||
|
||||
// 当前登录用户信息
|
||||
beego.Router("/backend/getCurrentUser", &controllers.BackendAuthController{}, "get:GetCurrentUser")
|
||||
|
||||
// 菜单接口
|
||||
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
|
||||
// 前端菜单接口
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
)
|
||||
|
||||
type PlatformLoginUser struct {
|
||||
ID uint64
|
||||
Account string
|
||||
Name string
|
||||
Tid uint64
|
||||
Rid uint64
|
||||
Avatar string
|
||||
RoleName string
|
||||
ID uint64
|
||||
Account string
|
||||
Name string
|
||||
Tid uint64
|
||||
TenantName string
|
||||
Rid uint64
|
||||
Avatar string
|
||||
RoleName string
|
||||
}
|
||||
|
||||
func adminRoleNameByID(roleID uint64) string {
|
||||
@@ -124,13 +125,14 @@ func BackendLogin(tenantName, account, password string) (string, *PlatformLoginU
|
||||
}
|
||||
|
||||
loginUser := &PlatformLoginUser{
|
||||
ID: tenantUser.Uid,
|
||||
Account: account,
|
||||
Name: "",
|
||||
Tid: tenant.ID,
|
||||
Rid: 0,
|
||||
Avatar: "",
|
||||
RoleName: "",
|
||||
ID: tenantUser.Uid,
|
||||
Account: account,
|
||||
Name: "",
|
||||
Tid: tenant.ID,
|
||||
TenantName: tenant.TenantName,
|
||||
Rid: 0,
|
||||
Avatar: "",
|
||||
RoleName: "",
|
||||
}
|
||||
if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" {
|
||||
loginUser.Account = strings.TrimSpace(*tenantUser.Account)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="organization-container">
|
||||
<el-row :gutter="16">
|
||||
<!-- 左侧:组织架构树 -->
|
||||
<el-col :span="8">
|
||||
<!-- 组织架构树(仅展示结构,增/改/删在节点悬浮按钮上操作) -->
|
||||
<el-col :span="24">
|
||||
<el-card shadow="hover" class="tree-card" v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
@@ -20,7 +20,6 @@
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node">
|
||||
@@ -41,60 +40,6 @@
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:详情面板 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" class="detail-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ detailTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="selectedNode" class="detail-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="组织名称">
|
||||
{{ selectedNode.org_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="组织编码">
|
||||
{{ selectedNode.org_code || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上级组织">
|
||||
{{ selectedNode.parent_name || '顶级组织' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ selectedNode.leader_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否是公司">
|
||||
<el-tag :type="selectedNode.is_company === 1 ? 'primary' : 'info'">
|
||||
{{ selectedNode.is_company === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
|
||||
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ selectedNode.remark || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" @click="handleEdit(selectedNode)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete(selectedNode)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="请选择左侧组织节点查看详情" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
@@ -108,11 +53,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import OrganizationEdit from './components/edit.vue'
|
||||
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
|
||||
import { getOrganizationList, deleteOrganization } from '@/api/erp'
|
||||
|
||||
// 树形结构数据(与后台字段对应)
|
||||
interface TreeNode {
|
||||
@@ -142,7 +87,6 @@ const treeProps = {
|
||||
|
||||
const treeRef = ref()
|
||||
const editRef = ref()
|
||||
const selectedNode = ref<TreeNode | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 将扁平列表构建为树形结构
|
||||
@@ -184,40 +128,6 @@ const buildTree = (list: any[]): TreeNode[] => {
|
||||
return roots
|
||||
}
|
||||
|
||||
// 获取组织机构详情
|
||||
const loadOrganizationDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await getOrganizationDetail(id)
|
||||
if (res.code === 200) {
|
||||
const data = res.data || null
|
||||
if (data) {
|
||||
selectedNode.value = {
|
||||
id: data.id,
|
||||
tid: data.tid,
|
||||
org_name: data.org_name,
|
||||
org_code: data.org_code,
|
||||
parent_id: data.parent_id,
|
||||
parent_name: data.parent_name,
|
||||
leader_id: data.leader_id,
|
||||
leader_name: data.leader_name,
|
||||
is_company: data.is_company,
|
||||
sort: data.sort,
|
||||
status: data.status,
|
||||
remark: data.remark,
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
selectedNode.value = null
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构详情失败:', error)
|
||||
ElMessage.error('获取组织机构详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取组织机构列表
|
||||
const fetchOrganizationList = async () => {
|
||||
loading.value = true
|
||||
@@ -226,11 +136,6 @@ const fetchOrganizationList = async () => {
|
||||
if (res.code === 200) {
|
||||
const list = res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
|
||||
// 如果当前没有选中的节点且有数据,默认加载第一个节点详情
|
||||
if (!selectedNode.value && list.length) {
|
||||
await loadOrganizationDetail(list[0].id)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构列表失败')
|
||||
}
|
||||
@@ -242,16 +147,6 @@ const fetchOrganizationList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 详情标题
|
||||
const detailTitle = computed(() => {
|
||||
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
|
||||
})
|
||||
|
||||
// 点击节点,加载详情
|
||||
const handleNodeClick = (data: TreeNode) => {
|
||||
loadOrganizationDetail(data.id)
|
||||
}
|
||||
|
||||
// 添加根节点
|
||||
const handleAddRoot = () => {
|
||||
editRef.value?.open(undefined, undefined)
|
||||
@@ -336,23 +231,4 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
.el-descriptions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- 通讯录表新增 QQ 字段
|
||||
-- 对应模型:go/models/contact.go BackendErpContact
|
||||
--
|
||||
-- 说明:历史迁移 alter_contact_rename_to_global.sql 把表名从 yz_backend_erp_contact
|
||||
-- 改名为 yz_backend_contact,但模型 BackendErpContact.TableName() 仍返回旧表名。
|
||||
-- 为避免不同环境表名不一致导致字段加不上,这里按实际存在的表逐个补齐,可重复执行。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- 旧表名(模型当前使用的表名)
|
||||
SET @tbl := 'yz_backend_erp_contact';
|
||||
SET @table_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl
|
||||
);
|
||||
SET @column_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qq'
|
||||
);
|
||||
SET @sql := IF(@table_exists > 0 AND @column_exists = 0,
|
||||
CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qq` VARCHAR(20) NULL COMMENT ''QQ号'' AFTER `wechat`'),
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 新表名(执行过改名迁移的环境)
|
||||
SET @tbl := 'yz_backend_contact';
|
||||
SET @table_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl
|
||||
);
|
||||
SET @column_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qq'
|
||||
);
|
||||
SET @sql := IF(@table_exists > 0 AND @column_exists = 0,
|
||||
CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qq` VARCHAR(20) NULL COMMENT ''QQ号'' AFTER `wechat`'),
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
Reference in New Issue
Block a user