index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <template>
  2. <div class="page-container staff-manage">
  3. <PageTitle title="人员管理" />
  4. <div class="main-content">
  5. <!-- 左侧部门树 -->
  6. <div class="dept-tree">
  7. <el-tree
  8. :data="deptTreeData"
  9. :props="{ label: 'deptName', children: 'children' }"
  10. node-key="deptId"
  11. default-expand-all
  12. highlight-current
  13. @node-click="handleDeptClick"
  14. >
  15. <template #default="{ node, data }">
  16. <span :class="['tree-node', { active: currentDeptId === data.deptId }]">{{ node.label }}</span>
  17. </template>
  18. </el-tree>
  19. </div>
  20. <!-- 右侧人员列表 -->
  21. <div class="staff-list">
  22. <!-- 操作栏 -->
  23. <div class="action-bar">
  24. <el-input v-model="queryParams.contactName" placeholder="搜索员工姓名" style="width: 200px" clearable @keyup.enter="handleQuery">
  25. <template #prefix
  26. ><el-icon><Search /></el-icon
  27. ></template>
  28. </el-input>
  29. <div class="action-buttons">
  30. <el-button type="danger" @click="handleAdd">+ 新增</el-button>
  31. <el-button :disabled="selectedRows.length === 0" @click="handleBatchDelete">批量删除</el-button>
  32. </div>
  33. </div>
  34. <!-- 表格 -->
  35. <el-table :data="staffList" border @selection-change="handleSelectionChange">
  36. <el-table-column type="selection" width="50" align="center" />
  37. <el-table-column prop="contactName" label="姓名" min-width="100" align="center" />
  38. <el-table-column prop="phone" label="手机号" min-width="140" align="center" />
  39. <el-table-column prop="roleName" label="角色" min-width="120" align="center" />
  40. <el-table-column prop="deptName" label="部门名称" min-width="120" align="center" />
  41. <el-table-column prop="status" label="状态" min-width="80" align="center">
  42. <template #default="{ row }">
  43. <span :class="['status-text', row.status === '启用' ? 'active' : '']">{{ row.status == '0' ? '启用' : '禁用' }}</span>
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="操作" width="100" align="center">
  47. <template #default="{ row }">
  48. <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
  49. <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
  50. </template>
  51. </el-table-column>
  52. </el-table>
  53. <!-- 分页 -->
  54. <TablePagination v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
  55. </div>
  56. </div>
  57. <!-- 新增/编辑弹窗 -->
  58. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
  59. <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
  60. <el-form-item label="姓名" prop="contactName">
  61. <el-input v-model="formData.contactName" placeholder="请输入姓名" />
  62. </el-form-item>
  63. <el-form-item label="手机号" prop="phone">
  64. <el-input v-model="formData.phone" placeholder="请输入手机号" />
  65. </el-form-item>
  66. <el-form-item label="所属部门" prop="deptId">
  67. <el-tree-select
  68. v-model="formData.deptId"
  69. :data="deptTreeData"
  70. :props="{ label: 'deptName', children: 'children' }"
  71. node-key="deptId"
  72. placeholder="请选择部门"
  73. style="width: 100%"
  74. check-strictly
  75. :render-after-expand="false"
  76. @change="handleDeptChange"
  77. />
  78. </el-form-item>
  79. <el-form-item label="角色" prop="roleId">
  80. <el-select v-model="formData.roleId" placeholder="请选择角色" style="width: 100%" @change="handRoleChange">
  81. <el-option v-for="role in roleList" :key="role.roleId" :label="role.roleName" :value="role.roleId.toString()" />
  82. </el-select>
  83. </el-form-item>
  84. <el-form-item label="状态">
  85. <el-radio-group v-model="formData.status">
  86. <el-radio label="0">启用</el-radio>
  87. <el-radio label="1">禁用</el-radio>
  88. </el-radio-group>
  89. </el-form-item>
  90. </el-form>
  91. <template #footer>
  92. <el-button @click="dialogVisible = false">取消</el-button>
  93. <el-button type="danger" @click="handleSubmit">确定</el-button>
  94. </template>
  95. </el-dialog>
  96. </div>
  97. </template>
  98. <script setup lang="ts">
  99. import { ref, reactive, computed, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
  100. import { ElMessage, ElMessageBox } from 'element-plus';
  101. import { Search } from '@element-plus/icons-vue';
  102. import { PageTitle, TablePagination } from '@/components';
  103. import { DeptInfo } from '@/api/pc/organization/types';
  104. import { getDeptTree, getRoleList, getContactList, addContact, updateContact, deleteContact } from '@/api/pc/organization';
  105. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  106. const currentDeptId = ref<number | null>(null);
  107. const dialogVisible = ref(false);
  108. const formRef = ref();
  109. const editingRow = ref<any>(null);
  110. const selectedRows = ref<any[]>([]);
  111. const queryParams = reactive({ pageNum: 1, pageSize: 10, deptId: null as number | null, contactName: '' });
  112. const total = ref(0);
  113. const deptTreeData = ref<DeptInfo[]>([]);
  114. const formData = reactive({
  115. id: null,
  116. contactName: '',
  117. phone: '',
  118. deptId: null as number | null,
  119. deptName: '',
  120. roleId: '',
  121. roleName: '',
  122. status: '0'
  123. });
  124. const formRules = {
  125. contactName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  126. phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
  127. deptId: [{ required: true, message: '请选择部门', trigger: 'change' }]
  128. };
  129. const dialogTitle = computed(() => (editingRow.value ? '编辑人员' : '新增人员'));
  130. // 人员列表数据
  131. const staffList = ref([]);
  132. const roleList = ref<any[]>([]);
  133. // 加载部门树
  134. const loadDeptTree = async () => {
  135. try {
  136. const res = await getDeptTree();
  137. if (res.code === 200 && res.data) {
  138. deptTreeData.value = res.data;
  139. if (Array.isArray(res.data)) {
  140. const treeData = proxy?.handleTree<DeptInfo>(res.data, 'deptId', 'parentId');
  141. deptTreeData.value = treeData || res.data;
  142. } else {
  143. deptTreeData.value = [];
  144. }
  145. }
  146. } catch (error) {
  147. console.error('获取部门树失败:', error);
  148. ElMessage.error('获取部门树失败');
  149. }
  150. };
  151. // 加载角色列表
  152. const loadRoleList = async () => {
  153. try {
  154. roleList.value = [
  155. { roleId: 1, roleName: '管理角色' },
  156. { roleId: 2, roleName: '采购角色' },
  157. { roleId: 3, roleName: '查询角色' },
  158. { roleId: 4, roleName: '审核角色' }
  159. ];
  160. // const res = await getRoleList(queryParams);
  161. // if (res.code === 200 && res.data) {
  162. // roleList.value = res.data;
  163. // }
  164. } catch (error) {
  165. console.error('获取角色列表失败:', error);
  166. }
  167. };
  168. const handleDeptChange = (value: any) => {
  169. console.log(value);
  170. // 定义递归查找函数
  171. const findNode = (nodes: any[], id: any): any => {
  172. for (const node of nodes) {
  173. // 类型转换后比较
  174. if (String(node.deptId) === String(id)) {
  175. return node;
  176. }
  177. // 如果有子节点,递归查找
  178. if (node.children && node.children.length > 0) {
  179. const found = findNode(node.children, id);
  180. if (found) return found;
  181. }
  182. }
  183. return null;
  184. };
  185. const selectedDept = findNode(deptTreeData.value, value);
  186. if (selectedDept) {
  187. formData.deptName = selectedDept.deptName;
  188. formData.deptId = selectedDept.deptId;
  189. }
  190. };
  191. const handRoleChange = (value: number) => {
  192. const selectedRole = roleList.value.find((item) => item.roleId === value);
  193. if (selectedRole) {
  194. formData.roleName = selectedRole.roleName;
  195. }
  196. };
  197. // 加载员工列表
  198. const loadContactList = async () => {
  199. try {
  200. const params: any = {
  201. pageNum: queryParams.pageNum,
  202. pageSize: queryParams.pageSize
  203. };
  204. if (queryParams.deptId) params.deptId = queryParams.deptId;
  205. if (queryParams.contactName) params.contactName = queryParams.contactName;
  206. const res = await getContactList(params);
  207. if (res.code === 200) {
  208. staffList.value = res.rows;
  209. total.value = res.total || 0;
  210. }
  211. } catch (error) {
  212. console.error('获取员工列表失败:', error);
  213. ElMessage.error('获取员工列表失败');
  214. }
  215. };
  216. const handleDeptClick = (data: any) => {
  217. currentDeptId.value = data.deptId;
  218. queryParams.deptId = data.deptId;
  219. queryParams.pageNum = 1;
  220. loadContactList();
  221. };
  222. const handleQuery = () => {
  223. queryParams.pageNum = 1;
  224. loadContactList();
  225. };
  226. const handleSelectionChange = (rows: any[]) => {
  227. selectedRows.value = rows;
  228. };
  229. const handleAdd = () => {
  230. editingRow.value = null;
  231. formData.contactName = '';
  232. formData.phone = '';
  233. formData.deptId = currentDeptId.value;
  234. formData.roleId = '';
  235. formData.status = '0';
  236. dialogVisible.value = true;
  237. };
  238. const handleEdit = (row: any) => {
  239. editingRow.value = row;
  240. formData.id = row.id;
  241. formData.contactName = row.contactName;
  242. formData.phone = row.phone;
  243. formData.deptId = row.deptId;
  244. formData.roleId = row.roleId;
  245. formData.status = row.status;
  246. dialogVisible.value = true;
  247. };
  248. const handleBatchDelete = () => {
  249. ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 名人员吗?`, '提示', {
  250. confirmButtonText: '确定',
  251. cancelButtonText: '取消',
  252. type: 'warning'
  253. }).then(() => {
  254. deleteContact(selectedRows.value.map((item) => item.id));
  255. ElMessage.success('删除成功');
  256. });
  257. };
  258. const handleDelete = (row: any) => {
  259. ElMessageBox.confirm(`确定要删除"${row.contactName}"吗?`, '提示', {
  260. confirmButtonText: '确定',
  261. cancelButtonText: '取消',
  262. type: 'warning'
  263. }).then(() => {
  264. const index = staffList.value.findIndex((item) => item.id === row.id);
  265. if (index > -1) {
  266. staffList.value.splice(index, 1);
  267. }
  268. ElMessage.success('删除成功');
  269. });
  270. };
  271. const handleSubmit = async () => {
  272. const valid = await formRef.value?.validate();
  273. if (!valid) return;
  274. if (editingRow.value) {
  275. await updateContact(formData);
  276. ElMessage.success('编辑成功');
  277. } else {
  278. await addContact(formData);
  279. ElMessage.success('新增成功');
  280. }
  281. loadContactList();
  282. dialogVisible.value = false;
  283. };
  284. // 页面加载时获取部门树和员工列表
  285. onMounted(() => {
  286. loadDeptTree();
  287. loadRoleList();
  288. loadContactList();
  289. });
  290. </script>
  291. <style scoped lang="scss">
  292. .staff-manage {
  293. .main-content {
  294. display: flex;
  295. gap: 20px;
  296. min-height: 500px;
  297. }
  298. .dept-tree {
  299. width: 200px;
  300. flex-shrink: 0;
  301. border-right: 1px solid #eee;
  302. padding-right: 15px;
  303. .tree-node {
  304. font-size: 14px;
  305. color: #333;
  306. &.active {
  307. color: #e60012;
  308. }
  309. }
  310. :deep(.el-tree-node__content) {
  311. height: 36px;
  312. }
  313. :deep(.el-tree-node.is-current > .el-tree-node__content) {
  314. background-color: #fff5f5;
  315. .tree-node {
  316. color: #e60012;
  317. }
  318. }
  319. }
  320. .staff-list {
  321. flex: 1;
  322. min-width: 0;
  323. .action-bar {
  324. margin-bottom: 15px;
  325. display: flex;
  326. justify-content: space-between;
  327. align-items: center;
  328. gap: 10px;
  329. .action-buttons {
  330. display: flex;
  331. gap: 10px;
  332. }
  333. }
  334. .status-text {
  335. color: #999;
  336. &.active {
  337. color: #67c23a;
  338. }
  339. }
  340. }
  341. }
  342. :deep(.el-table) {
  343. th.el-table__cell {
  344. background: #fafafa;
  345. font-weight: 500;
  346. color: #333;
  347. }
  348. }
  349. </style>