批量更新
This commit is contained in:
@@ -108,7 +108,7 @@ async function refreshCache() {
|
||||
// 触发菜单刷新事件,通知CommonAside组件刷新菜单
|
||||
window.dispatchEvent(new CustomEvent('menu-cache-refreshed'));
|
||||
|
||||
ElMessage.success('菜单缓存和路由更新成功');
|
||||
ElMessage.success('更新成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh cache', error);
|
||||
ElMessage.error('更新缓存失败,请检查网络连接');
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# OA 模块优化总结
|
||||
|
||||
## 优化完成时间
|
||||
2024年
|
||||
|
||||
## 优化内容
|
||||
|
||||
### 1. 创建 OA 基础数据 Store (`pc/src/stores/oa.js`)
|
||||
|
||||
**功能特性:**
|
||||
- ✅ 智能缓存机制(5分钟缓存)
|
||||
- ✅ 并发请求控制
|
||||
- ✅ 批量数据获取
|
||||
- ✅ 统一数据管理
|
||||
|
||||
### 2. 重构的页面
|
||||
|
||||
#### ✅ 员工管理 (`employees/index.vue`)
|
||||
- 使用 OA Store 获取部门、职位、角色数据
|
||||
- 利用缓存机制,减少重复请求
|
||||
- 数据更新后自动刷新缓存
|
||||
|
||||
#### ✅ 部门管理 (`departments/index.vue`)
|
||||
- 使用 OA Store 管理部门数据
|
||||
- 添加/编辑/删除后自动刷新缓存
|
||||
- 使用响应式数据绑定
|
||||
|
||||
#### ✅ 职位管理 (`positions/index.vue`)
|
||||
- 使用 OA Store 获取部门和职位数据
|
||||
- 利用缓存机制优化性能
|
||||
- 数据更新后自动刷新缓存
|
||||
|
||||
#### ✅ 组织架构 (`organization/index.vue`)
|
||||
- 使用 OA Store 获取部门和职位数据
|
||||
- 部门树和职位列表共享同一份数据
|
||||
- 数据更新后自动刷新缓存
|
||||
|
||||
## 性能提升
|
||||
|
||||
### 优化前
|
||||
- 每次进入页面都发起 3-4 个独立请求
|
||||
- 无缓存机制,频繁切换页面产生大量重复请求
|
||||
- 服务器压力大
|
||||
|
||||
### 优化后
|
||||
- **首次访问**:并行请求所有基础数据(更快)
|
||||
- **缓存有效期内**:只请求业务数据(员工列表等)
|
||||
- **请求次数减少**:75% 减少
|
||||
- **响应速度提升**:缓存命中时,几乎瞬时响应
|
||||
- **服务器压力降低**:减少 75% 的基础数据请求
|
||||
|
||||
## 缓存策略
|
||||
|
||||
### 缓存时间
|
||||
- 默认:5 分钟
|
||||
- 可配置:在 `oa.js` 中修改 `cacheTime` 常量
|
||||
|
||||
### 缓存管理
|
||||
- 自动失效:缓存过期后自动刷新
|
||||
- 手动刷新:调用 `refresh` 方法
|
||||
- 数据更新后:自动刷新相关缓存
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 在页面中使用
|
||||
|
||||
```javascript
|
||||
import { useOAStore } from '@/stores/oa';
|
||||
|
||||
const oaStore = useOAStore();
|
||||
|
||||
// 页面初始化
|
||||
onMounted(async () => {
|
||||
// 使用批量获取,自动利用缓存
|
||||
await oaStore.fetchAllBaseData();
|
||||
// 然后获取业务数据
|
||||
await fetchEmployees();
|
||||
});
|
||||
```
|
||||
|
||||
### 数据更新后刷新缓存
|
||||
|
||||
```javascript
|
||||
// 添加/编辑/删除后
|
||||
await addDepartment(data);
|
||||
await oaStore.refreshDepartments(); // 刷新部门缓存
|
||||
```
|
||||
|
||||
## 优化效果对比
|
||||
|
||||
| 指标 | 优化前 | 优化后 | 提升 |
|
||||
|------|--------|--------|------|
|
||||
| 请求次数(缓存命中) | 4 次 | 1 次 | **减少 75%** |
|
||||
| 响应时间(缓存命中) | ~500ms | ~50ms | **提升 90%** |
|
||||
| 服务器压力 | 高 | 低 | **降低 75%** |
|
||||
| 代码复用性 | 低 | 高 | **提升** |
|
||||
| 数据一致性 | 一般 | 优秀 | **提升** |
|
||||
|
||||
## 后续建议
|
||||
|
||||
1. **其他模块**:可以将类似的优化应用到其他模块(如用户管理、权限管理等)
|
||||
2. **缓存时间**:根据业务需求调整缓存时间
|
||||
3. **后端优化**:考虑提供批量接口,一次返回所有基础数据
|
||||
4. **监控**:添加性能监控,跟踪缓存命中率
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **缓存时间**:默认 5 分钟,可根据业务需求调整
|
||||
2. **数据一致性**:更新数据后记得调用 `refresh` 方法
|
||||
3. **租户隔离**:缓存是基于当前租户的,不同租户数据不会混淆
|
||||
4. **内存占用**:缓存数据存储在内存中,页面刷新后会清空
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增文件
|
||||
- `pc/src/stores/oa.js` - OA 基础数据 Store
|
||||
- `pc/src/stores/README_OA.md` - Store 使用文档
|
||||
- `pc/src/views/apps/oa/OPTIMIZATION_SUMMARY.md` - 优化总结(本文件)
|
||||
|
||||
### 重构文件
|
||||
- `pc/src/views/apps/oa/employees/index.vue` - 员工管理页面
|
||||
- `pc/src/views/apps/oa/departments/index.vue` - 部门管理页面
|
||||
- `pc/src/views/apps/oa/positions/index.vue` - 职位管理页面
|
||||
- `pc/src/views/apps/oa/organization/index.vue` - 组织架构页面
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **功能测试**:确保所有 CRUD 操作正常工作
|
||||
2. **缓存测试**:验证缓存机制是否正常工作
|
||||
3. **性能测试**:对比优化前后的性能指标
|
||||
4. **并发测试**:验证并发请求控制是否正常
|
||||
|
||||
---
|
||||
|
||||
**优化完成!** 🎉
|
||||
|
||||
所有 OA 模块页面已成功使用统一的 Store 进行数据管理,大幅提升了性能和代码质量。
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
style="width: 100%"
|
||||
:loading="loadingRoles"
|
||||
clearable
|
||||
:key="`role-${filteredRoleList.length}-${form.tenant_id}-${form.role}`"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in filteredRoleList"
|
||||
@@ -83,7 +84,7 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div v-if="filteredRoleList.length === 0 && !loadingRoles" style="color: #999; font-size: 12px; margin-top: 4px;">
|
||||
暂无可用角色
|
||||
暂无可用角色(当前租户ID: {{ form.tenant_id }})
|
||||
</div>
|
||||
<div v-if="form.role && !hasValidRole" style="color: #f56c6c; font-size: 12px; margin-top: 4px;">
|
||||
警告:当前选择的角色ID ({{ form.role }}) 在角色列表中不存在
|
||||
@@ -139,32 +140,79 @@ const props = defineProps<{
|
||||
loadingRoles: boolean;
|
||||
}>();
|
||||
|
||||
// 根据当前员工的 tenant_id 过滤角色列表
|
||||
// 根据当前员工的 tenant_id 和角色的 default 字段过滤角色列表
|
||||
const filteredRoleList = computed(() => {
|
||||
if (!props.roleList || props.roleList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取当前员工的 tenant_id
|
||||
const currentTenantId = props.formData?.tenant_id;
|
||||
// 获取当前员工的 tenant_id(优先使用 form.value.tenant_id,因为表单数据可能已更新)
|
||||
const currentTenantId = form.value.tenant_id !== null && form.value.tenant_id !== undefined
|
||||
? form.value.tenant_id
|
||||
: (props.formData?.tenant_id !== null && props.formData?.tenant_id !== undefined
|
||||
? props.formData.tenant_id
|
||||
: null);
|
||||
|
||||
// 调试日志
|
||||
console.log('filteredRoleList - currentTenantId:', currentTenantId);
|
||||
console.log('filteredRoleList - roleList length:', props.roleList.length);
|
||||
|
||||
if (currentTenantId === null || currentTenantId === undefined) {
|
||||
// 如果没有 tenant_id,返回所有角色(不应该发生,但做个兼容)
|
||||
console.log('filteredRoleList - 没有 tenant_id,返回所有角色');
|
||||
return props.roleList;
|
||||
}
|
||||
|
||||
// 过滤角色:显示当前租户的角色(tenant_id 匹配)和公共角色(tenant_id 为 0)
|
||||
return props.roleList.filter((role: any) => {
|
||||
const currentTenantIdNum = Number(currentTenantId);
|
||||
|
||||
// 过滤角色:根据 default 字段和 tenant_id 进行筛选
|
||||
// default = 1: 只匹配给 tenant_id = 1 的租户
|
||||
// default = 2: 分配给所有租户使用
|
||||
// default = 3: 租户单独设置的,给自己用(tenant_id 匹配当前租户)
|
||||
const filtered = props.roleList.filter((role: any) => {
|
||||
// 兼容不同的字段名:tenantId 或 tenant_id
|
||||
const roleTenantId = role.tenantId !== undefined ? role.tenantId : (role.tenant_id !== undefined ? role.tenant_id : null);
|
||||
const roleDefault = role.default !== undefined ? role.default : (role.Default !== undefined ? role.Default : 2); // 默认为2(所有租户可用)
|
||||
|
||||
// 转换为数字进行比较
|
||||
const currentTenantIdNum = Number(currentTenantId);
|
||||
const roleTenantIdNum = roleTenantId !== null && roleTenantId !== undefined ? Number(roleTenantId) : null;
|
||||
const roleDefaultNum = Number(roleDefault);
|
||||
|
||||
// 显示当前租户的角色或公共角色(tenant_id 为 0)
|
||||
return roleTenantIdNum === currentTenantIdNum || roleTenantIdNum === 0;
|
||||
let match = false;
|
||||
|
||||
// 情况1: default = 1,只给租户1使用
|
||||
if (roleDefaultNum === 1) {
|
||||
match = currentTenantIdNum === 1;
|
||||
}
|
||||
// 情况2: default = 2,所有租户可用
|
||||
else if (roleDefaultNum === 2) {
|
||||
match = true;
|
||||
}
|
||||
// 情况3: default = 3,租户专属(tenant_id 必须匹配)
|
||||
else if (roleDefaultNum === 3) {
|
||||
match = roleTenantIdNum === currentTenantIdNum;
|
||||
}
|
||||
// 兼容旧数据:如果没有 default 字段,使用旧的逻辑
|
||||
else {
|
||||
// 如果 tenant_id = 0,认为是公共角色,所有租户可用
|
||||
// 如果 tenant_id 匹配,认为是租户专属角色
|
||||
match = roleTenantIdNum === 0 || roleTenantIdNum === currentTenantIdNum;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
console.log('filteredRoleList - 匹配角色:', {
|
||||
id: role.roleId || role.id || role.role_id,
|
||||
name: role.roleName || role.name || role.role_name,
|
||||
roleTenantId: roleTenantIdNum,
|
||||
roleDefault: roleDefaultNum,
|
||||
currentTenantId: currentTenantIdNum
|
||||
});
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
console.log('filteredRoleList - 过滤后数量:', filtered.length);
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 根据选择的部门过滤职位列表
|
||||
@@ -267,11 +315,19 @@ watch(() => props.visible, async (val) => {
|
||||
department_id: props.formData.department_id ? Number(props.formData.department_id) : null,
|
||||
position_id: props.formData.position_id ? Number(props.formData.position_id) : null,
|
||||
role: props.formData.role ? Number(props.formData.role) : null,
|
||||
tenant_id: props.formData.tenant_id ? Number(props.formData.tenant_id) : null,
|
||||
};
|
||||
console.log('EmployeeEdit - 编辑模式,设置表单数据:', {
|
||||
tenant_id: form.value.tenant_id,
|
||||
role: form.value.role,
|
||||
formData_tenant_id: props.formData.tenant_id
|
||||
});
|
||||
// 再等待一个tick,确保组件能正确渲染
|
||||
await nextTick();
|
||||
} else if (val && !props.isEdit) {
|
||||
// 新增时重置表单
|
||||
// 从 props.formData 获取 tenant_id(如果有的话)
|
||||
const tenantId = props.formData?.tenant_id || null;
|
||||
form.value = {
|
||||
id: null,
|
||||
employeeNo: "",
|
||||
@@ -284,8 +340,11 @@ watch(() => props.visible, async (val) => {
|
||||
bank_name: "",
|
||||
bank_account: "",
|
||||
status: 1,
|
||||
tenant_id: null,
|
||||
tenant_id: tenantId ? Number(tenantId) : null,
|
||||
};
|
||||
console.log('EmployeeEdit - 新增模式,设置表单数据:', {
|
||||
tenant_id: form.value.tenant_id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="340" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
@@ -79,6 +79,7 @@
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
:disabled="scope.row.role === 5"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
|
||||
@@ -164,15 +164,15 @@ const handleCollapseAll = () => {
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
.el-button {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-color-primary);
|
||||
// .el-button {
|
||||
// padding: 0;
|
||||
// font-size: 13px;
|
||||
// color: var(--el-color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
// &:hover {
|
||||
// color: var(--el-color-primary-light-3);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -458,6 +458,12 @@ const submitForm = async (formData: any) => {
|
||||
};
|
||||
|
||||
const handleDelete = async (employee: Employee) => {
|
||||
// 如果角色ID是5,不允许删除
|
||||
if (employee.role === 5) {
|
||||
ElMessage.warning("该员工的角色不允许删除");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm("确认删除该员工?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
|
||||
@@ -273,7 +273,13 @@ const handleTaskChange = (task: any) => {
|
||||
// 初始化图表
|
||||
onMounted(() => {
|
||||
// 折线图
|
||||
new Chart(document.getElementById("lineChart") as HTMLCanvasElement, {
|
||||
const lineChartEl = document.getElementById("lineChart") as HTMLCanvasElement | null;
|
||||
if (!lineChartEl) {
|
||||
console.error("Line chart element not found");
|
||||
return;
|
||||
}
|
||||
|
||||
new Chart(lineChartEl, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: ["1月", "2月", "3月", "4月", "5月", "6月", "7月"],
|
||||
@@ -332,7 +338,13 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
// 柱状图
|
||||
new Chart(document.getElementById("barChart") as HTMLCanvasElement, {
|
||||
const barChartEl = document.getElementById("barChart") as HTMLCanvasElement | null;
|
||||
if (!barChartEl) {
|
||||
console.error("Bar chart element not found");
|
||||
return;
|
||||
}
|
||||
|
||||
new Chart(barChartEl, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
||||
|
||||
Reference in New Issue
Block a user