优化日程提醒模块
This commit is contained in:
@@ -46,66 +46,6 @@
|
||||
<el-date-picker v-model="form.birthday" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider />
|
||||
<div class="form-title">组织信息</div>
|
||||
|
||||
<el-form-item label="隶属单位" prop="affiliate_unit">
|
||||
<el-tree-select
|
||||
v-model="form.affiliate_unit"
|
||||
:data="organizationTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
node-key="idStr"
|
||||
placeholder="请选择隶属单位"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-tree-select
|
||||
v-model="form.department"
|
||||
:data="departmentTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="请选择部门"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位" prop="position">
|
||||
<el-select
|
||||
v-model="form.position"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="选择或输入职位"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in positionOptions"
|
||||
:key="item.id"
|
||||
:label="item.position_name"
|
||||
:value="item.position_name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider />
|
||||
<div class="form-title">联系与其它</div>
|
||||
|
||||
<el-form-item label="学历" prop="education">
|
||||
<el-select v-model="form.education" clearable placeholder="请选择学历" style="width: 100%">
|
||||
<el-option v-for="item in educationOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="民族" prop="nation">
|
||||
<el-input v-model="form.nation" maxlength="20" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机" prop="phone">
|
||||
<el-input v-model="form.phone" maxlength="20" />
|
||||
</el-form-item>
|
||||
@@ -129,6 +69,56 @@
|
||||
<el-option v-if="isEdit" label="离职" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider />
|
||||
<div class="form-title">组织信息</div>
|
||||
|
||||
<el-form-item label="隶属单位" prop="affiliate_unit">
|
||||
<el-tree-select
|
||||
v-model="form.affiliate_unit"
|
||||
:data="companyTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
node-key="idStr"
|
||||
placeholder="请选择隶属单位"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-tree-select
|
||||
v-model="form.department"
|
||||
:data="departmentTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
:disabled="!form.affiliate_unit"
|
||||
node-key="idStr"
|
||||
placeholder="请选择部门"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位" prop="position">
|
||||
<el-select
|
||||
v-model="form.position"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
:disabled="!form.department"
|
||||
placeholder="选择或输入职位"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredPositions"
|
||||
:key="item.id"
|
||||
:label="item.position_name"
|
||||
:value="item.position_name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -139,7 +129,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { buildOrgTree, moduleLabels, unwrap, useOrganizationApi } from '../composables';
|
||||
|
||||
@@ -158,13 +148,13 @@ const loading = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const editingId = ref(0);
|
||||
const formRef = ref();
|
||||
/** 编辑回填期间置 true,级联 watcher 跳过清空,避免回填值被覆盖 */
|
||||
const backfilling = ref(false);
|
||||
|
||||
const organizationTree = ref([]);
|
||||
const departmentTree = ref([]);
|
||||
const positionOptions = ref([]);
|
||||
|
||||
const treeSelectProps = { value: 'idStr', label: 'org_name', children: 'children' };
|
||||
const educationOptions = ['博士', '硕士', '本科', '专科', '高中', '其他'];
|
||||
|
||||
const emptyForm = () => ({
|
||||
account: '',
|
||||
@@ -176,8 +166,6 @@ const emptyForm = () => ({
|
||||
affiliate_unit: '',
|
||||
department: '',
|
||||
position: '',
|
||||
education: '',
|
||||
nation: '',
|
||||
phone: '',
|
||||
wechat: '',
|
||||
email: '',
|
||||
@@ -231,15 +219,74 @@ const withIdStr = (nodes) =>
|
||||
children: withIdStr(node.children || []),
|
||||
}));
|
||||
|
||||
/** 在树中按 id 查找节点 */
|
||||
const findNode = (nodes, id) => {
|
||||
for (const n of nodes || []) {
|
||||
if (String(n.id) === String(id)) return n;
|
||||
const found = findNode(n.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 只保留单位节点(is_company=1),保持单位之间的层级 */
|
||||
const filterCompanyTree = (nodes) =>
|
||||
(nodes || [])
|
||||
.filter((n) => Number(n.is_company) === 1)
|
||||
.map((n) => ({ ...n, children: filterCompanyTree(n.children || []) }));
|
||||
|
||||
/** 从指定单位节点下提取部门子树(is_company=0) */
|
||||
const buildDepartmentSubtree = (companyNode) => {
|
||||
if (!companyNode) return [];
|
||||
const filterDepts = (nodes) =>
|
||||
(nodes || [])
|
||||
.filter((n) => Number(n.is_company) === 0)
|
||||
.map((n) => ({ ...n, children: filterDepts(n.children || []) }));
|
||||
return filterDepts(companyNode.children || []);
|
||||
};
|
||||
|
||||
/** 隶属单位下拉:只显示单位 */
|
||||
const companyTree = computed(() => withIdStr(filterCompanyTree(organizationTree.value)));
|
||||
|
||||
/** 部门下拉:只显示所选隶属单位下的部门 */
|
||||
const departmentTree = computed(() => {
|
||||
if (!form.affiliate_unit) return [];
|
||||
const companyNode = findNode(organizationTree.value, form.affiliate_unit);
|
||||
return withIdStr(buildDepartmentSubtree(companyNode));
|
||||
});
|
||||
|
||||
/** 职位下拉:只显示所选部门下的职位 */
|
||||
const filteredPositions = computed(() => {
|
||||
if (!form.department) return [];
|
||||
return (positionOptions.value || []).filter(
|
||||
(p) => Number(p.department_id) === Number(form.department)
|
||||
);
|
||||
});
|
||||
|
||||
// 级联:切换隶属单位时清空部门和职位;切换部门时清空职位(回填期间跳过)
|
||||
watch(
|
||||
() => form.affiliate_unit,
|
||||
() => {
|
||||
if (backfilling.value) return;
|
||||
form.department = '';
|
||||
form.position = '';
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => form.department,
|
||||
() => {
|
||||
if (backfilling.value) return;
|
||||
form.position = '';
|
||||
}
|
||||
);
|
||||
|
||||
const loadOptions = async () => {
|
||||
try {
|
||||
const [orgList, positions] = await Promise.all([
|
||||
api.value.getOrganizationList().then(unwrap),
|
||||
api.value.getPositionList().then(unwrap),
|
||||
]);
|
||||
const tree = withIdStr(buildOrgTree(orgList));
|
||||
organizationTree.value = tree;
|
||||
departmentTree.value = tree;
|
||||
organizationTree.value = withIdStr(buildOrgTree(orgList));
|
||||
positionOptions.value = positions || [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载选项失败');
|
||||
@@ -258,6 +305,7 @@ const open = async (row, defaults = {}) => {
|
||||
if (isEdit.value) {
|
||||
try {
|
||||
const detail = unwrap(await api.value.getEmployeeDetail(row.id)) || {};
|
||||
backfilling.value = true;
|
||||
Object.assign(form, {
|
||||
account: detail.account || '',
|
||||
name: detail.name || '',
|
||||
@@ -266,16 +314,19 @@ const open = async (row, defaults = {}) => {
|
||||
affiliate_unit: detail.affiliate_unit ? String(detail.affiliate_unit) : '',
|
||||
department: detail.department ? String(detail.department) : '',
|
||||
position: detail.position || '',
|
||||
education: detail.education || '',
|
||||
nation: detail.nation || '',
|
||||
phone: detail.phone || '',
|
||||
wechat: detail.wechat || '',
|
||||
email: detail.email || '',
|
||||
home_address: detail.home_address || '',
|
||||
account_status: Number(detail.account_status ?? detail.status ?? 1),
|
||||
});
|
||||
// watcher 为 pre-flush 异步执行,需等其执行完毕再复位标志,
|
||||
// 否则复位后 watcher 才触发,仍会清空 department / position。
|
||||
await nextTick();
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载详情失败');
|
||||
} finally {
|
||||
backfilling.value = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -304,8 +355,6 @@ const handleSubmit = async () => {
|
||||
affiliate_unit: form.affiliate_unit || '',
|
||||
department: form.department || '',
|
||||
position: form.position || '',
|
||||
education: form.education || '',
|
||||
nation: form.nation || '',
|
||||
phone: form.phone || '',
|
||||
wechat: form.wechat || '',
|
||||
email: form.email || '',
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
stripe
|
||||
@selection-change="(rows) => (selectedRows = rows)"
|
||||
>
|
||||
<el-table-column type="selection" width="45" />
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<!--<el-table-column type="selection" width="45" />-->
|
||||
<!--<el-table-column prop="id" label="ID" width="70" align="center" />-->
|
||||
<el-table-column prop="account" label="账号" width="130" />
|
||||
<el-table-column prop="name" label="姓名" width="110">
|
||||
<template #default="{ row }">
|
||||
@@ -85,11 +85,12 @@
|
||||
<el-tag :type="employeeStatusTagType(row)" size="small">{{ employeeStatusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="viewDialogRef?.open(row)">查看</el-button>
|
||||
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="moveDialogRef?.open([row])">调岗</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -112,6 +113,28 @@
|
||||
<EmployeeEditDialog ref="editDialogRef" :module="module" @success="loadData" />
|
||||
<EmployeeViewDialog ref="viewDialogRef" :module="module" />
|
||||
<MoveEmployeeDialog ref="moveDialogRef" :module="module" @success="handleMoved" />
|
||||
|
||||
<!-- 重置密码结果弹窗 -->
|
||||
<el-dialog
|
||||
v-model="resetDialogVisible"
|
||||
title="重置密码成功"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="reset-password-box">
|
||||
<p class="reset-tip">
|
||||
员工「<strong>{{ resetTargetName }}</strong>」的密码已重置为:
|
||||
</p>
|
||||
<div class="reset-password-display">
|
||||
<span class="reset-password-text">{{ resetNewPassword }}</span>
|
||||
</div>
|
||||
<p class="reset-warn">请及时将新密码告知员工,并提醒其登录后修改。</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="copyPassword">复制</el-button>
|
||||
<el-button @click="resetDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -213,6 +236,58 @@ const handleDelete = async (row) => {
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------- 重置密码 ------------------------------- */
|
||||
|
||||
const resetDialogVisible = ref(false);
|
||||
const resetNewPassword = ref('');
|
||||
const resetTargetName = ref('');
|
||||
|
||||
/** 生成 10 位随机密码(排除易混淆字符) */
|
||||
const generateRandomPassword = () => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleResetPassword = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将「${row.name}」的密码重置为随机密码吗?`,
|
||||
'重置密码确认',
|
||||
{ type: 'warning', confirmButtonText: '重置', cancelButtonText: '取消' }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const newPwd = generateRandomPassword();
|
||||
try {
|
||||
unwrap(await api.value.updateEmployee(row.id, { password: newPwd }));
|
||||
resetTargetName.value = row.name;
|
||||
resetNewPassword.value = newPwd;
|
||||
resetDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '重置密码失败');
|
||||
}
|
||||
};
|
||||
|
||||
const copyPassword = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(resetNewPassword.value);
|
||||
ElMessage.success('已复制到剪贴板');
|
||||
} catch {
|
||||
const input = document.createElement('input');
|
||||
input.value = resetNewPassword.value;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
ElMessage.success('已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDepartments();
|
||||
loadData();
|
||||
@@ -281,4 +356,34 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.reset-password-box {
|
||||
.reset-tip {
|
||||
margin: 0 0 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.reset-password-display {
|
||||
background: #f5f7fa;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.reset-password-text {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3px;
|
||||
color: #303133;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-warn {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user