index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <!-- 用户管理 -->
  2. <template>
  3. <div class="app-container">
  4. <el-row :gutter="20">
  5. <!-- 部门树 -->
  6. <el-col :lg="4" :xs="24" class="mb-[12px]">
  7. <dept-tree v-model="queryParams.deptId" @node-click="handleQuery" />
  8. </el-col>
  9. <!-- 用户列表 -->
  10. <el-col :lg="20" :xs="24">
  11. <div class="search-container">
  12. <el-form ref="queryFormRef" :model="queryParams" :inline="true">
  13. <el-form-item label="关键字" prop="keywords">
  14. <el-input
  15. v-model="queryParams.keywords"
  16. placeholder="用户名/昵称/手机号"
  17. clearable
  18. style="width: 200px"
  19. @keyup.enter="handleQuery"
  20. />
  21. </el-form-item>
  22. <el-form-item label="状态" prop="state">
  23. <el-select
  24. v-model="queryParams.state"
  25. placeholder="全部"
  26. clearable
  27. class="!w-[100px]"
  28. >
  29. <el-option label="启用" value=0 />
  30. <el-option label="禁用" value=1 />
  31. </el-select>
  32. </el-form-item>
  33. <el-form-item>
  34. <el-button type="primary" @click="handleQuery"
  35. ><i-ep-search />搜索</el-button
  36. >
  37. <el-button @click="resetQuery">
  38. <i-ep-refresh />
  39. 重置</el-button
  40. >
  41. </el-form-item>
  42. </el-form>
  43. </div>
  44. <el-card shadow="never" class="table-container">
  45. <template #header>
  46. <div class="flex justify-between">
  47. <div>
  48. <el-button
  49. v-hasPerm="['sys:user:add']"
  50. type="primary"
  51. @click="openDialog('user-form')"
  52. ><i-ep-plus />新增</el-button
  53. >
  54. <el-button
  55. v-hasPerm="['sys:user:delete']"
  56. type="danger"
  57. :disabled="removeIds.length === 0"
  58. @click="handleDelete()"
  59. ><i-ep-delete />删除</el-button
  60. >
  61. </div>
  62. <div>
  63. <el-dropdown split-button>
  64. 导入
  65. <template #dropdown>
  66. <el-dropdown-menu>
  67. <el-dropdown-item @click="downloadTemplate">
  68. <i-ep-download />下载模板</el-dropdown-item
  69. >
  70. <el-dropdown-item @click="openDialog('user-import')">
  71. <i-ep-top />导入数据</el-dropdown-item
  72. >
  73. </el-dropdown-menu>
  74. </template>
  75. </el-dropdown>
  76. <el-button class="ml-3" @click="handleExport"
  77. ><template #icon><i-ep-download /></template>导出</el-button
  78. >
  79. </div>
  80. </div>
  81. </template>
  82. <el-table
  83. v-loading="loading"
  84. :data="pageData"
  85. @selection-change="handleSelectionChange"
  86. >
  87. <el-table-column type="selection" width="50" align="center" />
  88. <el-table-column
  89. label="用户名"
  90. width="120"
  91. align="center"
  92. prop="userName"
  93. />
  94. <el-table-column
  95. label="姓名"
  96. width="120"
  97. align="center"
  98. prop="nickName"
  99. />
  100. <el-table-column
  101. key="employeeCode"
  102. label="员工编号"
  103. align="center"
  104. prop="employeeCode"
  105. />
  106. <el-table-column
  107. label="性别"
  108. width="100"
  109. align="center"
  110. prop="sex"
  111. >
  112. <template #default="scope">
  113. <el-tag :type="scope.row.sex == 0 ? 'info' : 'success'">{{
  114. scope.row.sex == 0 ? "未知" : scope.row.sex == 1 ? "男" : "女"
  115. }}</el-tag>
  116. </template>
  117. </el-table-column>
  118. <el-table-column
  119. label="部门"
  120. width="120"
  121. align="center"
  122. overHidden="true"
  123. prop="deptNames"
  124. />
  125. <el-table-column
  126. label="手机号码"
  127. align="center"
  128. prop="phone"
  129. width="120"
  130. />
  131. <el-table-column label="状态" align="center" prop="state">
  132. <template #default="scope">
  133. <el-tag :type="scope.row.state == 0 ? 'success' : 'info'">{{
  134. scope.row.state == 0 ? "启用" : "禁用"
  135. }}</el-tag>
  136. </template>
  137. </el-table-column>
  138. <el-table-column
  139. label="创建时间"
  140. align="center"
  141. prop="created"
  142. width="180"
  143. />
  144. <el-table-column label="操作" fixed="right" width="220">
  145. <template #default="scope">
  146. <el-button
  147. v-hasPerm="['sys:user:reset_pwd']"
  148. type="primary"
  149. size="small"
  150. link
  151. v-if="scope.row.id !== '1'"
  152. @click="resetPassword(scope.row)"
  153. ><i-ep-refresh-left />重置密码</el-button
  154. >
  155. <el-button
  156. v-hasPerm="['sys:user:edit']"
  157. type="primary"
  158. link
  159. size="small"
  160. v-if="scope.row.id !== '1'"
  161. @click="openDialog('user-form', scope.row)"
  162. ><i-ep-edit />编辑</el-button
  163. >
  164. <el-button
  165. v-hasPerm="['sys:user:del']"
  166. type="primary"
  167. link
  168. size="small"
  169. v-if="scope.row.id !== '1'"
  170. @click="handleDelete(scope.row.id)"
  171. ><i-ep-delete />删除</el-button
  172. >
  173. </template>
  174. </el-table-column>
  175. </el-table>
  176. <pagination
  177. v-if="total > 0"
  178. v-model:total="total"
  179. v-model:page="queryParams.pageNo"
  180. v-model:limit="queryParams.pageSize"
  181. @pagination="handleQuery"
  182. />
  183. </el-card>
  184. </el-col>
  185. </el-row>
  186. <!-- 弹窗 -->
  187. <el-dialog
  188. v-model="dialog.visible"
  189. :title="dialog.title"
  190. :width="dialog.width"
  191. append-to-body
  192. @close="closeDialog"
  193. >
  194. <!-- 用户新增/编辑表单 -->
  195. <el-form
  196. v-if="dialog.type === 'user-form'"
  197. ref="userFormRef"
  198. :model="formData"
  199. :rules="rules"
  200. label-width="90px"
  201. >
  202. <el-row :gutter="22">
  203. <el-col :span="11">
  204. <el-form-item label="用户名" prop="userName">
  205. <el-input
  206. v-model="formData.userName"
  207. :disabled="!!formData.id"
  208. placeholder="请输入用户名"
  209. />
  210. </el-form-item>
  211. </el-col>
  212. <el-col :span="11">
  213. <el-form-item label="用户昵称" prop="nickName">
  214. <el-input v-model="formData.nickName" placeholder="请输入用户昵称" />
  215. </el-form-item>
  216. </el-col>
  217. </el-row>
  218. <el-row :gutter="22">
  219. <el-col :span="11">
  220. <el-form-item label="员工编号" prop="employeeCode">
  221. <el-input
  222. v-model="formData.employeeCode"
  223. placeholder="请输入员工编号"
  224. />
  225. </el-form-item>
  226. </el-col>
  227. <el-col :span="11">
  228. <el-form-item label="性别" prop="sex">
  229. <el-radio-group v-model="formData.sex">
  230. <el-radio :value=0>未知</el-radio>
  231. <el-radio :value=1>男</el-radio>
  232. <el-radio :value=2>女</el-radio>
  233. </el-radio-group>
  234. </el-form-item>
  235. </el-col>
  236. </el-row>
  237. <el-row :gutter="22">
  238. <el-col :span="22">
  239. <el-form-item label="所属部门" prop="deptIds">
  240. <el-tree-select
  241. v-model="formData.deptIds"
  242. placeholder="请选择所属部门"
  243. :data="deptList"
  244. :multiple="true"
  245. filterable
  246. show-checkbox
  247. load-key="deptName"
  248. value-key="id"
  249. :props="{ children: 'children', label: 'deptName',value: 'id', disabled: '' }"
  250. check-strictly
  251. :render-after-expand="false"
  252. />
  253. </el-form-item>
  254. </el-col>
  255. </el-row>
  256. <el-row :gutter="22">
  257. <el-col :span="22">
  258. <el-form-item label="角色" prop="roleIds">
  259. <el-select v-model="formData.roleIds" multiple placeholder="请选择">
  260. <el-option
  261. v-for="item in roleList"
  262. :key="item.id"
  263. :label="item.roleName"
  264. :value="item.id"
  265. />
  266. </el-select>
  267. </el-form-item>
  268. </el-col>
  269. </el-row>
  270. <el-row :gutter="22">
  271. <el-col :span="22">
  272. <el-form-item label="岗位" prop="postIds">
  273. <el-select v-model="formData.postIds" multiple placeholder="请选择">
  274. <el-option
  275. v-for="item in postList"
  276. :key="item.id"
  277. :label="item.postName"
  278. :value="item.id"
  279. />
  280. </el-select>
  281. </el-form-item>
  282. </el-col>
  283. </el-row>
  284. <el-row :gutter="22">
  285. <el-col :span="11">
  286. <el-form-item label="手机号码" prop="phone">
  287. <el-input
  288. v-model="formData.phone"
  289. placeholder="请输入手机号码"
  290. maxlength="11"
  291. />
  292. </el-form-item>
  293. </el-col>
  294. <el-col :span="11">
  295. <el-form-item label="邮箱" prop="email">
  296. <el-input
  297. v-model="formData.email"
  298. placeholder="请输入邮箱"
  299. maxlength="50"
  300. />
  301. </el-form-item>
  302. </el-col>
  303. </el-row>
  304. <el-form-item label="状态" prop="state">
  305. <el-radio-group v-model="formData.state">
  306. <el-radio :value=0>正常</el-radio>
  307. <el-radio :value=1>禁用</el-radio>
  308. </el-radio-group>
  309. </el-form-item>
  310. </el-form>
  311. <!-- 用户导入表单 -->
  312. <el-form
  313. v-else-if="dialog.type === 'user-import'"
  314. :model="importData"
  315. label-width="100px"
  316. >
  317. <el-form-item label="部门">
  318. <el-tree-select
  319. v-model="importData.deptId"
  320. placeholder="请选择部门"
  321. :data="deptList"
  322. load-key="deptName"
  323. value-key="id"
  324. :props="{ children: 'children', label: 'deptName',value: 'id', disabled: '' }"
  325. filterable
  326. check-strictly
  327. />
  328. </el-form-item>
  329. <el-form-item label="Excel文件">
  330. <el-upload
  331. ref="uploadRef"
  332. action=""
  333. drag
  334. accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
  335. :limit="1"
  336. :auto-upload="false"
  337. :file-list="importData.fileList"
  338. :on-change="handleFileChange"
  339. :on-exceed="handleFileExceed"
  340. >
  341. <el-icon class="el-icon--upload">
  342. <i-ep-upload-filled />
  343. </el-icon>
  344. <div class="el-upload__text">
  345. 将文件拖到此处,或
  346. <em>点击上传</em>
  347. </div>
  348. <template #tip>
  349. <div>xls/xlsx files</div>
  350. </template>
  351. </el-upload>
  352. </el-form-item>
  353. </el-form>
  354. <!-- 弹窗底部操作按钮 -->
  355. <template #footer>
  356. <div class="dialog-footer">
  357. <el-button type="primary" @click="handleSubmit">确 定</el-button>
  358. <el-button @click="closeDialog">取 消</el-button>
  359. </div>
  360. </template>
  361. </el-dialog>
  362. </div>
  363. </template>
  364. <script setup lang="ts">
  365. defineOptions({
  366. name: "User",
  367. inheritAttrs: false,
  368. });
  369. import {
  370. getUserPage,
  371. deleteUsers,
  372. addUser,
  373. updateUser,
  374. updateUserPassword,
  375. downloadTemplateApi,
  376. exportUser,
  377. getPostOptions,
  378. importUser,
  379. } from "@/api/system/user";
  380. import { treeList } from "@/api/system/dept";
  381. import { getRoleOptions } from "@/api/system/role";
  382. import {UserQuery, UserPageVO } from "@/api/system/user/types";
  383. import type { UploadInstance } from "element-plus";
  384. import { genFileId } from "element-plus";
  385. const queryFormRef = ref(ElForm); // 查询表单
  386. const userFormRef = ref(ElForm); // 用户表单
  387. const uploadRef = ref<UploadInstance>(); // 上传组件
  388. const loading = ref(false); // 加载状态
  389. const removeIds = ref([]); // 删除用户ID集合 用于批量删除
  390. const queryParams = reactive<UserQuery>({
  391. pageNo: 1,
  392. pageSize: 10,
  393. });
  394. const dateTimeRange = ref("");
  395. const total = ref(0); // 数据总数
  396. const pageData = ref<UserPageVO[]>(); // 用户分页数据
  397. const deptList = ref<OptionType[]>(); // 部门下拉数据源
  398. const roleList = ref<OptionType[]>(); // 角色下拉数据源
  399. const postList = ref<OptionType[]>(); // 岗位下拉数据源
  400. // 弹窗对象
  401. const dialog = reactive({
  402. visible: false,
  403. type: "user-form",
  404. width: 800,
  405. title: "",
  406. });
  407. // 用户表单数据
  408. const formData = reactive({
  409. state: 0,
  410. sex: 0
  411. });
  412. // 用户导入数据
  413. const importData = reactive({
  414. deptId: undefined,
  415. file: undefined,
  416. fileList: [],
  417. });
  418. // 校验规则
  419. const rules = reactive({
  420. userName: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
  421. nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
  422. deptIds: [{ required: true, message: "所属部门不能为空", trigger: "blur" }],
  423. roleIds: [{ required: true, message: "用户角色不能为空", trigger: "blur" }],
  424. postIds: [{ required: true, message: "用户岗位不能为空", trigger: "blur" }],
  425. email: [
  426. {
  427. pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
  428. message: "请输入正确的邮箱地址",
  429. trigger: "blur",
  430. },
  431. ],
  432. phone: [
  433. {
  434. pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
  435. message: "请输入正确的手机号码",
  436. trigger: "blur",
  437. },
  438. ],
  439. });
  440. /** 查询 */
  441. function handleQuery() {
  442. loading.value = true;
  443. getUserPage(queryParams)
  444. .then(({ data }) => {
  445. pageData.value = data.records;
  446. total.value = data.totalCount;
  447. })
  448. .finally(() => {
  449. loading.value = false;
  450. });
  451. }
  452. /** 重置查询 */
  453. function resetQuery() {
  454. queryFormRef.value.resetFields();
  455. dateTimeRange.value = "";
  456. queryParams.pageNo = 1;
  457. queryParams.deptId = undefined;
  458. queryParams.startTime = undefined;
  459. queryParams.endTime = undefined;
  460. handleQuery();
  461. }
  462. /** 行选中 */
  463. function handleSelectionChange(selection: any) {
  464. removeIds.value = selection.map((item: any) => item.id);
  465. }
  466. /** 重置密码 */
  467. function resetPassword(row: { [key: string]: any }) {
  468. ElMessageBox.confirm("确认重置用户密码吗?", "警告", {
  469. confirmButtonText: "确定",
  470. cancelButtonText: "取消",
  471. type: "warning",
  472. }).then(function () {
  473. updateUserPassword(row).then(() => {
  474. ElMessage.success("密码重置成功,新密码为系统初始密码");
  475. });
  476. });
  477. }
  478. /** 加载角色下拉数据源 */
  479. async function loadRoleOptions() {
  480. getRoleOptions({}).then((response) => {
  481. roleList.value = response.data;
  482. });
  483. }
  484. /** 加载岗位下拉数据源 */
  485. async function loadPostOptions() {
  486. getPostOptions({}).then((response) => {
  487. postList.value = response.data;
  488. });
  489. }
  490. /** 加载部门下拉数据源 */
  491. async function loadDeptOptions() {
  492. treeList().then((response) => {
  493. deptList.value = response.data;
  494. });
  495. }
  496. /**
  497. * 打开弹窗
  498. *
  499. * @param type 弹窗类型 用户表单:user-form | 用户导入:user-import
  500. * @param id 用户ID
  501. */
  502. async function openDialog(type: string, row?: number) {
  503. dialog.visible = true;
  504. dialog.type = type;
  505. if (dialog.type === "user-form") {
  506. // 用户表单弹窗
  507. await loadDeptOptions();
  508. await loadPostOptions();
  509. await loadRoleOptions();
  510. if (row) {
  511. dialog.title = "修改用户";
  512. Object.assign(formData, row);
  513. } else {
  514. dialog.title = "新增用户";
  515. }
  516. } else if (dialog.type === "user-import") {
  517. // 用户导入弹窗
  518. dialog.title = "导入用户";
  519. dialog.width = 600;
  520. loadDeptOptions();
  521. }
  522. }
  523. /**
  524. * 关闭弹窗
  525. *
  526. * @param type 弹窗类型 用户表单:user-form | 用户导入:user-import
  527. */
  528. function closeDialog() {
  529. dialog.visible = false;
  530. if (dialog.type === "user-form") {
  531. userFormRef.value.resetFields();
  532. userFormRef.value.clearValidate();
  533. formData.id = undefined;
  534. formData.status = 1;
  535. } else if (dialog.type === "user-import") {
  536. importData.file = undefined;
  537. importData.fileList = [];
  538. }
  539. }
  540. /** 表单提交 */
  541. const handleSubmit = useThrottleFn(() => {
  542. if (dialog.type === "user-form") {
  543. userFormRef.value.validate((valid: any) => {
  544. if (valid) {
  545. const userId = formData.id;
  546. loading.value = true;
  547. if (userId) {
  548. updateUser(userId, formData)
  549. .then(() => {
  550. ElMessage.success("修改用户成功");
  551. closeDialog();
  552. resetQuery();
  553. })
  554. .finally(() => (loading.value = false));
  555. } else {
  556. addUser(formData)
  557. .then(() => {
  558. ElMessage.success("新增用户成功");
  559. closeDialog();
  560. resetQuery();
  561. })
  562. .finally(() => (loading.value = false));
  563. }
  564. }
  565. });
  566. } else if (dialog.type === "user-import") {
  567. if (!importData?.deptId) {
  568. ElMessage.warning("请选择部门");
  569. return false;
  570. }
  571. if (!importData?.file) {
  572. ElMessage.warning("上传Excel文件不能为空");
  573. return false;
  574. }
  575. importUser(importData?.deptId, importData?.file).then((response) => {
  576. ElMessage.success(response.data);
  577. closeDialog();
  578. resetQuery();
  579. });
  580. }
  581. }, 3000);
  582. /** 删除用户 */
  583. function handleDelete(id?: number) {
  584. ElMessageBox.confirm("确认删除用户?", "警告", {
  585. confirmButtonText: "确定",
  586. cancelButtonText: "取消",
  587. type: "warning",
  588. }).then(function () {
  589. deleteUsers(id).then(() => {
  590. ElMessage.success("删除成功");
  591. resetQuery();
  592. });
  593. });
  594. }
  595. /** 下载导入模板 */
  596. function downloadTemplate() {
  597. downloadTemplateApi().then((response: any) => {
  598. const fileData = response.data;
  599. const fileName = decodeURI(
  600. response.headers["content-disposition"].split(";")[1].split("=")[1]
  601. );
  602. const fileType =
  603. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
  604. const blob = new Blob([fileData], { type: fileType });
  605. const downloadUrl = window.URL.createObjectURL(blob);
  606. const downloadLink = document.createElement("a");
  607. downloadLink.href = downloadUrl;
  608. downloadLink.download = fileName;
  609. document.body.appendChild(downloadLink);
  610. downloadLink.click();
  611. document.body.removeChild(downloadLink);
  612. window.URL.revokeObjectURL(downloadUrl);
  613. });
  614. }
  615. /** Excel文件 Change */
  616. function handleFileChange(file: any) {
  617. importData.file = file.raw;
  618. }
  619. /** Excel文件 Exceed */
  620. function handleFileExceed(files: any) {
  621. uploadRef.value!.clearFiles();
  622. const file = files[0];
  623. file.uid = genFileId();
  624. uploadRef.value!.handleStart(file);
  625. importData.file = file;
  626. }
  627. /** 导出用户 */
  628. function handleExport() {
  629. exportUser(queryParams).then((response: any) => {
  630. const fileData = response.data;
  631. const fileName = decodeURI(
  632. response.headers["content-disposition"].split(";")[1].split("=")[1]
  633. );
  634. const fileType =
  635. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
  636. const blob = new Blob([fileData], { type: fileType });
  637. const downloadUrl = window.URL.createObjectURL(blob);
  638. const downloadLink = document.createElement("a");
  639. downloadLink.href = downloadUrl;
  640. downloadLink.download = fileName;
  641. document.body.appendChild(downloadLink);
  642. downloadLink.click();
  643. document.body.removeChild(downloadLink);
  644. window.URL.revokeObjectURL(downloadUrl);
  645. });
  646. }
  647. onMounted?.(() => {
  648. handleQuery();
  649. });
  650. </script>