index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  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>
  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 { genFileId } from "element-plus";
  383. const queryFormRef = ref(ElForm); // 查询表单
  384. const userFormRef = ref(ElForm); // 用户表单
  385. const uploadRef = ref(); // 上传组件
  386. const loading = ref(false); // 加载状态
  387. const removeIds = ref([]); // 删除用户ID集合 用于批量删除
  388. const queryParams = reactive({
  389. pageNo: 1,
  390. pageSize: 10,
  391. });
  392. const dateTimeRange = ref("");
  393. const total = ref(0); // 数据总数
  394. const pageData = ref(); // 用户分页数据
  395. const deptList = ref(); // 部门下拉数据源
  396. const roleList = ref(); // 角色下拉数据源
  397. const postList = ref(); // 岗位下拉数据源
  398. // 弹窗对象
  399. const dialog = reactive({
  400. visible: false,
  401. type: "user-form",
  402. width: 800,
  403. title: "",
  404. });
  405. // 用户表单数据
  406. const formData = reactive({
  407. state: 0,
  408. sex: 0,
  409. email: '',
  410. });
  411. // 用户导入数据
  412. const importData = reactive({
  413. deptId: undefined,
  414. file: undefined,
  415. fileList: [],
  416. });
  417. // 校验规则
  418. const rules = reactive({
  419. userName: [{ required: true, message: "账号不能为空", trigger: "blur" }],
  420. nickName: [{ required: true, message: "姓名不能为空", trigger: "blur" }],
  421. employeeCode: [{ 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. queryParams.deptQuery = queryParams.deptId
  444. getUserPage(queryParams)
  445. .then(({ data }) => {
  446. pageData.value = data.records;
  447. total.value = data.totalCount;
  448. })
  449. .finally(() => {
  450. loading.value = false;
  451. });
  452. }
  453. /** 重置查询 */
  454. function resetQuery() {
  455. queryFormRef.value.resetFields();
  456. dateTimeRange.value = "";
  457. queryParams.pageNo = 1;
  458. //queryParams.deptId = undefined;
  459. queryParams.startTime = undefined;
  460. queryParams.endTime = undefined;
  461. handleQuery();
  462. }
  463. /** 行选中 */
  464. function handleSelectionChange(selection) {
  465. removeIds.value = selection.map((item) => item.id);
  466. }
  467. /** 重置密码 */
  468. function resetPassword(row) {
  469. ElMessageBox.confirm("确认重置用户密码吗?", "警告", {
  470. confirmButtonText: "确定",
  471. cancelButtonText: "取消",
  472. type: "warning",
  473. }).then(function () {
  474. updateUserPassword(row).then(() => {
  475. ElMessage.success("密码重置成功,新密码为系统初始密码");
  476. });
  477. });
  478. }
  479. /** 加载角色下拉数据源 */
  480. async function loadRoleOptions() {
  481. getRoleOptions({}).then((response) => {
  482. roleList.value = response.data;
  483. });
  484. }
  485. /** 加载岗位下拉数据源 */
  486. async function loadPostOptions() {
  487. getPostOptions({}).then((response) => {
  488. postList.value = response.data;
  489. });
  490. }
  491. /** 加载部门下拉数据源 */
  492. async function loadDeptOptions() {
  493. treeList().then((response) => {
  494. deptList.value = response.data;
  495. });
  496. }
  497. /**
  498. * 打开弹窗
  499. *
  500. * @param type 弹窗类型 用户表单:user-form | 用户导入:user-import
  501. * @param id 用户ID
  502. */
  503. async function openDialog(type, row) {
  504. dialog.visible = true;
  505. dialog.type = type;
  506. if (dialog.type === "user-form") {
  507. // 用户表单弹窗
  508. await loadDeptOptions();
  509. await loadPostOptions();
  510. await loadRoleOptions();
  511. if (row) {
  512. dialog.title = "修改用户";
  513. Object.assign(formData, row);
  514. } else {
  515. dialog.title = "新增用户";
  516. }
  517. } else if (dialog.type === "user-import") {
  518. // 用户导入弹窗
  519. dialog.title = "导入用户";
  520. dialog.width = 600;
  521. await loadDeptOptions();
  522. }
  523. }
  524. /**
  525. * 关闭弹窗
  526. *
  527. * @param type 弹窗类型 用户表单:user-form | 用户导入:user-import
  528. */
  529. function closeDialog() {
  530. dialog.visible = false;
  531. if (dialog.type === "user-form") {
  532. userFormRef.value.resetFields();
  533. userFormRef.value.clearValidate();
  534. formData.id = undefined;
  535. formData.status = 1;
  536. } else if (dialog.type === "user-import") {
  537. importData.file = undefined;
  538. importData.fileList = [];
  539. }
  540. }
  541. /** 表单提交 */
  542. const handleSubmit = useThrottleFn(() => {
  543. if (dialog.type === "user-form") {
  544. userFormRef.value.validate((valid) => {
  545. if (valid) {
  546. const userId = formData.id;
  547. loading.value = true;
  548. if (userId) {
  549. updateUser(userId, formData)
  550. .then(() => {
  551. ElMessage.success("修改用户成功");
  552. closeDialog();
  553. resetQuery();
  554. })
  555. .finally(() => (loading.value = false));
  556. } else {
  557. addUser(formData)
  558. .then(() => {
  559. ElMessage.success("新增用户成功");
  560. closeDialog();
  561. resetQuery();
  562. })
  563. .finally(() => (loading.value = false));
  564. }
  565. }
  566. });
  567. } else if (dialog.type === "user-import") {
  568. if (!importData?.deptId) {
  569. ElMessage.warning("请选择部门");
  570. return false;
  571. }
  572. if (!importData?.file) {
  573. ElMessage.warning("上传Excel文件不能为空");
  574. return false;
  575. }
  576. importUser(importData?.deptId, importData?.file).then((response) => {
  577. ElMessage.success(response.msg);
  578. closeDialog();
  579. resetQuery();
  580. });
  581. }
  582. }, 3000);
  583. /** 删除用户 */
  584. function handleDelete(id) {
  585. ElMessageBox.confirm("确认删除用户?", "警告", {
  586. confirmButtonText: "确定",
  587. cancelButtonText: "取消",
  588. type: "warning",
  589. }).then(function () {
  590. deleteUsers(id).then(() => {
  591. ElMessage.success("删除成功");
  592. resetQuery();
  593. });
  594. });
  595. }
  596. /** 下载导入模板 */
  597. function downloadTemplate() {
  598. downloadTemplateApi().then((response) => {
  599. const fileData = response.data;
  600. const fileName = decodeURI(
  601. response.headers["content-disposition"].split(";")[1].split("=")[1]
  602. );
  603. const fileType =
  604. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
  605. const blob = new Blob([fileData], { type: fileType });
  606. const downloadUrl = window.URL.createObjectURL(blob);
  607. const downloadLink = document.createElement("a");
  608. downloadLink.href = downloadUrl;
  609. downloadLink.download = fileName;
  610. document.body.appendChild(downloadLink);
  611. downloadLink.click();
  612. document.body.removeChild(downloadLink);
  613. window.URL.revokeObjectURL(downloadUrl);
  614. });
  615. }
  616. /** Excel文件 Change */
  617. function handleFileChange(file) {
  618. importData.file = file.raw;
  619. }
  620. /** Excel文件 Exceed */
  621. function handleFileExceed(files) {
  622. uploadRef.value.clearFiles();
  623. const file = files[0];
  624. file.uid = genFileId();
  625. uploadRef.value.handleStart(file);
  626. importData.file = file;
  627. }
  628. /** 导出用户 */
  629. function handleExport() {
  630. exportUser(queryParams).then((response) => {
  631. const fileData = response.data;
  632. const fileName = decodeURI(
  633. response.headers["content-disposition"].split(";")[1].split("=")[1]
  634. );
  635. const fileType =
  636. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
  637. const blob = new Blob([fileData], { type: fileType });
  638. const downloadUrl = window.URL.createObjectURL(blob);
  639. const downloadLink = document.createElement("a");
  640. downloadLink.href = downloadUrl;
  641. downloadLink.download = fileName;
  642. document.body.appendChild(downloadLink);
  643. downloadLink.click();
  644. document.body.removeChild(downloadLink);
  645. window.URL.revokeObjectURL(downloadUrl);
  646. });
  647. }
  648. onMounted?.(() => {
  649. handleQuery();
  650. });
  651. </script>